316 lines
7.7 KiB
Go
316 lines
7.7 KiB
Go
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
tea "github.com/charmbracelet/bubbletea"
|
||
"github.com/charmbracelet/lipgloss"
|
||
)
|
||
|
||
var (
|
||
checkersDarkSquare = lipgloss.NewStyle().Background(lipgloss.Color("236"))
|
||
checkersLightSquare = lipgloss.NewStyle().Background(lipgloss.Color("180"))
|
||
checkersWhitePieceColor = lipgloss.Color("255")
|
||
checkersBlackPieceColor = lipgloss.Color("124")
|
||
checkersWhitePiece = lipgloss.NewStyle().Foreground(checkersWhitePieceColor).Bold(true)
|
||
checkersBlackPiece = lipgloss.NewStyle().Foreground(checkersBlackPieceColor).Bold(true)
|
||
checkersCursorStyle = lipgloss.NewStyle().Background(lipgloss.Color("82")).Foreground(lipgloss.Color("16")).Bold(true)
|
||
checkersSelStyle = lipgloss.NewStyle().Background(lipgloss.Color("39")).Foreground(lipgloss.Color("16")).Bold(true)
|
||
)
|
||
|
||
// CheckersModel — TUI поверх CheckersGameState. Человек всегда
|
||
// играет белыми (ходят первыми), бот — чёрными.
|
||
type CheckersModel struct {
|
||
game *CheckersGameState
|
||
bot CheckersBot
|
||
human CheckersColor
|
||
cursor CheckersPos
|
||
selected *CheckersPos
|
||
|
||
message string
|
||
isError bool
|
||
|
||
quitting bool
|
||
backToMenu bool
|
||
}
|
||
|
||
// NewCheckersModel создаёт новую партию: человек играет белыми.
|
||
// depth — глубина поиска бота в полуходах (0 -> используется
|
||
// значение по умолчанию из CheckersBot).
|
||
func NewCheckersModel(depth int) CheckersModel {
|
||
return CheckersModel{
|
||
game: NewCheckersGame(),
|
||
bot: CheckersBot{Depth: depth},
|
||
human: CheckersWhite,
|
||
cursor: CheckersPos{Row: 5, Col: 0},
|
||
}
|
||
}
|
||
|
||
func (m CheckersModel) Init() tea.Cmd {
|
||
return m.maybeScheduleBot()
|
||
}
|
||
|
||
func (m CheckersModel) 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 *CheckersModel) setInfo(format string, args ...interface{}) {
|
||
m.message = fmt.Sprintf(format, args...)
|
||
m.isError = false
|
||
}
|
||
|
||
func (m *CheckersModel) setError(err error) {
|
||
m.message = T(err.Error())
|
||
m.isError = true
|
||
}
|
||
|
||
func (m CheckersModel) 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 CheckersModel) handleBotMove() (tea.Model, tea.Cmd) {
|
||
if m.game.Result != nil || m.game.Turn == m.human {
|
||
return m, nil
|
||
}
|
||
if err := m.bot.PlayFullTurn(m.game); err != nil {
|
||
m.setError(err)
|
||
return m, nil
|
||
}
|
||
m.setInfo(T("checkers.msg.bot_moved"))
|
||
return m, m.maybeScheduleBot()
|
||
}
|
||
|
||
func (m CheckersModel) 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 = NewCheckersGame()
|
||
m.selected = nil
|
||
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 < 7 {
|
||
m.cursor.Row++
|
||
}
|
||
case "left", "h":
|
||
if m.cursor.Col > 0 {
|
||
m.cursor.Col--
|
||
}
|
||
case "right", "l":
|
||
if m.cursor.Col < 7 {
|
||
m.cursor.Col++
|
||
}
|
||
case "esc":
|
||
m.selected = nil
|
||
m.message = ""
|
||
case "enter", " ":
|
||
return m.handleSelectOrMove()
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m CheckersModel) handleSelectOrMove() (tea.Model, tea.Cmd) {
|
||
if m.selected == nil {
|
||
piece := m.game.Board[m.cursor.Row][m.cursor.Col]
|
||
if piece == nil || piece.Color != m.human {
|
||
m.setError(ErrCheckersNoPiece)
|
||
return m, nil
|
||
}
|
||
if len(m.game.LegalMovesFrom(m.cursor)) == 0 {
|
||
m.setError(ErrCheckersInvalidMove)
|
||
return m, nil
|
||
}
|
||
pos := m.cursor
|
||
m.selected = &pos
|
||
m.message = ""
|
||
return m, nil
|
||
}
|
||
|
||
if *m.selected == m.cursor {
|
||
m.selected = nil
|
||
return m, nil
|
||
}
|
||
|
||
from := *m.selected
|
||
err := m.game.Move(from, m.cursor)
|
||
if err != nil {
|
||
m.setError(err)
|
||
return m, nil
|
||
}
|
||
m.setInfo(T("checkers.msg.moved"))
|
||
if m.game.ChainPos != nil {
|
||
pos := *m.game.ChainPos
|
||
m.selected = &pos
|
||
m.cursor = pos
|
||
} else {
|
||
m.selected = nil
|
||
}
|
||
return m, m.maybeScheduleBot()
|
||
}
|
||
|
||
// --- Отрисовка -----------------------------------------------------
|
||
|
||
// pieceGlyphChar возвращает "сырой" (без цвета) символ фигуры —
|
||
// используется вместе с pieceForegroundColor, чтобы построить один
|
||
// комбинированный стиль (фон клетки + цвет фигуры) вместо вложенных
|
||
// вызовов Render(), которые ломаются в некоторых терминалах (см.
|
||
// аналогичный баг, исправленный ранее в отрисовке карт Тонка).
|
||
func pieceGlyphChar(p *CheckersPiece) string {
|
||
if p == nil {
|
||
return " "
|
||
}
|
||
if p.King {
|
||
return "O"
|
||
}
|
||
return "o"
|
||
}
|
||
|
||
// pieceForegroundColor возвращает цвет текста фигуры (второе
|
||
// значение — false, если клетка пуста и раскрашивать нечего).
|
||
func pieceForegroundColor(p *CheckersPiece) (lipgloss.Color, bool) {
|
||
if p == nil {
|
||
return "", false
|
||
}
|
||
if p.Color == CheckersWhite {
|
||
return checkersWhitePieceColor, true
|
||
}
|
||
return checkersBlackPieceColor, true
|
||
}
|
||
|
||
func (m CheckersModel) legalDestinationSet() map[CheckersPos]bool {
|
||
set := map[CheckersPos]bool{}
|
||
if m.selected == nil {
|
||
return set
|
||
}
|
||
for _, opt := range m.game.LegalMovesFrom(*m.selected) {
|
||
set[opt.To] = true
|
||
}
|
||
return set
|
||
}
|
||
|
||
func (m CheckersModel) renderBoard() string {
|
||
dests := m.legalDestinationSet()
|
||
var b strings.Builder
|
||
fmt.Fprintln(&b, " a b c d e f g h")
|
||
for r := 0; r < 8; r++ {
|
||
fmt.Fprintf(&b, "%d ", 8-r)
|
||
for c := 0; c < 8; c++ {
|
||
pos := CheckersPos{Row: r, Col: c}
|
||
cellStyle := checkersLightSquare
|
||
if (r+c)%2 == 1 {
|
||
cellStyle = checkersDarkSquare
|
||
}
|
||
|
||
switch {
|
||
case m.cursor == pos:
|
||
cellStyle = checkersCursorStyle
|
||
case m.selected != nil && *m.selected == pos:
|
||
cellStyle = checkersSelStyle
|
||
case dests[pos]:
|
||
cellStyle = checkersSelStyle
|
||
}
|
||
|
||
piece := m.game.Board[r][c]
|
||
text := pieceGlyphChar(piece)
|
||
combined := cellStyle
|
||
if fg, ok := pieceForegroundColor(piece); ok {
|
||
combined = cellStyle.Foreground(fg).Bold(true)
|
||
}
|
||
fmt.Fprint(&b, combined.Render(" "+text+" "))
|
||
}
|
||
fmt.Fprintln(&b)
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
func (m CheckersModel) View() string {
|
||
if m.quitting {
|
||
return T("common.goodbye")
|
||
}
|
||
|
||
var b strings.Builder
|
||
fmt.Fprintln(&b, titleStyle.Render(T("checkers.title")))
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, m.renderBoard())
|
||
fmt.Fprintln(&b, dimStyle.Render(T("checkers.legend")))
|
||
fmt.Fprintln(&b)
|
||
|
||
if m.game.Result != nil {
|
||
res := m.game.Result
|
||
switch {
|
||
case res.Draw:
|
||
fmt.Fprintln(&b, errorStyle.Render(T("checkers.result.draw")))
|
||
case res.Winner == m.human:
|
||
fmt.Fprintln(&b, winStyle.Render(T("checkers.result.win")))
|
||
default:
|
||
fmt.Fprintln(&b, errorStyle.Render(T("checkers.result.lose")))
|
||
}
|
||
fmt.Fprintln(&b)
|
||
fmt.Fprintln(&b, dimStyle.Render(T("checkers.hint.gameover")))
|
||
return b.String()
|
||
}
|
||
|
||
turnLabel := T("checkers.turn.you")
|
||
if m.game.Turn != m.human {
|
||
turnLabel = T("checkers.turn.bot")
|
||
}
|
||
fmt.Fprintln(&b, turnLabel)
|
||
fmt.Fprintln(&b)
|
||
|
||
if m.game.Turn == m.human {
|
||
if m.selected == nil {
|
||
fmt.Fprintln(&b, T("checkers.hint.noselect"))
|
||
} else {
|
||
fmt.Fprintln(&b, T("checkers.hint.selected"))
|
||
}
|
||
}
|
||
|
||
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()
|
||
}
|