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