262 lines
9.3 KiB
TypeScript
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 {}
|
|
});
|
|
});
|
|
});
|