puz import

This commit is contained in:
2025-06-16 09:48:34 +03:00
parent 8b32d577e1
commit 5af6fb764e
+192
View File
@@ -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
}