package main import "errors" // DurakPlayer — состояние одного игрока в партии Дурак. type DurakPlayer struct { Name string Hand []Card IsHuman bool Out bool // "отбился насовсем": карт нет и колода пуста } // durakTablePair — одна атакующая карта на столе и (если есть) // карта, которой её отбили. type durakTablePair struct { Attack Card Defend *Card } // DurakPhase — чья сейчас очередь действовать. type DurakPhase int const ( // DurakPhaseAttack — самый первый ход раунда: атакующий обязан // подкинуть карту (стол ещё пуст, выбора "пасовать" нет). DurakPhaseAttack DurakPhase = iota // DurakPhaseDefend — защищающийся должен отбить последнюю // подкинутую карту либо забрать весь стол. DurakPhaseDefend // DurakPhaseThrowIn — стол полностью отбит; по очереди (см. // ThrowOrder/ThrowTurnPos) любой игрок, кроме защищающегося, // может либо подкинуть ещё одну карту подходящего ранга, либо // пасовать. Как только все подряд спасовали (или лимит атаки // исчерпан) — раунд уходит в отбой. DurakPhaseThrowIn // DurakPhaseGameOver — партия завершена. DurakPhaseGameOver ) var ( ErrDurakWrongPhase = errors.New("недопустимое действие для текущей фазы хода") ErrDurakCardNotInHand = errors.New("карты нет на руке игрока") ErrDurakInvalidThrowIn = errors.New("эту карту нельзя подкинуть: не тот ранг или лимит атаки исчерпан") ErrDurakInvalidDefend = errors.New("эта карта не бьёт атакующую карту") ErrDurakNothingToDefend = errors.New("на столе нет карты, которую нужно отбивать") ErrDurakEmptyTable = errors.New("на столе нет карт") ) // DurakResult — итог завершившейся партии. type DurakResult struct { Loser int // индекс проигравшего ("дурака"); не имеет смысла при NoLoser/Stalemate NoLoser bool // все игроки одновременно избавились от карт — без проигравшего Stalemate bool // партия принудительно завершена вничью защитой от зацикливания } // DurakGameState — полное состояние партии в Дурак. type DurakGameState struct { Players []*DurakPlayer Talon []Card // колода для добора; Talon[0] — верхняя (берётся первой) TrumpSuit Suit TrumpCard Card // вскрытая козырная карта — последняя в Talon, показывается отдельно AttackerIdx int DefenderIdx int Table []durakTablePair Phase DurakPhase Result *DurakResult // attackCap — сколько карт максимум может оказаться на столе в // этом раунде: min(6, размер руки защищающегося на момент // начала раунда). Фиксируется один раз при начале раунда. attackCap int // ThrowOrder — порядок подкидывающих в текущем раунде: все // активные игроки, кроме защищающегося, начиная с атакующего, по // кругу. Вычисляется один раз при начале раунда. ThrowOrder []int // ThrowTurnPos — индекс в ThrowOrder, чья сейчас очередь решать // "подкинуть или пасовать" в фазе DurakPhaseThrowIn. ThrowTurnPos int // passedSinceThrow считает подряд идущие пасы (без единого // подкидывания между ними) в текущей серии решений. Когда оно // достигает len(ThrowOrder), все отказались — раунд уходит в // отбой. passedSinceThrow int // takesSinceBito считает подряд идущие завершения раундов через // "взятие" (без единого "бито" между ними). Когда карты на столе // не выходят из игры (только перекладываются между руками), // партия теоретически может зациклиться навсегда — это защита // от такого зацикливания. takesSinceBito int } // maxTakesWithoutBito — порог принудительного завершения партии // вничью (Stalemate), если очень долго не происходит ни одного // "бито" подряд (карты не выходят из игры). const maxTakesWithoutBito = 200 // NewDurakGame создаёт новую партию: тасует колоду на 36 карт, // раздаёт по 6 карт (кому-то может достаться меньше — например, при // 6 игроках колода не делится ровно), вскрывает козырь и определяет // первого атакующего (у кого на руке самый младший козырь). func NewDurakGame(names []string, humanIdx int) *DurakGameState { deck := NewDurakDeck() deck.Shuffle() trumpCard := deck.cards[len(deck.cards)-1] players := make([]*DurakPlayer, len(names)) for i, name := range names { players[i] = &DurakPlayer{Name: name, IsHuman: i == humanIdx} } g := &DurakGameState{ Players: players, Talon: deck.cards, TrumpSuit: trumpCard.Suit, TrumpCard: trumpCard, } // начальная раздача: раунд-робин по одной карте, пока у всех не // станет по 6 либо в колоде не останется только козырная карта for dealt := true; dealt; { dealt = false for _, p := range g.Players { if len(p.Hand) < 6 && len(g.Talon) > 1 { p.Hand = append(p.Hand, g.drawReserveTrump()) dealt = true } } } g.AttackerIdx = g.findInitialAttacker() g.DefenderIdx = g.nextActive(g.AttackerIdx) g.startNewBout() return g } // startNewBout готовит служебные поля к началу нового раунда: // лимит атаки и порядок подкидывающих, всегда исходя из текущих // AttackerIdx/DefenderIdx. func (g *DurakGameState) startNewBout() { g.attackCap = g.currentAttackCap() g.ThrowOrder = g.computeThrowOrder() g.ThrowTurnPos = 0 g.passedSinceThrow = 0 g.Phase = DurakPhaseAttack } // computeThrowOrder строит порядок подкидывающих: все активные // игроки, кроме защищающегося, начиная с атакующего, по кругу. func (g *DurakGameState) computeThrowOrder() []int { order := []int{} idx := g.AttackerIdx for { if idx != g.DefenderIdx { order = append(order, idx) } next := g.nextActive(idx) if next == g.AttackerIdx { break } idx = next if len(order) > len(g.Players) { break // защита от зацикливания в вырожденных случаях } } return order } // drawReserveTrump снимает верхнюю карту талона, никогда не трогая // последнюю (козырную) карту, пока в талоне есть что-то ещё — // используется только при начальной раздаче. func (g *DurakGameState) drawReserveTrump() Card { c := g.Talon[0] g.Talon = g.Talon[1:] return c } // drawAny снимает верхнюю карту талона, включая последнюю // (козырную), если до неё дошла очередь — используется при доборе // карт между раундами. func (g *DurakGameState) drawAny() Card { c := g.Talon[0] g.Talon = g.Talon[1:] return c } // findInitialAttacker находит игрока с самым младшим козырем на // руке — он ходит первым. Если ни у кого нет козыря (редкий случай // при небольшом числе игроков), первым ходит игрок 0. func (g *DurakGameState) findInitialAttacker() int { best := -1 bestRank := 99 for i, p := range g.Players { for _, c := range p.Hand { if c.Suit == g.TrumpSuit && durakRankOrder[c.Rank] < bestRank { bestRank = durakRankOrder[c.Rank] best = i } } } if best == -1 { return 0 } return best } // nextActive возвращает индекс следующего по кругу игрока, который // ещё не вышел из игры (Out == false). func (g *DurakGameState) 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 *DurakGameState) attacker() *DurakPlayer { return g.Players[g.AttackerIdx] } func (g *DurakGameState) defender() *DurakPlayer { return g.Players[g.DefenderIdx] } func (g *DurakGameState) currentAttackCap() int { capacity := 6 if len(g.defender().Hand) < capacity { capacity = len(g.defender().Hand) } return capacity } // CurrentThrower возвращает индекс игрока, чья сейчас очередь // решать "подкинуть или пасовать" в фазе DurakPhaseThrowIn. Если // подкидывать сейчас некому (ThrowOrder пуст — вырожденный случай), // возвращает -1. func (g *DurakGameState) CurrentThrower() int { if len(g.ThrowOrder) == 0 { return -1 } return g.ThrowOrder[g.ThrowTurnPos] } func hasCard(hand []Card, c Card) bool { for _, card := range hand { if card == c { return true } } return false } func removeCard(hand []Card, c Card) []Card { for i, card := range hand { if card == c { return append(hand[:i], hand[i+1:]...) } } return hand } // tableHasRank проверяет, встречается ли ранг card среди уже // сыгранных на столе карт (атакующих и отбивающих) — именно такими // картами можно подкидывать. func (g *DurakGameState) tableHasRank(r Rank) bool { for _, pair := range g.Table { if pair.Attack.Rank == r { return true } if pair.Defend != nil && pair.Defend.Rank == r { return true } } return false } // hasThrowableCard проверяет, есть ли у игрока idx в руке хотя бы // одна карта, которую сейчас реально можно подкинуть (ранг уже // встречается на столе, и лимит атаки не превышен). Используется, // чтобы не заставлять игрока (человека или бота) явно пасовать, // когда подкинуть заведомо нечего — пустая рука тоже считается // "нечего подкинуть". func (g *DurakGameState) hasThrowableCard(idx int) bool { if idx < 0 || idx >= len(g.Players) { return false } if len(g.Table) >= g.attackCap { return false } for _, c := range g.Players[idx].Hand { if g.tableHasRank(c.Rank) { return true } } return false } // ThrowIn подкидывает карту на стол. В фазе DurakPhaseAttack это // обязательный первый ход раунда (действует атакующий, стол ещё // пуст). В фазе DurakPhaseThrowIn действует игрок из ThrowOrder, // чья сейчас очередь (CurrentThrower) — карта должна быть рангом, // уже встречающимся на столе. func (g *DurakGameState) ThrowIn(card Card) error { switch g.Phase { case DurakPhaseAttack: return g.throwInAsAttacker(card) case DurakPhaseThrowIn: return g.throwInAsThrower(card) default: return ErrDurakWrongPhase } } func (g *DurakGameState) throwInAsAttacker(card Card) error { if !hasCard(g.attacker().Hand, card) { return ErrDurakCardNotInHand } if len(g.Table) >= g.attackCap { return ErrDurakInvalidThrowIn } g.attacker().Hand = removeCard(g.attacker().Hand, card) g.Table = append(g.Table, durakTablePair{Attack: card}) g.Phase = DurakPhaseDefend return nil } func (g *DurakGameState) throwInAsThrower(card Card) error { throwerIdx := g.CurrentThrower() if throwerIdx == -1 { return ErrDurakWrongPhase } thrower := g.Players[throwerIdx] if !hasCard(thrower.Hand, card) { return ErrDurakCardNotInHand } if !g.tableHasRank(card.Rank) { return ErrDurakInvalidThrowIn } if len(g.Table) >= g.attackCap { return ErrDurakInvalidThrowIn } thrower.Hand = removeCard(thrower.Hand, card) g.Table = append(g.Table, durakTablePair{Attack: card}) g.passedSinceThrow = 0 g.advanceThrowTurn() g.Phase = DurakPhaseDefend return nil } // Pass — игрок, чья сейчас очередь в фазе DurakPhaseThrowIn, // отказывается подкидывать. Если после этого все по кругу подряд // отказались (или желающих подкидывать не осталось) — раунд уходит // в отбой. func (g *DurakGameState) Pass() error { if g.Phase != DurakPhaseThrowIn { return ErrDurakWrongPhase } if len(g.ThrowOrder) == 0 { g.resolveBito() return nil } g.passedSinceThrow++ g.advanceThrowTurn() if g.passedSinceThrow >= len(g.ThrowOrder) { g.resolveBito() } return nil } func (g *DurakGameState) advanceThrowTurn() { if len(g.ThrowOrder) == 0 { return } g.ThrowTurnPos = (g.ThrowTurnPos + 1) % len(g.ThrowOrder) } // enterThrowInPhase переводит раунд в фазу подкидывания после // успешной защиты — либо сразу завершает раунд отбоем, если лимит // атаки уже исчерпан или подкидывать больше некому. func (g *DurakGameState) enterThrowInPhase() { if len(g.Table) >= g.attackCap || len(g.ThrowOrder) == 0 { g.resolveBito() return } g.Phase = DurakPhaseThrowIn } // Defend — защищающийся отбивает последнюю подкинутую карту. func (g *DurakGameState) Defend(card Card) error { if g.Phase != DurakPhaseDefend { return ErrDurakWrongPhase } if len(g.Table) == 0 { return ErrDurakNothingToDefend } last := &g.Table[len(g.Table)-1] if last.Defend != nil { return ErrDurakNothingToDefend } if !hasCard(g.defender().Hand, card) { return ErrDurakCardNotInHand } if !durakBeats(card, last.Attack, g.TrumpSuit) { return ErrDurakInvalidDefend } g.defender().Hand = removeCard(g.defender().Hand, card) last.Defend = &card g.enterThrowInPhase() return nil } // resolveBito завершает раунд "бито": все карты на столе выходят из // игры навсегда, роли переходят по кругу от защищающегося. func (g *DurakGameState) resolveBito() { oldAttacker, oldDefender := g.AttackerIdx, g.DefenderIdx g.Table = nil g.takesSinceBito = 0 refillOrder := g.refillOrderFrom(oldAttacker, oldDefender) g.refill(refillOrder) g.finishBout(oldDefender) } // Take — защищающийся забирает весь стол себе в руку. func (g *DurakGameState) Take() error { if g.Phase != DurakPhaseDefend { return ErrDurakWrongPhase } if len(g.Table) == 0 { return ErrDurakEmptyTable } for _, pair := range g.Table { g.defender().Hand = append(g.defender().Hand, pair.Attack) if pair.Defend != nil { g.defender().Hand = append(g.defender().Hand, *pair.Defend) } } oldAttacker, oldDefender := g.AttackerIdx, g.DefenderIdx g.Table = nil g.takesSinceBito++ if g.takesSinceBito >= maxTakesWithoutBito { g.Result = &DurakResult{Stalemate: true} g.Phase = DurakPhaseGameOver return nil } // защищающийся забрал карты — добор ему не нужен, он и так с // полной рукой; добирают все остальные, начиная с атакующего refillOrder := g.refillOrderFrom(oldAttacker, oldDefender) filtered := refillOrder[:0] for _, idx := range refillOrder { if idx != oldDefender { filtered = append(filtered, idx) } } g.refill(filtered) g.finishBoutAfterTake(oldDefender) return nil } // refillOrderFrom строит порядок добора карт: начиная с атакующего // раунда, по кругу, заканчивая защищающимся — учитывая только ещё // не выбывших игроков. func (g *DurakGameState) refillOrderFrom(attackerIdx, defenderIdx int) []int { order := []int{} seen := map[int]bool{} idx := attackerIdx for { if !g.Players[idx].Out && !seen[idx] { order = append(order, idx) seen[idx] = true } if idx == defenderIdx { break } idx = g.nextActive(idx) if len(order) > len(g.Players) { break // защита от зацикливания в вырожденных случаях } } return order } func (g *DurakGameState) refill(order []int) { for _, idx := range order { p := g.Players[idx] for len(p.Hand) < 6 && len(g.Talon) > 0 { p.Hand = append(p.Hand, g.drawAny()) } } } // finishBout завершает раунд по "бито": роли переходят к следующей // паре игроков по кругу от старого защищающегося. func (g *DurakGameState) finishBout(oldDefenderIdx int) { g.markOutPlayers() if g.checkGameOver() { return } g.AttackerIdx = g.nextActive(oldDefenderIdx) g.DefenderIdx = g.nextActive(g.AttackerIdx) g.startNewBout() } // finishBoutAfterTake завершает раунд после того, как защищающийся // забрал стол: атакующим становится следующий по кругу от него. func (g *DurakGameState) finishBoutAfterTake(oldDefenderIdx int) { g.markOutPlayers() if g.checkGameOver() { return } g.AttackerIdx = g.nextActive(oldDefenderIdx) g.DefenderIdx = g.nextActive(g.AttackerIdx) g.startNewBout() } // markOutPlayers отмечает как выбывших (в безопасности) игроков, // у которых закончились и карты на руке, и колода для добора. func (g *DurakGameState) markOutPlayers() { if len(g.Talon) > 0 { return } for _, p := range g.Players { if len(p.Hand) == 0 { p.Out = true } } } // checkGameOver проверяет, не осталось ли активных (не вышедших) // игроков меньше двух — тогда партия завершена. func (g *DurakGameState) checkGameOver() bool { active := make([]int, 0, len(g.Players)) for i, p := range g.Players { if !p.Out { active = append(active, i) } } switch len(active) { case 0: g.Result = &DurakResult{NoLoser: true} g.Phase = DurakPhaseGameOver return true case 1: g.Result = &DurakResult{Loser: active[0]} g.Phase = DurakPhaseGameOver return true default: return false } }