game_grid/lib.rs
1//! # Game Grid
2//!
3//! A simple 2D grid for prototyping games. Including easy parsing, indexing and iterators.
4//!
5//! ## Key features:
6//! * Easy parsing of string literal to typed 2D grid thanks to a derive macro.
7//! * Indexing with a 2D vector struct ex: Point { x: i32, y: i32 } instead of always writing the usual i = y * width + x
8//! * Iterators and utilities.
9//!
10//! ## Description
11//!
12//! The main struct is `Grid` that implements a grid able to contain values of a user `Cell` type.
13//! The user cell can be any type but it works best with enums that implement the GridCell trait.
14//! The GridCell derive macro allows to implement automatically conversions to and from char, allowing to convert a grid to an from strings.
15//! `Grid` provides access to the cells with 2D indexing with user types that implement the `GridPosition` trait.
16//! On top of that `Grid` provides iterators and other utilities.
17//!
18//! ## Using the Grid with Bevy IVec2
19//! One of the core features of game-grid is to be able to index the grid with 2D vector structs that we use to make games.
20//! If you are using this with Bevy, the feature bevy-ivec2 includes a trait implementation of game_grid::GridPosition for IVec2 that allows to use IVec2 as index.
21//! To use it add this line to you Cargo.toml: game-grid = { features = ["bevy-ivec2"] }
22//!
23//! ## Example:
24//!
25//! ```
26//! use game_grid::*;
27//! // A custom Cell type deriving the trait GridCell with associated char literals.
28//! #[derive(GridCell, Copy, Clone, Debug, PartialEq, Eq, Default)]
29//! enum Cell {
30//! // Wall cells are represented by '#'.
31//! #[cell('#')]
32//! Wall,
33//!
34//! // Empty cells are represented by both ' ' and '.', the former will be used for display.
35//! // A default value can be used by some Grid functionalities.
36//! #[cell(' '|'.')]
37//! #[default]
38//! Empty,
39//!
40//! #[cell('o')]
41//! Food,
42//!
43//! // It is also possible to provide a range, the actual character can be captured.
44//! #[cell('A'..='Z')]
45//! Player(char),
46//! }
47//!
48//! // A 2D point struct deriving GridPosition in order to be used as index into the grid.
49//! #[derive(GridPosition, PartialEq, Eq, Debug)]
50//! struct Point {
51//! x: i32,
52//! y: i32,
53//! }
54//!
55//! // Create a grid of cells by parsing a string literal.
56//! let grid: Grid<Cell> = "#####\n\
57//! #A o#\n\
58//! #####".parse().unwrap();
59//!
60//! // Use iter() to iterate over the cells with associated position.
61//! let food_position: Point = grid.iter().find(|(_, cell)| *cell == Cell::Food).unwrap().0;
62//! assert_eq!(food_position, Point{ x: 3, y: 1 });
63//!
64//! // Index into the grid with 2D point type and retrieved the enum value captured during parsing.
65//! if let Cell::Player(player_char) = grid[Point::new(1, 1)] {
66//! println!("Player id: {player_char}");
67//! }
68//!
69//! // Print the grid.
70//! print!("{grid}");
71//! // outputs:
72//! // #####
73//! // #A o#
74//! // #####
75//!
76//! ```
77use core::slice::Iter;
78use std::error::Error;
79use std::marker::PhantomData;
80use std::ops::Index;
81use std::slice::IterMut;
82use std::{fmt::Display, str::FromStr};
83
84pub use game_grid_derive::GridCell;
85pub use game_grid_derive::GridPosition;
86
87/// Trait to implement a type that can be used as a grid cell with pparsing and display functionalities.
88///
89/// The trait itself is empty but requires to implement `TryFrom<char>`.
90/// This trait is most useful by using the derive macro and specifying assiciated char values.
91///
92/// ```
93/// use game_grid::*;
94/// #[derive(GridCell, Copy, Clone, Debug, PartialEq, Eq, Default)]
95/// enum Cell {
96/// // Wall cells are represented by '#'.
97/// #[cell('#')]
98/// Wall,
99///
100/// // Empty cells are represented by both ' ' and '.', the former will be used for display.
101/// // A default value can be used by some Grid functionalities.
102/// #[cell(' '|'.')]
103/// #[default]
104/// Empty,
105///
106/// #[cell('o')]
107/// Food,
108///
109/// // It is also possible to provide a range, the actual character can be captured.
110/// #[cell('A'..='Z')]
111/// Player(char),
112/// }
113/// ```
114pub trait GridCell: TryFrom<char> + Clone + Copy + PartialEq + Eq {}
115
116/// Trait to implement a type that can be used as a grid position.
117///
118/// This trait provides access to the x and y coordinates as well as a constructor used by the grid internaly.
119pub trait GridPosition {
120 /// Construct a position from x and y coordinates.
121 fn new(x: i32, y: i32) -> Self;
122
123 /// Access the x coordinate of a position.
124 fn x(&self) -> i32;
125
126 /// Access the y coordinate of a position.
127 fn y(&self) -> i32;
128}
129
130#[cfg(feature = "bevy-ivec2")]
131impl GridPosition for bevy::prelude::IVec2 {
132 fn new(x: i32, y: i32) -> Self {
133 Self::new(x, y)
134 }
135
136 fn x(&self) -> i32 {
137 self.x
138 }
139
140 fn y(&self) -> i32 {
141 self.y
142 }
143}
144
145/// A struct maintaining a grid usable for game prototyping.
146///
147/// The grid is stored as a linear `Vec` containing cells and Grid provides
148/// functions to look up and write to the grid with 2-dimentional vector types implementing the trait `GridPosition`
149///
150/// ```
151/// use game_grid::*;
152/// #[derive(Clone, Copy)]
153/// enum Cell {
154/// Wall,
155/// Empty,
156/// }
157///
158/// #[derive(GridPosition, PartialEq, Eq, Debug)]
159/// struct Point {
160/// x: i32,
161/// y: i32,
162/// }
163///
164/// // Create a 2x2 grid with empty cells.
165/// let mut grid: Grid<Cell> = Grid::new(2, 2, Cell::Empty);
166/// assert_eq!(grid.width(), 2);
167/// assert_eq!(grid.height(), 2);
168///
169/// // Add a wall at cell (0, 0).
170/// grid.set_cell(Point::new(0, 0), Cell::Wall);
171/// ```
172#[derive(Debug, Clone)]
173pub struct Grid<Cell> {
174 cells: Vec<Cell>,
175 width: usize,
176 height: usize,
177}
178
179impl<Cell> Grid<Cell>
180where
181 Cell: Clone + Default,
182{
183 /// Construct a grid from a slice and the desired row width.
184 ///
185 /// ```
186 /// use game_grid::*;
187 /// // Create a 2x2 grid with some data.
188 /// let grid: Grid<i32> = Grid::from_slice(2, &[0, 1, 2, 3]);
189 /// assert_eq!(grid.width(), 2);
190 /// assert_eq!(grid.height(), 2);
191 /// ```
192 ///
193 /// Any slice with width equal to 0 will produce an empty grid.
194 /// If the length of input slice is not a multiple of width,
195 /// the last row will be filled with default cell values so that the grid is square.
196 pub fn from_slice(width: usize, data: &[Cell]) -> Self {
197 if width == 0 {
198 return Self {
199 cells: vec![],
200 width,
201 height: 0,
202 };
203 }
204 let height = data.len() / width;
205 let mut cells: Vec<Cell> = data.into();
206
207 if data.len() < width * height {
208 cells.resize(width * height, Cell::default());
209 }
210
211 Self {
212 cells,
213 width,
214 height,
215 }
216 }
217}
218
219impl<Cell> Grid<Cell>
220where
221 Cell: Clone,
222{
223 /// Construct a grid from a slice and the desired row width.
224 /// Any slice with width equal to 0 will produce an empty grid.
225 /// The function will panic if the length of the input slice is not a multiple of width.
226 pub fn from_slice_exact(width: usize, data: &[Cell]) -> Self {
227 if width == 0 {
228 return Self {
229 cells: vec![],
230 width,
231 height: 0,
232 };
233 }
234 let height = data.len() / width;
235
236 if data.len() != width * height {
237 panic!("'from_slice_exact' expects the input data's length to be a multiple of width.");
238 }
239
240 Self {
241 cells: data.into(),
242 width,
243 height,
244 }
245 }
246}
247
248impl<Cell> Grid<Cell>
249where
250 Cell: Clone + Copy,
251{
252 /// Flips the order of the lines vertically. Useful when the game's y axis is upwards.
253 /// # Example:
254 /// ```
255 /// use game_grid::Grid;
256 ///
257 /// let string_grid = "aaa
258 /// bbb
259 /// ccc";
260 ///
261 /// let grid = string_grid.parse::<Grid<char>>().unwrap().flip_y();
262 ///
263 /// let string_grid_flipped = "ccc
264 /// bbb
265 /// aaa";
266 ///
267 /// assert_eq!(grid.to_string(), string_grid_flipped);
268 /// ```
269 pub fn flip_y(mut self) -> Self {
270 self.cells = self
271 .cells
272 .chunks(self.width)
273 .rev()
274 .flatten()
275 .copied()
276 .collect();
277 self
278 }
279
280 /// Get the cell value at some position.
281 pub fn cell_at<Point: GridPosition>(&self, position: Point) -> Cell {
282 self.cells[self.index_for_position(position)]
283 }
284
285 /// Construct a new grid with width, height and an initial value.
286 pub fn new(width: usize, height: usize, value: Cell) -> Self {
287 let mut cells = Vec::new();
288 cells.resize(width * height, value);
289 Self {
290 width,
291 height,
292 cells,
293 }
294 }
295}
296
297impl<Cell> Grid<Cell> {
298 /// Set the cell value at some position.
299 pub fn set_cell<Point: GridPosition>(&mut self, position: Point, value: Cell) {
300 let index = self.index_for_position(position);
301 self.cells[index] = value;
302 }
303
304 /// An iterator visiting the cells in order of memory.
305 pub fn cells(&self) -> Iter<'_, Cell> {
306 self.cells.iter()
307 }
308
309 /// An iterator visiting the cells mutably in order of memory.
310 pub fn mut_cells(&mut self) -> IterMut<'_, Cell> {
311 self.cells.iter_mut()
312 }
313
314 /// An iterator visiting the cell and associated position in the grid.
315 pub fn iter<Point: GridPosition>(&self) -> GridIter<Cell, Point> {
316 GridIter {
317 current: 0,
318 grid: self,
319 phantom: PhantomData,
320 }
321 }
322
323 /// Get the 2D position for an index in the linear array. index = y * width + x
324 ///
325 /// ```
326 /// use game_grid::*;
327 /// // A 2D point struct deriving GridPosition.
328 /// #[derive(GridPosition, PartialEq, Eq, Debug)]
329 /// struct Point {
330 /// x: i32,
331 /// y: i32,
332 /// }
333 /// let grid = Grid::<i32>::new(2, 2, 0);
334 ///
335 /// assert_eq!(grid.position_for_index::<Point>(3), Point::new(1, 1));
336 /// ```
337 pub fn position_for_index<Point: GridPosition>(&self, index: usize) -> Point {
338 Point::new((index % self.width) as i32, (index / self.width) as i32)
339 }
340
341 /// Get the index in the linear array for a 2D position. Index = y * width + x.
342 ///
343 /// ```
344 /// use game_grid::*;
345 /// // A 2D point struct deriving GridPosition.
346 /// #[derive(GridPosition, PartialEq, Eq, Debug)]
347 /// struct Point {
348 /// x: i32,
349 /// y: i32,
350 /// }
351 /// let grid = Grid::<i32>::new(2, 2, 0);
352 ///
353 /// assert_eq!(grid.index_for_position(Point::new(1, 1)), 3);
354 /// ```
355 pub fn index_for_position<Point: GridPosition>(&self, position: Point) -> usize {
356 position.x() as usize + self.width * position.y() as usize
357 }
358
359 /// Returns the number of cells in the grid.
360 pub fn len(&self) -> usize {
361 self.cells.len()
362 }
363
364 /// Check whether teh grid is empty.
365 pub fn is_empty(&self) -> bool {
366 self.cells.len() == 0
367 }
368
369 /// Returns the width of the grid.
370 pub fn width(&self) -> usize {
371 self.width
372 }
373
374 /// Returns the height of the grid.
375 pub fn height(&self) -> usize {
376 self.height
377 }
378
379 /// Check if a position is in the grid bounds.
380 pub fn is_in_bounds<Point: GridPosition>(&self, position: Point) -> bool {
381 position.x() >= 0
382 && position.x() < self.width as i32
383 && position.y() >= 0
384 && position.y() < self.height as i32
385 }
386}
387
388impl<Cell> Index<usize> for Grid<Cell> {
389 type Output = Cell;
390
391 fn index(&self, index: usize) -> &Self::Output {
392 &self.cells[index]
393 }
394}
395
396impl<Cell, Point: GridPosition> Index<Point> for Grid<Cell> {
397 type Output = Cell;
398
399 fn index(&self, position: Point) -> &Self::Output {
400 &self.cells[self.index_for_position(position)]
401 }
402}
403
404impl<Cell> Display for Grid<Cell>
405where
406 char: From<Cell>,
407 Cell: Copy,
408{
409 fn fmt(&self, formater: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410 let mut output_string = String::with_capacity(self.cells.len() + (self.height - 1));
411 for (index, line) in self.cells.chunks(self.width).enumerate() {
412 output_string.extend(line.iter().map(|cell| char::from(*cell)));
413 if index != self.height - 1 {
414 output_string.push('\n');
415 }
416 }
417 write!(formater, "{output_string}")
418 }
419}
420
421/// An iterator over a grid that gives access to a tupple `(Point, Cell)`
422pub struct GridIter<'a, Cell, Point> {
423 current: usize,
424 grid: &'a Grid<Cell>,
425 phantom: PhantomData<Point>,
426}
427
428impl<'a, Cell, Point> Iterator for GridIter<'a, Cell, Point>
429where
430 Point: GridPosition,
431 Cell: Copy,
432{
433 type Item = (Point, Cell);
434
435 fn next(&mut self) -> Option<Self::Item> {
436 if self.current == self.grid.len() {
437 return None;
438 }
439
440 let result = (
441 self.grid.position_for_index(self.current),
442 self.grid[self.current],
443 );
444
445 self.current += 1;
446
447 Some(result)
448 }
449}
450
451/// Error that can be raised when parsing a cell, repoting the character that could not be read.
452#[derive(Debug, PartialEq, Eq)]
453pub struct ParseCellError(pub char);
454
455impl Display for ParseCellError {
456 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457 write!(f, "Invalid character '{}'", self.0)
458 }
459}
460
461impl Error for ParseCellError {}
462
463/// Error that can be raised when parsing a grid.
464#[derive(Debug)]
465pub struct ParseGridError<UserError> {
466 source: UserError,
467}
468
469impl<UserError: Error> Display for ParseGridError<UserError> {
470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471 write!(f, "Error parsing grid: {}", self.source)
472 }
473}
474
475impl<UserError: Error> Error for ParseGridError<UserError> {}
476
477impl<UserError> From<UserError> for ParseGridError<UserError> {
478 fn from(value: UserError) -> Self {
479 ParseGridError { source: value }
480 }
481}
482
483impl<Cell> FromStr for Grid<Cell>
484where
485 Cell: Default + TryFrom<char> + Clone,
486{
487 type Err = ParseGridError<<Cell as TryFrom<char>>::Error>;
488
489 fn from_str(string: &str) -> Result<Self, Self::Err> {
490 let lines: Result<Vec<Vec<Cell>>, _> = string
491 .split('\n')
492 .map(|line| line.chars().map(|char| char.try_into()).collect())
493 .collect();
494
495 match lines {
496 Ok(mut lines) => {
497 let width = lines.iter().max_by_key(|line| line.len()).unwrap().len();
498 let height = lines.len();
499
500 for line in &mut lines {
501 line.resize(width, Cell::default());
502 }
503
504 let cells: Vec<Cell> = lines.into_iter().flatten().collect();
505 Ok(Grid {
506 cells,
507 width,
508 height,
509 })
510 }
511 Err(err) => Err(ParseGridError { source: err }),
512 }
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 // Using an enum.
521 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
522 enum Cell {
523 Wall(i32),
524 Empty,
525 }
526
527 impl Default for Cell {
528 fn default() -> Self {
529 Cell::Empty
530 }
531 }
532
533 impl From<Cell> for char {
534 fn from(cell: Cell) -> char {
535 match cell {
536 Cell::Wall(_) => '#',
537 Cell::Empty => ' ',
538 }
539 }
540 }
541
542 impl TryFrom<char> for Cell {
543 type Error = ();
544
545 fn try_from(value: char) -> Result<Self, Self::Error> {
546 match value {
547 '#' => Ok(Cell::Wall(0)),
548 ' ' => Ok(Cell::Empty),
549 _ => Err(()),
550 }
551 }
552 }
553
554 // A 2D point struct.
555 #[derive(GridPosition)]
556 struct Point {
557 x: i32,
558 y: i32,
559 }
560
561 #[test]
562 fn test_char_grid() {
563 // Valid input.
564 let result = "abc".parse::<Grid<char>>();
565 assert!(result.is_ok());
566 let result = result.unwrap();
567 assert_eq!(result.to_string(), "abc");
568 }
569
570 #[test]
571 fn test_struct_grid() {
572 // Using a stuct.
573 #[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
574 struct StructCell {
575 c: char,
576 }
577
578 impl From<StructCell> for char {
579 fn from(cell: StructCell) -> char {
580 cell.c
581 }
582 }
583
584 impl TryFrom<char> for StructCell {
585 type Error = ();
586
587 fn try_from(value: char) -> Result<Self, Self::Error> {
588 Ok(StructCell { c: value })
589 }
590 }
591
592 // Valid input.
593 let result = "abc".parse::<Grid<StructCell>>();
594 assert!(result.is_ok());
595 let result = result.unwrap();
596 assert_eq!(result.to_string(), "abc");
597 }
598
599 #[test]
600 fn test_enum_grid() {
601 // Empty string.
602 let result = "".parse::<Grid<Cell>>();
603 assert!(result.is_ok());
604 let result = result.unwrap();
605 assert!(result.is_empty());
606
607 // Valid input.
608 let result = "## #".parse::<Grid<Cell>>();
609 assert!(result.is_ok());
610 let result = result.unwrap();
611 assert_eq!(result.to_string(), "## #");
612
613 // Wrong character is error.
614 let result = "a".parse::<Grid<Cell>>();
615 assert!(result.is_err());
616 }
617
618 #[test]
619 fn test_enum_grid_without_impl() {
620 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
621 enum Plain {
622 A,
623 }
624
625 let grid: Grid<Plain> = Grid::from_slice_exact(1, &[Plain::A]);
626 assert_eq!(grid[0], Plain::A);
627 }
628
629 #[test]
630 fn test_indicing() {
631 let grid: Grid<char> = Grid::from_slice(2, &['a', 'b', 'c', 'd']);
632 assert_eq!(grid[0], 'a');
633 assert_eq!(grid[Point::new(0, 0)], 'a');
634 }
635
636 #[test]
637 fn test_derive() {
638 use game_grid_derive::GridCell;
639
640 #[derive(GridCell, PartialEq, Eq, Copy, Clone, Debug)]
641 enum Cell {
642 // #[cell('A'..='Z')]
643 // Wall,
644 #[cell('.')]
645 Empty,
646
647 #[cell('a'|'b')]
648 AOrB,
649 }
650
651 // Existing single entry.
652 assert_eq!(Cell::try_from('.'), Ok(Cell::Empty));
653 assert_eq!(char::from(Cell::Empty), '.');
654
655 // Non existing entry.
656 assert!(Cell::try_from(',').is_err());
657
658 // Or entries.
659 assert_eq!(Cell::try_from('a'), Ok(Cell::AOrB));
660 assert_eq!(Cell::try_from('b'), Ok(Cell::AOrB));
661 assert_eq!(char::from(Cell::AOrB), 'a');
662 }
663
664 #[test]
665 fn test_derive_range() {
666 use game_grid_derive::GridCell;
667
668 #[derive(GridCell, PartialEq, Eq, Copy, Clone, Debug, Default)]
669 enum Cell {
670 #[cell('a'..='z')]
671 Char(char),
672
673 #[default]
674 #[cell(' ')]
675 Empty,
676 }
677
678 // Existing single entry.
679 assert_eq!(Cell::try_from('a'), Ok(Cell::Char('a')));
680 assert_eq!(Cell::try_from('b'), Ok(Cell::Char('b')));
681
682 let result = "ab".parse::<Grid<Cell>>();
683 assert!(result.is_ok());
684 let result = result.unwrap();
685 assert_eq!(result[0], Cell::Char('a'));
686 assert_eq!(result[1], Cell::Char('b'));
687 }
688}