refactor: replace number health with Health value object in MagicalObject
- MagicalObject: internal #health and #maxHealth now Health type - HealingObject: constructor, create, and heal use Health value object - MagicalWeapon: constructor, create, and use use Health value object - DamageDealer and Healer interfaces: health: number -> health: Health - magical-objects.spec.ts: all assertions use .health.value - Run npm run checks: 0 errors, 70 tests passing
This commit is contained in:
parent
0540e5ff5b
commit
ba0903714c
@ -1,18 +1,19 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
|
||||
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
||||
import { isToolCallEventType } from '@earendil-works/pi-coding-agent';
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
if (!isToolCallEventType("bash", event)) return;
|
||||
pi.on('tool_call', async (event, ctx) => {
|
||||
if (!isToolCallEventType('bash', event)) return;
|
||||
|
||||
const cmd = event.input.command || "";
|
||||
const isYxCommand = cmd.includes("yx ");
|
||||
const cmd = event.input.command || '';
|
||||
const isYxCommand = cmd.includes('yx ');
|
||||
|
||||
// Allow yaks in interactive mode, block in print mode (sub-agents)
|
||||
if (isYxCommand && ctx.mode === "print") {
|
||||
if (isYxCommand && ctx.mode === 'print') {
|
||||
return {
|
||||
block: true,
|
||||
reason: "yx commands are disabled in print mode. Sub-agents must focus on domain work only.",
|
||||
reason:
|
||||
'yx commands are disabled in print mode. Sub-agents must focus on domain work only.',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@ -18,6 +18,7 @@ Coordinate sub-agent work using the `yx` CLI. The main agent creates tasks and d
|
||||
## Running Sub-Agents
|
||||
|
||||
Sub-agents execute in print mode and are **hard-blocked from using `yx` commands** (see `.pi/extensions/yak-mode-gate.ts`). They receive:
|
||||
|
||||
- The yak's `.context.md` (task description)
|
||||
- `AGENTS.md` (project conventions)
|
||||
- Domain files to work on
|
||||
@ -43,6 +44,7 @@ cat .yaks/<task-id>/.state
|
||||
```
|
||||
|
||||
States:
|
||||
|
||||
- `pending` — not yet started
|
||||
- `in-progress` — being worked on
|
||||
- `done` — completed
|
||||
@ -54,6 +56,7 @@ yx done <task-id>
|
||||
```
|
||||
|
||||
Always verify before marking done:
|
||||
|
||||
```bash
|
||||
# 1. Check state
|
||||
cat .yaks/<task-id>/.state
|
||||
@ -68,6 +71,7 @@ npm run checks
|
||||
## Sub-Agent Communication
|
||||
|
||||
Sub-agents can read yak state files directly (no yx CLI needed):
|
||||
|
||||
```bash
|
||||
cat .yaks/<task-id>/.name # task name
|
||||
cat .yaks/<task-id>/.state # current state
|
||||
|
||||
14
README.md
14
README.md
@ -51,13 +51,13 @@ The TypeScript implementation embraces functional patterns:
|
||||
|
||||
All five user stories are implemented and verified:
|
||||
|
||||
| Story | Topic | Status |
|
||||
|-------|-------|--------|
|
||||
| 1 | Character Creation & Damage | ✅ Done |
|
||||
| 2 | Levels | ✅ Done |
|
||||
| 3 | Factions | ✅ Done |
|
||||
| 4 | Magical Objects | ✅ Done |
|
||||
| 5 | Changing Level | ✅ Done |
|
||||
| Story | Topic | Status |
|
||||
| ----- | --------------------------- | ------- |
|
||||
| 1 | Character Creation & Damage | ✅ Done |
|
||||
| 2 | Levels | ✅ Done |
|
||||
| 3 | Factions | ✅ Done |
|
||||
| 4 | Magical Objects | ✅ Done |
|
||||
| 5 | Changing Level | ✅ Done |
|
||||
|
||||
**70 tests passing** across 6 spec files.
|
||||
|
||||
|
||||
@ -4,9 +4,7 @@ import { join, parse } from 'node:path';
|
||||
const VALUE_OBJECTS_DIR = join(import.meta.dirname, '..', 'src', 'value-objects');
|
||||
|
||||
function discoverValueObjects() {
|
||||
const files = readdirSync(VALUE_OBJECTS_DIR).filter(
|
||||
(f) => f.endsWith('.ts') && f !== 'index.ts',
|
||||
);
|
||||
const files = readdirSync(VALUE_OBJECTS_DIR).filter((f) => f.endsWith('.ts') && f !== 'index.ts');
|
||||
return files.map((f) => parse(f).name);
|
||||
}
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ describe('Magical Objects', () => {
|
||||
const target = Character.create({ name: 'goblin', level: Level.create(1) });
|
||||
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
|
||||
const result = attacker.useWeapon(weapon, target);
|
||||
return result.weapon.health === weaponHP - 1;
|
||||
return result.weapon.health.value === weaponHP - 1;
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -86,11 +86,11 @@ describe('Magical Objects', () => {
|
||||
// Kill the attacker using a separate killer
|
||||
const killer = Character.create({ name: 'boss', level: Level.create(1) });
|
||||
const deadAttacker = killer.dealDamage(attacker, 10000);
|
||||
const weaponHPBefore = weapon.health;
|
||||
const weaponHPBefore = weapon.health.value;
|
||||
const targetHealthBefore = target.health.value;
|
||||
const result = deadAttacker.useWeapon(weapon, target);
|
||||
return (
|
||||
result.weapon.health === weaponHPBefore &&
|
||||
result.weapon.health.value === weaponHPBefore &&
|
||||
result.target.health.value === targetHealthBefore
|
||||
);
|
||||
},
|
||||
@ -110,11 +110,11 @@ describe('Magical Objects', () => {
|
||||
const thief = Character.create({ name: 'thief', level: Level.create(1) });
|
||||
const target = Character.create({ name: 'goblin', level: Level.create(1) });
|
||||
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner });
|
||||
const weaponHPBefore = weapon.health;
|
||||
const weaponHPBefore = weapon.health.value;
|
||||
const targetHealthBefore = target.health.value;
|
||||
const result = thief.useWeapon(weapon, target);
|
||||
return (
|
||||
result.weapon.health === weaponHPBefore &&
|
||||
result.weapon.health.value === weaponHPBefore &&
|
||||
result.target.health.value === targetHealthBefore
|
||||
);
|
||||
},
|
||||
@ -136,7 +136,9 @@ describe('Magical Objects', () => {
|
||||
const targetHealthBefore = firstUse.target.health.value;
|
||||
// Try to use again on the destroyed weapon
|
||||
const result = owner.useWeapon(destroyedWeapon, firstUse.target);
|
||||
return result.weapon.health === 0 && result.target.health.value === targetHealthBefore;
|
||||
return (
|
||||
result.weapon.health.value === 0 && result.target.health.value === targetHealthBefore
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -203,7 +205,7 @@ describe('Magical Objects', () => {
|
||||
});
|
||||
const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
|
||||
const result = character.useHealingObject(object, healAmount);
|
||||
return result.object.health === objectHP - healAmount;
|
||||
return result.object.health.value === objectHP - healAmount;
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -255,11 +257,11 @@ describe('Magical Objects', () => {
|
||||
// Kill the character using a separate killer
|
||||
const killer = Character.create({ name: 'boss', level: Level.create(1) });
|
||||
const deadCharacter = killer.dealDamage(character, 10000);
|
||||
const objectHPBefore = object.health;
|
||||
const objectHPBefore = object.health.value;
|
||||
const characterHealthBefore = deadCharacter.health.value;
|
||||
const result = deadCharacter.useHealingObject(object, 100);
|
||||
return (
|
||||
result.object.health === objectHPBefore &&
|
||||
result.object.health.value === objectHPBefore &&
|
||||
result.character.health.value === characterHealthBefore
|
||||
);
|
||||
}),
|
||||
@ -287,7 +289,8 @@ describe('Magical Objects', () => {
|
||||
// Try to use again on the destroyed object
|
||||
const result = healedCharacter.useHealingObject(destroyedObject, 100);
|
||||
return (
|
||||
result.object.health === 0 && result.character.health.value === characterHealthBefore
|
||||
result.object.health.value === 0 &&
|
||||
result.character.health.value === characterHealthBefore
|
||||
);
|
||||
}),
|
||||
);
|
||||
@ -305,7 +308,7 @@ describe('Magical Objects', () => {
|
||||
const target = Character.create({ name: 'goblin', level: Level.create(1) });
|
||||
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
|
||||
const result = attacker.useWeapon(weapon, target);
|
||||
return result.weapon.health >= 0;
|
||||
return result.weapon.health.value >= 0;
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -320,7 +323,7 @@ describe('Magical Objects', () => {
|
||||
const character = Character.create({ name: 'hero', level: Level.create(1) });
|
||||
const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
|
||||
const result = character.useHealingObject(object, healAmount);
|
||||
return result.object.health >= 0;
|
||||
return result.object.health.value >= 0;
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -336,7 +339,7 @@ describe('Magical Objects', () => {
|
||||
const target = Character.create({ name: 'goblin', level: Level.create(1) });
|
||||
const weapon = MagicalWeapon.create({ damage, maxHealth: weaponHP, owner: attacker });
|
||||
const result = attacker.useWeapon(weapon, target);
|
||||
return result.weapon.health <= weaponHP;
|
||||
return result.weapon.health.value <= weaponHP;
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -351,7 +354,7 @@ describe('Magical Objects', () => {
|
||||
const character = Character.create({ name: 'hero', level: Level.create(1) });
|
||||
const object = HealingObject.create({ maxHealth: objectHP, currentHealth: objectHP });
|
||||
const result = character.useHealingObject(object, healAmount);
|
||||
return result.object.health <= objectHP;
|
||||
return result.object.health.value <= objectHP;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@ -7,14 +7,15 @@
|
||||
*/
|
||||
|
||||
import { Character } from '../characters/Character.ts';
|
||||
import { Health } from '../value-objects/Health.ts';
|
||||
import { Level } from '../value-objects/Level.ts';
|
||||
import { MagicalObject } from './MagicalObject.ts';
|
||||
import type { Healer } from './magical-object-types.ts';
|
||||
|
||||
export class HealingObject extends MagicalObject implements Healer {
|
||||
private constructor(
|
||||
health: number,
|
||||
maxHealth: number,
|
||||
health: Health,
|
||||
maxHealth: Health,
|
||||
status: { readonly kind: 'alive' } | { readonly kind: 'destroyed' },
|
||||
) {
|
||||
super(health, maxHealth, status);
|
||||
@ -32,7 +33,7 @@ export class HealingObject extends MagicalObject implements Healer {
|
||||
if (currentHealth > maxHealth) throw new Error('CurrentHealth cannot exceed maxHealth');
|
||||
const status =
|
||||
currentHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
|
||||
return new HealingObject(currentHealth, maxHealth, status);
|
||||
return new HealingObject(Health.create(currentHealth), Health.create(maxHealth), status);
|
||||
}
|
||||
|
||||
/** Use this object to heal a character. Returns updated object and character. */
|
||||
@ -44,7 +45,7 @@ export class HealingObject extends MagicalObject implements Healer {
|
||||
// Negative amount is invalid
|
||||
if (amount < 0) throw new Error('Heal amount must be non-negative');
|
||||
// Calculate actual heal amount: min of requested, object remaining, character headroom
|
||||
const objectRemaining = this.health;
|
||||
const objectRemaining = this.health.value;
|
||||
const characterMax = Level.maxHealthForLevel(character.level.value);
|
||||
const characterHeadroom = characterMax - character.health.value;
|
||||
const actualHeal = Math.min(amount, objectRemaining, characterHeadroom);
|
||||
@ -53,7 +54,7 @@ export class HealingObject extends MagicalObject implements Healer {
|
||||
return { object: this, character };
|
||||
}
|
||||
// Create updated object
|
||||
const newObjectHealth = this.health - actualHeal;
|
||||
const newObjectHealth = this.health.value - actualHeal;
|
||||
const newObjectStatus =
|
||||
newObjectHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
|
||||
// Create updated character
|
||||
@ -64,7 +65,7 @@ export class HealingObject extends MagicalObject implements Healer {
|
||||
health: newCharacterHealth,
|
||||
});
|
||||
return {
|
||||
object: new HealingObject(newObjectHealth, this.maxHealth, newObjectStatus),
|
||||
object: new HealingObject(Health.create(newObjectHealth), this.maxHealth, newObjectStatus),
|
||||
character: newCharacter,
|
||||
};
|
||||
}
|
||||
|
||||
@ -7,24 +7,26 @@
|
||||
* - Status derived from health (0 = destroyed, > 0 = alive)
|
||||
*/
|
||||
|
||||
import { Health } from '../value-objects/Health.ts';
|
||||
|
||||
export type MagicalObjectStatus = { readonly kind: 'alive' } | { readonly kind: 'destroyed' };
|
||||
|
||||
export class MagicalObject {
|
||||
readonly #health: number;
|
||||
readonly #maxHealth: number;
|
||||
readonly #health: Health;
|
||||
readonly #maxHealth: Health;
|
||||
readonly #status: MagicalObjectStatus;
|
||||
|
||||
protected constructor(health: number, maxHealth: number, status: MagicalObjectStatus) {
|
||||
protected constructor(health: Health, maxHealth: Health, status: MagicalObjectStatus) {
|
||||
this.#health = health;
|
||||
this.#maxHealth = maxHealth;
|
||||
this.#status = status;
|
||||
}
|
||||
|
||||
get health(): number {
|
||||
get health(): Health {
|
||||
return this.#health;
|
||||
}
|
||||
|
||||
get maxHealth(): number {
|
||||
get maxHealth(): Health {
|
||||
return this.#maxHealth;
|
||||
}
|
||||
|
||||
@ -35,7 +37,7 @@ export class MagicalObject {
|
||||
/** Create a destroyed object (health = 0). */
|
||||
static createDestroyed(maxHealth: number): MagicalObject {
|
||||
if (maxHealth < 0) throw new Error('MaxHealth cannot be negative');
|
||||
return new MagicalObject(0, maxHealth, { kind: 'destroyed' });
|
||||
return new MagicalObject(Health.create(0), Health.create(maxHealth), { kind: 'destroyed' });
|
||||
}
|
||||
|
||||
/** Check if this object is alive. */
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
* - Damage is non-negative
|
||||
*/
|
||||
import { Character } from '../characters/Character.ts';
|
||||
import { Health } from '../value-objects/Health.ts';
|
||||
import { MagicalObject } from './MagicalObject.ts';
|
||||
import type { DamageDealer } from './magical-object-types.ts';
|
||||
|
||||
@ -14,8 +15,8 @@ export class MagicalWeapon extends MagicalObject implements DamageDealer {
|
||||
readonly #owner: Character;
|
||||
|
||||
private constructor(
|
||||
health: number,
|
||||
maxHealth: number,
|
||||
health: Health,
|
||||
maxHealth: Health,
|
||||
status: { readonly kind: 'alive' } | { readonly kind: 'destroyed' },
|
||||
damage: number,
|
||||
owner: Character,
|
||||
@ -36,7 +37,13 @@ export class MagicalWeapon extends MagicalObject implements DamageDealer {
|
||||
}): MagicalWeapon {
|
||||
if (maxHealth < 0) throw new Error('MaxHealth cannot be negative');
|
||||
if (damage < 0) throw new Error('Damage cannot be negative');
|
||||
return new MagicalWeapon(maxHealth, maxHealth, { kind: 'alive' }, damage, owner);
|
||||
return new MagicalWeapon(
|
||||
Health.create(maxHealth),
|
||||
Health.create(maxHealth),
|
||||
{ kind: 'alive' },
|
||||
damage,
|
||||
owner,
|
||||
);
|
||||
}
|
||||
|
||||
get damage(): number {
|
||||
@ -63,12 +70,12 @@ export class MagicalWeapon extends MagicalObject implements DamageDealer {
|
||||
status: newTargetStatus,
|
||||
});
|
||||
// Reduce weapon health by 1
|
||||
const newWeaponHealth = this.health - 1;
|
||||
const newWeaponHealth = this.health.value - 1;
|
||||
const newWeaponStatus =
|
||||
newWeaponHealth === 0 ? { kind: 'destroyed' as const } : { kind: 'alive' as const };
|
||||
return {
|
||||
weapon: new MagicalWeapon(
|
||||
newWeaponHealth,
|
||||
Health.create(newWeaponHealth),
|
||||
this.maxHealth,
|
||||
newWeaponStatus,
|
||||
this.#damage,
|
||||
|
||||
@ -6,19 +6,20 @@
|
||||
* so Character can depend on abstractions rather than concrete classes.
|
||||
*/
|
||||
import type { Character } from '../characters/Character.ts';
|
||||
import type { Health } from '../value-objects/Health.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 health: Health;
|
||||
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 health: Health;
|
||||
readonly status: MagicalObjectStatus;
|
||||
heal(character: Character, amount: number): { object: Healer; character: Character };
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
13112
transcripts/eslint-rule-against-value-objects-and-yaks-to-refactor.html
Normal file
13112
transcripts/eslint-rule-against-value-objects-and-yaks-to-refactor.html
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user