diff --git a/tests/game-logic.test.mjs b/tests/game-logic.test.mjs new file mode 100644 index 0000000..d0ec1c0 --- /dev/null +++ b/tests/game-logic.test.mjs @@ -0,0 +1,109 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + DEFAULT_PIRATE_SHOT_DAMAGE, + PLAYER_HIT_COOLDOWN_MS, + PLAYER_MAX_HULL, + normalizePlayerDamage, + reducePlayerHitQueue, +} from '../src/game-logic.js'; + +test('first pirate hit removes exactly the default damage and does not end the game', () => { + const result = reducePlayerHitQueue({ + hull: PLAYER_MAX_HULL, + lastHitAt: -Infinity, + now: 1000, + damages: [DEFAULT_PIRATE_SHOT_DAMAGE], + }); + + assert.equal(result.hull, 94); + assert.equal(result.appliedHits, 1); + assert.equal(result.ignoredHits, 0); + assert.equal(result.totalDamage, 6); + assert.equal(result.gameOver, false); +}); + +test('three cannonballs arriving in one broadside only apply one hull hit', () => { + const result = reducePlayerHitQueue({ + hull: 100, + lastHitAt: -Infinity, + now: 2000, + damages: [6, 6, 6], + }); + + assert.equal(result.hull, 94); + assert.equal(result.appliedHits, 1); + assert.equal(result.ignoredHits, 2); +}); + +test('hit inside cooldown is ignored and exact cooldown boundary is accepted', () => { + const blocked = reducePlayerHitQueue({ + hull: 94, + lastHitAt: 1000, + now: 1000 + PLAYER_HIT_COOLDOWN_MS - 1, + damages: [6], + }); + + assert.equal(blocked.hull, 94); + assert.equal(blocked.appliedHits, 0); + assert.equal(blocked.ignoredHits, 1); + + const accepted = reducePlayerHitQueue({ + hull: 94, + lastHitAt: 1000, + now: 1000 + PLAYER_HIT_COOLDOWN_MS, + damages: [6], + }); + + assert.equal(accepted.hull, 88); + assert.equal(accepted.appliedHits, 1); +}); + +test('damage normalization handles invalid and extreme values safely', () => { + assert.equal(normalizePlayerDamage(undefined), 6); + assert.equal(normalizePlayerDamage('not-a-number'), 6); + assert.equal(normalizePlayerDamage(999), 25); + assert.equal(normalizePlayerDamage(-12), 1); + assert.equal(normalizePlayerDamage('8'), 8); +}); + +test('invalid hull state recovers from max hull before applying damage', () => { + const result = reducePlayerHitQueue({ + hull: Number.NaN, + lastHitAt: -Infinity, + now: 3000, + damages: [6], + }); + + assert.equal(result.hull, 94); + assert.equal(result.gameOver, false); +}); + +test('lethal hit clamps hull at zero and reports game over', () => { + const result = reducePlayerHitQueue({ + hull: 5, + lastHitAt: -Infinity, + now: 4000, + damages: [6], + }); + + assert.equal(result.hull, 0); + assert.equal(result.totalDamage, 6); + assert.equal(result.gameOver, true); +}); + +test('empty hit queue is a no-op', () => { + const result = reducePlayerHitQueue({ + hull: 72, + lastHitAt: 1234, + now: 5000, + damages: [], + }); + + assert.equal(result.hull, 72); + assert.equal(result.lastHitAt, 1234); + assert.equal(result.appliedHits, 0); + assert.equal(result.ignoredHits, 0); + assert.equal(result.gameOver, false); +});