From c9d962df8dc6fb3a568481fede6016b718a8ddfa Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Sat, 13 Dec 2025 21:29:29 -0800 Subject: [PATCH] Shape layout stuff, test driven developmen to the rescue --- Cargo.lock | 127 +++++++++++++++++++++++++++++--- engine/src/lib.rs | 2 +- justfile | 3 + tetris/Cargo.toml | 2 +- tetris/assets/t.shape.toml | 1 + tetris/src/blocks.rs | 121 +++++++++++++++++++++++++----- tetris/src/debug.rs | 57 +++++++------- tetris/src/main.rs | 30 +++++--- tetris/src/test/mod.rs | 3 + tetris/src/test/shape_layout.rs | 70 ++++++++++++++++++ 10 files changed, 347 insertions(+), 69 deletions(-) create mode 100644 tetris/src/test/mod.rs create mode 100644 tetris/src/test/shape_layout.rs diff --git a/Cargo.lock b/Cargo.lock index b72d183..24ad93a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -540,6 +540,7 @@ dependencies = [ "futures-io", "futures-lite", "js-sys", + "notify-debouncer-full", "parking_lot", "ron", "serde", @@ -1876,7 +1877,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -2384,6 +2385,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "file-id" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9" +dependencies = [ + "windows-sys 0.60.2", +] + [[package]] name = "fixedbitset" version = "0.5.7" @@ -2485,6 +2495,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -2539,7 +2558,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi", + "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] @@ -3096,6 +3115,26 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "ktx2" version = "0.4.0" @@ -3292,6 +3331,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + [[package]] name = "naga" version = "26.0.0" @@ -3472,6 +3523,43 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.9.1", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-debouncer-full" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d88b1a7538054351c8258338df7c931a590513fb3745e8c15eb9ff4199b8d1" +dependencies = [ + "file-id", + "log", + "notify", + "notify-types", + "walkdir", +] + +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "ntapi" version = "0.4.1" @@ -5284,6 +5372,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -5710,7 +5804,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.2", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -5754,7 +5848,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link", + "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", ] @@ -5766,7 +5860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -5820,6 +5914,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -5827,7 +5927,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -5854,7 +5954,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -5873,7 +5973,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -5912,6 +6012,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -5980,7 +6089,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 03322cb..fb7334f 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -23,6 +23,7 @@ pub use bevy::{ camera::{primitives::*, visibility::*, *}, color::palettes::css::*, feathers::controls::*, + feathers::*, gizmos::{aabb::AabbGizmoPlugin, light::LightGizmoPlugin}, input::{ ButtonState, @@ -39,7 +40,6 @@ pub use bevy::{ render::render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages}, sprite_render::*, time::common_conditions::*, - feathers::*, ui::*, ui_widgets::{Button, *}, window::{WindowResized, WindowResolution}, diff --git a/justfile b/justfile index 67dfd4d..a65e767 100644 --- a/justfile +++ b/justfile @@ -6,6 +6,9 @@ check: run: cargo run -p tetris +test: + cargo test -p tetris + bindgen profile name: mkdir -p ./dist/{{name}} diff --git a/tetris/Cargo.toml b/tetris/Cargo.toml index 2b0d1f3..317a446 100644 --- a/tetris/Cargo.toml +++ b/tetris/Cargo.toml @@ -16,4 +16,4 @@ thiserror = "*" [dependencies.bevy] version = "0.17.2" -features = ["wayland", "dynamic_linking", "track_location", "experimental_bevy_feathers", "experimental_bevy_ui_widgets"] +features = ["wayland", "dynamic_linking", "track_location", "experimental_bevy_feathers", "experimental_bevy_ui_widgets", "file_watcher"] diff --git a/tetris/assets/t.shape.toml b/tetris/assets/t.shape.toml index 96a8e66..5a17574 100644 --- a/tetris/assets/t.shape.toml +++ b/tetris/assets/t.shape.toml @@ -2,3 +2,4 @@ layout = [ [0,1,0], [1,1,1], ] +tint = "#AA4500" diff --git a/tetris/src/blocks.rs b/tetris/src/blocks.rs index 27a5f99..021c21b 100644 --- a/tetris/src/blocks.rs +++ b/tetris/src/blocks.rs @@ -1,5 +1,8 @@ use super::*; +// TODO: +// - When shape asset is updated, shape should update in real time + /// Create tetris game with camera that renders to subset of viewport /// /// Focus on a single piece and making it really tight mechanically @@ -14,6 +17,7 @@ impl Plugin for BlocksPlugin { .init_asset_loader::() .add_systems(OnEnter(Loading(true)), load_assets.run_if(run_once)) .add_systems(OnEnter(GameState::Setup), (setup_camera, setup_blocks)) + .add_systems(Update, updated_shape_asset.run_if(on_message::>)) .add_observer(add_shape); } } @@ -22,16 +26,64 @@ impl Plugin for BlocksPlugin { /// Stores shape data in an asset file, likely toml #[derive(Asset, TypePath, Debug, Deserialize)] struct ShapeAsset { - layout: Vec>, + layout: ShapeLayout, + #[serde(default = "default_tint")] + tint: String, #[serde(skip)] mesh: Mesh2d, #[serde(skip)] material: MeshMaterial2d, } +fn default_tint() -> String { + "#aa00aa".into() +} + impl ShapeAsset { - fn into_bundle(&self) -> impl Bundle { - (self.mesh.clone(), self.material.clone()) + fn as_bundle(&self) -> impl Bundle { + let [a, b, c, d] = self.layout.positions(); + + related!(ShapeBlocks[ + (self.mesh.clone(), self.material.clone(), a), + (self.mesh.clone(), self.material.clone(), b), + (self.mesh.clone(), self.material.clone(), c), + (self.mesh.clone(), self.material.clone(), d), + ]) + } +} + +/// Block positions relative to the shape's center +#[derive(Component, PartialEq, Debug)] +pub(crate) struct RelativePosition { + pub x: isize, + pub y: isize, +} + +impl From<(isize, isize)> for RelativePosition { + fn from((x, y): (isize, isize)) -> Self { + RelativePosition { x, y } + } +} + +/// Layout for a given shape +#[derive(Debug, Deserialize)] +pub(crate) struct ShapeLayout(pub Vec>); + +impl ShapeLayout { + pub(crate) fn positions(&self) -> [RelativePosition;4] { + let mut c: Vec = Vec::with_capacity(4); + let center = { + + }; + for (y, nested) in self.0.iter().enumerate() { + for (x, val) in nested.iter().enumerate() { + if *val == 1 { + println!("{x}{y}"); + } + } + } + + todo!() } } @@ -43,7 +95,9 @@ enum ShapeAssetError { #[error("Failed to read file {0}")] Io(#[from] std::io::Error), #[error("Failed to parse file {0}")] - Parse(#[from] toml::de::Error), + ParseFile(#[from] toml::de::Error), + #[error("Failed to parse tint {0}")] + ParseTint(#[from] HexColorError), } impl AssetLoader for ShapeAssetLoader { @@ -58,29 +112,26 @@ impl AssetLoader for ShapeAssetLoader { ) -> Result { let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; - // TODO: Create sub-assets for mesh and material + let parsed = toml::from_slice::(bytes.as_slice())?; let mesh = { // https://docs.rs/bevy/latest/bevy/asset/struct.LoadContext.html#method.add_labeled_asset let m: Mesh = Rectangle::new(100.0, 100.0).into(); - let h: Handle = load_context.add_labeled_asset(format!("{}#mesh", load_context.asset_path()), m); + let h: Handle = + load_context.add_labeled_asset(format!("{}#mesh", load_context.asset_path()), m); Mesh2d(h) }; let material = { - let m = ColorMaterial { - color: PURPLE.into(), - ..default() - }; - let h: Handle = load_context.add_labeled_asset(format!("{}#material", load_context.asset_path()), m); + let color = Srgba::hex(parsed.tint.clone())?.into(); + let m = ColorMaterial { color, ..default() }; + let h: Handle = load_context + .add_labeled_asset(format!("{}#material", load_context.asset_path()), m); MeshMaterial2d(h) }; - let parsed = toml::from_slice::(bytes.as_slice())?; - Ok( - ShapeAsset { - mesh, - material, - ..parsed - } - ) + Ok(ShapeAsset { + mesh, + material, + ..parsed + }) } fn extensions(&self) -> &[&str] { @@ -117,6 +168,16 @@ fn setup_blocks( checklist.spawn_shape = true; } +/// Blocks <- Shape Relationship +#[derive(Component)] +#[relationship(relationship_target = ShapeBlocks)] +struct ShapeBlock(Entity); + +/// Shape -> Blocks Relationship +#[derive(Component)] +#[relationship_target(relationship = ShapeBlock)] +struct ShapeBlocks(Vec); + /// Event handler for transforming a handle component into a thing fn add_shape( event: On>, @@ -126,5 +187,25 @@ fn add_shape( ) { let asset_component = query.get(event.entity).unwrap(); let shape = shapes.get(asset_component.handle.id()).unwrap(); - commands.entity(event.entity).insert(shape.into_bundle()); + + commands.entity(event.entity).insert(shape.as_bundle()); +} + +fn updated_shape_asset( + mut messages: MessageReader>, + query: Query<&AssetComponent>, +) { + messages.read().for_each(|asset_event| { + match asset_event { + AssetEvent::Added { id } => debug!("Asset added: {id:?}"), + AssetEvent::Modified { id } => query.iter().filter(|AssetComponent { handle }| { + handle.id() == *id + }).for_each(|ac| { + warn!("TODO: Update {ac:?}"); + }), + AssetEvent::Removed { id } => warn!("Asset removed: {id:?}"), + AssetEvent::Unused { id } => warn!("Asset is unused: {id:?}"), + AssetEvent::LoadedWithDependencies { id } => debug!("Asset lodaed: {id:?}"), + } + }) } diff --git a/tetris/src/debug.rs b/tetris/src/debug.rs index 511cc56..afe90ee 100644 --- a/tetris/src/debug.rs +++ b/tetris/src/debug.rs @@ -16,22 +16,28 @@ pub struct DebugPlugin; // - Show current state(s) in UI impl Plugin for DebugPlugin { fn build(&self, app: &mut App) { - app - .init_state::() + app.init_state::() .add_systems(Startup, setup_ui) - .add_systems(Update, - // Logging state transitions - ( - log_transition::.run_if(state_changed::), - log_transition::.run_if(state_changed::), - log_transition::.run_if(state_changed::), - )) - .add_systems(Update, toggle_debug.run_if(input_just_pressed(KeyCode::F12))) - .add_systems(Update, + .add_systems( + Update, + // Logging state transitions + ( + log_transition::.run_if(state_changed::), + log_transition::.run_if(state_changed::), + log_transition::.run_if(state_changed::), + ), + ) + .add_systems( + Update, + toggle_debug.run_if(input_just_pressed(KeyCode::F12)), + ) + .add_systems( + Update, ( toggle_state_visibility::, sync_state_to_ui::, - ).run_if(state_changed::) + ) + .run_if(state_changed::), ); } } @@ -53,9 +59,7 @@ impl fmt::Display for DebugState { } /// Setup the debugger UI -fn setup_ui( - mut commands: Commands, -) { +fn setup_ui(mut commands: Commands) { commands.spawn(( Node { display: Display::Flex, @@ -67,7 +71,11 @@ fn setup_ui( }, children![ (Text::new("debugger:"), ThemedText), - (toggle_switch((),), observe(checkbox_self_update), observe(debug_toggle)), + ( + toggle_switch((),), + observe(checkbox_self_update), + observe(debug_toggle) + ), ], )); @@ -81,18 +89,18 @@ fn setup_ui( }, DebugState(true), children![ - (Text::new("DebugState State"), SyncState::::default()), + ( + Text::new("DebugState State"), + SyncState::::default() + ), (Text::new("Loading State"), SyncState::::default()), (Text::new("Game State"), SyncState::::default()), - ] + ], )); } /// Logs all state transitions for state T -fn log_transition( - curr: Res>, - mut prev: Local>, -) { +fn log_transition(curr: Res>, mut prev: Local>) { debug_assert!(Some(curr.get().clone()) != *prev); info!("State Change:: {:?} -> {:?}", *prev, *curr); @@ -101,10 +109,7 @@ fn log_transition( } /// Toggle the debug state when a key is pressed -fn toggle_debug( - curr: Res>, - mut next: ResMut>, -) { +fn toggle_debug(curr: Res>, mut next: ResMut>) { next.set(DebugState(!curr.get().0)); } diff --git a/tetris/src/main.rs b/tetris/src/main.rs index 29647f2..5b17969 100644 --- a/tetris/src/main.rs +++ b/tetris/src/main.rs @@ -1,6 +1,7 @@ mod blocks; mod debug; mod fighter; +mod test; use engine::*; use serde::Deserialize; @@ -19,10 +20,10 @@ fn main() { .init_state::() .init_resource::() .init_resource::() - // Check if assets were added to loading queue + // Move game back to Loading(true) state .add_systems( Update, - loading_check + set_loading .run_if(in_state(Loading(true))) .run_if(resource_changed::), ) @@ -33,16 +34,21 @@ fn main() { // Check if the game is ready to progress .add_systems(Update, setup_wait.run_if(in_state(GameState::Setup))) // State toggles - .add_systems(Update, ( - ( - toggle_state_visibility::, - sync_state_to_ui::, - ).run_if(state_changed::), + .add_systems( + Update, ( - toggle_state_visibility::, - sync_state_to_ui::, - ).run_if(state_changed::) - )) + ( + toggle_state_visibility::, + sync_state_to_ui::, + ) + .run_if(state_changed::), + ( + toggle_state_visibility::, + sync_state_to_ui::, + ) + .run_if(state_changed::), + ), + ) .run(); } @@ -100,7 +106,7 @@ impl SetupChecklist { } /// Sends the game into Loading::Active if assets are added to the AllAssets list -fn loading_check(mut next: ResMut>, all_assets: Res) { +fn set_loading(mut next: ResMut>, all_assets: Res) { debug_assert!(all_assets.is_changed()); next.set(Loading(true)); diff --git a/tetris/src/test/mod.rs b/tetris/src/test/mod.rs new file mode 100644 index 0000000..1a5c317 --- /dev/null +++ b/tetris/src/test/mod.rs @@ -0,0 +1,3 @@ +use super::*; + +mod shape_layout; diff --git a/tetris/src/test/shape_layout.rs b/tetris/src/test/shape_layout.rs new file mode 100644 index 0000000..41597e6 --- /dev/null +++ b/tetris/src/test/shape_layout.rs @@ -0,0 +1,70 @@ +use super::*; + +#[test] +fn test_shape_layout_01() { + let actual = ShapeLayout(vec![ + vec![0, 1, 0], + vec![1, 1, 1], + ]).positions(); + + let expected: [RelativePosition;4] = [ + (1, 1).into(), + (-1, 0).into(), + (0, 0).into(), + (1, 0).into() + ]; + + debug_assert_eq!(expected, actual); +} + +#[test] +fn test_shape_layout_02a() { + let actual = ShapeLayout(vec![ + vec![1], + vec![1], + vec![1], + vec![1], + ]).positions(); + + let expected: [RelativePosition;4] = [ + (0, 2).into(), + (0, 1).into(), + (0, 0).into(), + (0, -1).into() + ]; + + debug_assert_eq!(expected, actual); +} + +#[test] +fn test_shape_layout_02b() { + let actual = ShapeLayout(vec![ + vec![1, 1, 1, 1], + ]).positions(); + + let expected: [RelativePosition;4] = [ + (-1, 0).into(), + (0, 0).into(), + (0, 1).into(), + (0, 2).into() + ]; + + debug_assert_eq!(expected, actual); +} + +#[test] +fn test_shape_layout_03() { + let actual = ShapeLayout(vec![ + vec![1, 1, 0], + vec![0, 1, 1], + ]).positions(); + + let expected: [RelativePosition;4] = [ + (-1, 1).into(), + (0, 1).into(), + (0, 0).into(), + (1, 0).into() + ]; + + debug_assert_eq!(expected, actual); +}