Files
ftcandClaude Opus 5 c60fbcbb94 M2: бот и headless-прогон матчей
- PlayerView: проекция состояния, в которой боту видно только то,
  что видно живому игроку (своя рука, стол, размеры чужих рук, счётчики)
- BasicBot: политика из GAMEPLAY.md, полностью детерминированная
- Match: обёртка с состоянием и подпиской на события (её же возьмёт UI)
- runMatch: прогон матча без движка и без таймеров
- soak-тесты: 3500 полных матчей (боты и случайные легальные ходы,
  2-4 игрока), инварианты состояния проверяются после каждого действия
- npm run stats:bot — распределение исходов самоигры

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 10:43:06 +03:00

79 lines
2.9 KiB
TypeScript

import { Card } from '../cards/Card';
import { Suit } from '../cards/Suit';
import { Action } from '../rules/Action';
import { GameState, Phase, PlayerId, TableSlot } from '../rules/GameState';
import { currentActor, legalActions, seatOf } from '../rules/DurakRules';
/** What one player can see about another. */
export interface OpponentView {
readonly id: PlayerId;
readonly handSize: number;
readonly out: boolean;
}
/**
* Everything a participant may legitimately know, and nothing else.
*
* Bots are handed one of these rather than the `GameState`, which makes
* cheating structurally impossible instead of a matter of discipline. It is
* also, not coincidentally, the shape a network protocol would send to a client.
*/
export interface PlayerView {
readonly me: PlayerId;
readonly hand: readonly Card[];
/** Opponents in clockwise order, starting with the seat after `me`. */
readonly opponents: readonly OpponentView[];
readonly table: readonly TableSlot[];
readonly trumpSuit: Suit;
/** The face-up card that set the trump. Visible all game, even once drawn. */
readonly trumpCard: Card;
readonly deckRemaining: number;
/** True while the trump card is still the bottom card of the draw pile. */
readonly trumpCardStillInDeck: boolean;
readonly discardCount: number;
readonly phase: Phase;
readonly turnNumber: number;
readonly attacker: PlayerId;
readonly defender: PlayerId;
readonly defenderTook: boolean;
/** True when the game is waiting on this player. */
readonly onMove: boolean;
/** Everything this player may do right now. Empty unless `onMove`. */
readonly legalActions: readonly Action[];
}
export function projectView(state: GameState, player: PlayerId): PlayerView {
const seat = seatOf(state, player);
const count = state.players.length;
const opponents: OpponentView[] = [];
for (let i = 1; i < count; i++) {
const other = state.players[(seat + i) % count];
opponents.push({ id: other.id, handSize: other.hand.length, out: other.out });
}
return {
me: player,
hand: state.players[seat].hand.slice(),
opponents,
table: state.table.map((s) => ({ attack: s.attack, defence: s.defence })),
trumpSuit: state.trumpSuit,
trumpCard: state.trumpCard,
deckRemaining: state.deck.length,
trumpCardStillInDeck: state.deck.length > 0,
discardCount: state.discard.length,
phase: state.phase,
turnNumber: state.turnNumber,
attacker: state.players[state.attacker].id,
defender: state.players[state.defender].id,
defenderTook: state.defenderTook,
onMove: currentActor(state) === player,
legalActions: legalActions(state, player),
};
}
/** Convenience: is this card a trump, from a view's point of view. */
export function isTrump(view: PlayerView, card: Card): boolean {
return card.suit === view.trumpSuit;
}