mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-07 20:07:46 -06:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0a15c1ca8 | |||
| fa12611c69 | |||
| 342f378860 | |||
| e8944598cc | |||
| 6af25af5f3 | |||
| f1935ad016 | |||
| 4d397bba5f |
164
src/game/mod.rs
164
src/game/mod.rs
@@ -10,15 +10,17 @@ use crate::map::builder::Map;
|
|||||||
use crate::systems::blinking::Blinking;
|
use crate::systems::blinking::Blinking;
|
||||||
use crate::systems::movement::{Movable, MovementState, Position};
|
use crate::systems::movement::{Movable, MovementState, Position};
|
||||||
use crate::systems::{
|
use crate::systems::{
|
||||||
|
audio::{audio_system, AudioEvent, AudioResource},
|
||||||
blinking::blinking_system,
|
blinking::blinking_system,
|
||||||
collision::collision_system,
|
collision::collision_system,
|
||||||
components::{
|
components::{
|
||||||
Collider, CollisionLayer, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider,
|
AudioState, Collider, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider, PacmanCollider,
|
||||||
PacmanCollider, PlayerBundle, PlayerControlled, RenderDirty, Renderable, Score, ScoreResource,
|
PlayerBundle, PlayerControlled, RenderDirty, Renderable, ScoreResource,
|
||||||
},
|
},
|
||||||
control::player_system,
|
control::player_system,
|
||||||
debug::{debug_render_system, DebugState, DebugTextureResource},
|
debug::{debug_render_system, DebugState, DebugTextureResource},
|
||||||
input::input_system,
|
input::input_system,
|
||||||
|
item::item_system,
|
||||||
movement::movement_system,
|
movement::movement_system,
|
||||||
profiling::{profile, SystemTimings},
|
profiling::{profile, SystemTimings},
|
||||||
render::{directional_render_system, dirty_render_system, render_system, BackbufferResource, MapTextureResource},
|
render::{directional_render_system, dirty_render_system, render_system, BackbufferResource, MapTextureResource},
|
||||||
@@ -69,6 +71,7 @@ impl Game {
|
|||||||
|
|
||||||
EventRegistry::register_event::<GameError>(&mut world);
|
EventRegistry::register_event::<GameError>(&mut world);
|
||||||
EventRegistry::register_event::<GameEvent>(&mut world);
|
EventRegistry::register_event::<GameEvent>(&mut world);
|
||||||
|
EventRegistry::register_event::<AudioEvent>(&mut world);
|
||||||
|
|
||||||
let mut backbuffer = texture_creator
|
let mut backbuffer = texture_creator
|
||||||
.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
|
.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
|
||||||
@@ -87,6 +90,9 @@ impl Game {
|
|||||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
debug_texture.set_scale_mode(ScaleMode::Nearest);
|
debug_texture.set_scale_mode(ScaleMode::Nearest);
|
||||||
|
|
||||||
|
// Initialize audio system
|
||||||
|
let audio = crate::audio::Audio::new();
|
||||||
|
|
||||||
// Load atlas and create map texture
|
// Load atlas and create map texture
|
||||||
let atlas_bytes = get_asset_bytes(Asset::Atlas)?;
|
let atlas_bytes = get_asset_bytes(Asset::Atlas)?;
|
||||||
let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
|
let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
|
||||||
@@ -173,7 +179,6 @@ impl Game {
|
|||||||
entity_type: EntityType::Player,
|
entity_type: EntityType::Player,
|
||||||
collider: Collider {
|
collider: Collider {
|
||||||
size: constants::CELL_SIZE as f32 * 1.375,
|
size: constants::CELL_SIZE as f32 * 1.375,
|
||||||
layer: CollisionLayer::PACMAN,
|
|
||||||
},
|
},
|
||||||
pacman_collider: PacmanCollider,
|
pacman_collider: PacmanCollider,
|
||||||
};
|
};
|
||||||
@@ -184,6 +189,7 @@ impl Game {
|
|||||||
world.insert_non_send_resource(BackbufferResource(backbuffer));
|
world.insert_non_send_resource(BackbufferResource(backbuffer));
|
||||||
world.insert_non_send_resource(MapTextureResource(map_texture));
|
world.insert_non_send_resource(MapTextureResource(map_texture));
|
||||||
world.insert_non_send_resource(DebugTextureResource(debug_texture));
|
world.insert_non_send_resource(DebugTextureResource(debug_texture));
|
||||||
|
world.insert_non_send_resource(AudioResource(audio));
|
||||||
|
|
||||||
world.insert_resource(map);
|
world.insert_resource(map);
|
||||||
world.insert_resource(GlobalState { exit: false });
|
world.insert_resource(GlobalState { exit: false });
|
||||||
@@ -193,16 +199,13 @@ impl Game {
|
|||||||
world.insert_resource(DeltaTime(0f32));
|
world.insert_resource(DeltaTime(0f32));
|
||||||
world.insert_resource(RenderDirty::default());
|
world.insert_resource(RenderDirty::default());
|
||||||
world.insert_resource(DebugState::default());
|
world.insert_resource(DebugState::default());
|
||||||
|
world.insert_resource(AudioState::default());
|
||||||
|
|
||||||
world.add_observer(
|
world.add_observer(
|
||||||
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| match *event {
|
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| {
|
||||||
GameEvent::Command(command) => match command {
|
if matches!(*event, GameEvent::Command(GameCommand::Exit)) {
|
||||||
GameCommand::Exit => {
|
state.exit = true;
|
||||||
state.exit = true;
|
}
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
GameEvent::Collision(_a, _b) => {}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
schedule.add_systems(
|
schedule.add_systems(
|
||||||
@@ -211,6 +214,8 @@ impl Game {
|
|||||||
profile("player", player_system),
|
profile("player", player_system),
|
||||||
profile("movement", movement_system),
|
profile("movement", movement_system),
|
||||||
profile("collision", collision_system),
|
profile("collision", collision_system),
|
||||||
|
profile("item", item_system),
|
||||||
|
profile("audio", audio_system),
|
||||||
profile("blinking", blinking_system),
|
profile("blinking", blinking_system),
|
||||||
profile("directional_render", directional_render_system),
|
profile("directional_render", directional_render_system),
|
||||||
profile("dirty_render", dirty_render_system),
|
profile("dirty_render", dirty_render_system),
|
||||||
@@ -236,6 +241,7 @@ impl Game {
|
|||||||
)
|
)
|
||||||
.chain(),
|
.chain(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Spawn player
|
// Spawn player
|
||||||
world.spawn(player);
|
world.spawn(player);
|
||||||
|
|
||||||
@@ -248,14 +254,11 @@ impl Game {
|
|||||||
let nodes: Vec<_> = world.resource::<Map>().iter_nodes().map(|(id, tile)| (*id, *tile)).collect();
|
let nodes: Vec<_> = world.resource::<Map>().iter_nodes().map(|(id, tile)| (*id, *tile)).collect();
|
||||||
|
|
||||||
for (node_id, tile) in nodes {
|
for (node_id, tile) in nodes {
|
||||||
let (item_type, score, sprite, size) = match tile {
|
let (item_type, sprite, size) = match tile {
|
||||||
crate::constants::MapTile::Pellet => (EntityType::Pellet, 10, pellet_sprite, constants::CELL_SIZE as f32 * 0.4),
|
crate::constants::MapTile::Pellet => (EntityType::Pellet, pellet_sprite, constants::CELL_SIZE as f32 * 0.4),
|
||||||
crate::constants::MapTile::PowerPellet => (
|
crate::constants::MapTile::PowerPellet => {
|
||||||
EntityType::PowerPellet,
|
(EntityType::PowerPellet, energizer_sprite, constants::CELL_SIZE as f32 * 0.95)
|
||||||
50,
|
}
|
||||||
energizer_sprite,
|
|
||||||
constants::CELL_SIZE as f32 * 0.95,
|
|
||||||
),
|
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -270,11 +273,7 @@ impl Game {
|
|||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
entity_type: item_type,
|
entity_type: item_type,
|
||||||
score: Score(score),
|
collider: Collider { size },
|
||||||
collider: Collider {
|
|
||||||
size,
|
|
||||||
layer: CollisionLayer::ITEM,
|
|
||||||
},
|
|
||||||
item_collider: ItemCollider,
|
item_collider: ItemCollider,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -289,78 +288,6 @@ impl Game {
|
|||||||
Ok(Game { world, schedule })
|
Ok(Game { world, schedule })
|
||||||
}
|
}
|
||||||
|
|
||||||
// fn handle_command(&mut self, command: crate::input::commands::GameCommand) {
|
|
||||||
// use crate::input::commands::GameCommand;
|
|
||||||
// match command {
|
|
||||||
// GameCommand::MovePlayer(direction) => {
|
|
||||||
// self.state.pacman.set_next_direction(direction);
|
|
||||||
// }
|
|
||||||
// GameCommand::ToggleDebug => {
|
|
||||||
// self.toggle_debug_mode();
|
|
||||||
// }
|
|
||||||
// GameCommand::MuteAudio => {
|
|
||||||
// let is_muted = self.state.audio.is_muted();
|
|
||||||
// self.state.audio.set_mute(!is_muted);
|
|
||||||
// }
|
|
||||||
// GameCommand::ResetLevel => {
|
|
||||||
// if let Err(e) = self.reset_game_state() {
|
|
||||||
// tracing::error!("Failed to reset game state: {}", e);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// GameCommand::TogglePause => {
|
|
||||||
// self.state.paused = !self.state.paused;
|
|
||||||
// }
|
|
||||||
// GameCommand::Exit => {}
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn process_events(&mut self) {
|
|
||||||
// while let Some(event) = self.state.event_queue.pop_front() {
|
|
||||||
// match event {
|
|
||||||
// GameEvent::Command(command) => self.handle_command(command),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// Resets the game state, randomizing ghost positions and resetting Pac-Man
|
|
||||||
// fn reset_game_state(&mut self) -> GameResult<()> {
|
|
||||||
// let pacman_start_node = self.state.map.start_positions.pacman;
|
|
||||||
// self.state.pacman = Pacman::new(&self.state.map.graph, pacman_start_node, &self.state.atlas)?;
|
|
||||||
|
|
||||||
// // Reset items
|
|
||||||
// self.state.items = self.state.map.generate_items(&self.state.atlas)?;
|
|
||||||
|
|
||||||
// // Randomize ghost positions
|
|
||||||
// let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde];
|
|
||||||
// let mut rng = SmallRng::from_os_rng();
|
|
||||||
|
|
||||||
// for (i, ghost) in self.state.ghosts.iter_mut().enumerate() {
|
|
||||||
// let random_node = rng.random_range(0..self.state.map.graph.node_count());
|
|
||||||
// *ghost = Ghost::new(&self.state.map.graph, random_node, ghost_types[i], &self.state.atlas)?;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Reset collision system
|
|
||||||
// self.state.collision_system = CollisionSystem::default();
|
|
||||||
|
|
||||||
// // Re-register Pac-Man
|
|
||||||
// self.state.pacman_id = self.state.collision_system.register_entity(self.state.pacman.position());
|
|
||||||
|
|
||||||
// // Re-register items
|
|
||||||
// self.state.item_ids.clear();
|
|
||||||
// for item in &self.state.items {
|
|
||||||
// let item_id = self.state.collision_system.register_entity(item.position());
|
|
||||||
// self.state.item_ids.push(item_id);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Re-register ghosts
|
|
||||||
// self.state.ghost_ids.clear();
|
|
||||||
// for ghost in &self.state.ghosts {
|
|
||||||
// let ghost_id = self.state.collision_system.register_entity(ghost.position());
|
|
||||||
// self.state.ghost_ids.push(ghost_id);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
|
|
||||||
/// Ticks the game state.
|
/// Ticks the game state.
|
||||||
///
|
///
|
||||||
/// Returns true if the game should exit.
|
/// Returns true if the game should exit.
|
||||||
@@ -375,52 +302,9 @@ impl Game {
|
|||||||
.get_resource::<GlobalState>()
|
.get_resource::<GlobalState>()
|
||||||
.expect("GlobalState could not be acquired");
|
.expect("GlobalState could not be acquired");
|
||||||
|
|
||||||
return state.exit;
|
state.exit
|
||||||
|
|
||||||
// // Process any events that have been posted (such as unpausing)
|
|
||||||
// self.process_events();
|
|
||||||
|
|
||||||
// // If the game is paused, we don't need to do anything beyond returning
|
|
||||||
// if self.state.paused {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// self.schedule.run(&mut self.world);
|
|
||||||
|
|
||||||
// self.state.pacman.tick(dt, &self.state.map.graph);
|
|
||||||
|
|
||||||
// // Update all ghosts
|
|
||||||
// for ghost in &mut self.state.ghosts {
|
|
||||||
// ghost.tick(dt, &self.state.map.graph);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Update collision system positions
|
|
||||||
// self.update_collision_positions();
|
|
||||||
|
|
||||||
// // Check for collisions
|
|
||||||
// self.check_collisions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// /// Toggles the debug mode on and off.
|
|
||||||
// ///
|
|
||||||
// /// When debug mode is enabled, the game will render additional information
|
|
||||||
// /// that is useful for debugging, such as the collision grid and entity paths.
|
|
||||||
// pub fn toggle_debug_mode(&mut self) {
|
|
||||||
// self.state.debug_mode = !self.state.debug_mode;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn update_collision_positions(&mut self) {
|
|
||||||
// // Update Pac-Man's position
|
|
||||||
// self.state
|
|
||||||
// .collision_system
|
|
||||||
// .update_position(self.state.pacman_id, self.state.pacman.position());
|
|
||||||
|
|
||||||
// // Update ghost positions
|
|
||||||
// for (ghost, &ghost_id) in self.state.ghosts.iter().zip(&self.state.ghost_ids) {
|
|
||||||
// self.state.collision_system.update_position(ghost_id, ghost.position());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fn check_collisions(&mut self) {
|
// fn check_collisions(&mut self) {
|
||||||
// // Check Pac-Man vs Items
|
// // Check Pac-Man vs Items
|
||||||
// let potential_collisions = self
|
// let potential_collisions = self
|
||||||
|
|||||||
54
src/systems/audio.rs
Normal file
54
src/systems/audio.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
//! Audio system for handling sound playback in the Pac-Man game.
|
||||||
|
//!
|
||||||
|
//! This module provides an ECS-based audio system that integrates with SDL2_mixer
|
||||||
|
//! for playing sound effects. The system uses NonSendMut resources to handle SDL2's
|
||||||
|
//! main-thread requirements while maintaining Bevy ECS compatibility.
|
||||||
|
|
||||||
|
use bevy_ecs::{
|
||||||
|
event::{Event, EventReader, EventWriter},
|
||||||
|
system::{NonSendMut, ResMut},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{audio::Audio, error::GameError, systems::components::AudioState};
|
||||||
|
|
||||||
|
/// Events for triggering audio playback
|
||||||
|
#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AudioEvent {
|
||||||
|
/// Play the "eat" sound when Pac-Man consumes a pellet
|
||||||
|
PlayEat,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Non-send resource wrapper for SDL2 audio system
|
||||||
|
///
|
||||||
|
/// This wrapper is needed because SDL2 audio components are not Send,
|
||||||
|
/// but Bevy ECS requires Send for regular resources. Using NonSendMut
|
||||||
|
/// allows us to use SDL2 audio on the main thread while integrating
|
||||||
|
/// with the ECS system.
|
||||||
|
pub struct AudioResource(pub Audio);
|
||||||
|
|
||||||
|
/// System that processes audio events and plays sounds
|
||||||
|
pub fn audio_system(
|
||||||
|
mut audio: NonSendMut<AudioResource>,
|
||||||
|
mut audio_state: ResMut<AudioState>,
|
||||||
|
mut audio_events: EventReader<AudioEvent>,
|
||||||
|
_errors: EventWriter<GameError>,
|
||||||
|
) {
|
||||||
|
// Set mute state if it has changed
|
||||||
|
if audio.0.is_muted() != audio_state.muted {
|
||||||
|
audio.0.set_mute(audio_state.muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process audio events
|
||||||
|
for event in audio_events.read() {
|
||||||
|
match event {
|
||||||
|
AudioEvent::PlayEat => {
|
||||||
|
if !audio.0.is_disabled() && !audio_state.muted {
|
||||||
|
audio.0.eat();
|
||||||
|
// Update the sound index for cycling through sounds
|
||||||
|
audio_state.sound_index = (audio_state.sound_index + 1) % 4;
|
||||||
|
// 4 eat sounds available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,7 +60,6 @@ bitflags! {
|
|||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct Collider {
|
pub struct Collider {
|
||||||
pub size: f32,
|
pub size: f32,
|
||||||
pub layer: CollisionLayer,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marker components for collision filtering optimization
|
/// Marker components for collision filtering optimization
|
||||||
@@ -73,9 +72,6 @@ pub struct GhostCollider;
|
|||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct ItemCollider;
|
pub struct ItemCollider;
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct Score(pub u32);
|
|
||||||
|
|
||||||
#[derive(Bundle)]
|
#[derive(Bundle)]
|
||||||
pub struct PlayerBundle {
|
pub struct PlayerBundle {
|
||||||
pub player: PlayerControlled,
|
pub player: PlayerControlled,
|
||||||
@@ -94,7 +90,6 @@ pub struct ItemBundle {
|
|||||||
pub position: Position,
|
pub position: Position,
|
||||||
pub sprite: Renderable,
|
pub sprite: Renderable,
|
||||||
pub entity_type: EntityType,
|
pub entity_type: EntityType,
|
||||||
pub score: Score,
|
|
||||||
pub collider: Collider,
|
pub collider: Collider,
|
||||||
pub item_collider: ItemCollider,
|
pub item_collider: ItemCollider,
|
||||||
}
|
}
|
||||||
@@ -112,3 +107,12 @@ pub struct DeltaTime(pub f32);
|
|||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
pub struct RenderDirty(pub bool);
|
pub struct RenderDirty(pub bool);
|
||||||
|
|
||||||
|
/// Resource for tracking audio state
|
||||||
|
#[derive(Resource, Debug, Clone, Default)]
|
||||||
|
pub struct AudioState {
|
||||||
|
/// Whether audio is currently muted
|
||||||
|
pub muted: bool,
|
||||||
|
/// Current sound index for cycling through eat sounds
|
||||||
|
pub sound_index: usize,
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use bevy_ecs::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
error::GameError,
|
error::GameError,
|
||||||
events::{GameCommand, GameEvent},
|
events::{GameCommand, GameEvent},
|
||||||
systems::components::{GlobalState, PlayerControlled},
|
systems::components::{AudioState, GlobalState, PlayerControlled},
|
||||||
systems::debug::DebugState,
|
systems::debug::DebugState,
|
||||||
systems::movement::Movable,
|
systems::movement::Movable,
|
||||||
};
|
};
|
||||||
@@ -18,6 +18,7 @@ pub fn player_system(
|
|||||||
mut events: EventReader<GameEvent>,
|
mut events: EventReader<GameEvent>,
|
||||||
mut state: ResMut<GlobalState>,
|
mut state: ResMut<GlobalState>,
|
||||||
mut debug_state: ResMut<DebugState>,
|
mut debug_state: ResMut<DebugState>,
|
||||||
|
mut audio_state: ResMut<AudioState>,
|
||||||
mut players: Query<&mut Movable, With<PlayerControlled>>,
|
mut players: Query<&mut Movable, With<PlayerControlled>>,
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
@@ -25,7 +26,10 @@ pub fn player_system(
|
|||||||
let mut movable = match players.single_mut() {
|
let mut movable = match players.single_mut() {
|
||||||
Ok(movable) => movable,
|
Ok(movable) => movable,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
errors.write(GameError::InvalidState(format!("No/multiple entities queried for player system: {}", e)).into());
|
errors.write(GameError::InvalidState(format!(
|
||||||
|
"No/multiple entities queried for player system: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -43,6 +47,10 @@ pub fn player_system(
|
|||||||
GameCommand::ToggleDebug => {
|
GameCommand::ToggleDebug => {
|
||||||
*debug_state = debug_state.next();
|
*debug_state = debug_state.next();
|
||||||
}
|
}
|
||||||
|
GameCommand::MuteAudio => {
|
||||||
|
audio_state.muted = !audio_state.muted;
|
||||||
|
tracing::info!("Audio {}", if audio_state.muted { "muted" } else { "unmuted" });
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,43 @@
|
|||||||
use num_width::NumberWidth;
|
use num_width::NumberWidth;
|
||||||
|
use smallvec::SmallVec;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
// Helper to split a duration into a integer, decimal, and unit
|
||||||
|
fn get_value(duration: &Duration) -> (u64, u32, &'static str) {
|
||||||
|
let (int, decimal, unit) = match duration {
|
||||||
|
// if greater than 1 second, return as seconds
|
||||||
|
n if n >= &Duration::from_secs(1) => {
|
||||||
|
let secs = n.as_secs();
|
||||||
|
let decimal = n.as_millis() as u64 % 1000;
|
||||||
|
(secs, decimal as u32, "s")
|
||||||
|
}
|
||||||
|
// if greater than 1 millisecond, return as milliseconds
|
||||||
|
n if n >= &Duration::from_millis(1) => {
|
||||||
|
let ms = n.as_millis() as u64;
|
||||||
|
let decimal = n.as_micros() as u64 % 1000;
|
||||||
|
(ms, decimal as u32, "ms")
|
||||||
|
}
|
||||||
|
// if greater than 1 microsecond, return as microseconds
|
||||||
|
n if n >= &Duration::from_micros(1) => {
|
||||||
|
let us = n.as_micros() as u64;
|
||||||
|
let decimal = n.as_nanos() as u64 % 1000;
|
||||||
|
(us, decimal as u32, "µs")
|
||||||
|
}
|
||||||
|
// otherwise, return as nanoseconds
|
||||||
|
n => {
|
||||||
|
let ns = n.as_nanos() as u64;
|
||||||
|
(ns, 0, "ns")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(int, decimal, unit)
|
||||||
|
}
|
||||||
|
|
||||||
/// Formats timing data into a vector of strings with proper alignment
|
/// Formats timing data into a vector of strings with proper alignment
|
||||||
pub fn format_timing_display(timing_data: Vec<(String, Duration, Duration)>) -> String {
|
pub fn format_timing_display(timing_data: impl IntoIterator<Item = (String, Duration, Duration)>) -> SmallVec<[String; 12]> {
|
||||||
if timing_data.is_empty() {
|
let mut iter = timing_data.into_iter().peekable();
|
||||||
return String::new();
|
if iter.peek().is_none() {
|
||||||
}
|
return SmallVec::new();
|
||||||
|
|
||||||
// Helper to split a duration into a integer, decimal, and unit
|
|
||||||
fn get_value(duration: &Duration) -> (u64, u32, &'static str) {
|
|
||||||
let (int, decimal, unit) = match duration {
|
|
||||||
// if greater than 1 second, return as seconds
|
|
||||||
n if n >= &Duration::from_secs(1) => {
|
|
||||||
let secs = n.as_secs();
|
|
||||||
let decimal = n.as_millis() as u64 % 1000;
|
|
||||||
(secs, decimal as u32, "s")
|
|
||||||
}
|
|
||||||
// if greater than 1 millisecond, return as milliseconds
|
|
||||||
n if n >= &Duration::from_millis(1) => {
|
|
||||||
let ms = n.as_millis() as u64;
|
|
||||||
let decimal = n.as_micros() as u64 % 1000;
|
|
||||||
(ms, decimal as u32, "ms")
|
|
||||||
}
|
|
||||||
// if greater than 1 microsecond, return as microseconds
|
|
||||||
n if n >= &Duration::from_micros(1) => {
|
|
||||||
let us = n.as_micros() as u64;
|
|
||||||
let decimal = n.as_nanos() as u64 % 1000;
|
|
||||||
(us, decimal as u32, "µs")
|
|
||||||
}
|
|
||||||
// otherwise, return as nanoseconds
|
|
||||||
n => {
|
|
||||||
let ns = n.as_nanos() as u64;
|
|
||||||
(ns, 0, "ns")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
(int, decimal, unit)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Entry {
|
struct Entry {
|
||||||
@@ -48,8 +50,7 @@ pub fn format_timing_display(timing_data: Vec<(String, Duration, Duration)>) ->
|
|||||||
std_unit: &'static str,
|
std_unit: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
let entries = timing_data
|
let entries = iter
|
||||||
.iter()
|
|
||||||
.map(|(name, avg, std_dev)| {
|
.map(|(name, avg, std_dev)| {
|
||||||
let (avg_int, avg_decimal, avg_unit) = get_value(&avg);
|
let (avg_int, avg_decimal, avg_unit) = get_value(&avg);
|
||||||
let (std_int, std_decimal, std_unit) = get_value(&std_dev);
|
let (std_int, std_decimal, std_unit) = get_value(&std_dev);
|
||||||
@@ -64,84 +65,38 @@ pub fn format_timing_display(timing_data: Vec<(String, Duration, Duration)>) ->
|
|||||||
std_unit,
|
std_unit,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<SmallVec<[Entry; 12]>>();
|
||||||
|
|
||||||
let max_name_width = entries.iter().map(|e| e.name.len() as usize).max().unwrap_or(0);
|
let (max_name_width, max_avg_int_width, max_avg_decimal_width, max_std_int_width, max_std_decimal_width) = entries
|
||||||
let max_avg_int_width = entries.iter().map(|e| e.avg_int.width() as usize).max().unwrap_or(0);
|
|
||||||
let max_avg_decimal_width = entries
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|e| e.avg_decimal.width() as usize)
|
.fold((0, 0, 3, 0, 3), |(name_w, avg_int_w, avg_dec_w, std_int_w, std_dec_w), e| {
|
||||||
.max()
|
(
|
||||||
.unwrap_or(0)
|
name_w.max(e.name.len()),
|
||||||
.max(3);
|
avg_int_w.max(e.avg_int.width() as usize),
|
||||||
let max_std_int_width = entries.iter().map(|e| e.std_int.width() as usize).max().unwrap_or(0);
|
avg_dec_w.max(e.avg_decimal.width() as usize),
|
||||||
let max_std_decimal_width = entries
|
std_int_w.max(e.std_int.width() as usize),
|
||||||
.iter()
|
std_dec_w.max(e.std_decimal.width() as usize),
|
||||||
.map(|e| e.std_decimal.width() as usize)
|
)
|
||||||
.max()
|
});
|
||||||
.unwrap_or(0)
|
|
||||||
.max(3);
|
|
||||||
|
|
||||||
let mut output_lines = Vec::new();
|
entries.iter().map(|e| {
|
||||||
|
format!(
|
||||||
// Format each line using the calculated max widths for alignment
|
"{name:max_name_width$} : {avg_int:max_avg_int_width$}.{avg_decimal:<max_avg_decimal_width$}{avg_unit} ± {std_int:max_std_int_width$}.{std_decimal:<max_std_decimal_width$}{std_unit}",
|
||||||
for Entry {
|
// Content
|
||||||
name,
|
name = e.name,
|
||||||
avg_int,
|
avg_int = e.avg_int,
|
||||||
avg_decimal,
|
avg_decimal = e.avg_decimal,
|
||||||
avg_unit,
|
std_int = e.std_int,
|
||||||
std_int,
|
std_decimal = e.std_decimal,
|
||||||
std_decimal,
|
// Units
|
||||||
std_unit,
|
avg_unit = e.avg_unit,
|
||||||
} in entries.iter()
|
std_unit = e.std_unit,
|
||||||
{
|
// Padding
|
||||||
output_lines.push(format!(
|
max_name_width = max_name_width,
|
||||||
"{name:max_name_width$} : {avg_int:max_avg_int_width$}.{avg_decimal:<max_avg_decimal_width$}{avg_unit} ± {std_int:max_std_int_width$}.{std_decimal:<max_std_decimal_width$}{std_unit}"
|
max_avg_int_width = max_avg_int_width,
|
||||||
));
|
max_avg_decimal_width = max_avg_decimal_width,
|
||||||
}
|
max_std_int_width = max_std_int_width,
|
||||||
|
max_std_decimal_width = max_std_decimal_width
|
||||||
output_lines.join("\n")
|
)
|
||||||
}
|
}).collect::<SmallVec<[String; 12]>>()
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_format_timing_display() {
|
|
||||||
let timing_data = vec![
|
|
||||||
("total".to_string(), Duration::from_micros(1234), Duration::from_micros(570)),
|
|
||||||
("input".to_string(), Duration::from_micros(120), Duration::from_micros(45)),
|
|
||||||
("player".to_string(), Duration::from_micros(456), Duration::from_micros(123)),
|
|
||||||
("movement".to_string(), Duration::from_micros(789), Duration::from_micros(234)),
|
|
||||||
("render".to_string(), Duration::from_micros(12), Duration::from_micros(3)),
|
|
||||||
("debug".to_string(), Duration::from_nanos(460), Duration::from_nanos(557)),
|
|
||||||
];
|
|
||||||
|
|
||||||
let result = format_timing_display(timing_data);
|
|
||||||
let lines: Vec<&str> = result.lines().collect();
|
|
||||||
|
|
||||||
// Verify we have the expected number of lines
|
|
||||||
assert_eq!(lines.len(), 6);
|
|
||||||
|
|
||||||
let expected = r#"
|
|
||||||
total : 1.234ms ± 570.0 µs
|
|
||||||
input : 120.0 µs ± 45.0 µs
|
|
||||||
player : 456.0 µs ± 123.0 µs
|
|
||||||
movement : 789.0 µs ± 234.0 µs
|
|
||||||
render : 12.0 µs ± 3.0 µs
|
|
||||||
debug : 460.0 ns ± 557.0 ns
|
|
||||||
"#
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
for (line, expected_line) in lines.iter().zip(expected.lines()) {
|
|
||||||
assert_eq!(*line, expected_line);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the result for manual inspection
|
|
||||||
println!("Formatted output:");
|
|
||||||
println!("{}", result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
49
src/systems/item.rs
Normal file
49
src/systems/item.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use bevy_ecs::{event::EventReader, prelude::*, query::With, system::Query};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
events::GameEvent,
|
||||||
|
systems::{
|
||||||
|
audio::AudioEvent,
|
||||||
|
components::{EntityType, ItemCollider, PacmanCollider, ScoreResource},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn item_system(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut collision_events: EventReader<GameEvent>,
|
||||||
|
mut score: ResMut<ScoreResource>,
|
||||||
|
pacman_query: Query<Entity, With<PacmanCollider>>,
|
||||||
|
item_query: Query<(Entity, &EntityType), With<ItemCollider>>,
|
||||||
|
mut events: EventWriter<AudioEvent>,
|
||||||
|
) {
|
||||||
|
for event in collision_events.read() {
|
||||||
|
if let GameEvent::Collision(entity1, entity2) = event {
|
||||||
|
// Check if one is Pacman and the other is an item
|
||||||
|
let (_pacman_entity, item_entity) = if pacman_query.get(*entity1).is_ok() && item_query.get(*entity2).is_ok() {
|
||||||
|
(*entity1, *entity2)
|
||||||
|
} else if pacman_query.get(*entity2).is_ok() && item_query.get(*entity1).is_ok() {
|
||||||
|
(*entity2, *entity1)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the item type and update score
|
||||||
|
if let Ok((item_ent, entity_type)) = item_query.get(item_entity) {
|
||||||
|
match entity_type {
|
||||||
|
EntityType::Pellet => {
|
||||||
|
score.0 += 10;
|
||||||
|
}
|
||||||
|
EntityType::PowerPellet => {
|
||||||
|
score.0 += 50;
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the collected item
|
||||||
|
commands.entity(item_ent).despawn();
|
||||||
|
|
||||||
|
events.write(AudioEvent::PlayEat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
//! This module contains all the ECS-related logic, including components, systems,
|
//! This module contains all the ECS-related logic, including components, systems,
|
||||||
//! and resources.
|
//! and resources.
|
||||||
|
|
||||||
|
pub mod audio;
|
||||||
pub mod blinking;
|
pub mod blinking;
|
||||||
pub mod collision;
|
pub mod collision;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
@@ -10,6 +11,7 @@ pub mod control;
|
|||||||
pub mod debug;
|
pub mod debug;
|
||||||
pub mod formatting;
|
pub mod formatting;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
|
pub mod item;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
pub mod profiling;
|
pub mod profiling;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ pub struct EdgeProgress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Pure spatial position component - works for both static and dynamic entities.
|
/// Pure spatial position component - works for both static and dynamic entities.
|
||||||
#[derive(Component, Debug, Copy, Clone, PartialEq)]
|
#[derive(Component, Debug, Copy, Clone, PartialEq, Default)]
|
||||||
pub struct Position {
|
pub struct Position {
|
||||||
/// The current/primary node this entity is at or traveling from
|
/// The current/primary node this entity is at or traveling from
|
||||||
pub node: NodeId,
|
pub node: NodeId,
|
||||||
@@ -29,10 +29,13 @@ pub struct Position {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Explicit movement state - only for entities that can move.
|
/// Explicit movement state - only for entities that can move.
|
||||||
#[derive(Component, Debug, Clone, Copy, PartialEq)]
|
#[derive(Component, Debug, Clone, Copy, PartialEq, Default)]
|
||||||
pub enum MovementState {
|
pub enum MovementState {
|
||||||
|
#[default]
|
||||||
Stopped,
|
Stopped,
|
||||||
Moving { direction: Direction },
|
Moving {
|
||||||
|
direction: Direction,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Movement capability and parameters - only for entities that can move.
|
/// Movement capability and parameters - only for entities that can move.
|
||||||
@@ -83,15 +86,6 @@ impl Position {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Position {
|
|
||||||
fn default() -> Self {
|
|
||||||
Position {
|
|
||||||
node: 0,
|
|
||||||
edge_progress: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl Position {
|
impl Position {
|
||||||
/// Returns `true` if the position is exactly at a node (not traveling).
|
/// Returns `true` if the position is exactly at a node (not traveling).
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::time::Duration;
|
|||||||
use thousands::Separable;
|
use thousands::Separable;
|
||||||
|
|
||||||
/// The maximum number of systems that can be profiled. Must not be exceeded, or it will panic.
|
/// The maximum number of systems that can be profiled. Must not be exceeded, or it will panic.
|
||||||
const MAX_SYSTEMS: usize = 11;
|
const MAX_SYSTEMS: usize = 12;
|
||||||
/// The number of durations to keep in the circular buffer.
|
/// The number of durations to keep in the circular buffer.
|
||||||
const TIMING_WINDOW_SIZE: usize = 30;
|
const TIMING_WINDOW_SIZE: usize = 30;
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ impl SystemTimings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the formatting module to format the data
|
// Use the formatting module to format the data
|
||||||
crate::systems::formatting::format_timing_display(timing_data)
|
crate::systems::formatting::format_timing_display(timing_data).join("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use bevy_ecs::system::{NonSendMut, Query, Res, ResMut};
|
|||||||
use sdl2::render::{Canvas, Texture};
|
use sdl2::render::{Canvas, Texture};
|
||||||
use sdl2::video::Window;
|
use sdl2::video::Window;
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
pub fn dirty_render_system(
|
pub fn dirty_render_system(
|
||||||
mut dirty: ResMut<RenderDirty>,
|
mut dirty: ResMut<RenderDirty>,
|
||||||
changed_renderables: Query<(), Or<(Changed<Renderable>, Changed<Position>)>>,
|
changed_renderables: Query<(), Or<(Changed<Renderable>, Changed<Position>)>>,
|
||||||
@@ -47,7 +48,7 @@ pub fn directional_render_system(
|
|||||||
renderable.sprite = new_tile;
|
renderable.sprite = new_tile;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errors.write(TextureError::RenderFailed(format!("Entity has no texture")).into());
|
errors.write(TextureError::RenderFailed("Entity has no texture".to_string()).into());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +60,7 @@ pub struct MapTextureResource(pub Texture<'static>);
|
|||||||
/// A non-send resource for the backbuffer texture. This just wraps the texture with a type so it can be differentiated when exposed as a resource.
|
/// A non-send resource for the backbuffer texture. This just wraps the texture with a type so it can be differentiated when exposed as a resource.
|
||||||
pub struct BackbufferResource(pub Texture<'static>);
|
pub struct BackbufferResource(pub Texture<'static>);
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn render_system(
|
pub fn render_system(
|
||||||
mut canvas: NonSendMut<&mut Canvas<Window>>,
|
mut canvas: NonSendMut<&mut Canvas<Window>>,
|
||||||
map_texture: NonSendMut<MapTextureResource>,
|
map_texture: NonSendMut<MapTextureResource>,
|
||||||
@@ -85,7 +87,11 @@ pub fn render_system(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render all entities to the backbuffer
|
// Render all entities to the backbuffer
|
||||||
for (_, renderable, position) in renderables.iter() {
|
for (_, renderable, position) in renderables
|
||||||
|
.iter()
|
||||||
|
.sort_by_key::<(Entity, &Renderable, &Position), _>(|(_, renderable, _)| renderable.layer)
|
||||||
|
.rev()
|
||||||
|
{
|
||||||
if !renderable.visible {
|
if !renderable.visible {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -105,7 +111,7 @@ pub fn render_system(
|
|||||||
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
|
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
errors.write(e.into());
|
errors.write(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,135 +1,95 @@
|
|||||||
use pacman::systems::formatting::format_timing_display;
|
use pacman::systems::formatting::format_timing_display;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[test]
|
use pretty_assertions::assert_eq;
|
||||||
fn test_basic_formatting() {
|
|
||||||
let timing_data = vec![
|
fn get_timing_data() -> Vec<(String, Duration, Duration)> {
|
||||||
("60 FPS".to_string(), Duration::from_micros(1234), Duration::from_micros(567)),
|
vec![
|
||||||
("input".to_string(), Duration::from_micros(123), Duration::from_micros(45)),
|
("total".to_string(), Duration::from_micros(1234), Duration::from_micros(570)),
|
||||||
|
("input".to_string(), Duration::from_micros(120), Duration::from_micros(45)),
|
||||||
("player".to_string(), Duration::from_micros(456), Duration::from_micros(123)),
|
("player".to_string(), Duration::from_micros(456), Duration::from_micros(123)),
|
||||||
("movement".to_string(), Duration::from_micros(789), Duration::from_micros(234)),
|
("movement".to_string(), Duration::from_micros(789), Duration::from_micros(234)),
|
||||||
("render".to_string(), Duration::from_micros(12), Duration::from_micros(3)),
|
("render".to_string(), Duration::from_micros(12), Duration::from_micros(3)),
|
||||||
("debug".to_string(), Duration::from_nanos(1000000), Duration::from_nanos(1000)),
|
("debug".to_string(), Duration::from_nanos(460), Duration::from_nanos(557)),
|
||||||
];
|
]
|
||||||
|
}
|
||||||
|
|
||||||
let result = format_timing_display(timing_data);
|
fn get_formatted_output() -> impl IntoIterator<Item = String> {
|
||||||
println!("Basic formatting test:");
|
format_timing_display(get_timing_data())
|
||||||
println!("{}", result);
|
|
||||||
println!();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_desired_format() {
|
fn test_formatting_alignment() {
|
||||||
// This test represents the exact format you want to achieve
|
let mut colon_positions = vec![];
|
||||||
let timing_data = vec![
|
let mut first_decimal_positions = vec![];
|
||||||
("total".to_string(), Duration::from_micros(1230), Duration::from_micros(570)),
|
let mut second_decimal_positions = vec![];
|
||||||
("input".to_string(), Duration::from_micros(120), Duration::from_micros(50)),
|
let mut first_unit_positions = vec![];
|
||||||
("player".to_string(), Duration::from_micros(460), Duration::from_micros(120)),
|
let mut second_unit_positions = vec![];
|
||||||
("movement".to_string(), Duration::from_micros(790), Duration::from_micros(230)),
|
|
||||||
("render".to_string(), Duration::from_micros(10), Duration::from_micros(3)),
|
|
||||||
("debug".to_string(), Duration::from_nanos(1000000), Duration::from_nanos(1000)),
|
|
||||||
];
|
|
||||||
|
|
||||||
let result = format_timing_display(timing_data);
|
get_formatted_output().into_iter().for_each(|line| {
|
||||||
println!("Desired format test:");
|
let (mut got_decimal, mut got_unit) = (false, false);
|
||||||
println!("{}", result);
|
for (i, char) in line.chars().enumerate() {
|
||||||
println!();
|
match char {
|
||||||
|
':' => colon_positions.push(i),
|
||||||
|
'.' => {
|
||||||
|
if got_decimal {
|
||||||
|
second_decimal_positions.push(i);
|
||||||
|
} else {
|
||||||
|
first_decimal_positions.push(i);
|
||||||
|
}
|
||||||
|
got_decimal = true;
|
||||||
|
}
|
||||||
|
's' => {
|
||||||
|
if got_unit {
|
||||||
|
first_unit_positions.push(i);
|
||||||
|
} else {
|
||||||
|
second_unit_positions.push(i);
|
||||||
|
got_unit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Expected output should look like:
|
// Assert that all positions were found
|
||||||
// total : 1.23 ms ± 0.57 ms
|
assert_eq!(
|
||||||
// input : 0.12 ms ± 0.05 ms
|
[
|
||||||
// player : 0.46 ms ± 0.12 ms
|
&colon_positions,
|
||||||
// movement : 0.79 ms ± 0.23 ms
|
&first_decimal_positions,
|
||||||
// render : 0.01 ms ± 0.003ms
|
&second_decimal_positions,
|
||||||
// debug : 0.001ms ± 0.000ms
|
&first_unit_positions,
|
||||||
}
|
&second_unit_positions
|
||||||
|
]
|
||||||
#[test]
|
.iter()
|
||||||
fn test_mixed_units() {
|
.all(|p| p.len() == 6),
|
||||||
let timing_data = vec![
|
true
|
||||||
("60 FPS".to_string(), Duration::from_millis(16), Duration::from_micros(500)),
|
);
|
||||||
(
|
|
||||||
"fast_system".to_string(),
|
// Assert that all positions are the same
|
||||||
Duration::from_nanos(500000),
|
assert!(
|
||||||
Duration::from_nanos(100000),
|
colon_positions.iter().all(|&p| p == colon_positions[0]),
|
||||||
),
|
"colon positions are not the same {:?}",
|
||||||
(
|
colon_positions
|
||||||
"medium_system".to_string(),
|
);
|
||||||
Duration::from_micros(2500),
|
assert!(
|
||||||
Duration::from_micros(500),
|
first_decimal_positions.iter().all(|&p| p == first_decimal_positions[0]),
|
||||||
),
|
"first decimal positions are not the same {:?}",
|
||||||
("slow_system".to_string(), Duration::from_millis(5), Duration::from_millis(1)),
|
first_decimal_positions
|
||||||
];
|
);
|
||||||
|
assert!(
|
||||||
let result = format_timing_display(timing_data);
|
second_decimal_positions.iter().all(|&p| p == second_decimal_positions[0]),
|
||||||
println!("Mixed units test:");
|
"second decimal positions are not the same {:?}",
|
||||||
println!("{}", result);
|
second_decimal_positions
|
||||||
println!();
|
);
|
||||||
}
|
assert!(
|
||||||
|
first_unit_positions.iter().all(|&p| p == first_unit_positions[0]),
|
||||||
#[test]
|
"first unit positions are not the same {:?}",
|
||||||
fn test_trailing_zeros() {
|
first_unit_positions
|
||||||
let timing_data = vec![
|
);
|
||||||
("60 FPS".to_string(), Duration::from_micros(1000), Duration::from_micros(500)),
|
assert!(
|
||||||
("exact_ms".to_string(), Duration::from_millis(1), Duration::from_micros(100)),
|
second_unit_positions.iter().all(|&p| p == second_unit_positions[0]),
|
||||||
("exact_us".to_string(), Duration::from_micros(1), Duration::from_nanos(100000)),
|
"second unit positions are not the same {:?}",
|
||||||
("exact_ns".to_string(), Duration::from_nanos(1000), Duration::from_nanos(100)),
|
second_unit_positions
|
||||||
];
|
);
|
||||||
|
|
||||||
let result = format_timing_display(timing_data);
|
|
||||||
println!("Trailing zeros test:");
|
|
||||||
println!("{}", result);
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edge_cases() {
|
|
||||||
let timing_data = vec![
|
|
||||||
("60 FPS".to_string(), Duration::from_nanos(1), Duration::from_nanos(1)),
|
|
||||||
("very_small".to_string(), Duration::from_nanos(100), Duration::from_nanos(50)),
|
|
||||||
("very_large".to_string(), Duration::from_secs(1), Duration::from_millis(100)),
|
|
||||||
("zero_time".to_string(), Duration::ZERO, Duration::ZERO),
|
|
||||||
];
|
|
||||||
|
|
||||||
let result = format_timing_display(timing_data);
|
|
||||||
println!("Edge cases test:");
|
|
||||||
println!("{}", result);
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_variable_name_lengths() {
|
|
||||||
let timing_data = vec![
|
|
||||||
("60 FPS".to_string(), Duration::from_micros(1234), Duration::from_micros(567)),
|
|
||||||
("a".to_string(), Duration::from_micros(123), Duration::from_micros(45)),
|
|
||||||
(
|
|
||||||
"very_long_system_name".to_string(),
|
|
||||||
Duration::from_micros(456),
|
|
||||||
Duration::from_micros(123),
|
|
||||||
),
|
|
||||||
("medium".to_string(), Duration::from_micros(789), Duration::from_micros(234)),
|
|
||||||
];
|
|
||||||
|
|
||||||
let result = format_timing_display(timing_data);
|
|
||||||
println!("Variable name lengths test:");
|
|
||||||
println!("{}", result);
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_empty_input() {
|
|
||||||
let timing_data = vec![];
|
|
||||||
let result = format_timing_display(timing_data);
|
|
||||||
assert_eq!(result, "");
|
|
||||||
println!("Empty input test: PASS");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_single_entry() {
|
|
||||||
let timing_data = vec![("60 FPS".to_string(), Duration::from_micros(1234), Duration::from_micros(567))];
|
|
||||||
|
|
||||||
let result = format_timing_display(timing_data);
|
|
||||||
println!("Single entry test:");
|
|
||||||
println!("{}", result);
|
|
||||||
println!();
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user