M1: ядро правил дурака (чистый TypeScript, без движка)
Детерминированный конечный автомат: apply(state, action) -> {state, events}.
Весь набор правил из GAMEPLAY.md проверяется headless-тестами.
- карты: Suit/Rank/Card (флайвейт, 36 штук), колода
- GameState + клонирование и сериализация в JSON
- действия (attack/defend/take/pass) и события для слоя отображения
- раздача, выбор первого атакующего, лимиты подкидывания,
взятие карт, добор, выбывание игроков, определение дурака
- 64 теста, strict typecheck
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { Card } from '../cards/Card';
|
||||
import { PlayerId } from './GameState';
|
||||
|
||||
export enum ActionType {
|
||||
/** Lay an attacking card — either the opening card of a turn or an addition. */
|
||||
Attack = 'attack',
|
||||
/** Beat one attacking card. */
|
||||
Defend = 'defend',
|
||||
/** Give up the defence and pick the whole table up. */
|
||||
Take = 'take',
|
||||
/** Declare that nothing more will be added this turn. */
|
||||
Pass = 'pass',
|
||||
}
|
||||
|
||||
export interface AttackAction {
|
||||
readonly type: ActionType.Attack;
|
||||
readonly player: PlayerId;
|
||||
readonly card: Card;
|
||||
}
|
||||
|
||||
export interface DefendAction {
|
||||
readonly type: ActionType.Defend;
|
||||
readonly player: PlayerId;
|
||||
readonly card: Card;
|
||||
/** Index into `GameState.table` of the slot being beaten. */
|
||||
readonly slot: number;
|
||||
}
|
||||
|
||||
export interface TakeAction {
|
||||
readonly type: ActionType.Take;
|
||||
readonly player: PlayerId;
|
||||
}
|
||||
|
||||
export interface PassAction {
|
||||
readonly type: ActionType.Pass;
|
||||
readonly player: PlayerId;
|
||||
}
|
||||
|
||||
export type Action = AttackAction | DefendAction | TakeAction | PassAction;
|
||||
|
||||
export const Actions = {
|
||||
attack(player: PlayerId, card: Card): AttackAction {
|
||||
return { type: ActionType.Attack, player, card };
|
||||
},
|
||||
defend(player: PlayerId, card: Card, slot: number): DefendAction {
|
||||
return { type: ActionType.Defend, player, card, slot };
|
||||
},
|
||||
take(player: PlayerId): TakeAction {
|
||||
return { type: ActionType.Take, player };
|
||||
},
|
||||
pass(player: PlayerId): PassAction {
|
||||
return { type: ActionType.Pass, player };
|
||||
},
|
||||
};
|
||||
|
||||
export function describeAction(action: Action): string {
|
||||
switch (action.type) {
|
||||
case ActionType.Attack: return `${action.player} attacks with ${action.card.code}`;
|
||||
case ActionType.Defend: return `${action.player} beats slot ${action.slot} with ${action.card.code}`;
|
||||
case ActionType.Take: return `${action.player} takes`;
|
||||
case ActionType.Pass: return `${action.player} passes`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Thrown when an action is not permitted in the current state. */
|
||||
export class IllegalActionError extends Error {
|
||||
constructor(action: Action, reason: string) {
|
||||
super(`Illegal action (${describeAction(action)}): ${reason}`);
|
||||
this.name = 'IllegalActionError';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user