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

275 lines
6 KiB
Go
Raw Permalink 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, colorChoice ColorChoice) GoModel {
human := resolveGoColor(colorChoice)
return GoModel{
game: NewGoGame(size),
bot: GoBot{Difficulty: difficulty},
rnd: rand.New(rand.NewSource(time.Now().UnixNano())),
human: human,
cursor: GoPos{Row: size / 2, Col: size / 2},
size: size,
difficulty: difficulty,
}
}
// resolveGoColor переводит выбор цвета из настроек в конкретный
// GoColor. ColorChoiceA — чёрные (традиционно первый ход в Го,
// соответствует прежнему поведению по умолчанию), ColorChoiceB —
// белые.
func resolveGoColor(choice ColorChoice) GoColor {
switch choice {
case ColorChoiceA:
return GoBlack
case ColorChoiceB:
return GoWhite
default:
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
if rnd.Intn(2) == 0 {
return GoBlack
}
return GoWhite
}
}
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()
}