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,57 @@
|
||||
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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user