package main import ( "fmt" "math/rand" "strings" "time" tea "github.com/charmbracelet/bubbletea" ) // UrModel — TUI поверх UrGameState. type UrModel struct { game *UrGameState bot UrBot rng *rand.Rand humanColor UrPlayer message string isError bool quitting bool backToMenu bool } // NewUrModel начинает новую партию; кто ходит первым — решается // случайным броском (как и предписывают правила). func NewUrModel(difficulty UrDifficulty) UrModel { rng := rand.New(rand.NewSource(time.Now().UnixNano())) first := UrPlayerA if rng.Intn(2) == 1 { first = UrPlayerB } return UrModel{ game: NewUrGame(first), bot: UrBot{Difficulty: difficulty}, rng: rng, humanColor: UrPlayerA, } } func (m UrModel) Init() tea.Cmd { return m.maybeScheduleBot() } func (m UrModel) maybeScheduleBot() tea.Cmd { if m.game.Result != nil || m.game.CurrentPlayer == m.humanColor { return nil } return tea.Tick(botMoveDelay, func(time.Time) tea.Msg { return botMoveMsg{} }) } func (m *UrModel) setInfo(key string) { m.message = T(key) m.isError = false } func (m *UrModel) setError(err error) { m.message = err.Error() m.isError = true } func (m UrModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case botMoveMsg: return m.handleBotMove() case tea.KeyMsg: return m.handleKey(msg) } return m, nil } func (m UrModel) handleBotMove() (tea.Model, tea.Cmd) { if m.game.Result != nil || m.game.CurrentPlayer == m.humanColor { return m, nil } if err := m.bot.PlayFullTurn(m.game, m.rng); err != nil { m.setError(err) return m, nil } m.setInfo("ur.msg.bot_moved") return m, m.maybeScheduleBot() } func (m UrModel) 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.Result != nil || m.game.CurrentPlayer != m.humanColor { return m, nil } if !m.game.Rolled { if key == "r" || key == "enter" || key == " " { if err := m.game.Roll(urRollDice(m.rng)); err != nil { m.setError(err) return m, nil } if m.game.CurrentPlayer != m.humanColor { m.setInfo("ur.msg.no_legal_moves") return m, m.maybeScheduleBot() } m.message = "" return m, nil } return m, nil } for i := 1; i <= 9; i++ { if key != fmt.Sprintf("%d", i) { continue } moves := m.game.LegalMoves() if i > len(moves) { return m, nil } if err := m.game.Move(moves[i-1].PieceIdx); err != nil { m.setError(err) return m, nil } m.message = "" if m.game.Result != nil { m.setInfo("ur.msg.game_over") return m, nil } return m, m.maybeScheduleBot() } return m, nil } func urPlayerSymbol(p UrPlayer) string { if p == UrPlayerA { return "○" } return "●" } // urMoveLabel описывает один ход человекочитаемо: откуда и куда // (в терминах 1-индексированного пути игрока, 0=рука, 15=снятие). func urMoveLabel(g *UrGameState, mv UrMove) string { pos := g.Path[g.CurrentPlayer][mv.PieceIdx] dest := pos + g.DiceValue from := T("ur.hand") if pos > 0 { from = fmt.Sprintf("%d", pos) } to := T("ur.off") if dest <= UrPathLength { to = fmt.Sprintf("%d", dest) } return Tf("ur.move_label", from, to) } func (m UrModel) View() string { if m.quitting { return T("common.goodbye") } var b strings.Builder fmt.Fprintln(&b, titleStyle.Render(T("ur.title"))) fmt.Fprintln(&b) fmt.Fprintln(&b, m.renderBoard()) fmt.Fprintln(&b) fmt.Fprintln(&b, Tf("ur.you_are", urPlayerSymbol(m.humanColor))) fmt.Fprintln(&b, Tf("ur.hand_off_line", urPlayerSymbol(UrPlayerA), m.countInHand(UrPlayerA), urPlayerSymbol(UrPlayerB), m.countInHand(UrPlayerB), urPlayerSymbol(UrPlayerA), m.countBorneOff(UrPlayerA), urPlayerSymbol(UrPlayerB), m.countBorneOff(UrPlayerB))) switch { case m.game.Result != nil: if m.game.Result.Winner == m.humanColor { fmt.Fprintln(&b, winStyle.Render(T("ur.outcome.win"))) } else { fmt.Fprintln(&b, errorStyle.Render(T("ur.outcome.lose"))) } case !m.game.Rolled: if m.game.CurrentPlayer == m.humanColor { fmt.Fprintln(&b, T("ur.hint.roll")) } else { fmt.Fprintln(&b, T("common.thinking_generic")) } default: fmt.Fprintln(&b, Tf("ur.dice_line", m.game.DiceValue)) if m.game.CurrentPlayer == m.humanColor { moves := m.game.LegalMoves() if len(moves) == 0 { fmt.Fprintln(&b, T("ur.hint.no_moves")) } else { fmt.Fprintln(&b, T("ur.hint.pick_move")) for i, mv := range moves { if i >= 9 { break } fmt.Fprintf(&b, "[%d] %s\n", i+1, urMoveLabel(m.game, mv)) } } } else { fmt.Fprintln(&b, T("common.thinking_generic")) } } if m.message != "" { if m.isError { fmt.Fprintln(&b, errorStyle.Render("! "+m.message)) } else { fmt.Fprintln(&b, infoStyle.Render(m.message)) } } fmt.Fprintln(&b) fmt.Fprintln(&b, dimStyle.Render(T("ur.hint.quit"))) return b.String() } func (m UrModel) countInHand(p UrPlayer) int { n := 0 for _, pos := range m.game.Path[p] { if pos == 0 { n++ } } return n } func (m UrModel) countBorneOff(p UrPlayer) int { n := 0 for _, pos := range m.game.Path[p] { if pos == UrPathLength+1 { n++ } } return n } // renderBoard рисует схематичную доску Ура: два частных ряда (по 4 // клетки входа и 2 выхода, с разрывом на месте "мостика", которого // в этих рядах физически нет) и общий средний ряд из восьми клеток. func (m UrModel) renderBoard() string { var b strings.Builder privateCell := func(player UrPlayer, pathIdx int) string { if _, occupied := m.game.occupantOnPrivate(player, pathIdx); occupied { return " " + urPlayerSymbol(player) + " " } if urRosettePathIndices[pathIdx] { return dimStyle.Render(" ✿ ") } return dimStyle.Render(" · ") } sharedCell := func(sharedIdx int) string { pathIdx := sharedIdx + 5 if occ, _, occupied := m.game.occupantOnShared(sharedIdx); occupied { return " " + urPlayerSymbol(occ) + " " } if urRosettePathIndices[pathIdx] { return dimStyle.Render(" ✿ ") } return dimStyle.Render(" · ") } var topRow, midRow, botRow []string for i := 1; i <= 4; i++ { topRow = append(topRow, privateCell(UrPlayerA, i)) botRow = append(botRow, privateCell(UrPlayerB, i)) } topRow = append(topRow, " ") botRow = append(botRow, " ") for i := 13; i <= 14; i++ { topRow = append(topRow, privateCell(UrPlayerA, i)) botRow = append(botRow, privateCell(UrPlayerB, i)) } for i := 0; i <= 7; i++ { midRow = append(midRow, sharedCell(i)) } fmt.Fprintln(&b, strings.Join(topRow, "")) fmt.Fprintln(&b, strings.Join(midRow, "")) fmt.Fprint(&b, strings.Join(botRow, "")) return b.String() }