go_games_collection/blackjack_game.go
2026-07-10 09:12:06 +03:00

355 lines
12 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"
// ПРАВИЛА В ЭТОЙ РЕАЛИЗАЦИИ: каждый игрок играет против дилера (не
// друг против друга). Дилер берёт карты, пока сумма меньше 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
// 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,
}
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
}
}
return NewBlackjackRound(names, humanIdx, g.StartingCapital, g.Bet, balances)
}