Compare commits

...

5 Commits

7 changed files with 122 additions and 46 deletions

10
.gitignore vendored
View File

@@ -1,7 +1,5 @@
/target
/dist
.idea
*.dll
rust-sdl2-emscripten/
assets/site/build.css
target/
dist/
emsdk/
.idea
rust-sdl2-emscripten/

View File

@@ -50,3 +50,15 @@ Built with SDL2 for cross-platform graphics and audio, this implementation can r
- Online Scoreboard
- WebAssembly build contains a special API key for communicating with server.
- To prevent abuse, the server will only accept scores from the WebAssembly build.
## Build Notes
- Install `cargo-vcpkg` with `cargo install cargo-vcpkg`, then run `cargo vcpkg build` to build the requisite dependencies via vcpkg.
- For the WASM build, you need to have the Emscripten SDK cloned; you can do so with `git clone https://github.com/emscripten-core/emsdk.git`
- The first time you clone, you'll need to install the appropriate SDK version with `./emsdk install 3.1.43` and then activate it with `./emsdk activate 3.1.43`. On Windows, use `./emsdk/emsdk.ps1` instead.
- You can then activate the Emscripten SDK with `source ./emsdk/emsdk_env.sh` or `./emsdk/emsdk_env.ps1` or `./emsdk/emsdk_env.bat` depending on your OS/terminal.
- While using the `web.build.ts` is not technically required, it simplifies the build process and is very helpful.
- It is intended to be run with `bun`, which you can acquire at [bun.sh](https://bun.sh/)
- Tip: You can launch a fileserver with `python` or `caddy` to serve the files in the `dist` folder.
- `python3 -m http.server 8080 -d dist`
- `caddy file-server --root dist` (install with `[sudo apt|brew|choco] install caddy` or [a dozen other ways](https://caddyserver.com/docs/install))

View File

@@ -92,6 +92,8 @@ impl App<'_> {
}
_ => {}
},
// It doesn't really make sense to have this available in the browser
#[cfg(not(target_os = "emscripten"))]
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape) | Some(Keycode::Q),

View File

@@ -5,6 +5,16 @@ use super::direction::Direction;
/// A unique identifier for a node, represented by its index in the graph's storage.
pub type NodeId = usize;
/// Defines who can traverse a given edge.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EdgePermissions {
/// Anyone can use this edge.
#[default]
All,
/// Only ghosts can use this edge.
GhostsOnly,
}
/// Represents a directed edge from one node to another with a given weight (e.g., distance).
#[derive(Debug, Clone, Copy)]
pub struct Edge {
@@ -14,6 +24,8 @@ pub struct Edge {
pub distance: f32,
/// The cardinal direction of this edge.
pub direction: Direction,
/// Defines who is allowed to traverse this edge.
pub permissions: EdgePermissions,
}
/// Represents a node in the graph, defined by its position.
@@ -121,8 +133,8 @@ impl Graph {
return Err("To node does not exist.");
}
let edge_a = self.add_edge(from, to, replace, distance, direction);
let edge_b = self.add_edge(to, from, replace, distance, direction.opposite());
let edge_a = self.add_edge(from, to, replace, distance, direction, EdgePermissions::default());
let edge_b = self.add_edge(to, from, replace, distance, direction.opposite(), EdgePermissions::default());
if edge_a.is_err() && edge_b.is_err() {
return Err("Failed to connect nodes in both directions.");
@@ -150,6 +162,7 @@ impl Graph {
replace: bool,
distance: Option<f32>,
direction: Direction,
permissions: EdgePermissions,
) -> Result<(), &'static str> {
let edge = Edge {
target: to,
@@ -168,6 +181,7 @@ impl Graph {
}
},
direction,
permissions,
};
if from >= self.adjacency_list.len() {
@@ -295,7 +309,10 @@ impl Traverser {
/// Creates a new traverser starting at the given node ID.
///
/// The traverser will immediately attempt to start moving in the initial direction.
pub fn new(graph: &Graph, start_node: NodeId, initial_direction: Direction) -> Self {
pub fn new<F>(graph: &Graph, start_node: NodeId, initial_direction: Direction, can_traverse: &F) -> Self
where
F: Fn(Edge) -> bool,
{
let mut traverser = Traverser {
position: Position::AtNode(start_node),
direction: initial_direction,
@@ -303,7 +320,7 @@ impl Traverser {
};
// This will kickstart the traverser into motion
traverser.advance(graph, 0.0);
traverser.advance(graph, 0.0, can_traverse);
traverser
}
@@ -329,7 +346,10 @@ impl Traverser {
/// - If it reaches a node, it attempts to transition to a new edge based on
/// the buffered direction or by continuing straight.
/// - If no valid move is possible, it stops at the node.
pub fn advance(&mut self, graph: &Graph, distance: f32) {
pub fn advance<F>(&mut self, graph: &Graph, distance: f32, can_traverse: &F)
where
F: Fn(Edge) -> bool,
{
// Decrement the remaining frames for the next direction
if let Some((direction, remaining)) = self.next_direction {
if remaining > 0 {
@@ -344,13 +364,15 @@ impl Traverser {
// We're not moving, but a buffered direction is available.
if let Some((next_direction, _)) = self.next_direction {
if let Some(edge) = graph.find_edge_in_direction(node_id, next_direction) {
// Start moving in that direction
self.position = Position::BetweenNodes {
from: node_id,
to: edge.target,
traversed: distance.max(0.0),
};
self.direction = next_direction;
if can_traverse(edge) {
// Start moving in that direction
self.position = Position::BetweenNodes {
from: node_id,
to: edge.target,
traversed: distance.max(0.0),
};
self.direction = next_direction;
}
}
self.next_direction = None; // Consume the buffered direction regardless of whether we started moving with it
@@ -382,26 +404,33 @@ impl Traverser {
// If we buffered a direction, try to find an edge in that direction
if let Some((next_dir, _)) = self.next_direction {
if let Some(edge) = graph.find_edge_in_direction(to, next_dir) {
self.position = Position::BetweenNodes {
from: to,
to: edge.target,
traversed: overflow,
};
if can_traverse(edge) {
self.position = Position::BetweenNodes {
from: to,
to: edge.target,
traversed: overflow,
};
self.direction = next_dir; // Remember our new direction
self.next_direction = None; // Consume the buffered direction
moved = true;
self.direction = next_dir; // Remember our new direction
self.next_direction = None; // Consume the buffered direction
moved = true;
}
}
}
// If we didn't move, try to continue in the current direction
if !moved {
if let Some(edge) = graph.find_edge_in_direction(to, self.direction) {
self.position = Position::BetweenNodes {
from: to,
to: edge.target,
traversed: overflow,
};
if can_traverse(edge) {
self.position = Position::BetweenNodes {
from: to,
to: edge.target,
traversed: overflow,
};
} else {
self.position = Position::AtNode(to);
self.next_direction = None;
}
} else {
self.position = Position::AtNode(to);
self.next_direction = None;
@@ -577,6 +606,7 @@ mod tests {
target: 1,
distance: 10.0,
direction: Direction::Up,
permissions: EdgePermissions::All,
},
);
intersection.set(
@@ -585,6 +615,7 @@ mod tests {
target: 2,
distance: 15.0,
direction: Direction::Right,
permissions: EdgePermissions::All,
},
);
@@ -607,6 +638,7 @@ mod tests {
target: 1,
distance: 10.0,
direction: Direction::Up,
permissions: EdgePermissions::All,
};
intersection.set(Direction::Up, edge);
@@ -624,6 +656,7 @@ mod tests {
target: 1,
distance: 10.0,
direction: Direction::Left,
permissions: EdgePermissions::All,
};
intersection.set(Direction::Left, edge);
@@ -687,7 +720,7 @@ mod tests {
#[test]
fn test_traverser_new() {
let graph = create_test_graph();
let traverser = Traverser::new(&graph, 0, Direction::Left);
let traverser = Traverser::new(&graph, 0, Direction::Left, &|_| true);
assert_eq!(traverser.direction, Direction::Left);
// The next_direction might be consumed immediately when the traverser starts moving
@@ -698,7 +731,7 @@ mod tests {
#[test]
fn test_traverser_set_next_direction() {
let graph = create_test_graph();
let mut traverser = Traverser::new(&graph, 0, Direction::Left);
let mut traverser = Traverser::new(&graph, 0, Direction::Left, &|_| true);
traverser.set_next_direction(Direction::Up);
assert!(traverser.next_direction.is_some());
@@ -712,10 +745,10 @@ mod tests {
#[test]
fn test_traverser_advance_at_node() {
let graph = create_test_graph();
let mut traverser = Traverser::new(&graph, 0, Direction::Right);
let mut traverser = Traverser::new(&graph, 0, Direction::Right, &|_| true);
// Should start moving in the initial direction
traverser.advance(&graph, 5.0);
traverser.advance(&graph, 5.0, &|_| true);
match traverser.position {
Position::BetweenNodes { from, to, traversed } => {
@@ -730,13 +763,13 @@ mod tests {
#[test]
fn test_traverser_advance_between_nodes() {
let graph = create_test_graph();
let mut traverser = Traverser::new(&graph, 0, Direction::Right);
let mut traverser = Traverser::new(&graph, 0, Direction::Right, &|_| true);
// Move to between nodes
traverser.advance(&graph, 5.0);
traverser.advance(&graph, 5.0, &|_| true);
// Advance further
traverser.advance(&graph, 3.0);
traverser.advance(&graph, 3.0, &|_| true);
match traverser.position {
Position::BetweenNodes { from, to, traversed } => {
@@ -754,6 +787,7 @@ mod tests {
target: 5,
distance: 10.5,
direction: Direction::Up,
permissions: EdgePermissions::All,
};
assert_eq!(edge.target, 5);

View File

@@ -2,7 +2,7 @@ use glam::{UVec2, Vec2};
use crate::constants::BOARD_PIXEL_OFFSET;
use crate::entity::direction::Direction;
use crate::entity::graph::{Graph, NodeId, Position, Traverser};
use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId, Position, Traverser};
use crate::helpers::centered_with_size;
use crate::texture::animated::AnimatedTexture;
use crate::texture::directional::DirectionalAnimatedTexture;
@@ -11,6 +11,10 @@ use sdl2::keyboard::Keycode;
use sdl2::render::{Canvas, RenderTarget};
use std::collections::HashMap;
fn can_pacman_traverse(edge: Edge) -> bool {
matches!(edge.permissions, EdgePermissions::All)
}
pub struct Pacman {
pub traverser: Traverser,
texture: DirectionalAnimatedTexture,
@@ -47,13 +51,13 @@ impl Pacman {
}
Self {
traverser: Traverser::new(graph, start_node, Direction::Left),
traverser: Traverser::new(graph, start_node, Direction::Left, &can_pacman_traverse),
texture: DirectionalAnimatedTexture::new(textures, stopped_textures),
}
}
pub fn tick(&mut self, dt: f32, graph: &Graph) {
self.traverser.advance(graph, dt * 60.0 * 1.125);
self.traverser.advance(graph, dt * 60.0 * 1.125, &can_pacman_traverse);
self.texture.tick(dt);
}
@@ -281,7 +285,7 @@ mod tests {
let mut pacman = Pacman::new(&graph, 0, &atlas);
// Move pacman between nodes - need to advance with a larger distance to ensure movement
pacman.traverser.advance(&graph, 5.0); // Larger advance to ensure movement
pacman.traverser.advance(&graph, 5.0, &can_pacman_traverse); // Larger advance to ensure movement
let pos = pacman.get_pixel_pos(&graph);
// Should be between (0,0) and (16,0), but not exactly at (8,0) due to advance distance

View File

@@ -2,7 +2,7 @@
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE};
use crate::entity::direction::{Direction, DIRECTIONS};
use crate::entity::graph::{Graph, Node, NodeId};
use crate::entity::graph::{EdgePermissions, Graph, Node, NodeId};
use crate::map::parser::MapTileParser;
use crate::map::render::MapRenderer;
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
@@ -259,10 +259,29 @@ impl Map {
// Create the center line
let (center_center_node_id, center_top_node_id) = create_house_line(graph, center_line_center_position);
// Connect the house entrance to the top line
// Create a ghost-only, two-way connection for the house door.
// This prevents Pac-Man from entering or exiting through the door.
graph
.connect(house_entrance_node_id, center_top_node_id, false, None, Direction::Down)
.expect("Failed to connect house entrance to top line");
.add_edge(
house_entrance_node_id,
center_top_node_id,
false,
None,
Direction::Down,
EdgePermissions::GhostsOnly,
)
.expect("Failed to create ghost-only entrance to house");
graph
.add_edge(
center_top_node_id,
house_entrance_node_id,
false,
None,
Direction::Up,
EdgePermissions::GhostsOnly,
)
.expect("Failed to create ghost-only exit from house");
// Create the left line
let (left_center_node_id, _) = create_house_line(

View File

@@ -184,6 +184,13 @@ async function main() {
const release = process.env.RELEASE !== "0";
const emsdkDir = resolve("./emsdk");
// Ensure the emsdk directory exists before attempting to activate or use it
if (!(await fs.exists(emsdkDir))) {
log(
`Emscripten SDK directory not found at ${emsdkDir}. Please install or clone 'emsdk' and try again.`
);
process.exit(1);
}
const vars = match(await activateEmsdk(emsdkDir)) // result handling
.with({ vars: P.select() }, (vars) => vars)
.with({ err: P.any }, ({ err }) => {