chore: cargo fmt across workspace; add analytics domain to CSP
Build and Deploy / build-and-push (push) Successful in 4m46s

- Apply cargo fmt to solitaire_engine, solitaire_server formatting.
- solitaire_server/src/lib.rs: add https://analytics.aleshym.co to
  script-src, img-src, and connect-src so the analytics beacon loads
  without a CSP violation.
- docs and README updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-02 12:21:32 -07:00
parent baf524ec75
commit 1cdb78caf2
25 changed files with 229 additions and 130 deletions
+66 -48
View File
@@ -13,8 +13,8 @@ use chrono::Utc;
use bevy::prelude::*;
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
use bevy::window::AppLifecycle;
use solitaire_core::game_state::{DrawMode, GameMode, GameState};
use klondike::KlondikePile;
use solitaire_core::game_state::{DrawMode, GameMode, GameState};
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve};
#[allow(deprecated)]
use solitaire_data::latest_replay_path;
@@ -521,10 +521,7 @@ fn handle_new_game(
// hides that information and reads naturally as "dealt from the
// deck." Skipped when LayoutResource isn't present (headless tests).
if let Some(layout) = layout.as_ref()
&& let Some(stock) = layout
.0
.pile_positions
.get(&klondike::KlondikePile::Stock)
&& let Some(stock) = layout.0.pile_positions.get(&klondike::KlondikePile::Stock)
{
for mut tx in &mut card_transforms {
tx.translation.x = stock.x;
@@ -1047,17 +1044,11 @@ fn foundation_slot(foundation: klondike::Foundation) -> Option<u8> {
/// previous heuristic incorrectly did (Quat hit this with 4 cards
/// remaining and the game just sat there).
pub fn has_legal_moves(game: &GameState) -> bool {
// Drawing from a non-empty stock, and recycling a non-empty waste back to
// stock, are always legal moves in standard Klondike (unlimited recycles).
// A game can only be genuinely stuck when both stock AND waste are exhausted.
let stock_empty = game
.stock_cards()
.is_empty();
let waste_empty = game
.waste_cards()
.is_empty();
let stock_empty = game.stock_cards().is_empty();
let waste_empty = game.waste_cards().is_empty();
if !stock_empty || !waste_empty {
return true;
}
@@ -1191,7 +1182,10 @@ fn handle_game_over_input(
if keys.just_pressed(KeyCode::KeyN) || keys.just_pressed(KeyCode::Escape) {
// confirmed: true — the game is already stuck; no abandon-confirmation needed.
new_game.write(NewGameRequestEvent { confirmed: true, ..default() });
new_game.write(NewGameRequestEvent {
confirmed: true,
..default()
});
} else if keys.just_pressed(KeyCode::KeyU) {
for entity in &screens {
commands.entity(entity).despawn();
@@ -1219,7 +1213,10 @@ fn handle_game_over_button_input(
}
if new_game_buttons.iter().any(|i| *i == Interaction::Pressed) {
// confirmed: true — the game is already stuck; no abandon-confirmation needed.
new_game.write(NewGameRequestEvent { confirmed: true, ..default() });
new_game.write(NewGameRequestEvent {
confirmed: true,
..default()
});
} else if undo_buttons.iter().any(|i| *i == Interaction::Pressed) {
for entity in &screens {
commands.entity(entity).despawn();
@@ -1388,9 +1385,11 @@ mod tests {
#[test]
fn new_game_request_reseeds() {
let mut app = test_app(1);
let before: Vec<u32> = app.world().resource::<GameStateResource>().0.pile(KlondikePile::Tableau(
Tableau::Tableau1,
))
let before: Vec<u32> = app
.world()
.resource::<GameStateResource>()
.0
.pile(KlondikePile::Tableau(Tableau::Tableau1))
.iter()
.map(|c| c.id)
.collect();
@@ -1402,9 +1401,11 @@ mod tests {
});
app.update();
let after: Vec<u32> = app.world().resource::<GameStateResource>().0.pile(KlondikePile::Tableau(
Tableau::Tableau1,
))
let after: Vec<u32> = app
.world()
.resource::<GameStateResource>()
.0
.pile(KlondikePile::Tableau(Tableau::Tableau1))
.iter()
.map(|c| c.id)
.collect();
@@ -1415,17 +1416,25 @@ mod tests {
fn settings_changed_updates_take_from_foundation_flag() {
let mut app = test_app(1);
assert!(
app.world().resource::<GameStateResource>().0.take_from_foundation,
app.world()
.resource::<GameStateResource>()
.0
.take_from_foundation,
"fresh game should inherit default take_from_foundation=true",
);
let mut settings = solitaire_data::Settings::default();
settings.take_from_foundation = false;
app.world_mut()
.write_message(crate::settings_plugin::SettingsChangedEvent(settings.clone()));
.write_message(crate::settings_plugin::SettingsChangedEvent(
settings.clone(),
));
app.update();
assert!(
!app.world().resource::<GameStateResource>().0.take_from_foundation,
!app.world()
.resource::<GameStateResource>()
.0
.take_from_foundation,
"settings event must forward take_from_foundation=false into live game state",
);
@@ -1434,7 +1443,10 @@ mod tests {
.write_message(crate::settings_plugin::SettingsChangedEvent(settings));
app.update();
assert!(
app.world().resource::<GameStateResource>().0.take_from_foundation,
app.world()
.resource::<GameStateResource>()
.0
.take_from_foundation,
"settings event must forward take_from_foundation=true into live game state",
);
}
@@ -1557,7 +1569,7 @@ mod tests {
);
}
/// auto_save_game_state writes to disk once the accumulator crosses 30 s.
/// auto_save_game_state writes to disk once the accumulator crosses 30 s.
///
/// The timer is pre-seeded just past the threshold and the test
/// re-arms it before each `app.update()` in a small bounded loop:
@@ -1634,20 +1646,23 @@ mod tests {
// Build a tableau with two face-up cards.
{
let mut gs = app.world_mut().resource_mut::<GameStateResource>();
gs.0.set_test_tableau_cards(Tableau::Tableau1, vec![
Card {
id: 910,
suit: Suit::Clubs,
rank: Rank::King,
face_up: true,
},
Card {
id: 911,
suit: Suit::Hearts,
rank: Rank::Queen,
face_up: true,
},
]);
gs.0.set_test_tableau_cards(
Tableau::Tableau1,
vec![
Card {
id: 910,
suit: Suit::Clubs,
rank: Rank::King,
face_up: true,
},
Card {
id: 911,
suit: Suit::Hearts,
rank: Rank::Queen,
face_up: true,
},
],
);
gs.0.set_test_tableau_cards(
Tableau::Tableau2,
vec![Card {
@@ -1782,7 +1797,7 @@ mod tests {
);
}
#[test]
#[test]
fn has_legal_moves_detects_non_top_face_up_card_as_source() {
// Regression: the bug only checked t.cards.last() (top face-up card).
// If the only legal move involves a face-up card that is NOT the top
@@ -1936,16 +1951,16 @@ mod tests {
);
}
/// Verify that the game-over overlay contains the expected header text and
/// Verify that the game-over overlay contains the expected header text and
/// action-hint strings so players understand why the overlay appeared and
/// what keys to press.
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
// Task #56 — Escape dismisses GameOverScreen and starts new game
// -----------------------------------------------------------------------
/// Pressing Escape while `GameOverScreen` is visible must fire
/// `NewGameRequestEvent` — identical behaviour to pressing N.
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
// Task #48 — Undo with empty stack fires InfoToastEvent
// -----------------------------------------------------------------------
@@ -1988,7 +2003,7 @@ mod tests {
/// When a King lands on a foundation that already holds Ace through
/// Queen, exactly one `FoundationCompletedEvent` must fire and carry
/// the matching slot + suit.
/// Moving a card to a tableau pile must never produce a
/// Moving a card to a tableau pile must never produce a
/// `FoundationCompletedEvent`, even if the source tableau happened
/// to have been a King.
#[test]
@@ -2051,7 +2066,7 @@ mod tests {
/// At 12 cards on a foundation (AceJack on the pile, Queen in
/// flight), the event must NOT fire — the flourish is only for the
/// final 13th completion.
/// A successful undo must NOT fire an `InfoToastEvent`.
/// A successful undo must NOT fire an `InfoToastEvent`.
#[test]
fn undo_after_draw_does_not_fire_info_toast() {
let mut app = test_app(42);
@@ -2086,7 +2101,7 @@ mod tests {
/// Drive a fresh game through a draw + a tableau→foundation move,
/// then assert the recording resource captured both, in order, with
/// the correct shape.
/// Invalid moves must not appear in the recording — the recording is
/// Invalid moves must not appear in the recording — the recording is
/// "what successfully happened", not "what was requested".
#[test]
fn replay_does_not_record_rejected_moves() {
@@ -2359,7 +2374,10 @@ mod tests {
Tableau::Tableau7,
] {
assert_eq!(
app.world().resource::<GameStateResource>().0.pile(KlondikePile::Tableau(tableau)),
app.world()
.resource::<GameStateResource>()
.0
.pile(KlondikePile::Tableau(tableau)),
expected.pile(KlondikePile::Tableau(tableau)),
"tableau column {tableau:?} must match the unfiltered seed",
);