Files
fool-cardgame/assets/scripts/core/rules/Action.ts
T
ftcandClaude Opus 5 fddab60527 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>
2026-09-13 10:38:49 +03:00

72 lines
2.2 KiB
TypeScript

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