core_rust/core/game/utils/
world.rs

1use super::errors::GameError;
2use super::unit::Unit;
3
4/// Cell terrain types in the grid.
5#[derive(Debug, Clone, Copy)]
6pub enum Terrain {
7    Grass,
8    Mountain,
9    Goal,
10}
11
12/// A single cell in the grid.
13#[derive(Debug, Clone, Copy)]
14pub struct Tile {
15    _x: usize,
16    _y: usize,
17    terrain: Terrain,
18    walkable: bool,
19    unit: Option<Unit>,
20}
21
22impl Tile {
23    /// Create a new tile at `(x, y)` with the given terrain.
24    pub fn new(x: usize, y: usize, terrain: Terrain) -> Self {
25        let walkable = !matches!(terrain, Terrain::Mountain);
26
27        Tile {
28            _x: x,
29            _y: y,
30            terrain,
31            walkable,
32            unit: None,
33        }
34    }
35
36    /// Whether this tile can be traversed.
37    pub fn is_walkable(self) -> bool {
38        self.walkable
39    }
40
41    /// Whether the tile currently contains the unit.
42    pub fn is_occupied(self) -> bool {
43        self.unit.is_some()
44    }
45
46    /// Place the unit on this tile.
47    pub fn place_unit(&mut self, unit: Unit) {
48        self.unit = Some(unit)
49    }
50
51    /// Remove the unit from this tile, if any.
52    pub fn remove_unit(&mut self) {
53        self.unit = None
54    }
55
56    /// Return the terrain type of this tile.
57    pub fn terrain(&self) -> Terrain {
58        self.terrain
59    }
60}
61
62/// Square grid map made up of `Tile`s and a single goal location.
63#[derive(Debug, Clone)]
64pub struct World {
65    pub size: usize,
66    tiles: Vec<Vec<Tile>>,
67    pub goal: (usize, usize),
68}
69
70impl World {
71    /// Build a `World` from a character grid.
72    pub fn new(world_vector: &[Vec<char>]) -> Result<Self, GameError> {
73        let rows = world_vector.len();
74        if rows == 0 {
75            return Err(GameError::WorldShapeError);
76        }
77
78        for row in world_vector.iter() {
79            if row.len() != rows {
80                return Err(GameError::WorldShapeError);
81            }
82        }
83
84        let mut tiles: Vec<Vec<Tile>> = Vec::with_capacity(rows);
85        let mut goal_location: Option<(usize, usize)> = None;
86
87        for (i, world_row) in world_vector.iter().enumerate() {
88            // if you want to enforce a square world: replace with `cols`
89            let mut tile_row: Vec<Tile> = Vec::with_capacity(world_row.len());
90
91            for (j, ch) in world_row.iter().enumerate() {
92                let terrain = match *ch {
93                    '.' => Terrain::Grass,
94                    'X' => Terrain::Mountain,
95                    'G' => {
96                        // mark goal
97                        if goal_location.is_some() {
98                            // two goals? fail
99                            return Err(GameError::DuplicateGoal);
100                        }
101                        goal_location = Some((i, j));
102                        Terrain::Goal
103                    }
104                    character => {
105                        // invalid character
106                        return Err(GameError::InvalidTileCharacter { character });
107                    }
108                };
109
110                tile_row.push(Tile::new(i, j, terrain));
111            }
112
113            tiles.push(tile_row);
114        }
115
116        // Ensure we found exactly one goal
117        let goal = goal_location.ok_or(GameError::MissingGoal)?;
118
119        Ok(World {
120            size: rows,
121            tiles,
122            goal,
123        })
124    }
125
126    /// Pretty-print the world with coordinates and grid lines.
127    pub fn print(&self) {
128        let cell_width = 5;
129        let size = self.size;
130
131        print!("     ");
132        for col in 0..size {
133            print!("{:^width$}", col, width = cell_width);
134        }
135        println!();
136
137        let hor = format!("  +{}+", "-".repeat(cell_width).repeat(size));
138        println!("{}", hor);
139
140        for (y, row) in self.tiles.iter().enumerate() {
141            print!("{:<2}|", y);
142
143            for tile in row {
144                let symbol = if let Some(unit) = &tile.unit {
145                    unit.get_symbol().to_string()
146                } else {
147                    match tile.terrain {
148                        Terrain::Grass => "·".into(),
149                        Terrain::Mountain => "X".into(),
150                        Terrain::Goal => "G".into(),
151                    }
152                };
153                print!("{:^width$}|", symbol, width = cell_width);
154            }
155            println!();
156
157            println!("{}", hor);
158        }
159    }
160
161    /// Find the unit’s current coordinates by scanning the grid.
162    fn get_unit_position(&mut self) -> Result<(usize, usize), GameError> {
163        for (y, row) in self.tiles.iter().enumerate() {
164            for (x, tile) in row.iter().enumerate() {
165                if tile.is_occupied() {
166                    return Ok((x, y));
167                }
168            }
169        }
170        Err(GameError::UnitNotFound)
171    }
172
173    /// Place a unit at its internal coordinates.
174    pub fn place_unit(&mut self, unit: Unit) {
175        let (x, y) = unit.get_position();
176        self.tiles[y][x].place_unit(unit);
177    }
178
179    /// Remove the unit from its current tile.
180    pub fn remove_unit(&mut self) -> Result<(), GameError> {
181        let (x, y) = match self.get_unit_position() {
182            Ok((x, y)) => (x, y),
183            Err(e) => return Err(e),
184        };
185
186        self.tiles[y][x].remove_unit();
187
188        Ok(())
189    }
190
191    /// Borrow a tile at `(x, y)`.
192    pub fn get_tile(&mut self, x: usize, y: usize) -> &Tile {
193        &self.tiles[y][x]
194    }
195}