rsnaker/graphics/sprites/
map.rs

1use crate::graphics::graphic_block::Position;
2use ratatui::layout::Rect;
3use ratatui::widgets::{Block, BorderType};
4
5/// A struct representing the game logic map.
6/// The map is resizable and represents the writeable area of the terminal, with a defined case size
7/// for each cell and a viewport for rendering the game logic world.
8///
9/// # Fields
10/// - `block`: A `Block` widget representing the map's visual border and title.
11/// - `CASE_SIZE`: The size of each cell (case) in pixels on the terminal screen.
12/// - `viewport`: The dimensions of the terminal viewport for rendering the map.
13#[derive(Clone)]
14pub struct Map<'a> {
15    block: Block<'a>,
16    case_size: u16,
17    viewport: Rect,
18}
19
20impl Map<'_> {
21    /// Creates a new `Map` instance with a given case size and viewport.
22    ///
23    /// # Parameters
24    /// - `case_size_in_px`: The size of each case (cell) on the map in pixels.
25    /// - `viewport`: The visible area of the terminal where the map will be rendered.
26    ///
27    /// # Returns
28    /// A new `Map` instance with the specified `case_size_in_px` and `viewport`.
29    ///
30    /// # Note
31    /// The map will have a double border and the title "Snake !".
32    #[must_use]
33    pub fn new<'a>(case_size_in_px: u16, viewport: Rect) -> Map<'a> {
34        Map {
35            block: Block::bordered().border_type(BorderType::Double),
36            //.title("Snake !"),
37            case_size: case_size_in_px,
38            viewport,
39        }
40    }
41    /// To resize the map viewport, not for in game logic use # unfair, but when after restarting it is OK
42    pub fn resize_to_terminal(&mut self, viewport: Rect) {
43        self.viewport = viewport;
44    }
45
46    /// Determines if a given position is outside the bounds of the map.
47    ///
48    /// # Parameters
49    /// - `Position { x, y }`: The position to check.
50    ///
51    /// # Returns
52    /// `true` if the position is outside the map's viewport, `false` otherwise.
53    ///
54    /// # Example
55    /// ```rust
56    /// use ratatui::layout::Rect;
57    /// use rsnaker::graphics::graphic_block::Position;
58    /// use rsnaker::graphics::sprites::map::Map;
59    /// let map = Map::new(10, Rect::new(0, 0, 100, 40));
60    /// let position = Position { x: 101, y: 20 };
61    /// assert!(map.out_of_map(&position));
62    /// ```
63    #[must_use]
64    pub fn out_of_map(&self, Position { x, y }: &Position) -> bool {
65        //+/- 2 to put out the bordered map (last case being ==== )
66        let x_max = self.viewport.width - (self.case_size + 2);
67        let y_max = self.viewport.height - 2;
68        let x_min = self.case_size;
69        let y_min = 1;
70        *x < x_min || *x > x_max || *y < y_min || *y > y_max
71    }
72
73    /// Reverses the position if it is outside the bounds of the map, effectively "wrapping" around
74    /// to the opposite edge of the map.
75    ///
76    /// # Parameters
77    /// - `Position { x, y }`: The position to check and possibly adjust.
78    ///
79    /// # Returns
80    /// A new `Position` where out-of-bounds coordinates are wrapped to the opposite edge of the map.
81    ///
82    /// # Example
83    /// ```rust
84    /// use ratatui::layout::Rect;
85    /// use rsnaker::graphics::graphic_block::Position;
86    /// use rsnaker::graphics::sprites::map::Map;
87    /// let map = Map::new(10, Rect::new(0, 0, 100, 40));
88    /// let position = Position { x: 101, y: 20 };
89    /// let new_position = map.out_of_map_reverse_position(&position);
90    /// assert_eq!(new_position.x, 10);  // Wrapped to the opposite side
91    /// ```
92    #[must_use]
93    pub fn out_of_map_reverse_position(&self, Position { x, y }: &Position) -> Position {
94        //*2 to put out the bordered map (last case being ==== )
95        let x_max = self.viewport.width - (self.case_size + 2);
96        let y_max = self.viewport.height - 2;
97        let x_min = self.case_size;
98        let y_min = 1;
99
100        //to keep the grid coherent
101        let x_remainder = (x_max - x_min) % self.case_size;
102        let x_max = x_max - x_remainder; // adjusted
103        // y automatic as %1
104
105        if *y > y_max {
106            Position { x: *x, y: y_min }
107        } else if *y < y_min {
108            Position { x: *x, y: y_max }
109        } else if *x > x_max {
110            Position { x: x_min, y: *y }
111        } else if *x < x_min {
112            Position { x: x_max, y: *y }
113        } else {
114            Position { x: *x, y: *y }
115        }
116    }
117    /// Returns the current viewport (the visible area) of the map.
118    ///
119    /// # Returns
120    /// A reference to the `Rect` representing the map's viewport.
121    #[must_use]
122    pub fn area(&self) -> &Rect {
123        &self.viewport
124    }
125
126    /// Returns the `Block` widget representing the map's border and title.
127    ///
128    /// # Returns
129    /// A reference to the `Block` widget.
130    #[must_use]
131    pub fn get_widget(&self) -> &Block {
132        &self.block
133    }
134
135    /// Returns the size of each case (cell) on the map.
136    ///
137    /// # Returns
138    /// The size of each case in pixels.
139    #[must_use]
140    pub fn get_case_size(&self) -> u16 {
141        self.case_size
142    }
143}