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.game = m.game.NextRound() m.message = "" m.isError = false 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")) } parts := make([]string, len(hand)) for i, c := range hand { parts[i] = renderCard(c) } return strings.Join(parts, " ") } 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 := " " if m.game.Phase == BJPhasePlayerTurn && i == m.game.CurrentPlayerIdx { marker = "> " } name := p.Name total := handTotal(p.Hand) line := fmt.Sprintf(T("blackjack.player_line"), marker, name, p.Bet, renderHandCards(p.Hand), total, statusLabel(p.Status)) if m.game.Phase == BJPhasePlayerTurn && i == m.game.CurrentPlayerIdx { line = turnStyle.Render(line) } fmt.Fprintln(&b, line) } return strings.TrimRight(b.String(), "\n") } func (m BlackjackModel) renderDealer() string { if m.game.Phase == BJPhasePlayerTurn { // вторая карта дилера скрыта, пока не завершится раунд hidden := dimStyle.Render("??") return fmt.Sprintf("%s %s", renderCard(m.game.Dealer[0]), hidden) } return fmt.Sprintf(T("blackjack.dealer_result"), renderHandCards(m.game.Dealer), handTotal(m.game.Dealer)) } 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) 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 := fmt.Sprintf(T("blackjack.dealer_result"), 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") }