316 lines
8.1 KiB
Go
316 lines
8.1 KiB
Go
// FILE: internal/transport/http/game_handler.go
|
|
package http
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"chess/internal/board"
|
|
"chess/internal/core"
|
|
"chess/internal/game"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// CreateGame creates a new game with specified player types
|
|
func (h *HTTPHandler) CreateGame(c *fiber.Ctx) error {
|
|
var req CreateGameRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(ErrorResponse{
|
|
Error: "invalid request body",
|
|
Code: ErrInvalidRequest,
|
|
Details: err.Error(),
|
|
})
|
|
}
|
|
|
|
gameID := uuid.New().String()
|
|
|
|
// Create game with proper type conversion
|
|
var fenArray []string
|
|
if req.FEN != "" {
|
|
fenArray = []string{req.FEN}
|
|
}
|
|
|
|
err := h.svc.NewGame(
|
|
gameID,
|
|
core.PlayerType(req.White),
|
|
core.PlayerType(req.Black),
|
|
fenArray...,
|
|
)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(ErrorResponse{
|
|
Error: "failed to create game",
|
|
Code: ErrInvalidRequest,
|
|
Details: err.Error(),
|
|
})
|
|
}
|
|
|
|
// Build response - cache game instance
|
|
g, _ := h.svc.GetGame(gameID)
|
|
response := h.buildGameResponse(gameID, g)
|
|
|
|
// Execute computer move if computer starts
|
|
if g.NextPlayer().Type == core.PlayerComputer && g.State() == core.StateOngoing {
|
|
if err := h.executeComputerMove(gameID, g, &response); err != nil {
|
|
// Log error but return game created successfully
|
|
fmt.Printf("Warning: failed to execute initial computer move: %v\n", err)
|
|
}
|
|
}
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(response)
|
|
}
|
|
|
|
// GetGame retrieves current game state, executing computer move if needed
|
|
func (h *HTTPHandler) GetGame(c *fiber.Ctx) error {
|
|
gameID := c.Params("gameId")
|
|
|
|
g, err := h.svc.GetGame(gameID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(ErrorResponse{
|
|
Error: "game not found",
|
|
Code: ErrGameNotFound,
|
|
})
|
|
}
|
|
|
|
response := h.buildGameResponse(gameID, g)
|
|
|
|
// Auto-execute computer move if it's computer's turn
|
|
if g.NextPlayer().Type == core.PlayerComputer && g.State() == core.StateOngoing {
|
|
if err := h.executeComputerMove(gameID, g, &response); err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(ErrorResponse{
|
|
Error: "failed to execute computer move",
|
|
Code: ErrInternalError,
|
|
Details: err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return c.JSON(response)
|
|
}
|
|
|
|
// MakeMove submits a human player move
|
|
func (h *HTTPHandler) MakeMove(c *fiber.Ctx) error {
|
|
gameID := c.Params("gameId")
|
|
|
|
var req MoveRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(ErrorResponse{
|
|
Error: "invalid request body",
|
|
Code: ErrInvalidRequest,
|
|
Details: err.Error(),
|
|
})
|
|
}
|
|
|
|
g, err := h.svc.GetGame(gameID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(ErrorResponse{
|
|
Error: "game not found",
|
|
Code: ErrGameNotFound,
|
|
})
|
|
}
|
|
|
|
// Check game state BEFORE making move
|
|
if g.State() != core.StateOngoing {
|
|
fmt.Printf("DEBUG: Move rejected - game over (state: %s)\n", g.State())
|
|
return c.Status(fiber.StatusBadRequest).JSON(ErrorResponse{
|
|
Error: "game is over",
|
|
Code: ErrGameOver,
|
|
Details: fmt.Sprintf("game state: %s", g.State()),
|
|
})
|
|
}
|
|
|
|
// Verify it's human's turn
|
|
currentPlayer := g.NextPlayer()
|
|
if currentPlayer.Type != core.PlayerHuman {
|
|
fmt.Printf("DEBUG: Move rejected - not human turn (current: %v, turn: %c)\n",
|
|
currentPlayer.Type, g.NextTurn())
|
|
return c.Status(fiber.StatusBadRequest).JSON(ErrorResponse{
|
|
Error: "not human player's turn",
|
|
Code: ErrNotHumanTurn,
|
|
Details: fmt.Sprintf("current turn: %c", g.NextTurn()),
|
|
})
|
|
}
|
|
|
|
fmt.Printf("DEBUG: Attempting human move %s for game %s\n", req.Move, gameID)
|
|
|
|
// Make human move
|
|
if err := h.svc.MakeHumanMove(gameID, req.Move); err != nil {
|
|
fmt.Printf("DEBUG: Move failed: %v\n", err)
|
|
return c.Status(fiber.StatusBadRequest).JSON(ErrorResponse{
|
|
Error: "invalid move",
|
|
Code: ErrInvalidMove,
|
|
Details: err.Error(),
|
|
})
|
|
}
|
|
|
|
// Get updated game state - refresh g
|
|
g, _ = h.svc.GetGame(gameID)
|
|
response := h.buildGameResponse(gameID, g)
|
|
|
|
// Include human move info from LastResult
|
|
if result := g.LastResult(); result != nil {
|
|
response.LastMove = &MoveInfo{
|
|
Move: result.Move,
|
|
Player: colorToString(result.Player),
|
|
}
|
|
}
|
|
|
|
fmt.Printf("DEBUG: Human move successful, new state: %s, next turn: %c\n",
|
|
g.State(), g.NextTurn())
|
|
|
|
// Execute computer response if needed
|
|
if g.NextPlayer().Type == core.PlayerComputer && g.State() == core.StateOngoing {
|
|
fmt.Printf("DEBUG: Executing computer response\n")
|
|
if err := h.executeComputerMove(gameID, g, &response); err != nil {
|
|
// Computer move failed, but human move succeeded
|
|
fmt.Printf("Warning: computer move failed: %v\n", err)
|
|
}
|
|
}
|
|
|
|
return c.JSON(response)
|
|
}
|
|
|
|
// UndoMove undoes one or more moves
|
|
func (h *HTTPHandler) UndoMove(c *fiber.Ctx) error {
|
|
gameID := c.Params("gameId")
|
|
|
|
var req UndoRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
// Body parsing failed, use default
|
|
req.Count = 1
|
|
}
|
|
|
|
if req.Count < 1 {
|
|
req.Count = 1
|
|
}
|
|
|
|
if err := h.svc.Undo(gameID, req.Count); err != nil {
|
|
// Determine if game not found or invalid undo
|
|
if strings.Contains(err.Error(), "not found") {
|
|
return c.Status(fiber.StatusNotFound).JSON(ErrorResponse{
|
|
Error: "game not found",
|
|
Code: ErrGameNotFound,
|
|
})
|
|
}
|
|
return c.Status(fiber.StatusBadRequest).JSON(ErrorResponse{
|
|
Error: "cannot undo moves",
|
|
Code: ErrInvalidRequest,
|
|
Details: err.Error(),
|
|
})
|
|
}
|
|
|
|
// Return updated game state
|
|
g, _ := h.svc.GetGame(gameID)
|
|
response := h.buildGameResponse(gameID, g)
|
|
|
|
return c.JSON(response)
|
|
}
|
|
|
|
// DeleteGame ends and cleans up a game
|
|
func (h *HTTPHandler) DeleteGame(c *fiber.Ctx) error {
|
|
gameID := c.Params("gameId")
|
|
|
|
if err := h.svc.DeleteGame(gameID); err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(ErrorResponse{
|
|
Error: "game not found",
|
|
Code: ErrGameNotFound,
|
|
})
|
|
}
|
|
|
|
return c.SendStatus(fiber.StatusNoContent)
|
|
}
|
|
|
|
// GetBoard returns ASCII representation of the board
|
|
func (h *HTTPHandler) GetBoard(c *fiber.Ctx) error {
|
|
gameID := c.Params("gameId")
|
|
|
|
g, err := h.svc.GetGame(gameID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(ErrorResponse{
|
|
Error: "game not found",
|
|
Code: ErrGameNotFound,
|
|
})
|
|
}
|
|
|
|
b, _ := h.svc.GetCurrentBoard(gameID)
|
|
|
|
// Generate ASCII board
|
|
ascii := h.generateASCIIBoard(b)
|
|
|
|
return c.JSON(BoardResponse{
|
|
FEN: g.CurrentFEN(),
|
|
Board: ascii,
|
|
})
|
|
}
|
|
|
|
// Helper: Build standard game response - FIXED to use GetPlayer()
|
|
func (h *HTTPHandler) buildGameResponse(gameID string, g *game.Game) GameResponse {
|
|
whitePlayer := g.GetPlayer(core.ColorWhite)
|
|
blackPlayer := g.GetPlayer(core.ColorBlack)
|
|
|
|
return GameResponse{
|
|
GameID: gameID,
|
|
FEN: g.CurrentFEN(),
|
|
Turn: colorToString(g.NextTurn()),
|
|
State: stateToString(g.State()),
|
|
Moves: g.Moves(),
|
|
Players: PlayersInfo{
|
|
White: PlayerType(whitePlayer.Type),
|
|
Black: PlayerType(blackPlayer.Type),
|
|
},
|
|
}
|
|
}
|
|
|
|
// Helper: Execute computer move and update response - FIXED to accept game instance
|
|
func (h *HTTPHandler) executeComputerMove(gameID string, g *game.Game, response *GameResponse) error {
|
|
result, err := h.svc.MakeComputerMove(gameID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Refresh game state after computer move
|
|
g, _ = h.svc.GetGame(gameID)
|
|
|
|
// Update response fields
|
|
response.FEN = g.CurrentFEN()
|
|
response.Turn = colorToString(g.NextTurn())
|
|
response.State = stateToString(g.State())
|
|
response.Moves = g.Moves()
|
|
|
|
// Add computer move info
|
|
if result != nil {
|
|
response.LastMove = &MoveInfo{
|
|
Move: result.Move,
|
|
Player: colorToString(result.Player),
|
|
Score: result.Score,
|
|
Depth: result.Depth,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper: Generate ASCII board representation
|
|
func (h *HTTPHandler) generateASCIIBoard(b *board.Board) string {
|
|
var sb strings.Builder
|
|
sb.WriteString(" 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++ {
|
|
square := fmt.Sprintf("%c%c", 'a'+f, '8'-r)
|
|
piece := b.GetPieceAt(square)
|
|
|
|
if piece == 0 {
|
|
sb.WriteString(". ")
|
|
} else {
|
|
sb.WriteString(fmt.Sprintf("%c ", piece))
|
|
}
|
|
}
|
|
sb.WriteString(fmt.Sprintf(" %d\n", 8-r))
|
|
}
|
|
sb.WriteString(" a b c d e f g h")
|
|
|
|
return sb.String()
|
|
} |