go_games_collection/go_tui.go
2026-07-07 09:12:09 +03:00

255 lines
5.3 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"
)
// goColumnLetters — буквы столбцов доски Го: традиционно пропускают
// "I", чтобы не путать с "1".
const goColumnLetters = "ABCDEFGHJKLMNOPQRST"
// GoModel — TUI поверх GoGameState. Человек всегда играет чёрными
// (ходят первыми), бот — белыми.
type GoModel struct {
game *GoGameState
bot GoBot
rnd *rand.Rand
human GoColor
cursor GoPos
size int
difficulty GoDifficulty
message string
isError bool
quitting bool
backToMenu bool
}
// NewGoModel создаёт новую партию на доске size x size с ботом
// уровня difficulty. Человек играет чёрными.
func NewGoModel(size int, difficulty GoDifficulty) GoModel {
return GoModel{
game: NewGoGame(size),
bot: GoBot{Difficulty: difficulty},
rnd: rand.New(rand.NewSource(time.Now().UnixNano())),
human: GoBlack,
cursor: GoPos{Row: size / 2, Col: size / 2},
size: size,
difficulty: difficulty,
}
}
func (m GoModel) Init() tea.Cmd {
return m.maybeScheduleBot()
}
func (m GoModel) maybeScheduleBot() tea.Cmd {
if m.game.Result != nil {
return nil
}
if m.game.Turn == m.human {
return nil
}
return tea.Tick(botMoveDelay, func(time.Time) tea.Msg { return botMoveMsg{} })
}
func (m *GoModel) setInfo(key string) {
m.message = T(key)
m.isError = false
}
func (m *GoModel) setError(err error) {
m.message = T(err.Error())
m.isError = true
}
func (m GoModel) 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 GoModel) handleBotMove() (tea.Model, tea.Cmd) {
if m.game.Result != nil || m.game.Turn == m.human {
return m, nil
}
passed, err := m.bot.PlayFullTurn(m.game, m.rnd)
if err != nil {
m.setError(err)
return m, nil
}
if passed {
m.setInfo("go.msg.bot_passed")
} else {
m.setInfo("go.msg.bot_moved")
}
return m, m.maybeScheduleBot()
}
func (m GoModel) 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 {
if key == "n" {
m.game = NewGoGame(m.size)
m.cursor = GoPos{Row: m.size / 2, Col: m.size / 2}
m.message = ""
m.isError = false
return m, m.maybeScheduleBot()
}
return m, nil
}
if m.game.Turn != m.human {
return m, nil
}
switch key {
case "up", "k":
if m.cursor.Row > 0 {
m.cursor.Row--
}
case "down", "j":
if m.cursor.Row < m.game.Size-1 {
m.cursor.Row++
}
case "left", "h":
if m.cursor.Col > 0 {
m.cursor.Col--
}
case "right", "l":
if m.cursor.Col < m.game.Size-1 {
m.cursor.Col++
}
case "enter", " ":
if err := m.game.Play(m.cursor); err != nil {
m.setError(err)
return m, nil
}
m.setInfo("go.msg.played")
return m, m.maybeScheduleBot()
case "p":
if err := m.game.Pass(); err != nil {
m.setError(err)
return m, nil
}
m.setInfo("go.msg.passed")
return m, m.maybeScheduleBot()
}
return m, nil
}
// --- Отрисовка -----------------------------------------------------
func goStoneGlyph(c GoColor) string {
switch c {
case GoBlack:
return checkersBlackPiece.Render("●")
case GoWhite:
return checkersWhitePiece.Render("○")
}
return dimStyle.Render("·")
}
func goStoneSymbolPlain(c GoColor) string {
switch c {
case GoBlack:
return "●"
case GoWhite:
return "○"
}
return "·"
}
func (m GoModel) renderBoard() string {
size := m.game.Size
var b strings.Builder
fmt.Fprint(&b, " ")
for c := 0; c < size; c++ {
fmt.Fprintf(&b, "%c ", goColumnLetters[c])
}
fmt.Fprintln(&b)
for r := 0; r < size; r++ {
label := size - r
fmt.Fprintf(&b, "%2d ", label)
for c := 0; c < size; c++ {
pos := GoPos{Row: r, Col: c}
glyph := goStoneGlyph(m.game.Board[r][c])
if pos == m.cursor && m.game.Result == nil {
glyph = cursorStyle.Render(goStoneSymbolPlain(m.game.Board[r][c]))
}
fmt.Fprint(&b, glyph+" ")
}
fmt.Fprintln(&b)
}
return b.String()
}
func (m GoModel) View() string {
if m.quitting {
return T("common.goodbye")
}
g := m.game
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render(T("go.title")))
fmt.Fprintln(&b)
fmt.Fprintln(&b, Tf("go.header.captures", g.CapturedByBlack, g.CapturedByWhite))
fmt.Fprintln(&b)
fmt.Fprintln(&b, m.renderBoard())
fmt.Fprintln(&b)
if g.Result != nil {
res := g.Result
winnerKey := "go.color.black"
if res.Winner == GoWhite {
winnerKey = "go.color.white"
}
fmt.Fprintln(&b, winStyle.Render(Tf("go.result.line", T(winnerKey), res.BlackScore, res.WhiteScore)))
fmt.Fprintln(&b)
fmt.Fprintln(&b, dimStyle.Render(T("go.hint.gameover")))
return b.String()
}
turnLabel := T("go.turn.you")
if g.Turn != m.human {
turnLabel = T("go.turn.bot")
}
fmt.Fprintln(&b, turnLabel)
fmt.Fprintln(&b)
if g.Turn == m.human {
fmt.Fprintln(&b, T("go.hint.play"))
}
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()
}