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

502 lines
17 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
}
// ThousandPhase — фаза текущей раздачи.
type ThousandPhase int
const (
ThousandPhaseBidding ThousandPhase = iota
ThousandPhaseDiscard
ThousandPhaseTrick
ThousandPhaseHandOver
)
var (
ErrThousandWrongPhase = errors.New("недопустимое действие для текущей фазы")
ErrThousandCardNotInHand = errors.New("карты нет на руке игрока")
ErrThousandBidTooLow = errors.New("ставка должна быть выше текущей минимум на 5")
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
}
// 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
}
// MatchTarget — партия идёт до этой суммы очков.
const ThousandMatchTarget = 1000
// 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() {
deck := NewThousandDeck()
deck.Shuffle()
for _, p := range g.Players {
p.Hand = nil
p.HandPoints = 0
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.Phase = ThousandPhaseBidding
}
func (g *ThousandGameState) current() *ThousandPlayer {
return g.Players[g.CurrentBidderIdx]
}
func (g *ThousandGameState) maxBidFor(playerIdx int) int {
return 120 + bestMarriageValue(g.Players[playerIdx].Hand)
}
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 — текущий игрок в торгах называет ставку.
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.HandPoints += thousandMarriageValue[card.Suit]
g.TrumpSuit = card.Suit
g.TrumpSet = 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() {
deltas := make([]int, 3)
made := g.Players[g.DeclarerIdx].HandPoints >= g.FinalBid
if made {
deltas[g.DeclarerIdx] = g.FinalBid
} else {
deltas[g.DeclarerIdx] = -g.FinalBid
}
for i, p := range g.Players {
if i == g.DeclarerIdx {
continue
}
deltas[i] = p.HandPoints
}
for i, p := range g.Players {
p.Score += deltas[i]
}
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
}
// NextHand начинает новую раздачу (сдающий переходит по кругу),
// сохраняя счёт игроков.
func (g *ThousandGameState) NextHand() {
if g.Result != nil && g.Result.MatchOver {
return
}
g.DealerIdx = (g.DealerIdx + 1) % 3
g.dealNewHand()
}