feat: regicide code

This commit is contained in:
hyper 2026-04-10 13:24:29 +08:00
parent 8b271448d2
commit 28e548d3de
7 changed files with 426 additions and 0 deletions

View File

@ -0,0 +1,107 @@
import {createGameCommandRegistry} from "@/core/game";
import {RegicideState} from "@/samples/regicide/state";
export const registry = createGameCommandRegistry<RegicideState>();
/**
*
*/
const playCmd = registry.register({
schema: 'play <player:string> <cardId:string>',
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 <player:string>',
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,
};

View File

@ -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<CardRank, number> = {
'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'];

View File

@ -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';

View File

@ -0,0 +1,17 @@
import {createPromptDef} from "@/core/game";
import {PlayerType} from "@/samples/regicide/types";
export const prompts = {
playCard: createPromptDef<[PlayerType, string]>(
'play <player:string> <cardId:string>',
'选择要打出的卡牌'
),
pass: createPromptDef<[PlayerType]>(
'pass <player:string>',
'让过不出牌'
),
discard: createPromptDef<[PlayerType, string[]]>(
'discard <player:string> <cardIds:string[]>',
'选择要弃掉的卡牌'
),
};

View File

@ -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<RegionType, Region> = {
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<PlayerType, string[]> = {
player1: [],
player2: [],
player3: [],
player4: [],
};
return {
regions,
cards: {} as Record<string, RegicideCard>,
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<typeof createInitialState>;

View File

@ -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<RegicideCardMeta>;
// 敌人类型
export type Enemy = {
id: string;
rank: CardRank;
suit: SuitType;
value: number; // 敌人面值(即攻击力)
hp: number; // 当前生命值
maxHp: number; // 最大生命值面值的2倍
};
// 游戏阶段
export type GamePhase = 'playing' | 'victory' | 'defeat';

View File

@ -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<string, RegicideCard> {
const cards: Record<string, RegicideCard> = {};
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
* 4K在最下4Q在中间4J在最上
*/
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<T>(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}`;
}