598 lines
24 KiB
Go
598 lines
24 KiB
Go
package main
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
)
|
||
|
||
// Phase — фаза текущего хода игрока.
|
||
type Phase int
|
||
|
||
const (
|
||
// PhaseDraw — игрок должен взять карту (из колоды или сброса)
|
||
// либо объявить Тонк, если это позволяет вес его руки.
|
||
PhaseDraw Phase = iota
|
||
// PhaseAction — игрок уже взял карту; может выкладывать
|
||
// комбинации, подкладывать карты к столу и в конце обязан
|
||
// сбросить одну карту.
|
||
PhaseAction
|
||
// PhaseGameOver — раунд завершён.
|
||
PhaseGameOver
|
||
)
|
||
|
||
// MeldKind различает тип выложенной на стол комбинации.
|
||
type MeldKind int
|
||
|
||
const (
|
||
KindSet MeldKind = iota
|
||
KindSequence
|
||
)
|
||
|
||
// TableMeld — комбинация, выложенная на стол. Владелец фиксируется
|
||
// тем, кто её впервые выложил, но подкладывать карты может любой
|
||
// игрок.
|
||
type TableMeld struct {
|
||
Owner int
|
||
Kind MeldKind
|
||
Cards []Card
|
||
}
|
||
|
||
var (
|
||
ErrWrongPhase = errors.New("недопустимое действие для текущей фазы хода")
|
||
ErrDeckExhausted = errors.New("колода и сброс исчерпаны — раунд закончен вничью")
|
||
ErrInvalidMeld = errors.New("карты не образуют валидную комбинацию")
|
||
ErrCardNotInHand = errors.New("карты нет на руке игрока")
|
||
ErrMeldNotFound = errors.New("комбинация на столе не найдена")
|
||
ErrCannotExtend = errors.New("карту нельзя подложить к этой комбинации")
|
||
ErrTonkNotAllowed = errors.New("вес руки превышает порог объявления Тонка")
|
||
ErrDiscardPileEmpty = errors.New("стопка сброса пуста")
|
||
)
|
||
|
||
// DefaultTonkThreshold — по умолчанию Тонк можно объявить, если
|
||
// сумма очков карт на руке не превышает 5. Это игровое правило
|
||
// (не связано с деньгами) — вес руки, при котором вообще
|
||
// разрешено объявить Тонк.
|
||
const DefaultTonkThreshold = 5
|
||
|
||
// Денежные параметры по умолчанию: фиксированная ставка (анте) в
|
||
// банк раунда с каждого игрока, стартовый капитал и множитель
|
||
// банка при победе через объявление Тонка.
|
||
const (
|
||
DefaultStartingCapital = 100
|
||
DefaultAnte = 5
|
||
DefaultTonkMultiplier = 2
|
||
)
|
||
|
||
// RoundResult — итог завершившегося раунда.
|
||
type RoundResult struct {
|
||
DeadRound bool // колода и сброс исчерпаны, банк возвращён игрокам
|
||
TonkCall bool // раунд завершён объявлением Тонка
|
||
DropCall bool // раунд завершён объявлением дропа
|
||
DropCaught bool // дроп был неудачным (объявившего "поймали")
|
||
Winner int // индекс победителя (не имеет смысла при DeadRound)
|
||
Pot int // размер банка на момент завершения раунда
|
||
BalanceDeltas []int // изменение капитала каждого игрока за этот раунд
|
||
}
|
||
|
||
// GameState — полное состояние партии в Тонк.
|
||
type GameState struct {
|
||
Players []*PlayerState
|
||
Deck *Deck
|
||
DiscardPile []Card // последний элемент — верхняя карта
|
||
Table []TableMeld
|
||
CurrentPlayer int
|
||
Phase Phase
|
||
TonkThreshold int
|
||
Result *RoundResult // заполняется, когда Phase == PhaseGameOver
|
||
|
||
// Денежная часть игры: фиксированная ставка с каждого игрока
|
||
// уходит в банк раунда; обычная победа забирает банк целиком,
|
||
// победа через Тонк — банк, умноженный на TonkMultiplier
|
||
// (недостающую часть доплачивают проигравшие сверх анте).
|
||
AnteAmount int
|
||
TonkMultiplier int
|
||
Pot int
|
||
|
||
// StartingCapital сохраняется отдельно от текущего капитала
|
||
// игроков — это сумма, с которой садится за стол новый игрок,
|
||
// в том числе бот, заменяющий обанкротившегося (см. пакетный
|
||
// уровень, функцию checkBankruptcies в tui.go).
|
||
StartingCapital int
|
||
|
||
// RoundsPlayed — сколько раундов уже сыграно, включая текущий.
|
||
// Нужно только для правила сторимода "минимум 3 раунда" — на
|
||
// обычную игру никак не влияет.
|
||
RoundsPlayed int
|
||
|
||
// preRoundBalance — капитал каждого игрока непосредственно
|
||
// перед списанием анте в начале раунда. Используется только
|
||
// для вычисления BalanceDeltas в RoundResult (чтобы показать
|
||
// игроку итог всего раунда целиком, а не отдельно анте и
|
||
// отдельно выигрыш).
|
||
preRoundBalance []int
|
||
|
||
// noProgressTurns считает подряд идущие завершённые ходы, за
|
||
// которые никто не выложил и не подложил ни одной карты в
|
||
// комбинацию на столе. Без такого предохранителя игроки без
|
||
// стратегии объявления Тонка теоретически могут застрять,
|
||
// бесконечно беря и сбрасывая одну и ту же лишнюю карту —
|
||
// такой раунд принудительно завершается вничью.
|
||
noProgressTurns int
|
||
}
|
||
|
||
// maxNoProgressTurns — порог принудительного завершения "мёртвого"
|
||
// раунда, если долгое время не происходит никакого прогресса по
|
||
// комбинациям на столе.
|
||
const maxNoProgressTurns = 400
|
||
|
||
// NewGame создаёт новую партию со стандартными денежными
|
||
// параметрами (см. DefaultStartingCapital, DefaultAnte,
|
||
// DefaultTonkMultiplier). humanIdx — индекс живого игрока (может
|
||
// быть -1, если играют только боты, например для симуляций).
|
||
func NewGame(names []string, humanIdx int) *GameState {
|
||
return NewGameWithStakes(names, humanIdx, DefaultStartingCapital, DefaultAnte, DefaultTonkMultiplier)
|
||
}
|
||
|
||
// NewGameWithStakes — то же самое, что NewGame, но с явно заданными
|
||
// денежными параметрами: стартовый капитал каждого игрока, размер
|
||
// фиксированной ставки (анте) в банк раунда и множитель банка при
|
||
// победе через Тонк.
|
||
func NewGameWithStakes(names []string, humanIdx, startingCapital, ante, tonkMultiplier int) *GameState {
|
||
isHuman := make([]bool, len(names))
|
||
if humanIdx >= 0 && humanIdx < len(names) {
|
||
isHuman[humanIdx] = true
|
||
}
|
||
balances := make([]int, len(names))
|
||
for i := range balances {
|
||
balances[i] = startingCapital
|
||
}
|
||
return newRound(names, isHuman, balances, DefaultTonkThreshold, startingCapital, ante, tonkMultiplier)
|
||
}
|
||
|
||
// NextRound начинает новый раунд партии, сохраняя капитал игроков
|
||
// (и то, кто из них человек) из завершившегося раунда g. Ставка
|
||
// (анте) списывается заново из перенесённого капитала. Если перед
|
||
// вызовом NextRound в g.Players были подменены обанкротившиеся
|
||
// игроки (см. checkBankruptcies в tui.go), новый раунд стартует уже
|
||
// с обновлённым составом.
|
||
func (g *GameState) NextRound() *GameState {
|
||
names := make([]string, len(g.Players))
|
||
isHuman := make([]bool, len(g.Players))
|
||
balances := make([]int, len(g.Players))
|
||
for i, p := range g.Players {
|
||
names[i] = p.Name
|
||
isHuman[i] = p.IsHuman
|
||
balances[i] = p.Balance
|
||
}
|
||
next := newRound(names, isHuman, balances, g.TonkThreshold, g.StartingCapital, g.AnteAmount, g.TonkMultiplier)
|
||
next.RoundsPlayed = g.RoundsPlayed + 1
|
||
return next
|
||
}
|
||
|
||
// newRound — общая внутренняя логика раздачи новой партии/раунда:
|
||
// тасует колоду, раздаёт по 5 карт, вскрывает первую карту сброса
|
||
// и собирает анте в банк.
|
||
func newRound(names []string, isHuman []bool, balances []int, tonkThreshold, startingCapital, ante, tonkMultiplier int) *GameState {
|
||
deck := NewDeck()
|
||
deck.Shuffle()
|
||
|
||
players := make([]*PlayerState, len(names))
|
||
for i, name := range names {
|
||
players[i] = &PlayerState{
|
||
Name: name,
|
||
Hand: deck.DrawN(5),
|
||
IsHuman: isHuman[i],
|
||
Balance: balances[i],
|
||
}
|
||
}
|
||
|
||
firstDiscard, _ := deck.Draw()
|
||
|
||
g := &GameState{
|
||
Players: players,
|
||
Deck: deck,
|
||
DiscardPile: []Card{firstDiscard},
|
||
CurrentPlayer: 0,
|
||
Phase: PhaseDraw,
|
||
TonkThreshold: tonkThreshold,
|
||
AnteAmount: ante,
|
||
TonkMultiplier: tonkMultiplier,
|
||
StartingCapital: startingCapital,
|
||
RoundsPlayed: 1,
|
||
}
|
||
g.collectAnte()
|
||
return g
|
||
}
|
||
|
||
// collectAnte списывает фиксированную ставку с каждого игрока в
|
||
// банк раунда. Капитал может уйти в минус, если у игрока не
|
||
// хватает средств на анте — это сознательное упрощение (реальный
|
||
// долг между игроками здесь не моделируется).
|
||
func (g *GameState) collectAnte() {
|
||
g.preRoundBalance = make([]int, len(g.Players))
|
||
for i, p := range g.Players {
|
||
g.preRoundBalance[i] = p.Balance
|
||
p.Balance -= g.AnteAmount
|
||
g.Pot += g.AnteAmount
|
||
}
|
||
}
|
||
|
||
func (g *GameState) current() *PlayerState {
|
||
return g.Players[g.CurrentPlayer]
|
||
}
|
||
|
||
// PeekDiscard возвращает верхнюю карту стопки сброса.
|
||
func (g *GameState) PeekDiscard() (Card, bool) {
|
||
if len(g.DiscardPile) == 0 {
|
||
return Card{}, false
|
||
}
|
||
return g.DiscardPile[len(g.DiscardPile)-1], true
|
||
}
|
||
|
||
// reshuffleDiscardIntoDeck перекладывает стопку сброса (кроме
|
||
// верхней карты) обратно в колоду и тасует её — стандартный приём
|
||
// раммі-игр, когда колода добора заканчивается.
|
||
func (g *GameState) reshuffleDiscardIntoDeck() {
|
||
if len(g.DiscardPile) <= 1 {
|
||
return
|
||
}
|
||
top := g.DiscardPile[len(g.DiscardPile)-1]
|
||
rest := g.DiscardPile[:len(g.DiscardPile)-1]
|
||
g.Deck.cards = append(g.Deck.cards, rest...)
|
||
g.Deck.Shuffle()
|
||
g.DiscardPile = []Card{top}
|
||
}
|
||
|
||
// DrawFromDeck — взять карту из колоды добора. Если колода пуста,
|
||
// сначала пытается переложить сброс в колоду; если и это
|
||
// невозможно, раунд завершается вничью.
|
||
func (g *GameState) DrawFromDeck() (Card, error) {
|
||
if g.Phase != PhaseDraw {
|
||
return Card{}, ErrWrongPhase
|
||
}
|
||
if g.Deck.Len() == 0 {
|
||
g.reshuffleDiscardIntoDeck()
|
||
}
|
||
card, ok := g.Deck.Draw()
|
||
if !ok {
|
||
g.endDeadRound()
|
||
return Card{}, ErrDeckExhausted
|
||
}
|
||
g.current().Hand = append(g.current().Hand, card)
|
||
g.Phase = PhaseAction
|
||
return card, nil
|
||
}
|
||
|
||
// DrawFromDiscard — взять верхнюю карту из стопки сброса.
|
||
func (g *GameState) DrawFromDiscard() (Card, error) {
|
||
if g.Phase != PhaseDraw {
|
||
return Card{}, ErrWrongPhase
|
||
}
|
||
card, ok := g.PeekDiscard()
|
||
if !ok {
|
||
return Card{}, ErrDiscardPileEmpty
|
||
}
|
||
g.DiscardPile = g.DiscardPile[:len(g.DiscardPile)-1]
|
||
g.current().Hand = append(g.current().Hand, card)
|
||
g.Phase = PhaseAction
|
||
return card, nil
|
||
}
|
||
|
||
// DeclareTonk — объявить Тонк. Разрешено только сразу после взятия
|
||
// карты (в фазе PhaseAction), до выкладывания комбинаций, и только
|
||
// если вес руки не превышает TonkThreshold.
|
||
func (g *GameState) DeclareTonk() (*RoundResult, error) {
|
||
if g.Phase != PhaseAction {
|
||
return nil, ErrWrongPhase
|
||
}
|
||
if !CanDeclareTonk(g.current().Hand, g.TonkThreshold) {
|
||
return nil, ErrTonkNotAllowed
|
||
}
|
||
return g.endByTonk(g.CurrentPlayer), nil
|
||
}
|
||
|
||
// Drop — объявить дроп: рискованная заявка "у меня самая низкая
|
||
// сумма очков за столом", доступная в НАЧАЛЕ хода, до взятия карты
|
||
// (в отличие от Тонка, порог веса руки не требуется — можно
|
||
// дропнуть с любой рукой). Если объявивший действительно строго
|
||
// ниже всех остальных — он забирает банк как за обычную победу
|
||
// (без множителя). Если нет — он "попался": банк достаётся
|
||
// игроку(ам) с реально самой низкой суммой, а сам объявивший вдобавок
|
||
// платит им штраф в размере ещё одной ставки (анте) сверху.
|
||
func (g *GameState) Drop() (*RoundResult, error) {
|
||
if g.Phase != PhaseDraw {
|
||
return nil, ErrWrongPhase
|
||
}
|
||
return g.endByDrop(g.CurrentPlayer), nil
|
||
}
|
||
|
||
// LayNewMeld выкладывает новую комбинацию на стол из карт руки
|
||
// текущего игрока.
|
||
func (g *GameState) LayNewMeld(cards []Card) error {
|
||
if g.Phase != PhaseAction {
|
||
return ErrWrongPhase
|
||
}
|
||
for _, c := range cards {
|
||
if !g.current().HasCard(c) {
|
||
return ErrCardNotInHand
|
||
}
|
||
}
|
||
var kind MeldKind
|
||
switch {
|
||
case IsSet(cards):
|
||
kind = KindSet
|
||
case IsSequence(cards):
|
||
kind = KindSequence
|
||
default:
|
||
return ErrInvalidMeld
|
||
}
|
||
// копируем ДО удаления из руки: если вызывающий код передал сам
|
||
// срез p.Hand (а не его копию), RemoveCards будет мутировать тот
|
||
// же массив в памяти, и cards станет невалидным
|
||
cardsCopy := append([]Card{}, cards...)
|
||
g.current().RemoveCards(cardsCopy)
|
||
g.Table = append(g.Table, TableMeld{
|
||
Owner: g.CurrentPlayer,
|
||
Kind: kind,
|
||
Cards: cardsCopy,
|
||
})
|
||
g.noProgressTurns = 0
|
||
g.checkEmptyHandWin()
|
||
return nil
|
||
}
|
||
|
||
// AddToMeld подкладывает одну карту руки текущего игрока к уже
|
||
// выложенной на столе комбинации (своей или чужой).
|
||
func (g *GameState) AddToMeld(meldIdx int, card Card) error {
|
||
if g.Phase != PhaseAction {
|
||
return ErrWrongPhase
|
||
}
|
||
if meldIdx < 0 || meldIdx >= len(g.Table) {
|
||
return ErrMeldNotFound
|
||
}
|
||
if !g.current().HasCard(card) {
|
||
return ErrCardNotInHand
|
||
}
|
||
meld := g.Table[meldIdx]
|
||
trial := append(append([]Card{}, meld.Cards...), card)
|
||
|
||
valid := false
|
||
switch meld.Kind {
|
||
case KindSet:
|
||
valid = IsSet(trial)
|
||
case KindSequence:
|
||
valid = IsSequence(trial)
|
||
}
|
||
if !valid {
|
||
return ErrCannotExtend
|
||
}
|
||
|
||
g.current().RemoveCard(card)
|
||
g.Table[meldIdx].Cards = trial
|
||
g.noProgressTurns = 0
|
||
g.checkEmptyHandWin()
|
||
return nil
|
||
}
|
||
|
||
// Discard сбрасывает карту и завершает ход текущего игрока.
|
||
func (g *GameState) Discard(card Card) error {
|
||
if g.Phase != PhaseAction {
|
||
return ErrWrongPhase
|
||
}
|
||
if g.Phase == PhaseGameOver {
|
||
return ErrWrongPhase
|
||
}
|
||
if !g.current().HasCard(card) {
|
||
return ErrCardNotInHand
|
||
}
|
||
g.current().RemoveCard(card)
|
||
g.DiscardPile = append(g.DiscardPile, card)
|
||
|
||
if len(g.current().Hand) == 0 {
|
||
g.endByEmptyHand(g.CurrentPlayer)
|
||
return nil
|
||
}
|
||
|
||
g.noProgressTurns++
|
||
if g.noProgressTurns >= maxNoProgressTurns {
|
||
g.endDeadRound()
|
||
return nil
|
||
}
|
||
|
||
g.advanceTurn()
|
||
return nil
|
||
}
|
||
|
||
// checkEmptyHandWin проверяет, не остался ли текущий игрок без
|
||
// карт сразу после выкладывания/подкладывания комбинации — в этом
|
||
// случае раунд заканчивается его победой без необходимости сброса.
|
||
func (g *GameState) checkEmptyHandWin() {
|
||
if g.Phase == PhaseGameOver {
|
||
return
|
||
}
|
||
if len(g.current().Hand) == 0 {
|
||
g.endByEmptyHand(g.CurrentPlayer)
|
||
}
|
||
}
|
||
|
||
func (g *GameState) advanceTurn() {
|
||
g.CurrentPlayer = (g.CurrentPlayer + 1) % len(g.Players)
|
||
g.Phase = PhaseDraw
|
||
}
|
||
|
||
// endByEmptyHand завершает раунд победой игрока winner, полностью
|
||
// избавившегося от карт. Победитель забирает весь банк раунда
|
||
// (сумму ставок всех игроков); остальные при этом уже заплатили
|
||
// свою часть банка в виде анте в начале раунда.
|
||
func (g *GameState) endByEmptyHand(winner int) {
|
||
g.Players[winner].Balance += g.Pot
|
||
pot := g.Pot
|
||
g.Pot = 0
|
||
|
||
g.Result = &RoundResult{
|
||
Winner: winner,
|
||
Pot: pot,
|
||
BalanceDeltas: g.balanceDeltasFromPreRound(),
|
||
}
|
||
g.Phase = PhaseGameOver
|
||
}
|
||
|
||
// endByTonk завершает раунд объявлением Тонка игроком declarer.
|
||
// Объявивший забирает банк, умноженный на TonkMultiplier: помимо
|
||
// уже собранного банка, каждый из остальных игроков доплачивает
|
||
// поровну недостающую разницу (округление в меньшую сторону —
|
||
// остаток от деления забирает победитель, чтобы сумма выплат
|
||
// сходилась без потерь).
|
||
func (g *GameState) endByTonk(declarer int) *RoundResult {
|
||
pot := g.Pot
|
||
totalPayout := pot * g.TonkMultiplier
|
||
extra := totalPayout - pot
|
||
|
||
losers := make([]int, 0, len(g.Players)-1)
|
||
for i := range g.Players {
|
||
if i != declarer {
|
||
losers = append(losers, i)
|
||
}
|
||
}
|
||
actualExtra := 0
|
||
if len(losers) > 0 {
|
||
share := extra / len(losers)
|
||
remainder := extra % len(losers)
|
||
for idx, i := range losers {
|
||
deduction := share
|
||
if idx < remainder {
|
||
// остаток от целочисленного деления распределяем по
|
||
// первым проигравшим, чтобы сумма выплат сходилась
|
||
// с точностью до копейки, а не терялась в округлении
|
||
deduction++
|
||
}
|
||
g.Players[i].Balance -= deduction
|
||
actualExtra += deduction
|
||
}
|
||
}
|
||
g.Players[declarer].Balance += pot + actualExtra
|
||
g.Pot = 0
|
||
|
||
g.Result = &RoundResult{
|
||
TonkCall: true,
|
||
Winner: declarer,
|
||
Pot: pot,
|
||
BalanceDeltas: g.balanceDeltasFromPreRound(),
|
||
}
|
||
g.Phase = PhaseGameOver
|
||
return g.Result
|
||
}
|
||
|
||
// endByDrop разрешает объявление дропа игроком dropper: сравнивает
|
||
// вес его руки с весами рук всех остальных игроков.
|
||
// - Если dropper строго ниже всех — он забирает банк как за
|
||
// обычную победу (без множителя).
|
||
// - Если нет — банк достаётся игроку(ам) с реально самой низкой
|
||
// суммой (при нескольких таких игроках банк делится поровну), а
|
||
// dropper вдобавок платит им штраф в размере ещё одной ставки
|
||
// (анте) сверху, разделённой между ними так же поровну.
|
||
func (g *GameState) endByDrop(dropper int) *RoundResult {
|
||
values := make([]int, len(g.Players))
|
||
minValue := g.Players[dropper].HandValue()
|
||
for i, p := range g.Players {
|
||
values[i] = p.HandValue()
|
||
if values[i] < minValue {
|
||
minValue = values[i]
|
||
}
|
||
}
|
||
|
||
var lowest []int
|
||
for i, v := range values {
|
||
if v == minValue {
|
||
lowest = append(lowest, i)
|
||
}
|
||
}
|
||
|
||
pot := g.Pot
|
||
result := &RoundResult{DropCall: true, Pot: pot}
|
||
|
||
if len(lowest) == 1 && lowest[0] == dropper {
|
||
// успешный дроп: объявивший действительно строго ниже всех
|
||
g.Players[dropper].Balance += pot
|
||
result.Winner = dropper
|
||
} else {
|
||
// дроп не удался — "попался"; банк и штраф достаются
|
||
// реальным обладателям самой низкой суммы (dropper среди них
|
||
// быть не может: либо его сумма не минимальна, либо она
|
||
// минимальна, но не единственна — в обоих случаях это не
|
||
// строго меньше всех остальных)
|
||
catchers := make([]int, 0, len(lowest))
|
||
for _, i := range lowest {
|
||
if i != dropper {
|
||
catchers = append(catchers, i)
|
||
}
|
||
}
|
||
penalty := g.AnteAmount
|
||
if len(catchers) > 0 {
|
||
potShare := pot / len(catchers)
|
||
potRemainder := pot % len(catchers)
|
||
penaltyShare := penalty / len(catchers)
|
||
penaltyRemainder := penalty % len(catchers)
|
||
totalPenaltyPaid := 0
|
||
for idx, i := range catchers {
|
||
share := potShare
|
||
if idx < potRemainder {
|
||
share++
|
||
}
|
||
pShare := penaltyShare
|
||
if idx < penaltyRemainder {
|
||
pShare++
|
||
}
|
||
g.Players[i].Balance += share + pShare
|
||
totalPenaltyPaid += pShare
|
||
}
|
||
g.Players[dropper].Balance -= totalPenaltyPaid
|
||
}
|
||
result.Winner = catchers[0]
|
||
result.DropCaught = true
|
||
}
|
||
|
||
g.Pot = 0
|
||
result.BalanceDeltas = g.balanceDeltasFromPreRound()
|
||
g.Result = result
|
||
g.Phase = PhaseGameOver
|
||
return result
|
||
}
|
||
|
||
// endDeadRound завершает раунд без победителя — колода и сброс
|
||
// исчерпаны. Банк возвращается игрокам поровну (каждому — его
|
||
// анте назад), чтобы никто не терял деньги в раунде без вины.
|
||
func (g *GameState) endDeadRound() {
|
||
for _, p := range g.Players {
|
||
p.Balance += g.AnteAmount
|
||
}
|
||
pot := g.Pot
|
||
g.Pot = 0
|
||
|
||
g.Result = &RoundResult{
|
||
DeadRound: true,
|
||
Pot: pot,
|
||
BalanceDeltas: g.balanceDeltasFromPreRound(),
|
||
}
|
||
g.Phase = PhaseGameOver
|
||
}
|
||
|
||
// balanceDeltasFromPreRound считает итоговое изменение капитала
|
||
// каждого игрока за весь раунд целиком — от капитала до списания
|
||
// анте и до текущего момента после выплат. Это удобнее для
|
||
// отображения игроку, чем показывать анте и выигрыш раздельно.
|
||
func (g *GameState) balanceDeltasFromPreRound() []int {
|
||
deltas := make([]int, len(g.Players))
|
||
for i, p := range g.Players {
|
||
deltas[i] = p.Balance - g.preRoundBalance[i]
|
||
}
|
||
return deltas
|
||
}
|
||
|
||
// String — компактное текстовое представление состояния стола,
|
||
// полезно для отладки и логирования симуляций.
|
||
func (g *GameState) String() string {
|
||
top, _ := g.PeekDiscard()
|
||
return fmt.Sprintf("ход: %s | фаза: %d | сброс сверху: %s | колода: %d карт",
|
||
g.current().Name, g.Phase, top, g.Deck.Len())
|
||
}
|