refactor: break circular dependency with DamageDealer and Healer interfaces

- Create src/magical-objects/magical-object-types.ts with DamageDealer
  and Healer interfaces
- MagicalWeapon implements DamageDealer
- HealingObject implements Healer
- Character depends on interfaces instead of concrete classes
This commit is contained in:
Willem van den Ende 2026-06-14 10:49:53 +01:00
parent f29e3c456f
commit 0805623b68
7 changed files with 26272 additions and 28 deletions

View File

@ -74,15 +74,15 @@ After a user-story-conversation produces an Allium spec and fast-check propertie
For each rule/invariant from the spec, decide whether to write a **property-based test** or an **example-based test**. Discuss with the user: For each rule/invariant from the spec, decide whether to write a **property-based test** or an **example-based test**. Discuss with the user:
| Signal | Choose | Rationale | | Signal | Choose | Rationale |
| --------------------------------------------------- | --------------- | -------------------------------------------- | | --------------------------------------------------- | -------------- | ------------------------------------------- |
| `fc.property` would be trivially short (< 5 lines) | Example-based | Property overhead not worth it | | `fc.property` would be trivially short (< 5 lines) | Example-based | Property overhead not worth it |
| Invariant is a simple arithmetic relationship | Example-based | One or two examples cover all cases | | Invariant is a simple arithmetic relationship | Example-based | One or two examples cover all cases |
| State transition has a small, finite input space | Example-based | Exhaustive examples are feasible | | State transition has a small, finite input space | Example-based | Exhaustive examples are feasible |
| Invariant involves collections, sequences, or math | Property-based | Need random inputs to find edge cases | | Invariant involves collections, sequences, or math | Property-based | Need random inputs to find edge cases |
| Rule has complex guards (requires + ensures chains) | Property-based | Random inputs surface hidden preconditions | | Rule has complex guards (requires + ensures chains) | Property-based | Random inputs surface hidden preconditions |
| User says "just show it works" | Example-based | Confidence test, not a robustness guarantee | | User says "just show it works" | Example-based | Confidence test, not a robustness guarantee |
| User says "prove it always holds" | Property-based | That's what properties are for | | User says "prove it always holds" | Property-based | That's what properties are for |
**Default:** start with example-based tests. Escalate to property-based only when the user or the spec demands broader coverage. This keeps the yak list smaller and faster to execute. **Default:** start with example-based tests. Escalate to property-based only when the user or the spec demands broader coverage. This keeps the yak list smaller and faster to execute.
@ -167,13 +167,13 @@ Clear description of the change.
## Integration with Other Skills ## Integration with Other Skills
| Skill | When to run problem-breakdown after | | Skill | When to run problem-breakdown after |
| ---------------------- | ------------------------------------------------ | | ----------------------- | ------------------------------------------------------------------------ |
| user-story-conversation| After Allium spec + properties are produced | | user-story-conversation | After Allium spec + properties are produced |
| distill | After spec extraction, before test generation | | distill | After spec extraction, before test generation |
| tend | After spec changes, to plan implementation updates | | tend | After spec changes, to plan implementation updates |
| propagate | When propagate produces obligations, to break them into file-level steps | | propagate | When propagate produces obligations, to break them into file-level steps |
| weed | After divergence is found, to plan alignment fixes | | weed | After divergence is found, to plan alignment fixes |
## Example: Full Breakdown ## Example: Full Breakdown

View File

@ -10,8 +10,8 @@ import type { Status } from './Status.ts';
import { StatusAlive, StatusDead } from './Status.ts'; import { StatusAlive, StatusDead } from './Status.ts';
import type { CharacterState } from './CharacterState.ts'; import type { CharacterState } from './CharacterState.ts';
import type { Faction } from './Faction.ts'; import type { Faction } from './Faction.ts';
import type { MagicalWeapon } from './MagicalWeapon.ts'; import type { DamageDealer } from './magical-objects/magical-object-types.ts';
import type { HealingObject } from './HealingObject.ts'; import type { Healer } from './magical-objects/magical-object-types.ts';
export interface CharacterCtor { export interface CharacterCtor {
name: string; name: string;
@ -233,10 +233,7 @@ export class Character {
* Dead characters cannot use weapons. Only the owner can use a weapon. * Dead characters cannot use weapons. Only the owner can use a weapon.
* Returns updated weapon and target. * Returns updated weapon and target.
*/ */
useWeapon( useWeapon(weapon: DamageDealer, target: Character): { weapon: DamageDealer; target: Character } {
weapon: MagicalWeapon,
target: Character,
): { weapon: MagicalWeapon; target: Character } {
// Dead characters cannot use weapons // Dead characters cannot use weapons
if (this.status.kind === 'dead') return { weapon, target }; if (this.status.kind === 'dead') return { weapon, target };
// Only the owner can use the weapon // Only the owner can use the weapon
@ -249,10 +246,7 @@ export class Character {
* Dead characters cannot use healing objects. * Dead characters cannot use healing objects.
* Returns updated object and character. * Returns updated object and character.
*/ */
useHealingObject( useHealingObject(object: Healer, amount: number): { object: Healer; character: Character } {
object: HealingObject,
amount: number,
): { object: HealingObject; character: Character } {
// Dead characters cannot use healing objects // Dead characters cannot use healing objects
if (this.status.kind === 'dead') return { object, character: this }; if (this.status.kind === 'dead') return { object, character: this };
return object.heal(this, amount); return object.heal(this, amount);

View File

@ -9,8 +9,9 @@
import { Character } from './Character.ts'; import { Character } from './Character.ts';
import { Level } from './Level.ts'; import { Level } from './Level.ts';
import { MagicalObject } from './MagicalObject.ts'; import { MagicalObject } from './MagicalObject.ts';
import type { Healer } from './magical-objects/magical-object-types.ts';
export class HealingObject extends MagicalObject { export class HealingObject extends MagicalObject implements Healer {
private constructor( private constructor(
health: number, health: number,
maxHealth: number, maxHealth: number,

View File

@ -7,8 +7,9 @@
*/ */
import { Character } from './Character.ts'; import { Character } from './Character.ts';
import { MagicalObject } from './MagicalObject.ts'; import { MagicalObject } from './MagicalObject.ts';
import type { DamageDealer } from './magical-objects/magical-object-types.ts';
export class MagicalWeapon extends MagicalObject { export class MagicalWeapon extends MagicalObject implements DamageDealer {
readonly #damage: number; readonly #damage: number;
readonly #owner: Character; readonly #owner: Character;

View File

@ -0,0 +1,24 @@
/**
* Magical object type interfaces break the circular dependency between
* Character.ts and the magical object implementations.
*
* These interfaces describe magical objects from Character's point of view,
* so Character can depend on abstractions rather than concrete classes.
*/
import type { Character } from '../Character.ts';
import type { MagicalObjectStatus } from '../MagicalObject.ts';
/** A magical object that deals damage — from Character's point of view */
export interface DamageDealer {
readonly owner: Character;
readonly health: number;
readonly status: MagicalObjectStatus;
use(target: Character): { weapon: DamageDealer; target: Character };
}
/** A magical object that heals — from Character's point of view */
export interface Healer {
readonly health: number;
readonly status: MagicalObjectStatus;
heal(character: Character, amount: number): { object: Healer; character: Character };
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long