559 lines
20 KiB
Go
559 lines
20 KiB
Go
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
|
||
}
|
||
}
|