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