package main import ( "fmt" "strings" "time" tea "github.com/charmbracelet/bubbletea" ) // ThousandModel — TUI поверх ThousandGameState. type ThousandModel struct { game *ThousandGameState humanIdx int bot ThousandBot cursor int bidAmount int message string isError bool quitting bool backToMenu bool } // NewThousandModel создаёт новую партию 1000 на троих (человек + // 2 бота со случайными именами). func NewThousandModel() ThousandModel { botNames := newThousandBotNames(2) names := append([]string{"Вы"}, botNames...) return ThousandModel{ game: NewThousandGame(names, 0), humanIdx: 0, bot: ThousandBot{}, } } func (m ThousandModel) Init() tea.Cmd { return m.maybeScheduleBot() } func (m ThousandModel) actorForPhase() int { switch m.game.Phase { case ThousandPhaseBidding: return m.game.CurrentBidderIdx case ThousandPhaseDiscard: return m.game.DeclarerIdx case ThousandPhaseTrick: return m.game.currentTrickActor() } return -1 } func (m ThousandModel) maybeScheduleBot() tea.Cmd { if m.game.Phase == ThousandPhaseHandOver { return nil } if m.actorForPhase() == m.humanIdx { return nil } return tea.Tick(botMoveDelay, func(time.Time) tea.Msg { return botMoveMsg{} }) } func (m *ThousandModel) resetSelection() { hand := m.currentHand() if len(hand) == 0 { m.cursor = 0 } else if m.cursor >= len(hand) { m.cursor = len(hand) - 1 } } func (m ThousandModel) currentHand() []Card { idx := m.actorForPhase() if idx < 0 { return nil } return m.game.Players[idx].Hand } func (m *ThousandModel) setInfo(format string, args ...interface{}) { m.message = fmt.Sprintf(format, args...) m.isError = false } func (m *ThousandModel) setError(err error) { m.message = err.Error() m.isError = true } func (m ThousandModel) 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 ThousandModel) handleBotMove() (tea.Model, tea.Cmd) { if m.game.Phase == ThousandPhaseHandOver { return m, nil } actor := m.actorForPhase() if actor == m.humanIdx { return m, nil } if err := m.bot.PlayFullTurn(m.game); err != nil { m.setError(fmt.Errorf("бот %s: %w", m.game.Players[actor].Name, err)) return m, nil } m.setInfo("%s сходил(а).", m.game.Players[actor].Name) if m.game.Phase != ThousandPhaseHandOver && m.actorForPhase() == m.humanIdx { m.resetSelection() m.bidAmount = m.game.HighBid + 5 if m.bidAmount < 100 { m.bidAmount = 100 } } return m, m.maybeScheduleBot() } func (m ThousandModel) 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 == ThousandPhaseHandOver { if key == "n" && !m.game.Result.MatchOver { m.game.NextHand() m.message = "" m.isError = false m.resetSelection() return m, m.maybeScheduleBot() } return m, nil } if m.actorForPhase() != m.humanIdx { return m, nil } switch m.game.Phase { case ThousandPhaseBidding: return m.handleBiddingKey(key) case ThousandPhaseDiscard: return m.handleDiscardKey(key) case ThousandPhaseTrick: return m.handleTrickKey(key) } return m, nil } func (m ThousandModel) handleBiddingKey(key string) (tea.Model, tea.Cmd) { if m.bidAmount < 100 { m.bidAmount = 100 } switch key { case "up", "right", "l", "k": m.bidAmount += 5 case "down", "left", "h", "j": if m.bidAmount > 100 { m.bidAmount -= 5 } case "enter": if err := m.game.Bid(m.bidAmount); err != nil { m.setError(err) return m, nil } m.setInfo("Вы поставили %d.", m.bidAmount) if m.game.Phase != ThousandPhaseHandOver { m.bidAmount = m.game.HighBid + 5 } return m, m.maybeScheduleBot() case "p": if err := m.game.Pass(); err != nil { m.setError(err) return m, nil } m.setInfo("Вы пасовали.") return m, m.maybeScheduleBot() } return m, nil } func (m ThousandModel) handleDiscardKey(key string) (tea.Model, tea.Cmd) { 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.DiscardCard(card); err != nil { m.setError(err) return m, nil } m.setInfo("Вы отдали %s.", card) m.resetSelection() if m.game.Phase != ThousandPhaseDiscard { return m, m.maybeScheduleBot() } } return m, nil } func (m ThousandModel) handleTrickKey(key string) (tea.Model, tea.Cmd) { 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 != ThousandPhaseHandOver { m.resetSelection() } return m, m.maybeScheduleBot() } return m, nil } // --- Отрисовка ----------------------------------------------------- func (m ThousandModel) 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 ThousandModel) renderPlayers() string { var b strings.Builder for i, p := range m.game.Players { marker := " " if m.game.Phase != ThousandPhaseHandOver && i == m.actorForPhase() { marker = "> " } name := p.Name if i == m.humanIdx { name += " (вы)" } role := "" if m.game.Phase != ThousandPhaseBidding && i == m.game.DeclarerIdx { role = " [торг]" } line := fmt.Sprintf("%s%-14s карт: %-2d очки за раздачу: %-4d счёт: %-5d%s", marker, name, len(p.Hand), p.HandPoints, p.Score, role) if m.game.Phase != ThousandPhaseHandOver && i == m.actorForPhase() { line = turnStyle.Render(line) } fmt.Fprintln(&b, line) } return strings.TrimRight(b.String(), "\n") } func (m ThousandModel) renderTrick() string { if len(m.game.CurrentTrick) == 0 { return dimStyle.Render("(взятка ещё не начата)") } parts := make([]string, len(m.game.CurrentTrick)) for i, play := range m.game.CurrentTrick { parts[i] = fmt.Sprintf("%s:%s", m.game.Players[play.PlayerIdx].Name, renderCard(play.Card)) } return strings.Join(parts, " ") } func (m ThousandModel) View() string { if m.quitting { return "До встречи!\n" } var b strings.Builder fmt.Fprintln(&b, titleStyle.Render("=== 1000 ===")) fmt.Fprintln(&b) fmt.Fprintln(&b, m.renderPlayers()) fmt.Fprintln(&b) if m.game.Phase == ThousandPhaseHandOver { 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() } switch m.game.Phase { case ThousandPhaseBidding: fmt.Fprintf(&b, "Текущая максимальная ставка: %d (у %s)\n\n", m.game.HighBid, m.bidderName()) case ThousandPhaseDiscard, ThousandPhaseTrick: trumpStr := "ещё не назначен" if m.game.TrumpSet { trumpStr = suitName(m.game.TrumpSuit) } fmt.Fprintf(&b, "Ставка: %d (у %s) Козырь: %s\n", m.game.FinalBid, m.game.Players[m.game.DeclarerIdx].Name, trumpStr) if m.game.Phase == ThousandPhaseTrick { fmt.Fprintln(&b, "Взятка:", m.renderTrick()) } fmt.Fprintln(&b) } if m.actorForPhase() == m.humanIdx { fmt.Fprintln(&b, "Ваша рука:") fmt.Fprintln(&b, m.renderHand()) fmt.Fprintln(&b) switch m.game.Phase { case ThousandPhaseBidding: fmt.Fprintf(&b, "[↑→/kl] +5 [↓←/hj] -5 [enter] поставить %d [p] пас\n", m.bidAmount) case ThousandPhaseDiscard: fmt.Fprintln(&b, "[←→/hl] выбор карты [enter] отдать карту следующему сопернику") case ThousandPhaseTrick: fmt.Fprintln(&b, "[←→/hl] выбор карты [enter] сыграть карту (король/дама масти = объявить марьяж)") } fmt.Fprintln(&b, dimStyle.Render("[q] в меню")) } else { fmt.Fprintf(&b, "%s думает...\n", m.game.Players[m.actorForPhase()].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 ThousandModel) bidderName() string { if m.game.HighBidderIdx < 0 { return "никого — торги ещё не начались" } return m.game.Players[m.game.HighBidderIdx].Name } func suitName(s Suit) string { switch s { case Clubs: return "трефы" case Diamonds: return "бубны" case Hearts: return "червы" case Spades: return "пики" } return "?" } func (m ThousandModel) renderResult() string { res := m.game.Result var b strings.Builder declarerName := m.game.Players[res.DeclarerIdx].Name if res.Made { fmt.Fprintln(&b, winStyle.Render(fmt.Sprintf("%s выполнил(а) ставку %d!", declarerName, res.FinalBid))) } else { fmt.Fprintln(&b, errorStyle.Render(fmt.Sprintf("%s не выполнил(а) ставку %d.", declarerName, res.FinalBid))) } fmt.Fprintln(&b) for i, p := range m.game.Players { fmt.Fprintln(&b, fmt.Sprintf(" %-14s %+d очков за раздачу (итого: %d)", p.Name, res.ScoreDeltas[i], p.Score)) } if res.MatchOver { fmt.Fprintln(&b) fmt.Fprintln(&b, winStyle.Render(fmt.Sprintf("Партия окончена! Победитель: %s", m.game.Players[res.MatchWinner].Name))) } return strings.TrimRight(b.String(), "\n") }