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