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

Xevion / Pac-Man / 17310465987

28 Aug 2025 11:35PM UTC coverage: 48.731% (-4.0%) from 52.708%
17310465987

push

github

Xevion
fix: add expected MovementModifiers to spawn_test_player to fix movement tests

1114 of 2286 relevant lines covered (48.73%)

1148.25 hits per line

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

90.71
/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
/// 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<IVec2, 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> {
28✔
59
        let parsed_map = MapTileParser::parse_board(raw_board)?;
28✔
60

61
        let map = parsed_map.tiles;
28✔
62
        let house_door = parsed_map.house_door;
28✔
63
        let tunnel_ends = parsed_map.tunnel_ends;
28✔
64

28✔
65
        let mut graph = Graph::new();
28✔
66
        let mut grid_to_node = HashMap::new();
28✔
67

28✔
68
        let cell_offset = Vec2::splat(CELL_SIZE as f32 / 2.0);
28✔
69

70
        // Find a starting point for the graph generation, preferably Pac-Man's position.
71
        let start_pos = parsed_map
28✔
72
            .pacman_start
28✔
73
            .ok_or_else(|| MapError::InvalidConfig("Pac-Man's starting position not found".to_string()))?;
28✔
74

75
        // Add the starting position to the graph/queue
76
        let mut queue = VecDeque::new();
28✔
77
        queue.push_back(start_pos);
28✔
78
        let pos = Vec2::new(
28✔
79
            (start_pos.x * CELL_SIZE as i32) as f32,
28✔
80
            (start_pos.y * CELL_SIZE as i32) as f32,
28✔
81
        ) + cell_offset;
28✔
82
        let node_id = graph.add_node(Node { position: pos });
28✔
83
        grid_to_node.insert(start_pos, node_id);
28✔
84

85
        // Iterate over the queue, adding nodes to the graph and connecting them to their neighbors
86
        while let Some(source_position) = queue.pop_front() {
8,372✔
87
            for dir in Direction::DIRECTIONS {
41,720✔
88
                let new_position = source_position + dir.as_ivec2();
33,376✔
89

33,376✔
90
                // Skip if the new position is out of bounds
33,376✔
91
                if new_position.x < 0
33,376✔
92
                    || new_position.x >= BOARD_CELL_SIZE.x as i32
33,348✔
93
                    || new_position.y < 0
33,320✔
94
                    || new_position.y >= BOARD_CELL_SIZE.y as i32
33,320✔
95
                {
96
                    continue;
56✔
97
                }
33,320✔
98

33,320✔
99
                // Skip if the new position is already in the graph
33,320✔
100
                if grid_to_node.contains_key(&new_position) {
33,320✔
101
                    continue;
9,380✔
102
                }
23,940✔
103

104
                // Skip if the new position is not a walkable tile
105
                if matches!(
15,624✔
106
                    map[new_position.x as usize][new_position.y as usize],
23,940✔
107
                    MapTile::Pellet | MapTile::PowerPellet | MapTile::Empty | MapTile::Tunnel
108
                ) {
109
                    // Add the new position to the graph/queue
110
                    let pos = Vec2::new(
8,316✔
111
                        (new_position.x * CELL_SIZE as i32) as f32,
8,316✔
112
                        (new_position.y * CELL_SIZE as i32) as f32,
8,316✔
113
                    ) + cell_offset;
8,316✔
114
                    let new_node_id = graph.add_node(Node { position: pos });
8,316✔
115
                    grid_to_node.insert(new_position, new_node_id);
8,316✔
116
                    queue.push_back(new_position);
8,316✔
117

8,316✔
118
                    // Connect the new node to the source node
8,316✔
119
                    let source_node_id = grid_to_node
8,316✔
120
                        .get(&source_position)
8,316✔
121
                        .unwrap_or_else(|| panic!("Source node not found for {source_position}"));
8,316✔
122

8,316✔
123
                    // Connect the new node to the source node
8,316✔
124
                    graph
8,316✔
125
                        .connect(*source_node_id, new_node_id, false, None, dir)
8,316✔
126
                        .map_err(|e| MapError::InvalidConfig(format!("Failed to add edge: {e}")))?;
8,316✔
127
                }
15,624✔
128
            }
129
        }
130

131
        // While most nodes are already connected to their neighbors, some may not be, so we need to connect them
132
        for (grid_pos, &node_id) in &grid_to_node {
8,372✔
133
            for dir in Direction::DIRECTIONS {
41,720✔
134
                // If the node doesn't have an edge in this direction, look for a neighbor in that direction
135
                if graph.adjacency_list[node_id].get(dir).is_none() {
33,376✔
136
                    let neighbor = grid_pos + dir.as_ivec2();
16,212✔
137
                    // If the neighbor exists, connect the node to it
138
                    if let Some(&neighbor_id) = grid_to_node.get(&neighbor) {
16,212✔
139
                        graph
532✔
140
                            .connect(node_id, neighbor_id, false, None, dir)
532✔
141
                            .map_err(|e| MapError::InvalidConfig(format!("Failed to add edge: {e}")))?;
532✔
142
                    }
15,680✔
143
                }
17,164✔
144
            }
145
        }
146

147
        // Build house structure
148
        let (house_entrance_node_id, left_center_node_id, center_center_node_id, right_center_node_id) =
28✔
149
            Self::build_house(&mut graph, &grid_to_node, &house_door)?;
28✔
150

151
        let start_positions = NodePositions {
28✔
152
            pacman: grid_to_node[&start_pos],
28✔
153
            blinky: house_entrance_node_id,
28✔
154
            pinky: left_center_node_id,
28✔
155
            inky: right_center_node_id,
28✔
156
            clyde: center_center_node_id,
28✔
157
        };
28✔
158

28✔
159
        // Build tunnel connections
28✔
160
        Self::build_tunnels(&mut graph, &grid_to_node, &tunnel_ends)?;
28✔
161

162
        Ok(Map {
28✔
163
            graph,
28✔
164
            grid_to_node,
28✔
165
            start_positions,
28✔
166
            tiles: map,
28✔
167
        })
28✔
168
    }
28✔
169

170
    pub fn iter_nodes(&self) -> impl Iterator<Item = (&NodeId, &MapTile)> {
×
171
        self.grid_to_node.iter().map(move |(pos, node_id)| {
×
172
            let tile = &self.tiles[pos.x as usize][pos.y as usize];
×
173
            (node_id, tile)
×
174
        })
×
175
    }
×
176

177
    /// Returns the `MapTile` at a given node id.
178
    pub fn tile_at_node(&self, node_id: NodeId) -> Option<MapTile> {
×
179
        // reverse lookup: node -> grid
180
        for (grid_pos, id) in &self.grid_to_node {
×
181
            if *id == node_id {
×
182
                return Some(self.tiles[grid_pos.x as usize][grid_pos.y as usize]);
×
183
            }
×
184
        }
185
        None
×
186
    }
×
187

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

223
            // Calculate the position of the house node
224
            let (node_id, node_position) = {
28✔
225
                let left_pos = graph.get_node(*left_node).ok_or(MapError::NodeNotFound(*left_node))?.position;
28✔
226
                let right_pos = graph
28✔
227
                    .get_node(*right_node)
28✔
228
                    .ok_or(MapError::NodeNotFound(*right_node))?
28✔
229
                    .position;
230
                let house_node = graph.add_node(Node {
28✔
231
                    position: left_pos.lerp(right_pos, 0.5),
28✔
232
                });
28✔
233
                (house_node, left_pos.lerp(right_pos, 0.5))
28✔
234
            };
28✔
235

28✔
236
            // Connect the house door to the left and right nodes
28✔
237
            graph
28✔
238
                .connect(node_id, *left_node, true, None, Direction::Left)
28✔
239
                .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house door to left node: {e}")))?;
28✔
240
            graph
28✔
241
                .connect(node_id, *right_node, true, None, Direction::Right)
28✔
242
                .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house door to right node: {e}")))?;
28✔
243

244
            (node_id, node_position)
28✔
245
        };
28✔
246

28✔
247
        // A helper function to help create the various 'lines' of nodes within the house
28✔
248
        let create_house_line = |graph: &mut Graph, center_pos: Vec2| -> GameResult<(NodeId, NodeId)> {
84✔
249
            // Place the nodes at, above, and below the center position
84✔
250
            let center_node_id = graph.add_node(Node { position: center_pos });
84✔
251
            let top_node_id = graph.add_node(Node {
84✔
252
                position: center_pos + (Direction::Up.as_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(),
84✔
253
            });
84✔
254
            let bottom_node_id = graph.add_node(Node {
84✔
255
                position: center_pos + (Direction::Down.as_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(),
84✔
256
            });
84✔
257

84✔
258
            // Connect the center node to the top and bottom nodes
84✔
259
            graph
84✔
260
                .connect(center_node_id, top_node_id, false, None, Direction::Up)
84✔
261
                .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house line to top node: {e}")))?;
84✔
262
            graph
84✔
263
                .connect(center_node_id, bottom_node_id, false, None, Direction::Down)
84✔
264
                .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house line to bottom node: {e}")))?;
84✔
265

266
            Ok((center_node_id, top_node_id))
84✔
267
        };
84✔
268

269
        // Calculate the position of the center line's center node
270
        let center_line_center_position =
28✔
271
            house_entrance_node_position + (Direction::Down.as_ivec2() * (3 * CELL_SIZE as i32)).as_vec2();
28✔
272

273
        // Create the center line
274
        let (center_center_node_id, center_top_node_id) = create_house_line(graph, center_line_center_position)?;
28✔
275

276
        // Create a ghost-only, two-way connection for the house door.
277
        // This prevents Pac-Man from entering or exiting through the door.
278
        graph
28✔
279
            .add_edge(
28✔
280
                house_entrance_node_id,
28✔
281
                center_top_node_id,
28✔
282
                false,
28✔
283
                None,
28✔
284
                Direction::Down,
28✔
285
                TraversalFlags::GHOST,
28✔
286
            )
28✔
287
            .map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only entrance to house: {e}")))?;
28✔
288

289
        graph
28✔
290
            .add_edge(
28✔
291
                center_top_node_id,
28✔
292
                house_entrance_node_id,
28✔
293
                false,
28✔
294
                None,
28✔
295
                Direction::Up,
28✔
296
                TraversalFlags::GHOST,
28✔
297
            )
28✔
298
            .map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only exit from house: {e}")))?;
28✔
299

300
        // Create the left line
301
        let (left_center_node_id, _) = create_house_line(
28✔
302
            graph,
28✔
303
            center_line_center_position + (Direction::Left.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
28✔
304
        )?;
28✔
305

306
        // Create the right line
307
        let (right_center_node_id, _) = create_house_line(
28✔
308
            graph,
28✔
309
            center_line_center_position + (Direction::Right.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
28✔
310
        )?;
28✔
311

312
        debug!("Left center node id: {left_center_node_id}");
28✔
313

314
        // Connect the center line to the left and right lines
315
        graph
28✔
316
            .connect(center_center_node_id, left_center_node_id, false, None, Direction::Left)
28✔
317
            .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house entrance to left top line: {e}")))?;
28✔
318

319
        graph
28✔
320
            .connect(center_center_node_id, right_center_node_id, false, None, Direction::Right)
28✔
321
            .map_err(|e| MapError::InvalidConfig(format!("Failed to connect house entrance to right top line: {e}")))?;
28✔
322

323
        debug!("House entrance node id: {house_entrance_node_id}");
28✔
324

325
        Ok((
28✔
326
            house_entrance_node_id,
28✔
327
            left_center_node_id,
28✔
328
            center_center_node_id,
28✔
329
            right_center_node_id,
28✔
330
        ))
28✔
331
    }
28✔
332

333
    /// Creates horizontal tunnel portals for instant teleportation across the maze.
334
    ///
335
    /// Establishes the tunnel system that allows entities to instantly travel from the left edge of the maze to the right edge.
336
    /// Creates hidden intermediate nodes beyond the visible tunnel entrances and connects them with zero-distance edges for instantaneous traversal.
337
    fn build_tunnels(
28✔
338
        graph: &mut Graph,
28✔
339
        grid_to_node: &HashMap<IVec2, NodeId>,
28✔
340
        tunnel_ends: &[Option<IVec2>; 2],
28✔
341
    ) -> GameResult<()> {
28✔
342
        // Create the hidden tunnel nodes
343
        let left_tunnel_hidden_node_id = {
28✔
344
            let left_tunnel_entrance_node_id =
28✔
345
                grid_to_node[&tunnel_ends[0].ok_or_else(|| MapError::InvalidConfig("Left tunnel end not found".to_string()))?];
28✔
346
            let left_tunnel_entrance_node = graph
28✔
347
                .get_node(left_tunnel_entrance_node_id)
28✔
348
                .ok_or_else(|| MapError::InvalidConfig("Left tunnel entrance node not found".to_string()))?;
28✔
349

350
            graph
28✔
351
                .add_connected(
28✔
352
                    left_tunnel_entrance_node_id,
28✔
353
                    Direction::Left,
28✔
354
                    Node {
28✔
355
                        position: left_tunnel_entrance_node.position
28✔
356
                            + (Direction::Left.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
28✔
357
                    },
28✔
358
                )
28✔
359
                .map_err(|e| {
28✔
360
                    MapError::InvalidConfig(format!(
×
361
                        "Failed to connect left tunnel entrance to left tunnel hidden node: {}",
×
362
                        e
×
363
                    ))
×
364
                })?
28✔
365
        };
366

367
        // Create the right tunnel nodes
368
        let right_tunnel_hidden_node_id = {
28✔
369
            let right_tunnel_entrance_node_id =
28✔
370
                grid_to_node[&tunnel_ends[1].ok_or_else(|| MapError::InvalidConfig("Right tunnel end not found".to_string()))?];
28✔
371
            let right_tunnel_entrance_node = graph
28✔
372
                .get_node(right_tunnel_entrance_node_id)
28✔
373
                .ok_or_else(|| MapError::InvalidConfig("Right tunnel entrance node not found".to_string()))?;
28✔
374

375
            graph
28✔
376
                .add_connected(
28✔
377
                    right_tunnel_entrance_node_id,
28✔
378
                    Direction::Right,
28✔
379
                    Node {
28✔
380
                        position: right_tunnel_entrance_node.position
28✔
381
                            + (Direction::Right.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(),
28✔
382
                    },
28✔
383
                )
28✔
384
                .map_err(|e| {
28✔
385
                    MapError::InvalidConfig(format!(
×
386
                        "Failed to connect right tunnel entrance to right tunnel hidden node: {}",
×
387
                        e
×
388
                    ))
×
389
                })?
28✔
390
        };
391

392
        // Connect the left tunnel hidden node to the right tunnel hidden node
393
        graph
28✔
394
            .connect(
28✔
395
                left_tunnel_hidden_node_id,
28✔
396
                right_tunnel_hidden_node_id,
28✔
397
                false,
28✔
398
                Some(0.0),
28✔
399
                Direction::Left,
28✔
400
            )
28✔
401
            .map_err(|e| {
28✔
402
                MapError::InvalidConfig(format!(
×
403
                    "Failed to connect left tunnel hidden node to right tunnel hidden node: {}",
×
404
                    e
×
405
                ))
×
406
            })?;
28✔
407

408
        Ok(())
28✔
409
    }
28✔
410
}
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