refactor: fix regicide, presumably?
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { createGameHost, GameHost } from 'boardgame-core';
|
||||
import { gameModule, createInitialState } from './regicide';
|
||||
import { gameModule, createInitialState, prompts } from './regicide';
|
||||
import type { RegicideState, Card, Enemy } from './types';
|
||||
|
||||
describe('Regicide Game Module', () => {
|
||||
@@ -14,7 +14,11 @@ describe('Regicide Game Module', () => {
|
||||
afterEach(async () => {
|
||||
// 等待游戏循环完成,然后 dispose
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
// 等待 gamePromise 结束,suppress 预期的 cancel 错误
|
||||
gamePromise?.catch(() => {});
|
||||
gameHost.dispose();
|
||||
// 等待 dispose 完成
|
||||
await new Promise(resolve => setTimeout(resolve, 20));
|
||||
});
|
||||
|
||||
describe('Initial State', () => {
|
||||
@@ -92,8 +96,8 @@ describe('Regicide Game Module', () => {
|
||||
const initialHp = enemy.currentHp;
|
||||
const card = state.hand[0];
|
||||
|
||||
// 出牌 - 使用 input 命令格式
|
||||
const error = gameHost.onInput(`input play ${card.id}`);
|
||||
// 出牌 - 使用 tryAnswerPrompt 命令格式
|
||||
const error = gameHost.tryAnswerPrompt(prompts.playerAction, 'play', [card.id]);
|
||||
expect(error).toBeNull(); // 验证成功
|
||||
|
||||
// 等待下一个 prompt
|
||||
@@ -122,7 +126,7 @@ describe('Regicide Game Module', () => {
|
||||
const handSizeBefore = stateBefore.hand.length;
|
||||
|
||||
// 尝试打出不存在的牌 - 应该被 validator 拒绝
|
||||
const error = gameHost.onInput('input play non-existent-card');
|
||||
const error = gameHost.tryAnswerPrompt(prompts.playerAction, 'play', ['non-existent-card']);
|
||||
// 错误会被 catch 并继续循环,所以返回 null
|
||||
expect(error).toBeNull();
|
||||
|
||||
@@ -145,7 +149,7 @@ describe('Regicide Game Module', () => {
|
||||
|
||||
// 出一张牌造成伤害(不一定击败敌人)
|
||||
const card = state.hand[0];
|
||||
gameHost.onInput(`input play ${card.id}`);
|
||||
gameHost.tryAnswerPrompt(prompts.playerAction, 'play', [card.id]);
|
||||
|
||||
// 等待反击阶段或下一个 prompt
|
||||
await waitForPrompt(gameHost);
|
||||
@@ -177,8 +181,8 @@ describe('Regicide Game Module', () => {
|
||||
gamePromise = gameHost.start();
|
||||
await waitForPrompt(gameHost);
|
||||
|
||||
// 放弃回合 - 使用 input 命令格式
|
||||
const error = gameHost.onInput('input yield');
|
||||
// 放弃回合 - 使用 tryAnswerPrompt 命令格式
|
||||
const error = gameHost.tryAnswerPrompt(prompts.playerAction, 'yield', []);
|
||||
|
||||
// 等待处理完成
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
@@ -198,8 +202,8 @@ describe('Regicide Game Module', () => {
|
||||
const state = gameHost.state.value;
|
||||
const initialHandSize = state.hand.length;
|
||||
|
||||
// 使用小丑牌 - 使用 input 命令格式
|
||||
const error = gameHost.onInput('input useJester');
|
||||
// 使用小丑牌 - 使用 tryAnswerPrompt 命令格式
|
||||
const error = gameHost.tryAnswerPrompt(prompts.playerAction, 'useJester', []);
|
||||
|
||||
// 等待处理完成
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
createGameCommandRegistry,
|
||||
createPromptDef,
|
||||
IGameContext,
|
||||
GameModule,
|
||||
} from 'boardgame-core';
|
||||
@@ -90,30 +91,33 @@ async function playCard(game: RegicideGame, cardId: string) {
|
||||
// 计算伤害
|
||||
let damage = card.value;
|
||||
const suits = card.suit ? [card.suit] : [];
|
||||
|
||||
|
||||
if (suits.includes('clubs')) damage *= 2;
|
||||
|
||||
let isImmune = false;
|
||||
if (state.currentEnemy.immunitySuit && suits.includes(state.currentEnemy.immunitySuit)) {
|
||||
const enemy = state.currentEnemy;
|
||||
if (enemy.immunitySuit && suits.includes(enemy.immunitySuit)) {
|
||||
isImmune = true;
|
||||
damage = card.value;
|
||||
}
|
||||
|
||||
const enemy = state.currentEnemy;
|
||||
enemy.currentHp -= damage;
|
||||
const isDefeated = enemy.currentHp <= 0;
|
||||
|
||||
game.produce((state: RegicideState) => {
|
||||
const cardIndex = state.hand.findIndex(c => c.id === cardId);
|
||||
if (cardIndex !== -1) state.hand.splice(cardIndex, 1);
|
||||
state.discardPile.push(card);
|
||||
|
||||
|
||||
// 计算最终伤害并应用到敌人
|
||||
let finalDamage = damage;
|
||||
const e = state.currentEnemy!;
|
||||
e.currentHp -= finalDamage;
|
||||
const isDefeated = e.currentHp <= 0;
|
||||
|
||||
if (isDefeated) {
|
||||
enemy.isDefeated = true;
|
||||
state.defeatedEnemies.push({ ...enemy });
|
||||
e.isDefeated = true;
|
||||
state.defeatedEnemies.push({ ...e });
|
||||
state.currentEnemy = null;
|
||||
state.phase = 'enemyDefeated';
|
||||
|
||||
|
||||
if (state.defeatedEnemies.length >= 12) {
|
||||
state.isGameOver = true;
|
||||
state.victoryLevel = calculateVictoryLevel(state.jestersUsed);
|
||||
@@ -122,16 +126,20 @@ async function playCard(game: RegicideGame, cardId: string) {
|
||||
} else {
|
||||
state.phase = 'enemyCounterattack';
|
||||
}
|
||||
|
||||
|
||||
state.currentPlayed = {
|
||||
cards: [card],
|
||||
totalDamage: isImmune ? card.value : damage,
|
||||
totalDamage: isImmune ? card.value : finalDamage,
|
||||
suits,
|
||||
hasJester: false,
|
||||
};
|
||||
});
|
||||
|
||||
return { damage, remainingHp: Math.max(0, enemy.currentHp), isImmune, isDefeated };
|
||||
// 从 produce 后的状态读取结果
|
||||
const finalEnemy = game.value.defeatedEnemies.find(e => e.id === enemy.id) || game.value.currentEnemy;
|
||||
const wasDefeated = game.value.defeatedEnemies.some(e => e.id === enemy.id);
|
||||
|
||||
return { damage, remainingHp: wasDefeated ? 0 : Math.max(0, (game.value.currentEnemy?.currentHp ?? 0)), isImmune, isDefeated: wasDefeated };
|
||||
}
|
||||
|
||||
registry.register('play <cardId:string>', playCard);
|
||||
@@ -162,32 +170,35 @@ async function playCombo(game: RegicideGame, cardIds: string[]) {
|
||||
|
||||
let damage = totalValue;
|
||||
const suits = selectedCards.map(c => c.suit).filter((s): s is CardSuit => s !== null);
|
||||
|
||||
|
||||
if (suits.includes('clubs')) damage *= 2;
|
||||
|
||||
let isImmune = false;
|
||||
if (state.currentEnemy!.immunitySuit && suits.includes(state.currentEnemy!.immunitySuit)) {
|
||||
const enemy = state.currentEnemy!;
|
||||
if (enemy.immunitySuit && suits.includes(enemy.immunitySuit)) {
|
||||
isImmune = true;
|
||||
damage = totalValue;
|
||||
}
|
||||
|
||||
const enemy = state.currentEnemy!;
|
||||
enemy.currentHp -= damage;
|
||||
const isDefeated = enemy.currentHp <= 0;
|
||||
|
||||
game.produce((state: RegicideState) => {
|
||||
for (const card of selectedCards) {
|
||||
const cardIndex = state.hand.findIndex(c => c.id === card.id);
|
||||
if (cardIndex !== -1) state.hand.splice(cardIndex, 1);
|
||||
state.discardPile.push(card);
|
||||
}
|
||||
|
||||
|
||||
// 计算最终伤害并应用到敌人
|
||||
let finalDamage = damage;
|
||||
const e = state.currentEnemy!;
|
||||
e.currentHp -= finalDamage;
|
||||
const isDefeated = e.currentHp <= 0;
|
||||
|
||||
if (isDefeated) {
|
||||
enemy.isDefeated = true;
|
||||
state.defeatedEnemies.push({ ...enemy });
|
||||
e.isDefeated = true;
|
||||
state.defeatedEnemies.push({ ...e });
|
||||
state.currentEnemy = null;
|
||||
state.phase = 'enemyDefeated';
|
||||
|
||||
|
||||
if (state.defeatedEnemies.length >= 12) {
|
||||
state.isGameOver = true;
|
||||
state.victoryLevel = calculateVictoryLevel(state.jestersUsed);
|
||||
@@ -196,16 +207,18 @@ async function playCombo(game: RegicideGame, cardIds: string[]) {
|
||||
} else {
|
||||
state.phase = 'enemyCounterattack';
|
||||
}
|
||||
|
||||
|
||||
state.currentPlayed = {
|
||||
cards: selectedCards,
|
||||
totalDamage: isImmune ? totalValue : damage,
|
||||
totalDamage: isImmune ? totalValue : finalDamage,
|
||||
suits,
|
||||
hasJester: false,
|
||||
};
|
||||
});
|
||||
|
||||
return { damage, remainingHp: Math.max(0, enemy.currentHp), isImmune, isDefeated };
|
||||
const wasDefeated = game.value.defeatedEnemies.some(e => e.id === enemy.id);
|
||||
|
||||
return { damage, remainingHp: wasDefeated ? 0 : Math.max(0, (game.value.currentEnemy?.currentHp ?? 0)), isImmune, isDefeated: wasDefeated };
|
||||
}
|
||||
|
||||
registry.register('combo <cardIds>', playCombo);
|
||||
@@ -334,6 +347,16 @@ function calculateVictoryLevel(jestersUsed: number): 'gold' | 'silver' | 'bronze
|
||||
return 'bronze';
|
||||
}
|
||||
|
||||
|
||||
export const prompts = {
|
||||
playerAction: createPromptDef<[action: string, cardIds: string[]]>(
|
||||
'play <action:string> <cardIds:string[]>'
|
||||
),
|
||||
counterattack: createPromptDef<[cardIds: string[]]>(
|
||||
'counterattack <cardIds:string[]>'
|
||||
),
|
||||
};
|
||||
|
||||
// start 函数 - 游戏主循环
|
||||
export async function start(game: RegicideGame) {
|
||||
// 首先执行 setup
|
||||
@@ -345,33 +368,27 @@ export async function start(game: RegicideGame) {
|
||||
|
||||
// 玩家回合 - 等待玩家输入任何命令
|
||||
if (state.phase === 'playerTurn' && state.currentEnemy && !state.currentEnemy.isDefeated) {
|
||||
// 使用通用的 input 命令来等待玩家输入
|
||||
const { inputStr } = await game.prompt(
|
||||
'input <arg1:string> [arg2:string] [arg3:string]',
|
||||
(arg1: string, arg2: string, arg3: string) => {
|
||||
const parts = [arg1, arg2, arg3].filter(a => a !== undefined && a !== '');
|
||||
const { action, cardIds } = await game.prompt(
|
||||
prompts.playerAction,
|
||||
(action: string, cardIds: string[]) => {
|
||||
const parts = cardIds.filter(a => a !== undefined && a !== '');
|
||||
if (parts.length === 0) {
|
||||
throw '请输入有效的命令';
|
||||
}
|
||||
return { inputStr: parts.join(' ') };
|
||||
return { action, cardIds: parts };
|
||||
},
|
||||
state.currentEnemy.id
|
||||
);
|
||||
|
||||
// 解析并执行命令
|
||||
const parts = inputStr.split(' ');
|
||||
const command = parts[0];
|
||||
const args = parts.slice(1);
|
||||
|
||||
try {
|
||||
if (command === 'play' && args.length >= 1) {
|
||||
await playCard(game, args[0]);
|
||||
} else if (command === 'combo' && args.length >= 1) {
|
||||
const cardIds = args[0].split(',');
|
||||
if (action === 'play' && cardIds.length >= 1) {
|
||||
await playCard(game, cardIds[0]);
|
||||
} else if (action === 'combo' && cardIds.length >= 2) {
|
||||
await playCombo(game, cardIds);
|
||||
} else if (command === 'yield') {
|
||||
} else if (action === 'yield') {
|
||||
await yieldTurn(game);
|
||||
} else if (command === 'useJester') {
|
||||
} else if (action === 'useJester') {
|
||||
await useJester(game);
|
||||
} else {
|
||||
// 无效命令,继续等待
|
||||
@@ -389,7 +406,7 @@ export async function start(game: RegicideGame) {
|
||||
if (state.currentEnemy) {
|
||||
const requiredValue = state.currentEnemy.counterDamage;
|
||||
const { cardIds } = await game.prompt(
|
||||
'counterattack <cardIds>',
|
||||
prompts.counterattack,
|
||||
(cardIds: string[]) => {
|
||||
const selectedCards: Card[] = [];
|
||||
for (const cardId of cardIds) {
|
||||
|
||||
@@ -82,13 +82,4 @@ export interface RegicideState {
|
||||
|
||||
// 索引签名以满足 Record<string, unknown> 约束
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 命令参数类型
|
||||
export interface PlayCardCommand {
|
||||
cardIds: string[];
|
||||
}
|
||||
|
||||
export interface CounterattackCommand {
|
||||
cardIds: string[];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user