cli_tilemap/
lib.rs

1//! Basic implementation of `TileMap` for `CLI`-based games!
2//!
3//! This module contains the [`Tile`] trait, allowing to represent other data types as `tile`,
4//! or more specifically as [`StyledContent<&'static str>`], provided by the [`crossterm`] crate,
5//! and the [`TileMap<T>`] type, representing a tilemap of `T`, where `T`: [`Tile`] + [`Default`],
6//! which is based on the [`GridMap<V>`] from [`grid-math`] crate, which is a wrapper around the [`std::collections::HashMap`].
7//!
8//! [`TileMap<T>`] implements the [`Deref`] and the [`DerefMut`] traits to deref to the inner `HashMap`,
9//! so we can work with it in the same way as with the [`std::collections::HashMap`].
10//!
11//! # Note
12//!
13//! - Crate is in the "work in progress" state, so the public API may change in the future. Feel free to contribute!
14//!
15//! # Examples
16//!
17//! TileMap for custom data type:
18//! ```
19//! use cli_tilemap::{Tile, TileMap, Formatting};
20//! use crossterm::style::{Stylize, StyledContent};
21//! use grid_math::Cell;
22//! use std::io::stdout;
23//!
24//!
25//! #[derive(Default, Debug)]
26//! enum Entity {
27//!     Enemy,
28//!     Hero,
29//!     #[default]
30//!     Air,
31//! }
32//!
33//! impl Tile for Entity {
34//!     fn tile(&self) -> StyledContent<&'static str> {
35//!         match self {
36//!             Self::Air => "[-]".dark_grey().bold(),
37//!             Self::Hero => "[&]".green().bold(),
38//!             Self::Enemy => "[@]".red().bold(),
39//!         }
40//!     }
41//! }
42//!
43//! // new 5x5 tilemap:
44//! let mut map: TileMap<Entity> = TileMap::new(5, 5);
45//! // insert entities:
46//! map.insert(Cell::new(3, 3), Entity::Enemy);
47//! map.insert(Cell::new(1, 0), Entity::Hero);
48//! // draw map to the raw stdout:
49//! map.draw(&mut stdout()).expect("should be able to draw to the stdout!");
50//! // change row and tile spacing:
51//! map.formatting.row_spacing = 2;
52//! map.formatting.tile_spacing = 4;
53//! // format as a string and print:
54//! let map_string = map.to_string();
55//! println!("{map_string}");
56//! ```
57//!
58//! For more documentation about the `Grid`, `GridMap` and `Cell` types, visit https://crates.io/crates/grid-math
59
60use crossterm::{
61    execute,
62    style::{Print, PrintStyledContent, StyledContent},
63};
64use grid_math::{Cell, Grid, GridMap};
65use std::{
66    collections::HashMap,
67    convert::From,
68    fmt::Display,
69    io,
70    ops::{Deref, DerefMut},
71};
72
73/// `Tile` allows to represent any other data type as `tile`,
74/// or more specifically as `StyledContent<&'static str>`
75///
76/// # Examples
77///
78/// ```
79/// use cli_tilemap::Tile;
80/// use crossterm::style::{Stylize, StyledContent};
81///
82/// #[derive(Default, Debug)]
83/// enum Entity {
84///     Enemy,
85///     Hero,
86///     #[default]
87///     Air,
88/// }
89///
90/// impl Tile for Entity {
91///     fn tile(&self) -> StyledContent<&'static str> {
92///         match self {
93///             Self::Air => "[-]".dark_grey().bold(),
94///             Self::Hero => "[&]".green().bold(),
95///             Self::Enemy => "[@]".red().bold(),
96///         }
97///     }
98/// }
99///
100/// let hero = Entity::Hero;
101/// assert_eq!(hero.tile(), "[&]".green().bold());
102/// ```
103pub trait Tile {
104    fn tile(&self) -> StyledContent<&'static str>;
105}
106
107/// `Formatting` represents instructions for `TileMap<T>` on how to draw tilemap to the terminal
108///
109/// `row_spacing` - number of additional newlines between every row, defaults to 1
110/// `tile_spacing` - number of spaces between every tile, defaults to 1
111/// `top_indent` - number of newlines to insert before drawing the tilemap, defaults to 3
112/// `left_indent` - number of tabs to insert at the start of every row, defaults to 1
113/// `bottom_indent` - number of newlines to insert after drawing the tilemap, defaults to 2
114///
115/// # Examples
116///
117/// ```
118/// use cli_tilemap::Formatting;
119///
120/// let f = Formatting::default();
121/// assert_eq!(f.row_spacing, 1);
122/// assert_eq!(f.tile_spacing, 1);
123/// assert_eq!(f.top_indent, 3);
124/// assert_eq!(f.left_indent, 1);
125/// assert_eq!(f.bottom_indent, 2);
126/// ```
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub struct Formatting {
129    pub row_spacing: u8,
130    pub tile_spacing: u8,
131    pub top_indent: u8,
132    pub left_indent: u8,
133    pub bottom_indent: u8,
134}
135
136/// Implements default values for `Formatting`
137///
138impl Default for Formatting {
139    fn default() -> Self {
140        Self {
141            row_spacing: 1,
142            tile_spacing: 1,
143            top_indent: 3,
144            left_indent: 1,
145            bottom_indent: 2,
146        }
147    }
148}
149
150/// `TileMap<T>`, represents a tilemap over the type `T`, where `T` is `Tile` + `Default`
151///
152/// `TileMap<T>` is based on the `GridMap<V>` from the `grid-math` crate,
153/// and implements the `Deref` and the `DerefMut` traits to deref to the inner `GridMap<T>`
154///
155/// # Examples
156///
157/// ```
158/// use cli_tilemap::{Tile, TileMap, Formatting};
159/// use crossterm::style::{Stylize, StyledContent};
160/// use grid_math::Cell;
161/// use std::io::stdout;
162///
163///
164/// #[derive(Default, Debug)]
165/// enum Entity {
166///     Enemy,
167///     Hero,
168///     #[default]
169///     Air,
170/// }
171///
172/// impl Tile for Entity {
173///     fn tile(&self) -> StyledContent<&'static str> {
174///         match self {
175///             Self::Air => "[-]".dark_grey().bold(),
176///             Self::Hero => "[&]".green().bold(),
177///             Self::Enemy => "[@]".red().bold(),
178///         }
179///     }
180/// }
181///
182/// // new 5x5 tilemap:
183/// let mut map: TileMap<Entity> = TileMap::new(5, 5);
184/// // insert entities:
185/// map.insert(Cell::new(3, 3), Entity::Enemy);
186/// map.insert(Cell::new(1, 0), Entity::Hero);
187/// // draw map to the raw stdout:
188/// map.draw(&mut stdout()).expect("should be able to draw to the stdout!");
189/// // change row and tile spacing:
190/// map.formatting.row_spacing = 2;
191/// map.formatting.tile_spacing = 4;
192/// // format as a string and print:
193/// let map_string = map.to_string();
194/// println!("{map_string}");
195/// ```
196#[derive(Debug, Clone)]
197pub struct TileMap<T>
198where
199    T: Tile + Default,
200{
201    pub formatting: Formatting,
202    gridmap: GridMap<T>,
203}
204
205impl<T> TileMap<T>
206where
207    T: Tile + Default,
208{
209    /// Creates new `TileMap<T>` with the empty inner `GridMap<T>` of specified size,
210    /// and with the defult `Formatting`
211    ///
212    /// For more info, visit `grid-math` crate docs
213    ///
214    pub fn new(width: u8, depth: u8) -> Self {
215        Self {
216            formatting: Formatting::default(),
217            gridmap: GridMap::new(width, depth),
218        }
219    }
220
221    /// Creates new `TileMap<T>` with the empty inner `GridMap<T>` of specified size,
222    /// and with the given `Formatting`
223    ///
224    /// For more info, visit `grid-math` crate docs
225    ///
226    pub fn formatted(width: u8, depth: u8, formatting: Formatting) -> Self {
227        Self {
228            formatting,
229            gridmap: GridMap::new(width, depth),
230        }
231    }
232
233    /// Draws the `TileMap<T>` to the given `stdout`, using the inner `Formatting` rules
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// use cli_tilemap::{Tile, TileMap};
239    /// use crossterm::style::{Stylize, StyledContent};
240    /// use std::io::stdout;
241    ///
242    /// #[derive(Default)]
243    /// struct Empty;
244    ///
245    /// impl Tile for Empty {
246    ///     fn tile(&self) -> StyledContent<&'static str> {
247    ///         "[-]".dark_grey().bold()
248    ///     }
249    /// }
250    ///
251    /// let mut map: TileMap<Empty> = TileMap::new(5, 5);
252    /// map.draw(&mut stdout()).expect("should be able to draw to the stdout!");
253    /// ```
254    pub fn draw<W: io::Write>(&self, stdout: &mut W) -> io::Result<()> {
255        execute!(
256            stdout,
257            Print("\n\r".repeat(self.formatting.top_indent as usize))
258        )?;
259        for row in self.grid().rows() {
260            execute!(
261                stdout,
262                Print("\n\r".repeat(self.formatting.row_spacing as usize)),
263                Print("\t".repeat(self.formatting.left_indent as usize))
264            )?;
265            for cell in row.cells() {
266                execute!(
267                    stdout,
268                    Print(" ".repeat(self.formatting.tile_spacing as usize)),
269                    PrintStyledContent(self.get(&cell).unwrap_or(&T::default()).tile())
270                )?;
271            }
272            execute!(stdout, Print("\n\r"))?;
273        }
274        execute!(
275            stdout,
276            Print("\n\r".repeat(self.formatting.bottom_indent as usize))
277        )?;
278        Ok(())
279    }
280}
281
282impl<T> Display for TileMap<T>
283where
284    T: Tile + Default,
285{
286    /// Implements `fmt` method for the `TileMap<T>` in the same way as the `draw` method works
287    ///
288    /// # Examples
289    ///
290    /// ```
291    /// use cli_tilemap::{Tile, TileMap};
292    /// use crossterm::style::{Stylize, StyledContent};
293    ///
294    /// #[derive(Default)]
295    /// struct Empty;
296    ///
297    /// impl Tile for Empty {
298    ///     fn tile(&self) -> StyledContent<&'static str> {
299    ///         "[-]".dark_grey().bold()
300    ///     }
301    /// }
302    ///
303    /// let mut map: TileMap<Empty> = TileMap::new(5, 5);
304    /// println!("{map}");
305    /// ```
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        write!(f, "{}", "\n\r".repeat(self.formatting.top_indent as usize))?;
308        for row in self.grid().rows() {
309            write!(f, "{}", "\n\r".repeat(self.formatting.row_spacing as usize))?;
310            write!(f, "{}", "\t".repeat(self.formatting.left_indent as usize))?;
311            for cell in row.cells() {
312                write!(f, "{}", " ".repeat(self.formatting.tile_spacing as usize))?;
313                write!(f, "{}", self.get(&cell).unwrap_or(&T::default()).tile())?;
314            }
315            write!(f, "\n\r")?;
316        }
317        write!(
318            f,
319            "{}",
320            "\n\r".repeat(self.formatting.bottom_indent as usize)
321        )?;
322        Ok(())
323    }
324}
325
326impl<T> From<Grid> for TileMap<T>
327where
328    T: Tile + Default,
329{
330    /// Creates new empty `TileMap<T>` from the specified `Grid`
331    ///
332    /// # Examples
333    ///
334    /// ```
335    /// use cli_tilemap::{Tile, TileMap};
336    /// use crossterm::style::{Stylize, StyledContent};
337    /// use grid_math::{Cell, Grid};
338    ///
339    /// #[derive(Default)]
340    /// struct Empty;
341    ///
342    /// impl Tile for Empty {
343    ///     fn tile(&self) -> StyledContent<&'static str> {
344    ///         "[-]".dark_grey().bold()
345    ///     }
346    /// }
347    ///
348    /// let cells = (Cell::new(2, 2), Cell::new(5, 5));
349    /// let grid = Grid::from(cells);
350    /// let map: TileMap<Empty> = TileMap::from(grid);
351    /// assert_eq!(map.grid(), grid);
352    /// ```
353    fn from(grid: Grid) -> Self {
354        Self {
355            formatting: Formatting::default(),
356            gridmap: GridMap::from(grid),
357        }
358    }
359}
360
361impl<T> From<GridMap<T>> for TileMap<T>
362where
363    T: Tile + Default,
364{
365    /// Creates `TileMap<T>` from the existing `GridMap<T>` where `T`: `Tile` + `Default`
366    ///
367    /// # Examples
368    ///
369    /// ```
370    /// use cli_tilemap::{Tile, TileMap};
371    /// use crossterm::style::{Stylize, StyledContent};
372    /// use grid_math::{Cell, GridMap};
373    ///
374    /// #[derive(Debug, Default, PartialEq, Eq)]
375    /// struct Empty;
376    ///
377    /// impl Tile for Empty {
378    ///     fn tile(&self) -> StyledContent<&'static str> {
379    ///         "[-]".dark_grey().bold()
380    ///     }
381    /// }
382    ///
383    /// let mut gridmap: GridMap<Empty> = GridMap::new(5, 5);
384    /// let target = Cell::new(1, 2);
385    /// gridmap.insert(target, Empty);
386    /// let map: TileMap<Empty> = TileMap::from(gridmap);
387    /// assert_eq!(map.get(&target), Some(&Empty));
388    /// ```
389    fn from(gridmap: GridMap<T>) -> Self {
390        Self {
391            formatting: Formatting::default(),
392            gridmap,
393        }
394    }
395}
396
397impl<T> From<(Grid, HashMap<Cell, T>)> for TileMap<T>
398where
399    T: Tile + Default,
400{
401    /// Creates `TileMap<T>` from the existing `HashMap<Cell, T>` and the given `Grid`
402    ///
403    /// # Panics
404    /// Panics if the given `HashMap<Cell, T>` contains `Cell`s that are not within the given `Grid`
405    /// This panic is a part of `grid-math` crate current state, error handling may change in the future
406    ///
407    /// # Examples
408    ///
409    /// ```
410    /// use cli_tilemap::{Tile, TileMap};
411    /// use crossterm::style::{Stylize, StyledContent};
412    /// use grid_math::{Cell, Grid};
413    /// use std::collections::HashMap;
414    ///
415    /// #[derive(Debug, Default, PartialEq, Eq)]
416    /// struct Empty;
417    ///
418    /// impl Tile for Empty {
419    ///     fn tile(&self) -> StyledContent<&'static str> {
420    ///         "[-]".dark_grey().bold()
421    ///     }
422    /// }
423    ///
424    /// let grid = Grid::new(5, 5);
425    /// let mut hashmap: HashMap<Cell, Empty> = HashMap::new();
426    /// let target = Cell::new(1, 2);
427    /// hashmap.insert(target, Empty);
428    /// let map: TileMap<Empty> = TileMap::from((grid, hashmap));
429    /// assert_eq!(map.get(&target), Some(&Empty));
430    /// ```
431    ///
432    /// ```should_panic
433    /// use cli_tilemap::{Tile, TileMap};
434    /// use crossterm::style::{Stylize, StyledContent};
435    /// use grid_math::{Cell, Grid};
436    /// use std::collections::HashMap;
437    ///
438    /// #[derive(Debug, Default, PartialEq, Eq)]
439    /// struct Empty;
440    ///
441    /// impl Tile for Empty {
442    ///     fn tile(&self) -> StyledContent<&'static str> {
443    ///         "[-]".dark_grey().bold()
444    ///     }
445    /// }
446    ///
447    /// let grid = Grid::new(5, 5);
448    /// let mut hashmap: HashMap<Cell, Empty> = HashMap::new();
449    /// let target = Cell::new(7, 1);
450    /// hashmap.insert(target, Empty);
451    /// let map: TileMap<Empty> = TileMap::from((grid, hashmap)); // panic!
452    /// ```
453    fn from(data: (Grid, HashMap<Cell, T>)) -> Self {
454        Self {
455            formatting: Formatting::default(),
456            gridmap: GridMap::from(data),
457        }
458    }
459}
460
461/// Implements `Deref` trait for `TileMap<T>`, to return ref to the inner `GridMap<T>`
462///
463/// For more info, visit `grid-math` crate docs
464///
465impl<T> Deref for TileMap<T>
466where
467    T: Tile + Default,
468{
469    type Target = GridMap<T>;
470    fn deref(&self) -> &Self::Target {
471        &self.gridmap
472    }
473}
474
475/// Implements `Deref` trait for `TileMap<T>`, to return ref to the inner `GridMap<T>`
476///
477/// For more info, visit `grid-math` crate docs
478///
479impl<T> DerefMut for TileMap<T>
480where
481    T: Tile + Default,
482{
483    fn deref_mut(&mut self) -> &mut Self::Target {
484        &mut self.gridmap
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491    use crossterm::style::Stylize;
492    use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
493    use std::io::stdout;
494
495    // declare Entity enum
496    #[derive(Default, Debug)]
497    enum Entity {
498        Enemy,
499        Hero,
500        #[default]
501        Air,
502    }
503
504    // represent Entity as tile
505    impl Tile for Entity {
506        fn tile(&self) -> StyledContent<&'static str> {
507            match self {
508                Self::Air => "[-]".dark_grey().bold(),
509                Self::Hero => "[&]".green().bold(),
510                Self::Enemy => "[@]".red().bold(),
511            }
512        }
513    }
514
515    #[test]
516    fn draw_tilemap() -> io::Result<()> {
517        // create 5x5 tilemap:
518        let mut map: TileMap<Entity> = TileMap::new(5, 5);
519        // insert entities:
520        map.insert(Cell::new(3, 3), Entity::Enemy);
521        map.insert(Cell::new(1, 0), Entity::Hero);
522        // test in terminal raw mode:
523        enable_raw_mode()?;
524        // draw map to the raw stdout:
525        map.draw(&mut stdout())?;
526        // print using formatting:
527        println!("{map}");
528        // return terminal to the normal mode:
529        disable_raw_mode()?;
530
531        Ok(())
532    }
533}
534
535// 🦀!⭐!!!