Compare commits

..

26 Commits

Author SHA1 Message Date
Ryan Walters
9c274de901 feat: setup dying sprites with sprite validation tests 2025-09-06 12:15:08 -05:00
Ryan Walters
9633611ae8 fix: downgrade to codecov-action v4, update escapes pattern, ignore codecov.json, slim codecov config 2025-09-06 12:15:07 -05:00
Ryan Walters
897b9b8621 fix: switch from lcov to codecov.json for Codecov reporting 2025-09-06 12:15:07 -05:00
Ryan Walters
ee2569b70c ci: drop coveralls, add codecov config, change badge 2025-09-06 12:15:07 -05:00
Ryan Walters
84caa6c25f ci: setup codecov coverage 2025-09-06 12:15:06 -05:00
Ryan Walters
f92c9175b9 test: add ttf renderer tests 2025-09-06 12:15:06 -05:00
Ryan Walters
d561b446c5 test: remove useless/redundant tests 2025-09-06 12:15:05 -05:00
Ryan Walters
9219c771d7 test: improve input & map_builder test coverage 2025-09-06 12:15:05 -05:00
Ryan Walters
cd501aafc4 test: general game testing 2025-09-06 12:15:05 -05:00
Ryan Walters
feae1ee191 test: add asset tests, file exists & has min size 2025-09-06 12:15:04 -05:00
Ryan Walters
2f0b9825c6 test: blinking system tests 2025-09-06 12:15:04 -05:00
Ryan Walters
cac490565e refactor: use speculoos for all test assertions 2025-09-06 12:15:04 -05:00
Ryan Walters
b60888219b fix: remove unused BlinkingTexture 2025-09-06 12:15:03 -05:00
Ryan Walters
3c50bfeab6 refactor: add ticks to DeltaTime, rewrite Blinking system for tick-based calculations with absolute calculations, rewrite Blinking/Direction tests 2025-09-06 12:15:03 -05:00
Ryan Walters
132067c573 feat: re-implement CustomFormatter to clone Full formatterr 2025-09-06 12:15:03 -05:00
Ryan Walters
42e309a46b feat: enhance profiling with tick-based timing management and zero-padding for skipped frames 2025-09-06 12:15:02 -05:00
Ryan Walters
a38423f006 refactor: use welford's algorithm for one-pass avg/std dev. calculations, input logging tweaks 2025-09-06 12:15:02 -05:00
Ryan Walters
07bd127596 chore: move ttf context out of game.rs, remove unnecessary window event logging 2025-09-06 12:15:01 -05:00
Ryan Walters
da42d017e7 refactor: reorganize game.rs new() into separate functions 2025-09-06 12:15:01 -05:00
Ryan Walters
8b623ffabe feat: sprite enums for avoiding hardcoded string paths 2025-09-06 12:15:01 -05:00
Ryan Walters
af81390e30 fix: use LARGE_SCALE for BatchedLineResource calculations 2025-09-06 12:15:00 -05:00
Ryan Walters
2fabd5d7a2 feat: measure total system timings using threading indifferent method, padded formatting 2025-09-06 12:15:00 -05:00
Ryan Walters
bcd9865430 chore: move BufferedWriter into tracing_buffer.rs 2025-09-06 12:15:00 -05:00
Ryan Walters
ed16da1e8f feat: special formatting with game tick counter, remove date from tracing formatter 2025-09-06 12:14:59 -05:00
Ryan Walters
14882531c9 fix(ci): allow dead code in buffered_writer & tracing_buffer for desktop non-windows checks 2025-09-06 12:14:59 -05:00
Ryan Walters
2d36d49b13 feat: enumerate and display render driver info, increase node id text opacity 2025-09-06 12:14:59 -05:00
7 changed files with 144 additions and 81 deletions

View File

@@ -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,41 +46,12 @@ jobs:
- name: Generate coverage report - name: Generate coverage report
run: | run: |
just coverage just coverage-codecov
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v4
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info files: ./codecov.json
disable_search: true
- name: Download Coveralls CLI verbose: true
if: ${{ env.COVERALLS_REPO_TOKEN != '' }}
run: |
# use GitHub Releases URL instead of coveralls.io because they can't maintain their own files; it 404s
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
- name: Upload coverage to Coveralls
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
View File

@@ -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

View File

@@ -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

3
codecov.yml Normal file
View File

@@ -0,0 +1,3 @@
ignore:
- "src/(?:bin|platform))/.+\\.rs"
- "src/(?:app|events|formatter)\\.rs"

View File

@@ -266,7 +266,7 @@ pub fn debug_render_system(
} }
canvas.set_draw_color(Color { canvas.set_draw_color(Color {
a: f32_to_u8(0.6), a: f32_to_u8(0.65),
..Color::RED ..Color::RED
}); });
canvas.set_blend_mode(sdl2::render::BlendMode::Blend); canvas.set_blend_mode(sdl2::render::BlendMode::Blend);

View File

@@ -15,6 +15,8 @@ pub enum PacmanSprite {
Moving(Direction, u8), Moving(Direction, u8),
/// The full, closed-mouth Pac-Man sprite. /// The full, closed-mouth Pac-Man sprite.
Full, Full,
/// A single frame of the dying animation.
Dying(u8),
} }
/// Represents the color of a frightened ghost. /// Represents the color of a frightened ghost.
@@ -60,45 +62,50 @@ impl GameSprite {
/// This path corresponds to the filename in the texture atlas JSON file. /// This path corresponds to the filename in the texture atlas JSON file.
pub fn to_path(self) -> String { pub fn to_path(self) -> String {
match self { match self {
GameSprite::Pacman(sprite) => match sprite { GameSprite::Pacman(PacmanSprite::Moving(dir, frame)) => format!(
PacmanSprite::Moving(dir, frame) => { "pacman/{}_{}.png",
let frame_char = match frame { dir.as_ref(),
0 => 'a', match frame {
1 => 'b', 0 => "a",
_ => panic!("Invalid animation frame"), 1 => "b",
}; _ => panic!("Invalid animation frame"),
format!("pacman/{}_{}.png", dir.as_ref().to_lowercase(), frame_char)
} }
PacmanSprite::Full => "pacman/full.png".to_string(), ),
}, GameSprite::Pacman(PacmanSprite::Full) => "pacman/full.png".to_string(),
GameSprite::Ghost(sprite) => match sprite { GameSprite::Pacman(PacmanSprite::Dying(frame)) => format!("pacman/death/{}.png", frame),
GhostSprite::Normal(ghost, dir, frame) => {
let frame_char = match frame { // Ghost sprites
0 => 'a', GameSprite::Ghost(GhostSprite::Normal(ghost_type, dir, frame)) => {
1 => 'b', let frame_char = match frame {
_ => panic!("Invalid animation frame"), 0 => 'a',
}; 1 => 'b',
format!("ghost/{}/{}_{}.png", ghost.as_str(), dir.as_ref().to_lowercase(), frame_char) _ => panic!("Invalid animation frame"),
} };
GhostSprite::Frightened(color, frame) => { format!(
let frame_char = match frame { "ghost/{}/{}_{}.png",
0 => 'a', ghost_type.as_str(),
1 => 'b', dir.as_ref().to_lowercase(),
_ => panic!("Invalid animation frame"), frame_char
}; )
let color_str = match color { }
FrightenedColor::Blue => "blue", GameSprite::Ghost(GhostSprite::Frightened(color, frame)) => {
FrightenedColor::White => "white", let frame_char = match frame {
}; 0 => 'a',
format!("ghost/frightened/{}_{}.png", color_str, frame_char) 1 => 'b',
} _ => panic!("Invalid animation frame"),
GhostSprite::Eyes(dir) => format!("ghost/eyes/{}.png", dir.as_ref().to_lowercase()), };
}, let color_str = match color {
GameSprite::Maze(sprite) => match sprite { FrightenedColor::Blue => "blue",
MazeSprite::Tile(index) => format!("maze/tiles/{}.png", index), FrightenedColor::White => "white",
MazeSprite::Pellet => "maze/pellet.png".to_string(), };
MazeSprite::Energizer => "maze/energizer.png".to_string(), format!("ghost/frightened/{}_{}.png", color_str, frame_char)
}, }
GameSprite::Ghost(GhostSprite::Eyes(dir)) => format!("ghost/eyes/{}.png", dir.as_ref().to_lowercase()),
// Maze sprites
GameSprite::Maze(MazeSprite::Tile(index)) => format!("maze/tiles/{}.png", index),
GameSprite::Maze(MazeSprite::Pellet) => "maze/pellet.png".to_string(),
GameSprite::Maze(MazeSprite::Energizer) => "maze/energizer.png".to_string(),
} }
} }
} }

73
tests/sprites.rs Normal file
View File

@@ -0,0 +1,73 @@
//! Tests for the sprite path generation.
use pacman::{
game::ATLAS_FRAMES,
map::direction::Direction,
systems::components::Ghost,
texture::sprites::{FrightenedColor, GameSprite, GhostSprite, MazeSprite, PacmanSprite},
};
#[test]
fn test_all_sprite_paths_exist() {
let mut sprites_to_test = Vec::new();
// Pac-Man sprites
for &dir in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
for frame in 0..2 {
sprites_to_test.push(GameSprite::Pacman(PacmanSprite::Moving(dir, frame)));
}
}
sprites_to_test.push(GameSprite::Pacman(PacmanSprite::Full));
for frame in 0..=10 {
sprites_to_test.push(GameSprite::Pacman(PacmanSprite::Dying(frame)));
}
// Ghost sprites
for &ghost in &[Ghost::Blinky, Ghost::Pinky, Ghost::Inky, Ghost::Clyde] {
for &dir in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
for frame in 0..2 {
sprites_to_test.push(GameSprite::Ghost(GhostSprite::Normal(ghost, dir, frame)));
}
sprites_to_test.push(GameSprite::Ghost(GhostSprite::Eyes(dir)));
}
}
for &color in &[FrightenedColor::Blue, FrightenedColor::White] {
for frame in 0..2 {
sprites_to_test.push(GameSprite::Ghost(GhostSprite::Frightened(color, frame)));
}
}
// Maze sprites
for i in 0..=34 {
sprites_to_test.push(GameSprite::Maze(MazeSprite::Tile(i)));
}
sprites_to_test.push(GameSprite::Maze(MazeSprite::Pellet));
sprites_to_test.push(GameSprite::Maze(MazeSprite::Energizer));
for sprite in sprites_to_test {
let path = sprite.to_path();
assert!(
ATLAS_FRAMES.contains_key(&path),
"Sprite path '{}' does not exist in the atlas.",
path
);
}
}
#[test]
fn test_invalid_sprite_paths_do_not_exist() {
let invalid_sprites = vec![
// An invalid Pac-Man dying frame
GameSprite::Pacman(PacmanSprite::Dying(99)),
// An invalid maze tile
GameSprite::Maze(MazeSprite::Tile(99)),
];
for sprite in invalid_sprites {
let path = sprite.to_path();
assert!(
!ATLAS_FRAMES.contains_key(&path),
"Invalid sprite path '{}' was found in the atlas, but it should not exist.",
path
);
}
}