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") }