diff --git a/src/samples/regicide/commands/index.ts b/src/samples/regicide/commands/index.ts new file mode 100644 index 0000000..9f614b9 --- /dev/null +++ b/src/samples/regicide/commands/index.ts @@ -0,0 +1,107 @@ +import {createGameCommandRegistry} from "@/core/game"; +import {RegicideState} from "@/samples/regicide/state"; + +export const registry = createGameCommandRegistry(); + +/** + * 打出一张牌 + */ +const playCmd = registry.register({ + schema: 'play ', + run: async (game, player, cardId) => { + const state = game.value; + const card = state.cards[cardId]; + + if (!card) { + return {success: false, error: `卡牌 ${cardId} 不存在`}; + } + + // 检查卡牌是否在玩家手牌中 + const playerHand = state.playerHands[player as keyof typeof state.playerHands]; + if (!playerHand || !playerHand.includes(cardId)) { + return {success: false, error: `卡牌 ${cardId} 不在玩家 ${player} 的手牌中`}; + } + + // 检查是否有当前敌人 + if (!state.currentEnemy) { + return {success: false, error: '没有活跃的敌人'}; + } + + // 计算伤害(基础伤害为卡牌面值) + let damage = card.value; + + // TODO: 花色能力 - 梅花双倍伤害 + // if (card.suit === 'clubs') { + // damage *= 2; + // } + + // TODO: A牌配合机制 - 如果card.rank === 'A',可以额外打出一张牌 + + // 对敌人造成伤害 + game.produce(state => { + state.currentEnemy!.hp -= damage; + + // 从手牌移除卡牌 + const hand = state.playerHands[player as keyof typeof state.playerHands]; + const cardIndex = hand.indexOf(cardId); + if (cardIndex !== -1) { + hand.splice(cardIndex, 1); + } + + // 将卡牌移到弃牌堆 + state.cards[cardId].regionId = 'discardPile'; + + // TODO: 触发花色能力 + // TODO: 检查敌人是否被击败 + }); + + return {success: true, result: {damage, enemyHp: state.currentEnemy.hp}}; + } +}); + +/** + * 让过(不出牌) + */ +const passCmd = registry.register({ + schema: 'pass ', + run: async (game, player) => { + const state = game.value; + + // 即使让过,也会受到敌人反击 + // TODO: 实现反击逻辑 + + return {success: true, result: {message: `${player} 让过`}}; + } +}); + +/** + * 检查敌人是否被击败 + */ +const checkEnemyDefeatedCmd = registry.register({ + schema: 'check-enemy-defeated', + run: async (game) => { + const state = game.value; + + if (!state.currentEnemy) { + return {success: false, error: '没有活跃的敌人'}; + } + + const isDefeated = state.currentEnemy.hp <= 0; + + if (isDefeated) { + // 敌人被击败,移到弃牌堆 + game.produce(state => { + // TODO: 将当前敌人移到弃牌堆 + // TODO: 翻开下一个敌人 + }); + } + + return {success: true, result: {isDefeated, enemy: state.currentEnemy}}; + } +}); + +export { + playCmd as play, + passCmd as pass, + checkEnemyDefeatedCmd as checkEnemyDefeated, +}; diff --git a/src/samples/regicide/constants.ts b/src/samples/regicide/constants.ts new file mode 100644 index 0000000..c650099 --- /dev/null +++ b/src/samples/regicide/constants.ts @@ -0,0 +1,36 @@ +import {CardRank, SuitType} from "@/samples/regicide/types"; + +// 敌人总数 +export const ENEMY_COUNT = 12; + +// 初始手牌数 +export const INITIAL_HAND_SIZE = 6; + +// 卡牌面值映射 +export const CARD_VALUES: Record = { + 'A': 1, + '2': 2, + '3': 3, + '4': 4, + '5': 5, + '6': 6, + '7': 7, + '8': 8, + '9': 9, + '10': 10, + 'J': 10, + 'Q': 15, + 'K': 20, +}; + +// 所有花色 +export const ALL_SUITS: SuitType[] = ['spades', 'hearts', 'diamonds', 'clubs']; + +// 所有牌面值(不含大小王) +export const ALL_RANKS: CardRank[] = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']; + +// 人头牌(J/Q/K) +export const FACE_CARDS: CardRank[] = ['J', 'Q', 'K']; + +// 数字牌(A-10) +export const NUMBER_CARDS: CardRank[] = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10']; diff --git a/src/samples/regicide/index.ts b/src/samples/regicide/index.ts new file mode 100644 index 0000000..a150c29 --- /dev/null +++ b/src/samples/regicide/index.ts @@ -0,0 +1,52 @@ +// Types +export type { + SuitType, + CardRank, + PlayerType, + RegionType, + RegicideCardMeta, + RegicideCard, + Enemy, + GamePhase +} from './types'; + +// Constants +export { + ENEMY_COUNT, + INITIAL_HAND_SIZE, + CARD_VALUES, + ALL_SUITS, + ALL_RANKS, + FACE_CARDS, + NUMBER_CARDS +} from './constants'; + +// State +export { + createInitialState, + type RegicideState +} from './state'; + +// Prompts +export {prompts} from './prompts'; + +// Commands +export { + registry, + play as playCmd, + pass as passCmd, + checkEnemyDefeated as checkEnemyDefeatedCmd +} from './commands'; + +// Utils +export { + getCardValue, + createCard, + createEnemy, + createAllCards, + buildEnemyDeck, + buildTavernDeck, + drawFromDeck, + isEnemyDefeated, + getPlayerHandRegionId +} from './utils'; diff --git a/src/samples/regicide/prompts.ts b/src/samples/regicide/prompts.ts new file mode 100644 index 0000000..56ad565 --- /dev/null +++ b/src/samples/regicide/prompts.ts @@ -0,0 +1,17 @@ +import {createPromptDef} from "@/core/game"; +import {PlayerType} from "@/samples/regicide/types"; + +export const prompts = { + playCard: createPromptDef<[PlayerType, string]>( + 'play ', + '选择要打出的卡牌' + ), + pass: createPromptDef<[PlayerType]>( + 'pass ', + '让过不出牌' + ), + discard: createPromptDef<[PlayerType, string[]]>( + 'discard ', + '选择要弃掉的卡牌' + ), +}; diff --git a/src/samples/regicide/state.ts b/src/samples/regicide/state.ts new file mode 100644 index 0000000..743830b --- /dev/null +++ b/src/samples/regicide/state.ts @@ -0,0 +1,38 @@ +import {createRegion, Region} from "@/core/region"; +import {Enemy, GamePhase, PlayerType, RegionType} from "@/samples/regicide/types"; +import {RegicideCard} from "@/samples/regicide/types"; +import {INITIAL_HAND_SIZE} from "@/samples/regicide/constants"; + +export function createInitialState() { + const regions: Record = { + enemyDeck: createRegion('enemyDeck', []), + tavernDeck: createRegion('tavernDeck', []), + discardPile: createRegion('discardPile', []), + currentEnemy: createRegion('currentEnemy', []), + hand_player1: createRegion('hand_player1', []), + hand_player2: createRegion('hand_player2', []), + hand_player3: createRegion('hand_player3', []), + hand_player4: createRegion('hand_player4', []), + }; + + const playerHands: Record = { + player1: [], + player2: [], + player3: [], + player4: [], + }; + + return { + regions, + cards: {} as Record, + playerHands, + currentPlayerIndex: 0, + playerCount: 1, + currentEnemy: null as Enemy | null, + enemyDeck: [] as Enemy[], + phase: 'playing' as GamePhase, + winner: null as boolean | null, + }; +} + +export type RegicideState = ReturnType; diff --git a/src/samples/regicide/types.ts b/src/samples/regicide/types.ts new file mode 100644 index 0000000..a841f22 --- /dev/null +++ b/src/samples/regicide/types.ts @@ -0,0 +1,44 @@ +import {Part} from "@/core/part"; + +// 花色类型 +export type SuitType = 'spades' | 'hearts' | 'diamonds' | 'clubs'; + +// 牌面值 +export type CardRank = 'A' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | 'J' | 'Q' | 'K'; + +// 玩家类型(1-4人合作) +export type PlayerType = 'player1' | 'player2' | 'player3' | 'player4'; + +// 区域类型 +export type RegionType = + | 'enemyDeck' // 敌人牌堆 + | 'tavernDeck' // 酒馆牌堆 + | 'discardPile' // 弃牌堆 + | 'currentEnemy' // 当前敌人 + | 'hand_player1' + | 'hand_player2' + | 'hand_player3' + | 'hand_player4'; + +// 卡牌元数据 +export type RegicideCardMeta = { + suit: SuitType; + rank: CardRank; + value: number; // 卡牌面值 +}; + +// 卡牌部件类型 +export type RegicideCard = Part; + +// 敌人类型 +export type Enemy = { + id: string; + rank: CardRank; + suit: SuitType; + value: number; // 敌人面值(即攻击力) + hp: number; // 当前生命值 + maxHp: number; // 最大生命值(面值的2倍) +}; + +// 游戏阶段 +export type GamePhase = 'playing' | 'victory' | 'defeat'; diff --git a/src/samples/regicide/utils.ts b/src/samples/regicide/utils.ts new file mode 100644 index 0000000..baea100 --- /dev/null +++ b/src/samples/regicide/utils.ts @@ -0,0 +1,132 @@ +import {ALL_RANKS, ALL_SUITS, CARD_VALUES, ENEMY_COUNT, FACE_CARDS} from "@/samples/regicide/constants"; +import {CardRank, Enemy, RegicideCard, SuitType} from "@/samples/regicide/types"; +import {RNG} from "@/utils/rng"; + +/** + * 获取卡牌面值 + */ +export function getCardValue(rank: CardRank): number { + return CARD_VALUES[rank]; +} + +/** + * 创建卡牌部件 + */ +export function createCard(id: string, suit: SuitType, rank: CardRank): RegicideCard { + return { + id, + regionId: '', + position: [], + suit, + rank, + value: getCardValue(rank), + }; +} + +/** + * 创建敌人 + */ +export function createEnemy(id: string, rank: CardRank, suit: SuitType): Enemy { + const value = getCardValue(rank); + return { + id, + rank, + suit, + value, + hp: value * 2, + maxHp: value * 2, + }; +} + +/** + * 生成完整卡牌列表(52张,不含大小王) + */ +export function createAllCards(): Record { + const cards: Record = {}; + + for (const suit of ALL_SUITS) { + for (const rank of ALL_RANKS) { + const id = `${suit}_${rank}`; + cards[id] = createCard(id, suit, rank); + } + } + + return cards; +} + +/** + * 构建敌人牌堆(J/Q/K 共12张) + * 规则:4张K在最下,4张Q在中间,4张J在最上 + */ +export function buildEnemyDeck(rng: RNG): Enemy[] { + const enemies: Enemy[] = []; + let idCounter = 0; + + // 创建所有J/Q/K敌人 + for (const rank of FACE_CARDS) { + for (const suit of ALL_SUITS) { + const id = `enemy_${idCounter++}`; + enemies.push(createEnemy(id, rank, suit)); + } + } + + // 分离J、Q、K + const jEnemies = enemies.filter(e => e.rank === 'J'); + const qEnemies = enemies.filter(e => e.rank === 'Q'); + const kEnemies = enemies.filter(e => e.rank === 'K'); + + // 分别洗牌 + shuffleArray(jEnemies, rng); + shuffleArray(qEnemies, rng); + shuffleArray(kEnemies, rng); + + // J在最上(先遇到),Q在中间,K在最下 + return [...jEnemies, ...qEnemies, ...kEnemies]; +} + +/** + * 构建酒馆牌堆(移除J/Q/K后的剩余牌) + */ +export function buildTavernDeck(rng: RNG): RegicideCard[] { + const allCards = createAllCards(); + const numberCards = Object.values(allCards).filter(card => !FACE_CARDS.includes(card.rank)); + + shuffleArray(numberCards, rng); + + return numberCards; +} + +/** + * 洗牌辅助函数 + */ +function shuffleArray(array: T[], rng: RNG): void { + for (let i = array.length - 1; i > 0; i--) { + const j = rng.nextInt(i + 1); + [array[i], array[j]] = [array[j], array[i]]; + } +} + +/** + * 从牌堆抽牌 + */ +export function drawFromDeck( + deck: RegicideCard[], + count: number +): RegicideCard[] { + const drawn = deck.splice(0, count); + return drawn; +} + +/** + * 检查敌人是否被击败 + */ +export function isEnemyDefeated(enemy: Enemy | null): boolean { + return enemy !== null && enemy.hp <= 0; +} + +/** + * 获取当前玩家手牌区域ID + */ +export function getPlayerHandRegionId(playerId: string): string { + return `hand_${playerId}`; +}