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:
ftc
2026-09-13 10:38:49 +03:00
co-authored by Claude Opus 5
parent c980440856
commit fddab60527
22 changed files with 1836 additions and 8 deletions
+36
View File
@@ -0,0 +1,36 @@
/** The four suits of a standard deck. */
export enum Suit {
Clubs = 0,
Diamonds = 1,
Hearts = 2,
Spades = 3,
}
export const ALL_SUITS: readonly Suit[] = [Suit.Clubs, Suit.Diamonds, Suit.Hearts, Suit.Spades];
const SUIT_SYMBOLS: Record<Suit, string> = {
[Suit.Clubs]: '\u2663',
[Suit.Diamonds]: '\u2666',
[Suit.Hearts]: '\u2665',
[Suit.Spades]: '\u2660',
};
/** Suit names as used by the card art file names. */
const SUIT_NAMES: Record<Suit, string> = {
[Suit.Clubs]: 'clubs',
[Suit.Diamonds]: 'diamonds',
[Suit.Hearts]: 'hearts',
[Suit.Spades]: 'spades',
};
export function suitSymbol(suit: Suit): string {
return SUIT_SYMBOLS[suit];
}
export function suitName(suit: Suit): string {
return SUIT_NAMES[suit];
}
export function isRedSuit(suit: Suit): boolean {
return suit === Suit.Diamonds || suit === Suit.Hearts;
}