From 20a3e72ac4ecafb8d48410711e7e6f72e52a0adb Mon Sep 17 00:00:00 2001 From: lashman Date: Mon, 22 Sep 2025 21:51:26 +0300 Subject: [PATCH] the api surface --- cruciverb-server/src/api.rs | 165 ++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 cruciverb-server/src/api.rs diff --git a/cruciverb-server/src/api.rs b/cruciverb-server/src/api.rs new file mode 100644 index 0000000..47c345d --- /dev/null +++ b/cruciverb-server/src/api.rs @@ -0,0 +1,165 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use cruciverb_core::grid::Cell; +use cruciverb_core::puzzle::Puzzle; + +// -- request types -- + +#[derive(Deserialize)] +pub struct GenerateRequest { + pub difficulty: Option, + pub size: Option, + pub density: Option, + pub clue_difficulty: Option, + pub seed: Option, + pub theme: Option, + pub cryptic: Option, + pub pack: Option, +} + +#[derive(Deserialize)] +pub struct ValidateRequest { + pub cells: Vec>>, + pub elapsed: Option, + // kept for client compatibility; the server now uses its own clock for streaks + #[allow(dead_code)] + pub local_date: Option, + pub room_code: Option, + pub cell_results: Option, +} + +#[derive(Deserialize)] +pub struct HintRequest { + pub kind: String, + pub row: usize, + pub col: usize, + pub direction: Option, + pub letter: Option, +} + +#[derive(Deserialize)] +pub struct ProgressRequest { + pub cells: Vec>>, +} + +#[derive(Deserialize)] +pub struct UpdateMeRequest { + pub display_name: Option, +} + +// -- response types -- + +#[derive(Serialize)] +pub struct GenerateResponse { + pub id: String, + pub short_id: String, + pub puzzle: ClientPuzzle, +} + +#[derive(Serialize)] +pub struct ClientPuzzleResponse { + pub id: String, + pub short_id: String, + pub puzzle: ClientPuzzle, +} + +#[derive(Serialize)] +pub struct ClientPuzzle { + pub width: usize, + pub height: usize, + pub cells: Vec>, + pub clues_across: Vec, + pub clues_down: Vec, + pub difficulty_score: u32, + pub cell_numbers: HashMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub theme: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub theme_entries: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ClientCell { + Black, + Empty, +} + +#[derive(Serialize)] +pub struct ClientClue { + pub number: u32, + pub direction: String, + pub text: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub definition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub wordplay_type: Option, +} + +#[derive(Serialize)] +#[serde(untagged)] +pub enum HintResponse { + CheckCell { is_correct: bool }, + RevealLetter { row: usize, col: usize, letter: String }, + RevealWord { letters: Vec }, +} + +#[derive(Serialize)] +pub struct LetterPosition { + pub row: usize, + pub col: usize, + pub letter: String, +} + +// -- conversions -- + +pub fn to_client_puzzle(puzzle: &Puzzle, _id: Uuid) -> ClientPuzzle { + let mut cells = Vec::with_capacity(puzzle.height); + for r in 0..puzzle.height { + let mut row = Vec::with_capacity(puzzle.width); + for c in 0..puzzle.width { + match puzzle.grid.get(r, c) { + Cell::Black => row.push(ClientCell::Black), + _ => row.push(ClientCell::Empty), + } + } + cells.push(row); + } + + let mut cell_numbers: HashMap = HashMap::new(); + for word in &puzzle.words { + let key = format!("{},{}", word.row, word.col); + cell_numbers.entry(key).or_insert(word.clue_number); + } + + let clues_across = puzzle.clues_across.iter().map(|c| ClientClue { + number: c.number, + direction: "across".into(), + text: c.text.clone(), + definition: c.definition.clone(), + wordplay_type: c.wordplay_type.clone(), + }).collect(); + + let clues_down = puzzle.clues_down.iter().map(|c| ClientClue { + number: c.number, + direction: "down".into(), + text: c.text.clone(), + definition: c.definition.clone(), + wordplay_type: c.wordplay_type.clone(), + }).collect(); + + ClientPuzzle { + width: puzzle.width, + height: puzzle.height, + cells, + clues_across, + clues_down, + difficulty_score: puzzle.difficulty_score, + cell_numbers, + theme: puzzle.theme.clone(), + theme_entries: puzzle.theme_entries.clone(), + } +}