Детерминированный конечный автомат: 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>
58 lines
2.9 KiB
TypeScript
58 lines
2.9 KiB
TypeScript
import { Card } from '../cards/Card';
|
|
import { PlayerId } from './GameState';
|
|
|
|
/**
|
|
* What happened, as opposed to what someone wanted to do. The presentation layer
|
|
* animates this stream; it is the only thing the view needs in order to stay in
|
|
* sync with the rules.
|
|
*
|
|
* The stream is a god's-eye view — `Dealt` and `Drew` carry real cards. It is
|
|
* the view's job to show only what the local player may see. Anything that
|
|
* genuinely must not know (a bot) reads a `PlayerView` instead.
|
|
*/
|
|
export enum GameEventType {
|
|
Dealt = 'dealt',
|
|
TrumpRevealed = 'trumpRevealed',
|
|
TurnStarted = 'turnStarted',
|
|
Attacked = 'attacked',
|
|
Defended = 'defended',
|
|
TookDeclared = 'tookDeclared',
|
|
Passed = 'passed',
|
|
TableDiscarded = 'tableDiscarded',
|
|
TableTaken = 'tableTaken',
|
|
Drew = 'drew',
|
|
PlayerOut = 'playerOut',
|
|
GameOver = 'gameOver',
|
|
}
|
|
|
|
export type GameEvent =
|
|
| { type: GameEventType.Dealt; player: PlayerId; cards: Card[] }
|
|
| { type: GameEventType.TrumpRevealed; card: Card }
|
|
| { type: GameEventType.TurnStarted; turnNumber: number; attacker: PlayerId; defender: PlayerId }
|
|
| { type: GameEventType.Attacked; player: PlayerId; card: Card; slot: number }
|
|
| { type: GameEventType.Defended; player: PlayerId; card: Card; slot: number }
|
|
| { type: GameEventType.TookDeclared; player: PlayerId }
|
|
| { type: GameEventType.Passed; player: PlayerId }
|
|
| { type: GameEventType.TableDiscarded; cards: Card[] }
|
|
| { type: GameEventType.TableTaken; player: PlayerId; cards: Card[] }
|
|
| { type: GameEventType.Drew; player: PlayerId; cards: Card[]; deckRemaining: number }
|
|
| { type: GameEventType.PlayerOut; player: PlayerId }
|
|
| { type: GameEventType.GameOver; loser: PlayerId | null };
|
|
|
|
export function describeEvent(event: GameEvent): string {
|
|
switch (event.type) {
|
|
case GameEventType.Dealt: return `${event.player} was dealt ${event.cards.length} cards`;
|
|
case GameEventType.TrumpRevealed: return `trump is ${event.card.code}`;
|
|
case GameEventType.TurnStarted: return `turn ${event.turnNumber}: ${event.attacker} attacks ${event.defender}`;
|
|
case GameEventType.Attacked: return `${event.player} played ${event.card.code}`;
|
|
case GameEventType.Defended: return `${event.player} beat slot ${event.slot} with ${event.card.code}`;
|
|
case GameEventType.TookDeclared: return `${event.player} declared a take`;
|
|
case GameEventType.Passed: return `${event.player} passed`;
|
|
case GameEventType.TableDiscarded: return `${event.cards.length} cards discarded`;
|
|
case GameEventType.TableTaken: return `${event.player} took ${event.cards.length} cards`;
|
|
case GameEventType.Drew: return `${event.player} drew ${event.cards.length} (${event.deckRemaining} left)`;
|
|
case GameEventType.PlayerOut: return `${event.player} is out`;
|
|
case GameEventType.GameOver: return event.loser ? `${event.loser} is the fool` : 'a draw';
|
|
}
|
|
}
|