package main import "errors" // ПРИМЕЧАНИЕ О ПРАВИЛАХ: у игры "101" существует много вариантов, // расходящихся в деталях. Реализован следующий, наиболее часто // встречающийся набор правил: // - Колода 36 карт (6-туз), как и в Дураке. // - Раздача: сдающий раздаёт по 5 карт каждому игроку, кроме // себя — себе он оставляет 4, а пятую (лишнюю) кладёт в центр // стола лицом вверх — это и есть стартовая карта сброса. // - Ход: нужно положить карту той же масти или того же ранга, // что и верхняя карта сброса. Если подходящей карты нет — // игрок обязан брать карты из колоды добора по одной, пока не // возьмёт подходящую (тогда сразу её играет) либо пока колода // не закончится (тогда ход просто переходит дальше). // - Специальные карты: туз пропускает ход следующего игрока; // шестёрка заставляет следующего взять 1 карту и пропустить // ход; семёрка — взять 2 карты и пропустить ход; пиковый // король — взять 4 карты и пропустить ход. Остальные короли и // карты обычные. // - Игрок, избавившийся от всех карт, выигрывает раунд. Если его // последней сыгранной картой была дама — он получает бонус // -20 очков за раунд (-40, если это была червовая дама) вместо // обычных 0; остальные складывают очковую стоимость карт, // оставшихся на руках, и добавляют к своему общему счёту. // - Набравший 101 очко и более выбывает из партии, если только // это не ровно 101 — тогда счёт обнуляется (традиционное // правило "отыграться"). Партия продолжается, пока не останется // один игрок — он и побеждает. // OneOhOnePlayer — состояние одного игрока в партии 101. type OneOhOnePlayer struct { Name string Hand []Card IsHuman bool Score int // суммарный счёт по итогам сыгранных раундов Out bool } // OneOhOnePhase — фаза текущего раунда. type OneOhOnePhase int const ( // OneOhOnePhasePlay — обычный ход текущего игрока: сыграть карту // или взять из колоды добора. OneOhOnePhasePlay OneOhOnePhase = iota // OneOhOnePhaseChooseSuit — только что положили даму: тот же // игрок обязан назвать масть для следующего хода, прежде чем ход // перейдёт дальше (см. ChooseSuit). OneOhOnePhaseChooseSuit // OneOhOnePhaseRoundOver — раунд завершён. OneOhOnePhaseRoundOver ) var ( ErrOOOWrongPhase = errors.New("недопустимое действие для текущей фазы") ErrOOOCardNotInHand = errors.New("карты нет на руке игрока") ErrOOOInvalidPlay = errors.New("эта карта не подходит по масти или рангу") ErrOOOMustPlay = errors.New("нужно сыграть карту, которая подходит — брать из колоды нельзя") ErrOOOInvalidSuit = errors.New("недопустимая масть") ) // OneOhOneRoundResult — итог одного завершившегося раунда. type OneOhOneRoundResult struct { Winner int QueenBonus bool // победитель закончил дамой (бонус учтён в ScoreDeltas) ScoreDeltas []int // изменение ОЧКОВ ЗА ЭТОТ РАУНД (не общего счёта) Eliminated []int // индексы игроков, выбывших в этом раунде (счёт >101) Reset []int // индексы игроков, у которых счёт обнулился (ровно 101) MatchOver bool MatchWinner int // имеет смысл только при MatchOver NoWinner bool // вырожденный случай: все выбыли одновременно Stalemate bool // раунд принудительно завершён защитой от зацикливания } // OneOhOneGameState — полное состояние партии в 101. type OneOhOneGameState struct { Players []*OneOhOnePlayer Stock []Card // колода добора; Stock[0] — верхняя (берётся первой) Discard []Card // сброс; последний элемент — верхняя (видимая) карта CurrentPlayerIdx int DealerIdx int Phase OneOhOnePhase Result *OneOhOneRoundResult // DeclaredSuit — если не nil, названная масть (после дамы), // действующая вместо масти верхней карты сброса ровно для // следующего хода (см. effectiveSuit, ChooseSuit). DeclaredSuit *Suit // PendingEightSuit — если не nil, на столе лежит непокрытая // восьмёрка этой масти: следующий ход обязан быть либо той же // масти, либо другой восьмёркой (которая продолжит цепочку — // см. canPlay, applyEffectAndAdvance). PendingEightSuit *Suit turnsSinceStart int } // maxOneOhOneTurns — защита от зацикливания: если раунд длится // неправдоподобно долго, принудительно завершаем его вничью (по // текущему весу рук), чтобы партия гарантированно не зависла. // Увеличено с 5000 после добавления обязательного покрытия // восьмёрки — цепочки вынужденного добора честно могут занимать // заметно больше ходов, чем раньше (замер показал до ~10000 на // редкий раунд). const maxOneOhOneTurns = 20000 // oneOhOneCardValue — очковая стоимость карты для подсчёта штрафа // за карты, оставшиеся на руках. func oneOhOneCardValue(r Rank) int { switch r { case Ace: return 11 case Ten: return 10 case Nine: return 0 case Eight: return 8 case Seven: return 7 case Six: return 6 case King: return 4 case Queen: return 3 case Jack: return 2 } return 0 } // NewOneOhOneGame создаёт новую партию: игрок 0 — первый сдающий. func NewOneOhOneGame(names []string, humanIdx int) *OneOhOneGameState { players := make([]*OneOhOnePlayer, len(names)) for i, name := range names { players[i] = &OneOhOnePlayer{Name: name, IsHuman: i == humanIdx} } g := &OneOhOneGameState{Players: players, DealerIdx: 0} g.dealNewRound() return g } // dealNewRound тасует колоду и раздаёт новый раунд: по 5 карт всем // АКТИВНЫМ (не выбывшим) игрокам, кроме сдающего (ему 4, а "лишняя" // пятая уходит в сброс как стартовая карта). Первым ходит игрок // слева от сдающего. func (g *OneOhOneGameState) dealNewRound() { deck := NewDurakDeck() deck.Shuffle() stock := deck.cards for _, p := range g.Players { p.Hand = nil } active := make([]int, 0, len(g.Players)) for i, p := range g.Players { if !p.Out { active = append(active, i) } } for _, idx := range active { count := 5 if idx == g.DealerIdx { count = 4 } for c := 0; c < count; c++ { card := stock[0] stock = stock[1:] g.Players[idx].Hand = append(g.Players[idx].Hand, card) } } // "лишняя" пятая карта сдающего уходит в сброс как стартовая openingCard := stock[0] stock = stock[1:] g.Stock = stock g.Discard = []Card{openingCard} g.CurrentPlayerIdx = g.nextActive(g.DealerIdx) g.Phase = OneOhOnePhasePlay g.Result = nil g.turnsSinceStart = 0 g.DeclaredSuit = nil if openingCard.Rank == Eight { suit := openingCard.Suit g.PendingEightSuit = &suit } else { g.PendingEightSuit = nil } } func (g *OneOhOneGameState) nextActive(from int) int { n := len(g.Players) for i := 1; i <= n; i++ { idx := (from + i) % n if !g.Players[idx].Out { return idx } } return from } func (g *OneOhOneGameState) current() *OneOhOnePlayer { return g.Players[g.CurrentPlayerIdx] } func (g *OneOhOneGameState) topDiscard() Card { return g.Discard[len(g.Discard)-1] } // effectiveSuit возвращает "текущую" масть для проверки хода: // обычно масть верхней карты сброса, но если недавно была положена // дама и названа масть — используется названная масть вместо масти // самой дамы (действует ровно один следующий ход). func (g *OneOhOneGameState) effectiveSuit() Suit { if g.DeclaredSuit != nil { return *g.DeclaredSuit } return g.topDiscard().Suit } // canPlay проверяет, подходит ли карта по масти или рангу под // верхнюю карту сброса. // // Три особых случая (в порядке приоритета): // 1. Если на столе висит непокрытая восьмёрка (PendingEightSuit != // nil) — годится ТОЛЬКО карта той же масти или другая восьмёрка; // ничего иного не подходит, даже дама. // 2. Иначе дама кладётся на что угодно, независимо от масти и ранга. // 3. Иначе — обычное правило: масть (с учётом объявленной после // прошлой дамы, если есть) или ранг верхней карты. func (g *OneOhOneGameState) canPlay(card Card) bool { if g.PendingEightSuit != nil { return card.Suit == *g.PendingEightSuit || card.Rank == Eight } if card.Rank == Queen { return true } top := g.topDiscard() return card.Suit == g.effectiveSuit() || card.Rank == top.Rank } // hasPlayableCard проверяет, есть ли у игрока хоть одна карта, // подходящая под текущий сброс. func (g *OneOhOneGameState) hasPlayableCard(p *OneOhOnePlayer) bool { for _, c := range p.Hand { if g.canPlay(c) { return true } } return false } // drawOne снимает верхнюю карту колоды добора, при необходимости // сначала перетасовывая сброс (кроме верхней карты) в новую колоду. func (g *OneOhOneGameState) drawOne() Card { if len(g.Stock) == 0 { g.reshuffleStockFromDiscard() } if len(g.Stock) == 0 { return Card{} } c := g.Stock[0] g.Stock = g.Stock[1:] return c } // stockExhausted проверяет, остались ли вообще карты для добора (в // колоде или в сбросе под верхней картой). func (g *OneOhOneGameState) stockExhausted() bool { return len(g.Stock) == 0 && len(g.Discard) <= 1 } func (g *OneOhOneGameState) reshuffleStockFromDiscard() { if len(g.Discard) <= 1 { return } top := g.Discard[len(g.Discard)-1] rest := g.Discard[:len(g.Discard)-1] newStock := &Deck{cards: append([]Card{}, rest...)} newStock.Shuffle() g.Stock = newStock.cards g.Discard = []Card{top} } // PlayCard — текущий игрок играет карту из руки. func (g *OneOhOneGameState) PlayCard(card Card) error { if g.Phase != OneOhOnePhasePlay { return ErrOOOWrongPhase } p := g.current() if !hasCard(p.Hand, card) { return ErrOOOCardNotInHand } if !g.canPlay(card) { return ErrOOOInvalidPlay } p.Hand = removeCard(p.Hand, card) g.Discard = append(g.Discard, card) if len(p.Hand) == 0 { g.resolveRoundEnd(g.CurrentPlayerIdx, card) return nil } g.applyEffectAndAdvance(card) return nil } // Draw — текущий игрок берёт карту из колоды добора (разрешено // только если у него нет ни одной подходящей карты на руке). Если // взятая карта подходит — она сразу же автоматически играется; // иначе колода добора снова спрашивается на следующем вызове Draw // тем же игроком (в TUI/боте это просто повторные нажатия/вызовы). func (g *OneOhOneGameState) Draw() error { if g.Phase != OneOhOnePhasePlay { return ErrOOOWrongPhase } p := g.current() if g.hasPlayableCard(p) { return ErrOOOMustPlay } if g.stockExhausted() { // брать неоткуда — ход переходит дальше без штрафов g.advanceTurnPlain() return nil } card := g.drawOne() p.Hand = append(p.Hand, card) if g.canPlay(card) { p.Hand = removeCard(p.Hand, card) g.Discard = append(g.Discard, card) if len(p.Hand) == 0 { g.resolveRoundEnd(g.CurrentPlayerIdx, card) return nil } g.applyEffectAndAdvance(card) } return nil } // applyEffectAndAdvance обрабатывает эффект специальной карты (если // есть) и передаёт ход дальше. // // Восьмёрка выставляет/продолжает висящее требование "покрыть эту // масть или другой восьмёркой" (см. PendingEightSuit) — само по себе // не пропускает ходов и не даёт штрафов. Любая другая карта снимает // это требование. // // Дама не передаёт ход сразу: игрок, который её положил, должен // сначала явно назвать масть через ChooseSuit — только тогда ход // пойдёт дальше. func (g *OneOhOneGameState) applyEffectAndAdvance(played Card) { if played.Rank == Eight { suit := played.Suit g.PendingEightSuit = &suit } else { g.PendingEightSuit = nil } if played.Rank == Queen { g.Phase = OneOhOnePhaseChooseSuit return } g.DeclaredSuit = nil // объявленная масть действовала только на этот один ход g.advanceAfterEffect(played) } // ChooseSuit — игрок, только что положивший даму, называет масть, // которая будет считаться "текущей" для следующего хода (вместо // масти самой дамы). После этого ход передаётся дальше как обычно. func (g *OneOhOneGameState) ChooseSuit(suit Suit) error { if g.Phase != OneOhOnePhaseChooseSuit { return ErrOOOWrongPhase } if suit < Clubs || suit > Spades { return ErrOOOInvalidSuit } g.DeclaredSuit = &suit g.Phase = OneOhOnePhasePlay g.advanceAfterEffect(g.topDiscard()) // сама дама не даёт скип/штраф — просто передаём ход return nil } // advanceAfterEffect считает эффект "скип + штраф" сыгранной карты и // передаёт ход дальше — вынесено отдельно от applyEffectAndAdvance, // чтобы вызываться и сразу для обычных карт, и из ChooseSuit после // того, как масть для дамы уже названа. func (g *OneOhOneGameState) advanceAfterEffect(played Card) { g.turnsSinceStart++ if g.turnsSinceStart >= maxOneOhOneTurns { g.resolveStalemate() return } skip, penalty := oneOhOneCardEffect(played) if !skip { g.CurrentPlayerIdx = g.nextActive(g.CurrentPlayerIdx) return } skippedIdx := g.nextActive(g.CurrentPlayerIdx) skipped := g.Players[skippedIdx] for i := 0; i < penalty; i++ { if g.stockExhausted() { break } skipped.Hand = append(skipped.Hand, g.drawOne()) } g.CurrentPlayerIdx = g.nextActive(skippedIdx) } func (g *OneOhOneGameState) advanceTurnPlain() { g.turnsSinceStart++ if g.turnsSinceStart >= maxOneOhOneTurns { g.resolveStalemate() return } g.CurrentPlayerIdx = g.nextActive(g.CurrentPlayerIdx) } // oneOhOneCardEffect возвращает, пропускает ли карта следующего // игрока и сколько штрафных карт он должен взять. func oneOhOneCardEffect(c Card) (skip bool, penalty int) { switch { case c.Rank == Ace: return true, 0 case c.Rank == Six: return true, 1 case c.Rank == Seven: return true, 2 case c.Rank == King && c.Suit == Spades: return true, 4 default: return false, 0 } } func (g *OneOhOneGameState) resolveStalemate() { g.Result = &OneOhOneRoundResult{Stalemate: true, ScoreDeltas: make([]int, len(g.Players))} g.Phase = OneOhOnePhaseRoundOver } // resolveRoundEnd завершает раунд победой игрока winnerIdx: считает // штрафные очки остальных, бонус за даму, обновляет общий счёт и // проверяет выбывание/окончание партии. func (g *OneOhOneGameState) resolveRoundEnd(winnerIdx int, lastCard Card) { deltas := make([]int, len(g.Players)) queenBonus := false if lastCard.Rank == Queen { queenBonus = true if lastCard.Suit == Hearts { deltas[winnerIdx] = -40 } else { deltas[winnerIdx] = -20 } } for i, p := range g.Players { if i == winnerIdx || p.Out { continue } sum := 0 for _, c := range p.Hand { sum += oneOhOneCardValue(c.Rank) } deltas[i] = sum } for i, p := range g.Players { p.Score += deltas[i] } var eliminated, reset []int for i, p := range g.Players { if p.Score == 101 { p.Score = 0 reset = append(reset, i) } else if p.Score > 101 { p.Out = true eliminated = append(eliminated, i) } } result := &OneOhOneRoundResult{ Winner: winnerIdx, QueenBonus: queenBonus, ScoreDeltas: deltas, Eliminated: eliminated, Reset: reset, } active := 0 lastActive := -1 for i, p := range g.Players { if !p.Out { active++ lastActive = i } } switch active { case 0: result.MatchOver = true result.NoWinner = true case 1: result.MatchOver = true result.MatchWinner = lastActive } g.Result = result g.Phase = OneOhOnePhaseRoundOver if !result.MatchOver { g.DealerIdx = winnerIdx } } // NextRound начинает новый раунд партии (сдающий — победитель // предыдущего раунда), сохраняя счёт и статус выбывания игроков. func (g *OneOhOneGameState) NextRound() { if g.Result != nil && g.Result.MatchOver { return } g.dealNewRound() }