1use pencil_puzzles_core::{
3 Budget, Difficulty, Hint, Metadata, ParseError, Rating, Solutions, Violation,
4 text::{self, Document},
5};
6use std::error::Error;
7pub type RegistryError = Box<dyn Error + Send + Sync>;
9#[derive(Debug)]
11#[non_exhaustive]
12pub struct Generated {
13 pub text: String,
15 pub rating: Rating,
17 pub attempts: u32,
19}
20pub trait DynGenre: Sync {
23 fn text_limits(&self) -> text::GridLimits;
25
26 fn metadata(&self) -> &'static Metadata;
28 fn default_params(&self) -> String;
30 fn params_for_shape(&self, shape: &str) -> Result<String, RegistryError>;
32 fn parse_document(&self, document: Document) -> Result<String, RegistryError>;
34 fn read_id(&self, id: &str) -> Result<String, RegistryError>;
36 fn id(&self, text: &str) -> Result<String, RegistryError>;
38 fn generate(
40 &self,
41 params: &str,
42 difficulty: Option<Difficulty>,
43 seed: u64,
44 max_attempts: u32,
45 budget: &Budget,
46 ) -> Result<Generated, RegistryError>;
47 fn solve(&self, text: &str, budget: &Budget) -> Result<Solutions<String>, RegistryError>;
49 fn rate(&self, text: &str, budget: &Budget) -> Result<Rating, RegistryError>;
51 fn rate_with_steps(
53 &self,
54 text: &str,
55 budget: &Budget,
56 ) -> Result<(Rating, Vec<pencil_puzzles_core::Step>), RegistryError>;
57 fn check(
59 &self,
60 text: &str,
61 answer: Option<&str>,
62 finished: bool,
63 budget: &Budget,
64 ) -> Result<Vec<Violation>, RegistryError>;
65 fn hint(
67 &self,
68 text: &str,
69 answer: Option<&str>,
70 budget: &Budget,
71 ) -> Result<Hint, RegistryError>;
72 fn with_solution(&self, text: &str, answer: Option<&str>) -> Result<String, RegistryError>;
74 fn solution(&self, text: &str) -> Result<Option<String>, RegistryError>;
76 fn with_rating(&self, text: &str, rating: &Rating) -> Result<String, RegistryError>;
78 fn editor(
80 &self,
81 shape: &str,
82 puzzle: Option<&str>,
83 ) -> Result<pencil_puzzles_core::PuzzleEditor, RegistryError>;
84 fn puzzle_from_editor(&self, shape: &str, values: &[String]) -> Result<String, RegistryError>;
86 fn view(
88 &self,
89 text: &str,
90 answer: Option<&str>,
91 ) -> Result<pencil_puzzles_core::View, RegistryError>;
92 fn answer_from_values(&self, text: &str, values: &[String]) -> Result<String, RegistryError>;
94 fn render(&self, text: &str, answer: Option<&str>) -> Result<String, RegistryError>;
96 #[cfg(feature = "serde")]
98 fn to_json(
99 &self,
100 text: &str,
101 rating: Option<&Rating>,
102 ) -> Result<serde_json::Value, RegistryError>;
103 #[cfg(feature = "serde")]
105 fn read_json(&self, value: serde_json::Value) -> Result<Input, RegistryError>;
106}
107pub struct Input {
109 pub genre: &'static dyn DynGenre,
111 pub text: String,
113 pub rating: Option<Rating>,
115}
116#[cfg(any(
119 feature = "fillomino",
120 feature = "hashi",
121 feature = "hitori",
122 feature = "kakurasu",
123 feature = "kakuro",
124 feature = "sudoku",
125 feature = "nurikabe"
126))]
127mod typed {
128 use super::*;
129 use pencil_puzzles_core::{Genre, Record};
130 use std::marker::PhantomData;
131 fn one<G: Genre>(source: &str) -> Result<Record<G::Puzzle, G::Answer>, RegistryError> {
132 let mut docs = text::parse_with_limits(source, G::text_limits())?;
133 if docs.len() != 1 {
134 return Err(ParseError::new("expected exactly one puzzle").into());
135 }
136 let mut doc = docs.remove(0);
137 if doc
138 .genre()
139 .is_some_and(|id| G::metadata().aliases.contains(&id))
140 {
141 doc.set_genre(G::metadata().id);
142 }
143 Ok(G::parse_document(doc)?)
144 }
145 fn answer<G: Genre>(
146 record: &Record<G::Puzzle, G::Answer>,
147 source: Option<&str>,
148 ) -> Result<G::Answer, RegistryError> {
149 match source {
150 Some(source) => Ok(G::parse_answer(&record.puzzle, source)?),
151 None => record.solution.clone().ok_or_else(|| {
152 ParseError::new("provide an answer file or a solution block").into()
153 }),
154 }
155 }
156 #[cfg(feature = "serde")]
157 type GenreRecord<G> = Record<<G as Genre>::Puzzle, <G as Genre>::Answer>;
158 #[cfg(feature = "serde")]
159 type Decoded<G> = (GenreRecord<G>, Option<Rating>);
160
161 trait WireGenre: Genre {
162 #[cfg(feature = "serde")]
163 fn encode(
164 record: &Record<Self::Puzzle, Self::Answer>,
165 rating: Option<&Rating>,
166 ) -> Result<serde_json::Value, RegistryError>;
167 #[cfg(feature = "serde")]
168 fn decode(value: serde_json::Value) -> Result<Decoded<Self>, RegistryError>;
169 }
170 #[cfg(not(feature = "serde"))]
171 impl<G: Genre> WireGenre for G {}
172 #[cfg(feature = "serde")]
173 #[derive(serde::Serialize, serde::Deserialize)]
174 #[serde(
175 deny_unknown_fields,
176 bound(deserialize = "P: serde::Deserialize<'de>, A: serde::Deserialize<'de>")
177 )]
178 struct Envelope<P, A> {
179 genre: String,
180 puzzle: P,
181 #[serde(skip_serializing_if = "Option::is_none", default)]
182 solution: Option<A>,
183 #[serde(skip_serializing_if = "Option::is_none", default)]
184 rating: Option<Rating>,
185 #[serde(skip_serializing_if = "Option::is_none", default)]
186 seed: Option<String>,
187 #[serde(skip_serializing_if = "Option::is_none", default)]
188 generator_version: Option<u32>,
189 #[serde(skip_serializing_if = "Option::is_none", default)]
190 params: Option<String>,
191 #[serde(skip_serializing_if = "Option::is_none", default)]
192 difficulty: Option<Difficulty>,
193 }
194 #[cfg(feature = "serde")]
195 impl<G> WireGenre for G
196 where
197 G: Genre,
198 G::Puzzle: serde::Serialize + serde::de::DeserializeOwned,
199 G::Answer: serde::Serialize + serde::de::DeserializeOwned,
200 {
201 fn encode(
202 record: &Record<Self::Puzzle, Self::Answer>,
203 rating: Option<&Rating>,
204 ) -> Result<serde_json::Value, RegistryError> {
205 Ok(serde_json::to_value(Envelope {
206 genre: G::metadata().id.into(),
207 puzzle: &record.puzzle,
208 solution: record.solution.as_ref(),
209 rating: rating.cloned(),
210 seed: record.shared.seed.map(|s| s.to_string()),
211 generator_version: record.shared.generator_version,
212 params: record.shared.params.clone(),
213 difficulty: record.shared.difficulty,
214 })?)
215 }
216 fn decode(value: serde_json::Value) -> Result<Decoded<Self>, RegistryError> {
217 let envelope: Envelope<G::Puzzle, G::Answer> = serde_json::from_value(value)?;
218 let m = G::metadata();
219 if envelope.genre != m.id && !m.aliases.contains(&envelope.genre.as_str()) {
220 return Err(ParseError::new("the envelope names a different genre").into());
221 }
222 let mut record = Record::new(envelope.puzzle);
223 record.solution = envelope.solution;
224 record.shared.seed = envelope.seed.map(|s| s.parse::<u64>()).transpose()?;
225 record.shared.generator_version = envelope.generator_version;
226 record.shared.params = envelope.params;
227 record.shared.difficulty = envelope.difficulty;
228 if let Some(r) = &envelope.rating {
229 record.shared.rating = Some((r.level, r.score));
230 record.shared.rater_version = Some(r.rater_version);
231 }
232 let record = one::<G>(&G::write_record(&record))?;
234 Ok((record, envelope.rating))
235 }
236 }
237 pub(super) struct Adapter<G>(PhantomData<fn() -> G>);
238 impl<G: WireGenre> DynGenre for Adapter<G> {
239 fn metadata(&self) -> &'static Metadata {
240 G::metadata()
241 }
242 fn text_limits(&self) -> text::GridLimits {
243 G::text_limits()
244 }
245 fn default_params(&self) -> String {
246 G::Params::default().to_string()
247 }
248 fn params_for_shape(&self, shape: &str) -> Result<String, RegistryError> {
249 Ok(G::params_for_shape(shape.parse()?).to_string())
250 }
251 fn parse_document(&self, mut document: Document) -> Result<String, RegistryError> {
252 if let Some(id) = document.genre()
253 && id != G::metadata().id
254 && !G::metadata().aliases.contains(&id)
255 {
256 return Err(ParseError::at_line(
257 document.start_line(),
258 "the document names a different genre",
259 )
260 .into());
261 }
262 document.set_genre(G::metadata().id);
263 Ok(G::write_record(&G::parse_document(document)?))
264 }
265 fn read_id(&self, id: &str) -> Result<String, RegistryError> {
266 let (name, shape, description) = pencil_puzzles_core::id::split(id)?;
267 if name != G::metadata().id && !G::metadata().aliases.contains(&name) {
268 return Err(ParseError::at_char(1, "the ID names a different genre").into());
269 }
270 let canonical = pencil_puzzles_core::id::join(G::metadata().id, shape, description);
271 Ok(G::write_record(&Record::new(G::from_id(&canonical)?)))
272 }
273 fn id(&self, text: &str) -> Result<String, RegistryError> {
274 Ok(G::id(&one::<G>(text)?.puzzle))
275 }
276 fn generate(
277 &self,
278 params: &str,
279 difficulty: Option<Difficulty>,
280 seed: u64,
281 max_attempts: u32,
282 budget: &Budget,
283 ) -> Result<Generated, RegistryError> {
284 let g = G::generate(¶ms.parse()?, difficulty, seed, max_attempts, budget)?;
285 Ok(Generated {
286 text: G::write_record(&g.record),
287 rating: g.rating,
288 attempts: g.attempts,
289 })
290 }
291 fn solve(&self, text: &str, budget: &Budget) -> Result<Solutions<String>, RegistryError> {
292 Ok(G::solve(&one::<G>(text)?.puzzle, budget)?.map(|a| a.to_string()))
293 }
294 fn rate(&self, text: &str, budget: &Budget) -> Result<Rating, RegistryError> {
295 Ok(G::rate(&one::<G>(text)?.puzzle, budget)?)
296 }
297 fn rate_with_steps(
298 &self,
299 text: &str,
300 budget: &Budget,
301 ) -> Result<(Rating, Vec<pencil_puzzles_core::Step>), RegistryError> {
302 Ok(G::rate_with_steps(&one::<G>(text)?.puzzle, budget)?)
303 }
304 fn check(
305 &self,
306 text: &str,
307 source: Option<&str>,
308 finished: bool,
309 budget: &Budget,
310 ) -> Result<Vec<Violation>, RegistryError> {
311 let r = one::<G>(text)?;
312 let a = answer::<G>(&r, source)?;
313 let a = if finished { G::finished(&a) } else { a };
314 G::check(&r.puzzle, &a, budget)
315 }
316 fn hint(
317 &self,
318 text: &str,
319 source: Option<&str>,
320 budget: &Budget,
321 ) -> Result<Hint, RegistryError> {
322 let r = one::<G>(text)?;
323 let a = if source.is_none() && r.solution.is_none() {
324 G::empty_answer(&r.puzzle)
325 } else {
326 answer::<G>(&r, source)?
327 };
328 Ok(G::hint(&r.puzzle, &a, budget)?)
329 }
330 fn with_solution(&self, text: &str, source: Option<&str>) -> Result<String, RegistryError> {
331 let mut r = one::<G>(text)?;
332 r.solution = source.map(|s| G::parse_answer(&r.puzzle, s)).transpose()?;
333 let text = G::write_record(&r);
334 let _ = one::<G>(&text)?;
335 Ok(text)
336 }
337 fn solution(&self, text: &str) -> Result<Option<String>, RegistryError> {
338 Ok(one::<G>(text)?.solution.map(|a| a.to_string()))
339 }
340 fn with_rating(&self, text: &str, rating: &Rating) -> Result<String, RegistryError> {
341 let mut r = one::<G>(text)?;
342 r.shared.rating = Some((rating.level, rating.score));
343 r.shared.rater_version = Some(rating.rater_version);
344 Ok(G::write_record(&r))
345 }
346 fn editor(
347 &self,
348 shape: &str,
349 puzzle: Option<&str>,
350 ) -> Result<pencil_puzzles_core::PuzzleEditor, RegistryError> {
351 let p = puzzle.map(one::<G>).transpose()?;
352 Ok(G::editor(shape.parse()?, p.as_ref().map(|r| &r.puzzle)))
353 }
354 fn puzzle_from_editor(
355 &self,
356 shape: &str,
357 values: &[String],
358 ) -> Result<String, RegistryError> {
359 Ok(G::write_record(&Record::new(G::puzzle_from_editor(
360 shape.parse()?,
361 values,
362 )?)))
363 }
364 fn view(
365 &self,
366 text: &str,
367 source: Option<&str>,
368 ) -> Result<pencil_puzzles_core::View, RegistryError> {
369 let r = one::<G>(text)?;
370 let answer = source.map(|s| G::parse_answer(&r.puzzle, s)).transpose()?;
371 Ok(G::view(&r.puzzle, answer.as_ref()))
372 }
373 fn answer_from_values(
374 &self,
375 text: &str,
376 values: &[String],
377 ) -> Result<String, RegistryError> {
378 let r = one::<G>(text)?;
379 Ok(G::answer_from_values(&r.puzzle, values)?.to_string())
380 }
381 fn render(&self, text: &str, source: Option<&str>) -> Result<String, RegistryError> {
382 let r = one::<G>(text)?;
383 let a = source.map(|s| G::parse_answer(&r.puzzle, s)).transpose()?;
384 Ok(G::render(&r.puzzle, a.as_ref()))
385 }
386 #[cfg(feature = "serde")]
387 fn to_json(
388 &self,
389 text: &str,
390 rating: Option<&Rating>,
391 ) -> Result<serde_json::Value, RegistryError> {
392 G::encode(&one::<G>(text)?, rating)
393 }
394 #[cfg(feature = "serde")]
395 fn read_json(&self, value: serde_json::Value) -> Result<Input, RegistryError> {
396 let (r, rating) = G::decode(value)?;
397 Ok(Input {
398 genre: find(G::metadata().id)
399 .ok_or_else(|| ParseError::new("genre is not registered"))?,
400 text: G::write_record(&r),
401 rating,
402 })
403 }
404 }
405 #[cfg(feature = "fillomino")]
406 pub(super) static FILLOMINO: Adapter<crate::fillomino::Fillomino> = Adapter(PhantomData);
407 #[cfg(feature = "hashi")]
408 pub(super) static HASHI: Adapter<crate::hashi::Hashi> = Adapter(PhantomData);
409 #[cfg(feature = "hitori")]
410 pub(super) static HITORI: Adapter<crate::hitori::Hitori> = Adapter(PhantomData);
411 #[cfg(feature = "kakurasu")]
412 pub(super) static KAKURASU: Adapter<crate::kakurasu::Kakurasu> = Adapter(PhantomData);
413 #[cfg(feature = "kakuro")]
414 pub(super) static KAKURO: Adapter<crate::kakuro::Kakuro> = Adapter(PhantomData);
415 #[cfg(feature = "nurikabe")]
416 pub(super) static NURIKABE: Adapter<crate::nurikabe::Nurikabe> = Adapter(PhantomData);
417 #[cfg(feature = "sudoku")]
418 pub(super) static SUDOKU: Adapter<crate::sudoku::Sudoku> = Adapter(PhantomData);
419}
420static GENRES: &[&dyn DynGenre] = &[
422 #[cfg(feature = "fillomino")]
423 &typed::FILLOMINO,
424 #[cfg(feature = "hashi")]
425 &typed::HASHI,
426 #[cfg(feature = "hitori")]
427 &typed::HITORI,
428 #[cfg(feature = "kakurasu")]
429 &typed::KAKURASU,
430 #[cfg(feature = "kakuro")]
431 &typed::KAKURO,
432 #[cfg(feature = "nurikabe")]
433 &typed::NURIKABE,
434 #[cfg(feature = "sudoku")]
435 &typed::SUDOKU,
436];
437pub fn genres() -> &'static [&'static dyn DynGenre] {
439 GENRES
440}
441pub fn find(name: &str) -> Option<&'static dyn DynGenre> {
443 genres()
444 .iter()
445 .copied()
446 .find(|g| g.metadata().id == name || g.metadata().aliases.contains(&name))
447}
448pub fn read(source: &str) -> Result<Vec<Input>, RegistryError> {
451 let trimmed = source.trim();
452 #[cfg(feature = "serde")]
453 if trimmed.starts_with('{') || trimmed.starts_with('[') {
454 let value: serde_json::Value = serde_json::from_str(trimmed)?;
455 let values = match value {
456 serde_json::Value::Array(values) => values,
457 value => vec![value],
458 };
459 return values
460 .into_iter()
461 .map(|value| {
462 let id = value
463 .get("genre")
464 .and_then(|v| v.as_str())
465 .ok_or_else(|| ParseError::new("the JSON envelope needs a genre"))?;
466 let g = find(id).ok_or_else(|| ParseError::new(format!("unknown genre {id:?}")))?;
467 g.read_json(value)
468 })
469 .collect();
470 }
471 if !trimmed.contains('\n') && trimmed.matches(':').count() >= 2 {
472 let (id, _, _) = pencil_puzzles_core::id::split(trimmed)?;
473 let genre = find(id).ok_or_else(|| ParseError::new(format!("unknown genre {id:?}")))?;
474 return Ok(vec![Input {
475 genre,
476 text: genre.read_id(trimmed)?,
477 rating: None,
478 }]);
479 }
480 let docs = text::parse_with_genre_limits(source, |id| {
481 id.and_then(find)
482 .map_or_else(text::GridLimits::default, |g| g.text_limits())
483 })?;
484 if docs.is_empty() {
485 return Err(ParseError::new("there is no puzzle in the input").into());
486 }
487 docs.into_iter()
488 .map(|d| {
489 let id = d.genre().ok_or_else(|| {
490 ParseError::at_line(d.start_line(), "each puzzle needs a genre: line")
491 })?;
492 let genre = find(id).ok_or_else(|| {
493 ParseError::at_line(d.start_line(), format!("unknown genre {id:?}"))
494 })?;
495 Ok(Input {
496 genre,
497 text: genre.parse_document(d)?,
498 rating: None,
499 })
500 })
501 .collect()
502}