ipuz import
This commit is contained in:
@@ -0,0 +1,201 @@
|
|||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::config::SymmetryType;
|
||||||
|
use crate::grid::{Cell, Grid};
|
||||||
|
use crate::grid::slot::Direction;
|
||||||
|
use crate::puzzle::{Clue, PlacedWord, Puzzle};
|
||||||
|
|
||||||
|
pub fn from_ipuz(json: &Value) -> Result<Puzzle, String> {
|
||||||
|
let dims = json.get("dimensions").ok_or("missing dimensions")?;
|
||||||
|
let width = dims.get("width").and_then(|v| v.as_u64()).ok_or("missing width")? as usize;
|
||||||
|
let height = dims.get("height").and_then(|v| v.as_u64()).ok_or("missing height")? as usize;
|
||||||
|
|
||||||
|
if width == 0 || height == 0 || width > 50 || height > 50 {
|
||||||
|
return Err(format!("invalid grid size {}x{}", width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
let solution = json.get("solution").ok_or("missing solution grid")?;
|
||||||
|
let rows = solution.as_array().ok_or("solution is not an array")?;
|
||||||
|
if rows.len() != height {
|
||||||
|
return Err(format!("solution has {} rows, expected {}", rows.len(), height));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut grid = Grid::new(width, height);
|
||||||
|
for (r, row) in rows.iter().enumerate() {
|
||||||
|
let cells = row.as_array().ok_or("solution row is not an array")?;
|
||||||
|
if cells.len() != width {
|
||||||
|
return Err(format!("row {} has {} cells, expected {}", r, cells.len(), width));
|
||||||
|
}
|
||||||
|
for (c, cell) in cells.iter().enumerate() {
|
||||||
|
if cell.is_null() {
|
||||||
|
grid.set(r, c, Cell::Empty);
|
||||||
|
} else if let Some(s) = cell.as_str() {
|
||||||
|
if s == "#" {
|
||||||
|
grid.set(r, c, Cell::Black);
|
||||||
|
} else if s.len() == 1 {
|
||||||
|
let ch = s.chars().next().unwrap().to_ascii_uppercase();
|
||||||
|
if ch.is_ascii_alphabetic() {
|
||||||
|
grid.set(r, c, Cell::Letter(ch));
|
||||||
|
} else {
|
||||||
|
grid.set(r, c, Cell::Empty);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
grid.set(r, c, Cell::Empty);
|
||||||
|
}
|
||||||
|
} else if cell.as_u64().is_some() || cell.as_i64().is_some() {
|
||||||
|
// some ipuz files use 0 for empty cells in solution
|
||||||
|
grid.set(r, c, Cell::Empty);
|
||||||
|
} else {
|
||||||
|
grid.set(r, c, Cell::Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse clues
|
||||||
|
let clues_obj = json.get("clues").ok_or("missing clues")?;
|
||||||
|
let across_arr = clues_obj.get("Across").and_then(|v| v.as_array()).ok_or("missing Across clues")?;
|
||||||
|
let down_arr = clues_obj.get("Down").and_then(|v| v.as_array()).ok_or("missing Down clues")?;
|
||||||
|
|
||||||
|
let parsed_across = parse_clue_array(across_arr)?;
|
||||||
|
let parsed_down = parse_clue_array(down_arr)?;
|
||||||
|
|
||||||
|
// build word list and full clues from grid + parsed clue data
|
||||||
|
let mut clues_across = Vec::new();
|
||||||
|
let mut clues_down = Vec::new();
|
||||||
|
let mut words = Vec::new();
|
||||||
|
|
||||||
|
for (num, text) in &parsed_across {
|
||||||
|
if let Some((r, c)) = find_numbered_cell(&grid, &json, width, height, *num) {
|
||||||
|
let answer = read_word(&grid, r, c, width, height, Direction::Across);
|
||||||
|
words.push(PlacedWord {
|
||||||
|
word: answer.clone(),
|
||||||
|
row: r,
|
||||||
|
col: c,
|
||||||
|
direction: Direction::Across,
|
||||||
|
clue_number: *num,
|
||||||
|
});
|
||||||
|
clues_across.push(Clue {
|
||||||
|
number: *num,
|
||||||
|
direction: Direction::Across,
|
||||||
|
text: text.clone(),
|
||||||
|
answer,
|
||||||
|
definition: None,
|
||||||
|
wordplay_type: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (num, text) in &parsed_down {
|
||||||
|
if let Some((r, c)) = find_numbered_cell(&grid, &json, width, height, *num) {
|
||||||
|
let answer = read_word(&grid, r, c, width, height, Direction::Down);
|
||||||
|
words.push(PlacedWord {
|
||||||
|
word: answer.clone(),
|
||||||
|
row: r,
|
||||||
|
col: c,
|
||||||
|
direction: Direction::Down,
|
||||||
|
clue_number: *num,
|
||||||
|
});
|
||||||
|
clues_down.push(Clue {
|
||||||
|
number: *num,
|
||||||
|
direction: Direction::Down,
|
||||||
|
text: text.clone(),
|
||||||
|
answer,
|
||||||
|
definition: None,
|
||||||
|
wordplay_type: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Puzzle {
|
||||||
|
grid,
|
||||||
|
words,
|
||||||
|
clues_across,
|
||||||
|
clues_down,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
symmetry: SymmetryType::None,
|
||||||
|
difficulty_score: 50,
|
||||||
|
generation_time_ms: 0,
|
||||||
|
theme: None,
|
||||||
|
theme_entries: vec![],
|
||||||
|
is_cryptic: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_clue_array(arr: &[Value]) -> Result<Vec<(u32, String)>, String> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for entry in arr {
|
||||||
|
if let Some(pair) = entry.as_array() {
|
||||||
|
if pair.len() >= 2 {
|
||||||
|
let num = pair[0].as_u64().ok_or("clue number not a number")? as u32;
|
||||||
|
let text = pair[1].as_str().unwrap_or("").to_string();
|
||||||
|
result.push((num, text));
|
||||||
|
}
|
||||||
|
} else if let Some(obj) = entry.as_object() {
|
||||||
|
// some ipuz files use object format: {"number": N, "clue": "text"}
|
||||||
|
let num = obj.get("number").and_then(|v| v.as_u64()).ok_or("clue missing number")? as u32;
|
||||||
|
let text = obj.get("clue").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||||
|
result.push((num, text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_numbered_cell(grid: &Grid, json: &Value, width: usize, height: usize, target: u32) -> Option<(usize, usize)> {
|
||||||
|
// try the puzzle grid first (ipuz stores numbers there)
|
||||||
|
if let Some(puzzle_grid) = json.get("puzzle").and_then(|v| v.as_array()) {
|
||||||
|
for (r, row) in puzzle_grid.iter().enumerate() {
|
||||||
|
if let Some(cells) = row.as_array() {
|
||||||
|
for (c, cell) in cells.iter().enumerate() {
|
||||||
|
let num = if let Some(n) = cell.as_u64() {
|
||||||
|
n as u32
|
||||||
|
} else if let Some(obj) = cell.as_object() {
|
||||||
|
obj.get("cell").and_then(|v| v.as_u64()).unwrap_or(0) as u32
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
};
|
||||||
|
if num == target {
|
||||||
|
return Some((r, c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback: compute numbering from grid
|
||||||
|
let mut number = 1u32;
|
||||||
|
for r in 0..height {
|
||||||
|
for c in 0..width {
|
||||||
|
if grid.get(r, c).is_black() { continue; }
|
||||||
|
let starts_across = (c == 0 || grid.get(r, c.wrapping_sub(1)).is_black() || c.wrapping_sub(1) >= width)
|
||||||
|
&& c + 1 < width && !grid.get(r, c + 1).is_black();
|
||||||
|
let starts_down = (r == 0 || grid.get(r.wrapping_sub(1), c).is_black() || r.wrapping_sub(1) >= height)
|
||||||
|
&& r + 1 < height && !grid.get(r + 1, c).is_black();
|
||||||
|
if starts_across || starts_down {
|
||||||
|
if number == target {
|
||||||
|
return Some((r, c));
|
||||||
|
}
|
||||||
|
number += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_word(grid: &Grid, row: usize, col: usize, width: usize, height: usize, dir: Direction) -> String {
|
||||||
|
let mut word = String::new();
|
||||||
|
let (mut r, mut c) = (row, col);
|
||||||
|
loop {
|
||||||
|
if r >= height || c >= width { break; }
|
||||||
|
match grid.get(r, c) {
|
||||||
|
Cell::Letter(ch) => word.push(ch),
|
||||||
|
Cell::Empty => word.push('_'),
|
||||||
|
Cell::Black => break,
|
||||||
|
}
|
||||||
|
match dir {
|
||||||
|
Direction::Across => c += 1,
|
||||||
|
Direction::Down => r += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
word
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
pub mod puz;
|
||||||
|
pub mod ipuz;
|
||||||
|
|
||||||
|
pub fn detect_and_parse(data: &[u8]) -> Result<crate::puzzle::Puzzle, String> {
|
||||||
|
// try puz first (check for ACROSS&DOWN magic at offset 2)
|
||||||
|
if data.len() > 14 && &data[2..14] == b"ACROSS&DOWN\0" {
|
||||||
|
return puz::from_puz(data);
|
||||||
|
}
|
||||||
|
// try ipuz (valid JSON with crossword markers)
|
||||||
|
if let Ok(text) = std::str::from_utf8(data) {
|
||||||
|
if let Ok(json) = serde_json::from_str::<serde_json::Value>(text) {
|
||||||
|
if json.get("kind").is_some() || json.get("puzzle").is_some() || json.get("dimensions").is_some() {
|
||||||
|
return ipuz::from_ipuz(&json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err("unrecognized file format".into())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user