Merge pull request 'refactor(engine): first ambiguity burn-down batch — 302 → 198 pairs' (#146) from refactor/ambiguity-burndown into master
Web WASM Rebuild / rebuild (push) Has been cancelled
Build and Deploy / build-and-push (push) Successful in 1m53s
Test / test (push) Successful in 20m32s

This commit was merged in pull request #146.
This commit is contained in:
2026-07-07 03:37:53 +00:00
5 changed files with 70 additions and 22 deletions
+5 -3
View File
@@ -570,8 +570,8 @@ impl Plugin for CardPlugin {
tick_flip_anim,
update_drag_shadow,
update_card_shadows_on_drag.after(sync_cards_on_change),
tick_hint_highlight,
handle_right_click,
tick_hint_highlight.after(GameMutation),
handle_right_click.after(GameMutation),
tick_right_click_highlights,
clear_right_click_highlights_on_state_change.after(GameMutation),
clear_right_click_highlights_on_pause,
@@ -580,7 +580,9 @@ impl Plugin for CardPlugin {
.after(GameMutation)
.run_if(resource_changed::<GameStateResource>),
collect_resize_events.after(LayoutSystem::UpdateOnResize),
snap_cards_on_window_resize.after(collect_resize_events),
snap_cards_on_window_resize
.after(collect_resize_events)
.after(GameMutation),
),
);
+48 -9
View File
@@ -63,6 +63,18 @@ pub struct GameOverScreen;
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct GameMutation;
/// System set for every writer of [`crate::events::NewGameRequestEvent`].
///
/// Many UI entry points fire this trigger (buttons, keyboard, modals,
/// mode pickers). Their relative append order within a frame is
/// meaningless — consumers drain the whole queue — so members are
/// registered `.in_set(NewGameRequestWriters).ambiguous_with(NewGameRequestWriters)`
/// to declare writer-vs-writer order irrelevant instead of leaving it as an
/// ambiguity (#143). Only ever combine with `.ambiguous_with` on the same
/// set; do NOT hang ordering edges off this set.
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct NewGameRequestWriters;
/// Persistence path for the in-progress game state file. `None` disables I/O.
#[derive(Resource, Debug, Clone)]
pub struct GameStatePath(pub Option<PathBuf>);
@@ -208,7 +220,13 @@ impl Plugin for GamePlugin {
.add_message::<AppLifecycle>()
// add_message is idempotent; SettingsPlugin also registers this.
.add_message::<crate::settings_plugin::SettingsChangedEvent>()
.add_systems(Update, poll_pending_new_game_seed.before(GameMutation))
.add_systems(
Update,
poll_pending_new_game_seed
.before(GameMutation)
.in_set(NewGameRequestWriters)
.ambiguous_with(NewGameRequestWriters),
)
.add_systems(
Update,
(handle_new_game, handle_draw, handle_move, handle_undo)
@@ -217,19 +235,40 @@ impl Plugin for GamePlugin {
)
.add_systems(Update, check_no_moves.after(GameMutation))
.add_systems(Update, record_replay_on_win.after(GameMutation))
.add_systems(Update, handle_confirm_input.after(GameMutation))
.add_systems(Update, handle_confirm_button_input.after(GameMutation))
.add_systems(Update, handle_game_over_input.after(GameMutation))
.add_systems(Update, handle_game_over_button_input.after(GameMutation))
.add_systems(
Update,
(
handle_confirm_input,
handle_confirm_button_input,
handle_game_over_input,
handle_game_over_button_input,
)
.after(GameMutation)
.in_set(NewGameRequestWriters)
.ambiguous_with(NewGameRequestWriters),
)
// Restore prompt: spawn the modal once the splash is gone,
// route Continue / New Game intents back into the existing
// GameMutation flow.
.add_systems(Update, spawn_restore_prompt_if_pending)
.add_systems(Update, handle_restore_prompt.before(GameMutation))
.add_systems(Update, sync_settings_to_game.before(GameMutation))
// All three pre-mutation game-state writers are chained: elapsed
// time ticks first, settings sync next, then the restore prompt —
// a deterministic spine instead of three unordered ResMut holders
// (ambiguity burn-down, #143).
.add_systems(
Update,
(
tick_elapsed_time,
sync_settings_to_game,
handle_restore_prompt
.in_set(NewGameRequestWriters)
.ambiguous_with(NewGameRequestWriters),
)
.chain()
.before(GameMutation),
)
.init_resource::<AutoSaveTimer>()
.add_systems(Update, tick_elapsed_time)
.add_systems(Update, auto_save_game_state)
.add_systems(Update, auto_save_game_state.after(GameMutation))
.add_systems(Last, save_game_state_on_exit);
}
}
+14 -8
View File
@@ -36,7 +36,7 @@ use crate::events::{
UndoRequestEvent, WinStreakMilestoneEvent,
};
use crate::font_plugin::FontResource;
use crate::game_plugin::GameMutation;
use crate::game_plugin::{GameMutation, NewGameRequestWriters};
#[cfg(target_os = "android")]
use crate::input_plugin::TouchDragSet;
use crate::layout::HUD_BAND_HEIGHT;
@@ -478,10 +478,12 @@ impl Plugin for HudPlugin {
.add_systems(Update, announce_auto_complete.after(GameMutation))
.add_systems(
Update,
update_selection_hud.run_if(
resource_exists_and_changed::<SelectionState>
.or(resource_exists_and_changed::<GameStateResource>),
),
update_selection_hud
.after(GameMutation)
.run_if(
resource_exists_and_changed::<SelectionState>
.or(resource_exists_and_changed::<GameStateResource>),
),
)
.add_systems(Update, update_hud_typography)
.add_systems(
@@ -503,13 +505,17 @@ impl Plugin for HudPlugin {
.add_systems(
Update,
(
handle_new_game_button,
handle_new_game_button
.in_set(NewGameRequestWriters)
.ambiguous_with(NewGameRequestWriters),
handle_undo_button,
handle_pause_button,
handle_help_button,
handle_hint_button,
handle_hint_button.after(GameMutation),
handle_modes_button,
handle_mode_option_click,
handle_mode_option_click
.in_set(NewGameRequestWriters)
.ambiguous_with(NewGameRequestWriters),
handle_modes_backdrop_click,
close_modes_popover_on_escape,
handle_menu_button,
+1 -1
View File
@@ -38,7 +38,7 @@ mod tests {
/// ordering edge — add `.before`/`.after` (order matters) or
/// `.ambiguous_with` (provably order-independent) at the registration
/// site. When triage lowers the real count, lower this constant too.
const AMBIGUITY_BASELINE: usize = 302;
const AMBIGUITY_BASELINE: usize = 198;
fn cluster_app() -> App {
let mut app = App::new();
+2 -1
View File
@@ -11,6 +11,7 @@ use solitaire_core::{FOUNDATIONS, TABLEAUS};
use solitaire_core::Suit;
use crate::events::{HintVisualEvent, StateChangedEvent};
use crate::game_plugin::GameMutation;
use crate::hud_plugin::HudVisibility;
use crate::layout::{
Layout, LayoutResource, LayoutSystem, TABLE_COLOUR, apply_dynamic_tableau_fan, compute_layout,
@@ -103,7 +104,7 @@ impl Plugin for TablePlugin {
apply_theme_on_settings_change,
apply_hint_pile_highlight,
tick_hint_pile_highlights,
sync_pile_marker_visibility,
sync_pile_marker_visibility.after(GameMutation),
),
);
}