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() }