698 lines
26 KiB
Go
698 lines
26 KiB
Go
package main
|
||
|
||
import "errors"
|
||
|
||
// ПРИМЕЧАНИЕ О ПРАВИЛАХ: у "Тысячи" очень много вариантов и
|
||
// договорённостей (бочка, золотой кон, самосвал, тузовый марьяж,
|
||
// игра втёмную, "расписать игру" и т.д.). Реализован базовый,
|
||
// самый распространённый вариант ИМЕННО НА ТРОИХ игроков (у игры
|
||
// вдвоём/вчетвером заметно другая механика прикупа и сдающего —
|
||
// сознательно не реализована). Набор правил:
|
||
// - Колода 24 карты (9,В,Д,К,10,Т в каждой масти). Старшинство:
|
||
// 9 < В < Д < К < 10 < Т (да, десятка сильнее короля, но
|
||
// слабее туза — особенность именно этой игры).
|
||
// - Раздача: по 7 карт каждому, 3 карты — в прикуп.
|
||
// - Торги: первый ходящий (слева от сдающего) обязан назвать
|
||
// не менее 100 очков. Далее по кругу каждый либо поднимает
|
||
// ставку (шаг 5, до 120 без марьяжа на руке, либо до
|
||
// 120+стоимость марьяжа, если марьяж есть), либо пасует.
|
||
// Спасовавший больше не участвует в этих торгах. Торги
|
||
// заканчиваются, когда остаётся один игрок — он берёт прикуп.
|
||
// - Победитель торгов забирает прикуп (10 карт на руке), отдаёт
|
||
// по одной карте каждому из двух соперников (по одной,
|
||
// соперникам становится по 8 карт, у него самого остаётся 8) и
|
||
// ходит первым.
|
||
// - Взятки: обязательно ходить в масть; если её нет — козырем
|
||
// (если он уже объявлен); если и его нет — чем угодно. Взятку
|
||
// берёт старшая карта хода, либо старший козырь, если он был
|
||
// подложен.
|
||
// - Марьяж: если у игрока, который сейчас ходит (не в первой
|
||
// взятке раздачи), есть на руке дама и король одной масти, он
|
||
// может объявить марьяж, походив с одной из этих карт — тогда
|
||
// эта масть становится (или меняется на) козырь до конца
|
||
// раздачи, а стоимость марьяжа сразу добавляется к очкам
|
||
// игрока за эту раздачу. Стоимость: пики 40, трефы 60, бубны
|
||
// 80, червы 100.
|
||
// - Подсчёт очков по итогам раздачи: если объявивший торг игрок
|
||
// набрал (взятками и марьяжами) не меньше своей ставки — ему
|
||
// засчитывается ставка целиком; если меньше — ставка вычитается
|
||
// из его счёта. Остальные добавляют себе ровно то, что
|
||
// заработали (взятки+марьяжи), без штрафов.
|
||
// - Партия идёт до 1000 очков. "Бочка", "золотой кон",
|
||
// "самосвал", тузовый марьяж и игра "втёмную" — не реализованы.
|
||
|
||
// ThousandPlayer — состояние одного игрока в партии 1000.
|
||
type ThousandPlayer struct {
|
||
Name string
|
||
Hand []Card
|
||
IsHuman bool
|
||
Score int // суммарный счёт по итогам сыгранных раздач
|
||
HandPoints int // очки ВЗЯТОК (без марьяжей), набранные в ТЕКУЩЕЙ раздаче
|
||
Passed bool
|
||
|
||
// HandMarriagePoints — очки марьяжей (обычных и тузового) за
|
||
// текущую раздачу, отдельно от HandPoints. Разделены специально:
|
||
// удвоение золотого кона и игры "в тёмную" марьяжей не касается —
|
||
// удваиваются только очки взяток (см. resolveHandEnd).
|
||
HandMarriagePoints int
|
||
AceMarriageDeclared bool // объявлен ли уже тузовый марьяж в текущей раздаче
|
||
|
||
// OnBarrel / BarrelAttempts — состояние "бочки" (см. applyBarrelRules).
|
||
OnBarrel bool
|
||
BarrelAttempts int
|
||
}
|
||
|
||
// ThousandPhase — фаза текущей раздачи.
|
||
type ThousandPhase int
|
||
|
||
const (
|
||
ThousandPhaseBidding ThousandPhase = iota
|
||
ThousandPhaseDiscard
|
||
ThousandPhaseTrick
|
||
ThousandPhaseHandOver
|
||
)
|
||
|
||
var (
|
||
ErrThousandWrongPhase = errors.New("недопустимое действие для текущей фазы")
|
||
ErrThousandCardNotInHand = errors.New("карты нет на руке игрока")
|
||
ErrThousandBidTooLow = errors.New("ставка должна быть выше текущей минимум на 5")
|
||
ErrThousandBlindTooLate = errors.New("играть втёмную можно только на самом первом ходе торгов")
|
||
ErrThousandBidTooHigh = errors.New("ставка превышает разрешённый максимум (120 плюс марьяж на руке)")
|
||
ErrThousandMustBid = errors.New("первый ходящий обязан назвать ставку не менее 100 и не может пасовать")
|
||
ErrThousandInvalidFollow = errors.New("нужно ходить в масть или козырем, если они есть на руке")
|
||
ErrThousandAlreadyDiscarded = errors.New("уже отдано нужное количество карт")
|
||
)
|
||
|
||
// thousandRankOrder — старшинство рангов в Тысяче: девятка младше
|
||
// всех, туз старше всех, но десятка стоит МЕЖДУ королём и тузом.
|
||
var thousandRankOrder = map[Rank]int{
|
||
Nine: 0, Jack: 1, Queen: 2, King: 3, Ten: 4, Ace: 5,
|
||
}
|
||
|
||
// thousandMarriageValue — стоимость козырного марьяжа по масти.
|
||
var thousandMarriageValue = map[Suit]int{
|
||
Spades: 40, Clubs: 60, Diamonds: 80, Hearts: 100,
|
||
}
|
||
|
||
// NewThousandDeck создаёт колоду для Тысячи: 24 карты (9,В,Д,К,10,Т
|
||
// в каждой масти).
|
||
func NewThousandDeck() *Deck {
|
||
ranks := []Rank{Nine, Jack, Queen, King, Ten, Ace}
|
||
d := &Deck{cards: make([]Card, 0, 24)}
|
||
for s := Clubs; s <= Spades; s++ {
|
||
for _, r := range ranks {
|
||
d.cards = append(d.cards, Card{Suit: s, Rank: r})
|
||
}
|
||
}
|
||
return d
|
||
}
|
||
|
||
// thousandCardValue — очковая стоимость карты (та же шкала, что и
|
||
// в 101: туз 11, десятка 10, король 4, дама 3, валет 2, девятка 0).
|
||
func thousandCardValue(r Rank) int {
|
||
return oneOhOneCardValue(r)
|
||
}
|
||
|
||
// hasMarriage проверяет, есть ли на руке король и дама указанной
|
||
// масти.
|
||
func hasMarriage(hand []Card, suit Suit) bool {
|
||
hasKing, hasQueen := false, false
|
||
for _, c := range hand {
|
||
if c.Suit != suit {
|
||
continue
|
||
}
|
||
if c.Rank == King {
|
||
hasKing = true
|
||
}
|
||
if c.Rank == Queen {
|
||
hasQueen = true
|
||
}
|
||
}
|
||
return hasKing && hasQueen
|
||
}
|
||
|
||
// hasAllAces проверяет, есть ли на руке все четыре туза — условие
|
||
// тузового марьяжа.
|
||
func hasAllAces(hand []Card) bool {
|
||
found := map[Suit]bool{}
|
||
for _, c := range hand {
|
||
if c.Rank == Ace {
|
||
found[c.Suit] = true
|
||
}
|
||
}
|
||
return len(found) == 4
|
||
}
|
||
|
||
// bestMarriageValue возвращает наибольшую стоимость марьяжа,
|
||
// имеющегося на руке (0, если марьяжей нет).
|
||
func bestMarriageValue(hand []Card) int {
|
||
best := 0
|
||
for s := Clubs; s <= Spades; s++ {
|
||
if hasMarriage(hand, s) {
|
||
if v := thousandMarriageValue[s]; v > best {
|
||
best = v
|
||
}
|
||
}
|
||
}
|
||
return best
|
||
}
|
||
|
||
type thousandTrickPlay struct {
|
||
PlayerIdx int
|
||
Card Card
|
||
}
|
||
|
||
// ThousandHandResult — итог одной завершившейся раздачи.
|
||
type ThousandHandResult struct {
|
||
DeclarerIdx int
|
||
FinalBid int
|
||
Made bool // выполнил ли объявивший торг свою ставку
|
||
ScoreDeltas []int
|
||
MatchOver bool
|
||
MatchWinner int
|
||
}
|
||
|
||
// ThousandGameState — полное состояние партии в 1000 (ровно на 3
|
||
// игроков).
|
||
type ThousandGameState struct {
|
||
Players []*ThousandPlayer
|
||
Kitty []Card
|
||
|
||
DealerIdx int
|
||
Phase ThousandPhase
|
||
Result *ThousandHandResult
|
||
|
||
// --- Торги ---
|
||
CurrentBidderIdx int
|
||
HighBid int
|
||
HighBidderIdx int
|
||
firstBidderIdx int
|
||
bidsMade int
|
||
|
||
// --- Прикуп/снос ---
|
||
DeclarerIdx int
|
||
FinalBid int
|
||
discardsGiven int
|
||
discardTargets []int // получатели 1-й и 2-й отданной карты
|
||
|
||
// --- Взятка ---
|
||
TrumpSuit Suit
|
||
TrumpSet bool
|
||
LeaderIdx int
|
||
CurrentTrick []thousandTrickPlay
|
||
TricksPlayed int
|
||
|
||
// HandNumber — порядковый номер текущей раздачи за весь матч
|
||
// (считается с 1). Первые ThousandGoldenHands раздач матча —
|
||
// "золотой кон" (см. resolveHandEnd).
|
||
HandNumber int
|
||
|
||
// PlayedBlind — текущая раздача объявлена и разыгрывается
|
||
// "в тёмную" (см. DeclareBlind).
|
||
PlayedBlind bool
|
||
}
|
||
|
||
// MatchTarget — партия идёт до этой суммы очков.
|
||
const ThousandMatchTarget = 1000
|
||
|
||
// ПРИМЕЧАНИЕ О НОВЫХ ПРАВИЛАХ: "бочка", "золотой кон", "самосвал",
|
||
// "тузовый марьяж" и "игра втёмную" — народные договорённости с
|
||
// заметными региональными расхождениями (единого свода правил не
|
||
// существует). Здесь выбран один конкретный, наиболее часто
|
||
// встречающийся вариант каждой — подробности см. в комментариях
|
||
// у соответствующих функций и в тексте правил игры.
|
||
const (
|
||
// ThousandBarrelThreshold — при достижении этой суммы очков
|
||
// игрок "садится на бочку" (см. applyBarrelRules).
|
||
ThousandBarrelThreshold = 880
|
||
// ThousandBarrelMaxAttempts — сколько раз подряд сидящий на
|
||
// бочке может провалить свою заявку, прежде чем слетит с неё
|
||
// со штрафом.
|
||
ThousandBarrelMaxAttempts = 3
|
||
// ThousandBarrelPenalty — штраф при неудачном слёте с бочки
|
||
// (по исчерпании попыток или из-за того, что кто-то другой
|
||
// одновременно тоже добрался до бочки).
|
||
ThousandBarrelPenalty = 120
|
||
|
||
// ThousandGoldenHands — первые столько раздач матча считаются
|
||
// "золотым коном" (см. resolveHandEnd).
|
||
ThousandGoldenHands = 3
|
||
|
||
// ThousandSamosvalThreshold — при выходе счёта за ±это значение
|
||
// счёт обнуляется ("самосвал").
|
||
ThousandSamosvalThreshold = 555
|
||
|
||
// ThousandAceMarriageValue — стоимость тузового марьяжа (все
|
||
// четыре туза).
|
||
ThousandAceMarriageValue = 200
|
||
)
|
||
|
||
// NewThousandGame создаёт новую партию на ровно 3 игроков.
|
||
func NewThousandGame(names []string, humanIdx int) *ThousandGameState {
|
||
if len(names) != 3 {
|
||
panic("1000 поддерживается только на троих игроков")
|
||
}
|
||
players := make([]*ThousandPlayer, 3)
|
||
for i, name := range names {
|
||
players[i] = &ThousandPlayer{Name: name, IsHuman: i == humanIdx}
|
||
}
|
||
g := &ThousandGameState{Players: players, DealerIdx: 0}
|
||
g.dealNewHand()
|
||
return g
|
||
}
|
||
|
||
func (g *ThousandGameState) dealNewHand() {
|
||
g.HandNumber++
|
||
deck := NewThousandDeck()
|
||
deck.Shuffle()
|
||
|
||
for _, p := range g.Players {
|
||
p.Hand = nil
|
||
p.HandPoints = 0
|
||
p.HandMarriagePoints = 0
|
||
p.AceMarriageDeclared = false
|
||
p.Passed = false
|
||
}
|
||
|
||
for i := 0; i < 3; i++ {
|
||
idx := (g.DealerIdx + 1 + i) % 3
|
||
g.Players[idx].Hand = append(g.Players[idx].Hand, deck.cards[:7]...)
|
||
deck.cards = deck.cards[7:]
|
||
}
|
||
g.Kitty = append([]Card{}, deck.cards[:3]...)
|
||
|
||
g.firstBidderIdx = (g.DealerIdx + 1) % 3
|
||
g.CurrentBidderIdx = g.firstBidderIdx
|
||
g.HighBid = 0
|
||
g.HighBidderIdx = -1
|
||
g.bidsMade = 0
|
||
g.DeclarerIdx = -1
|
||
g.FinalBid = 0
|
||
g.discardsGiven = 0
|
||
g.discardTargets = []int{(g.firstBidderIdx)}
|
||
g.TrumpSet = false
|
||
g.CurrentTrick = nil
|
||
g.TricksPlayed = 0
|
||
g.Result = nil
|
||
g.PlayedBlind = false
|
||
g.Phase = ThousandPhaseBidding
|
||
}
|
||
|
||
func (g *ThousandGameState) current() *ThousandPlayer {
|
||
return g.Players[g.CurrentBidderIdx]
|
||
}
|
||
|
||
func (g *ThousandGameState) maxBidFor(playerIdx int) int {
|
||
hand := g.Players[playerIdx].Hand
|
||
max := 120 + bestMarriageValue(hand)
|
||
if hasAllAces(hand) {
|
||
max += ThousandAceMarriageValue
|
||
}
|
||
return max
|
||
}
|
||
|
||
func (g *ThousandGameState) nextBidder(from int) int {
|
||
for i := 1; i <= 3; i++ {
|
||
idx := (from + i) % 3
|
||
if !g.Players[idx].Passed {
|
||
return idx
|
||
}
|
||
}
|
||
return from
|
||
}
|
||
|
||
func (g *ThousandGameState) activeBidderCount() int {
|
||
n := 0
|
||
for _, p := range g.Players {
|
||
if !p.Passed {
|
||
n++
|
||
}
|
||
}
|
||
return n
|
||
}
|
||
|
||
// Bid — текущий игрок в торгах называет ставку.
|
||
// DeclareBlind — первый ходящий в торгах может, не называя обычную
|
||
// ставку и не дожидаясь конкурентных торгов, сразу объявить игру
|
||
// "в тёмную": фиксированная заявка 120 без торгов. Награда и штраф
|
||
// за эту раздачу удваиваются (см. resolveHandEnd), а очки соперников
|
||
// от взяток в этой раздаче — тоже удваиваются (марьяжи — нет).
|
||
// Доступно только пока никто ещё не сделал ни одной ставки.
|
||
func (g *ThousandGameState) DeclareBlind() error {
|
||
if g.Phase != ThousandPhaseBidding {
|
||
return ErrThousandWrongPhase
|
||
}
|
||
if g.bidsMade != 0 || g.CurrentBidderIdx != g.firstBidderIdx {
|
||
return ErrThousandBlindTooLate
|
||
}
|
||
g.PlayedBlind = true
|
||
g.HighBid = 120
|
||
g.HighBidderIdx = g.firstBidderIdx
|
||
g.bidsMade++
|
||
g.finishBidding()
|
||
return nil
|
||
}
|
||
|
||
func (g *ThousandGameState) Bid(amount int) error {
|
||
if g.Phase != ThousandPhaseBidding {
|
||
return ErrThousandWrongPhase
|
||
}
|
||
if amount%5 != 0 {
|
||
return ErrThousandBidTooLow
|
||
}
|
||
minRequired := g.HighBid + 5
|
||
if g.bidsMade == 0 {
|
||
minRequired = 100
|
||
}
|
||
if amount < minRequired {
|
||
return ErrThousandBidTooLow
|
||
}
|
||
if amount > g.maxBidFor(g.CurrentBidderIdx) {
|
||
return ErrThousandBidTooHigh
|
||
}
|
||
|
||
g.HighBid = amount
|
||
g.HighBidderIdx = g.CurrentBidderIdx
|
||
g.bidsMade++
|
||
g.advanceOrFinishBidding()
|
||
return nil
|
||
}
|
||
|
||
// Pass — текущий игрок в торгах отказывается от дальнейшего
|
||
// участия. Самый первый ходящий обязан назвать ставку и не может
|
||
// пасовать.
|
||
func (g *ThousandGameState) Pass() error {
|
||
if g.Phase != ThousandPhaseBidding {
|
||
return ErrThousandWrongPhase
|
||
}
|
||
if g.bidsMade == 0 && g.CurrentBidderIdx == g.firstBidderIdx {
|
||
return ErrThousandMustBid
|
||
}
|
||
g.Players[g.CurrentBidderIdx].Passed = true
|
||
g.advanceOrFinishBidding()
|
||
return nil
|
||
}
|
||
|
||
func (g *ThousandGameState) advanceOrFinishBidding() {
|
||
if g.activeBidderCount() <= 1 {
|
||
g.finishBidding()
|
||
return
|
||
}
|
||
g.CurrentBidderIdx = g.nextBidder(g.CurrentBidderIdx)
|
||
}
|
||
|
||
func (g *ThousandGameState) finishBidding() {
|
||
g.DeclarerIdx = g.HighBidderIdx
|
||
g.FinalBid = g.HighBid
|
||
g.Players[g.DeclarerIdx].Hand = append(g.Players[g.DeclarerIdx].Hand, g.Kitty...)
|
||
g.discardTargets = []int{
|
||
(g.DeclarerIdx + 1) % 3,
|
||
(g.DeclarerIdx + 2) % 3,
|
||
}
|
||
g.discardsGiven = 0
|
||
g.Phase = ThousandPhaseDiscard
|
||
}
|
||
|
||
// DiscardCard — объявивший торг игрок (declarer) отдаёт одну карту
|
||
// следующему по очереди сопернику. Вызывается дважды подряд.
|
||
func (g *ThousandGameState) DiscardCard(card Card) error {
|
||
if g.Phase != ThousandPhaseDiscard {
|
||
return ErrThousandWrongPhase
|
||
}
|
||
if g.discardsGiven >= 2 {
|
||
return ErrThousandAlreadyDiscarded
|
||
}
|
||
p := g.Players[g.DeclarerIdx]
|
||
if !hasCard(p.Hand, card) {
|
||
return ErrThousandCardNotInHand
|
||
}
|
||
p.Hand = removeCard(p.Hand, card)
|
||
target := g.discardTargets[g.discardsGiven]
|
||
g.Players[target].Hand = append(g.Players[target].Hand, card)
|
||
g.discardsGiven++
|
||
|
||
if g.discardsGiven == 2 {
|
||
g.LeaderIdx = g.DeclarerIdx
|
||
g.Phase = ThousandPhaseTrick
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (g *ThousandGameState) ledSuit() (Suit, bool) {
|
||
if len(g.CurrentTrick) == 0 {
|
||
return 0, false
|
||
}
|
||
return g.CurrentTrick[0].Card.Suit, true
|
||
}
|
||
|
||
func containsSuit(hand []Card, suit Suit) bool {
|
||
for _, c := range hand {
|
||
if c.Suit == suit {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// isValidPlay проверяет, разрешено ли сыграть card в текущей
|
||
// взятке для игрока с рукой hand.
|
||
func (g *ThousandGameState) isValidPlay(hand []Card, card Card) bool {
|
||
led, ok := g.ledSuit()
|
||
if !ok {
|
||
return true // ходит первым — любая карта разрешена (кроме марьяжа см. отдельно)
|
||
}
|
||
if containsSuit(hand, led) {
|
||
return card.Suit == led
|
||
}
|
||
if g.TrumpSet && containsSuit(hand, g.TrumpSuit) {
|
||
return card.Suit == g.TrumpSuit
|
||
}
|
||
return true
|
||
}
|
||
|
||
// PlayCard — текущий игрок в раскладе взятки играет карту. Если
|
||
// это первая карта взятки (игрок ходит), король или дама масти, по
|
||
// которой на руке есть парная карта, и это не самая первая взятка
|
||
// раздачи — автоматически объявляется марьяж (козырь меняется на
|
||
// эту масть, очки марьяжа сразу начисляются).
|
||
func (g *ThousandGameState) PlayCard(card Card) error {
|
||
if g.Phase != ThousandPhaseTrick {
|
||
return ErrThousandWrongPhase
|
||
}
|
||
actor := g.currentTrickActor()
|
||
p := g.Players[actor]
|
||
if !hasCard(p.Hand, card) {
|
||
return ErrThousandCardNotInHand
|
||
}
|
||
|
||
leading := len(g.CurrentTrick) == 0
|
||
if !leading {
|
||
if !g.isValidPlay(p.Hand, card) {
|
||
return ErrThousandInvalidFollow
|
||
}
|
||
} else if (card.Rank == King || card.Rank == Queen) && g.TricksPlayed > 0 && hasMarriage(p.Hand, card.Suit) {
|
||
p.HandMarriagePoints += thousandMarriageValue[card.Suit]
|
||
g.TrumpSuit = card.Suit
|
||
g.TrumpSet = true
|
||
} else if card.Rank == Ace && g.TricksPlayed > 0 && !p.AceMarriageDeclared && hasAllAces(p.Hand) {
|
||
// Тузовый марьяж: игрок, уже взявший хотя бы одну взятку (не
|
||
// на первом ходе раздачи — как и обычный марьяж), заходит с
|
||
// туза, имея на руках все четыре. Козырь при этом НЕ
|
||
// меняется (в отличие от обычного марьяжа) — это отдельный
|
||
// вариант правил, региональные соглашения расходятся в том,
|
||
// должен ли последний туз тузового марьяжа тоже назначать
|
||
// козырь; здесь выбран более простой и чаще встречающийся
|
||
// вариант без смены козыря.
|
||
p.HandMarriagePoints += ThousandAceMarriageValue
|
||
p.AceMarriageDeclared = true
|
||
}
|
||
|
||
p.Hand = removeCard(p.Hand, card)
|
||
g.CurrentTrick = append(g.CurrentTrick, thousandTrickPlay{PlayerIdx: actor, Card: card})
|
||
|
||
if len(g.CurrentTrick) == 3 {
|
||
g.resolveTrick()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// currentTrickActor возвращает индекс игрока, чья очередь класть
|
||
// карту в текущей взятке.
|
||
func (g *ThousandGameState) currentTrickActor() int {
|
||
return (g.LeaderIdx + len(g.CurrentTrick)) % 3
|
||
}
|
||
|
||
func (g *ThousandGameState) resolveTrick() {
|
||
led := g.CurrentTrick[0].Card.Suit
|
||
best := g.CurrentTrick[0]
|
||
bestIsTrump := g.TrumpSet && best.Card.Suit == g.TrumpSuit
|
||
|
||
for _, play := range g.CurrentTrick[1:] {
|
||
isTrump := g.TrumpSet && play.Card.Suit == g.TrumpSuit
|
||
switch {
|
||
case isTrump && !bestIsTrump:
|
||
best = play
|
||
bestIsTrump = true
|
||
case isTrump == bestIsTrump && play.Card.Suit == best.Card.Suit && thousandRankOrder[play.Card.Rank] > thousandRankOrder[best.Card.Rank]:
|
||
best = play
|
||
case !isTrump && !bestIsTrump && play.Card.Suit == led && best.Card.Suit != led:
|
||
best = play
|
||
}
|
||
}
|
||
|
||
points := 0
|
||
for _, play := range g.CurrentTrick {
|
||
points += thousandCardValue(play.Card.Rank)
|
||
}
|
||
g.Players[best.PlayerIdx].HandPoints += points
|
||
|
||
g.LeaderIdx = best.PlayerIdx
|
||
g.CurrentTrick = nil
|
||
g.TricksPlayed++
|
||
|
||
if g.TricksPlayed == 8 {
|
||
g.resolveHandEnd()
|
||
}
|
||
}
|
||
|
||
func (g *ThousandGameState) resolveHandEnd() {
|
||
declarer := g.Players[g.DeclarerIdx]
|
||
declarerTotal := declarer.HandPoints + declarer.HandMarriagePoints
|
||
made := declarerTotal >= g.FinalBid
|
||
|
||
deltas := make([]int, 3)
|
||
|
||
declarerDelta := g.FinalBid
|
||
if !made {
|
||
declarerDelta = -g.FinalBid
|
||
}
|
||
if g.PlayedBlind {
|
||
declarerDelta *= 2 // удвоенная награда/штраф за игру "в тёмную"
|
||
}
|
||
deltas[g.DeclarerIdx] = declarerDelta
|
||
|
||
for i, p := range g.Players {
|
||
if i == g.DeclarerIdx {
|
||
continue
|
||
}
|
||
multiplier := 1
|
||
if g.HandNumber <= ThousandGoldenHands {
|
||
multiplier *= 2 // золотой кон — первые несколько раздач матча
|
||
}
|
||
if g.PlayedBlind {
|
||
multiplier *= 2 // очки соперников удваиваются и при игре втёмную
|
||
}
|
||
// марьяжи никогда не удваиваются — ни золотым коном, ни тёмной
|
||
deltas[i] = p.HandPoints*multiplier + p.HandMarriagePoints
|
||
}
|
||
|
||
g.applyBarrelAdjustments(deltas, made)
|
||
|
||
for i, p := range g.Players {
|
||
p.Score += deltas[i]
|
||
}
|
||
|
||
g.applySamosval()
|
||
g.updateBarrelSeating()
|
||
|
||
result := &ThousandHandResult{
|
||
DeclarerIdx: g.DeclarerIdx,
|
||
FinalBid: g.FinalBid,
|
||
Made: made,
|
||
ScoreDeltas: deltas,
|
||
}
|
||
|
||
bestIdx := -1
|
||
for i, p := range g.Players {
|
||
if p.Score >= ThousandMatchTarget {
|
||
if bestIdx == -1 || p.Score > g.Players[bestIdx].Score {
|
||
bestIdx = i
|
||
}
|
||
}
|
||
}
|
||
if bestIdx != -1 {
|
||
result.MatchOver = true
|
||
result.MatchWinner = bestIdx
|
||
}
|
||
|
||
g.Result = result
|
||
g.Phase = ThousandPhaseHandOver
|
||
}
|
||
|
||
// applyBarrelAdjustments переписывает уже посчитанные "обычные"
|
||
// дельты для игроков, сидящих на бочке (см. ThousandBarrelThreshold):
|
||
// - объявивший игру и сидящий на бочке при провале своей заявки не
|
||
// теряет FinalBid обычным образом — вместо этого засчитывается
|
||
// попытка; после ThousandBarrelMaxAttempts подряд неудачных
|
||
// попыток он слетает с бочки со штрафом ThousandBarrelPenalty;
|
||
// - сидящий на бочке, но не объявлявший игру в этой раздаче, вообще
|
||
// не получает очков от чужих взяток — сидя на бочке, можно расти
|
||
// только через собственную выполненную заявку.
|
||
func (g *ThousandGameState) applyBarrelAdjustments(deltas []int, made bool) {
|
||
for i, p := range g.Players {
|
||
if !p.OnBarrel {
|
||
continue
|
||
}
|
||
if i == g.DeclarerIdx {
|
||
if made {
|
||
continue // обычная (уже посчитанная) прибавка верна как есть
|
||
}
|
||
p.BarrelAttempts++
|
||
if p.BarrelAttempts >= ThousandBarrelMaxAttempts {
|
||
deltas[i] = -ThousandBarrelPenalty
|
||
p.OnBarrel = false
|
||
p.BarrelAttempts = 0
|
||
} else {
|
||
deltas[i] = 0 // попытка провалена, но лимит ещё не исчерпан — счёт не меняется
|
||
}
|
||
} else {
|
||
deltas[i] = 0
|
||
}
|
||
}
|
||
}
|
||
|
||
// applySamosval обнуляет счёт игрока, если по итогам раздачи он
|
||
// оказался ТОЧНО равен ±ThousandSamosvalThreshold — это редкое
|
||
// нумерологическое совпадение ("555"), а не общий потолок счёта;
|
||
// если бы обнуление срабатывало при ЛЮБОМ превышении, партия
|
||
// никогда не смогла бы дойти до цели в 1000 очков.
|
||
func (g *ThousandGameState) applySamosval() {
|
||
for _, p := range g.Players {
|
||
if p.Score == ThousandSamosvalThreshold || p.Score == -ThousandSamosvalThreshold {
|
||
p.Score = 0
|
||
p.OnBarrel = false
|
||
p.BarrelAttempts = 0
|
||
}
|
||
}
|
||
}
|
||
|
||
// updateBarrelSeating сажает на бочку игрока, чей счёт по итогам
|
||
// этой раздачи впервые достиг ThousandBarrelThreshold (но ещё не
|
||
// выиграл матч). Одновременно на бочке может сидеть только один
|
||
// игрок — если садится новый, прежний слетает со штрафом.
|
||
func (g *ThousandGameState) updateBarrelSeating() {
|
||
for i, p := range g.Players {
|
||
if p.OnBarrel || p.Score < ThousandBarrelThreshold || p.Score >= ThousandMatchTarget {
|
||
continue
|
||
}
|
||
for j, other := range g.Players {
|
||
if j != i && other.OnBarrel {
|
||
other.OnBarrel = false
|
||
other.BarrelAttempts = 0
|
||
other.Score -= ThousandBarrelPenalty
|
||
}
|
||
}
|
||
p.OnBarrel = true
|
||
p.BarrelAttempts = 0
|
||
}
|
||
}
|
||
|
||
// NextHand начинает новую раздачу (сдающий переходит по кругу),
|
||
// сохраняя счёт игроков.
|
||
func (g *ThousandGameState) NextHand() {
|
||
if g.Result != nil && g.Result.MatchOver {
|
||
return
|
||
}
|
||
g.DealerIdx = (g.DealerIdx + 1) % 3
|
||
g.dealNewHand()
|
||
}
|