mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 05:15:49 -06:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9633611ae8 | ||
|
|
897b9b8621 | ||
|
|
ee2569b70c | ||
|
|
84caa6c25f | ||
|
|
f92c9175b9 | ||
|
|
d561b446c5 | ||
|
|
9219c771d7 | ||
|
|
cd501aafc4 | ||
|
|
feae1ee191 |
41
.github/workflows/coverage.yaml
vendored
41
.github/workflows/coverage.yaml
vendored
@@ -9,8 +9,6 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
coverage:
|
coverage:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
@@ -48,35 +46,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate coverage report
|
- name: Generate coverage report
|
||||||
run: |
|
run: |
|
||||||
just coverage
|
just coverage-codecov
|
||||||
|
|
||||||
- name: Download Coveralls CLI
|
- name: Upload coverage reports to Codecov
|
||||||
if: ${{ env.COVERALLS_REPO_TOKEN != '' }}
|
uses: codecov/codecov-action@v4
|
||||||
run: |
|
with:
|
||||||
# use GitHub Releases URL instead of coveralls.io because they can't maintain their own files; it 404s
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
curl -L https://github.com/coverallsapp/coverage-reporter/releases/download/v0.6.15/coveralls-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin
|
files: ./codecov.json
|
||||||
|
disable_search: true
|
||||||
- name: Upload coverage to Coveralls
|
verbose: true
|
||||||
if: ${{ env.COVERALLS_REPO_TOKEN != '' }}
|
|
||||||
run: |
|
|
||||||
if [ ! -f "lcov.info" ]; then
|
|
||||||
echo "Error: lcov.info file not found. Coverage generation may have failed."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
for i in {1..10}; do
|
|
||||||
echo "Attempt $i: Uploading coverage to Coveralls..."
|
|
||||||
if coveralls -n report lcov.info; then
|
|
||||||
echo "Successfully uploaded coverage report."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $i -lt 10 ]; then
|
|
||||||
delay=$((2**i))
|
|
||||||
echo "Attempt $i failed. Retrying in $delay seconds..."
|
|
||||||
sleep $delay
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Failed to upload coverage report after 10 attempts."
|
|
||||||
exit 1
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@ assets/site/build.css
|
|||||||
|
|
||||||
# Coverage reports
|
# Coverage reports
|
||||||
lcov.info
|
lcov.info
|
||||||
|
codecov.json
|
||||||
coverage.html
|
coverage.html
|
||||||
|
|
||||||
# Profiling output
|
# Profiling output
|
||||||
|
|||||||
22
Justfile
22
Justfile
@@ -2,8 +2,8 @@ set shell := ["bash", "-c"]
|
|||||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
||||||
|
|
||||||
# Regex to exclude files from coverage report, double escapes for Justfile + CLI
|
# Regex to exclude files from coverage report, double escapes for Justfile + CLI
|
||||||
# You can use src\\\\..., but the filename alone is acceptable too
|
# You can use src/..., but the filename alone is acceptable too
|
||||||
coverage_exclude_pattern := "src\\\\app\\.rs|audio\\.rs|src\\\\error\\.rs|platform\\\\emscripten\\.rs|bin\\\\.+\\.rs|main\\.rs|platform\\\\desktop\\.rs|platform\\\\tracing_buffer\\.rs|platform\\\\buffered_writer\\.rs|systems\\\\debug\\.rs|systems\\\\profiling\\.rs"
|
coverage_exclude_pattern := "src/app\\.rs|audio\\.rs|src/error\\.rs|platform/emscripten\\.rs|bin/.+\\.rs|main\\.rs|platform/desktop\\.rs|platform/tracing_buffer\\.rs|platform/buffered_writer\\.rs|systems/debug\\.rs|systems/profiling\\.rs"
|
||||||
|
|
||||||
binary_extension := if os() == "windows" { ".exe" } else { "" }
|
binary_extension := if os() == "windows" { ".exe" } else { "" }
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ binary_extension := if os() == "windows" { ".exe" } else { "" }
|
|||||||
# !!! --remap-path-prefix prevents the absolute path from being used in the generated report
|
# !!! --remap-path-prefix prevents the absolute path from being used in the generated report
|
||||||
|
|
||||||
# Generate HTML report (for humans, source line inspection)
|
# Generate HTML report (for humans, source line inspection)
|
||||||
html: coverage
|
html: coverage-lcov
|
||||||
cargo llvm-cov report \
|
cargo llvm-cov report \
|
||||||
--remap-path-prefix \
|
--remap-path-prefix \
|
||||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
||||||
@@ -19,13 +19,13 @@ html: coverage
|
|||||||
--open
|
--open
|
||||||
|
|
||||||
# Display report (for humans)
|
# Display report (for humans)
|
||||||
report-coverage: coverage
|
report-coverage: coverage-lcov
|
||||||
cargo llvm-cov report \
|
cargo llvm-cov report \
|
||||||
--remap-path-prefix \
|
--remap-path-prefix \
|
||||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}"
|
--ignore-filename-regex "{{ coverage_exclude_pattern }}"
|
||||||
|
|
||||||
# Run & generate report (for CI)
|
# Run & generate LCOV report (as base report)
|
||||||
coverage:
|
coverage-lcov:
|
||||||
cargo llvm-cov \
|
cargo llvm-cov \
|
||||||
--lcov \
|
--lcov \
|
||||||
--remap-path-prefix \
|
--remap-path-prefix \
|
||||||
@@ -34,6 +34,16 @@ coverage:
|
|||||||
--profile coverage \
|
--profile coverage \
|
||||||
--no-fail-fast nextest
|
--no-fail-fast nextest
|
||||||
|
|
||||||
|
# Run & generate Codecov report (for CI)
|
||||||
|
coverage-codecov:
|
||||||
|
cargo llvm-cov \
|
||||||
|
--codecov \
|
||||||
|
--remap-path-prefix \
|
||||||
|
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
||||||
|
--output-path codecov.json \
|
||||||
|
--profile coverage \
|
||||||
|
--no-fail-fast nextest
|
||||||
|
|
||||||
# Profile the project using 'samply'
|
# Profile the project using 'samply'
|
||||||
samply:
|
samply:
|
||||||
cargo build --profile profile
|
cargo build --profile profile
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# Pac-Man
|
# Pac-Man
|
||||||
|
|
||||||
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![If you're seeing this, Coveralls.io is broken again and it's not my fault.][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
|
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![Code Coverage][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
|
||||||
|
|
||||||
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
|
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
|
||||||
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
|
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
|
||||||
[badge-coverage]: https://coveralls.io/repos/github/Xevion/Pac-Man/badge.svg?branch=master
|
[badge-coverage]: https://codecov.io/github/Xevion/Pac-Man/branch/master/graph/badge.svg?token=R2RBYUQK3I
|
||||||
[badge-demo]: https://img.shields.io/github/deployments/Xevion/Pac-Man/github-pages?label=GitHub%20Pages
|
[badge-demo]: https://img.shields.io/github/deployments/Xevion/Pac-Man/github-pages?label=GitHub%20Pages
|
||||||
[badge-online-demo]: https://img.shields.io/badge/GitHub%20Pages-Demo-brightgreen
|
[badge-online-demo]: https://img.shields.io/badge/GitHub%20Pages-Demo-brightgreen
|
||||||
[badge-last-commit]: https://img.shields.io/github/last-commit/Xevion/Pac-Man
|
[badge-last-commit]: https://img.shields.io/github/last-commit/Xevion/Pac-Man
|
||||||
[build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
|
[build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
|
||||||
[test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml
|
[test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml
|
||||||
[coverage]: https://coveralls.io/github/Xevion/Pac-Man?branch=master
|
[coverage]: https://codecov.io/github/Xevion/Pac-Man
|
||||||
[demo]: https://xevion.github.io/Pac-Man/
|
[demo]: https://xevion.github.io/Pac-Man/
|
||||||
[commits]: https://github.com/Xevion/Pac-Man/commits/master
|
[commits]: https://github.com/Xevion/Pac-Man/commits/master
|
||||||
|
|
||||||
|
|||||||
3
codecov.yml
Normal file
3
codecov.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ignore:
|
||||||
|
- "src/(?:bin|platform))/.+\\.rs"
|
||||||
|
- "src/(?:app|events|formatter)\\.rs"
|
||||||
@@ -359,12 +359,7 @@ impl Map {
|
|||||||
+ IVec2::from(Direction::Left.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
|
+ IVec2::from(Direction::Left.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.expect("Failed to connect left tunnel entrance to left tunnel hidden node")
|
||||||
MapError::InvalidConfig(format!(
|
|
||||||
"Failed to connect left tunnel entrance to left tunnel hidden node: {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the right tunnel nodes
|
// Create the right tunnel nodes
|
||||||
@@ -384,12 +379,7 @@ impl Map {
|
|||||||
+ IVec2::from(Direction::Right.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
|
+ IVec2::from(Direction::Right.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.expect("Failed to connect right tunnel entrance to right tunnel hidden node")
|
||||||
MapError::InvalidConfig(format!(
|
|
||||||
"Failed to connect right tunnel entrance to right tunnel hidden node: {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect the left tunnel hidden node to the right tunnel hidden node
|
// Connect the left tunnel hidden node to the right tunnel hidden node
|
||||||
@@ -401,12 +391,7 @@ impl Map {
|
|||||||
Some(0.0),
|
Some(0.0),
|
||||||
Direction::Left,
|
Direction::Left,
|
||||||
)
|
)
|
||||||
.map_err(|e| {
|
.expect("Failed to connect left tunnel hidden node to right tunnel hidden node");
|
||||||
MapError::InvalidConfig(format!(
|
|
||||||
"Failed to connect left tunnel hidden node to right tunnel hidden node: {}",
|
|
||||||
e
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Touch input constants
|
// Touch input constants
|
||||||
const TOUCH_DIRECTION_THRESHOLD: f32 = 10.0;
|
pub const TOUCH_DIRECTION_THRESHOLD: f32 = 10.0;
|
||||||
const TOUCH_EASING_DISTANCE_THRESHOLD: f32 = 1.0;
|
pub const TOUCH_EASING_DISTANCE_THRESHOLD: f32 = 1.0;
|
||||||
const MAX_TOUCH_MOVEMENT_SPEED: f32 = 100.0;
|
pub const MAX_TOUCH_MOVEMENT_SPEED: f32 = 100.0;
|
||||||
const TOUCH_EASING_FACTOR: f32 = 1.5;
|
pub const TOUCH_EASING_FACTOR: f32 = 1.5;
|
||||||
|
|
||||||
#[derive(Resource, Default, Debug, Copy, Clone)]
|
#[derive(Resource, Default, Debug, Copy, Clone)]
|
||||||
pub enum CursorPosition {
|
pub enum CursorPosition {
|
||||||
@@ -35,7 +35,7 @@ pub enum CursorPosition {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, Default, Debug)]
|
#[derive(Resource, Default, Debug, Clone)]
|
||||||
pub struct TouchState {
|
pub struct TouchState {
|
||||||
pub active_touch: Option<TouchData>,
|
pub active_touch: Option<TouchData>,
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ pub fn process_simple_key_events(bindings: &mut Bindings, frame_events: &[Simple
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates the primary direction from a 2D vector delta
|
/// Calculates the primary direction from a 2D vector delta
|
||||||
fn calculate_direction_from_delta(delta: Vec2) -> Direction {
|
pub fn calculate_direction_from_delta(delta: Vec2) -> Direction {
|
||||||
if delta.x.abs() > delta.y.abs() {
|
if delta.x.abs() > delta.y.abs() {
|
||||||
if delta.x > 0.0 {
|
if delta.x > 0.0 {
|
||||||
Direction::Right
|
Direction::Right
|
||||||
@@ -179,7 +179,7 @@ fn calculate_direction_from_delta(delta: Vec2) -> Direction {
|
|||||||
/// This slowly moves the start_pos towards the current_pos, with the speed
|
/// This slowly moves the start_pos towards the current_pos, with the speed
|
||||||
/// decreasing as the distance gets smaller. The maximum movement speed is capped.
|
/// decreasing as the distance gets smaller. The maximum movement speed is capped.
|
||||||
/// Returns the delta vector and its length for reuse by the caller.
|
/// Returns the delta vector and its length for reuse by the caller.
|
||||||
fn update_touch_reference_position(touch_data: &mut TouchData, delta_time: f32) -> (Vec2, f32) {
|
pub fn update_touch_reference_position(touch_data: &mut TouchData, delta_time: f32) -> (Vec2, f32) {
|
||||||
// Calculate the vector from start to current position
|
// Calculate the vector from start to current position
|
||||||
let delta = touch_data.current_pos - touch_data.start_pos;
|
let delta = touch_data.current_pos - touch_data.start_pos;
|
||||||
let distance = delta.length();
|
let distance = delta.length();
|
||||||
@@ -220,16 +220,6 @@ pub fn input_system(
|
|||||||
// Collect all events for this frame.
|
// Collect all events for this frame.
|
||||||
let frame_events: SmallVec<[Event; 3]> = pump.poll_iter().collect();
|
let frame_events: SmallVec<[Event; 3]> = pump.poll_iter().collect();
|
||||||
|
|
||||||
// Warn if the smallvec was heap allocated due to exceeding stack capacity
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
if frame_events.len() > frame_events.capacity() {
|
|
||||||
tracing::warn!(
|
|
||||||
"More than {} events in a frame, consider adjusting stack capacity: {:?}",
|
|
||||||
frame_events.capacity(),
|
|
||||||
frame_events
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle non-keyboard events inline and build a simplified keyboard event stream.
|
// Handle non-keyboard events inline and build a simplified keyboard event stream.
|
||||||
let mut simple_key_events: SmallVec<[SimpleKeyEvent; 3]> = smallvec![];
|
let mut simple_key_events: SmallVec<[SimpleKeyEvent; 3]> = smallvec![];
|
||||||
for event in &frame_events {
|
for event in &frame_events {
|
||||||
|
|||||||
17
tests/asset.rs
Normal file
17
tests/asset.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use pacman::asset::Asset;
|
||||||
|
use speculoos::prelude::*;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_asset_paths_exist() {
|
||||||
|
for asset in Asset::iter() {
|
||||||
|
let path = asset.path();
|
||||||
|
let full_path = format!("assets/game/{}", path);
|
||||||
|
|
||||||
|
let metadata = std::fs::metadata(&full_path)
|
||||||
|
.map_err(|e| format!("Error getting metadata for {}: {}", full_path, e))
|
||||||
|
.unwrap();
|
||||||
|
assert_that(&metadata.is_file()).is_true();
|
||||||
|
assert_that(&metadata.len()).is_greater_than(1024);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
use pacman::constants::*;
|
|
||||||
use speculoos::prelude::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_raw_board_structure() {
|
|
||||||
// Test board dimensions match expected size
|
|
||||||
assert_that(&RAW_BOARD.len()).is_equal_to(BOARD_CELL_SIZE.y as usize);
|
|
||||||
for row in RAW_BOARD.iter() {
|
|
||||||
assert_that(&row.len()).is_equal_to(BOARD_CELL_SIZE.x as usize);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test boundaries are properly walled
|
|
||||||
assert_that(&RAW_BOARD[0].chars().all(|c| c == '#')).is_true();
|
|
||||||
assert_that(&RAW_BOARD[RAW_BOARD.len() - 1].chars().all(|c| c == '#')).is_true();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_raw_board_contains_required_elements() {
|
|
||||||
// Test that essential game elements are present
|
|
||||||
assert_that(&RAW_BOARD.iter().any(|row| row.contains('X'))).is_true();
|
|
||||||
assert_that(&RAW_BOARD.iter().any(|row| row.contains("=="))).is_true();
|
|
||||||
assert_that(&RAW_BOARD.iter().any(|row| row.chars().any(|c| c == 'T'))).is_true();
|
|
||||||
assert_that(&RAW_BOARD.iter().any(|row| row.chars().any(|c| c == 'o'))).is_true();
|
|
||||||
}
|
|
||||||
@@ -1,76 +1,7 @@
|
|||||||
use pacman::error::{
|
use pacman::error::{GameError, GameResult, IntoGameError, OptionExt, ResultExt};
|
||||||
AssetError, EntityError, GameError, GameResult, IntoGameError, MapError, OptionExt, ParseError, ResultExt, TextureError,
|
|
||||||
};
|
|
||||||
use speculoos::prelude::*;
|
use speculoos::prelude::*;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_game_error_from_asset_error() {
|
|
||||||
let asset_error = AssetError::NotFound("test.png".to_string());
|
|
||||||
let game_error: GameError = asset_error.into();
|
|
||||||
assert_that(&matches!(game_error, GameError::Asset(_))).is_true();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_game_error_from_parse_error() {
|
|
||||||
let parse_error = ParseError::UnknownCharacter('Z');
|
|
||||||
let game_error: GameError = parse_error.into();
|
|
||||||
assert_that(&matches!(game_error, GameError::MapParse(_))).is_true();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_game_error_from_map_error() {
|
|
||||||
let map_error = MapError::NodeNotFound(42);
|
|
||||||
let game_error: GameError = map_error.into();
|
|
||||||
assert_that(&matches!(game_error, GameError::Map(_))).is_true();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_game_error_from_texture_error() {
|
|
||||||
let texture_error = TextureError::LoadFailed("Failed to load".to_string());
|
|
||||||
let game_error: GameError = texture_error.into();
|
|
||||||
assert_that(&matches!(game_error, GameError::Texture(_))).is_true();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_game_error_from_entity_error() {
|
|
||||||
let entity_error = EntityError::NodeNotFound(10);
|
|
||||||
let game_error: GameError = entity_error.into();
|
|
||||||
assert_that(&matches!(game_error, GameError::Entity(_))).is_true();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_game_error_from_io_error() {
|
|
||||||
let io_error = io::Error::new(io::ErrorKind::NotFound, "File not found");
|
|
||||||
let game_error: GameError = io_error.into();
|
|
||||||
assert_that(&matches!(game_error, GameError::Io(_))).is_true();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_asset_error_from_io_error() {
|
|
||||||
let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "Permission denied");
|
|
||||||
let asset_error: AssetError = io_error.into();
|
|
||||||
assert_that(&matches!(asset_error, AssetError::Io(_))).is_true();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_error_display() {
|
|
||||||
let error = ParseError::UnknownCharacter('!');
|
|
||||||
assert_that(&error.to_string()).is_equal_to("Unknown character in board: !".to_string());
|
|
||||||
|
|
||||||
let error = ParseError::InvalidHouseDoorCount(3);
|
|
||||||
assert_that(&error.to_string()).is_equal_to("House door must have exactly 2 positions, found 3".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_entity_error_display() {
|
|
||||||
let error = EntityError::NodeNotFound(42);
|
|
||||||
assert_that(&error.to_string()).is_equal_to("Node not found in graph: 42".to_string());
|
|
||||||
|
|
||||||
let error = EntityError::EdgeNotFound { from: 1, to: 2 };
|
|
||||||
assert_that(&error.to_string()).is_equal_to("Edge not found: from 1 to 2".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_into_game_error_trait() {
|
fn test_into_game_error_trait() {
|
||||||
let result: Result<i32, io::Error> = Err(io::Error::new(io::ErrorKind::Other, "test error"));
|
let result: Result<i32, io::Error> = Err(io::Error::new(io::ErrorKind::Other, "test error"));
|
||||||
|
|||||||
79
tests/game.rs
Normal file
79
tests/game.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
use pacman::error::{GameError, GameResult};
|
||||||
|
use pacman::game::Game;
|
||||||
|
use speculoos::prelude::*;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
use common::setup_sdl;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_game_30_seconds_60fps() -> GameResult<()> {
|
||||||
|
let (canvas, texture_creator, _sdl_context) = setup_sdl().map_err(GameError::Sdl)?;
|
||||||
|
let ttf_context = sdl2::ttf::init().map_err(GameError::Sdl)?;
|
||||||
|
let event_pump = _sdl_context
|
||||||
|
.event_pump()
|
||||||
|
.map_err(|e| pacman::error::GameError::Sdl(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut game = Game::new(canvas, ttf_context, texture_creator, event_pump)?;
|
||||||
|
|
||||||
|
// Run for 30 seconds at 60 FPS = 1800 frames
|
||||||
|
let frame_time = 1.0 / 60.0;
|
||||||
|
let total_frames = 1800;
|
||||||
|
let mut frame_count = 0;
|
||||||
|
|
||||||
|
for _ in 0..total_frames {
|
||||||
|
let should_exit = game.tick(frame_time);
|
||||||
|
|
||||||
|
if should_exit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
frame_count, total_frames,
|
||||||
|
"Should have processed exactly {} frames",
|
||||||
|
total_frames
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test that runs the game for 30 seconds with variable frame timing
|
||||||
|
#[test]
|
||||||
|
fn test_game_30_seconds_variable_timing() -> GameResult<()> {
|
||||||
|
let (canvas, texture_creator, _sdl_context) = setup_sdl().map_err(GameError::Sdl)?;
|
||||||
|
let ttf_context = sdl2::ttf::init().map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
|
let event_pump = _sdl_context
|
||||||
|
.event_pump()
|
||||||
|
.map_err(|e| pacman::error::GameError::Sdl(e.to_string()))?;
|
||||||
|
|
||||||
|
let mut game = Game::new(canvas, ttf_context, texture_creator, event_pump)?;
|
||||||
|
|
||||||
|
// Simulate 30 seconds with variable frame timing
|
||||||
|
let mut total_time = 0.0;
|
||||||
|
let target_time = 30.0;
|
||||||
|
let mut frame_count = 0;
|
||||||
|
|
||||||
|
while total_time < target_time {
|
||||||
|
// Alternate between different frame rates to simulate real gameplay
|
||||||
|
let frame_time = match frame_count % 4 {
|
||||||
|
0 => 1.0 / 60.0, // 60 FPS
|
||||||
|
1 => 1.0 / 30.0, // 30 FPS (lag spike)
|
||||||
|
2 => 1.0 / 120.0, // 120 FPS (very fast)
|
||||||
|
_ => 1.0 / 60.0, // 60 FPS
|
||||||
|
};
|
||||||
|
|
||||||
|
let should_exit = game.tick(frame_time);
|
||||||
|
|
||||||
|
if should_exit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
total_time += frame_time;
|
||||||
|
frame_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_that(&total_time).is_greater_than_or_equal_to(target_time);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
332
tests/input.rs
332
tests/input.rs
@@ -1,39 +1,321 @@
|
|||||||
|
use glam::Vec2;
|
||||||
use pacman::events::{GameCommand, GameEvent};
|
use pacman::events::{GameCommand, GameEvent};
|
||||||
use pacman::map::direction::Direction;
|
use pacman::map::direction::Direction;
|
||||||
use pacman::systems::input::{process_simple_key_events, Bindings, SimpleKeyEvent};
|
use pacman::systems::input::{
|
||||||
|
calculate_direction_from_delta, process_simple_key_events, update_touch_reference_position, Bindings, CursorPosition,
|
||||||
|
SimpleKeyEvent, TouchData, TouchState, TOUCH_DIRECTION_THRESHOLD, TOUCH_EASING_DISTANCE_THRESHOLD,
|
||||||
|
};
|
||||||
use sdl2::keyboard::Keycode;
|
use sdl2::keyboard::Keycode;
|
||||||
use speculoos::prelude::*;
|
use speculoos::prelude::*;
|
||||||
|
|
||||||
#[test]
|
// Test modules for better organization
|
||||||
fn resumes_previous_direction_when_secondary_key_released() {
|
mod keyboard_tests {
|
||||||
let mut bindings = Bindings::default();
|
use super::*;
|
||||||
|
|
||||||
// Frame 1: Press W (Up) => emits Move Up
|
#[test]
|
||||||
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::W)]);
|
fn key_down_emits_bound_command() {
|
||||||
assert_that(&events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Up)))).is_true();
|
let mut bindings = Bindings::default();
|
||||||
|
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::W)]);
|
||||||
|
assert_that(&events).contains(GameEvent::Command(GameCommand::MovePlayer(Direction::Up)));
|
||||||
|
}
|
||||||
|
|
||||||
// Frame 2: Press D (Right) => emits Move Right
|
#[test]
|
||||||
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::D)]);
|
fn key_down_emits_non_movement_commands() {
|
||||||
assert_that(&events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Right)))).is_true();
|
let mut bindings = Bindings::default();
|
||||||
|
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::P)]);
|
||||||
|
assert_that(&events).contains(GameEvent::Command(GameCommand::TogglePause));
|
||||||
|
}
|
||||||
|
|
||||||
// Frame 3: Release D, no new key this frame => should continue previous key W (Up)
|
#[test]
|
||||||
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyUp(Keycode::D)]);
|
fn unbound_key_emits_nothing() {
|
||||||
assert_that(&events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Up)))).is_true();
|
let mut bindings = Bindings::default();
|
||||||
|
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::Z)]);
|
||||||
|
assert_that(&events).is_empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn movement_key_held_continues_across_frames() {
|
||||||
|
let mut bindings = Bindings::default();
|
||||||
|
process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::Left)]);
|
||||||
|
let events = process_simple_key_events(&mut bindings, &[]);
|
||||||
|
assert_that(&events).contains(GameEvent::Command(GameCommand::MovePlayer(Direction::Left)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn releasing_movement_key_stops_continuation() {
|
||||||
|
let mut bindings = Bindings::default();
|
||||||
|
process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::Up)]);
|
||||||
|
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyUp(Keycode::Up)]);
|
||||||
|
assert_that(&events).is_empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_movement_keys_resumes_previous_when_current_released() {
|
||||||
|
let mut bindings = Bindings::default();
|
||||||
|
process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::W)]);
|
||||||
|
process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::D)]);
|
||||||
|
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyUp(Keycode::D)]);
|
||||||
|
assert_that(&events).contains(GameEvent::Command(GameCommand::MovePlayer(Direction::Up)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
mod direction_calculation_tests {
|
||||||
fn holds_last_pressed_key_across_frames_when_no_new_input() {
|
use super::*;
|
||||||
let mut bindings = Bindings::default();
|
|
||||||
|
|
||||||
// Frame 1: Press Left
|
#[test]
|
||||||
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyDown(Keycode::Left)]);
|
fn prioritizes_horizontal_movement() {
|
||||||
assert_that(&events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Left)))).is_true();
|
let test_cases = vec![
|
||||||
|
(Vec2::new(6.0, 5.0), Direction::Right),
|
||||||
|
(Vec2::new(-6.0, 5.0), Direction::Left),
|
||||||
|
];
|
||||||
|
|
||||||
// Frame 2: No input => continues Left
|
for (delta, expected) in test_cases {
|
||||||
let events = process_simple_key_events(&mut bindings, &[]);
|
assert_that(&calculate_direction_from_delta(delta)).is_equal_to(expected);
|
||||||
assert_that(&events.contains(&GameEvent::Command(GameCommand::MovePlayer(Direction::Left)))).is_true();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Frame 3: Release Left, no input remains => nothing emitted
|
#[test]
|
||||||
let events = process_simple_key_events(&mut bindings, &[SimpleKeyEvent::KeyUp(Keycode::Left)]);
|
fn uses_vertical_when_dominant() {
|
||||||
assert_that(&events.is_empty()).is_true();
|
let test_cases = vec![
|
||||||
|
(Vec2::new(3.0, 10.0), Direction::Down),
|
||||||
|
(Vec2::new(3.0, -10.0), Direction::Up),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (delta, expected) in test_cases {
|
||||||
|
assert_that(&calculate_direction_from_delta(delta)).is_equal_to(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_zero_delta() {
|
||||||
|
let delta = Vec2::ZERO;
|
||||||
|
// Should default to Up when both components are zero
|
||||||
|
assert_that(&calculate_direction_from_delta(delta)).is_equal_to(Direction::Up);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn handles_equal_magnitudes() {
|
||||||
|
// When x and y have equal absolute values, should prioritize vertical
|
||||||
|
let delta = Vec2::new(5.0, 5.0);
|
||||||
|
assert_that(&calculate_direction_from_delta(delta)).is_equal_to(Direction::Down);
|
||||||
|
|
||||||
|
let delta = Vec2::new(-5.0, 5.0);
|
||||||
|
assert_that(&calculate_direction_from_delta(delta)).is_equal_to(Direction::Down);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod touch_easing_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn easing_within_threshold_does_nothing() {
|
||||||
|
let mut touch_data = TouchData::new(0, Vec2::new(100.0, 100.0));
|
||||||
|
touch_data.current_pos = Vec2::new(100.0 + TOUCH_EASING_DISTANCE_THRESHOLD - 0.1, 100.0);
|
||||||
|
|
||||||
|
let (_delta, distance) = update_touch_reference_position(&mut touch_data, 0.016);
|
||||||
|
|
||||||
|
assert_that(&distance).is_less_than(TOUCH_EASING_DISTANCE_THRESHOLD);
|
||||||
|
assert_that(&touch_data.start_pos).is_equal_to(Vec2::new(100.0, 100.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn easing_beyond_threshold_moves_towards_target() {
|
||||||
|
let mut touch_data = TouchData::new(0, Vec2::new(100.0, 100.0));
|
||||||
|
touch_data.current_pos = Vec2::new(150.0, 100.0);
|
||||||
|
|
||||||
|
let original_start_pos = touch_data.start_pos;
|
||||||
|
let (_delta, distance) = update_touch_reference_position(&mut touch_data, 0.016);
|
||||||
|
|
||||||
|
assert_that(&distance).is_greater_than(TOUCH_EASING_DISTANCE_THRESHOLD);
|
||||||
|
assert_that(&touch_data.start_pos.x).is_greater_than(original_start_pos.x);
|
||||||
|
assert_that(&touch_data.start_pos.x).is_less_than(touch_data.current_pos.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn easing_overshoot_sets_to_target() {
|
||||||
|
let mut touch_data = TouchData::new(0, Vec2::new(100.0, 100.0));
|
||||||
|
touch_data.current_pos = Vec2::new(101.0, 100.0);
|
||||||
|
|
||||||
|
let (_delta, _distance) = update_touch_reference_position(&mut touch_data, 10.0);
|
||||||
|
|
||||||
|
assert_that(&touch_data.start_pos).is_equal_to(touch_data.current_pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn easing_returns_correct_delta() {
|
||||||
|
let mut touch_data = TouchData::new(0, Vec2::new(100.0, 100.0));
|
||||||
|
touch_data.current_pos = Vec2::new(120.0, 110.0);
|
||||||
|
|
||||||
|
let (delta, distance) = update_touch_reference_position(&mut touch_data, 0.016);
|
||||||
|
|
||||||
|
let expected_delta = Vec2::new(20.0, 10.0);
|
||||||
|
let expected_distance = expected_delta.length();
|
||||||
|
|
||||||
|
assert_that(&delta).is_equal_to(expected_delta);
|
||||||
|
assert_that(&distance).is_equal_to(expected_distance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integration tests for the full input system
|
||||||
|
mod integration_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn mouse_motion_event(x: i32, y: i32) -> sdl2::event::Event {
|
||||||
|
sdl2::event::Event::MouseMotion {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
xrel: 0,
|
||||||
|
yrel: 0,
|
||||||
|
mousestate: sdl2::mouse::MouseState::from_sdl_state(0),
|
||||||
|
which: 0,
|
||||||
|
window_id: 0,
|
||||||
|
timestamp: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_button_down_event(x: i32, y: i32) -> sdl2::event::Event {
|
||||||
|
sdl2::event::Event::MouseButtonDown {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
mouse_btn: sdl2::mouse::MouseButton::Left,
|
||||||
|
clicks: 1,
|
||||||
|
which: 0,
|
||||||
|
window_id: 0,
|
||||||
|
timestamp: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mouse_button_up_event(x: i32, y: i32) -> sdl2::event::Event {
|
||||||
|
sdl2::event::Event::MouseButtonUp {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
mouse_btn: sdl2::mouse::MouseButton::Left,
|
||||||
|
clicks: 1,
|
||||||
|
which: 0,
|
||||||
|
window_id: 0,
|
||||||
|
timestamp: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplified helper for testing SDL integration
|
||||||
|
fn run_input_system_with_events(events: Vec<sdl2::event::Event>, delta_time: f32) -> (CursorPosition, TouchState) {
|
||||||
|
use bevy_ecs::{event::Events, system::RunSystemOnce, world::World};
|
||||||
|
use pacman::systems::components::DeltaTime;
|
||||||
|
use pacman::systems::input::input_system;
|
||||||
|
|
||||||
|
let sdl_context = sdl2::init().expect("Failed to initialize SDL");
|
||||||
|
let event_subsystem = sdl_context.event().expect("Failed to get event subsystem");
|
||||||
|
let event_pump = sdl_context.event_pump().expect("Failed to create event pump");
|
||||||
|
|
||||||
|
let mut world = World::new();
|
||||||
|
world.insert_resource(Events::<GameEvent>::default());
|
||||||
|
world.insert_resource(DeltaTime {
|
||||||
|
seconds: delta_time,
|
||||||
|
ticks: 1,
|
||||||
|
});
|
||||||
|
world.insert_resource(Bindings::default());
|
||||||
|
world.insert_resource(CursorPosition::None);
|
||||||
|
world.insert_resource(TouchState::default());
|
||||||
|
world.insert_non_send_resource(event_pump);
|
||||||
|
|
||||||
|
// Inject events into SDL's event queue
|
||||||
|
for event in events {
|
||||||
|
event_subsystem.push_event(event).expect("Failed to push event");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the real input system
|
||||||
|
world
|
||||||
|
.run_system_once(input_system)
|
||||||
|
.expect("Input system should run successfully");
|
||||||
|
|
||||||
|
let cursor = *world.resource::<CursorPosition>();
|
||||||
|
let touch_state = world.resource::<TouchState>().clone();
|
||||||
|
|
||||||
|
(cursor, touch_state)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mouse_motion_updates_cursor_position() {
|
||||||
|
let events = vec![mouse_motion_event(100, 200)];
|
||||||
|
let (cursor, _touch_state) = run_input_system_with_events(events, 0.016);
|
||||||
|
|
||||||
|
match cursor {
|
||||||
|
CursorPosition::Some {
|
||||||
|
position,
|
||||||
|
remaining_time,
|
||||||
|
} => {
|
||||||
|
assert_that(&position).is_equal_to(Vec2::new(100.0, 200.0));
|
||||||
|
assert_that(&remaining_time).is_equal_to(0.20);
|
||||||
|
}
|
||||||
|
CursorPosition::None => panic!("Expected cursor position to be set"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mouse_button_down_starts_touch() {
|
||||||
|
let events = vec![mouse_button_down_event(150, 250)];
|
||||||
|
let (_cursor, touch_state) = run_input_system_with_events(events, 0.016);
|
||||||
|
|
||||||
|
assert_that(&touch_state.active_touch).is_some();
|
||||||
|
if let Some(touch_data) = &touch_state.active_touch {
|
||||||
|
assert_that(&touch_data.finger_id).is_equal_to(0);
|
||||||
|
assert_that(&touch_data.start_pos).is_equal_to(Vec2::new(150.0, 250.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mouse_button_up_ends_touch() {
|
||||||
|
let events = vec![mouse_button_down_event(150, 250), mouse_button_up_event(150, 250)];
|
||||||
|
let (_cursor, touch_state) = run_input_system_with_events(events, 0.016);
|
||||||
|
|
||||||
|
assert_that(&touch_state.active_touch).is_none();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch direction tests
|
||||||
|
mod touch_direction_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn movement_above_threshold_emits_direction() {
|
||||||
|
let mut touch_data = TouchData::new(1, Vec2::new(100.0, 100.0));
|
||||||
|
touch_data.current_pos = Vec2::new(100.0 + TOUCH_DIRECTION_THRESHOLD + 5.0, 100.0);
|
||||||
|
|
||||||
|
let (delta, distance) = update_touch_reference_position(&mut touch_data, 0.016);
|
||||||
|
|
||||||
|
assert_that(&distance).is_greater_than_or_equal_to(TOUCH_DIRECTION_THRESHOLD);
|
||||||
|
let direction = calculate_direction_from_delta(delta);
|
||||||
|
assert_that(&direction).is_equal_to(Direction::Right);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn movement_below_threshold_no_direction() {
|
||||||
|
let mut touch_data = TouchData::new(1, Vec2::new(100.0, 100.0));
|
||||||
|
touch_data.current_pos = Vec2::new(100.0 + TOUCH_DIRECTION_THRESHOLD - 1.0, 100.0);
|
||||||
|
|
||||||
|
let (_delta, distance) = update_touch_reference_position(&mut touch_data, 0.016);
|
||||||
|
|
||||||
|
assert_that(&distance).is_less_than(TOUCH_DIRECTION_THRESHOLD);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_directions_work_correctly() {
|
||||||
|
let test_cases = vec![
|
||||||
|
(Vec2::new(TOUCH_DIRECTION_THRESHOLD + 5.0, 0.0), Direction::Right),
|
||||||
|
(Vec2::new(-TOUCH_DIRECTION_THRESHOLD - 5.0, 0.0), Direction::Left),
|
||||||
|
(Vec2::new(0.0, TOUCH_DIRECTION_THRESHOLD + 5.0), Direction::Down),
|
||||||
|
(Vec2::new(0.0, -TOUCH_DIRECTION_THRESHOLD - 5.0), Direction::Up),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (offset, expected_direction) in test_cases {
|
||||||
|
let mut touch_data = TouchData::new(1, Vec2::new(100.0, 100.0));
|
||||||
|
touch_data.current_pos = Vec2::new(100.0, 100.0) + offset;
|
||||||
|
|
||||||
|
let (delta, distance) = update_touch_reference_position(&mut touch_data, 0.016);
|
||||||
|
|
||||||
|
assert_that(&distance).is_greater_than_or_equal_to(TOUCH_DIRECTION_THRESHOLD);
|
||||||
|
let direction = calculate_direction_from_delta(delta);
|
||||||
|
assert_that(&direction).is_equal_to(expected_direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
use pacman::constants::{CELL_SIZE, RAW_BOARD};
|
use pacman::constants::{CELL_SIZE, RAW_BOARD};
|
||||||
use pacman::map::builder::Map;
|
use pacman::map::builder::Map;
|
||||||
|
use pacman::map::graph::TraversalFlags;
|
||||||
use speculoos::prelude::*;
|
use speculoos::prelude::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_map_creation() {
|
fn test_map_creation_success() {
|
||||||
let map = Map::new(RAW_BOARD).unwrap();
|
let map = Map::new(RAW_BOARD).unwrap();
|
||||||
|
|
||||||
assert_that(&map.graph.nodes().count()).is_greater_than(0);
|
assert_that(&map.graph.nodes().count()).is_greater_than(0);
|
||||||
@@ -22,7 +23,7 @@ fn test_map_creation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_map_node_positions() {
|
fn test_map_node_positions_accuracy() {
|
||||||
let map = Map::new(RAW_BOARD).unwrap();
|
let map = Map::new(RAW_BOARD).unwrap();
|
||||||
|
|
||||||
for (grid_pos, &node_id) in &map.grid_to_node {
|
for (grid_pos, &node_id) in &map.grid_to_node {
|
||||||
@@ -35,3 +36,54 @@ fn test_map_node_positions() {
|
|||||||
assert_that(&node.position).is_equal_to(expected_pos);
|
assert_that(&node.position).is_equal_to(expected_pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_start_positions_are_valid() {
|
||||||
|
let map = Map::new(RAW_BOARD).unwrap();
|
||||||
|
let positions = &map.start_positions;
|
||||||
|
|
||||||
|
// All start positions should exist in the graph
|
||||||
|
assert_that(&map.graph.get_node(positions.pacman)).is_some();
|
||||||
|
assert_that(&map.graph.get_node(positions.blinky)).is_some();
|
||||||
|
assert_that(&map.graph.get_node(positions.pinky)).is_some();
|
||||||
|
assert_that(&map.graph.get_node(positions.inky)).is_some();
|
||||||
|
assert_that(&map.graph.get_node(positions.clyde)).is_some();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ghost_house_has_ghost_only_entrance() {
|
||||||
|
let map = Map::new(RAW_BOARD).unwrap();
|
||||||
|
|
||||||
|
// Find the house entrance node
|
||||||
|
let house_entrance = map.start_positions.blinky;
|
||||||
|
|
||||||
|
// Check that there's a ghost-only connection from the house entrance
|
||||||
|
let mut has_ghost_only_connection = false;
|
||||||
|
for edge in map.graph.adjacency_list[house_entrance as usize].edges() {
|
||||||
|
if edge.traversal_flags == TraversalFlags::GHOST {
|
||||||
|
has_ghost_only_connection = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_that(&has_ghost_only_connection).is_true();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tunnel_connections_exist() {
|
||||||
|
let map = Map::new(RAW_BOARD).unwrap();
|
||||||
|
|
||||||
|
// Find tunnel nodes by looking for nodes with zero-distance connections
|
||||||
|
let mut has_tunnel_connection = false;
|
||||||
|
for intersection in &map.graph.adjacency_list {
|
||||||
|
for edge in intersection.edges() {
|
||||||
|
if edge.distance == 0.0f32 {
|
||||||
|
has_tunnel_connection = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if has_tunnel_connection {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_that(&has_tunnel_connection).is_true();
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,25 +73,6 @@ fn test_default_zero_timing_for_unused_systems() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pre_populated_timing_entries() {
|
|
||||||
let timings = SystemTimings::default();
|
|
||||||
|
|
||||||
// Verify that we can add timing to any SystemId without panicking
|
|
||||||
// (this would fail with the old implementation if the entry didn't exist)
|
|
||||||
// Use the same tick for all systems to avoid zero-padding
|
|
||||||
for id in SystemId::iter() {
|
|
||||||
timings.add_timing(id, Duration::from_nanos(1), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify all systems now have non-zero timing
|
|
||||||
let stats = timings.get_stats(1);
|
|
||||||
for id in SystemId::iter() {
|
|
||||||
let (avg, _) = stats.get(&id).unwrap();
|
|
||||||
assert_that(&(*avg > Duration::ZERO)).is_true();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_total_system_timing() {
|
fn test_total_system_timing() {
|
||||||
let timings = SystemTimings::default();
|
let timings = SystemTimings::default();
|
||||||
|
|||||||
115
tests/ttf.rs
Normal file
115
tests/ttf.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use pacman::texture::ttf::{TtfAtlas, TtfRenderer};
|
||||||
|
use sdl2::pixels::Color;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_width_calculates_correctly_for_empty_string() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer = TtfRenderer::new(1.0);
|
||||||
|
let width = renderer.text_width(&atlas, "");
|
||||||
|
|
||||||
|
assert_eq!(width, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_width_calculates_correctly_for_single_character() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer = TtfRenderer::new(1.0);
|
||||||
|
let width = renderer.text_width(&atlas, "A");
|
||||||
|
|
||||||
|
assert!(width > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_width_scales_correctly() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer1 = TtfRenderer::new(1.0);
|
||||||
|
let renderer2 = TtfRenderer::new(2.0);
|
||||||
|
|
||||||
|
let width1 = renderer1.text_width(&atlas, "Test");
|
||||||
|
let width2 = renderer2.text_width(&atlas, "Test");
|
||||||
|
|
||||||
|
assert_eq!(width2, width1 * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_height_returns_non_zero_for_valid_atlas() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer = TtfRenderer::new(1.0);
|
||||||
|
let height = renderer.text_height(&atlas);
|
||||||
|
|
||||||
|
assert!(height > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_height_scales_correctly() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer1 = TtfRenderer::new(1.0);
|
||||||
|
let renderer2 = TtfRenderer::new(2.0);
|
||||||
|
|
||||||
|
let height1 = renderer1.text_height(&atlas);
|
||||||
|
let height2 = renderer2.text_height(&atlas);
|
||||||
|
|
||||||
|
assert_eq!(height2, height1 * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_text_handles_empty_string() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer = TtfRenderer::new(1.0);
|
||||||
|
let result = renderer.render_text(&mut canvas, &mut atlas, "", glam::Vec2::new(0.0, 0.0), Color::WHITE);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_text_handles_single_character() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer = TtfRenderer::new(1.0);
|
||||||
|
let result = renderer.render_text(&mut canvas, &mut atlas, "A", glam::Vec2::new(10.0, 10.0), Color::RED);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user