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

Xevion / Pac-Man / 17561013916

08 Sep 2025 06:50PM UTC coverage: 82.576% (+1.3%) from 81.258%
17561013916

push

github

Xevion
feat: improve tracing logs application-wide

67 of 94 new or added lines in 9 files covered. (71.28%)

8 existing lines in 4 files now uncovered.

1962 of 2376 relevant lines covered (82.58%)

26957.3 hits per line

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

98.26
/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::{I8Vec2, IVec2, Vec2};
9
use std::collections::{HashMap, VecDeque};
10
use tracing::debug;
11

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

14
/// Predefined spawn locations for all game entities within the navigation graph.
15
///
16
/// These positions are determined during map parsing and graph construction.
17
pub struct NodePositions {
18
    /// Pac-Man's starting position in the lower section of the maze
19
    pub pacman: NodeId,
20
    /// Blinky starts at the ghost house entrance
21
    pub blinky: NodeId,
22
    /// Pinky starts in the left area of the ghost house
23
    pub pinky: NodeId,
24
    /// Inky starts in the right area of the ghost house
25
    pub inky: NodeId,
26
    /// Clyde starts in the center of the ghost house
27
    pub clyde: NodeId,
28
}
29

30
/// Complete maze representation combining visual layout with navigation pathfinding.
31
///
32
/// Transforms the ASCII board layout into a fully connected navigation graph
33
/// while preserving tile-based collision and rendering data. The graph enables
34
/// smooth entity movement with proper pathfinding, while the grid mapping allows
35
/// efficient spatial queries and debug visualization.
36
#[derive(Resource)]
37
pub struct Map {
38
    /// Connected graph of navigable positions.
39
    pub graph: Graph,
40
    /// Bidirectional mapping between 2D grid coordinates and graph node indices.
41
    pub grid_to_node: HashMap<I8Vec2, NodeId>,
42
    /// Predetermined spawn locations for all game entities
43
    pub start_positions: NodePositions,
44
    /// 2D array of tile types for collision detection and rendering
45
    tiles: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize],
46
}
47

48
impl Map {
49
    /// Creates a new `Map` instance from a raw board layout.
50
    ///
51
    /// This constructor initializes the map tiles based on the provided character layout
52
    /// and then generates a navigation graph from the walkable areas.
53
    ///
54
    /// # Panics
55
    ///
56
    /// This function will panic if the board layout contains unknown characters or if
57
    /// the house door is not defined by exactly two '=' characters.
58
    pub fn new(raw_board: [&str; BOARD_CELL_SIZE.y as usize]) -> GameResult<Map> {
34✔
59
        debug!("Starting map construction from character layout");
34✔
60
        let parsed_map = MapTileParser::parse_board(raw_board)?;
34✔
61

62
        let map = parsed_map.tiles;
34✔
63
        let house_door = parsed_map.house_door;
34✔
64
        let tunnel_ends = parsed_map.tunnel_ends;
34✔
65
        debug!(
34✔
NEW
66
            house_door_count = house_door.len(),
×
NEW
67
            tunnel_ends_count = tunnel_ends.len(),
×
NEW
68
            "Parsed map special locations"
×
69
        );
70

71
        let mut graph = Graph::new();
34✔
72
        let mut grid_to_node = HashMap::new();
34✔
73

74
        let cell_offset = Vec2::splat(CELL_SIZE as f32 / 2.0);
34✔
75

76
        // Find a starting point for the graph generation, preferably Pac-Man's position.
77
        let start_pos = parsed_map
34✔
78
            .pacman_start
34✔
79
            .ok_or_else(|| MapError::InvalidConfig("Pac-Man's starting position not found".to_string()))?;
34✔
80

81
        // Add the starting position to the graph/queue
82
        let mut queue = VecDeque::new();
34✔
83
        queue.push_back(start_pos);
34✔
84
        let pos = Vec2::new(
34✔
85
            (start_pos.x as i32 * CELL_SIZE as i32) as f32,
34✔
86
            (start_pos.y as i32 * CELL_SIZE as i32) as f32,
34✔
87
        ) + cell_offset;
34✔
88
        let node_id = graph.add_node(Node { position: pos });
34✔
89
        grid_to_node.insert(start_pos, node_id);
34✔
90

91
        // Iterate over the queue, adding nodes to the graph and connecting them to their neighbors
92
        while let Some(source_position) = queue.pop_front() {
10,166✔
93
            for dir in Direction::DIRECTIONS {
50,660✔
94
                let new_position = source_position + dir.as_ivec2();
40,528✔
95

96
                // Skip if the new position is out of bounds
97
                if new_position.x < 0
40,528✔
98
                    || new_position.x as i32 >= BOARD_CELL_SIZE.x as i32
40,494✔
99
                    || new_position.y < 0
40,460✔
100
                    || new_position.y as i32 >= BOARD_CELL_SIZE.y as i32
40,460✔
101
                {
102
                    continue;
68✔
103
                }
40,460✔
104

105
                // Skip if the new position is already in the graph
106
                if grid_to_node.contains_key(&new_position) {
40,460✔
107
                    continue;
11,390✔
108
                }
29,070✔
109

110
                // Skip if the new position is not a walkable tile
111
                if matches!(
18,972✔
112
                    map[new_position.x as usize][new_position.y as usize],
29,070✔
113
                    MapTile::Pellet | MapTile::PowerPellet | MapTile::Empty | MapTile::Tunnel
114
                ) {
115
                    // Add the new position to the graph/queue
116
                    let pos = Vec2::new(
10,098✔
117
                        (new_position.x as i32 * CELL_SIZE as i32) as f32,
10,098✔
118
                        (new_position.y as i32 * CELL_SIZE as i32) as f32,
10,098✔
119
                    ) + cell_offset;
10,098✔
120
                    let new_node_id = graph.add_node(Node { position: pos });
10,098✔
121
                    grid_to_node.insert(new_position, new_node_id);
10,098✔
122
                    queue.push_back(new_position);
10,098✔
123

124
                    // Connect the new node to the source node
125
                    let source_node_id = grid_to_node
10,098✔
126
                        .get(&source_position)
10,098✔
127
                        .unwrap_or_else(|| panic!("Source node not found for {source_position}"));
10,098✔
128

129
                    // Connect the new node to the source node
130
                    graph
10,098✔
131
                        .connect(*source_node_id, new_node_id, false, None, dir)
10,098✔
132
                        .map_err(|e| MapError::InvalidConfig(format!("Failed to add edge: {e}")))?;
10,098✔
133
                }
18,972✔
134
            }
135
        }
136

137
        // While most nodes are already connected to their neighbors, some may not be, so we need to connect them
138
        for (grid_pos, &node_id) in &grid_to_node {
10,166✔
139
            for dir in Direction::DIRECTIONS {
50,660✔
140
                // If the node doesn't have an edge in this direction, look for a neighbor in that direction
141
                if graph.adjacency_list[node_id as usize].get(dir).is_none() {
40,528✔
142
                    let neighbor = grid_pos + dir.as_ivec2();
19,686✔
143
                    // If the neighbor exists, connect the node to it
144
                    if let Some(&neighbor_id) = grid_to_node.get(&neighbor) {
19,686✔
145
                        graph
646✔
146
                            .connect(node_id, neighbor_id, false, None, dir)
646✔
147
                            .map_err(|e| MapError::InvalidConfig(format!("Failed to add edge: {e}")))?;
646✔
148
                    }
19,040✔
149
                }
20,842✔
150
            }
151
        }
152

153
        // Build house structure
154
        let (house_entrance_node_id, left_center_node_id, center_center_node_id, right_center_node_id) =
34✔
155
            Self::build_house(&mut graph, &grid_to_node, &house_door)?;
34✔
156

157
        let start_positions = NodePositions {
34✔
158
            pacman: grid_to_node[&start_pos],
34✔
159
            blinky: house_entrance_node_id,
34✔
160
            pinky: left_center_node_id,
34✔
161
            inky: right_center_node_id,
34✔
162
            clyde: center_center_node_id,
34✔
163
        };
34✔
164

165
        // Build tunnel connections
166
        debug!("Building tunnel connections");
34✔
167
        Self::build_tunnels(&mut graph, &grid_to_node, &tunnel_ends)?;
34✔
168

169
        debug!(node_count = graph.nodes().count(), "Map construction completed successfully");
34✔
170
        Ok(Map {
34✔
171
            graph,
34✔
172
            grid_to_node,
34✔
173
            start_positions,
34✔
174
            tiles: map,
34✔
175
        })
34✔
176
    }
34✔
177

178
    pub fn iter_nodes(&self) -> impl Iterator<Item = (&NodeId, &MapTile)> {
2✔
179
        self.grid_to_node.iter().map(move |(pos, node_id)| {
596✔
180
            let tile = &self.tiles[pos.x as usize][pos.y as usize];
596✔
181
            (node_id, tile)
596✔
182
        })
596✔
183
    }
2✔
184

185
    /// Returns the `MapTile` at a given node id.
186
    pub fn tile_at_node(&self, node_id: NodeId) -> Option<MapTile> {
2,101✔
187
        // reverse lookup: node -> grid
188
        for (grid_pos, id) in &self.grid_to_node {
157,707✔
189
            if *id == node_id {
157,707✔
190
                return Some(self.tiles[grid_pos.x as usize][grid_pos.y as usize]);
2,101✔
191
            }
155,606✔
192
        }
193
        None
×
194
    }
2,101✔
195

196
    /// Constructs the ghost house area with restricted access and internal navigation.
197
    ///
198
    /// Creates a multi-level ghost house with entrance control, internal movement
199
    /// areas, and starting positions for each ghost. The house entrance uses
200
    /// ghost-only traversal flags to prevent Pac-Man from entering while allowing
201
    /// ghosts to exit. Internal nodes are arranged in vertical lines to provide
202
    /// distinct starting areas for each ghost character.
203
    ///
204
    /// # Returns
205
    ///
206
    /// Tuple of node IDs: (house_entrance, left_center, center_center, right_center)
207
    /// representing the four key positions within the ghost house structure.
208
    fn build_house(
34✔
209
        graph: &mut Graph,
34✔
210
        grid_to_node: &HashMap<I8Vec2, NodeId>,
34✔
211
        house_door: &[Option<I8Vec2>; 2],
34✔
212
    ) -> GameResult<(NodeId, NodeId, NodeId, NodeId)> {
34✔
213
        // Calculate the position of the house entrance node
214
        let (house_entrance_node_id, house_entrance_node_position) = {
34✔
215
            // Translate the grid positions to the actual node ids
216
            let left_node = grid_to_node
34✔
217
                .get(
34✔
218
                    &(house_door[0]
34✔
219
                        .ok_or_else(|| MapError::InvalidConfig("First house door position not acquired".to_string()))?
34✔
220
                        + Direction::Left.as_ivec2()),
34✔
221
                )
222
                .ok_or_else(|| MapError::InvalidConfig("Left house door node not found".to_string()))?;
34✔
223
            let right_node = grid_to_node
34✔
224
                .get(
34✔
225
                    &(house_door[1]
34✔
226
                        .ok_or_else(|| MapError::InvalidConfig("Second house door position not acquired".to_string()))?
34✔
227
                        + Direction::Right.as_ivec2()),
34✔
228
                )
229
                .ok_or_else(|| MapError::InvalidConfig("Right house door node not found".to_string()))?;
34✔
230

231
            // Calculate the position of the house node
232
            let (node_id, node_position) = {
34✔
233
                let left_pos = graph
34✔
234
                    .get_node(*left_node)
34✔
235
                    .ok_or(MapError::NodeNotFound(*left_node as usize))?
34✔
236
                    .position;
237
                let right_pos = graph
34✔
238
                    .get_node(*right_node)
34✔
239
                    .ok_or(MapError::NodeNotFound(*right_node as usize))?
34✔
240
                    .position;
241
                let house_node = graph.add_node(Node {
34✔
242
                    position: left_pos.lerp(right_pos, 0.5),
34✔
243
                });
34✔
244
                (house_node, left_pos.lerp(right_pos, 0.5))
34✔
245
            };
246

247
            // Connect the house door to the left and right nodes
248
            graph
34✔
249
                .connect(node_id, *left_node, true, None, Direction::Left)
34✔
250
                .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house door to left node: {e}")))?;
34✔
251
            graph
34✔
252
                .connect(node_id, *right_node, true, None, Direction::Right)
34✔
253
                .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house door to right node: {e}")))?;
34✔
254

255
            (node_id, node_position)
34✔
256
        };
257

258
        // A helper function to help create the various 'lines' of nodes within the house
259
        let create_house_line = |graph: &mut Graph, center_pos: Vec2| -> GameResult<(NodeId, NodeId)> {
102✔
260
            // Place the nodes at, above, and below the center position
261
            let center_node_id = graph.add_node(Node { position: center_pos });
102✔
262
            let top_node_id = graph.add_node(Node {
102✔
263
                position: center_pos + IVec2::from(Direction::Up.as_ivec2()).as_vec2() * (CELL_SIZE as f32 / 2.0),
102✔
264
            });
102✔
265
            let bottom_node_id = graph.add_node(Node {
102✔
266
                position: center_pos + IVec2::from(Direction::Down.as_ivec2()).as_vec2() * (CELL_SIZE as f32 / 2.0),
102✔
267
            });
102✔
268

269
            // Connect the center node to the top and bottom nodes
270
            graph
102✔
271
                .connect(center_node_id, top_node_id, false, None, Direction::Up)
102✔
272
                .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house line to top node: {e}")))?;
102✔
273
            graph
102✔
274
                .connect(center_node_id, bottom_node_id, false, None, Direction::Down)
102✔
275
                .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house line to bottom node: {e}")))?;
102✔
276

277
            Ok((center_node_id, top_node_id))
102✔
278
        };
102✔
279

280
        // Calculate the position of the center line's center node
281
        let center_line_center_position =
34✔
282
            house_entrance_node_position + IVec2::from(Direction::Down.as_ivec2()).as_vec2() * (3.0 * CELL_SIZE as f32);
34✔
283

284
        // Create the center line
285
        let (center_center_node_id, center_top_node_id) = create_house_line(graph, center_line_center_position)?;
34✔
286

287
        // Create a ghost-only, two-way connection for the house door.
288
        // This prevents Pac-Man from entering or exiting through the door.
289
        graph
34✔
290
            .add_edge(
34✔
291
                house_entrance_node_id,
34✔
292
                center_top_node_id,
34✔
293
                false,
294
                None,
34✔
295
                Direction::Down,
34✔
296
                TraversalFlags::GHOST,
297
            )
298
            .map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only entrance to house: {e}")))?;
34✔
299

300
        graph
34✔
301
            .add_edge(
34✔
302
                center_top_node_id,
34✔
303
                house_entrance_node_id,
34✔
304
                false,
305
                None,
34✔
306
                Direction::Up,
34✔
307
                TraversalFlags::GHOST,
308
            )
309
            .map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only exit from house: {e}")))?;
34✔
310

311
        // Create the left line
312
        let (left_center_node_id, _) = create_house_line(
34✔
313
            graph,
34✔
314
            center_line_center_position + IVec2::from(Direction::Left.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
34✔
315
        )?;
34✔
316

317
        // Create the right line
318
        let (right_center_node_id, _) = create_house_line(
34✔
319
            graph,
34✔
320
            center_line_center_position + IVec2::from(Direction::Right.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
34✔
321
        )?;
34✔
322

323
        debug!("Left center node id: {left_center_node_id}");
34✔
324

325
        // Connect the center line to the left and right lines
326
        graph
34✔
327
            .connect(center_center_node_id, left_center_node_id, false, None, Direction::Left)
34✔
328
            .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house entrance to left top line: {e}")))?;
34✔
329

330
        graph
34✔
331
            .connect(center_center_node_id, right_center_node_id, false, None, Direction::Right)
34✔
332
            .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house entrance to right top line: {e}")))?;
34✔
333

334
        debug!("House entrance node id: {house_entrance_node_id}");
34✔
335

336
        Ok((
34✔
337
            house_entrance_node_id,
34✔
338
            left_center_node_id,
34✔
339
            center_center_node_id,
34✔
340
            right_center_node_id,
34✔
341
        ))
34✔
342
    }
34✔
343

344
    /// Creates horizontal tunnel portals for instant teleportation across the maze.
345
    ///
346
    /// Establishes the tunnel system that allows entities to instantly travel from the left edge of the maze to the right edge.
347
    /// Creates hidden intermediate nodes beyond the visible tunnel entrances and connects them with zero-distance edges for instantaneous traversal.
348
    fn build_tunnels(
34✔
349
        graph: &mut Graph,
34✔
350
        grid_to_node: &HashMap<I8Vec2, NodeId>,
34✔
351
        tunnel_ends: &[Option<I8Vec2>; 2],
34✔
352
    ) -> GameResult<()> {
34✔
353
        // Create the hidden tunnel nodes
354
        let left_tunnel_hidden_node_id = {
34✔
355
            let left_tunnel_entrance_node_id =
34✔
356
                grid_to_node[&tunnel_ends[0].ok_or_else(|| MapError::InvalidConfig("Left tunnel end not found".to_string()))?];
34✔
357
            let left_tunnel_entrance_node = graph
34✔
358
                .get_node(left_tunnel_entrance_node_id)
34✔
359
                .ok_or_else(|| MapError::InvalidConfig("Left tunnel entrance node not found".to_string()))?;
34✔
360

361
            graph
34✔
362
                .add_connected(
34✔
363
                    left_tunnel_entrance_node_id,
34✔
364
                    Direction::Left,
34✔
365
                    Node {
34✔
366
                        position: left_tunnel_entrance_node.position
34✔
367
                            + IVec2::from(Direction::Left.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
34✔
368
                    },
34✔
369
                )
370
                .expect("Failed to connect left tunnel entrance to left tunnel hidden node")
34✔
371
        };
372

373
        // Create the right tunnel nodes
374
        let right_tunnel_hidden_node_id = {
34✔
375
            let right_tunnel_entrance_node_id =
34✔
376
                grid_to_node[&tunnel_ends[1].ok_or_else(|| MapError::InvalidConfig("Right tunnel end not found".to_string()))?];
34✔
377
            let right_tunnel_entrance_node = graph
34✔
378
                .get_node(right_tunnel_entrance_node_id)
34✔
379
                .ok_or_else(|| MapError::InvalidConfig("Right tunnel entrance node not found".to_string()))?;
34✔
380

381
            graph
34✔
382
                .add_connected(
34✔
383
                    right_tunnel_entrance_node_id,
34✔
384
                    Direction::Right,
34✔
385
                    Node {
34✔
386
                        position: right_tunnel_entrance_node.position
34✔
387
                            + IVec2::from(Direction::Right.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
34✔
388
                    },
34✔
389
                )
390
                .expect("Failed to connect right tunnel entrance to right tunnel hidden node")
34✔
391
        };
392

393
        // Connect the left tunnel hidden node to the right tunnel hidden node
394
        graph
34✔
395
            .connect(
34✔
396
                left_tunnel_hidden_node_id,
34✔
397
                right_tunnel_hidden_node_id,
34✔
398
                false,
399
                Some(0.0),
34✔
400
                Direction::Left,
34✔
401
            )
402
            .expect("Failed to connect left tunnel hidden node to right tunnel hidden node");
34✔
403

404
        Ok(())
34✔
405
    }
34✔
406
}
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