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

306 lines
7.4 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"
)
// OneOhOneModel — TUI поверх OneOhOneGameState.
type OneOhOneModel struct {
game *OneOhOneGameState
humanIdx int
bot OneOhOneBot
cursor int
message string
isError bool
quitting bool
backToMenu bool
}
// NewOneOhOneModel создаёт новую партию 101 на numPlayers игроков
// (2-6, включая человека) со случайными именами ботов.
func NewOneOhOneModel(numPlayers int) OneOhOneModel {
if numPlayers < 2 {
numPlayers = 2
}
if numPlayers > 6 {
numPlayers = 6
}
botNames := newOneOhOneBotNames(numPlayers - 1)
names := append([]string{"Вы"}, botNames...)
return OneOhOneModel{
game: NewOneOhOneGame(names, 0),
humanIdx: 0,
bot: OneOhOneBot{},
}
}
func (m OneOhOneModel) Init() tea.Cmd {
return m.maybeScheduleBot()
}
func (m OneOhOneModel) maybeScheduleBot() tea.Cmd {
if m.game.Phase != OneOhOnePhasePlay {
return nil
}
if m.game.CurrentPlayerIdx == m.humanIdx {
return nil
}
return tea.Tick(botMoveDelay, func(time.Time) tea.Msg { return botMoveMsg{} })
}
func (m *OneOhOneModel) resetSelection() {
hand := m.currentHand()
if len(hand) == 0 {
m.cursor = 0
} else if m.cursor >= len(hand) {
m.cursor = len(hand) - 1
}
}
func (m OneOhOneModel) currentHand() []Card {
if m.game.Phase != OneOhOnePhasePlay {
return nil
}
return m.game.Players[m.game.CurrentPlayerIdx].Hand
}
func (m *OneOhOneModel) setInfo(format string, args ...interface{}) {
m.message = fmt.Sprintf(format, args...)
m.isError = false
}
func (m *OneOhOneModel) setError(err error) {
m.message = err.Error()
m.isError = true
}
func (m OneOhOneModel) 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 OneOhOneModel) handleBotMove() (tea.Model, tea.Cmd) {
if m.game.Phase != OneOhOnePhasePlay {
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)
if m.game.Phase == OneOhOnePhasePlay && m.game.CurrentPlayerIdx == m.humanIdx {
m.resetSelection()
}
return m, m.maybeScheduleBot()
}
func (m OneOhOneModel) 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 == OneOhOnePhaseRoundOver {
if key == "n" && !m.game.Result.MatchOver {
m.game.NextRound()
m.message = ""
m.isError = false
m.resetSelection()
return m, m.maybeScheduleBot()
}
return m, nil
}
if m.game.CurrentPlayerIdx != m.humanIdx {
return m, nil
}
hand := m.currentHand()
switch key {
case "left", "h":
if m.cursor > 0 {
m.cursor--
}
case "right", "l":
if m.cursor < len(hand)-1 {
m.cursor++
}
case "enter", " ":
if len(hand) == 0 {
return m, nil
}
card := hand[m.cursor]
if err := m.game.PlayCard(card); err != nil {
m.setError(err)
return m, nil
}
m.setInfo("Вы сыграли %s.", card)
if m.game.Phase == OneOhOnePhasePlay {
m.resetSelection()
}
return m, m.maybeScheduleBot()
case "d":
if err := m.game.Draw(); err != nil {
m.setError(err)
return m, nil
}
m.setInfo("Вы взяли карту.")
if m.game.Phase == OneOhOnePhasePlay {
m.resetSelection()
}
return m, m.maybeScheduleBot()
}
return m, nil
}
// --- Отрисовка -----------------------------------------------------
func (m OneOhOneModel) renderHand() string {
hand := m.currentHand()
if len(hand) == 0 {
return dimStyle.Render("(нет карт)")
}
parts := make([]string, len(hand))
for i, c := range hand {
text := renderCard(c)
if i == m.cursor {
text = cursorStyle.Render(c.String())
}
parts[i] = text
}
return strings.Join(parts, " ")
}
func (m OneOhOneModel) renderPlayers() string {
var b strings.Builder
for i, p := range m.game.Players {
marker := " "
if m.game.Phase == OneOhOnePhasePlay && i == m.game.CurrentPlayerIdx {
marker = "> "
}
name := p.Name
if i == m.humanIdx {
name += " (вы)"
}
status := ""
if p.Out {
status = " [выбыл]"
}
line := fmt.Sprintf("%s%-16s карт: %-2d счёт: %-4d%s", marker, name, len(p.Hand), p.Score, status)
if m.game.Phase == OneOhOnePhasePlay && i == m.game.CurrentPlayerIdx {
line = turnStyle.Render(line)
}
fmt.Fprintln(&b, line)
}
return strings.TrimRight(b.String(), "\n")
}
func (m OneOhOneModel) View() string {
if m.quitting {
return "До встречи!\n"
}
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render("=== 101 ==="))
fmt.Fprintln(&b)
fmt.Fprintln(&b, m.renderPlayers())
fmt.Fprintln(&b)
fmt.Fprintf(&b, "Сброс сверху: %s В колоде: %d карт\n\n", renderCard(m.game.topDiscard()), len(m.game.Stock))
if m.game.Phase == OneOhOnePhaseRoundOver {
fmt.Fprintln(&b, m.renderResult())
fmt.Fprintln(&b)
if m.game.Result.MatchOver {
fmt.Fprintln(&b, dimStyle.Render("[q] в меню"))
} else {
fmt.Fprintln(&b, dimStyle.Render("[n] новый раунд [q] в меню"))
}
return b.String()
}
if m.game.CurrentPlayerIdx == m.humanIdx {
fmt.Fprintln(&b, "Ваша рука:")
fmt.Fprintln(&b, m.renderHand())
fmt.Fprintln(&b)
fmt.Fprintln(&b, "[←→/hl] выбор карты [enter] сыграть карту [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 OneOhOneModel) renderResult() string {
res := m.game.Result
var b strings.Builder
if res.Stalemate {
fmt.Fprintln(&b, errorStyle.Render("Раунд зашёл в тупик и принудительно завершён без изменения счёта."))
return strings.TrimRight(b.String(), "\n")
}
winnerName := m.game.Players[res.Winner].Name
winLine := fmt.Sprintf("%s избавился(лась) от всех карт и выиграл(а) раунд!", winnerName)
if res.QueenBonus {
winLine += " (бонус за даму!)"
}
fmt.Fprintln(&b, winStyle.Render(winLine))
fmt.Fprintln(&b)
for i, p := range m.game.Players {
delta := res.ScoreDeltas[i]
note := ""
for _, e := range res.Eliminated {
if e == i {
note = " — выбыл(а) из партии!"
}
}
for _, r := range res.Reset {
if r == i {
note = " — ровно 101, счёт обнулён!"
}
}
fmt.Fprintln(&b, fmt.Sprintf(" %-16s %+d очков за раунд (итого: %d)%s", p.Name, delta, p.Score, note))
}
if res.MatchOver {
fmt.Fprintln(&b)
if res.NoWinner {
fmt.Fprintln(&b, errorStyle.Render("Партия завершилась без победителя (все выбыли одновременно)."))
} else {
fmt.Fprintln(&b, winStyle.Render(fmt.Sprintf("Партия окончена! Победитель: %s", m.game.Players[res.MatchWinner].Name)))
}
}
return strings.TrimRight(b.String(), "\n")
}