go_games_collection/blackjack_tui.go
2026-07-04 13:51:04 +03:00

276 lines
7 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{"Вы"}
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")
}