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() }