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{"Вы"} 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 = 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("бот %s: %w", m.game.Players[idx].Name, err)) return m, nil } m.setInfo("%s сходил(а).", 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("Вы взяли карту.") return m, m.maybeScheduleBot() case "s": if err := m.game.Stand(); err != nil { m.setError(err) return m, nil } m.setInfo("Вы остановились.") return m, m.maybeScheduleBot() case "d": if err := m.game.DoubleDown(); err != nil { m.setError(err) return m, nil } m.setInfo("Вы удвоили ставку.") return m, m.maybeScheduleBot() } return m, nil } // --- Отрисовка ----------------------------------------------------- func renderHandCards(hand []Card) string { if len(hand) == 0 { return dimStyle.Render("(нет карт)") } 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 "стоп" case BJStatusBusted: return "перебор" case BJStatusBlackjack: return "блэкджек!" default: return "ходит" } } 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 if i == m.humanIdx { name += " (вы)" } total := handTotal(p.Hand) line := fmt.Sprintf("%s%-20s ставка: %-4d карты: %s (%d) [%s]", 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("%s (%d)", renderHandCards(m.game.Dealer), handTotal(m.game.Dealer)) } func (m BlackjackModel) View() string { if m.quitting { return "До встречи!\n" } var b strings.Builder fmt.Fprintln(&b, titleStyle.Render("=== БЛЭКДЖЕК ===")) fmt.Fprintln(&b) fmt.Fprintf(&b, "Дилер: %s\n\n", 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("[n] новый раунд [q] в меню")) return b.String() } if m.game.CurrentPlayerIdx == m.humanIdx { fmt.Fprintln(&b, "[h] взять карту [s] остановиться [d] удвоить ставку [q] в меню") } else { fmt.Fprintf(&b, "%s думает...\n", 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("Дилер: %s (%d)", renderHandCards(m.game.Dealer), res.DealerTotal) if res.DealerBusted { dealerLine += " — перебор!" } fmt.Fprintln(&b, dealerLine) fmt.Fprintln(&b) outcomeText := map[string]string{ "blackjack": "блэкджек! выигрыш 3:2", "win": "выигрыш", "push": "ничья, ставка возвращена", "lose": "проигрыш", "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(" %-20s %-24s изменение капитала: %+d (итого: %d)", p.Name, outcomeText[r.Outcome], r.Delta, p.Balance))) } return strings.TrimRight(b.String(), "\n") }