go_games_collection/oneohone_tui.go
2026-07-10 09:12:06 +03:00

344 lines
8 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{T("common.you")}, 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 && m.game.Phase != OneOhOnePhaseChooseSuit {
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 = T(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 && m.game.Phase != OneOhOnePhaseChooseSuit {
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)
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.Phase == OneOhOnePhaseChooseSuit {
if m.game.CurrentPlayerIdx != m.humanIdx {
return m, nil
}
var suit Suit
switch key {
case "1":
suit = Clubs
case "2":
suit = Diamonds
case "3":
suit = Hearts
case "4":
suit = Spades
default:
return m, nil
}
if err := m.game.ChooseSuit(suit); err != nil {
m.setError(err)
return m, nil
}
m.setInfo(T("oneohone.msg.suit_chosen"), suitName(suit))
m.resetSelection()
return m, m.maybeScheduleBot()
}
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(T("oneohone.msg.played"), 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(T("oneohone.msg.drew"))
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(T("common.no_cards"))
}
return renderCardBoxHand(hand, cursorHighlights(len(hand), m.cursor))
}
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
status := ""
if p.Out {
status = T("oneohone.status.out")
}
line := fmt.Sprintf(T("oneohone.player_line"), 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 T("common.goodbye")
}
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("oneohone.title")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, m.renderPlayers())
fmt.Fprintln(&b)
fmt.Fprintf(&b, T("oneohone.status_line"), renderCard(m.game.topDiscard()), len(m.game.Stock))
if m.game.PendingEightSuit != nil {
fmt.Fprintln(&b, errorStyle.Render(Tf("oneohone.pending_eight_line", m.game.PendingEightSuit.String())))
}
if m.game.DeclaredSuit != nil {
fmt.Fprintln(&b, errorStyle.Render(Tf("oneohone.declared_suit_line", m.game.DeclaredSuit.String())))
}
if m.game.Phase == OneOhOnePhaseRoundOver {
fmt.Fprintln(&b, m.renderResult())
fmt.Fprintln(&b)
if m.game.Result.MatchOver {
fmt.Fprintln(&b, dimStyle.Render(T("oneohone.hint.gameover_match")))
} else {
fmt.Fprintln(&b, dimStyle.Render(T("oneohone.hint.gameover")))
}
return b.String()
}
if m.game.Phase == OneOhOnePhaseChooseSuit {
if m.game.CurrentPlayerIdx == m.humanIdx {
fmt.Fprintln(&b, T("oneohone.choose_suit_prompt"))
} 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()
}
if m.game.CurrentPlayerIdx == m.humanIdx {
fmt.Fprintln(&b, T("common.your_hand"))
fmt.Fprintln(&b, m.renderHand())
fmt.Fprintln(&b)
fmt.Fprintln(&b, T("oneohone.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 OneOhOneModel) renderResult() string {
res := m.game.Result
var b strings.Builder
if res.Stalemate {
fmt.Fprintln(&b, errorStyle.Render(T("oneohone.result.stalemate")))
return strings.TrimRight(b.String(), "\n")
}
winnerName := m.game.Players[res.Winner].Name
winLine := Tf("oneohone.result.win", winnerName)
if res.QueenBonus {
winLine += T("oneohone.result.queen_bonus")
}
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 = T("oneohone.result.eliminated")
}
}
for _, r := range res.Reset {
if r == i {
note = T("oneohone.result.reset")
}
}
fmt.Fprintln(&b, fmt.Sprintf(T("oneohone.result.delta_line"), p.Name, delta, p.Score, note))
}
if res.MatchOver {
fmt.Fprintln(&b)
if res.NoWinner {
fmt.Fprintln(&b, errorStyle.Render(T("oneohone.result.no_winner")))
} else {
fmt.Fprintln(&b, winStyle.Render(Tf("oneohone.result.match_over", m.game.Players[res.MatchWinner].Name)))
}
}
return strings.TrimRight(b.String(), "\n")
}