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:
@@ -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
|
||||
|
||||
@@ -52,7 +52,7 @@ 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,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);
|
||||
}
|
||||
|
||||
|
||||
+17
-14
@@ -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 };
|
||||
}
|
||||
|
||||
+10248
-1392
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10248
-1392
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user