Files
chess/internal/service/service.go

240 lines
5.5 KiB
Go

// FILE: internal/service/service.go
package service
import (
"fmt"
"strings"
"chess/internal/board"
"chess/internal/core"
"chess/internal/engine"
"chess/internal/game"
)
type Service struct {
games map[string]*game.Game
engine *engine.UCI
}
func New() (*Service, error) {
eng, err := engine.New()
if err != nil {
return nil, fmt.Errorf("failed to initialize engine: %v", err)
}
return &Service{
games: make(map[string]*game.Game),
engine: eng,
}, nil
}
func (s *Service) NewGame(id string, whiteType, blackType core.PlayerType, fen ...string) error {
initialFEN := board.StartingFEN
if len(fen) > 0 && fen[0] != "" {
initialFEN = fen[0]
}
// Use the engine to validate and canonicalize the FEN
s.engine.NewGame()
s.engine.SetPosition(initialFEN, []string{})
validatedFEN, err := s.engine.GetFEN()
if err != nil {
return fmt.Errorf("could not get FEN from engine: %v", err)
}
b, err := board.FEN(validatedFEN)
if err != nil {
return fmt.Errorf("engine returned invalid FEN: %v", err)
}
startingTurn := b.Turn()
// Setup players based on types
whitePlayer := &core.Player{
ID: "white",
Type: whiteType,
}
if whiteType == core.PlayerComputer {
whitePlayer.ID = "stockfish-white"
}
blackPlayer := &core.Player{
ID: "black",
Type: blackType,
}
if blackType == core.PlayerComputer {
blackPlayer.ID = "stockfish-black"
}
s.games[id] = game.New(validatedFEN, whitePlayer, blackPlayer, startingTurn)
return nil
}
func (s *Service) MakeHumanMove(gameID, uci string) error {
// Basic move format validation
uci = strings.ToLower(strings.TrimSpace(uci))
if len(uci) < 4 || len(uci) > 5 {
return fmt.Errorf("invalid move format: expected e2e4 or e7e8q")
}
g, ok := s.games[gameID]
if !ok {
return fmt.Errorf("game not found")
}
// Check if it's human's turn
if g.NextPlayer().Type != core.PlayerHuman {
return fmt.Errorf("not a human player's turn")
}
currentFEN := g.CurrentFEN()
humanColor := g.NextTurn()
// Try to apply human move
s.engine.SetPosition(currentFEN, []string{uci})
// Get FEN after human move to check if move was legal
humanMoveFEN, err := s.engine.GetFEN()
if err != nil {
return fmt.Errorf("failed to get position: %v", err)
}
// If position didn't change, move was illegal
if humanMoveFEN == currentFEN {
return fmt.Errorf("illegal move")
}
// Record human move
g.AddSnapshot(humanMoveFEN, uci, core.OppositeColor(humanColor))
// Check if opponent has any legal moves
s.engine.SetPosition(humanMoveFEN, []string{})
search, _ := s.engine.Search(100) // Quick search to check for legal moves
result := &game.MoveResult{
Move: uci,
Player: humanColor,
GameState: core.StateOngoing,
}
if search.BestMove == "" || search.BestMove == "(none)" {
// Human checkmated the opponent
if humanColor == core.ColorWhite {
g.SetState(core.StateWhiteWins)
} else {
g.SetState(core.StateBlackWins)
}
result.GameState = g.State()
}
// Store result in game instead of service
g.SetLastResult(result)
return nil
}
func (s *Service) MakeComputerMove(gameID string) (*game.MoveResult, error) {
g, ok := s.games[gameID]
if !ok {
return nil, fmt.Errorf("game not found: %s", gameID)
}
if g.NextPlayer().Type != core.PlayerComputer {
return nil, fmt.Errorf("not computer's turn")
}
currentColor := g.NextTurn()
s.engine.SetPosition(g.CurrentFEN(), []string{})
search, err := s.engine.Search(1000)
if err != nil {
return nil, fmt.Errorf("engine error: %v", err)
}
result := &game.MoveResult{
Player: currentColor,
Score: search.Score,
Depth: search.Depth,
GameState: core.StateOngoing,
}
if search.BestMove == "" || search.BestMove == "(none)" {
// No legal moves - computer is checkmated
if currentColor == core.ColorWhite {
g.SetState(core.StateBlackWins)
} else {
g.SetState(core.StateWhiteWins)
}
result.GameState = g.State()
g.SetLastResult(result)
return result, nil
}
result.Move = search.BestMove
// Apply move and get resulting FEN
s.engine.SetPosition(g.CurrentFEN(), []string{search.BestMove})
newFEN, err := s.engine.GetFEN()
if err != nil {
return nil, fmt.Errorf("failed to get position: %v", err)
}
g.AddSnapshot(newFEN, search.BestMove, core.OppositeColor(currentColor))
// Check if opponent has any legal moves
s.engine.SetPosition(newFEN, []string{})
testSearch, _ := s.engine.Search(100)
if testSearch.BestMove == "" || testSearch.BestMove == "(none)" {
// Computer checkmated the opponent
if currentColor == core.ColorWhite {
g.SetState(core.StateWhiteWins)
} else {
g.SetState(core.StateBlackWins)
}
result.GameState = g.State()
}
// Store result in game
g.SetLastResult(result)
return result, nil
}
func (s *Service) Undo(gameID string, count int) error {
g, ok := s.games[gameID]
if !ok {
return fmt.Errorf("game not found: %s", gameID)
}
return g.UndoMoves(count)
}
func (s *Service) GetCurrentBoard(gameID string) (*board.Board, error) {
g, ok := s.games[gameID]
if !ok {
return nil, fmt.Errorf("game not found: %s", gameID)
}
return board.FEN(g.CurrentFEN())
}
func (s *Service) GetGame(gameID string) (*game.Game, error) {
g, ok := s.games[gameID]
if !ok {
return nil, fmt.Errorf("game not found: %s", gameID)
}
return g, nil
}
func (s *Service) DeleteGame(gameID string) error {
if _, ok := s.games[gameID]; !ok {
return fmt.Errorf("game not found: %s", gameID)
}
delete(s.games, gameID)
return nil
}
func (s *Service) Close() error {
if s.engine != nil {
return s.engine.Close()
}
return nil
}