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// 🦀!⭐!!!