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".repeat(self.formatting.top_indent as usize))
258        )?;
259        for row in self.grid().rows() {
260            execute!(
261                stdout,
262                Print("\n".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"))?;
273        }
274        execute!(
275            stdout,
276            Print("\n".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".repeat(self.formatting.top_indent as usize))?;
308        for row in self.grid().rows() {
309            write!(f, "{}", "\n".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            writeln!(f)?;
316        }
317        write!(f, "{}", "\n".repeat(self.formatting.bottom_indent as usize))?;
318        Ok(())
319    }
320}
321
322impl<T> From<Grid> for TileMap<T>
323where
324    T: Tile + Default,
325{
326    /// Creates new empty `TileMap<T>` from the specified `Grid`
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use cli_tilemap::{Tile, TileMap};
332    /// use crossterm::style::{Stylize, StyledContent};
333    /// use grid_math::{Cell, Grid};
334    ///
335    /// #[derive(Default)]
336    /// struct Empty;
337    ///
338    /// impl Tile for Empty {
339    ///     fn tile(&self) -> StyledContent<&'static str> {
340    ///         "[-]".dark_grey().bold()
341    ///     }
342    /// }
343    ///
344    /// let cells = (Cell::new(2, 2), Cell::new(5, 5));
345    /// let grid = Grid::from(cells);
346    /// let map: TileMap<Empty> = TileMap::from(grid);
347    /// assert_eq!(map.grid(), grid);
348    /// ```
349    fn from(grid: Grid) -> Self {
350        Self {
351            formatting: Formatting::default(),
352            gridmap: GridMap::from(grid),
353        }
354    }
355}
356
357impl<T> From<GridMap<T>> for TileMap<T>
358where
359    T: Tile + Default,
360{
361    /// Creates `TileMap<T>` from the existing `GridMap<T>` where `T`: `Tile` + `Default`
362    ///
363    /// # Examples
364    ///
365    /// ```
366    /// use cli_tilemap::{Tile, TileMap};
367    /// use crossterm::style::{Stylize, StyledContent};
368    /// use grid_math::{Cell, GridMap};
369    ///
370    /// #[derive(Debug, Default, PartialEq, Eq)]
371    /// struct Empty;
372    ///
373    /// impl Tile for Empty {
374    ///     fn tile(&self) -> StyledContent<&'static str> {
375    ///         "[-]".dark_grey().bold()
376    ///     }
377    /// }
378    ///
379    /// let mut gridmap: GridMap<Empty> = GridMap::new(5, 5);
380    /// let target = Cell::new(1, 2);
381    /// gridmap.insert(target, Empty);
382    /// let map: TileMap<Empty> = TileMap::from(gridmap);
383    /// assert_eq!(map.get(&target), Some(&Empty));
384    /// ```
385    fn from(gridmap: GridMap<T>) -> Self {
386        Self {
387            formatting: Formatting::default(),
388            gridmap,
389        }
390    }
391}
392
393impl<T> From<(Grid, HashMap<Cell, T>)> for TileMap<T>
394where
395    T: Tile + Default,
396{
397    /// Creates `TileMap<T>` from the existing `HashMap<Cell, T>` and the given `Grid`
398    ///
399    /// # Panics
400    /// Panics if the given `HashMap<Cell, T>` contains `Cell`s that are not within the given `Grid`
401    /// This panic is a part of `grid-math` crate current state, error handling may change in the future
402    ///
403    /// # Examples
404    ///
405    /// ```
406    /// use cli_tilemap::{Tile, TileMap};
407    /// use crossterm::style::{Stylize, StyledContent};
408    /// use grid_math::{Cell, Grid};
409    /// use std::collections::HashMap;
410    ///
411    /// #[derive(Debug, Default, PartialEq, Eq)]
412    /// struct Empty;
413    ///
414    /// impl Tile for Empty {
415    ///     fn tile(&self) -> StyledContent<&'static str> {
416    ///         "[-]".dark_grey().bold()
417    ///     }
418    /// }
419    ///
420    /// let grid = Grid::new(5, 5);
421    /// let mut hashmap: HashMap<Cell, Empty> = HashMap::new();
422    /// let target = Cell::new(1, 2);
423    /// hashmap.insert(target, Empty);
424    /// let map: TileMap<Empty> = TileMap::from((grid, hashmap));
425    /// assert_eq!(map.get(&target), Some(&Empty));
426    /// ```
427    ///
428    /// ```should_panic
429    /// use cli_tilemap::{Tile, TileMap};
430    /// use crossterm::style::{Stylize, StyledContent};
431    /// use grid_math::{Cell, Grid};
432    /// use std::collections::HashMap;
433    ///
434    /// #[derive(Debug, Default, PartialEq, Eq)]
435    /// struct Empty;
436    ///
437    /// impl Tile for Empty {
438    ///     fn tile(&self) -> StyledContent<&'static str> {
439    ///         "[-]".dark_grey().bold()
440    ///     }
441    /// }
442    ///
443    /// let grid = Grid::new(5, 5);
444    /// let mut hashmap: HashMap<Cell, Empty> = HashMap::new();
445    /// let target = Cell::new(7, 1);
446    /// hashmap.insert(target, Empty);
447    /// let map: TileMap<Empty> = TileMap::from((grid, hashmap)); // panic!
448    /// ```
449    fn from(data: (Grid, HashMap<Cell, T>)) -> Self {
450        Self {
451            formatting: Formatting::default(),
452            gridmap: GridMap::from(data),
453        }
454    }
455}
456
457/// Implements `Deref` trait for `TileMap<T>`, to return ref to the inner `GridMap<T>`
458///
459/// For more info, visit `grid-math` crate docs
460///
461impl<T> Deref for TileMap<T>
462where
463    T: Tile + Default,
464{
465    type Target = GridMap<T>;
466    fn deref(&self) -> &Self::Target {
467        &self.gridmap
468    }
469}
470
471/// Implements `Deref` trait for `TileMap<T>`, to return ref to the inner `GridMap<T>`
472///
473/// For more info, visit `grid-math` crate docs
474///
475impl<T> DerefMut for TileMap<T>
476where
477    T: Tile + Default,
478{
479    fn deref_mut(&mut self) -> &mut Self::Target {
480        &mut self.gridmap
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487    use crossterm::style::Stylize;
488    use std::io::stdout;
489
490    // declare Entity enum
491    #[derive(Default, Debug)]
492    enum Entity {
493        Enemy,
494        Hero,
495        #[default]
496        Air,
497    }
498
499    // represent Entity as tile
500    impl Tile for Entity {
501        fn tile(&self) -> StyledContent<&'static str> {
502            match self {
503                Self::Air => "[-]".dark_grey().bold(),
504                Self::Hero => "[&]".green().bold(),
505                Self::Enemy => "[@]".red().bold(),
506            }
507        }
508    }
509
510    #[test]
511    fn draw_tilemap() {
512        // create 5x5 tilemap:
513        let mut map: TileMap<Entity> = TileMap::new(5, 5);
514        // insert entities:
515        map.insert(Cell::new(3, 3), Entity::Enemy);
516        map.insert(Cell::new(1, 0), Entity::Hero);
517        // draw map to the raw stdout:
518        map.draw(&mut stdout()).expect("should draw!");
519    }
520}
521
522// 🦀!⭐!!!