v0.1.0 chess game in go, using external stockfish engine

This commit is contained in:
2025-10-26 17:34:50 -04:00
commit 8ba4357920
15 changed files with 1433 additions and 0 deletions

View File

@ -0,0 +1,248 @@
// FILE: internal/transport/cli/handler.go
package cli
import (
"fmt"
"strconv"
"strings"
"chess/internal/cli"
"chess/internal/core"
"chess/internal/service"
"github.com/google/uuid"
)
type CLIHandler struct {
svc *service.Service
view *cli.CLI
gameID string
}
func New(svc *service.Service, view *cli.CLI) *CLIHandler {
return &CLIHandler{
svc: svc,
view: view,
}
}
// Main game loop - simple command processing
func (h *CLIHandler) Run() {
for {
// Generate prompt based on current game state
prompt := h.getPrompt()
h.view.ShowPrompt(prompt)
// Get command (blocking)
cmd, err := h.view.GetCommand()
if err != nil {
break
}
// Process command - returns false to exit
if !h.ProcessCommand(cmd) {
break
}
}
}
// Generates the appropriate command prompt
func (h *CLIHandler) getPrompt() string {
prompt := "> "
if h.gameID != "" {
g, err := h.svc.GetGame(h.gameID)
if err == nil && g.State() == core.StateOngoing {
// Always show whose turn it is
prompt = fmt.Sprintf("[%c]> ", g.NextTurn())
if g.NextPlayer().Type == core.PlayerComputer {
prompt = "ENTER to execute computer move\n" + prompt
}
}
}
return prompt
}
// Handles user commands - returns false to exit
func (h *CLIHandler) ProcessCommand(cmd *cli.Command) bool {
switch cmd.Type {
case cli.CmdQuit:
return false
case cli.CmdNone:
// Empty command triggers computer move if it's computer's turn
if h.gameID != "" {
g, err := h.svc.GetGame(h.gameID)
if err == nil && g.State() == core.StateOngoing &&
g.NextPlayer().Type == core.PlayerComputer {
h.executeComputerMove()
}
}
return true
case cli.CmdNew:
return h.handleNewGame("")
case cli.CmdResume:
if len(cmd.Args) < 1 {
h.view.ShowMessage("Usage: resume <FEN string>")
return true
}
fen := strings.Join(cmd.Args, " ")
return h.handleNewGame(fen)
case cli.CmdMove:
if h.gameID == "" {
h.view.ShowMessage("No active game. Use 'new' or 'resume <FEN>'.")
return true
}
g, _ := h.svc.GetGame(h.gameID)
if g.NextPlayer().Type != core.PlayerHuman {
h.view.ShowMessage("It's not a human player's turn. Press ENTER to execute computer move.")
return true
}
if err := h.svc.MakeHumanMove(h.gameID, cmd.Args[0]); err != nil {
h.view.ShowError(fmt.Errorf("invalid move: %v", err))
return true
}
// Get result and display human move
g, _ = h.svc.GetGame(h.gameID)
result := g.LastResult()
if result != nil {
h.view.ShowHumanMove(result.Move)
}
board, _ := h.svc.GetCurrentBoard(h.gameID)
h.view.DisplayBoard(board)
if result != nil && result.GameState != core.StateOngoing {
h.view.ShowGameOver(result.GameState)
h.gameID = ""
}
case cli.CmdUndo:
if h.gameID == "" {
h.view.ShowMessage("No active game.")
return true
}
// Parse undo count
count := 1
if len(cmd.Args) > 0 {
if n, err := strconv.Atoi(cmd.Args[0]); err == nil && n > 0 {
count = n
} else {
h.view.ShowMessage("Invalid undo count. Usage: undo [count]")
return true
}
}
if err := h.svc.Undo(h.gameID, count); err != nil {
h.view.ShowError(err)
} else {
if count == 1 {
h.view.ShowMessage("Move undone")
} else {
h.view.ShowMessage(fmt.Sprintf("%d moves undone", count))
}
board, _ := h.svc.GetCurrentBoard(h.gameID)
h.view.DisplayBoard(board)
}
case cli.CmdColor:
if len(cmd.Args) < 1 {
h.view.ShowMessage("Usage: color <off|brown|green|gray>")
return true
}
theme := cli.ColorTheme(cmd.Args[0])
if err := h.view.SetTheme(theme); err != nil {
h.view.ShowError(err)
} else {
h.view.ShowMessage(fmt.Sprintf("Color theme set to: %s", theme))
if h.gameID != "" {
board, _ := h.svc.GetCurrentBoard(h.gameID)
h.view.DisplayBoard(board)
}
}
case cli.CmdVerbose:
verbose := h.view.ToggleVerbose()
h.view.ShowMessage(fmt.Sprintf("Verbose mode: %t", verbose))
case cli.CmdHistory:
if h.gameID == "" {
h.view.ShowMessage("No active game.")
return true
}
g, _ := h.svc.GetGame(h.gameID)
h.view.ShowGameHistory(g)
case cli.CmdHelp:
h.view.ShowHelp()
}
return true
}
func (h *CLIHandler) executeComputerMove() {
result, err := h.svc.MakeComputerMove(h.gameID)
if err != nil {
h.view.ShowError(fmt.Errorf("engine error: %v", err))
h.gameID = ""
return
}
h.view.ShowComputerMove(result)
board, _ := h.svc.GetCurrentBoard(h.gameID)
h.view.DisplayBoard(board)
if result.GameState != core.StateOngoing {
h.view.ShowGameOver(result.GameState)
h.gameID = ""
}
}
// Starts a new game with player type selection
func (h *CLIHandler) handleNewGame(fen string) bool {
// Get player types
h.view.ShowPrompt("Select White player (h/c): ")
whiteInput := h.view.ReadLine()
var whiteType core.PlayerType
if whiteInput == "c" || whiteInput == "computer" {
whiteType = core.PlayerComputer
} else {
whiteType = core.PlayerHuman
}
h.view.ShowPrompt("Select Black player (h/c): ")
blackInput := h.view.ReadLine()
var blackType core.PlayerType
if blackInput == "c" || blackInput == "computer" {
blackType = core.PlayerComputer
} else {
blackType = core.PlayerHuman
}
// Create new game
h.gameID = uuid.New().String()
var fenArray []string
if fen != "" {
fenArray = []string{fen}
}
if err := h.svc.NewGame(h.gameID, whiteType, blackType, fenArray...); err != nil {
h.view.ShowError(fmt.Errorf("could not start the game: %v", err))
h.gameID = ""
return true
}
h.view.ShowMessage("Game started.")
board, _ := h.svc.GetCurrentBoard(h.gameID)
h.view.DisplayBoard(board)
return true
}