feat: regicide code
This commit is contained in:
parent
8b271448d2
commit
28e548d3de
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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'];
|
||||
|
|
@ -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';
|
||||
|
|
@ -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[]>',
|
||||
'选择要弃掉的卡牌'
|
||||
),
|
||||
};
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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';
|
||||
|
|
@ -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张)
|
||||
* 规则: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<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}`;
|
||||
}
|
||||
Loading…
Reference in New Issue