rpg-combat-pi-01/src/damage-and-health.spec.ts
Willem van den Ende 4cdb048dfc refactor(story2): immutable dealDamage, reference guard, negative damage validation, spec precision
- dealDamage returns new Character instead of mutating in-place
- SelfDamageForbidden uses reference equality (this === target)
- Negative damage throws at the boundary
- Removed duplicate Health.maxHealthForLevel (Level.ts is source of truth)
- Allium spec uses max(0, ...) for health floor precision
- New property: NegativeDamageForbidden (11 total properties)
2026-06-13 15:44:46 +01:00

138 lines
4.8 KiB
TypeScript

import fc from 'fast-check';
import { describe, it } from 'vitest';
import { Character } from './Character.ts';
import { Level } from './Level.ts';
describe('DamageAndHealth', () => {
describe('DamageReducesHealth', () => {
it('property: health is reduced by the damage amount', () => {
fc.assert(
fc.property(
fc.integer({ min: 0, max: 1000 }),
fc.integer({ min: 0, max: 10000 }),
(health, damage) => {
const attacker = Character.create({ name: 'attacker', level: Level.create(1) });
const target = Character.createWithHealth({
name: 'target',
level: Level.create(1),
health,
});
const expected = Math.max(0, health - damage);
const result = attacker.dealDamage(target, damage);
return result.health.value === expected;
},
),
);
});
});
describe('HealthNonNegative', () => {
it('property: health never goes below zero after damage', () => {
fc.assert(
fc.property(
fc.integer({ min: 0, max: 1000 }),
fc.integer({ min: 0, max: 10000 }),
(health, damage) => {
const attacker = Character.create({ name: 'attacker', level: Level.create(1) });
const target = Character.createWithHealth({
name: 'target',
level: Level.create(1),
health,
});
const result = attacker.dealDamage(target, damage);
return result.health.value >= 0;
},
),
);
});
});
describe('DeathAtZeroHealth', () => {
it('property: character dies when health reaches zero', () => {
fc.assert(
fc.property(
fc.integer({ min: 0, max: 1000 }),
fc.integer({ min: 0, max: 10000 }),
(health, damage) => {
const attacker = Character.create({ name: 'attacker', level: Level.create(1) });
const target = Character.createWithHealth({
name: 'target',
level: Level.create(1),
health,
});
const result = attacker.dealDamage(target, damage);
const expected = Math.max(0, health - damage);
if (expected === 0) {
return result.status.kind === 'dead';
}
return result.status.kind === 'alive';
},
),
);
});
});
describe('SelfDamageForbidden', () => {
it('property: a character cannot deal damage to itself — returns same reference, state unchanged', () => {
fc.assert(
fc.property(
fc.integer({ min: 0, max: 1000 }),
fc.integer({ min: 0, max: 10000 }),
(health, damage) => {
const c = Character.createWithHealth({ name: 'hero', level: Level.create(1), health });
const healthBefore = c.health.value;
const statusBefore = c.status.kind;
const result = c.dealDamage(c, damage);
// Should return the same reference
return (
result === c &&
result.health.value === healthBefore &&
result.status.kind === statusBefore
);
},
),
);
});
});
describe('DeadCannotTakeDamage', () => {
it('property: dead characters cannot take damage — returns same reference, state unchanged', () => {
fc.assert(
fc.property(fc.integer({ min: 0, max: 10000 }), (damage) => {
const attacker = Character.create({ name: 'attacker', level: Level.create(1) });
const target = Character.create({ name: 'target', level: Level.create(1) });
// Kill the target first — capture the returned (dead) character
const deadTarget = attacker.dealDamage(target, 10000);
const healthBefore = deadTarget.health.value;
const statusBefore = deadTarget.status.kind;
// Then try to deal more damage to the dead character
const result = attacker.dealDamage(deadTarget, damage);
return (
result === deadTarget &&
result.health.value === healthBefore &&
result.status.kind === statusBefore
);
}),
);
});
});
describe('NegativeDamageForbidden', () => {
it('property: negative damage throws an error', () => {
fc.assert(
fc.property(fc.integer({ min: -10000, max: -1 }), (negativeDamage) => {
const attacker = Character.create({ name: 'attacker', level: Level.create(1) });
const target = Character.create({ name: 'target', level: Level.create(1) });
let threw = false;
try {
attacker.dealDamage(target, negativeDamage);
} catch {
threw = true;
}
return threw;
}),
);
});
});
});