go_games_collection/thousand_game.go

698 lines
26 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"
// ПРИМЕЧАНИЕ О ПРАВИЛАХ: у "Тысячи" очень много вариантов и
// договорённостей (бочка, золотой кон, самосвал, тузовый марьяж,
// игра втёмную, "расписать игру" и т.д.). Реализован базовый,
// самый распространённый вариант ИМЕННО НА ТРОИХ игроков (у игры
// вдвоём/вчетвером заметно другая механика прикупа и сдающего —
// сознательно не реализована). Набор правил:
// - Колода 24 карты (9,В,Д,К,10,Т в каждой масти). Старшинство:
// 9 < В < Д < К < 10 < Т (да, десятка сильнее короля, но
// слабее туза — особенность именно этой игры).
// - Раздача: по 7 карт каждому, 3 карты — в прикуп.
// - Торги: первый ходящий (слева от сдающего) обязан назвать
// не менее 100 очков. Далее по кругу каждый либо поднимает
// ставку (шаг 5, до 120 без марьяжа на руке, либо до
// 120+стоимость марьяжа, если марьяж есть), либо пасует.
// Спасовавший больше не участвует в этих торгах. Торги
// заканчиваются, когда остаётся один игрок — он берёт прикуп.
// - Победитель торгов забирает прикуп (10 карт на руке), отдаёт
// по одной карте каждому из двух соперников (по одной,
// соперникам становится по 8 карт, у него самого остаётся 8) и
// ходит первым.
// - Взятки: обязательно ходить в масть; если её нет — козырем
// (если он уже объявлен); если и его нет — чем угодно. Взятку
// берёт старшая карта хода, либо старший козырь, если он был
// подложен.
// - Марьяж: если у игрока, который сейчас ходит (не в первой
// взятке раздачи), есть на руке дама и король одной масти, он
// может объявить марьяж, походив с одной из этих карт — тогда
// эта масть становится (или меняется на) козырь до конца
// раздачи, а стоимость марьяжа сразу добавляется к очкам
// игрока за эту раздачу. Стоимость: пики 40, трефы 60, бубны
// 80, червы 100.
// - Подсчёт очков по итогам раздачи: если объявивший торг игрок
// набрал (взятками и марьяжами) не меньше своей ставки — ему
// засчитывается ставка целиком; если меньше — ставка вычитается
// из его счёта. Остальные добавляют себе ровно то, что
// заработали (взятки+марьяжи), без штрафов.
// - Партия идёт до 1000 очков. "Бочка", "золотой кон",
// "самосвал", тузовый марьяж и игра "втёмную" — не реализованы.
// ThousandPlayer — состояние одного игрока в партии 1000.
type ThousandPlayer struct {
Name string
Hand []Card
IsHuman bool
Score int // суммарный счёт по итогам сыгранных раздач
HandPoints int // очки ВЗЯТОК (без марьяжей), набранные в ТЕКУЩЕЙ раздаче
Passed bool
// HandMarriagePoints — очки марьяжей (обычных и тузового) за
// текущую раздачу, отдельно от HandPoints. Разделены специально:
// удвоение золотого кона и игры "в тёмную" марьяжей не касается —
// удваиваются только очки взяток (см. resolveHandEnd).
HandMarriagePoints int
AceMarriageDeclared bool // объявлен ли уже тузовый марьяж в текущей раздаче
// OnBarrel / BarrelAttempts — состояние "бочки" (см. applyBarrelRules).
OnBarrel bool
BarrelAttempts int
}
// ThousandPhase — фаза текущей раздачи.
type ThousandPhase int
const (
ThousandPhaseBidding ThousandPhase = iota
ThousandPhaseDiscard
ThousandPhaseTrick
ThousandPhaseHandOver
)
var (
ErrThousandWrongPhase = errors.New("недопустимое действие для текущей фазы")
ErrThousandCardNotInHand = errors.New("карты нет на руке игрока")
ErrThousandBidTooLow = errors.New("ставка должна быть выше текущей минимум на 5")
ErrThousandBlindTooLate = errors.New("играть втёмную можно только на самом первом ходе торгов")
ErrThousandBidTooHigh = errors.New("ставка превышает разрешённый максимум (120 плюс марьяж на руке)")
ErrThousandMustBid = errors.New("первый ходящий обязан назвать ставку не менее 100 и не может пасовать")
ErrThousandInvalidFollow = errors.New("нужно ходить в масть или козырем, если они есть на руке")
ErrThousandAlreadyDiscarded = errors.New("уже отдано нужное количество карт")
)
// thousandRankOrder — старшинство рангов в Тысяче: девятка младше
// всех, туз старше всех, но десятка стоит МЕЖДУ королём и тузом.
var thousandRankOrder = map[Rank]int{
Nine: 0, Jack: 1, Queen: 2, King: 3, Ten: 4, Ace: 5,
}
// thousandMarriageValue — стоимость козырного марьяжа по масти.
var thousandMarriageValue = map[Suit]int{
Spades: 40, Clubs: 60, Diamonds: 80, Hearts: 100,
}
// NewThousandDeck создаёт колоду для Тысячи: 24 карты (9,В,Д,К,10,Т
// в каждой масти).
func NewThousandDeck() *Deck {
ranks := []Rank{Nine, Jack, Queen, King, Ten, Ace}
d := &Deck{cards: make([]Card, 0, 24)}
for s := Clubs; s <= Spades; s++ {
for _, r := range ranks {
d.cards = append(d.cards, Card{Suit: s, Rank: r})
}
}
return d
}
// thousandCardValue — очковая стоимость карты (та же шкала, что и
// в 101: туз 11, десятка 10, король 4, дама 3, валет 2, девятка 0).
func thousandCardValue(r Rank) int {
return oneOhOneCardValue(r)
}
// hasMarriage проверяет, есть ли на руке король и дама указанной
// масти.
func hasMarriage(hand []Card, suit Suit) bool {
hasKing, hasQueen := false, false
for _, c := range hand {
if c.Suit != suit {
continue
}
if c.Rank == King {
hasKing = true
}
if c.Rank == Queen {
hasQueen = true
}
}
return hasKing && hasQueen
}
// hasAllAces проверяет, есть ли на руке все четыре туза — условие
// тузового марьяжа.
func hasAllAces(hand []Card) bool {
found := map[Suit]bool{}
for _, c := range hand {
if c.Rank == Ace {
found[c.Suit] = true
}
}
return len(found) == 4
}
// bestMarriageValue возвращает наибольшую стоимость марьяжа,
// имеющегося на руке (0, если марьяжей нет).
func bestMarriageValue(hand []Card) int {
best := 0
for s := Clubs; s <= Spades; s++ {
if hasMarriage(hand, s) {
if v := thousandMarriageValue[s]; v > best {
best = v
}
}
}
return best
}
type thousandTrickPlay struct {
PlayerIdx int
Card Card
}
// ThousandHandResult — итог одной завершившейся раздачи.
type ThousandHandResult struct {
DeclarerIdx int
FinalBid int
Made bool // выполнил ли объявивший торг свою ставку
ScoreDeltas []int
MatchOver bool
MatchWinner int
}
// ThousandGameState — полное состояние партии в 1000 (ровно на 3
// игроков).
type ThousandGameState struct {
Players []*ThousandPlayer
Kitty []Card
DealerIdx int
Phase ThousandPhase
Result *ThousandHandResult
// --- Торги ---
CurrentBidderIdx int
HighBid int
HighBidderIdx int
firstBidderIdx int
bidsMade int
// --- Прикуп/снос ---
DeclarerIdx int
FinalBid int
discardsGiven int
discardTargets []int // получатели 1-й и 2-й отданной карты
// --- Взятка ---
TrumpSuit Suit
TrumpSet bool
LeaderIdx int
CurrentTrick []thousandTrickPlay
TricksPlayed int
// HandNumber — порядковый номер текущей раздачи за весь матч
// (считается с 1). Первые ThousandGoldenHands раздач матча —
// "золотой кон" (см. resolveHandEnd).
HandNumber int
// PlayedBlind — текущая раздача объявлена и разыгрывается
// "в тёмную" (см. DeclareBlind).
PlayedBlind bool
}
// MatchTarget — партия идёт до этой суммы очков.
const ThousandMatchTarget = 1000
// ПРИМЕЧАНИЕ О НОВЫХ ПРАВИЛАХ: "бочка", "золотой кон", "самосвал",
// "тузовый марьяж" и "игра втёмную" — народные договорённости с
// заметными региональными расхождениями (единого свода правил не
// существует). Здесь выбран один конкретный, наиболее часто
// встречающийся вариант каждой — подробности см. в комментариях
// у соответствующих функций и в тексте правил игры.
const (
// ThousandBarrelThreshold — при достижении этой суммы очков
// игрок "садится на бочку" (см. applyBarrelRules).
ThousandBarrelThreshold = 880
// ThousandBarrelMaxAttempts — сколько раз подряд сидящий на
// бочке может провалить свою заявку, прежде чем слетит с неё
// со штрафом.
ThousandBarrelMaxAttempts = 3
// ThousandBarrelPenalty — штраф при неудачном слёте с бочки
// (по исчерпании попыток или из-за того, что кто-то другой
// одновременно тоже добрался до бочки).
ThousandBarrelPenalty = 120
// ThousandGoldenHands — первые столько раздач матча считаются
// "золотым коном" (см. resolveHandEnd).
ThousandGoldenHands = 3
// ThousandSamosvalThreshold — при выходе счёта за ±это значение
// счёт обнуляется ("самосвал").
ThousandSamosvalThreshold = 555
// ThousandAceMarriageValue — стоимость тузового марьяжа (все
// четыре туза).
ThousandAceMarriageValue = 200
)
// NewThousandGame создаёт новую партию на ровно 3 игроков.
func NewThousandGame(names []string, humanIdx int) *ThousandGameState {
if len(names) != 3 {
panic("1000 поддерживается только на троих игроков")
}
players := make([]*ThousandPlayer, 3)
for i, name := range names {
players[i] = &ThousandPlayer{Name: name, IsHuman: i == humanIdx}
}
g := &ThousandGameState{Players: players, DealerIdx: 0}
g.dealNewHand()
return g
}
func (g *ThousandGameState) dealNewHand() {
g.HandNumber++
deck := NewThousandDeck()
deck.Shuffle()
for _, p := range g.Players {
p.Hand = nil
p.HandPoints = 0
p.HandMarriagePoints = 0
p.AceMarriageDeclared = false
p.Passed = false
}
for i := 0; i < 3; i++ {
idx := (g.DealerIdx + 1 + i) % 3
g.Players[idx].Hand = append(g.Players[idx].Hand, deck.cards[:7]...)
deck.cards = deck.cards[7:]
}
g.Kitty = append([]Card{}, deck.cards[:3]...)
g.firstBidderIdx = (g.DealerIdx + 1) % 3
g.CurrentBidderIdx = g.firstBidderIdx
g.HighBid = 0
g.HighBidderIdx = -1
g.bidsMade = 0
g.DeclarerIdx = -1
g.FinalBid = 0
g.discardsGiven = 0
g.discardTargets = []int{(g.firstBidderIdx)}
g.TrumpSet = false
g.CurrentTrick = nil
g.TricksPlayed = 0
g.Result = nil
g.PlayedBlind = false
g.Phase = ThousandPhaseBidding
}
func (g *ThousandGameState) current() *ThousandPlayer {
return g.Players[g.CurrentBidderIdx]
}
func (g *ThousandGameState) maxBidFor(playerIdx int) int {
hand := g.Players[playerIdx].Hand
max := 120 + bestMarriageValue(hand)
if hasAllAces(hand) {
max += ThousandAceMarriageValue
}
return max
}
func (g *ThousandGameState) nextBidder(from int) int {
for i := 1; i <= 3; i++ {
idx := (from + i) % 3
if !g.Players[idx].Passed {
return idx
}
}
return from
}
func (g *ThousandGameState) activeBidderCount() int {
n := 0
for _, p := range g.Players {
if !p.Passed {
n++
}
}
return n
}
// Bid — текущий игрок в торгах называет ставку.
// DeclareBlind — первый ходящий в торгах может, не называя обычную
// ставку и не дожидаясь конкурентных торгов, сразу объявить игру
// "в тёмную": фиксированная заявка 120 без торгов. Награда и штраф
// за эту раздачу удваиваются (см. resolveHandEnd), а очки соперников
// от взяток в этой раздаче — тоже удваиваются (марьяжи — нет).
// Доступно только пока никто ещё не сделал ни одной ставки.
func (g *ThousandGameState) DeclareBlind() error {
if g.Phase != ThousandPhaseBidding {
return ErrThousandWrongPhase
}
if g.bidsMade != 0 || g.CurrentBidderIdx != g.firstBidderIdx {
return ErrThousandBlindTooLate
}
g.PlayedBlind = true
g.HighBid = 120
g.HighBidderIdx = g.firstBidderIdx
g.bidsMade++
g.finishBidding()
return nil
}
func (g *ThousandGameState) Bid(amount int) error {
if g.Phase != ThousandPhaseBidding {
return ErrThousandWrongPhase
}
if amount%5 != 0 {
return ErrThousandBidTooLow
}
minRequired := g.HighBid + 5
if g.bidsMade == 0 {
minRequired = 100
}
if amount < minRequired {
return ErrThousandBidTooLow
}
if amount > g.maxBidFor(g.CurrentBidderIdx) {
return ErrThousandBidTooHigh
}
g.HighBid = amount
g.HighBidderIdx = g.CurrentBidderIdx
g.bidsMade++
g.advanceOrFinishBidding()
return nil
}
// Pass — текущий игрок в торгах отказывается от дальнейшего
// участия. Самый первый ходящий обязан назвать ставку и не может
// пасовать.
func (g *ThousandGameState) Pass() error {
if g.Phase != ThousandPhaseBidding {
return ErrThousandWrongPhase
}
if g.bidsMade == 0 && g.CurrentBidderIdx == g.firstBidderIdx {
return ErrThousandMustBid
}
g.Players[g.CurrentBidderIdx].Passed = true
g.advanceOrFinishBidding()
return nil
}
func (g *ThousandGameState) advanceOrFinishBidding() {
if g.activeBidderCount() <= 1 {
g.finishBidding()
return
}
g.CurrentBidderIdx = g.nextBidder(g.CurrentBidderIdx)
}
func (g *ThousandGameState) finishBidding() {
g.DeclarerIdx = g.HighBidderIdx
g.FinalBid = g.HighBid
g.Players[g.DeclarerIdx].Hand = append(g.Players[g.DeclarerIdx].Hand, g.Kitty...)
g.discardTargets = []int{
(g.DeclarerIdx + 1) % 3,
(g.DeclarerIdx + 2) % 3,
}
g.discardsGiven = 0
g.Phase = ThousandPhaseDiscard
}
// DiscardCard — объявивший торг игрок (declarer) отдаёт одну карту
// следующему по очереди сопернику. Вызывается дважды подряд.
func (g *ThousandGameState) DiscardCard(card Card) error {
if g.Phase != ThousandPhaseDiscard {
return ErrThousandWrongPhase
}
if g.discardsGiven >= 2 {
return ErrThousandAlreadyDiscarded
}
p := g.Players[g.DeclarerIdx]
if !hasCard(p.Hand, card) {
return ErrThousandCardNotInHand
}
p.Hand = removeCard(p.Hand, card)
target := g.discardTargets[g.discardsGiven]
g.Players[target].Hand = append(g.Players[target].Hand, card)
g.discardsGiven++
if g.discardsGiven == 2 {
g.LeaderIdx = g.DeclarerIdx
g.Phase = ThousandPhaseTrick
}
return nil
}
func (g *ThousandGameState) ledSuit() (Suit, bool) {
if len(g.CurrentTrick) == 0 {
return 0, false
}
return g.CurrentTrick[0].Card.Suit, true
}
func containsSuit(hand []Card, suit Suit) bool {
for _, c := range hand {
if c.Suit == suit {
return true
}
}
return false
}
// isValidPlay проверяет, разрешено ли сыграть card в текущей
// взятке для игрока с рукой hand.
func (g *ThousandGameState) isValidPlay(hand []Card, card Card) bool {
led, ok := g.ledSuit()
if !ok {
return true // ходит первым — любая карта разрешена (кроме марьяжа см. отдельно)
}
if containsSuit(hand, led) {
return card.Suit == led
}
if g.TrumpSet && containsSuit(hand, g.TrumpSuit) {
return card.Suit == g.TrumpSuit
}
return true
}
// PlayCard — текущий игрок в раскладе взятки играет карту. Если
// это первая карта взятки (игрок ходит), король или дама масти, по
// которой на руке есть парная карта, и это не самая первая взятка
// раздачи — автоматически объявляется марьяж (козырь меняется на
// эту масть, очки марьяжа сразу начисляются).
func (g *ThousandGameState) PlayCard(card Card) error {
if g.Phase != ThousandPhaseTrick {
return ErrThousandWrongPhase
}
actor := g.currentTrickActor()
p := g.Players[actor]
if !hasCard(p.Hand, card) {
return ErrThousandCardNotInHand
}
leading := len(g.CurrentTrick) == 0
if !leading {
if !g.isValidPlay(p.Hand, card) {
return ErrThousandInvalidFollow
}
} else if (card.Rank == King || card.Rank == Queen) && g.TricksPlayed > 0 && hasMarriage(p.Hand, card.Suit) {
p.HandMarriagePoints += thousandMarriageValue[card.Suit]
g.TrumpSuit = card.Suit
g.TrumpSet = true
} else if card.Rank == Ace && g.TricksPlayed > 0 && !p.AceMarriageDeclared && hasAllAces(p.Hand) {
// Тузовый марьяж: игрок, уже взявший хотя бы одну взятку (не
// на первом ходе раздачи — как и обычный марьяж), заходит с
// туза, имея на руках все четыре. Козырь при этом НЕ
// меняется (в отличие от обычного марьяжа) — это отдельный
// вариант правил, региональные соглашения расходятся в том,
// должен ли последний туз тузового марьяжа тоже назначать
// козырь; здесь выбран более простой и чаще встречающийся
// вариант без смены козыря.
p.HandMarriagePoints += ThousandAceMarriageValue
p.AceMarriageDeclared = true
}
p.Hand = removeCard(p.Hand, card)
g.CurrentTrick = append(g.CurrentTrick, thousandTrickPlay{PlayerIdx: actor, Card: card})
if len(g.CurrentTrick) == 3 {
g.resolveTrick()
}
return nil
}
// currentTrickActor возвращает индекс игрока, чья очередь класть
// карту в текущей взятке.
func (g *ThousandGameState) currentTrickActor() int {
return (g.LeaderIdx + len(g.CurrentTrick)) % 3
}
func (g *ThousandGameState) resolveTrick() {
led := g.CurrentTrick[0].Card.Suit
best := g.CurrentTrick[0]
bestIsTrump := g.TrumpSet && best.Card.Suit == g.TrumpSuit
for _, play := range g.CurrentTrick[1:] {
isTrump := g.TrumpSet && play.Card.Suit == g.TrumpSuit
switch {
case isTrump && !bestIsTrump:
best = play
bestIsTrump = true
case isTrump == bestIsTrump && play.Card.Suit == best.Card.Suit && thousandRankOrder[play.Card.Rank] > thousandRankOrder[best.Card.Rank]:
best = play
case !isTrump && !bestIsTrump && play.Card.Suit == led && best.Card.Suit != led:
best = play
}
}
points := 0
for _, play := range g.CurrentTrick {
points += thousandCardValue(play.Card.Rank)
}
g.Players[best.PlayerIdx].HandPoints += points
g.LeaderIdx = best.PlayerIdx
g.CurrentTrick = nil
g.TricksPlayed++
if g.TricksPlayed == 8 {
g.resolveHandEnd()
}
}
func (g *ThousandGameState) resolveHandEnd() {
declarer := g.Players[g.DeclarerIdx]
declarerTotal := declarer.HandPoints + declarer.HandMarriagePoints
made := declarerTotal >= g.FinalBid
deltas := make([]int, 3)
declarerDelta := g.FinalBid
if !made {
declarerDelta = -g.FinalBid
}
if g.PlayedBlind {
declarerDelta *= 2 // удвоенная награда/штраф за игру "в тёмную"
}
deltas[g.DeclarerIdx] = declarerDelta
for i, p := range g.Players {
if i == g.DeclarerIdx {
continue
}
multiplier := 1
if g.HandNumber <= ThousandGoldenHands {
multiplier *= 2 // золотой кон — первые несколько раздач матча
}
if g.PlayedBlind {
multiplier *= 2 // очки соперников удваиваются и при игре втёмную
}
// марьяжи никогда не удваиваются — ни золотым коном, ни тёмной
deltas[i] = p.HandPoints*multiplier + p.HandMarriagePoints
}
g.applyBarrelAdjustments(deltas, made)
for i, p := range g.Players {
p.Score += deltas[i]
}
g.applySamosval()
g.updateBarrelSeating()
result := &ThousandHandResult{
DeclarerIdx: g.DeclarerIdx,
FinalBid: g.FinalBid,
Made: made,
ScoreDeltas: deltas,
}
bestIdx := -1
for i, p := range g.Players {
if p.Score >= ThousandMatchTarget {
if bestIdx == -1 || p.Score > g.Players[bestIdx].Score {
bestIdx = i
}
}
}
if bestIdx != -1 {
result.MatchOver = true
result.MatchWinner = bestIdx
}
g.Result = result
g.Phase = ThousandPhaseHandOver
}
// applyBarrelAdjustments переписывает уже посчитанные "обычные"
// дельты для игроков, сидящих на бочке (см. ThousandBarrelThreshold):
// - объявивший игру и сидящий на бочке при провале своей заявки не
// теряет FinalBid обычным образом — вместо этого засчитывается
// попытка; после ThousandBarrelMaxAttempts подряд неудачных
// попыток он слетает с бочки со штрафом ThousandBarrelPenalty;
// - сидящий на бочке, но не объявлявший игру в этой раздаче, вообще
// не получает очков от чужих взяток — сидя на бочке, можно расти
// только через собственную выполненную заявку.
func (g *ThousandGameState) applyBarrelAdjustments(deltas []int, made bool) {
for i, p := range g.Players {
if !p.OnBarrel {
continue
}
if i == g.DeclarerIdx {
if made {
continue // обычная (уже посчитанная) прибавка верна как есть
}
p.BarrelAttempts++
if p.BarrelAttempts >= ThousandBarrelMaxAttempts {
deltas[i] = -ThousandBarrelPenalty
p.OnBarrel = false
p.BarrelAttempts = 0
} else {
deltas[i] = 0 // попытка провалена, но лимит ещё не исчерпан — счёт не меняется
}
} else {
deltas[i] = 0
}
}
}
// applySamosval обнуляет счёт игрока, если по итогам раздачи он
// оказался ТОЧНО равен ±ThousandSamosvalThreshold — это редкое
// нумерологическое совпадение ("555"), а не общий потолок счёта;
// если бы обнуление срабатывало при ЛЮБОМ превышении, партия
// никогда не смогла бы дойти до цели в 1000 очков.
func (g *ThousandGameState) applySamosval() {
for _, p := range g.Players {
if p.Score == ThousandSamosvalThreshold || p.Score == -ThousandSamosvalThreshold {
p.Score = 0
p.OnBarrel = false
p.BarrelAttempts = 0
}
}
}
// updateBarrelSeating сажает на бочку игрока, чей счёт по итогам
// этой раздачи впервые достиг ThousandBarrelThreshold (но ещё не
// выиграл матч). Одновременно на бочке может сидеть только один
// игрок — если садится новый, прежний слетает со штрафом.
func (g *ThousandGameState) updateBarrelSeating() {
for i, p := range g.Players {
if p.OnBarrel || p.Score < ThousandBarrelThreshold || p.Score >= ThousandMatchTarget {
continue
}
for j, other := range g.Players {
if j != i && other.OnBarrel {
other.OnBarrel = false
other.BarrelAttempts = 0
other.Score -= ThousandBarrelPenalty
}
}
p.OnBarrel = true
p.BarrelAttempts = 0
}
}
// NextHand начинает новую раздачу (сдающий переходит по кругу),
// сохраняя счёт игроков.
func (g *ThousandGameState) NextHand() {
if g.Result != nil && g.Result.MatchOver {
return
}
g.DealerIdx = (g.DealerIdx + 1) % 3
g.dealNewHand()
}