go_games_collection/blackjack_tui.go
2026-07-12 13:49:34 +03:00

295 lines
7.6 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 (
"fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
)
// BlackjackModel — TUI поверх BlackjackGameState. Действий у
// игрока немного и они не требуют выбора карты (в отличие от
// Тонка/Дурака) — только hit/stand/double, поэтому курсор по руке
// не нужен.
type BlackjackModel struct {
game *BlackjackGameState
humanIdx int
bot BlackjackBot
message string
isError bool
quitting bool
backToMenu bool
}
// NewBlackjackModel создаёт новую партию: человек — игрок 0, дальше
// numPlayers-1 ботов со случайными именами (тот же пул, что и в
// Тонке — это просто имена, без всякого смысла про уровень игры).
func NewBlackjackModel(numPlayers, startingCapital, bet int) BlackjackModel {
if numPlayers < 1 {
numPlayers = 1
}
if numPlayers > 6 {
numPlayers = 6
}
names := []string{T("common.you")}
if numPlayers > 1 {
botNames, _ := newBotRoster(numPlayers - 1)
names = append(names, botNames...)
}
return BlackjackModel{
game: NewBlackjackGame(names, 0, startingCapital, bet),
humanIdx: 0,
bot: BlackjackBot{},
}
}
func (m BlackjackModel) Init() tea.Cmd {
return m.maybeScheduleBot()
}
func (m BlackjackModel) maybeScheduleBot() tea.Cmd {
if m.game.Phase != BJPhasePlayerTurn {
return nil
}
if m.game.CurrentPlayerIdx == m.humanIdx {
return nil
}
return tea.Tick(botMoveDelay, func(time.Time) tea.Msg { return botMoveMsg{} })
}
func (m *BlackjackModel) setInfo(format string, args ...interface{}) {
m.message = fmt.Sprintf(format, args...)
m.isError = false
}
func (m *BlackjackModel) setError(err error) {
m.message = T(err.Error())
m.isError = true
}
func (m BlackjackModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m.handleKey(msg)
case botMoveMsg:
return m.handleBotMove()
}
return m, nil
}
func (m BlackjackModel) handleBotMove() (tea.Model, tea.Cmd) {
if m.game.Phase != BJPhasePlayerTurn {
return m, nil
}
idx := m.game.CurrentPlayerIdx
if idx == m.humanIdx {
return m, nil
}
if err := m.bot.PlayFullTurn(m.game); err != nil {
m.setError(fmt.Errorf(T("бот %s: %w"), m.game.Players[idx].Name, err))
return m, nil
}
m.setInfo(T("tonk.msg.bot_moved"), m.game.Players[idx].Name)
return m, m.maybeScheduleBot()
}
func (m BlackjackModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
key := msg.String()
if key == "ctrl+c" {
m.quitting = true
return m, tea.Quit
}
if key == "q" {
m.backToMenu = true
return m, nil
}
if m.game.Phase == BJPhaseRoundOver {
if key == "n" && !m.humanIsBankrupt() {
m.game = m.game.NextRound()
m.message = ""
m.isError = false
if m.humanIdx >= 0 && m.humanIdx < len(m.game.RoundBetReduced) && m.game.RoundBetReduced[m.humanIdx] {
if m.game.Players[m.humanIdx].Bet == 0 {
m.setInfo(T("blackjack.msg.no_funds"))
} else {
m.setInfo(T("blackjack.msg.bet_reduced"), m.game.Players[m.humanIdx].Bet)
}
}
return m, m.maybeScheduleBot()
}
return m, nil
}
if m.game.CurrentPlayerIdx != m.humanIdx {
// сейчас ходит бот — игнорируем остальной ввод
return m, nil
}
switch key {
case "h":
if err := m.game.Hit(); err != nil {
m.setError(err)
return m, nil
}
m.setInfo(T("blackjack.msg.hit"))
return m, m.maybeScheduleBot()
case "s":
if err := m.game.Stand(); err != nil {
m.setError(err)
return m, nil
}
m.setInfo(T("blackjack.msg.stand"))
return m, m.maybeScheduleBot()
case "d":
if err := m.game.DoubleDown(); err != nil {
m.setError(err)
return m, nil
}
m.setInfo(T("blackjack.msg.double"))
return m, m.maybeScheduleBot()
}
return m, nil
}
// --- Отрисовка -----------------------------------------------------
func renderHandCards(hand []Card) string {
if len(hand) == 0 {
return dimStyle.Render(T("common.no_cards"))
}
return renderCardBoxHand(hand, nil)
}
func statusLabel(s BlackjackStatus) string {
switch s {
case BJStatusStood:
return T("blackjack.status.stood")
case BJStatusBusted:
return T("blackjack.status.busted")
case BJStatusBlackjack:
return T("blackjack.status.blackjack")
default:
return T("blackjack.status.playing")
}
}
func (m BlackjackModel) renderPlayers() string {
var b strings.Builder
for i, p := range m.game.Players {
marker := " "
isTurn := m.game.Phase == BJPhasePlayerTurn && i == m.game.CurrentPlayerIdx
if isTurn {
marker = "> "
}
total := handTotal(p.Hand)
header := fmt.Sprintf(T("blackjack.player_header"), marker, p.Name, p.Bet, total, statusLabel(p.Status))
if isTurn {
header = turnStyle.Render(header)
}
fmt.Fprintln(&b, header)
fmt.Fprintln(&b, renderHandCards(p.Hand))
fmt.Fprintln(&b)
}
return strings.TrimRight(b.String(), "\n")
}
func (m BlackjackModel) renderDealer() string {
if m.game.Phase == BJPhasePlayerTurn {
// вторая карта дилера скрыта, пока не завершится раунд
visible := renderCardBox(m.game.Dealer[0], CardNormal)
hidden := renderFaceDownBox()
lines := make([]string, 4)
for i := 0; i < 4; i++ {
lines[i] = visible[i] + " " + hidden[i]
}
return strings.Join(lines, "\n")
}
return Tf("blackjack.hand_with_total", renderHandCards(m.game.Dealer), handTotal(m.game.Dealer))
}
func (m BlackjackModel) humanIsBankrupt() bool {
if m.humanIdx < 0 || m.humanIdx >= len(m.game.Players) {
return false
}
return m.game.Players[m.humanIdx].Balance == 0
}
func (m BlackjackModel) View() string {
if m.quitting {
return T("common.goodbye")
}
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("blackjack.title")))
fmt.Fprintln(&b)
fmt.Fprintf(&b, T("blackjack.dealer_line"), m.renderDealer())
fmt.Fprintln(&b, m.renderPlayers())
fmt.Fprintln(&b)
if m.game.Phase == BJPhaseRoundOver {
fmt.Fprintln(&b, m.renderResult())
fmt.Fprintln(&b)
if m.humanIsBankrupt() {
fmt.Fprintln(&b, errorStyle.Render(T("blackjack.bankrupt")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, dimStyle.Render(T("blackjack.hint.bankrupt")))
} else {
fmt.Fprintln(&b, dimStyle.Render(T("blackjack.hint.gameover")))
}
return b.String()
}
if m.game.CurrentPlayerIdx == m.humanIdx {
fmt.Fprintln(&b, T("blackjack.hint.turn"))
} else {
fmt.Fprintf(&b, T("common.thinking"), m.game.Players[m.game.CurrentPlayerIdx].Name)
}
fmt.Fprintln(&b)
if m.message != "" {
if m.isError {
fmt.Fprintln(&b, errorStyle.Render("! "+m.message))
} else {
fmt.Fprintln(&b, infoStyle.Render(m.message))
}
}
return b.String()
}
func (m BlackjackModel) renderResult() string {
res := m.game.Result
var b strings.Builder
dealerLine := Tf("blackjack.hand_with_total", renderHandCards(m.game.Dealer), res.DealerTotal)
if res.DealerBusted {
dealerLine += T("blackjack.busted_suffix")
}
fmt.Fprintln(&b, dealerLine)
fmt.Fprintln(&b)
outcomeText := map[string]string{
"blackjack": T("blackjack.outcome.blackjack"),
"win": T("blackjack.outcome.win"),
"push": T("blackjack.outcome.push"),
"lose": T("blackjack.outcome.lose"),
"bust": T("blackjack.outcome.bust"),
}
for i, p := range m.game.Players {
r := res.PlayerResults[i]
style := infoStyle
if r.Delta < 0 {
style = errorStyle
} else if r.Delta > 0 {
style = winStyle
}
fmt.Fprintln(&b, style.Render(fmt.Sprintf(T("blackjack.result_line"),
p.Name, outcomeText[r.Outcome], r.Delta, p.Balance)))
}
return strings.TrimRight(b.String(), "\n")
}