puz import
This commit is contained in:
@@ -0,0 +1,192 @@
|
|||||||
|
use crate::config::SymmetryType;
|
||||||
|
use crate::grid::{Cell, Grid};
|
||||||
|
use crate::grid::slot::Direction;
|
||||||
|
use crate::puzzle::{Clue, PlacedWord, Puzzle};
|
||||||
|
|
||||||
|
pub fn from_puz(data: &[u8]) -> Result<Puzzle, String> {
|
||||||
|
if data.len() < 52 {
|
||||||
|
return Err("file too short for .puz format".into());
|
||||||
|
}
|
||||||
|
if &data[2..14] != b"ACROSS&DOWN\0" {
|
||||||
|
return Err("missing ACROSS&DOWN magic".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = data[44] as usize;
|
||||||
|
let height = data[45] as usize;
|
||||||
|
let num_clues = u16::from_le_bytes([data[46], data[47]]) as usize;
|
||||||
|
|
||||||
|
if width == 0 || height == 0 || width > 50 || height > 50 {
|
||||||
|
return Err(format!("invalid grid size {}x{}", width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
let board_size = width * height;
|
||||||
|
let solution_start = 52;
|
||||||
|
let state_start = solution_start + board_size;
|
||||||
|
let strings_start = state_start + board_size;
|
||||||
|
|
||||||
|
if data.len() < strings_start {
|
||||||
|
return Err("file truncated before grid data".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let solution = &data[solution_start..state_start];
|
||||||
|
|
||||||
|
// build grid
|
||||||
|
let mut grid = Grid::new(width, height);
|
||||||
|
for i in 0..board_size {
|
||||||
|
let r = i / width;
|
||||||
|
let c = i % width;
|
||||||
|
let b = solution[i];
|
||||||
|
if b == b'.' {
|
||||||
|
grid.set(r, c, Cell::Black);
|
||||||
|
} else if b.is_ascii_alphabetic() {
|
||||||
|
grid.set(r, c, Cell::Letter((b as char).to_ascii_uppercase()));
|
||||||
|
} else {
|
||||||
|
grid.set(r, c, Cell::Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse null-terminated strings after the state grid
|
||||||
|
let string_data = &data[strings_start..];
|
||||||
|
let strings = parse_null_strings(string_data);
|
||||||
|
|
||||||
|
// strings[0] = title, strings[1] = author, strings[2] = copyright
|
||||||
|
// strings[3..3+num_clues] = clue texts, rest = notes
|
||||||
|
if strings.len() < 3 + num_clues {
|
||||||
|
return Err(format!("expected {} clue strings, found {}", num_clues, strings.len().saturating_sub(3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let clue_strings: Vec<&str> = strings[3..3 + num_clues].iter().map(|s| s.as_str()).collect();
|
||||||
|
|
||||||
|
// number the cells and assign clues
|
||||||
|
let (clues_across, clues_down, words) = number_and_assign(
|
||||||
|
&grid, width, height, &clue_strings,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
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_null_strings(data: &[u8]) -> Vec<String> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
let mut start = 0;
|
||||||
|
for (i, &b) in data.iter().enumerate() {
|
||||||
|
if b == 0 {
|
||||||
|
let s = String::from_utf8_lossy(&data[start..i]).to_string();
|
||||||
|
result.push(s);
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn number_and_assign(
|
||||||
|
grid: &Grid,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
clue_strings: &[&str],
|
||||||
|
) -> Result<(Vec<Clue>, Vec<Clue>, Vec<PlacedWord>), String> {
|
||||||
|
let mut clues_across = Vec::new();
|
||||||
|
let mut clues_down = Vec::new();
|
||||||
|
let mut words = Vec::new();
|
||||||
|
let mut clue_idx = 0;
|
||||||
|
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 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let num = number;
|
||||||
|
number += 1;
|
||||||
|
|
||||||
|
if starts_across {
|
||||||
|
if clue_idx >= clue_strings.len() {
|
||||||
|
return Err("ran out of clue strings".into());
|
||||||
|
}
|
||||||
|
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: clue_strings[clue_idx].to_string(),
|
||||||
|
answer,
|
||||||
|
definition: None,
|
||||||
|
wordplay_type: None,
|
||||||
|
});
|
||||||
|
clue_idx += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if starts_down {
|
||||||
|
if clue_idx >= clue_strings.len() {
|
||||||
|
return Err("ran out of clue strings".into());
|
||||||
|
}
|
||||||
|
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: clue_strings[clue_idx].to_string(),
|
||||||
|
answer,
|
||||||
|
definition: None,
|
||||||
|
wordplay_type: None,
|
||||||
|
});
|
||||||
|
clue_idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((clues_across, clues_down, words))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user