go_games_collection/durak_tui.go

408 lines
9.6 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"
)
// DurakModel — TUI поверх DurakGameState. Действующее лицо всегда
// одно: атакующий (обязательный первый ход раунда), защищающийся
// (отбить/забрать) либо очередной подкидывающий (подкинуть/пасовать
// в фазе DurakPhaseThrowIn) — определяется через currentActor().
type DurakModel struct {
game *DurakGameState
humanIdx int
bot DurakBot
cursor int
message string
isError bool
quitting bool
backToMenu bool
}
// NewDurakModel создаёт новую партию Дурака на numPlayers игроков
// (2-6, включая человека) со случайными именами ботов.
func NewDurakModel(numPlayers int) DurakModel {
if numPlayers < 2 {
numPlayers = 2
}
if numPlayers > 6 {
numPlayers = 6
}
botNames := newDurakBotNames(numPlayers - 1)
names := append([]string{T("common.you")}, botNames...)
return DurakModel{
game: NewDurakGame(names, 0),
humanIdx: 0,
bot: DurakBot{},
}
}
func (m DurakModel) Init() tea.Cmd {
return m.maybeScheduleBot()
}
// currentActor возвращает индекс игрока, который должен действовать
// прямо сейчас: атакующий в фазе первого хода, защищающийся в фазе
// защиты, очередной подкидывающий в фазе подкидывания.
func (m DurakModel) currentActor() int {
switch m.game.Phase {
case DurakPhaseAttack:
return m.game.AttackerIdx
case DurakPhaseDefend:
return m.game.DefenderIdx
case DurakPhaseThrowIn:
return m.game.CurrentThrower()
}
return -1
}
func (m DurakModel) maybeScheduleBot() tea.Cmd {
if m.game.Phase == DurakPhaseGameOver {
return nil
}
if m.currentActor() == m.humanIdx {
return nil
}
return tea.Tick(botMoveDelay, func(time.Time) tea.Msg { return botMoveMsg{} })
}
func (m *DurakModel) resetSelection() {
hand := m.currentHand()
if len(hand) == 0 {
m.cursor = 0
} else if m.cursor >= len(hand) {
m.cursor = len(hand) - 1
}
}
func (m DurakModel) currentHand() []Card {
idx := m.currentActor()
if idx < 0 {
return nil
}
return m.game.Players[idx].Hand
}
func (m *DurakModel) setInfo(format string, args ...interface{}) {
m.message = fmt.Sprintf(format, args...)
m.isError = false
}
func (m *DurakModel) setError(err error) {
m.message = T(err.Error())
m.isError = true
}
func (m DurakModel) 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 DurakModel) handleBotMove() (tea.Model, tea.Cmd) {
if m.game.Phase == DurakPhaseGameOver {
return m, nil
}
actor := m.currentActor()
if actor == m.humanIdx {
return m, nil
}
if err := m.bot.PlayFullTurn(m.game); err != nil {
m.setError(fmt.Errorf(T("бот %s: %w"), m.game.Players[actor].Name, err))
return m, nil
}
m.setInfo(T("tonk.msg.bot_moved"), m.game.Players[actor].Name)
if m.game.Phase != DurakPhaseGameOver && m.currentActor() == m.humanIdx {
m.resetSelection()
}
return m, m.maybeScheduleBot()
}
func (m DurakModel) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
key := msg.String()
if key == "ctrl+c" {
m.quitting = true
return m, tea.Quit
}
if m.game.Phase == DurakPhaseGameOver {
switch key {
case "n":
numPlayers := len(m.game.Players)
m = NewDurakModel(numPlayers)
return m, m.maybeScheduleBot()
case "q":
m.backToMenu = true
return m, nil
}
return m, nil
}
if key == "q" {
m.backToMenu = true
return m, nil
}
if m.currentActor() != m.humanIdx {
// сейчас действует бот — игнорируем остальной ввод
return m, nil
}
switch m.game.Phase {
case DurakPhaseAttack:
return m.handleAttackKey(key)
case DurakPhaseDefend:
return m.handleDefendKey(key)
case DurakPhaseThrowIn:
return m.handleThrowInKey(key)
}
return m, nil
}
func (m DurakModel) handleAttackKey(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.ThrowIn(card); err != nil {
m.setError(err)
return m, nil
}
m.setInfo(T("durak.msg.threw_in"), card)
if m.game.Phase != DurakPhaseGameOver {
m.resetSelection()
}
return m, m.maybeScheduleBot()
}
return m, nil
}
func (m DurakModel) handleThrowInKey(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.ThrowIn(card); err != nil {
m.setError(err)
return m, nil
}
m.setInfo(T("durak.msg.threw_in"), card)
if m.game.Phase != DurakPhaseGameOver {
m.resetSelection()
}
return m, m.maybeScheduleBot()
case "p":
if err := m.game.Pass(); err != nil {
m.setError(err)
return m, nil
}
m.setInfo(T("durak.msg.passed"))
if m.game.Phase != DurakPhaseGameOver {
m.resetSelection()
}
return m, m.maybeScheduleBot()
}
return m, nil
}
func (m DurakModel) handleDefendKey(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.Defend(card); err != nil {
m.setError(err)
return m, nil
}
m.setInfo(T("durak.msg.defended"), card)
if m.game.Phase != DurakPhaseGameOver {
m.resetSelection()
}
return m, m.maybeScheduleBot()
case "t":
if err := m.game.Take(); err != nil {
m.setError(err)
return m, nil
}
m.setInfo(T("durak.msg.took"))
if m.game.Phase != DurakPhaseGameOver {
m.resetSelection()
}
return m, m.maybeScheduleBot()
}
return m, nil
}
// --- Отрисовка -----------------------------------------------------
func (m DurakModel) renderHand() string {
hand := m.currentHand()
if len(hand) == 0 {
return dimStyle.Render(T("common.no_cards"))
}
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 DurakModel) renderTable() string {
if len(m.game.Table) == 0 {
return dimStyle.Render(T("tonk.table_empty"))
}
parts := make([]string, len(m.game.Table))
for i, pair := range m.game.Table {
if pair.Defend != nil {
parts[i] = fmt.Sprintf("%s/%s", renderCard(pair.Attack), renderCard(*pair.Defend))
} else {
parts[i] = fmt.Sprintf("%s/__", renderCard(pair.Attack))
}
}
return strings.Join(parts, " ")
}
func (m DurakModel) renderPlayers() string {
var b strings.Builder
for i, p := range m.game.Players {
marker := " "
switch {
case m.game.Phase == DurakPhaseThrowIn && i == m.game.CurrentThrower():
marker = "T "
case i == m.game.AttackerIdx && m.game.Phase != DurakPhaseGameOver:
marker = "A "
case i == m.game.DefenderIdx && m.game.Phase != DurakPhaseGameOver:
marker = "D "
}
name := p.Name
status := ""
if p.Out {
status = T("durak.status_out")
}
line := fmt.Sprintf(T("durak.player_line"), marker, name, len(p.Hand), status)
if i == m.game.AttackerIdx || i == m.game.DefenderIdx || (m.game.Phase == DurakPhaseThrowIn && i == m.game.CurrentThrower()) {
line = turnStyle.Render(line)
}
fmt.Fprintln(&b, line)
}
return strings.TrimRight(b.String(), "\n")
}
func (m DurakModel) View() string {
if m.quitting {
return T("common.goodbye")
}
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("durak.title")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, m.renderPlayers())
fmt.Fprintln(&b, dimStyle.Render(T("durak.legend")))
fmt.Fprintln(&b)
trumpStr := renderCard(m.game.TrumpCard)
fmt.Fprintf(&b, T("durak.status_line"), trumpStr, len(m.game.Talon))
fmt.Fprintln(&b, T("durak.table_label"))
fmt.Fprintln(&b, m.renderTable())
fmt.Fprintln(&b)
if m.game.Phase == DurakPhaseGameOver {
fmt.Fprintln(&b, m.renderResult())
fmt.Fprintln(&b)
fmt.Fprintln(&b, dimStyle.Render(T("durak.hint.gameover")))
return b.String()
}
if m.currentActor() == m.humanIdx {
fmt.Fprintln(&b, T("common.your_hand"))
fmt.Fprintln(&b, m.renderHand())
fmt.Fprintln(&b)
switch m.game.Phase {
case DurakPhaseAttack:
fmt.Fprintln(&b, T("durak.hint.attack"))
case DurakPhaseDefend:
fmt.Fprintln(&b, T("durak.hint.defend"))
case DurakPhaseThrowIn:
fmt.Fprintln(&b, T("durak.hint.throwin"))
}
} else {
fmt.Fprintf(&b, T("common.thinking"), m.game.Players[m.currentActor()].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 DurakModel) renderResult() string {
res := m.game.Result
if res.Stalemate {
return errorStyle.Render(T("durak.result.stalemate"))
}
if res.NoLoser {
return winStyle.Render(T("durak.result.noloser"))
}
loserName := m.game.Players[res.Loser].Name
if res.Loser == m.humanIdx {
return errorStyle.Render(Tf("durak.result.loser", loserName))
}
return winStyle.Render(Tf("durak.result.loser", loserName))
}