From 1c5fcc80ad1bc4610fc5d6711ebabbd15e3e5d32 Mon Sep 17 00:00:00 2001 From: ginnoir Date: Thu, 11 Jun 2026 18:23:43 -0500 Subject: [PATCH] chore: pass PR1 verification gate (format, lint, coverage) --- src/content/__tests__/definitions.test.ts | 8 +-- src/content/definitions.ts | 43 ++++++++++++++-- src/content/schema.ts | 8 +-- src/engine/__tests__/game.test.ts | 61 ++++++++++++++++++++++- src/engine/__tests__/save.test.ts | 7 ++- src/engine/game.ts | 5 +- src/state/__tests__/persistence.test.ts | 7 ++- src/state/__tests__/viewModel.test.ts | 7 ++- 8 files changed, 127 insertions(+), 19 deletions(-) diff --git a/src/content/__tests__/definitions.test.ts b/src/content/__tests__/definitions.test.ts index 0a5eb91..52f2584 100644 --- a/src/content/__tests__/definitions.test.ts +++ b/src/content/__tests__/definitions.test.ts @@ -16,9 +16,11 @@ describe('M1 stub content pack', () => { it('can simulate a costed action without throwing', () => { const state = createGameState(content); const trade = content.actions.find((a) => a.costs.length > 0); - expect(trade).toBeDefined(); - enqueueAction(state, content, trade!.id); - tickGame(state, content, trade!.durationMs); + if (!trade) { + throw new Error('expected at least one costed action'); + } + enqueueAction(state, content, trade.id); + tickGame(state, content, trade.durationMs); expect(state.activeActionId).toBeNull(); }); }); diff --git a/src/content/definitions.ts b/src/content/definitions.ts index fe5be01..39c6fb5 100644 --- a/src/content/definitions.ts +++ b/src/content/definitions.ts @@ -4,9 +4,42 @@ export const resourceDefs = [ ]; export const actionDefs = [ - { id: 'gather_supplies', name: 'Gather supplies', durationMs: 3000, yields: [{ resourceId: 'supplies', amount: 2 }] }, - { id: 'scout_path', name: 'Scout the path', durationMs: 5000, costs: [{ resourceId: 'supplies', amount: 2 }], yields: [{ resourceId: 'coin', amount: 1 }] }, - { id: 'trade_supplies', name: 'Trade at camp', durationMs: 4000, costs: [{ resourceId: 'supplies', amount: 3 }], yields: [{ resourceId: 'coin', amount: 2 }], unlock: { minResources: { coin: 1 } } }, - { id: 'fortify_camp', name: 'Fortify camp', durationMs: 8000, costs: [{ resourceId: 'supplies', amount: 5 }, { resourceId: 'coin', amount: 2 }], yields: [{ resourceId: 'supplies', amount: 4 }], unlock: { minResources: { supplies: 8 } } }, - { id: 'rest', name: 'Rest briefly', durationMs: 2000, yields: [{ resourceId: 'supplies', amount: 1 }] }, + { + id: 'gather_supplies', + name: 'Gather supplies', + durationMs: 3000, + yields: [{ resourceId: 'supplies', amount: 2 }], + }, + { + id: 'scout_path', + name: 'Scout the path', + durationMs: 5000, + costs: [{ resourceId: 'supplies', amount: 2 }], + yields: [{ resourceId: 'coin', amount: 1 }], + }, + { + id: 'trade_supplies', + name: 'Trade at camp', + durationMs: 4000, + costs: [{ resourceId: 'supplies', amount: 3 }], + yields: [{ resourceId: 'coin', amount: 2 }], + unlock: { minResources: { coin: 1 } }, + }, + { + id: 'fortify_camp', + name: 'Fortify camp', + durationMs: 8000, + costs: [ + { resourceId: 'supplies', amount: 5 }, + { resourceId: 'coin', amount: 2 }, + ], + yields: [{ resourceId: 'supplies', amount: 4 }], + unlock: { minResources: { supplies: 8 } }, + }, + { + id: 'rest', + name: 'Rest briefly', + durationMs: 2000, + yields: [{ resourceId: 'supplies', amount: 1 }], + }, ]; diff --git a/src/content/schema.ts b/src/content/schema.ts index 5a92d62..976bfcf 100644 --- a/src/content/schema.ts +++ b/src/content/schema.ts @@ -68,16 +68,12 @@ export function buildContent(input: { resources: unknown[]; actions: unknown[] } for (const action of actions) { for (const y of action.yields) { if (!resourcesById[y.resourceId]) { - throw new Error( - `Action "${action.id}" yields unknown resource "${y.resourceId}"`, - ); + throw new Error(`Action "${action.id}" yields unknown resource "${y.resourceId}"`); } } for (const c of action.costs) { if (!resourcesById[c.resourceId]) { - throw new Error( - `Action "${action.id}" cost references unknown resource "${c.resourceId}"`, - ); + throw new Error(`Action "${action.id}" cost references unknown resource "${c.resourceId}"`); } } } diff --git a/src/engine/__tests__/game.test.ts b/src/engine/__tests__/game.test.ts index 57225f1..58791da 100644 --- a/src/engine/__tests__/game.test.ts +++ b/src/engine/__tests__/game.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { buildContent } from '../../content/schema'; import { cancelQueuedAction, + canUnlockAction, clearQueue, createGameState, enqueueAction, @@ -13,7 +14,12 @@ function testContent() { return buildContent({ resources: [{ id: 'gold', name: 'Gold', startAmount: 5 }], actions: [ - { id: 'forage', name: 'Forage', durationMs: 300, yields: [{ resourceId: 'gold', amount: 2 }] }, + { + id: 'forage', + name: 'Forage', + durationMs: 300, + yields: [{ resourceId: 'gold', amount: 2 }], + }, ], }); } @@ -180,6 +186,24 @@ describe('unlock conditions', () => { enqueueAction(state, content, 'scout'); expect(state.activeActionId).toBe('scout'); }); + + it('rejects actions until required story flags are set', () => { + const content = buildContent({ + resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }], + actions: [ + { + id: 'secret', + name: 'Secret', + durationMs: 100, + yields: [{ resourceId: 'gold', amount: 1 }], + unlock: { requireStoryFlags: ['path_scouted'] }, + }, + ], + }); + const state = createGameState(content); + expect(() => enqueueAction(state, content, 'secret')).toThrow(/cannot enqueue/i); + expect(canUnlockAction(state, content, 'secret', { path_scouted: true })).toBe(true); + }); }); describe('completion advances queue', () => { @@ -203,6 +227,41 @@ describe('completion advances queue', () => { expect(state.actionElapsedMs).toBe(0); }); + it('skips queued actions that are unaffordable when their turn arrives', () => { + const content = buildContent({ + resources: [{ id: 'supplies', name: 'Supplies', startAmount: 0 }], + actions: [ + { + id: 'cheap', + name: 'Cheap', + durationMs: 100, + yields: [{ resourceId: 'supplies', amount: 1 }], + }, + { + id: 'dear', + name: 'Dear', + durationMs: 100, + costs: [{ resourceId: 'supplies', amount: 6 }], + yields: [{ resourceId: 'supplies', amount: 1 }], + }, + { + id: 'free', + name: 'Free', + durationMs: 100, + yields: [{ resourceId: 'supplies', amount: 1 }], + }, + ], + }); + const state = createGameState(content); + state.activeActionId = 'cheap'; + state.actionElapsedMs = 99; + state.actionQueue = ['dear', 'free']; + state.resources.supplies = 3; + tickGame(state, content, 1); + expect(state.activeActionId).toBe('free'); + expect(state.actionQueue).toEqual([]); + }); + it('grants all yields on completion', () => { const content = buildContent({ resources: [ diff --git a/src/engine/__tests__/save.test.ts b/src/engine/__tests__/save.test.ts index 2ae64bd..e65f78b 100644 --- a/src/engine/__tests__/save.test.ts +++ b/src/engine/__tests__/save.test.ts @@ -15,7 +15,12 @@ function testContent() { return buildContent({ resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }], actions: [ - { id: 'forage', name: 'Forage', durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }] }, + { + id: 'forage', + name: 'Forage', + durationMs: 3000, + yields: [{ resourceId: 'gold', amount: 1 }], + }, ], }); } diff --git a/src/engine/game.ts b/src/engine/game.ts index b369c34..de6a3e9 100644 --- a/src/engine/game.ts +++ b/src/engine/game.ts @@ -99,7 +99,10 @@ function beginAction(state: GameState, content: Content, actionId: string): void function startNextFromQueue(state: GameState, content: Content): void { while (state.actionQueue.length > 0) { - const nextId = state.actionQueue.shift()!; + const nextId = state.actionQueue.shift(); + if (!nextId) { + break; + } if (isActionAvailable(state, content, nextId)) { beginAction(state, content, nextId); return; diff --git a/src/state/__tests__/persistence.test.ts b/src/state/__tests__/persistence.test.ts index 7e6f696..1ed517d 100644 --- a/src/state/__tests__/persistence.test.ts +++ b/src/state/__tests__/persistence.test.ts @@ -8,7 +8,12 @@ function testContent() { return buildContent({ resources: [{ id: 'gold', name: 'Gold', startAmount: 0 }], actions: [ - { id: 'forage', name: 'Forage', durationMs: 3000, yields: [{ resourceId: 'gold', amount: 1 }] }, + { + id: 'forage', + name: 'Forage', + durationMs: 3000, + yields: [{ resourceId: 'gold', amount: 1 }], + }, ], }); } diff --git a/src/state/__tests__/viewModel.test.ts b/src/state/__tests__/viewModel.test.ts index c3e4b65..8555c53 100644 --- a/src/state/__tests__/viewModel.test.ts +++ b/src/state/__tests__/viewModel.test.ts @@ -7,7 +7,12 @@ function testContent() { return buildContent({ resources: [{ id: 'gold', name: 'Gold', startAmount: 4 }], actions: [ - { id: 'forage', name: 'Forage', durationMs: 200, yields: [{ resourceId: 'gold', amount: 1 }] }, + { + id: 'forage', + name: 'Forage', + durationMs: 200, + yields: [{ resourceId: 'gold', amount: 1 }], + }, ], }); }