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}