go_games_collection/ur_tui.go

287 lines
6.9 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"
"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()
}