275 lines
6 KiB
Go
275 lines
6 KiB
Go
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()
|
||
}
|