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>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user