mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 07:15:41 -06:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5deccc54a7 | ||
|
|
2455d9724b | ||
|
|
ac7c1b9ce1 | ||
|
|
d68d76c854 | ||
|
|
f1927cc67e | ||
|
|
68ab4627d8 | ||
|
|
0d8d869580 | ||
|
|
a31b85b5df | ||
|
|
21b08d4866 | ||
|
|
f075caaa17 | ||
|
|
9422168ffc | ||
|
|
35e557e298 | ||
|
|
e810419063 | ||
|
|
f7e7dee28f | ||
|
|
4b0b8f4f2e | ||
|
|
03249c88a4 | ||
|
|
2d4f97e04b | ||
|
|
317fce796c | ||
|
|
9832abd131 | ||
|
|
c94ebc6b4b | ||
|
|
8b23c1c7bd | ||
|
|
5e325a4691 |
41
.github/workflows/coverage.yaml
vendored
41
.github/workflows/coverage.yaml
vendored
@@ -9,6 +9,8 @@ env:
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
@@ -46,12 +48,41 @@ jobs:
|
||||
|
||||
- name: Generate coverage report
|
||||
run: |
|
||||
just coverage-codecov
|
||||
just coverage
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./codecov.json
|
||||
disable_search: true
|
||||
verbose: true
|
||||
files: lcov.info
|
||||
|
||||
- name: Download Coveralls CLI
|
||||
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
1
.gitignore
vendored
@@ -14,7 +14,6 @@ assets/site/build.css
|
||||
|
||||
# Coverage reports
|
||||
lcov.info
|
||||
codecov.json
|
||||
coverage.html
|
||||
|
||||
# Profiling output
|
||||
|
||||
22
Justfile
22
Justfile
@@ -2,8 +2,8 @@ set shell := ["bash", "-c"]
|
||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
||||
|
||||
# Regex to exclude files from coverage report, double escapes for Justfile + CLI
|
||||
# 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"
|
||||
# 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"
|
||||
|
||||
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
|
||||
|
||||
# Generate HTML report (for humans, source line inspection)
|
||||
html: coverage-lcov
|
||||
html: coverage
|
||||
cargo llvm-cov report \
|
||||
--remap-path-prefix \
|
||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
||||
@@ -19,13 +19,13 @@ html: coverage-lcov
|
||||
--open
|
||||
|
||||
# Display report (for humans)
|
||||
report-coverage: coverage-lcov
|
||||
report-coverage: coverage
|
||||
cargo llvm-cov report \
|
||||
--remap-path-prefix \
|
||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}"
|
||||
|
||||
# Run & generate LCOV report (as base report)
|
||||
coverage-lcov:
|
||||
# Run & generate report (for CI)
|
||||
coverage:
|
||||
cargo llvm-cov \
|
||||
--lcov \
|
||||
--remap-path-prefix \
|
||||
@@ -34,16 +34,6 @@ coverage-lcov:
|
||||
--profile coverage \
|
||||
--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'
|
||||
samply:
|
||||
cargo build --profile profile
|
||||
|
||||
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
ignore:
|
||||
- "src/(?:bin|platform))/.+\\.rs"
|
||||
- "src/(?:app|events|formatter)\\.rs"
|
||||
@@ -19,8 +19,6 @@ pub enum Asset {
|
||||
AtlasImage,
|
||||
/// Terminal Vector font for text rendering (TerminalVector.ttf)
|
||||
Font,
|
||||
/// Sound effect for Pac-Man's death
|
||||
DeathSound,
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
@@ -39,7 +37,6 @@ impl Asset {
|
||||
Wav4 => "sound/waka/4.ogg",
|
||||
AtlasImage => "atlas.png",
|
||||
Font => "TerminalVector.ttf",
|
||||
DeathSound => "sound/pacman_death.wav",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
src/audio.rs
45
src/audio.rs
@@ -16,7 +16,6 @@ const SOUND_ASSETS: [Asset; 4] = [Asset::Wav1, Asset::Wav2, Asset::Wav3, Asset::
|
||||
pub struct Audio {
|
||||
_mixer_context: Option<mixer::Sdl2MixerContext>,
|
||||
sounds: Vec<Chunk>,
|
||||
death_sound: Option<Chunk>,
|
||||
next_sound_index: usize,
|
||||
muted: bool,
|
||||
disabled: bool,
|
||||
@@ -45,7 +44,6 @@ impl Audio {
|
||||
return Self {
|
||||
_mixer_context: None,
|
||||
sounds: Vec::new(),
|
||||
death_sound: None,
|
||||
next_sound_index: 0,
|
||||
muted: false,
|
||||
disabled: true,
|
||||
@@ -67,7 +65,6 @@ impl Audio {
|
||||
return Self {
|
||||
_mixer_context: None,
|
||||
sounds: Vec::new(),
|
||||
death_sound: None,
|
||||
next_sound_index: 0,
|
||||
muted: false,
|
||||
disabled: true,
|
||||
@@ -96,33 +93,12 @@ impl Audio {
|
||||
}
|
||||
}
|
||||
|
||||
let death_sound = match get_asset_bytes(Asset::DeathSound) {
|
||||
Ok(data) => match RWops::from_bytes(&data) {
|
||||
Ok(rwops) => match rwops.load_wav() {
|
||||
Ok(chunk) => Some(chunk),
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to load death sound from asset API: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to create RWops for death sound: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::warn!("Failed to load death sound asset: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// If no sounds loaded successfully, disable audio
|
||||
if sounds.is_empty() && death_sound.is_none() {
|
||||
if sounds.is_empty() {
|
||||
tracing::warn!("No sounds loaded successfully. Audio will be disabled.");
|
||||
return Self {
|
||||
_mixer_context: Some(mixer_context),
|
||||
sounds: Vec::new(),
|
||||
death_sound: None,
|
||||
next_sound_index: 0,
|
||||
muted: false,
|
||||
disabled: true,
|
||||
@@ -132,7 +108,6 @@ impl Audio {
|
||||
Audio {
|
||||
_mixer_context: Some(mixer_context),
|
||||
sounds,
|
||||
death_sound,
|
||||
next_sound_index: 0,
|
||||
muted: false,
|
||||
disabled: false,
|
||||
@@ -163,24 +138,6 @@ impl Audio {
|
||||
self.next_sound_index = (self.next_sound_index + 1) % self.sounds.len();
|
||||
}
|
||||
|
||||
/// Plays the death sound effect.
|
||||
pub fn death(&mut self) {
|
||||
if self.disabled || self.muted {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(chunk) = &self.death_sound {
|
||||
mixer::Channel::all().play(chunk, 0).ok();
|
||||
}
|
||||
}
|
||||
|
||||
/// Halts all currently playing audio channels.
|
||||
pub fn stop_all(&mut self) {
|
||||
if !self.disabled {
|
||||
mixer::Channel::all().halt();
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantly mutes or unmutes all audio channels by adjusting their volume.
|
||||
///
|
||||
/// Sets all 4 mixer channels to zero volume when muting, or restores them to
|
||||
|
||||
@@ -65,7 +65,6 @@ pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
|
||||
Asset::Wav4 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/4.ogg"))),
|
||||
Asset::AtlasImage => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.png"))),
|
||||
Asset::Font => Ok(Cow::Borrowed(include_bytes!("../../assets/game/TerminalVector.ttf"))),
|
||||
Asset::DeathSound => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman_death.wav"))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,6 @@ pub struct AudioState {
|
||||
pub enum AudioEvent {
|
||||
/// Play the "eat" sound when Pac-Man consumes a pellet
|
||||
PlayEat,
|
||||
/// Play the death sound
|
||||
PlayDeath,
|
||||
/// Stop all currently playing sounds
|
||||
StopAll,
|
||||
}
|
||||
|
||||
/// Non-send resource wrapper for SDL2 audio system
|
||||
@@ -63,16 +59,6 @@ pub fn audio_system(
|
||||
// 4 eat sounds available
|
||||
}
|
||||
}
|
||||
AudioEvent::PlayDeath => {
|
||||
if !audio.0.is_disabled() && !audio_state.muted {
|
||||
audio.0.death();
|
||||
}
|
||||
}
|
||||
AudioEvent::StopAll => {
|
||||
if !audio.0.is_disabled() {
|
||||
audio.0.stop_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ pub fn debug_render_system(
|
||||
}
|
||||
|
||||
canvas.set_draw_color(Color {
|
||||
a: f32_to_u8(0.65),
|
||||
a: f32_to_u8(0.6),
|
||||
..Color::RED
|
||||
});
|
||||
canvas.set_blend_mode(sdl2::render::BlendMode::Blend);
|
||||
|
||||
@@ -15,8 +15,6 @@ pub enum PacmanSprite {
|
||||
Moving(Direction, u8),
|
||||
/// The full, closed-mouth Pac-Man sprite.
|
||||
Full,
|
||||
/// A single frame of the dying animation.
|
||||
Dying(u8),
|
||||
}
|
||||
|
||||
/// Represents the color of a frightened ghost.
|
||||
@@ -62,50 +60,45 @@ impl GameSprite {
|
||||
/// This path corresponds to the filename in the texture atlas JSON file.
|
||||
pub fn to_path(self) -> String {
|
||||
match self {
|
||||
GameSprite::Pacman(PacmanSprite::Moving(dir, frame)) => format!(
|
||||
"pacman/{}_{}.png",
|
||||
dir.as_ref(),
|
||||
match frame {
|
||||
0 => "a",
|
||||
1 => "b",
|
||||
_ => panic!("Invalid animation frame"),
|
||||
GameSprite::Pacman(sprite) => match sprite {
|
||||
PacmanSprite::Moving(dir, frame) => {
|
||||
let frame_char = match frame {
|
||||
0 => 'a',
|
||||
1 => 'b',
|
||||
_ => panic!("Invalid animation frame"),
|
||||
};
|
||||
format!("pacman/{}_{}.png", dir.as_ref().to_lowercase(), frame_char)
|
||||
}
|
||||
),
|
||||
GameSprite::Pacman(PacmanSprite::Full) => "pacman/full.png".to_string(),
|
||||
GameSprite::Pacman(PacmanSprite::Dying(frame)) => format!("pacman/death/{}.png", frame),
|
||||
|
||||
// Ghost sprites
|
||||
GameSprite::Ghost(GhostSprite::Normal(ghost_type, dir, frame)) => {
|
||||
let frame_char = match frame {
|
||||
0 => 'a',
|
||||
1 => 'b',
|
||||
_ => panic!("Invalid animation frame"),
|
||||
};
|
||||
format!(
|
||||
"ghost/{}/{}_{}.png",
|
||||
ghost_type.as_str(),
|
||||
dir.as_ref().to_lowercase(),
|
||||
frame_char
|
||||
)
|
||||
}
|
||||
GameSprite::Ghost(GhostSprite::Frightened(color, frame)) => {
|
||||
let frame_char = match frame {
|
||||
0 => 'a',
|
||||
1 => 'b',
|
||||
_ => panic!("Invalid animation frame"),
|
||||
};
|
||||
let color_str = match color {
|
||||
FrightenedColor::Blue => "blue",
|
||||
FrightenedColor::White => "white",
|
||||
};
|
||||
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(),
|
||||
PacmanSprite::Full => "pacman/full.png".to_string(),
|
||||
},
|
||||
GameSprite::Ghost(sprite) => match sprite {
|
||||
GhostSprite::Normal(ghost, dir, frame) => {
|
||||
let frame_char = match frame {
|
||||
0 => 'a',
|
||||
1 => 'b',
|
||||
_ => panic!("Invalid animation frame"),
|
||||
};
|
||||
format!("ghost/{}/{}_{}.png", ghost.as_str(), dir.as_ref().to_lowercase(), frame_char)
|
||||
}
|
||||
GhostSprite::Frightened(color, frame) => {
|
||||
let frame_char = match frame {
|
||||
0 => 'a',
|
||||
1 => 'b',
|
||||
_ => panic!("Invalid animation frame"),
|
||||
};
|
||||
let color_str = match color {
|
||||
FrightenedColor::Blue => "blue",
|
||||
FrightenedColor::White => "white",
|
||||
};
|
||||
format!("ghost/frightened/{}_{}.png", color_str, frame_char)
|
||||
}
|
||||
GhostSprite::Eyes(dir) => format!("ghost/eyes/{}.png", dir.as_ref().to_lowercase()),
|
||||
},
|
||||
GameSprite::Maze(sprite) => match sprite {
|
||||
MazeSprite::Tile(index) => format!("maze/tiles/{}.png", index),
|
||||
MazeSprite::Pellet => "maze/pellet.png".to_string(),
|
||||
MazeSprite::Energizer => "maze/energizer.png".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
//! 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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user