package main import "errors" // ПРАВИЛА В ЭТОЙ РЕАЛИЗАЦИИ: каждый игрок играет против дилера (не // друг против друга). Дилер берёт карты, пока сумма меньше 17, и // останавливается на любых 17 (включая "мягкие" 17 с тузом) — это // осознанный выбор в пользу простоты и предсказуемости, распростра- // нённый вариант казино-правил. Сплиты не поддерживаются, удвоение // ставки — только на первых двух картах. // BlackjackStatus — состояние руки игрока в текущем раунде. type BlackjackStatus int const ( BJStatusPlaying BlackjackStatus = iota BJStatusStood BJStatusBusted BJStatusBlackjack ) // BlackjackPlayer — состояние одного игрока за столом. type BlackjackPlayer struct { Name string Hand []Card IsHuman bool Balance int Bet int Status BlackjackStatus } // BJPhase — фаза текущего раунда. type BJPhase int const ( // BJPhasePlayerTurn — ходит игрок с индексом CurrentPlayerIdx: // взять карту, остановиться или удвоить ставку. BJPhasePlayerTurn BJPhase = iota // BJPhaseRoundOver — раунд завершён, дилер сыграл, выплаты // посчитаны. BJPhaseRoundOver ) var ( ErrBJWrongPhase = errors.New("недопустимое действие для текущей фазы") ErrBJNotEnoughFunds = errors.New("недостаточно средств для этого действия") ErrBJCannotDouble = errors.New("удвоить ставку можно только на первых двух картах") ) // BlackjackPlayerResult — итог одного игрока в завершившемся раунде. type BlackjackPlayerResult struct { Outcome string // "blackjack", "win", "push", "lose", "bust" Delta int // изменение баланса за раунд (относительно капитала до ставки) } // BlackjackResult — итог завершившегося раунда. type BlackjackResult struct { DealerBusted bool DealerTotal int PlayerResults []BlackjackPlayerResult } // BlackjackGameState — полное состояние раунда блэкджека. type BlackjackGameState struct { Players []*BlackjackPlayer Dealer []Card Deck *Deck CurrentPlayerIdx int Phase BJPhase Bet int // фиксированная базовая ставка за раунд StartingCapital int Result *BlackjackResult // RoundsPlayed — сколько раундов уже сыграно, включая текущий. // Нужно только для правила сторимода "минимум 3 раунда, чтобы // засчиталась победа" — на обычную игру никак не влияет. RoundsPlayed int // RoundBetReduced[i] == true, если у игрока i не хватило баланса // на полную ставку Bet в этом раунде — тогда он честно ставит // всё, что у него осталось (в том числе 0, если денег нет вовсе), // но никогда не уходит в минус. RoundBetReduced []bool preRoundBalance []int } // blackjackCardValue — очковая стоимость карты для блэкджека: туз // по умолчанию считается за 11 (см. handTotal, где мягкие тузы при // переборе понижаются до 1), картинки — за 10, остальные — по // номиналу. func blackjackCardValue(r Rank) int { switch { case r == Ace: return 11 case r >= Jack: return 10 default: return int(r) } } // handTotal считает сумму очков руки с учётом мягких тузов: каждый // туз сначала считается за 11, но при переборе поочерёдно // понижается до 1, пока сумма не окажется 21 или меньше (либо тузы // не закончатся). func handTotal(hand []Card) int { total := 0 aces := 0 for _, c := range hand { total += blackjackCardValue(c.Rank) if c.Rank == Ace { aces++ } } for total > 21 && aces > 0 { total -= 10 aces-- } return total } // isNaturalBlackjack проверяет, что рука — блэкджек с раздачи: ровно // 2 карты и сумма 21 (туз + карта на 10 очков). func isNaturalBlackjack(hand []Card) bool { return len(hand) == 2 && handTotal(hand) == 21 } // NewBlackjackGame создаёт новый раунд: тасует колоду, списывает // ставку с каждого игрока, раздаёт по 2 карты игрокам и дилеру, // сразу отмечает натуральные блэкджеки и передаёт ход первому // игроку, которому есть что решать. func NewBlackjackGame(names []string, humanIdx int, startingCapital, bet int) *BlackjackGameState { return NewBlackjackRound(names, humanIdx, startingCapital, bet, nil) } // NewBlackjackRound — то же самое, что NewBlackjackGame, но // позволяет передать капитал игроков с предыдущего раунда (balances // == nil означает "все со стартовым капиталом" — используется при // самом первом раунде партии). func NewBlackjackRound(names []string, humanIdx int, startingCapital, bet int, balances []int) *BlackjackGameState { deck := NewDeck() deck.Shuffle() players := make([]*BlackjackPlayer, len(names)) for i, name := range names { balance := startingCapital if balances != nil { balance = balances[i] } players[i] = &BlackjackPlayer{ Name: name, IsHuman: i == humanIdx, Balance: balance, } } g := &BlackjackGameState{ Players: players, Deck: deck, Bet: bet, StartingCapital: startingCapital, RoundsPlayed: 1, } g.preRoundBalance = make([]int, len(players)) g.RoundBetReduced = make([]bool, len(players)) for i, p := range players { g.preRoundBalance[i] = p.Balance roundBet := bet if roundBet > p.Balance { roundBet = p.Balance g.RoundBetReduced[i] = true } if roundBet < 0 { roundBet = 0 } p.Balance -= roundBet p.Bet = roundBet } // раздача: по 2 карты каждому игроку и дилеру, по одной за проход for round := 0; round < 2; round++ { for _, p := range g.Players { p.Hand = append(p.Hand, g.draw()) } g.Dealer = append(g.Dealer, g.draw()) } for _, p := range g.Players { if isNaturalBlackjack(p.Hand) { p.Status = BJStatusBlackjack } } g.CurrentPlayerIdx = -1 g.advanceToNextPlayerOrResolve() return g } func (g *BlackjackGameState) draw() Card { if g.Deck.Len() == 0 { // вырожденный случай: даже одна полная колода на 6 игроков // не должна кончиться за один раунд, но на всякий случай // подмешиваем новую, чтобы не паниковать в рантайме fresh := NewDeck() fresh.Shuffle() g.Deck = fresh } c, _ := g.Deck.Draw() return c } func (g *BlackjackGameState) current() *BlackjackPlayer { if g.CurrentPlayerIdx < 0 || g.CurrentPlayerIdx >= len(g.Players) { return nil } return g.Players[g.CurrentPlayerIdx] } // Hit — текущий игрок берёт ещё одну карту. func (g *BlackjackGameState) Hit() error { if g.Phase != BJPhasePlayerTurn { return ErrBJWrongPhase } p := g.current() p.Hand = append(p.Hand, g.draw()) if handTotal(p.Hand) > 21 { p.Status = BJStatusBusted g.advanceToNextPlayerOrResolve() } return nil } // Stand — текущий игрок останавливается со своей текущей суммой. func (g *BlackjackGameState) Stand() error { if g.Phase != BJPhasePlayerTurn { return ErrBJWrongPhase } p := g.current() p.Status = BJStatusStood g.advanceToNextPlayerOrResolve() return nil } // DoubleDown — удвоить ставку, взять ровно одну карту и // автоматически остановиться. Доступно только на первых двух картах // и только если хватает капитала на доплату ставки. func (g *BlackjackGameState) DoubleDown() error { if g.Phase != BJPhasePlayerTurn { return ErrBJWrongPhase } p := g.current() if len(p.Hand) != 2 { return ErrBJCannotDouble } if p.Balance < p.Bet { return ErrBJNotEnoughFunds } p.Balance -= p.Bet p.Bet *= 2 p.Hand = append(p.Hand, g.draw()) if handTotal(p.Hand) > 21 { p.Status = BJStatusBusted } else { p.Status = BJStatusStood } g.advanceToNextPlayerOrResolve() return nil } // advanceToNextPlayerOrResolve передаёт ход следующему игроку, у // которого статус ещё "playing"; если таких не осталось — доигрывает // дилера и считает выплаты. func (g *BlackjackGameState) advanceToNextPlayerOrResolve() { for idx := g.CurrentPlayerIdx + 1; idx < len(g.Players); idx++ { if g.Players[idx].Status == BJStatusPlaying { g.CurrentPlayerIdx = idx g.Phase = BJPhasePlayerTurn return } } g.resolveDealerAndPayouts() } // resolveDealerAndPayouts доигрывает руку дилера (берёт карты, пока // сумма меньше 17, останавливается на любых 17) и рассчитывает // выплаты каждому игроку. func (g *BlackjackGameState) resolveDealerAndPayouts() { anyoneNeedsDealer := false for _, p := range g.Players { if p.Status == BJStatusStood || p.Status == BJStatusBlackjack { anyoneNeedsDealer = true break } } if anyoneNeedsDealer { for handTotal(g.Dealer) < 17 { g.Dealer = append(g.Dealer, g.draw()) } } dealerTotal := handTotal(g.Dealer) dealerBusted := dealerTotal > 21 dealerBlackjack := isNaturalBlackjack(g.Dealer) results := make([]BlackjackPlayerResult, len(g.Players)) for i, p := range g.Players { switch p.Status { case BJStatusBusted: results[i] = BlackjackPlayerResult{Outcome: "bust", Delta: 0} case BJStatusBlackjack: if dealerBlackjack { p.Balance += p.Bet results[i] = BlackjackPlayerResult{Outcome: "push"} } else { payout := p.Bet + p.Bet*3/2 p.Balance += payout results[i] = BlackjackPlayerResult{Outcome: "blackjack"} } case BJStatusStood: playerTotal := handTotal(p.Hand) switch { case dealerBusted || playerTotal > dealerTotal: p.Balance += p.Bet * 2 results[i] = BlackjackPlayerResult{Outcome: "win"} case playerTotal == dealerTotal: p.Balance += p.Bet results[i] = BlackjackPlayerResult{Outcome: "push"} default: results[i] = BlackjackPlayerResult{Outcome: "lose"} } } results[i].Delta = p.Balance - g.preRoundBalance[i] } g.Result = &BlackjackResult{ DealerBusted: dealerBusted, DealerTotal: dealerTotal, PlayerResults: results, } g.Phase = BJPhaseRoundOver } // NextRound начинает новый раунд, перенося капитал игроков из // завершившегося раунда. func (g *BlackjackGameState) NextRound() *BlackjackGameState { names := make([]string, len(g.Players)) balances := make([]int, len(g.Players)) humanIdx := -1 for i, p := range g.Players { names[i] = p.Name balances[i] = p.Balance if p.IsHuman { humanIdx = i } } next := NewBlackjackRound(names, humanIdx, g.StartingCapital, g.Bet, balances) next.RoundsPlayed = g.RoundsPlayed + 1 return next }