boardgame-core/tests/samples/slay-the-spire-like/combat/procedure.test.ts

262 lines
9.3 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { createGameHost, GameHost } from '@/core/game-host';
import { createGameContext, createGameCommandRegistry } from '@/core/game';
import type { CombatState, CombatGameContext } from '@/samples/slay-the-spire-like/combat/types';
import { createCombatState } from '@/samples/slay-the-spire-like/combat/state';
import { runCombat } from '@/samples/slay-the-spire-like/combat/procedure';
import { prompts } from '@/samples/slay-the-spire-like/combat/prompts';
import { createGridInventory, placeItem } from '@/samples/slay-the-spire-like/grid-inventory';
import type { GridInventory, InventoryItem } from '@/samples/slay-the-spire-like/grid-inventory';
import type { GameItemMeta, PlayerState } from '@/samples/slay-the-spire-like/progress/types';
import { IDENTITY_TRANSFORM } from '@/samples/slay-the-spire-like/utils/shape-collision';
import { parseShapeString } from '@/samples/slay-the-spire-like/utils/parse-shape';
import { encounterDesertData, enemyDesertData } from '@/samples/slay-the-spire-like/data';
function createTestMeta(name: string, shapeStr: string): GameItemMeta {
const shape = parseShapeString(shapeStr);
return {
itemData: {
type: 'weapon',
name,
shape: shapeStr,
costType: 'energy',
costCount: 1,
targetType: 'single',
price: 10,
desc: '测试',
},
shape,
};
}
function createTestInventory(): GridInventory<GameItemMeta> {
const inv = createGridInventory<GameItemMeta>(6, 4);
const meta1 = createTestMeta('短刀', 'oe');
const item1: InventoryItem<GameItemMeta> = {
id: 'item-1',
shape: meta1.shape,
transform: { ...IDENTITY_TRANSFORM, offset: { x: 0, y: 0 } },
meta: meta1,
};
placeItem(inv, item1);
return inv;
}
function createTestCombatState(): CombatState {
const inv = createTestInventory();
const playerState: PlayerState = { maxHp: 50, currentHp: 50, gold: 0 };
const encounter = encounterDesertData.find(e => e.name === '仙人掌怪')!;
return createCombatState(playerState, inv, encounter);
}
function waitForPrompt(host: GameHost<CombatState>): Promise<void> {
return new Promise((resolve) => {
const check = () => {
if (host.activePromptSchema.value !== null) {
resolve();
} else {
setTimeout(check, 10);
}
};
check();
});
}
describe('combat/procedure', () => {
describe('runCombat with GameHost', () => {
it('should start combat and prompt for player action', async () => {
const registry = createGameCommandRegistry<CombatState>();
const initialState = createTestCombatState();
const host = new GameHost(
registry,
() => createTestCombatState(),
async (ctx) => {
return await runCombat(ctx);
},
);
const combatPromise = host.start(42);
await waitForPrompt(host);
expect(host.activePromptSchema.value).not.toBeNull();
expect(host.activePromptSchema.value?.name).toBe('play-card');
host._context._commands._cancel();
try { await combatPromise; } catch {}
});
it('should accept play-card input', async () => {
const registry = createGameCommandRegistry<CombatState>();
const host = new GameHost(
registry,
() => createTestCombatState(),
async (ctx) => {
return await runCombat(ctx);
},
);
const combatPromise = host.start(42);
await waitForPrompt(host);
const state = host.state.value;
const cardId = state.player.deck.hand[0];
const error = host.tryAnswerPrompt(prompts.playCard, cardId, state.enemyOrder[0]);
expect(error).toBeNull();
host._context._commands._cancel();
try { await combatPromise; } catch {}
});
it('should reject invalid card play', async () => {
const registry = createGameCommandRegistry<CombatState>();
const host = new GameHost(
registry,
() => createTestCombatState(),
async (ctx) => {
return await runCombat(ctx);
},
);
const combatPromise = host.start(42);
await waitForPrompt(host);
const error = host.tryAnswerPrompt(prompts.playCard, 'nonexistent-card');
expect(error).not.toBeNull();
host._context._commands._cancel();
try { await combatPromise; } catch {}
});
it('should transition to end-turn after playing cards', async () => {
const registry = createGameCommandRegistry<CombatState>();
const host = new GameHost(
registry,
() => createTestCombatState(),
async (ctx) => {
return await runCombat(ctx);
},
);
const combatPromise = host.start(42);
await waitForPrompt(host);
const state = host.state.value;
const cardId = state.player.deck.hand[0];
host.tryAnswerPrompt(prompts.playCard, cardId, state.enemyOrder[0]);
await waitForPrompt(host);
expect(host.activePromptSchema.value).not.toBeNull();
host._context._commands._cancel();
try { await combatPromise; } catch {}
});
it('should accept end-turn', async () => {
const registry = createGameCommandRegistry<CombatState>();
const host = new GameHost(
registry,
() => createTestCombatState(),
async (ctx) => {
return await runCombat(ctx);
},
);
const combatPromise = host.start(42);
await waitForPrompt(host);
const error = host.tryAnswerPrompt(prompts.endTurn);
expect(error).toBeNull();
host._context._commands._cancel();
try { await combatPromise; } catch {}
});
});
describe('combat outcome', () => {
it('should return victory when all enemies are dead', async () => {
const registry = createGameCommandRegistry<CombatState>();
const host = new GameHost(
registry,
() => {
const state = createTestCombatState();
for (const enemyId of state.enemyOrder) {
state.enemies[enemyId].hp = 1;
}
return state;
},
async (ctx) => {
return await runCombat(ctx);
},
);
const combatPromise = host.start(42);
await waitForPrompt(host);
let iterations = 0;
while (host.status.value === 'running' && iterations < 100) {
const state = host.state.value;
if (host.activePromptSchema.value?.name === 'play-card') {
const cardId = state.player.deck.hand[0];
if (cardId) {
const targetId = state.enemyOrder.find(id => state.enemies[id].isAlive);
host.tryAnswerPrompt(prompts.playCard, cardId, targetId);
}
} else if (host.activePromptSchema.value?.name === 'end-turn') {
host.tryAnswerPrompt(prompts.endTurn);
}
await new Promise(r => setTimeout(r, 10));
iterations++;
}
if (host.status.value === 'running') {
host._context._commands._cancel();
try { await combatPromise; } catch {}
}
});
});
describe('combat state transitions', () => {
it('should track turn number across turns', async () => {
const registry = createGameCommandRegistry<CombatState>();
const host = new GameHost(
registry,
() => createTestCombatState(),
async (ctx) => {
return await runCombat(ctx);
},
);
const combatPromise = host.start(42);
await waitForPrompt(host);
host.tryAnswerPrompt(prompts.endTurn);
await waitForPrompt(host);
host._context._commands._cancel();
try { await combatPromise; } catch {}
});
it('should reset energy at start of player turn', async () => {
const registry = createGameCommandRegistry<CombatState>();
const host = new GameHost(
registry,
() => createTestCombatState(),
async (ctx) => {
return await runCombat(ctx);
},
);
const combatPromise = host.start(42);
await waitForPrompt(host);
const state = host.state.value;
expect(state.player.energy).toBe(state.player.maxEnergy);
host._context._commands._cancel();
try { await combatPromise; } catch {}
});
});
});