use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::engine::game::Game;
use crate::engine::san::move_to_san;
use crate::engine::types::{Color, Move, PieceType, Square};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateGameRequest {
pub fen: Option<String>,
pub ai_enabled: Option<bool>,
pub ai_difficulty: Option<String>,
pub ai_color: Option<String>,
pub white_player: Option<String>,
pub black_player: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MoveRequest {
pub from: String,
pub to: String,
pub promotion: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AiMoveRequest {
pub difficulty: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FenRequest {
pub fen: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListGamesQuery {
pub limit: Option<usize>,
pub offset: Option<usize>,
pub status: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LegalMovesQuery {
pub from: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnalysisQuery {
pub difficulty: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatRequest {
pub message: String,
pub provider: Option<String>,
pub api_key: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatResponse {
pub response: String,
pub provider: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub game_context: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggestions: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReactionRequest {
#[serde(rename = "move")]
pub chess_move: String,
pub provider: Option<String>,
pub api_key: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReactionResponse {
pub reaction: String,
pub provider: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub game_context: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChatStatusResponse {
pub enabled: bool,
pub provider: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HealthResponse {
pub status: String,
pub version: String,
pub language: String,
pub engine: String,
pub uptime: u64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteResponse {
pub success: bool,
pub message: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GameResponse {
pub id: String,
pub board: Vec<Vec<Option<String>>>,
pub fen: String,
pub status: String,
pub current_player: String,
pub move_history: Vec<MoveHistoryEntry>,
pub captured_pieces: CapturedPieces,
pub check: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_move: Option<LastMove>,
pub players: Players,
pub created_at: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MoveHistoryEntry {
pub from: String,
pub to: String,
pub piece: PieceInfo,
pub captured: Option<PieceInfo>,
pub promotion: Option<String>,
pub san: String,
pub fen: String,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PieceInfo {
#[serde(rename = "type")]
pub piece_type: String,
pub color: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CapturedPieces {
pub white: Vec<String>,
pub black: Vec<String>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct LastMove {
pub from: String,
pub to: String,
pub san: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Players {
pub white: String,
pub black: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ListGamesResponse {
pub games: Vec<GameResponse>,
pub total: usize,
pub limit: usize,
pub offset: usize,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MoveListResponse {
pub moves: Vec<MoveHistoryEntry>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LegalMoveEntry {
pub from: String,
pub to: String,
pub san: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub promotion: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LegalMovesResponse {
pub moves: Vec<LegalMoveEntry>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AiMoveResponse {
#[serde(rename = "move")]
pub ai_move: LastMove,
#[serde(flatten)]
pub game: GameResponse,
pub evaluation: Option<i32>,
pub thinking_time: u64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct HintResponse {
pub hint: LastMove,
pub evaluation: Option<i32>,
pub thinking_time: u64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AnalysisResponse {
pub evaluation: i32,
pub best_move: Option<LastMove>,
pub depth: u32,
pub nodes_searched: u64,
pub thinking_time: u64,
}
fn piece_type_name(pt: PieceType) -> &'static str {
match pt {
PieceType::Pawn => "pawn",
PieceType::Knight => "knight",
PieceType::Bishop => "bishop",
PieceType::Rook => "rook",
PieceType::Queen => "queen",
PieceType::King => "king",
}
}
pub(crate) fn color_name(c: Color) -> &'static str {
match c {
Color::White => "white",
Color::Black => "black",
}
}
pub fn parse_promotion(s: &str) -> Option<PieceType> {
match s.to_lowercase().as_str() {
"queen" | "q" => Some(PieceType::Queen),
"rook" | "r" => Some(PieceType::Rook),
"bishop" | "b" => Some(PieceType::Bishop),
"knight" | "n" => Some(PieceType::Knight),
_ => None,
}
}
fn promotion_name(pt: PieceType) -> &'static str {
match pt {
PieceType::Queen => "queen",
PieceType::Rook => "rook",
PieceType::Bishop => "bishop",
PieceType::Knight => "knight",
_ => "queen",
}
}
pub fn board_to_api(game: &Game) -> Vec<Vec<Option<String>>> {
let pos = game.position();
let mut rows = Vec::with_capacity(8);
for rank in (0..8u8).rev() {
let mut row = Vec::with_capacity(8);
for file in 0..8u8 {
let sq = Square::from_file_rank(file, rank);
match pos.piece_at(sq) {
Some((color, pt)) => {
let ch = pt.to_char(color);
row.push(Some(ch.to_string()));
}
None => row.push(None),
}
}
rows.push(row);
}
rows
}
pub fn captured_pieces(game: &Game) -> CapturedPieces {
let mut white_captured: Vec<String> = Vec::new(); let mut black_captured: Vec<String> = Vec::new();
let starting_counts = if game.started_from_fen() {
if let Ok(start) = crate::engine::board::Position::from_fen(game.starting_fen()) {
count_pieces(&start)
} else {
starting_piece_counts()
}
} else {
starting_piece_counts()
};
let current = count_pieces(game.position());
for pt_idx in 0..6 {
let diff = starting_counts[0][pt_idx] as i32 - current[0][pt_idx] as i32;
let pt = PieceType::ALL[pt_idx];
for _ in 0..diff.max(0) {
white_captured.push(pt.to_char(Color::White).to_string());
}
}
for pt_idx in 0..6 {
let diff = starting_counts[1][pt_idx] as i32 - current[1][pt_idx] as i32;
let pt = PieceType::ALL[pt_idx];
for _ in 0..diff.max(0) {
black_captured.push(pt.to_char(Color::Black).to_string());
}
}
CapturedPieces {
white: black_captured, black: white_captured, }
}
fn count_pieces(pos: &crate::engine::board::Position) -> [[u32; 6]; 2] {
let mut counts = [[0u32; 6]; 2];
for (color, row) in counts.iter_mut().enumerate() {
for (pt, cell) in row.iter_mut().enumerate() {
*cell = pos.pieces[color][pt].pop_count();
}
}
counts
}
fn starting_piece_counts() -> [[u32; 6]; 2] {
[[8, 2, 2, 2, 1, 1], [8, 2, 2, 2, 1, 1]]
}
pub fn game_to_response(game: &Game) -> GameResponse {
let status = game.status();
let is_check = matches!(status, crate::engine::types::GameStatus::Check);
let last_move = game.move_history().last().map(|rec| LastMove {
from: rec.mv.from.to_algebraic(),
to: rec.mv.to.to_algebraic(),
san: rec.san.clone(),
});
GameResponse {
id: game.id.clone(),
board: board_to_api(game),
fen: game.to_fen(),
status: status.as_str().to_string(),
current_player: color_name(game.side_to_move()).to_string(),
move_history: build_move_history(game),
captured_pieces: captured_pieces(game),
check: is_check,
last_move,
players: Players {
white: game.white_player.clone(),
black: game.black_player.clone(),
},
created_at: game.created_at.to_rfc3339(),
}
}
fn build_move_history(game: &Game) -> Vec<MoveHistoryEntry> {
let history = game.move_history();
if history.is_empty() {
return Vec::new();
}
let mut replay = if game.started_from_fen() {
match Game::from_fen(game.starting_fen()) {
Ok(g) => g,
Err(_) => return Vec::new(),
}
} else {
Game::new()
};
let mut entries = Vec::with_capacity(history.len());
for rec in history {
let mv = rec.mv;
let pos = replay.position();
let side = pos.side_to_move;
let piece_info = pos
.piece_at(mv.from)
.map(|(c, pt)| PieceInfo {
piece_type: piece_type_name(pt).to_string(),
color: color_name(c).to_string(),
})
.unwrap_or(PieceInfo {
piece_type: "pawn".to_string(),
color: color_name(side).to_string(),
});
let captured = if mv.flags.is_capture() {
if mv.flags.is_en_passant() {
Some(PieceInfo {
piece_type: "pawn".to_string(),
color: color_name(!side).to_string(),
})
} else {
pos.piece_at(mv.to).map(|(c, pt)| PieceInfo {
piece_type: piece_type_name(pt).to_string(),
color: color_name(c).to_string(),
})
}
} else {
None
};
let promotion = mv.promotion.map(|pt| promotion_name(pt).to_string());
let _ = replay.make_move(mv);
let fen_after = replay.to_fen();
entries.push(MoveHistoryEntry {
from: mv.from.to_algebraic(),
to: mv.to.to_algebraic(),
piece: piece_info,
captured,
promotion,
san: rec.san.clone(),
fen: fen_after,
});
}
entries
}
pub fn legal_move_entry(game: &Game, mv: Move, legal: &[Move]) -> LegalMoveEntry {
let san = move_to_san(game.position(), mv, legal);
LegalMoveEntry {
from: mv.from.to_algebraic(),
to: mv.to.to_algebraic(),
san,
promotion: mv.promotion.map(|pt| promotion_name(pt).to_string()),
}
}