crossword_puzzle/lib.rs
1//! This crate provides functionality for generating crossword puzzles.
2//! It includes data structures for representing words, grid, and algorithms
3//! for placing words and solving the puzzle.
4
5use std::collections::VecDeque;
6use std::fmt::Debug;
7
8use crate::error::{Error, GridError, WordError};
9use crate::word::{Direction, Position, Word};
10
11pub mod error;
12pub mod word;
13
14/// `Neighbor` represents the characters and their positions in the cells immediately adjacent to a given position on the crossword grid.
15/// It is used internally to check for conflicts or valid placements when adding words.
16#[derive(Debug, Default)]
17pub struct Neighbor {
18 /// The character and `Position` of the cell directly above the current position.
19 /// `None` if the cell is out of bounds or empty.
20 pub up: Option<(Position, char)>,
21 /// The character and `Position` of the cell directly to the right of the current position.
22 /// `None` if the cell is out of bounds or empty.
23 pub right: Option<(Position, char)>,
24 /// The character and `Position` of the cell directly below the current position.
25 /// `None` if the cell is out of bounds or empty.
26 pub down: Option<(Position, char)>,
27 /// The character and `Position` of the cell directly to the left of the current position.
28 /// `None` if the cell is out of bounds or empty.
29 pub left: Option<(Position, char)>,
30}
31
32/// `Grid` represents the crossword puzzle board and manages the placement and validation of words.
33/// It dynamically resizes to accommodate words and provides methods for adding words and finding valid placements.
34#[derive(Clone, Debug)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize))]
36pub struct Grid<'a> {
37 /// A collection of `Word`s that have been successfully placed on the grid.
38 pub words: Vec<Word<'a>>,
39 /// The 2D vector of characters representing the crossword board itself.
40 /// Empty cells are typically represented by a space character (' ').
41 pub board: Vec<Vec<char>>,
42}
43
44impl<'a> Default for Grid<'a> {
45 /// Creates a new empty `Grid`.
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51/// Type alias for a function that calculates the (x, y) position of a character within a word.
52/// It takes a reference to a `Word` and an index, returning the `(x, y)` coordinates.
53type GetPosFn<'a> = Box<dyn Fn(&Word<'a>, usize) -> (usize, usize)>;
54/// Type alias for a function that extracts the origin coordinate (x or y) of a word.
55/// It takes a reference to a `Word` and returns its origin coordinate as a `usize`.
56type GetOriginFn<'a> = Box<dyn Fn(&Word<'a>) -> usize>;
57/// Type alias representing a tuple used to help with word placement.
58/// It contains the `Direction` of placement, a `GetPosFn` for calculating character positions,
59/// and a `GetOriginFn` for determining the word's origin.
60type PlacementHelper<'a> = (Direction, GetPosFn<'a>, GetOriginFn<'a>);
61
62impl<'a> Grid<'a> {
63 /// Creates a new, empty `Grid` instance.
64 /// The grid is initialized with a single empty cell (`' '`).
65 ///
66 /// # Returns
67 ///
68 /// A new `Grid` with an empty `words` vector and a `board` containing one empty cell.
69 ///
70 /// # Examples
71 ///
72 /// ```
73 /// use crossword_puzzle::Grid;
74 ///
75 /// let grid = Grid::new();
76 /// assert!(grid.words.is_empty());
77 /// assert_eq!(grid.board, vec![vec![' ']]);
78 /// ```
79 pub fn new() -> Self {
80 Self {
81 words: vec![],
82 board: vec![vec![' ']],
83 }
84 }
85
86 /// Adds a `Word` to the grid.
87 ///
88 /// This function first ensures the grid is large enough to accommodate the new word,
89 /// then updates the word's internal position, fills the corresponding cells on the board
90 /// with the word's characters, and finally adds the word to the grid's list of placed words.
91 ///
92 /// # Arguments
93 ///
94 /// * `word` - A mutable `Word` instance to be added to the grid.
95 ///
96 /// # Returns
97 ///
98 /// - `Ok(())` if the word was successfully added.
99 /// - `Err(GridError)` if there was an issue with resizing the grid or filling the word.
100 ///
101 /// # Errors
102 ///
103 /// Returns a `GridError` if:
104 /// - The grid cannot be resized to fit the word.
105 /// - The word's direction is `NotSet` during filling.
106 ///
107 /// # Examples
108 ///
109 /// ```
110 /// use crossword_puzzle::{Grid, word::{Word, Direction}};
111 ///
112 /// let mut grid = Grid::new();
113 /// let word = Word::value("", 'R', "UST").unwrap().direction(Direction::Horizontal);
114 /// assert!(grid.add_word(word).is_ok());
115 /// ```
116 pub fn add_word(&mut self, mut word: Word<'a>) -> Result<(), GridError> {
117 self.ensure_grid_size(&mut word)?;
118 word.update_position();
119 self.fill_word(&word)?;
120
121 self.words.push(word);
122
123 for word in self.words.iter_mut() {
124 word.update_position();
125 }
126
127 Ok(())
128 }
129
130 /// Recursively resizes the grid by adding empty cells in the specified direction.
131 ///
132 /// This function expands the grid by `amount` in the given `direction`.
133 /// If `is_prepend` is `true`, cells are added at the beginning (top or left);
134 /// otherwise, they are added at the end (bottom or right).
135 /// Word positions are adjusted accordingly if cells are prepended.
136 ///
137 /// # Arguments
138 ///
139 /// * `amount` - The number of cells to add.
140 /// * `direction` - The `Direction` in which to resize the grid (Horizontal or Vertical).
141 /// * `is_prepend` - A boolean indicating whether to add cells at the beginning (`true`) or end (`false`).
142 ///
143 /// # Returns
144 ///
145 /// - `Ok(())` if the grid was successfully resized.
146 /// - `Err(GridError)` if an invalid direction is provided.
147 ///
148 /// # Errors
149 ///
150 /// Returns a `GridError::InvalidDirection` if `Direction::NotSet` is provided.
151 ///
152 /// # Examples
153 ///
154 /// ```
155 /// use crossword_puzzle::{Grid, word::Direction};
156 ///
157 /// let mut grid = Grid::new();
158 /// // Resize horizontally by 5 cells, prepending them
159 /// assert!(grid.resize_grid(5, Direction::Horizontal, true).is_ok());
160 /// // Resize vertically by 3 cells, appending them
161 /// assert!(grid.resize_grid(3, Direction::Vertical, false).is_ok());
162 /// ```
163 pub fn resize_grid(
164 &mut self,
165 amount: usize,
166 direction: Direction,
167 is_prepend: bool,
168 ) -> Result<(), GridError> {
169 if amount == 0 {
170 return Ok(());
171 }
172
173 match direction {
174 Direction::Horizontal => {
175 for row in self.board.iter_mut() {
176 if is_prepend {
177 row.insert(0, ' ');
178 } else {
179 row.push(' ');
180 }
181 }
182
183 if is_prepend {
184 for word in self.words.iter_mut() {
185 word.position.x += 1;
186 }
187 }
188 }
189 Direction::Vertical => {
190 let length = self.board[0].len();
191 if is_prepend {
192 self.board.insert(0, [' '].repeat(length));
193 } else {
194 self.board.push([' '].repeat(length));
195 }
196 if is_prepend {
197 for word in self.words.iter_mut() {
198 word.position.y += 1;
199 }
200 }
201 }
202 Direction::NotSet => {
203 return Err(GridError::InvalidDirection(
204 "Invalid direction for grid resize.".to_string(),
205 ))
206 }
207 }
208
209 self.resize_grid(amount - 1, direction, is_prepend)
210 }
211
212 /// Ensures the grid is large enough to accommodate a given `Word`.
213 ///
214 /// This function checks if the word, including its prefix and suffix, would extend
215 /// beyond the current grid boundaries. If necessary, it resizes the grid by calling
216 /// `resize_grid` and adjusts the word's position to reflect the new grid dimensions.
217 ///
218 /// # Arguments
219 ///
220 /// * `word` - A mutable reference to the `Word` to be placed.
221 ///
222 /// # Returns
223 ///
224 /// - `Ok(())` if the grid is successfully ensured to be large enough.
225 /// - `Err(GridError)` if there's an issue during grid resizing.
226 ///
227 /// # Errors
228 ///
229 /// Returns a `GridError` if `resize_grid` encounters an error (e.g., `InvalidDirection`).
230 ///
231 /// # Examples
232 ///
233 /// ```
234 /// use crossword_puzzle::{Grid, word::{Word, Direction}};
235 ///
236 /// let mut grid = Grid::new();
237 /// let mut word = Word::value("", 'A', "BC").unwrap().position(0, 0).direction(Direction::Horizontal);
238 /// // Initially, the grid is 1x1. This will cause it to resize.
239 /// assert!(grid.ensure_grid_size(&mut word).is_ok());
240 /// ```
241 pub fn ensure_grid_size(&mut self, word: &mut Word<'a>) -> Result<(), GridError> {
242 let position = &mut word.position;
243 let segment = &word.segment;
244
245 let prefix_pos = if word.direction == Direction::Horizontal {
246 position.x as isize - segment.prefix.len() as isize
247 } else {
248 position.y as isize - segment.prefix.len() as isize
249 };
250
251 if prefix_pos < 0 {
252 let abs_prefix_pos = prefix_pos.unsigned_abs();
253 self.resize_grid(abs_prefix_pos, word.direction, true)?;
254
255 if word.direction == Direction::Horizontal {
256 position.x += abs_prefix_pos;
257 } else {
258 position.y += abs_prefix_pos;
259 }
260 }
261
262 let suffix_pos = if word.direction == Direction::Horizontal {
263 let length = position.x.saturating_add(segment.suffix.len()) + 1;
264 self.board[0].len() as isize - length as isize
265 } else {
266 let length = position.y.saturating_add(segment.suffix.len()) + 1;
267 self.board.len() as isize - length as isize
268 };
269 if suffix_pos < 0 {
270 self.resize_grid(suffix_pos.unsigned_abs(), word.direction, false)?;
271 }
272
273 Ok(())
274 }
275
276 /// Fills the grid with the characters of a given `Word`.
277 ///
278 /// This function iterates through the characters of the word and places them onto the `board`
279 /// at the word's calculated `position` and `direction`.
280 ///
281 /// # Arguments
282 ///
283 /// * `word` - A reference to the `Word` whose characters will fill the grid.
284 ///
285 /// # Returns
286 ///
287 /// - `Ok(())` if the word was successfully filled onto the grid.
288 /// - `Err(GridError)` if an invalid direction is provided.
289 ///
290 /// # Errors
291 ///
292 /// Returns a `GridError::InvalidDirection` if `Direction::NotSet` is provided.
293 ///
294 /// # Examples
295 ///
296 /// ```
297 /// use crossword_puzzle::{Grid, word::{Word, Direction}};
298 ///
299 /// let mut grid = Grid::new();
300 /// let word = Word::value("", 'T', "EST").unwrap().position(0, 0).direction(Direction::Horizontal);
301 /// assert!(grid.add_word(word).is_ok());
302 /// ```
303 pub fn fill_word(&mut self, word: &Word<'a>) -> Result<(), GridError> {
304 match word.direction {
305 Direction::Horizontal => {
306 for (ch, index) in word.segment.full_word().iter().zip(word.origin.x..) {
307 self.board[word.position.y][index] = *ch;
308 }
309 }
310 Direction::Vertical => {
311 for (ch, index) in word.segment.full_word().iter().zip(word.origin.y..) {
312 self.board[index][word.position.x] = *ch;
313 }
314 }
315 Direction::NotSet => {
316 return Err(GridError::InvalidDirection(
317 "Invalid direction for filling word.".to_string(),
318 ))
319 }
320 }
321 Ok(())
322 }
323
324 /*
325 * VALIDATOR
326 */
327
328 /// Returns the character at the given `Position` on the grid, if it exists.
329 ///
330 /// This function safely retrieves a character from the `board` at the specified `Position`,
331 /// returning `None` if the position is out of bounds.
332 ///
333 /// # Arguments
334 ///
335 /// * `position` - The `Position` (x, y) on the grid to retrieve the character from.
336 ///
337 /// # Returns
338 ///
339 /// - `Some(char)` if a character exists at the given position.
340 /// - `None` if the position is out of bounds.
341 ///
342 /// # Examples
343 ///
344 /// ```
345 /// use crossword_puzzle::{Grid, word::{Word, Direction, Position}};
346 ///
347 /// let mut grid = Grid::new();
348 /// let word = Word::value("", 'A', "").unwrap().position(0, 0).direction(Direction::Horizontal);
349 /// grid.add_word(word).unwrap();
350 ///
351 /// assert_eq!(grid.get_char(Position { x: 0, y: 0 }), Some('A'));
352 /// assert_eq!(grid.get_char(Position { x: 1, y: 0 }), None);
353 /// ```
354 pub fn get_char(&self, position: Position) -> Option<char> {
355 self.board
356 .get(position.y)
357 .and_then(|col| col.get(position.x))
358 .copied()
359 }
360
361 /// Helper function to get a neighbor at a given offset from the current position.
362 ///
363 /// Returns `Some((Position, char))` if the neighbor exists and is within bounds,
364 /// otherwise `None`.
365 ///
366 /// # Arguments
367 ///
368 /// * `current_pos` - The starting `Position`.
369 /// * `dx` - The x-offset (change in column).
370 /// * `dy` - The y-offset (change in row).
371 ///
372 /// # Returns
373 ///
374 /// - `Some((Position, char))` if the neighbor exists and is within bounds.
375 /// - `None` if the new position is out of bounds.
376 ///
377 /// # Examples
378 ///
379 /// ```
380 /// use crossword_puzzle::{Grid, word::{Word, Direction, Position}};
381 ///
382 /// let mut grid = Grid::new();
383 /// let word = Word::value("", 'A', "").unwrap().position(0, 0).direction(Direction::Horizontal);
384 /// grid.add_word(word).unwrap();
385 ///
386 /// // Get the character at (0,0) (the current position)
387 /// assert_eq!(grid.get_neighbor_at_offset(Position { x: 0, y: 0 }, 0, 0), Some((Position { x: 0, y: 0 }, 'A')));
388 /// // Try to get a neighbor out of bounds
389 /// assert_eq!(grid.get_neighbor_at_offset(Position { x: 0, y: 0 }, -1, 0), None);
390 /// ```
391 pub fn get_neighbor_at_offset(
392 &self,
393 current_pos: Position,
394 dx: isize,
395 dy: isize,
396 ) -> Option<(Position, char)> {
397 let new_x = current_pos.x as isize + dx;
398 let new_y = current_pos.y as isize + dy;
399
400 if new_x >= 0 && new_y >= 0 {
401 let new_pos = Position {
402 x: new_x as usize,
403 y: new_y as usize,
404 };
405 self.get_char(new_pos).map(|ch| (new_pos, ch))
406 } else {
407 None
408 }
409 }
410
411 /// Returns the `Neighbor` struct for a given `Position` on the grid.
412 ///
413 /// This function checks the cells immediately above, right, below, and left of the given `position`.
414 /// It returns a `Neighbor` struct containing the character and position of each existing neighbor.
415 ///
416 /// # Arguments
417 ///
418 /// * `position` - The `Position` for which to find neighbors.
419 ///
420 /// # Returns
421 ///
422 /// A `Neighbor` struct populated with the characters and positions of adjacent cells.
423 ///
424 /// # Examples
425 ///
426 /// ```
427 /// use crossword_puzzle::{Grid, word::{Word, Direction, Position}, Neighbor};
428 ///
429 /// let mut grid = Grid::new();
430 /// let word = Word::value("", 'A', "").unwrap().position(0, 0).direction(Direction::Horizontal);
431 /// grid.add_word(word).unwrap();
432 ///
433 /// let neighbors = grid.get_neighbor(Position { x: 0, y: 0 });
434 /// assert!(neighbors.up.is_none());
435 /// assert!(neighbors.right.is_none());
436 /// assert!(neighbors.down.is_none());
437 /// assert!(neighbors.left.is_none());
438 /// ```
439 pub fn get_neighbor(&self, position: Position) -> Neighbor {
440 Neighbor {
441 up: self.get_neighbor_at_offset(position, 0, -1),
442 right: self.get_neighbor_at_offset(position, 1, 0),
443 down: self.get_neighbor_at_offset(position, 0, 1),
444 left: self.get_neighbor_at_offset(position, -1, 0),
445 }
446 }
447
448 /// Calculates the next `Position` based on the current position, direction, and step.
449 ///
450 /// This helper function determines the new `Position` by moving `step` units from `current_pos`
451 /// in the specified `direction` (Horizontal or Vertical).
452 ///
453 /// # Arguments
454 ///
455 /// * `current_pos` - The starting `Position`.
456 /// * `direction` - The `Direction` of movement (Horizontal or Vertical).
457 /// * `step` - The number of units to move (can be negative for backward movement).
458 ///
459 /// # Returns
460 ///
461 /// - `Ok(Position)` representing the new calculated position.
462 /// - `Err(GridError)` if an invalid direction is provided.
463 ///
464 /// # Errors
465 ///
466 /// Returns a `GridError::InvalidDirection` if `Direction::NotSet` is provided.
467 ///
468 /// # Examples
469 ///
470 /// ```
471 /// use crossword_puzzle::{Grid, word::{Direction, Position}};
472 ///
473 /// let grid = Grid::new();
474 /// let pos = Position { x: 5, y: 5 };
475 ///
476 /// // Move horizontally by 2
477 /// assert_eq!(grid.get_next_pos(pos, Direction::Horizontal, 2).unwrap(), Position { x: 7, y: 5 });
478 /// // Move vertically by -3
479 /// assert_eq!(grid.get_next_pos(pos, Direction::Vertical, -3).unwrap(), Position { x: 5, y: 2 });
480 /// ```
481 pub fn get_next_pos(
482 &self,
483 current_pos: Position,
484 direction: Direction,
485 step: isize,
486 ) -> Result<Position, GridError> {
487 match direction {
488 Direction::Horizontal => Ok(Position {
489 x: (current_pos.x as isize + step) as usize,
490 y: current_pos.y,
491 }),
492 Direction::Vertical => Ok(Position {
493 x: current_pos.x,
494 y: (current_pos.y as isize + step) as usize,
495 }),
496 Direction::NotSet => Err(GridError::InvalidDirection("Invalid direction".to_string())),
497 }
498 }
499
500 /// Retrieves the coordinate value (x or y) based on the given direction.
501 ///
502 /// This helper function extracts either the `x` or `y` coordinate from `current_pos`
503 /// depending on the provided `direction` (Horizontal or Vertical).
504 ///
505 /// # Arguments
506 ///
507 /// * `current_pos` - The `Position` from which to extract the coordinate.
508 /// * `direction` - The `Direction` indicating which coordinate to retrieve (Horizontal for x, Vertical for y).
509 ///
510 /// # Returns
511 ///
512 /// - `Ok(usize)` representing the extracted coordinate value.
513 /// - `Err(GridError)` if an invalid direction is provided.
514 ///
515 /// # Errors
516 ///
517 /// Returns a `GridError::InvalidDirection` if `Direction::NotSet` is provided.
518 ///
519 /// # Examples
520 ///
521 /// ```
522 /// use crossword_puzzle::{Grid, word::{Direction, Position}};
523 ///
524 /// let grid = Grid::new();
525 /// let pos = Position { x: 10, y: 20 };
526 ///
527 /// assert_eq!(grid.get_coord_val(pos, Direction::Horizontal).unwrap(), 10);
528 /// assert_eq!(grid.get_coord_val(pos, Direction::Vertical).unwrap(), 20);
529 /// ```
530 pub fn get_coord_val(
531 &self,
532 current_pos: Position,
533 direction: Direction,
534 ) -> Result<usize, GridError> {
535 match direction {
536 Direction::Horizontal => Ok(current_pos.x),
537 Direction::Vertical => Ok(current_pos.y),
538 Direction::NotSet => Err(GridError::InvalidDirection("Invalid direction".to_string())),
539 }
540 }
541
542 /// Helper function to check if a character option is empty or contains a space.
543 ///
544 /// This function is used to determine if a cell on the grid is effectively empty.
545 ///
546 /// # Arguments
547 ///
548 /// * `char_option` - An `Option` containing a `(Position, char)` tuple.
549 ///
550 /// # Returns
551 ///
552 /// - `true` if `char_option` is `None` or if it contains a space character (`' '`).
553 /// - `false` otherwise.
554 ///
555 /// # Examples
556 ///
557 /// ```
558 /// use crossword_puzzle::{Grid, word::Position};
559 ///
560 /// let grid = Grid::new();
561 ///
562 /// assert!(grid.is_char_empty_or_none(None));
563 /// assert!(grid.is_char_empty_or_none(Some((Position { x: 0, y: 0 }, ' '))));
564 /// assert!(!grid.is_char_empty_or_none(Some((Position { x: 0, y: 0 }, 'A'))));
565 /// ```
566 pub fn is_char_empty_or_none(&self, char_option: Option<(Position, char)>) -> bool {
567 match char_option {
568 Some((_, ch)) => ch == ' ',
569 None => true,
570 }
571 }
572
573 /// Checks if the neighboring cells in the specified direction are empty.
574 ///
575 /// This function is used to validate if a word can be placed without conflicting with
576 /// existing characters in adjacent cells (not directly part of the word's path).
577 /// It examines the cells perpendicular to the word's direction at `current_pos`.
578 /// For `Direction::Horizontal`, it checks `up` and `down` neighbors. For `Direction::Vertical`,
579 /// it checks `left` and `right` neighbors.
580 ///
581 /// # Arguments
582 ///
583 /// * `current_pos` - The `Position` to check neighbors around.
584 /// * `direction` - The `Direction` of the word being placed (Horizontal or Vertical).
585 ///
586 /// # Returns
587 ///
588 /// - `Ok(true)` if the relevant neighboring cells are empty.
589 /// - `Ok(false)` if any relevant neighboring cell contains a character.
590 /// - `Err(GridError)` if an invalid direction is provided.
591 ///
592 /// # Errors
593 ///
594 /// Returns a `GridError::InvalidDirection` if `Direction::NotSet` is provided.
595 ///
596 /// # Examples
597 ///
598 /// ```
599 /// use crossword_puzzle::{Grid, word::{Word, Direction, Position}};
600 ///
601 /// let mut grid = Grid::new();
602 /// // Initially, all cells are empty
603 /// assert!(grid.is_neighbor_cell_empty(Position { x: 0, y: 0 }, Direction::Horizontal).unwrap());
604 ///
605 /// // Place a word to make some cells occupied
606 /// let word = Word::value("A", 'B', "C").unwrap().position(0, 0).direction(Direction::Vertical);
607 /// grid.add_word(word).unwrap();
608 ///
609 /// // Now, checking (0,0) horizontally should return false because (0,1) is occupied by 'B'
610 /// assert!(!grid.is_neighbor_cell_empty(Position { x: 0, y: 0 }, Direction::Horizontal).unwrap());
611 /// ```
612 pub fn is_neighbor_cell_empty(
613 &self,
614 current_pos: Position,
615 direction: Direction,
616 ) -> Result<bool, GridError> {
617 let neighbor = self.get_neighbor(current_pos);
618
619 match direction {
620 Direction::Horizontal => Ok(self.is_char_empty_or_none(neighbor.up)
621 && self.is_char_empty_or_none(neighbor.down)),
622 Direction::Vertical => Ok(self.is_char_empty_or_none(neighbor.left)
623 && self.is_char_empty_or_none(neighbor.right)),
624 Direction::NotSet => Err(GridError::InvalidDirection("Invalid direction".to_string())),
625 }
626 }
627
628 /// Checks if a `Word` can be validly placed on the grid.
629 ///
630 /// This function performs a series of checks to ensure that placing the given `word`
631 /// on the grid at its specified position and direction does not violate any crossword rules.
632 /// It verifies that the word does not overlap with existing characters incorrectly and that
633 /// adjacent cells are empty where required.
634 ///
635 /// # Arguments
636 ///
637 /// * `word` - A reference to the `Word` to validate for placement.
638 ///
639 /// # Returns
640 ///
641 /// - `Ok(true)` if the word can be validly placed.
642 /// - `Ok(false)` if the placement is invalid due to conflicts or rule violations.
643 /// - `Err(GridError)` if an invalid direction is provided during internal checks.
644 ///
645 /// # Errors
646 ///
647 /// Returns a `GridError` if any internal helper function (e.g., `get_next_pos`,
648 /// `is_neighbor_cell_empty`) encounters an `InvalidDirection` error.
649 ///
650 /// # Examples
651 ///
652 /// ```
653 /// use crossword_puzzle::{Grid, word::{Word, Direction}};
654 ///
655 /// let mut grid = Grid::new();
656 /// let word1 = Word::value("", 'T', "EST").unwrap().position(0, 0).direction(Direction::Horizontal);
657 /// grid.add_word(word1).unwrap();
658 ///
659 /// // A word that overlaps correctly (e.g., 'T' from TEST and TEAM)
660 /// let word2 = Word::value("", 'T', "EAM").unwrap().position(0, 0).direction(Direction::Vertical);
661 /// assert!(grid.is_valid_placement(&word2).unwrap());
662 ///
663 /// // A word that conflicts (e.g., 'B' from BEST conflicts with 'T' from TEST)
664 /// let word3 = Word::value("BES", 'T', "").unwrap().position(4, 0).direction(Direction::Horizontal);
665 /// assert!(!grid.is_valid_placement(&word3).unwrap());
666 /// ```
667 pub fn is_valid_placement(&self, word: &Word<'a>) -> Result<bool, GridError> {
668 let mut pos = word.position;
669
670 // Check prefix
671 if !self.check_segment_placement(word, &mut pos, word.segment.prefix.chars().rev(), -1)? {
672 return Ok(false);
673 }
674
675 // Check suffix
676 pos = word.position;
677 if !self.check_segment_placement(word, &mut pos, word.segment.suffix.chars(), 1)? {
678 return Ok(false);
679 }
680
681 Ok(true)
682 }
683
684 /// Helper function to check the placement of a word segment (prefix or suffix).
685 ///
686 /// This function iterates through the characters of a word segment and validates their
687 /// placement on the grid. It checks for conflicts with existing characters and ensures
688 /// that perpendicular neighbors are empty where required.
689 ///
690 /// # Arguments
691 ///
692 /// * `word` - A reference to the `Word` being checked.
693 /// * `pos` - A mutable reference to the current `Position` on the grid, which is updated during iteration.
694 /// * `chars` - An iterator over the characters of the segment (prefix or suffix).
695 /// * `step` - The step size and direction (e.g., `1` for forward, `-1` for backward).
696 ///
697 /// # Returns
698 ///
699 /// - `Ok(true)` if the segment can be validly placed.
700 /// - `Ok(false)` if a conflict is found.
701 /// - `Err(GridError)` if an invalid direction is provided during internal checks.
702 ///
703 /// # Errors
704 ///
705 /// Returns a `GridError` if any internal helper function (e.g., `get_next_pos`,
706 /// `is_neighbor_cell_empty`) encounters an `InvalidDirection` error.
707 pub fn check_segment_placement(
708 &self,
709 word: &Word<'a>,
710 pos: &mut Position,
711 chars: impl Iterator<Item = char>,
712 step: isize,
713 ) -> Result<bool, GridError> {
714 for ch in chars {
715 *pos = self.get_next_pos(*pos, word.direction, step)?;
716 if let Some(board_ch) = self.get_char(*pos) {
717 if board_ch == ch {
718 continue;
719 }
720 if board_ch != ' ' && board_ch != ch {
721 return Ok(false);
722 }
723 if !self.is_neighbor_cell_empty(*pos, word.direction)? {
724 return Ok(false);
725 }
726 if self.get_coord_val(*pos, word.direction)? != 0 {
727 continue;
728 }
729 }
730 break;
731 }
732 // After the loop, check the character at the next position if the iterator is exhausted
733 *pos = self.get_next_pos(*pos, word.direction, step)?;
734 if let Some(board_ch) = self.get_char(*pos) {
735 if board_ch != ' ' {
736 return Ok(false);
737 }
738 }
739 Ok(true)
740 }
741
742 /// Finds valid placements for a word segment based on a crossed character.
743 ///
744 /// This function searches for existing words on the grid that can be crossed by a new word segment.
745 /// It generates potential `Word` candidates based on the `prefix`, `crossed` character, `suffix`,
746 /// and `direction`, and then validates each potential placement using `is_valid_placement`.
747 ///
748 /// # Arguments
749 ///
750 /// * `prefix` - The string segment before the crossed character.
751 /// * `crossed` - The character that crosses an existing word.
752 /// * `suffix` - The string segment after the crossed character.
753 /// * `direction` - The `Direction` of the new word (Horizontal or Vertical).
754 ///
755 /// # Returns
756 ///
757 /// - `Ok(Vec<Word>)` containing a list of valid `Word` placements.
758 /// - `Err(GridError)` if an invalid direction is provided or a `WordError` occurs during `Word::value` creation.
759 ///
760 /// # Errors
761 ///
762 /// Returns a `GridError` if `Direction::NotSet` is provided or if `Word::value` returns a `WordError`.
763 pub fn find_valid_placements_for_segment(
764 &self,
765 prefix: &'a str,
766 crossed: char,
767 suffix: &'a str,
768 direction: Direction,
769 ) -> Result<Vec<Word<'a>>, GridError> {
770 let mut placements = Vec::new();
771 let (opposite_direction, get_pos, get_origin): PlacementHelper<'a> = match direction {
772 Direction::Horizontal => (
773 Direction::Vertical,
774 Box::new(|w, i| (w.position.x, i)),
775 Box::new(|w| w.origin.y),
776 ),
777 Direction::Vertical => (
778 Direction::Horizontal,
779 Box::new(|w, i| (i, w.position.y)),
780 Box::new(|w| w.origin.x),
781 ),
782 Direction::NotSet => {
783 return Err(GridError::InvalidDirection("Invalid direction".to_string()))
784 }
785 };
786
787 for word in self
788 .words
789 .iter()
790 .filter(|p| p.direction == opposite_direction)
791 {
792 let full_word = word.segment.full_word();
793 for (ch, index) in full_word.iter().zip(get_origin(word)..) {
794 if *ch == crossed {
795 let (x, y) = get_pos(word, index);
796 let new_word = Word::value(prefix, crossed, suffix)?
797 .position(x, y)
798 .direction(direction);
799
800 if self.is_valid_placement(&new_word)? {
801 placements.push(new_word);
802 }
803 }
804 }
805 }
806
807 Ok(placements)
808 }
809
810 /// Finds all valid placements for a given word string on the current grid.
811 ///
812 /// This function iterates through each character of the `word_str` to consider it as a potential
813 /// crossing point. For each potential crossing, it attempts to find valid horizontal and vertical
814 /// placements on the grid. If the grid is empty, it considers initial horizontal and vertical placements.
815 ///
816 /// # Arguments
817 ///
818 /// * `word_str` - The string slice representing the word to find placements for.
819 ///
820 /// # Returns
821 ///
822 /// - `Ok(Vec<Word>)` containing a list of all valid `Word` placements found.
823 /// - `Err(GridError)` if an invalid direction is provided or a `WordError` occurs during `Word` creation.
824 ///
825 /// # Errors
826 ///
827 /// Returns a `GridError` if `handle_initial_placements` or `find_valid_placements_for_segment`
828 /// return an error.
829 ///
830 /// # Examples
831 ///
832 /// ```
833 /// use crossword_puzzle::{Grid, word::{Word, Direction}};
834 ///
835 /// let mut grid = Grid::new();
836 /// let word1 = Word::value("", 'R', "UST").unwrap().direction(Direction::Horizontal);
837 /// grid.add_word(word1).unwrap();
838 ///
839 /// let placements = grid.find_valid_placements("TEST").unwrap();
840 /// assert!(!placements.is_empty());
841 /// ```
842 pub fn find_valid_placements(&self, word_str: &'a str) -> Result<Vec<Word<'a>>, GridError> {
843 let mut placements = Vec::new();
844
845 for index in 0..word_str.len() {
846 let (prefix, remain) = word_str.split_at(index);
847 let (mid, suffix) = remain.split_at(1);
848 let crossed = mid.chars().next().unwrap();
849
850 if self.words.is_empty() {
851 placements.extend(self.handle_initial_placements(prefix, crossed, suffix)?);
852 } else {
853 placements.extend(self.find_valid_placements_for_segment(
854 prefix,
855 crossed,
856 suffix,
857 Direction::Horizontal,
858 )?);
859 placements.extend(self.find_valid_placements_for_segment(
860 prefix,
861 crossed,
862 suffix,
863 Direction::Vertical,
864 )?);
865 }
866 }
867
868 Ok(placements)
869 }
870
871 /// Handles the initial placements when the grid is empty.
872 ///
873 /// It generates both horizontal and vertical `Word` placements for the given segment,
874 /// assuming an empty grid where the word can be placed at the origin (0,0).
875 ///
876 /// # Arguments
877 ///
878 /// * `prefix` - The string segment before the crossed character.
879 /// * `crossed` - The character that would be at the crossing point.
880 /// * `suffix` - The string segment after the crossed character.
881 ///
882 /// # Returns
883 ///
884 /// - `Ok(Vec<Word>)` containing two `Word` candidates: one horizontal and one vertical.
885 /// - `Err(GridError)` if a `WordError` occurs during `Word::value` creation.
886 ///
887 /// # Errors
888 ///
889 /// Returns a `GridError` if `Word::value` returns a `WordError` (e.g., invalid segment).
890 ///
891 /// # Examples
892 ///
893 /// ```
894 /// use crossword_puzzle::{Grid, word::Direction};
895 ///
896 /// let grid = Grid::new();
897 /// let placements = grid.handle_initial_placements("", 'A', "BC").unwrap();
898 /// assert_eq!(placements.len(), 2);
899 /// assert_eq!(placements[0].direction, Direction::Horizontal);
900 /// assert_eq!(placements[1].direction, Direction::Vertical);
901 /// ```
902 pub fn handle_initial_placements(
903 &self,
904 prefix: &'a str,
905 crossed: char,
906 suffix: &'a str,
907 ) -> Result<Vec<Word<'a>>, GridError> {
908 let mut placements = Vec::new();
909 let horizontal_word =
910 Word::value(prefix, crossed, suffix)?.direction(Direction::Horizontal);
911 placements.push(horizontal_word);
912
913 let vertical_word = Word::value(prefix, crossed, suffix)?.direction(Direction::Vertical);
914 placements.push(vertical_word);
915 Ok(placements)
916 }
917
918 /// Serializes the `Grid` into a JSON string.
919 ///
920 /// This function requires the `serde` feature to be enabled.
921 ///
922 /// # Returns
923 ///
924 /// - `Ok(String)` containing the JSON representation of the grid.
925 /// - `Err(serde_json::Error)` if serialization fails.
926 #[cfg(feature = "serde")]
927 pub fn to_json(&self) -> Result<String, serde_json::Error> {
928 serde_json::to_string(&self)
929 }
930
931 /// Serializes the `Grid` into a pretty-printed JSON string.
932 ///
933 /// This function requires the `serde` feature to be enabled.
934 ///
935 /// # Returns
936 ///
937 /// - `Ok(String)` containing the pretty-printed JSON representation of the grid.
938 /// - `Err(serde_json::Error)` if serialization fails.
939 #[cfg(feature = "serde")]
940 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
941 serde_json::to_string_pretty(&self)
942 }
943}
944
945#[derive(Clone, Debug)]
946pub struct PossibleWord<'a> {
947 /// The string value of the word.
948 pub value: &'a str,
949 /// The number of remaining attempts to place this word on the grid.
950 pub remaining: usize,
951}
952
953impl<'a> PossibleWord<'a> {
954 /// Creates a new `PossibleWord` instance
955 ///
956 /// Initializes a `PossibleWord` with the given string `value` and sets
957 /// `remaining` attempts to `3` by default.
958 ///
959 /// # Arguments
960 ///
961 /// * `value` - The string slice representing the word.
962 ///
963 /// # Returns
964 ///
965 /// A new `PossibleWord` instance.
966 ///
967 /// # Examples
968 ///
969 /// ```
970 /// use crossword_puzzle::PossibleWord;
971 ///
972 /// let pw = PossibleWord::new("HELLO");
973 /// assert_eq!(pw.value, "HELLO");
974 /// assert_eq!(pw.remaining, 3);
975 /// ```
976 pub fn new(value: &'a str) -> Self {
977 Self {
978 value,
979 remaining: 3,
980 }
981 }
982}
983
984/// Calculates the squared Euclidean distance between two `Position`s.
985///
986/// This function is used to determine the distance between two points on the grid
987/// without computing the square root, which is often sufficient for comparisons.
988/// It's particularly useful for sorting placements by their proximity to a reference point.
989///
990/// # Arguments
991///
992/// * `a` - The first `Position`.
993/// * `b` - The second `Position`.
994///
995/// # Returns
996///
997/// The squared Euclidean distance as an `i32`.
998fn squared_euclidean(a: Position, b: Position) -> i32 {
999 let dx = a.x as i32 - b.x as i32;
1000 let dy = a.y as i32 - b.y as i32;
1001 (dx * dx) + (dy * dy)
1002}
1003
1004/// A backtracking function to generate the crossword puzzle.
1005///
1006/// This function attempts to place words one by one onto the grid using a recursive
1007/// backtracking approach. It explores possible placements for each word. If a word
1008/// cannot be placed in the current attempt and has remaining retries, it is re-queued
1009/// for a later attempt. If a placement leads to a dead end, the function backtracks
1010/// to try another path.
1011///
1012/// # Arguments
1013///
1014/// * `grid` - The current `Grid` state.
1015/// * `words_to_place` - A `VecDeque` containing `PossibleWord`s that still need to be placed,
1016/// along with their remaining placement attempts.
1017///
1018/// # Returns
1019///
1020/// - `Ok(Some(Grid))` if a complete and valid crossword puzzle grid is successfully generated.
1021/// - `Ok(None)` if no valid grid can be generated from the given words after all attempts.
1022/// - `Err(Error)` if an error occurs during grid operations (e.g., invalid word segments).
1023///
1024/// # Errors
1025///
1026/// Returns an `Error` if `Grid::find_valid_placements` or `Grid::add_word` return an error.
1027pub fn backtrack<'a>(
1028 grid: Grid<'a>,
1029 mut words_to_place: VecDeque<PossibleWord<'a>>,
1030) -> Result<Option<Grid<'a>>, Error> {
1031 if let Some(mut current_word) = words_to_place.pop_front() {
1032 let mut placements = grid.find_valid_placements(current_word.value)?;
1033 if placements.is_empty() && current_word.remaining > 1 {
1034 current_word.remaining = current_word.remaining.saturating_sub(1);
1035 words_to_place.push_back(current_word);
1036
1037 return backtrack(grid, words_to_place);
1038 }
1039
1040 let ref_position = Position {
1041 x: grid.board[0].len() / 2,
1042 y: grid.board.len() / 2,
1043 };
1044 placements.sort_by_key(|w| squared_euclidean(w.position, ref_position));
1045 for placement_word in placements {
1046 let mut new_grid = grid.clone();
1047 new_grid.add_word(placement_word)?;
1048
1049 if let Some(final_grid) = backtrack(new_grid, words_to_place.clone())? {
1050 return Ok(Some(final_grid));
1051 }
1052 }
1053 }
1054
1055 Ok((!grid.words.is_empty() || words_to_place.is_empty()).then_some(grid))
1056}
1057
1058/// Eliminates words that do not share any common characters with other words.
1059///
1060/// This function filters the initial list of words, keeping only those that have at least
1061/// one common character with another word in the list. This helps in reducing the search space
1062/// for the crossword generation by focusing on words that can actually intersect.
1063/// The words are then sorted by length in reverse order (longest first).
1064///
1065/// # Arguments
1066///
1067/// * `words_to_place` - A slice of string slices (`&[&'a str]`) representing the initial list of words.
1068///
1069/// # Returns
1070///
1071/// A `VecDeque<PossibleWord>` containing the filtered and sorted words, wrapped in `PossibleWord` structs.
1072///
1073/// # Examples
1074///
1075/// ```
1076/// use crossword_puzzle::{eliminate_words, PossibleWord};
1077/// use std::collections::VecDeque;
1078///
1079/// let words = &["RUST", "TEST", "CODE", "ZIP"];
1080/// let filtered_words = eliminate_words(words);
1081///
1082/// // "ZIP" does not share any common characters with "RUST", "TEST", or "CODE"
1083/// // So it should be eliminated.
1084/// assert_eq!(filtered_words.len(), 3);
1085/// ```
1086pub fn eliminate_words<'a>(words_to_place: &[&'a str]) -> VecDeque<PossibleWord<'a>> {
1087 let mut filtered_words_set = std::collections::HashSet::new();
1088
1089 for i in 0..words_to_place.len() {
1090 let word1 = words_to_place[i];
1091 let chars1: std::collections::HashSet<char> = word1.chars().collect();
1092
1093 let mut has_common_char_with_other_word = false;
1094 for (j, word2) in words_to_place.iter().enumerate() {
1095 if i == j {
1096 continue;
1097 }
1098 let chars2: std::collections::HashSet<char> = word2.chars().collect();
1099
1100 if chars1.intersection(&chars2).next().is_some() {
1101 has_common_char_with_other_word = true;
1102 break;
1103 }
1104 }
1105
1106 if has_common_char_with_other_word {
1107 filtered_words_set.insert(word1);
1108 }
1109 }
1110
1111 let mut filtered_words: Vec<&'a str> = filtered_words_set.into_iter().collect();
1112 filtered_words.sort_by_key(|c| std::cmp::Reverse(c.len()));
1113 VecDeque::from(
1114 filtered_words
1115 .iter()
1116 .map(|w| PossibleWord::new(w))
1117 .collect::<Vec<_>>(),
1118 )
1119}
1120
1121/// Generates a crossword puzzle grid from a given list of words.
1122///
1123/// This function takes a slice of string slices, where each string represents a word
1124/// to be included in the crossword puzzle. It attempts to arrange these words
1125/// into a valid crossword grid using a backtracking algorithm.
1126///
1127/// The process involves:
1128/// 1. Eliminating words that do not share common characters with other words to
1129/// reduce the search space.
1130/// 2. Attempting to place words one by one onto the grid.
1131/// 3. If a word cannot be placed, the algorithm backtracks and tries a different
1132/// placement or a different word.
1133///
1134/// # Arguments
1135///
1136/// * `words` - A slice of string slices (`&[&'a str]`) representing the words
1137/// to be used in the crossword puzzle.
1138///
1139/// # Returns
1140///
1141/// A `Result` which is:
1142/// - `Ok(Some(Grid))` if a valid crossword puzzle grid is successfully generated.
1143/// - `Ok(None)` if no valid grid can be generated from the given words.
1144/// - `Err(Error)` if an error occurs during the generation process (e.g.,
1145/// invalid word segments).
1146///
1147/// # Examples
1148///
1149/// ```
1150/// use crossword_puzzle::{generate, Grid};
1151///
1152/// let words = &["LOREM", "IPSUM", "DOLOR", "SIT", "AMET"];
1153/// match generate(words) {
1154/// Ok(Some(grid)) => {
1155/// // Print the generated grid
1156/// for row in grid.board {
1157/// println!("{}", row.iter().collect::<String>());
1158/// }
1159/// },
1160/// Ok(None) => println!("Could not generate a crossword puzzle."),
1161/// Err(e) => eprintln!("Error generating puzzle: {}", e),
1162/// }
1163/// ```
1164pub fn generate<'a>(words: &[&'a str]) -> Result<Option<Grid<'a>>, Error> {
1165 for word in words.iter() {
1166 if word.chars().any(|c| c.is_lowercase()) {
1167 return Err(Error::WordError(WordError::LowercaseCharactersInSegment));
1168 }
1169 }
1170
1171 let words_queue = eliminate_words(words);
1172 let initial_grid = Grid::new();
1173 backtrack(initial_grid, words_queue)
1174}