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

435 lines
14 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"
// ПРИМЕЧАНИЕ О ПРАВИЛАХ: у игры "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
// OneOhOnePhaseRoundOver — раунд завершён.
OneOhOnePhaseRoundOver
)
var (
ErrOOOWrongPhase = errors.New("недопустимое действие для текущей фазы")
ErrOOOCardNotInHand = errors.New("карты нет на руке игрока")
ErrOOOInvalidPlay = errors.New("эта карта не подходит по масти или рангу")
ErrOOOMustPlay = 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
turnsSinceStart int
}
// maxOneOhOneTurns — защита от зацикливания: если раунд длится
// неправдоподобно долго, принудительно завершаем его вничью (по
// текущему весу рук), чтобы партия гарантированно не зависла.
const maxOneOhOneTurns = 5000
// 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
}
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]
}
// canPlay проверяет, подходит ли карта по масти или рангу под
// верхнюю карту сброса.
func (g *OneOhOneGameState) canPlay(card Card) bool {
top := g.topDiscard()
return card.Suit == top.Suit || 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 обрабатывает эффект специальной карты (если
// есть) и передаёт ход дальше.
func (g *OneOhOneGameState) applyEffectAndAdvance(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()
}