293 lines
6.4 KiB
Go
293 lines
6.4 KiB
Go
// FILE: internal/cli/cli.go
|
|
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"chess/internal/board"
|
|
"chess/internal/core"
|
|
"chess/internal/game"
|
|
)
|
|
|
|
type CommandType int
|
|
|
|
const (
|
|
CmdNone CommandType = iota
|
|
CmdNew
|
|
CmdResume
|
|
CmdMove
|
|
CmdUndo
|
|
CmdColor
|
|
CmdVerbose
|
|
CmdHistory
|
|
CmdHelp
|
|
CmdQuit
|
|
)
|
|
|
|
type Command struct {
|
|
Type CommandType
|
|
Args []string
|
|
Raw string
|
|
}
|
|
|
|
type ColorTheme string
|
|
|
|
const (
|
|
ThemeOff ColorTheme = "off"
|
|
ThemeBrown ColorTheme = "brown"
|
|
ThemeGreen ColorTheme = "green"
|
|
ThemeGray ColorTheme = "gray"
|
|
)
|
|
|
|
type themeColors struct {
|
|
lightBg string
|
|
darkBg string
|
|
white string
|
|
black string
|
|
reset string
|
|
}
|
|
|
|
var themes = map[ColorTheme]themeColors{
|
|
ThemeOff: {
|
|
lightBg: "",
|
|
darkBg: "",
|
|
white: "",
|
|
black: "",
|
|
reset: "",
|
|
},
|
|
ThemeBrown: {
|
|
lightBg: "\033[48;5;230m", // Beige
|
|
darkBg: "\033[48;5;94m", // Brown
|
|
white: "\033[97m",
|
|
black: "\033[30m",
|
|
reset: "\033[0m",
|
|
},
|
|
ThemeGreen: {
|
|
lightBg: "\033[48;5;157m", // Light green
|
|
darkBg: "\033[48;5;22m", // Dark green
|
|
white: "\033[97m",
|
|
black: "\033[30m",
|
|
reset: "\033[0m",
|
|
},
|
|
ThemeGray: {
|
|
lightBg: "\033[48;5;251m", // Light gray
|
|
darkBg: "\033[48;5;240m", // Dark gray
|
|
white: "\033[97m",
|
|
black: "\033[30m",
|
|
reset: "\033[0m",
|
|
},
|
|
}
|
|
|
|
type CLI struct {
|
|
input *bufio.Scanner
|
|
output io.Writer
|
|
theme ColorTheme
|
|
verbose bool
|
|
}
|
|
|
|
func New(input io.Reader, output io.Writer) *CLI {
|
|
return &CLI{
|
|
input: bufio.NewScanner(input),
|
|
output: output,
|
|
theme: ThemeOff,
|
|
verbose: false,
|
|
}
|
|
}
|
|
|
|
// Reads a command synchronously
|
|
func (c *CLI) GetCommand() (*Command, error) {
|
|
if !c.input.Scan() {
|
|
if err := c.input.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &Command{Type: CmdQuit}, nil
|
|
}
|
|
|
|
input := strings.TrimSpace(c.input.Text())
|
|
if input == "" {
|
|
return &Command{Type: CmdNone}, nil
|
|
}
|
|
|
|
return c.parseCommand(input), nil
|
|
}
|
|
|
|
func (c *CLI) parseCommand(input string) *Command {
|
|
parts := strings.Fields(input)
|
|
if len(parts) == 0 {
|
|
return &Command{Type: CmdNone}
|
|
}
|
|
|
|
cmd := parts[0]
|
|
args := parts[1:]
|
|
|
|
switch cmd {
|
|
case "new":
|
|
return &Command{Type: CmdNew, Args: args}
|
|
case "resume":
|
|
return &Command{Type: CmdResume, Args: args, Raw: input}
|
|
case "undo":
|
|
return &Command{Type: CmdUndo, Args: args}
|
|
case "color":
|
|
return &Command{Type: CmdColor, Args: args}
|
|
case "verbose":
|
|
return &Command{Type: CmdVerbose}
|
|
case "history":
|
|
return &Command{Type: CmdHistory}
|
|
case "help", "?":
|
|
return &Command{Type: CmdHelp}
|
|
case "quit", "exit":
|
|
return &Command{Type: CmdQuit}
|
|
default:
|
|
// Assume it's a move
|
|
return &Command{Type: CmdMove, Args: []string{cmd}}
|
|
}
|
|
}
|
|
|
|
func (c *CLI) SetTheme(theme ColorTheme) error {
|
|
if _, ok := themes[theme]; !ok {
|
|
return fmt.Errorf("invalid theme: %s (use: off, brown, green, gray)", theme)
|
|
}
|
|
c.theme = theme
|
|
return nil
|
|
}
|
|
|
|
func (c *CLI) ToggleVerbose() bool {
|
|
c.verbose = !c.verbose
|
|
return c.verbose
|
|
}
|
|
|
|
func (c *CLI) IsVerbose() bool {
|
|
return c.verbose
|
|
}
|
|
|
|
func (c *CLI) ShowMessage(msg string) {
|
|
fmt.Fprintln(c.output, msg)
|
|
}
|
|
|
|
func (c *CLI) ShowError(err error) {
|
|
c.ShowMessage(fmt.Sprintf("Error: %v\n", err))
|
|
}
|
|
|
|
func (c *CLI) ShowPrompt(prompt string) {
|
|
fmt.Fprint(c.output, prompt)
|
|
}
|
|
|
|
func (c *CLI) ReadLine() string {
|
|
if c.input.Scan() {
|
|
return strings.TrimSpace(c.input.Text())
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *CLI) DisplayBoard(b *board.Board) {
|
|
theme := themes[c.theme]
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString("\n a b c d e f g h\n")
|
|
|
|
for r := 0; r < 8; r++ {
|
|
sb.WriteString(fmt.Sprintf("%d ", 8-r))
|
|
for f := 0; f < 8; f++ {
|
|
// Get piece at position
|
|
square := fmt.Sprintf("%c%c", 'a'+f, '8'-r)
|
|
piece := b.GetPieceAt(square)
|
|
|
|
if c.theme == ThemeOff {
|
|
// No colors, just show piece or space
|
|
if piece == 0 {
|
|
sb.WriteString(" ")
|
|
} else {
|
|
sb.WriteString(fmt.Sprintf("%c ", piece))
|
|
}
|
|
} else {
|
|
// Apply theme colors
|
|
bg := theme.darkBg
|
|
if (r+f)%2 == 0 {
|
|
bg = theme.lightBg
|
|
}
|
|
|
|
if piece == 0 {
|
|
sb.WriteString(fmt.Sprintf("%s %s", bg, theme.reset))
|
|
} else {
|
|
color := theme.black
|
|
if piece >= 'A' && piece <= 'Z' {
|
|
color = theme.white
|
|
}
|
|
sb.WriteString(fmt.Sprintf("%s%s%c %s", bg, color, piece, theme.reset))
|
|
}
|
|
}
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" %d\n", 8-r))
|
|
}
|
|
sb.WriteString(" a b c d e f g h\n")
|
|
|
|
c.ShowMessage(sb.String())
|
|
}
|
|
|
|
func (c *CLI) ShowHelp() {
|
|
help := `Commands:
|
|
new - Start a new game with player type selection
|
|
resume <FEN> - Resume from a specific board position
|
|
<move> - Make a move (e.g., e2e4, g1f3)
|
|
undo [count] - Undo last move(s), default 1
|
|
color <theme> - Set board color theme (off|brown|green|gray)
|
|
verbose - Toggle detailed move information
|
|
history - Show game move history and positions
|
|
quit/exit - Exit the program
|
|
help/? - Show this help message
|
|
|
|
During any game:
|
|
Press ENTER - Execute computer move (when it's computer's turn)`
|
|
|
|
c.ShowMessage(help)
|
|
}
|
|
|
|
func (c *CLI) ShowWelcome() {
|
|
c.ShowMessage("Welcome to Chess!")
|
|
c.ShowMessage("Commands: new, resume <FEN>, <move>, undo, quit/exit, verbose, history, help/?")
|
|
c.ShowMessage("Example: 'resume 4k3/8/8/8/8/8/8/4K2R w K - 0 1' to start from a puzzle.")
|
|
c.ShowMessage("Press ENTER to execute computer moves when it's computer's turn.")
|
|
c.ShowMessage("")
|
|
}
|
|
|
|
func (c *CLI) ShowGameHistory(g *game.Game) {
|
|
c.ShowMessage(fmt.Sprintf("Starting FEN: %s\n", g.InitialFEN()))
|
|
|
|
moves := g.Moves()
|
|
for i := 0; i < len(moves); i += 2 {
|
|
moveNum := i/2 + 1
|
|
white := moves[i]
|
|
if i+1 < len(moves) {
|
|
black := moves[i+1]
|
|
c.ShowMessage(fmt.Sprintf("%d. %s | %s\n", moveNum, white, black))
|
|
} else {
|
|
c.ShowMessage(fmt.Sprintf("%d. %s | ...\n", moveNum, white))
|
|
}
|
|
}
|
|
c.ShowMessage(fmt.Sprintf("Current FEN: %s\n", g.CurrentFEN()))
|
|
c.ShowMessage(fmt.Sprintf("Game state: %s\n", g.State()))
|
|
}
|
|
|
|
func (c *CLI) ShowComputerMove(result *game.MoveResult) {
|
|
if c.verbose {
|
|
c.ShowMessage(fmt.Sprintf("Computer (%c): %s (depth=%d, score=%d)\n",
|
|
result.Player, result.Move, result.Depth, result.Score))
|
|
} else {
|
|
// Always show computer moves in non-verbose mode too
|
|
c.ShowMessage(fmt.Sprintf("Computer (%c): %s\n", result.Player, result.Move))
|
|
}
|
|
}
|
|
|
|
func (c *CLI) ShowHumanMove(move string) {
|
|
if c.verbose {
|
|
c.ShowMessage(fmt.Sprintf("Your move: %s\n", move))
|
|
}
|
|
}
|
|
|
|
func (c *CLI) ShowGameOver(state core.State) {
|
|
c.ShowMessage(fmt.Sprintf("\nGame Over: %s\n", state))
|
|
c.ShowMessage("Start a new game with 'new' or 'resume'.")
|
|
} |