import fc from 'fast-check'; import { describe, it } from 'vitest'; import { Character } from './characters/Character.ts'; import { Level } from './value-objects/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; }), ); }); }); });