go_games_collection/durak_game.go
2026-07-04 13:51:04 +03:00

538 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
// 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
}
}