• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Xevion / Pac-Man / 17012447928

16 Aug 2025 08:12PM UTC coverage: 38.85% (-12.3%) from 51.196%
17012447928

Pull #3

github

Xevion
chore: add cargo checks to pre-commit
Pull Request #3: ECS Refactor

161 of 1172 new or added lines in 23 files covered. (13.74%)

9 existing lines in 4 files now uncovered.

777 of 2000 relevant lines covered (38.85%)

101.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.13
/src/map/builder.rs
1
//! Map construction and building functionality.
2
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE};
3
use crate::map::direction::Direction;
4
use crate::map::graph::{Graph, Node, TraversalFlags};
5
use crate::map::parser::MapTileParser;
6
use crate::systems::movement::NodeId;
7
use bevy_ecs::resource::Resource;
8
use glam::{IVec2, Vec2};
9
use std::collections::{HashMap, VecDeque};
10
use tracing::debug;
11

12
use crate::error::{GameResult, MapError};
13

14
/// The starting positions of the entities in the game.
15
pub struct NodePositions {
16
    pub pacman: NodeId,
17
    pub blinky: NodeId,
18
    pub pinky: NodeId,
19
    pub inky: NodeId,
20
    pub clyde: NodeId,
21
}
22

23
/// The main map structure containing the game board and navigation graph.
24
#[derive(Resource)]
25
pub struct Map {
26
    /// The node map for entity movement.
27
    pub graph: Graph,
28
    /// A mapping from grid positions to node IDs.
29
    pub grid_to_node: HashMap<IVec2, NodeId>,
30
    /// A mapping of the starting positions of the entities.
31
    pub start_positions: NodePositions,
32
    /// The raw tile data for the map.
33
    tiles: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize],
34
}
35

36
impl Map {
37
    /// Creates a new `Map` instance from a raw board layout.
38
    ///
39
    /// This constructor initializes the map tiles based on the provided character layout
40
    /// and then generates a navigation graph from the walkable areas.
41
    ///
42
    /// # Panics
43
    ///
44
    /// This function will panic if the board layout contains unknown characters or if
45
    /// the house door is not defined by exactly two '=' characters.
46
    pub fn new(raw_board: [&str; BOARD_CELL_SIZE.y as usize]) -> GameResult<Map> {
2✔
47
        let parsed_map = MapTileParser::parse_board(raw_board)?;
2✔
48

49
        let map = parsed_map.tiles;
2✔
50
        let house_door = parsed_map.house_door;
2✔
51
        let tunnel_ends = parsed_map.tunnel_ends;
2✔
52

2✔
53
        let mut graph = Graph::new();
2✔
54
        let mut grid_to_node = HashMap::new();
2✔
55

2✔
56
        let cell_offset = Vec2::splat(CELL_SIZE as f32 / 2.0);
2✔
57

58
        // Find a starting point for the graph generation, preferably Pac-Man's position.
59
        let start_pos = parsed_map
2✔
60
            .pacman_start
2✔
61
            .ok_or_else(|| MapError::InvalidConfig("Pac-Man's starting position not found".to_string()))?;
2✔
62

63
        // Add the starting position to the graph/queue
64
        let mut queue = VecDeque::new();
2✔
65
        queue.push_back(start_pos);
2✔
66
        let pos = Vec2::new(
2✔
67
            (start_pos.x * CELL_SIZE as i32) as f32,
2✔
68
            (start_pos.y * CELL_SIZE as i32) as f32,
2✔
69
        ) + cell_offset;
2✔
70
        let node_id = graph.add_node(Node { position: pos });
2✔
71
        grid_to_node.insert(start_pos, node_id);
2✔
72

73
        // Iterate over the queue, adding nodes to the graph and connecting them to their neighbors
74
        while let Some(source_position) = queue.pop_front() {
598✔
75
            for dir in Direction::DIRECTIONS {
2,980✔
76
                let new_position = source_position + dir.as_ivec2();
2,384✔
77

2,384✔
78
                // Skip if the new position is out of bounds
2,384✔
79
                if new_position.x < 0
2,384✔
80
                    || new_position.x >= BOARD_CELL_SIZE.x as i32
2,382✔
81
                    || new_position.y < 0
2,380✔
82
                    || new_position.y >= BOARD_CELL_SIZE.y as i32
2,380✔
83
                {
84
                    continue;
4✔
85
                }
2,380✔
86

2,380✔
87
                // Skip if the new position is already in the graph
2,380✔
88
                if grid_to_node.contains_key(&new_position) {
2,380✔
89
                    continue;
670✔
90
                }
1,710✔
91

92
                // Skip if the new position is not a walkable tile
93
                if matches!(
1,116✔
94
                    map[new_position.x as usize][new_position.y as usize],
1,710✔
95
                    MapTile::Pellet | MapTile::PowerPellet | MapTile::Empty | MapTile::Tunnel
96
                ) {
97
                    // Add the new position to the graph/queue
98
                    let pos = Vec2::new(
594✔
99
                        (new_position.x * CELL_SIZE as i32) as f32,
594✔
100
                        (new_position.y * CELL_SIZE as i32) as f32,
594✔
101
                    ) + cell_offset;
594✔
102
                    let new_node_id = graph.add_node(Node { position: pos });
594✔
103
                    grid_to_node.insert(new_position, new_node_id);
594✔
104
                    queue.push_back(new_position);
594✔
105

594✔
106
                    // Connect the new node to the source node
594✔
107
                    let source_node_id = grid_to_node
594✔
108
                        .get(&source_position)
594✔
109
                        .unwrap_or_else(|| panic!("Source node not found for {source_position}"));
594✔
110

594✔
111
                    // Connect the new node to the source node
594✔
112
                    graph
594✔
113
                        .connect(*source_node_id, new_node_id, false, None, dir)
594✔
114
                        .map_err(|e| MapError::InvalidConfig(format!("Failed to add edge: {e}")))?;
594✔
115
                }
1,116✔
116
            }
117
        }
118

119
        // While most nodes are already connected to their neighbors, some may not be, so we need to connect them
120
        for (grid_pos, &node_id) in &grid_to_node {
598✔
121
            for dir in Direction::DIRECTIONS {
2,980✔
122
                // If the node doesn't have an edge in this direction, look for a neighbor in that direction
123
                if graph.adjacency_list[node_id].get(dir).is_none() {
2,384✔
124
                    let neighbor = grid_pos + dir.as_ivec2();
1,158✔
125
                    // If the neighbor exists, connect the node to it
126
                    if let Some(&neighbor_id) = grid_to_node.get(&neighbor) {
1,158✔
127
                        graph
38✔
128
                            .connect(node_id, neighbor_id, false, None, dir)
38✔
129
                            .map_err(|e| MapError::InvalidConfig(format!("Failed to add edge: {e}")))?;
38✔
130
                    }
1,120✔
131
                }
1,226✔
132
            }
133
        }
134

135
        // Build house structure
136
        let (house_entrance_node_id, left_center_node_id, center_center_node_id, right_center_node_id) =
2✔
137
            Self::build_house(&mut graph, &grid_to_node, &house_door)?;
2✔
138

139
        let start_positions = NodePositions {
2✔
140
            pacman: grid_to_node[&start_pos],
2✔
141
            blinky: house_entrance_node_id,
2✔
142
            pinky: left_center_node_id,
2✔
143
            inky: right_center_node_id,
2✔
144
            clyde: center_center_node_id,
2✔
145
        };
2✔
146

2✔
147
        // Build tunnel connections
2✔
148
        Self::build_tunnels(&mut graph, &grid_to_node, &tunnel_ends)?;
2✔
149

150
        Ok(Map {
2✔
151
            graph,
2✔
152
            grid_to_node,
2✔
153
            start_positions,
2✔
154
            tiles: map,
2✔
155
        })
2✔
156
    }
2✔
157

NEW
158
    pub fn iter_nodes(&self) -> impl Iterator<Item = (&NodeId, &MapTile)> {
×
NEW
159
        self.grid_to_node.iter().map(move |(pos, node_id)| {
×
NEW
160
            let tile = &self.tiles[pos.x as usize][pos.y as usize];
×
NEW
161
            (node_id, tile)
×
NEW
162
        })
×
UNCOV
163
    }
×
164

165
    /// Builds the house structure in the graph.
166
    fn build_house(
2✔
167
        graph: &mut Graph,
2✔
168
        grid_to_node: &HashMap<IVec2, NodeId>,
2✔
169
        house_door: &[Option<IVec2>; 2],
2✔
170
    ) -> GameResult<(usize, usize, usize, usize)> {
2✔
171
        // Calculate the position of the house entrance node
172
        let (house_entrance_node_id, house_entrance_node_position) = {
2✔
173
            // Translate the grid positions to the actual node ids
174
            let left_node = grid_to_node
2✔
175
                .get(
2✔
176
                    &(house_door[0]
2✔
177
                        .ok_or_else(|| MapError::InvalidConfig("First house door position not acquired".to_string()))?
2✔
178
                        + Direction::Left.as_ivec2()),
2✔
179
                )
2✔
180
                .ok_or_else(|| MapError::InvalidConfig("Left house door node not found".to_string()))?;
2✔
181
            let right_node = grid_to_node
2✔
182
                .get(
2✔
183
                    &(house_door[1]
2✔
184
                        .ok_or_else(|| MapError::InvalidConfig("Second house door position not acquired".to_string()))?
2✔
185
                        + Direction::Right.as_ivec2()),
2✔
186
                )
2✔
187
                .ok_or_else(|| MapError::InvalidConfig("Right house door node not found".to_string()))?;
2✔
188

189
            // Calculate the position of the house node
190
            let (node_id, node_position) = {
2✔
191
                let left_pos = graph.get_node(*left_node).ok_or(MapError::NodeNotFound(*left_node))?.position;
2✔
192
                let right_pos = graph
2✔
193
                    .get_node(*right_node)
2✔
194
                    .ok_or(MapError::NodeNotFound(*right_node))?
2✔
195
                    .position;
196
                let house_node = graph.add_node(Node {
2✔
197
                    position: left_pos.lerp(right_pos, 0.5),
2✔
198
                });
2✔
199
                (house_node, left_pos.lerp(right_pos, 0.5))
2✔
200
            };
2✔
201

2✔
202
            // Connect the house door to the left and right nodes
2✔
203
            graph
2✔
204
                .connect(node_id, *left_node, true, None, Direction::Left)
2✔
205
                .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house door to left node: {e}")))?;
2✔
206
            graph
2✔
207
                .connect(node_id, *right_node, true, None, Direction::Right)
2✔
208
                .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house door to right node: {e}")))?;
2✔
209

210
            (node_id, node_position)
2✔
211
        };
2✔
212

2✔
213
        // A helper function to help create the various 'lines' of nodes within the house
2✔
214
        let create_house_line = |graph: &mut Graph, center_pos: Vec2| -> GameResult<(NodeId, NodeId)> {
6✔
215
            // Place the nodes at, above, and below the center position
6✔
216
            let center_node_id = graph.add_node(Node { position: center_pos });
6✔
217
            let top_node_id = graph.add_node(Node {
6✔
218
                position: center_pos + (Direction::Up.as_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(),
6✔
219
            });
6✔
220
            let bottom_node_id = graph.add_node(Node {
6✔
221
                position: center_pos + (Direction::Down.as_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(),
6✔
222
            });
6✔
223

6✔
224
            // Connect the center node to the top and bottom nodes
6✔
225
            graph
6✔
226
                .connect(center_node_id, top_node_id, false, None, Direction::Up)
6✔
227
                .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house line to top node: {e}")))?;
6✔
228
            graph
6✔
229
                .connect(center_node_id, bottom_node_id, false, None, Direction::Down)
6✔
230
                .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house line to bottom node: {e}")))?;
6✔
231

232
            Ok((center_node_id, top_node_id))
6✔
233
        };
6✔
234

235
        // Calculate the position of the center line's center node
236
        let center_line_center_position =
2✔
237
            house_entrance_node_position + (Direction::Down.as_ivec2() * (3 * CELL_SIZE as i32)).as_vec2();
2✔
238

239
        // Create the center line
240
        let (center_center_node_id, center_top_node_id) = create_house_line(graph, center_line_center_position)?;
2✔
241

242
        // Create a ghost-only, two-way connection for the house door.
243
        // This prevents Pac-Man from entering or exiting through the door.
244
        graph
2✔
245
            .add_edge(
2✔
246
                house_entrance_node_id,
2✔
247
                center_top_node_id,
2✔
248
                false,
2✔
249
                None,
2✔
250
                Direction::Down,
2✔
251
                TraversalFlags::GHOST,
2✔
252
            )
2✔
253
            .map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only entrance to house: {e}")))?;
2✔
254

255
        graph
2✔
256
            .add_edge(
2✔
257
                center_top_node_id,
2✔
258
                house_entrance_node_id,
2✔
259
                false,
2✔
260
                None,
2✔
261
                Direction::Up,
2✔
262
                TraversalFlags::GHOST,
2✔
263
            )
2✔
264
            .map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only exit from house: {e}")))?;
2✔
265

266
        // Create the left line
267
        let (left_center_node_id, _) = create_house_line(
2✔
268
            graph,
2✔
269
            center_line_center_position + (Direction::Left.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
2✔
270
        )?;
2✔
271

272
        // Create the right line
273
        let (right_center_node_id, _) = create_house_line(
2✔
274
            graph,
2✔
275
            center_line_center_position + (Direction::Right.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
2✔
276
        )?;
2✔
277

278
        debug!("Left center node id: {left_center_node_id}");
2✔
279

280
        // Connect the center line to the left and right lines
281
        graph
2✔
282
            .connect(center_center_node_id, left_center_node_id, false, None, Direction::Left)
2✔
283
            .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house entrance to left top line: {e}")))?;
2✔
284

285
        graph
2✔
286
            .connect(center_center_node_id, right_center_node_id, false, None, Direction::Right)
2✔
287
            .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house entrance to right top line: {e}")))?;
2✔
288

289
        debug!("House entrance node id: {house_entrance_node_id}");
2✔
290

291
        Ok((
2✔
292
            house_entrance_node_id,
2✔
293
            left_center_node_id,
2✔
294
            center_center_node_id,
2✔
295
            right_center_node_id,
2✔
296
        ))
2✔
297
    }
2✔
298

299
    /// Builds the tunnel connections in the graph.
300
    fn build_tunnels(
2✔
301
        graph: &mut Graph,
2✔
302
        grid_to_node: &HashMap<IVec2, NodeId>,
2✔
303
        tunnel_ends: &[Option<IVec2>; 2],
2✔
304
    ) -> GameResult<()> {
2✔
305
        // Create the hidden tunnel nodes
306
        let left_tunnel_hidden_node_id = {
2✔
307
            let left_tunnel_entrance_node_id =
2✔
308
                grid_to_node[&tunnel_ends[0].ok_or_else(|| MapError::InvalidConfig("Left tunnel end not found".to_string()))?];
2✔
309
            let left_tunnel_entrance_node = graph
2✔
310
                .get_node(left_tunnel_entrance_node_id)
2✔
311
                .ok_or_else(|| MapError::InvalidConfig("Left tunnel entrance node not found".to_string()))?;
2✔
312

313
            graph
2✔
314
                .add_connected(
2✔
315
                    left_tunnel_entrance_node_id,
2✔
316
                    Direction::Left,
2✔
317
                    Node {
2✔
318
                        position: left_tunnel_entrance_node.position
2✔
319
                            + (Direction::Left.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
2✔
320
                    },
2✔
321
                )
2✔
322
                .map_err(|e| {
2✔
323
                    MapError::InvalidConfig(format!(
×
324
                        "Failed to connect left tunnel entrance to left tunnel hidden node: {}",
×
325
                        e
×
326
                    ))
×
327
                })?
2✔
328
        };
329

330
        // Create the right tunnel nodes
331
        let right_tunnel_hidden_node_id = {
2✔
332
            let right_tunnel_entrance_node_id =
2✔
333
                grid_to_node[&tunnel_ends[1].ok_or_else(|| MapError::InvalidConfig("Right tunnel end not found".to_string()))?];
2✔
334
            let right_tunnel_entrance_node = graph
2✔
335
                .get_node(right_tunnel_entrance_node_id)
2✔
336
                .ok_or_else(|| MapError::InvalidConfig("Right tunnel entrance node not found".to_string()))?;
2✔
337

338
            graph
2✔
339
                .add_connected(
2✔
340
                    right_tunnel_entrance_node_id,
2✔
341
                    Direction::Right,
2✔
342
                    Node {
2✔
343
                        position: right_tunnel_entrance_node.position
2✔
344
                            + (Direction::Right.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
2✔
345
                    },
2✔
346
                )
2✔
347
                .map_err(|e| {
2✔
348
                    MapError::InvalidConfig(format!(
×
349
                        "Failed to connect right tunnel entrance to right tunnel hidden node: {}",
×
350
                        e
×
351
                    ))
×
352
                })?
2✔
353
        };
354

355
        // Connect the left tunnel hidden node to the right tunnel hidden node
356
        graph
2✔
357
            .connect(
2✔
358
                left_tunnel_hidden_node_id,
2✔
359
                right_tunnel_hidden_node_id,
2✔
360
                false,
2✔
361
                Some(0.0),
2✔
362
                Direction::Left,
2✔
363
            )
2✔
364
            .map_err(|e| {
2✔
365
                MapError::InvalidConfig(format!(
×
366
                    "Failed to connect left tunnel hidden node to right tunnel hidden node: {}",
×
367
                    e
×
368
                ))
×
369
            })?;
2✔
370

371
        Ok(())
2✔
372
    }
2✔
373
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc