340 lines
11 KiB
Go
340 lines
11 KiB
Go
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
|
||
|
||
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))
|
||
for i, p := range players {
|
||
g.preRoundBalance[i] = p.Balance
|
||
p.Balance -= bet
|
||
p.Bet = bet
|
||
}
|
||
|
||
// раздача: по 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)
|
||
}
|