go_games_collection/checkers_tui.go
2026-07-04 13:51:04 +03:00

295 lines
6.8 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("94"))
checkersWhitePiece = lipgloss.NewStyle().Foreground(lipgloss.Color("255")).Bold(true)
checkersBlackPiece = lipgloss.NewStyle().Foreground(lipgloss.Color("16")).Bold(true)
checkersCursorStyle = lipgloss.NewStyle().Background(lipgloss.Color("220")).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 = 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("Бот сходил.")
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("Ход выполнен.")
if m.game.ChainPos != nil {
pos := *m.game.ChainPos
m.selected = &pos
m.cursor = pos
} else {
m.selected = nil
}
return m, m.maybeScheduleBot()
}
// --- Отрисовка -----------------------------------------------------
func pieceGlyph(p *CheckersPiece) string {
if p == nil {
return " "
}
glyph := "o"
if p.King {
glyph = "O"
}
style := checkersBlackPiece
if p.Color == CheckersWhite {
style = checkersWhitePiece
}
return style.Render(glyph)
}
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
}
glyph := pieceGlyph(m.game.Board[r][c])
switch {
case m.selected != nil && *m.selected == pos:
cellStyle = checkersSelStyle
case dests[pos]:
cellStyle = checkersSelStyle
case m.cursor == pos:
cellStyle = checkersCursorStyle
}
fmt.Fprint(&b, cellStyle.Render(" "+glyph+" "))
}
fmt.Fprintln(&b)
}
return b.String()
}
func (m CheckersModel) View() string {
if m.quitting {
return "До встречи!\n"
}
var b strings.Builder
fmt.Fprintln(&b, titleStyle.Render("=== ШАШКИ ==="))
fmt.Fprintln(&b)
fmt.Fprintln(&b, m.renderBoard())
fmt.Fprintln(&b)
if m.game.Result != nil {
res := m.game.Result
switch {
case res.Draw:
fmt.Fprintln(&b, errorStyle.Render("Ничья (слишком долго не было взятий)."))
case res.Winner == m.human:
fmt.Fprintln(&b, winStyle.Render("Вы выиграли!"))
default:
fmt.Fprintln(&b, errorStyle.Render("Победил бот."))
}
fmt.Fprintln(&b)
fmt.Fprintln(&b, dimStyle.Render("[n] новая партия [q] в меню"))
return b.String()
}
turnLabel := "Ваш ход (белые)"
if m.game.Turn != m.human {
turnLabel = "Бот думает (чёрные)..."
}
fmt.Fprintln(&b, turnLabel)
fmt.Fprintln(&b)
if m.game.Turn == m.human {
if m.selected == nil {
fmt.Fprintln(&b, "[↑↓←→/hjkl] курсор [enter] выбрать шашку [q] в меню")
} else {
fmt.Fprintln(&b, "[↑↓←→/hjkl] курсор [enter] переместить (или отменить выбор) [esc] отменить выбор")
}
}
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()
}