1use smallvec::{smallvec, SmallVec};
5use std::collections::HashMap;
6use std::fmt::Debug;
7use std::sync::atomic::AtomicBool;
8use std::sync::Arc;
9
10#[cfg(feature = "serde")]
11use serde_derive::{Deserialize, Serialize};
12
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Deserializer, Serialize, Serializer};
15
16use crate::types::{GlyphId, WordId};
17use crate::util::build_glyph_counts_by_cell;
18use crate::word_list::WordList;
19use crate::{MAX_SLOT_COUNT, MAX_SLOT_LENGTH};
20
21pub type CrossingId = usize;
25
26pub type SlotId = usize;
28
29pub type GridCoord = (usize, usize);
31
32#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
36#[allow(dead_code)]
37pub enum Direction {
38 Across,
39 Down,
40}
41
42#[derive(Debug, Clone)]
45pub struct Crossing {
46 pub other_slot_id: SlotId,
47 pub other_slot_cell: usize,
48 pub crossing_id: CrossingId,
49}
50
51#[derive(Debug, Clone)]
53pub struct SlotConfig {
54 pub id: SlotId,
55 pub start_cell: GridCoord,
56 pub direction: Direction,
57 pub length: usize,
58 pub crossings: SmallVec<[Option<Crossing>; MAX_SLOT_LENGTH]>,
59}
60
61impl SlotConfig {
62 #[must_use]
64 pub fn cell_coords(&self) -> Vec<GridCoord> {
65 (0..self.length)
66 .map(|cell_idx| match self.direction {
67 Direction::Across => (self.start_cell.0 + cell_idx, self.start_cell.1),
68 Direction::Down => (self.start_cell.0, self.start_cell.1 + cell_idx),
69 })
70 .collect()
71 }
72
73 #[must_use]
75 pub fn cell_fill_indices(&self, grid_width: usize) -> Vec<usize> {
76 self.cell_coords()
77 .iter()
78 .map(|loc| loc.0 + loc.1 * grid_width)
79 .collect()
80 }
81
82 #[must_use]
84 pub fn fill(&self, fill: &[Option<GlyphId>], grid_width: usize) -> Vec<Option<GlyphId>> {
85 self.cell_fill_indices(grid_width)
86 .iter()
87 .map(|&idx| fill[idx])
88 .collect()
89 }
90
91 #[must_use]
93 pub fn complete_fill(
94 &self,
95 fill: &[Option<GlyphId>],
96 grid_width: usize,
97 ) -> Option<Vec<GlyphId>> {
98 self.fill(fill, grid_width).into_iter().collect()
99 }
100
101 #[must_use]
103 pub fn slot_spec(&self) -> SlotSpec {
104 SlotSpec {
105 start_cell: self.start_cell,
106 direction: self.direction,
107 length: self.length,
108 }
109 }
110
111 #[must_use]
113 pub fn slot_key(&self) -> String {
114 self.slot_spec().to_key()
115 }
116}
117
118#[allow(dead_code)]
121#[derive(Clone)]
122pub struct GridConfig<'a> {
123 pub word_list: &'a WordList,
125
126 pub fill: &'a [Option<GlyphId>],
129
130 pub slot_configs: &'a [SlotConfig],
132
133 pub slot_options: &'a [Vec<WordId>],
136
137 pub width: usize,
139 pub height: usize,
140
141 pub crossing_count: usize,
143
144 pub abort: Option<&'a AtomicBool>,
146}
147
148pub struct OwnedGridConfig {
150 pub word_list: WordList,
151 pub fill: Vec<Option<GlyphId>>,
152 pub slot_configs: SmallVec<[SlotConfig; MAX_SLOT_COUNT]>,
153 pub slot_options: SmallVec<[Vec<WordId>; MAX_SLOT_COUNT]>,
154 pub width: usize,
155 pub height: usize,
156 pub crossing_count: usize,
157 pub abort: Option<Arc<AtomicBool>>,
158}
159
160impl OwnedGridConfig {
161 #[allow(dead_code)]
162 #[must_use]
163 pub fn to_config_ref(&self) -> GridConfig {
164 GridConfig {
165 word_list: &self.word_list,
166 fill: &self.fill,
167 slot_configs: &self.slot_configs,
168 slot_options: &self.slot_options,
169 width: self.width,
170 height: self.height,
171 crossing_count: self.crossing_count,
172 abort: self.abort.as_deref(),
173 }
174 }
175}
176
177#[allow(clippy::cast_lossless)]
182pub fn sort_slot_options(
183 word_list: &WordList,
184 slot_configs: &[SlotConfig],
185 slot_options: &mut SmallVec<[Vec<WordId>; MAX_SLOT_COUNT]>,
186) {
187 let glyph_counts_by_cell_by_slot: Vec<_> = slot_configs
190 .iter()
191 .map(|slot_config| {
192 build_glyph_counts_by_cell(word_list, slot_config.length, &slot_options[slot_config.id])
193 })
194 .collect();
195
196 for slot_idx in 0..slot_configs.len() {
198 let slot_config = &slot_configs[slot_idx];
199 let slot_options = &mut slot_options[slot_idx];
200
201 slot_options.sort_by_cached_key(|&option| {
202 let word = &word_list.words[slot_config.length][option];
203
204 let fill_score = slot_config
210 .crossings
211 .iter()
212 .zip(&word.glyphs)
213 .map(|(crossing, &glyph)| match crossing {
214 Some(crossing) => {
215 let crossing_counts_by_cell =
216 &glyph_counts_by_cell_by_slot[crossing.other_slot_id];
217
218 (crossing_counts_by_cell[crossing.other_slot_cell][glyph] as f32).log10()
219 }
220 None => 0.0,
221 })
222 .fold(0.0, |a, b| a + b)
223 / (slot_config.length as f32);
224
225 -((fill_score * 900.0) as i64
229 + ((word.letter_score as f32) * 5.0) as i64
230 + ((word.score as f32) * 5.0) as i64)
231 });
232 }
233}
234
235#[derive(Debug, PartialEq, Eq, Hash, Clone)]
237pub struct SlotSpec {
238 pub start_cell: GridCoord,
239 pub direction: Direction,
240 pub length: usize,
241}
242
243impl SlotSpec {
244 pub fn from_key(key: &str) -> Result<SlotSpec, String> {
246 let key_parts: Vec<&str> = key.split(',').collect();
247 if key_parts.len() != 4 {
248 return Err(format!("invalid slot key: {key}"));
249 }
250
251 let x: Result<usize, _> = key_parts[0].parse();
252 let y: Result<usize, _> = key_parts[1].parse();
253 let direction: Option<Direction> = match key_parts[2] {
254 "across" => Some(Direction::Across),
255 "down" => Some(Direction::Down),
256 _ => None,
257 };
258 let length: Result<usize, _> = key_parts[3].parse();
259
260 if let (Ok(x), Ok(y), Some(direction), Ok(length)) = (x, y, direction, length) {
261 Ok(SlotSpec {
262 start_cell: (x, y),
263 direction,
264 length,
265 })
266 } else {
267 Err(format!("invalid slot key: {key:?}"))
268 }
269 }
270
271 #[must_use]
273 pub fn to_key(&self) -> String {
274 let direction = match self.direction {
275 Direction::Across => "across",
276 Direction::Down => "down",
277 };
278 format!(
279 "{},{},{},{}",
280 self.start_cell.0, self.start_cell.1, direction, self.length,
281 )
282 }
283
284 #[must_use]
286 pub fn matches_slot(&self, slot: &SlotConfig) -> bool {
287 self.start_cell == slot.start_cell
288 && self.direction == slot.direction
289 && self.length == slot.length
290 }
291
292 #[must_use]
294 pub fn cell_coords(&self) -> Vec<GridCoord> {
295 (0..self.length)
296 .map(|cell_idx| match self.direction {
297 Direction::Across => (self.start_cell.0 + cell_idx, self.start_cell.1),
298 Direction::Down => (self.start_cell.0, self.start_cell.1 + cell_idx),
299 })
300 .collect()
301 }
302}
303
304#[cfg(feature = "serde")]
306impl Serialize for SlotSpec {
307 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
308 where
309 S: Serializer,
310 {
311 serializer.serialize_str(&self.to_key())
312 }
313}
314
315#[cfg(feature = "serde")]
317impl<'de> Deserialize<'de> for SlotSpec {
318 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
319 where
320 D: Deserializer<'de>,
321 {
322 let raw_string = String::deserialize(deserializer)?;
323 SlotSpec::from_key(&raw_string).map_err(serde::de::Error::custom)
324 }
325}
326
327#[must_use]
330pub fn generate_slot_configs(
331 entries: &[SlotSpec],
332) -> (SmallVec<[SlotConfig; MAX_SLOT_COUNT]>, usize) {
333 #[derive(Debug)]
334 struct GridCell {
335 entries: Vec<(usize, usize)>, number: Option<u32>,
337 }
338
339 let mut slot_configs: SmallVec<[SlotConfig; MAX_SLOT_COUNT]> = smallvec![];
340
341 let mut cell_by_loc: HashMap<GridCoord, GridCell> = HashMap::new();
344
345 for (entry_idx, entry) in entries.iter().enumerate() {
346 for (cell_idx, &loc) in entry.cell_coords().iter().enumerate() {
347 let grid_cell = cell_by_loc.entry(loc).or_insert_with(|| GridCell {
348 entries: vec![],
349 number: None,
350 });
351 grid_cell.entries.push((entry_idx, cell_idx));
352 }
353 }
354
355 let mut ordered_coords: Vec<_> = cell_by_loc.keys().copied().collect();
356 ordered_coords.sort_by_key(|&(x, y)| (y, x));
357 let mut current_number = 1;
358 for coord in ordered_coords {
359 if cell_by_loc[&coord]
360 .entries
361 .iter()
362 .any(|&(_, cell_idx)| cell_idx == 0)
363 {
364 cell_by_loc.get_mut(&coord).unwrap().number = Some(current_number);
365 current_number += 1;
366 }
367 }
368
369 let mut constraint_id_cache: Vec<(SlotId, SlotId)> = vec![];
375
376 for (entry_idx, entry) in entries.iter().enumerate() {
378 let crossings: SmallVec<[Option<Crossing>; MAX_SLOT_LENGTH]> = entry
379 .cell_coords()
380 .iter()
381 .map(|&loc| {
382 let crossing_idxs: Vec<_> = cell_by_loc[&loc]
383 .entries
384 .iter()
385 .filter(|&&(e, _)| e != entry_idx)
386 .collect();
387
388 if crossing_idxs.is_empty() {
389 None
390 } else if crossing_idxs.len() > 1 {
391 panic!("More than two entries crossing in cell?");
392 } else {
393 let &(other_slot_id, other_slot_cell) = crossing_idxs[0];
394
395 let crossing_id = if let Some(found_constraint_id) = constraint_id_cache
396 .iter()
397 .enumerate()
398 .find(|&(_, &id_pair)| id_pair == (entry_idx, other_slot_id))
399 .map(|(crossing_id, _)| crossing_id)
400 {
401 found_constraint_id
402 } else {
403 constraint_id_cache.push((other_slot_id, entry_idx));
404 constraint_id_cache.len() - 1
405 };
406
407 Some(Crossing {
408 other_slot_id,
409 other_slot_cell,
410 crossing_id,
411 })
412 }
413 })
414 .collect();
415
416 slot_configs.push(SlotConfig {
417 id: entry_idx,
418 start_cell: entry.start_cell,
419 direction: entry.direction,
420 length: entry.length,
421 crossings,
422 });
423 }
424
425 (slot_configs, constraint_id_cache.len())
426}
427
428pub fn generate_slot_options(
432 word_list: &mut WordList,
433 fill: &[Option<GlyphId>],
434 slot_configs: &[SlotConfig],
435 grid_width: usize,
436 min_score: u16,
437) -> SmallVec<[Vec<WordId>; MAX_SLOT_COUNT]> {
438 let mut slot_options: SmallVec<[Vec<WordId>; MAX_SLOT_COUNT]> = smallvec![];
439
440 for slot in slot_configs {
441 let entry_fill = slot.fill(fill, grid_width);
442
443 let complete_fill: Option<SmallVec<[GlyphId; MAX_SLOT_COUNT]>> =
446 entry_fill.iter().copied().collect();
447
448 if let Some(complete_fill) = complete_fill {
449 let word_string: String = complete_fill
450 .iter()
451 .map(|&glyph_id| word_list.glyphs[glyph_id])
452 .collect();
453
454 let (_word_length, word_id) = word_list.get_word_id_or_add_hidden(&word_string);
455
456 slot_options.push(vec![word_id]);
457 } else {
458 let options: Vec<WordId> = (0..word_list.words[slot.length].len())
459 .filter(|&word_id| {
460 let word = &word_list.words[slot.length][word_id];
461 if word.hidden || word.score < min_score {
462 return false;
463 }
464
465 entry_fill.iter().enumerate().all(|(cell_idx, cell_fill)| {
466 cell_fill
467 .map(|g| g == word.glyphs[cell_idx])
468 .unwrap_or(true)
469 })
470 })
471 .collect();
472
473 slot_options.push(options);
474 }
475 }
476
477 slot_options
478}
479
480#[must_use]
482pub fn generate_grid_config<'a>(
483 mut word_list: WordList,
484 entries: &'a [SlotSpec],
485 raw_fill: &'a [Option<String>],
486 width: usize,
487 height: usize,
488 min_score: u16,
489) -> OwnedGridConfig {
490 let (slot_configs, crossing_count) = generate_slot_configs(entries);
491
492 let fill: Vec<Option<GlyphId>> = raw_fill
493 .iter()
494 .map(|cell_str| {
495 cell_str
496 .as_ref()
497 .map(|cell_str| word_list.glyph_id_for_char(cell_str.chars().next().unwrap()))
498 })
499 .collect();
500
501 let mut slot_options =
502 generate_slot_options(&mut word_list, &fill, &slot_configs, width, min_score);
503
504 sort_slot_options(&word_list, &slot_configs, &mut slot_options);
505
506 OwnedGridConfig {
507 word_list,
508 fill,
509 slot_configs,
510 slot_options,
511 width,
512 height,
513 crossing_count,
514 abort: None,
515 }
516}
517
518#[allow(dead_code)]
521#[must_use]
522pub fn generate_slots_from_template_string(template: &str) -> Vec<SlotSpec> {
523 fn build_words(template: &[Vec<char>]) -> Vec<Vec<GridCoord>> {
524 let mut result: Vec<Vec<GridCoord>> = vec![];
525
526 for (y, line) in template.iter().enumerate() {
527 let mut current_word_coords: Vec<GridCoord> = vec![];
528
529 for (x, &cell) in line.iter().enumerate() {
530 if cell == '#' {
531 if current_word_coords.len() > 1 {
532 result.push(current_word_coords);
533 }
534 current_word_coords = vec![];
535 } else {
536 current_word_coords.push((x, y));
537 }
538 }
539
540 if current_word_coords.len() > 1 {
541 result.push(current_word_coords);
542 }
543 }
544
545 result
546 }
547
548 let template: Vec<Vec<char>> = template
549 .lines()
550 .filter_map(|line| {
551 let line = line.trim();
552 if line.is_empty() {
553 None
554 } else {
555 Some(line.chars().collect())
556 }
557 })
558 .collect();
559
560 let mut slot_specs: Vec<SlotSpec> = vec![];
561
562 for coords in build_words(&template) {
563 slot_specs.push(SlotSpec {
564 start_cell: coords[0],
565 length: coords.len(),
566 direction: Direction::Across,
567 });
568 }
569
570 let transposed_template: Vec<Vec<char>> = (0..template[0].len())
571 .map(|y| (0..template.len()).map(|x| template[x][y]).collect())
572 .collect();
573
574 for coords in build_words(&transposed_template) {
575 let coords: Vec<GridCoord> = coords.iter().copied().map(|(y, x)| (x, y)).collect();
576 slot_specs.push(SlotSpec {
577 start_cell: coords[0],
578 length: coords.len(),
579 direction: Direction::Down,
580 });
581 }
582
583 slot_specs
584}
585
586#[allow(dead_code)]
589#[must_use]
590pub fn generate_grid_config_from_template_string(
591 word_list: WordList,
592 template: &str,
593 min_score: u16,
594) -> OwnedGridConfig {
595 let slot_specs = generate_slots_from_template_string(template);
596
597 let fill: Vec<Vec<Option<String>>> = template
598 .lines()
599 .filter_map(|line| {
600 let line = line.trim();
601 if line.is_empty() {
602 None
603 } else {
604 Some(
605 line.chars()
606 .map(|c| {
607 if c == '.' || c == '#' {
608 None
609 } else {
610 Some(c.to_lowercase().to_string())
611 }
612 })
613 .collect(),
614 )
615 }
616 })
617 .collect();
618
619 let width = fill[0].len();
620 let height = fill.len();
621
622 generate_grid_config(
623 word_list,
624 &slot_specs,
625 &fill.into_iter().flatten().collect::<Vec<_>>(),
626 width,
627 height,
628 min_score,
629 )
630}
631
632#[derive(Debug, Clone)]
634pub struct Choice {
635 pub slot_id: SlotId,
636 pub word_id: WordId,
637}
638
639#[allow(dead_code)]
641#[must_use]
642pub fn render_grid(config: &GridConfig, choices: &[Choice]) -> String {
643 let max_x = config
644 .slot_configs
645 .iter()
646 .map(|slot_config| match slot_config.direction {
647 Direction::Across => slot_config.start_cell.0 + slot_config.length - 1,
648 Direction::Down => slot_config.start_cell.0,
649 })
650 .max()
651 .expect("Grid must have slots");
652
653 let max_y = config
654 .slot_configs
655 .iter()
656 .map(|slot_config| match slot_config.direction {
657 Direction::Across => slot_config.start_cell.1,
658 Direction::Down => slot_config.start_cell.1 + slot_config.length - 1,
659 })
660 .max()
661 .expect("Grid must have slots");
662
663 let mut grid: Vec<Vec<Option<char>>> = (0..=max_y)
664 .map(|_| (0..=max_x).map(|_| None).collect::<Vec<_>>())
665 .collect();
666
667 for &Choice { slot_id, word_id } in choices {
668 let slot_config = &config.slot_configs[slot_id];
669 let word = &config.word_list.words[slot_config.length][word_id];
670
671 for (cell_idx, &glyph) in word.glyphs.iter().enumerate() {
672 let (x, y) = match slot_config.direction {
673 Direction::Across => (
674 slot_config.start_cell.0 + cell_idx,
675 slot_config.start_cell.1,
676 ),
677 Direction::Down => (
678 slot_config.start_cell.0,
679 slot_config.start_cell.1 + cell_idx,
680 ),
681 };
682
683 grid[y][x] = Some(config.word_list.glyphs[glyph]);
684 }
685 }
686
687 grid.iter()
688 .map(|line| {
689 line.iter()
690 .map(|cell| cell.unwrap_or('.').to_string())
691 .collect::<String>()
692 })
693 .collect::<Vec<_>>()
694 .join("\n")
695}
696
697#[cfg(all(test, feature = "serde"))]
698mod serde_tests {
699 use crate::grid_config::{Direction, SlotSpec};
700
701 #[test]
702 fn test_slot_spec_serialization() {
703 let slot_spec = SlotSpec {
704 start_cell: (1, 2),
705 direction: Direction::Across,
706 length: 5,
707 };
708
709 let slot_key = serde_json::to_string(&slot_spec).unwrap();
710
711 assert_eq!(slot_key, "\"1,2,across,5\"");
712 }
713
714 #[test]
715 fn test_slot_spec_deserialization() {
716 let slot_spec: SlotSpec = serde_json::from_str("\"3,4,down,12\"").unwrap();
717
718 assert_eq!(
719 slot_spec,
720 SlotSpec {
721 start_cell: (3, 4),
722 direction: Direction::Down,
723 length: 12,
724 }
725 );
726 }
727}