- PlayerView: проекция состояния, в которой боту видно только то, что видно живому игроку (своя рука, стол, размеры чужих рук, счётчики) - BasicBot: политика из GAMEPLAY.md, полностью детерминированная - Match: обёртка с состоянием и подпиской на события (её же возьмёт UI) - runMatch: прогон матча без движка и без таймеров - soak-тесты: 3500 полных матчей (боты и случайные легальные ходы, 2-4 игрока), инварианты состояния проверяются после каждого действия - npm run stats:bot — распределение исходов самоигры Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
79 lines
2.9 KiB
TypeScript
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;
|
|
}
|