style: cargo fmt under rustfmt 1.9 and gate formatting in CI
The repo was formatted under an older stable; rustfmt 1.9 (Rust 1.95) wraps signatures and call sites differently, so every touched file was picking up unrelated formatting hunks. One mechanical pass, and a 'cargo fmt --check' step in the test workflow (same pinned 1.95.0 toolchain) so drift can't accumulate again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,7 +46,7 @@ jobs:
|
|||||||
uses: dtolnay/rust-toolchain@master
|
uses: dtolnay/rust-toolchain@master
|
||||||
with:
|
with:
|
||||||
toolchain: 1.95.0
|
toolchain: 1.95.0
|
||||||
components: clippy
|
components: clippy, rustfmt
|
||||||
|
|
||||||
- name: Cache cargo build
|
- name: Cache cargo build
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -60,6 +60,9 @@ jobs:
|
|||||||
libasound2-dev libudev-dev pkg-config libx11-dev libxcursor-dev \
|
libasound2-dev libudev-dev pkg-config libx11-dev libxcursor-dev \
|
||||||
libxrandr-dev libxi-dev libwayland-dev libxkbcommon-dev
|
libxrandr-dev libxi-dev libwayland-dev libxkbcommon-dev
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: cargo fmt --check
|
||||||
|
|
||||||
# SQLX_OFFLINE uses the checked-in `.sqlx/` query cache (no live DB),
|
# SQLX_OFFLINE uses the checked-in `.sqlx/` query cache (no live DB),
|
||||||
# same as the web-e2e workflow's server prebuild.
|
# same as the web-e2e workflow's server prebuild.
|
||||||
- name: Clippy (deny warnings)
|
- name: Clippy (deny warnings)
|
||||||
|
|||||||
@@ -439,10 +439,7 @@ impl GameState {
|
|||||||
self.session.history().len()
|
self.session.history().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cards_with_face(
|
fn cards_with_face(cards: impl IntoIterator<Item = Card>, face_up: bool) -> Vec<(Card, bool)> {
|
||||||
cards: impl IntoIterator<Item = Card>,
|
|
||||||
face_up: bool,
|
|
||||||
) -> Vec<(Card, bool)> {
|
|
||||||
cards.into_iter().map(|card| (card, face_up)).collect()
|
cards.into_iter().map(|card| (card, face_up)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,8 +504,10 @@ impl GameState {
|
|||||||
Self::cards_with_face(cards.iter().cloned(), true)
|
Self::cards_with_face(cards.iter().cloned(), true)
|
||||||
}
|
}
|
||||||
KlondikePile::Tableau(tableau) => {
|
KlondikePile::Tableau(tableau) => {
|
||||||
let mut cards =
|
let mut cards = Self::cards_with_face(
|
||||||
Self::cards_with_face(state.tableau_face_down_cards(tableau).iter().cloned(), false);
|
state.tableau_face_down_cards(tableau).iter().cloned(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
cards.extend(Self::cards_with_face(
|
cards.extend(Self::cards_with_face(
|
||||||
state.tableau_face_up_cards(tableau).iter().cloned(),
|
state.tableau_face_up_cards(tableau).iter().cloned(),
|
||||||
true,
|
true,
|
||||||
@@ -823,9 +822,7 @@ impl GameState {
|
|||||||
) -> Option<(KlondikePile, KlondikePile, usize)> {
|
) -> Option<(KlondikePile, KlondikePile, usize)> {
|
||||||
let state = self.session.state().state().state();
|
let state = self.session.state().state().state();
|
||||||
match instruction {
|
match instruction {
|
||||||
KlondikeInstruction::RotateStock => {
|
KlondikeInstruction::RotateStock => Some((KlondikePile::Stock, KlondikePile::Stock, 1)),
|
||||||
Some((KlondikePile::Stock, KlondikePile::Stock, 1))
|
|
||||||
}
|
|
||||||
KlondikeInstruction::DstFoundation(dst_foundation) => {
|
KlondikeInstruction::DstFoundation(dst_foundation) => {
|
||||||
if matches!(dst_foundation.src, KlondikePile::Foundation(_)) {
|
if matches!(dst_foundation.src, KlondikePile::Foundation(_)) {
|
||||||
return None;
|
return None;
|
||||||
@@ -928,10 +925,7 @@ impl GameState {
|
|||||||
///
|
///
|
||||||
/// Returns [`MoveError::RuleViolation`] if the instruction is illegal in the
|
/// Returns [`MoveError::RuleViolation`] if the instruction is illegal in the
|
||||||
/// current position, or [`MoveError::GameAlreadyWon`] once the game is over.
|
/// current position, or [`MoveError::GameAlreadyWon`] once the game is over.
|
||||||
pub fn apply_instruction(
|
pub fn apply_instruction(&mut self, instruction: KlondikeInstruction) -> Result<(), MoveError> {
|
||||||
&mut self,
|
|
||||||
instruction: KlondikeInstruction,
|
|
||||||
) -> Result<(), MoveError> {
|
|
||||||
if self.is_won() {
|
if self.is_won() {
|
||||||
return Err(MoveError::GameAlreadyWon);
|
return Err(MoveError::GameAlreadyWon);
|
||||||
}
|
}
|
||||||
@@ -1296,12 +1290,13 @@ mod tests {
|
|||||||
|
|
||||||
game.take_from_foundation = false;
|
game.take_from_foundation = false;
|
||||||
assert!(!game.can_move_cards(&from, &to, 1));
|
assert!(!game.can_move_cards(&from, &to, 1));
|
||||||
assert!(
|
assert!(legal_pile_moves(&game).iter().all(|(f, t, _)| !matches!(
|
||||||
legal_pile_moves(&game)
|
f,
|
||||||
.iter()
|
KlondikePile::Foundation(_)
|
||||||
.all(|(f, t, _)| !matches!(f, KlondikePile::Foundation(_))
|
) || !matches!(
|
||||||
|| !matches!(t, KlondikePile::Tableau(_)))
|
t,
|
||||||
);
|
KlondikePile::Tableau(_)
|
||||||
|
)));
|
||||||
assert!(game.move_cards(from, to, 1).is_err());
|
assert!(game.move_cards(from, to, 1).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1360,9 +1355,18 @@ mod tests {
|
|||||||
fn budget_is_passed_through_not_clamped() {
|
fn budget_is_passed_through_not_clamped() {
|
||||||
// This seed is Inconclusive at 1k states but Winnable at 5k — proving the
|
// This seed is Inconclusive at 1k states but Winnable at 5k — proving the
|
||||||
// budget reaches the solver unchanged.
|
// budget reaches the solver unchanged.
|
||||||
let easy = GameState::solve_fresh_deal(0xD1FF_0000_0000_0012, DrawStockConfig::DrawOne, 1_000, 1_000);
|
let easy = GameState::solve_fresh_deal(
|
||||||
let medium =
|
0xD1FF_0000_0000_0012,
|
||||||
GameState::solve_fresh_deal(0xD1FF_0000_0000_0012, DrawStockConfig::DrawOne, 5_000, 5_000);
|
DrawStockConfig::DrawOne,
|
||||||
|
1_000,
|
||||||
|
1_000,
|
||||||
|
);
|
||||||
|
let medium = GameState::solve_fresh_deal(
|
||||||
|
0xD1FF_0000_0000_0012,
|
||||||
|
DrawStockConfig::DrawOne,
|
||||||
|
5_000,
|
||||||
|
5_000,
|
||||||
|
);
|
||||||
assert!(easy.is_err());
|
assert!(easy.is_err());
|
||||||
assert!(matches!(medium, Ok(Some(_))));
|
assert!(matches!(medium, Ok(Some(_))));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ pub mod scoring;
|
|||||||
// when decoding instructions to piles in `instruction_to_piles`) and do not
|
// when decoding instructions to piles in `instruction_to_piles`) and do not
|
||||||
// appear in any public method signature.
|
// appear in any public method signature.
|
||||||
pub use card_game::{Card, Deck, Rank, Session, SolveError, Suit};
|
pub use card_game::{Card, Deck, Rank, Session, SolveError, Suit};
|
||||||
pub use klondike::{DrawStockConfig, Foundation, Klondike, KlondikeInstruction, KlondikePile, Tableau};
|
pub use klondike::{
|
||||||
|
DrawStockConfig, Foundation, Klondike, KlondikeInstruction, KlondikePile, Tableau,
|
||||||
|
};
|
||||||
|
|
||||||
// Solvability check API (delegates to `card_game::Session::solve`); replaces the
|
// Solvability check API (delegates to `card_game::Session::solve`); replaces the
|
||||||
// former `solitaire_data::solver` wrapper module.
|
// former `solitaire_data::solver` wrapper module.
|
||||||
|
|||||||
@@ -41,13 +41,20 @@ fn all_cards(game: &GameState) -> Vec<Card> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
for t in &tableaux {
|
for t in &tableaux {
|
||||||
cards.extend(game.pile(KlondikePile::Tableau(*t)).iter().map(|(c, _)| c.clone()));
|
cards.extend(
|
||||||
|
game.pile(KlondikePile::Tableau(*t))
|
||||||
|
.iter()
|
||||||
|
.map(|(c, _)| c.clone()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
cards
|
cards
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_mode_strategy() -> impl Strategy<Value = DrawStockConfig> {
|
fn draw_mode_strategy() -> impl Strategy<Value = DrawStockConfig> {
|
||||||
prop_oneof![Just(DrawStockConfig::DrawOne), Just(DrawStockConfig::DrawThree)]
|
prop_oneof![
|
||||||
|
Just(DrawStockConfig::DrawOne),
|
||||||
|
Just(DrawStockConfig::DrawThree)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply a sequence of random actions to a game, silently ignoring errors.
|
/// Apply a sequence of random actions to a game, silently ignoring errors.
|
||||||
|
|||||||
@@ -553,8 +553,8 @@ mod tests {
|
|||||||
"saved file must use schema version 5",
|
"saved file must use schema version 5",
|
||||||
);
|
);
|
||||||
|
|
||||||
let loaded = load_game_state_from(&path)
|
let loaded =
|
||||||
.expect("a valid in-progress game must load without error");
|
load_game_state_from(&path).expect("a valid in-progress game must load without error");
|
||||||
|
|
||||||
// The forward instruction history round-trips, so the reconstructed board
|
// The forward instruction history round-trips, so the reconstructed board
|
||||||
// re-serialises to byte-identical JSON.
|
// re-serialises to byte-identical JSON.
|
||||||
@@ -569,7 +569,11 @@ mod tests {
|
|||||||
|
|
||||||
// Derived board reads match the live game (move count + recycle count are
|
// Derived board reads match the live game (move count + recycle count are
|
||||||
// both rebuilt from the replayed forward history).
|
// both rebuilt from the replayed forward history).
|
||||||
assert_eq!(loaded.move_count(), gs.move_count(), "move_count round-trips");
|
assert_eq!(
|
||||||
|
loaded.move_count(),
|
||||||
|
gs.move_count(),
|
||||||
|
"move_count round-trips"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
loaded.recycle_count(),
|
loaded.recycle_count(),
|
||||||
gs.recycle_count(),
|
gs.recycle_count(),
|
||||||
|
|||||||
@@ -911,8 +911,7 @@ mod tests {
|
|||||||
|
|
||||||
// Put the active game in Zen mode. evaluate_on_win reads
|
// Put the active game in Zen mode. evaluate_on_win reads
|
||||||
// GameStateResource.mode directly to populate last_win_is_zen.
|
// GameStateResource.mode directly to populate last_win_is_zen.
|
||||||
app.world_mut().resource_mut::<GameStateResource>().0.mode =
|
app.world_mut().resource_mut::<GameStateResource>().0.mode = GameMode::Zen;
|
||||||
GameMode::Zen;
|
|
||||||
|
|
||||||
app.world_mut().write_message(GameWonEvent {
|
app.world_mut().write_message(GameWonEvent {
|
||||||
score: 0,
|
score: 0,
|
||||||
|
|||||||
@@ -174,9 +174,9 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::game_plugin::GamePlugin;
|
use crate::game_plugin::GamePlugin;
|
||||||
use crate::table_plugin::TablePlugin;
|
use crate::table_plugin::TablePlugin;
|
||||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
|
||||||
use solitaire_core::{Deck, Rank, Suit};
|
use solitaire_core::{Deck, Rank, Suit};
|
||||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||||
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
|
|
||||||
fn headless_app() -> App {
|
fn headless_app() -> App {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
@@ -214,7 +214,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
g.set_test_tableau_cards(
|
g.set_test_tableau_cards(
|
||||||
Tableau::Tableau1,
|
Tableau::Tableau1,
|
||||||
vec![solitaire_core::Card::new(Deck::Deck1, Suit::Clubs, Rank::Ace)],
|
vec![solitaire_core::Card::new(
|
||||||
|
Deck::Deck1,
|
||||||
|
Suit::Clubs,
|
||||||
|
Rank::Ace,
|
||||||
|
)],
|
||||||
);
|
);
|
||||||
g.set_test_auto_completable(true);
|
g.set_test_auto_completable(true);
|
||||||
let expected = (
|
let expected = (
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ pub struct HoverState {
|
|||||||
/// Describes a user action that arrived while cards were still animating.
|
/// Describes a user action that arrived while cards were still animating.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum BufferedInput {
|
pub enum BufferedInput {
|
||||||
Move {
|
Move { from: MoveRequestEvent },
|
||||||
from: MoveRequestEvent,
|
|
||||||
},
|
|
||||||
Draw,
|
Draw,
|
||||||
Undo,
|
Undo,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ use crate::animation_plugin::EffectiveSlideDuration;
|
|||||||
use crate::events::{CardFaceRevealedEvent, CardFlippedEvent};
|
use crate::events::{CardFaceRevealedEvent, CardFlippedEvent};
|
||||||
use crate::layout::LayoutResource;
|
use crate::layout::LayoutResource;
|
||||||
use crate::resources::DragState;
|
use crate::resources::DragState;
|
||||||
use crate::ui_theme::{
|
use crate::ui_theme::{CARD_SHADOW_ALPHA_DRAG, CARD_SHADOW_COLOR, CARD_SHADOW_LOCAL_Z};
|
||||||
CARD_SHADOW_ALPHA_DRAG, CARD_SHADOW_COLOR, CARD_SHADOW_LOCAL_Z,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Listens for `CardFlippedEvent` and inserts a `CardFlipAnim` on the entity.
|
/// Listens for `CardFlippedEvent` and inserts a `CardFlipAnim` on the entity.
|
||||||
///
|
///
|
||||||
@@ -197,4 +195,3 @@ pub(super) fn update_card_shadows_on_drag(
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Task #28 — Hint highlight tick system
|
// Task #28 — Hint highlight tick system
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
use bevy::color::Color;
|
use bevy::color::Color;
|
||||||
use solitaire_core::Card;
|
use solitaire_core::Card;
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
@@ -272,5 +271,3 @@ pub(super) fn find_top_card_at(
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Task #28 — Stock-empty visual indicator
|
// Task #28 — Stock-empty visual indicator
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
use bevy::color::Color;
|
use bevy::color::Color;
|
||||||
use bevy::sprite::Anchor;
|
use bevy::sprite::Anchor;
|
||||||
use solitaire_core::{Card, Rank, Suit};
|
use solitaire_core::{Card, Rank, Suit};
|
||||||
@@ -205,4 +204,3 @@ pub(super) fn add_android_corner_label(
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Task #34 — Card-flip animation systems
|
// Task #34 — Card-flip animation systems
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,7 @@ use crate::font_plugin::FontResource;
|
|||||||
use crate::layout::{Layout, LayoutResource};
|
use crate::layout::{Layout, LayoutResource};
|
||||||
use crate::resources::GameStateResource;
|
use crate::resources::GameStateResource;
|
||||||
use crate::table_plugin::PileMarker;
|
use crate::table_plugin::PileMarker;
|
||||||
use crate::ui_theme::{
|
use crate::ui_theme::{CARD_SHADOW_ALPHA_DRAG, CARD_SHADOW_PADDING_DRAG, CARD_SHADOW_PADDING_IDLE};
|
||||||
CARD_SHADOW_ALPHA_DRAG, CARD_SHADOW_PADDING_DRAG,
|
|
||||||
CARD_SHADOW_PADDING_IDLE,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Coalesces every `WindowResized` event arriving this frame into the latest
|
/// Coalesces every `WindowResized` event arriving this frame into the latest
|
||||||
/// pending size on [`ResizeThrottle`].
|
/// pending size on [`ResizeThrottle`].
|
||||||
@@ -347,5 +344,3 @@ pub(super) fn fill_tableau_fan_on_startup(
|
|||||||
};
|
};
|
||||||
crate::layout::apply_dynamic_tableau_fan(&game.0, &mut layout.0);
|
crate::layout::apply_dynamic_tableau_fan(&game.0, &mut layout.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use bevy::color::Color;
|
use bevy::color::Color;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use solitaire_core::{KlondikePile, Tableau};
|
|
||||||
use solitaire_core::Card;
|
use solitaire_core::Card;
|
||||||
|
use solitaire_core::{KlondikePile, Tableau};
|
||||||
|
|
||||||
use crate::card_animation::CardAnimation;
|
use crate::card_animation::CardAnimation;
|
||||||
use crate::events::{CardFaceRevealedEvent, CardFlippedEvent};
|
use crate::events::{CardFaceRevealedEvent, CardFlippedEvent};
|
||||||
@@ -577,10 +577,7 @@ impl Plugin for CardPlugin {
|
|||||||
// the chain each pair is a scheduler ambiguity (#143). All
|
// the chain each pair is a scheduler ambiguity (#143). All
|
||||||
// members are cheap and mostly change-gated; sequential
|
// members are cheap and mostly change-gated; sequential
|
||||||
// execution is not a cost that matters here.
|
// execution is not a cost that matters here.
|
||||||
.configure_sets(
|
.configure_sets(Update, LayoutSystem::UpdateOnResize.before(BoardVisuals))
|
||||||
Update,
|
|
||||||
LayoutSystem::UpdateOnResize.before(BoardVisuals),
|
|
||||||
)
|
|
||||||
.add_systems(
|
.add_systems(
|
||||||
Update,
|
Update,
|
||||||
(
|
(
|
||||||
@@ -597,8 +594,7 @@ impl Plugin for CardPlugin {
|
|||||||
clear_right_click_highlights_on_pause,
|
clear_right_click_highlights_on_pause,
|
||||||
tick_hint_highlight,
|
tick_hint_highlight,
|
||||||
update_stock_empty_indicator,
|
update_stock_empty_indicator,
|
||||||
update_stock_count_badge
|
update_stock_count_badge.run_if(resource_changed::<GameStateResource>),
|
||||||
.run_if(resource_changed::<GameStateResource>),
|
|
||||||
collect_resize_events,
|
collect_resize_events,
|
||||||
snap_cards_on_window_resize,
|
snap_cards_on_window_resize,
|
||||||
resize_android_corner_labels,
|
resize_android_corner_labels,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
use bevy::color::Color;
|
use bevy::color::Color;
|
||||||
use solitaire_core::KlondikePile;
|
use solitaire_core::KlondikePile;
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
@@ -12,10 +11,7 @@ use crate::font_plugin::FontResource;
|
|||||||
use crate::layout::{Layout, LayoutResource};
|
use crate::layout::{Layout, LayoutResource};
|
||||||
use crate::resources::GameStateResource;
|
use crate::resources::GameStateResource;
|
||||||
use crate::table_plugin::{PILE_MARKER_DEFAULT_COLOUR, PileMarker};
|
use crate::table_plugin::{PILE_MARKER_DEFAULT_COLOUR, PileMarker};
|
||||||
use crate::ui_theme::{
|
use crate::ui_theme::{STOCK_BADGE_BG, STOCK_BADGE_FG, TEXT_PRIMARY, TYPE_BODY, Z_STOCK_BADGE};
|
||||||
STOCK_BADGE_BG, STOCK_BADGE_FG, TEXT_PRIMARY,
|
|
||||||
TYPE_BODY, Z_STOCK_BADGE,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Sprite colour applied to the stock `PileMarker` when the stock pile is empty,
|
/// Sprite colour applied to the stock `PileMarker` when the stock pile is empty,
|
||||||
/// to signal to the player that there are no more cards to draw. Pure white
|
/// to signal to the player that there are no more cards to draw. Pure white
|
||||||
@@ -288,4 +284,3 @@ pub(super) fn update_stock_count_badge(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ use super::*;
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use bevy::color::Color;
|
use bevy::color::Color;
|
||||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
|
||||||
use solitaire_core::{Card, Rank, Suit};
|
use solitaire_core::{Card, Rank, Suit};
|
||||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||||
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
|
|
||||||
use crate::animation_plugin::{CARD_ANIM_Z_LIFT, CardAnim, EffectiveSlideDuration};
|
use crate::animation_plugin::{CARD_ANIM_Z_LIFT, CardAnim, EffectiveSlideDuration};
|
||||||
use crate::card_animation::CardAnimation;
|
use crate::card_animation::CardAnimation;
|
||||||
@@ -700,4 +700,3 @@ pub(super) fn update_card_entity(
|
|||||||
commands.entity(entity).insert(new_children_key);
|
commands.entity(entity).insert(new_children_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::game_plugin::GamePlugin;
|
|
||||||
use crate::layout::TABLEAU_FAN_FRAC;
|
|
||||||
use crate::table_plugin::TablePlugin;
|
|
||||||
use solitaire_core::Deck;
|
|
||||||
use bevy::window::WindowResized;
|
|
||||||
use solitaire_core::{Card, Rank, Suit};
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use crate::events::StateChangedEvent;
|
use crate::events::StateChangedEvent;
|
||||||
|
use crate::game_plugin::GamePlugin;
|
||||||
use crate::layout::LayoutResource;
|
use crate::layout::LayoutResource;
|
||||||
|
use crate::layout::TABLEAU_FAN_FRAC;
|
||||||
use crate::resources::DragState;
|
use crate::resources::DragState;
|
||||||
|
use crate::table_plugin::TablePlugin;
|
||||||
use crate::ui_theme::TEXT_PRIMARY_HC;
|
use crate::ui_theme::TEXT_PRIMARY_HC;
|
||||||
|
use bevy::window::WindowResized;
|
||||||
|
use solitaire_core::Deck;
|
||||||
|
use solitaire_core::{Card, Rank, Suit};
|
||||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
/// Convenience constructor — all unit tests use Deck1.
|
/// Convenience constructor — all unit tests use Deck1.
|
||||||
fn make_card(suit: Suit, rank: Rank) -> Card {
|
fn make_card(suit: Suit, rank: Rank) -> Card {
|
||||||
@@ -119,8 +118,7 @@ fn waste_draw_one_only_renders_top_card() {
|
|||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
let _ = g.draw();
|
let _ = g.draw();
|
||||||
}
|
}
|
||||||
let waste_ids: HashSet<Card> =
|
let waste_ids: HashSet<Card> = g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
||||||
g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
|
||||||
assert_eq!(waste_ids.len(), 3);
|
assert_eq!(waste_ids.len(), 3);
|
||||||
|
|
||||||
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
@@ -163,8 +161,7 @@ fn waste_draw_three_renders_up_to_three_fanned_cards() {
|
|||||||
"need at least 3 waste cards for this test"
|
"need at least 3 waste cards for this test"
|
||||||
);
|
);
|
||||||
|
|
||||||
let waste_ids: HashSet<Card> =
|
let waste_ids: HashSet<Card> = waste_pile.iter().map(|c| c.0.clone()).collect();
|
||||||
waste_pile.iter().map(|c| c.0.clone()).collect();
|
|
||||||
|
|
||||||
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
let positions = card_positions(&g, &layout);
|
||||||
@@ -216,8 +213,7 @@ fn waste_draw_three_fans_correctly_when_pile_smaller_than_visible() {
|
|||||||
let count = waste_pile.len();
|
let count = waste_pile.len();
|
||||||
assert!(count >= 2, "need at least 2 waste cards");
|
assert!(count >= 2, "need at least 2 waste cards");
|
||||||
|
|
||||||
let waste_ids: HashSet<Card> =
|
let waste_ids: HashSet<Card> = waste_pile.iter().map(|c| c.0.clone()).collect();
|
||||||
waste_pile.iter().map(|c| c.0.clone()).collect();
|
|
||||||
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
let positions = card_positions(&g, &layout);
|
||||||
|
|
||||||
@@ -254,8 +250,7 @@ fn waste_draw_one_buffer_card_at_same_xy_as_top() {
|
|||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
let _ = g.draw();
|
let _ = g.draw();
|
||||||
}
|
}
|
||||||
let waste_ids: HashSet<Card> =
|
let waste_ids: HashSet<Card> = g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
||||||
g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
|
||||||
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
let positions = card_positions(&g, &layout);
|
||||||
let waste_rendered: Vec<_> = positions
|
let waste_rendered: Vec<_> = positions
|
||||||
@@ -745,8 +740,7 @@ fn advance_past_resize_throttle(app: &mut App) {
|
|||||||
|
|
||||||
fn fire_window_resize(app: &mut App, width: f32, height: f32) {
|
fn fire_window_resize(app: &mut App, width: f32, height: f32) {
|
||||||
// Any Entity will do — the snap system reads only width/height.
|
// Any Entity will do — the snap system reads only width/height.
|
||||||
let window = Entity::from_raw_u32(0)
|
let window = Entity::from_raw_u32(0).expect("Entity::from_raw_u32(0) is a valid placeholder");
|
||||||
.expect("Entity::from_raw_u32(0) is a valid placeholder");
|
|
||||||
app.world_mut().write_message(WindowResized {
|
app.world_mut().write_message(WindowResized {
|
||||||
window,
|
window,
|
||||||
width,
|
width,
|
||||||
@@ -928,8 +922,7 @@ fn resize_in_place_updates_card_label_font_size() {
|
|||||||
// Sanity-check: the new font size matches FONT_SIZE_FRAC × the
|
// Sanity-check: the new font size matches FONT_SIZE_FRAC × the
|
||||||
// post-resize card width, so the in-place path is using the
|
// post-resize card width, so the in-place path is using the
|
||||||
// refreshed Layout.
|
// refreshed Layout.
|
||||||
let expected_layout =
|
let expected_layout = crate::layout::compute_layout(Vec2::new(800.0, 600.0), 0.0, 0.0, true);
|
||||||
crate::layout::compute_layout(Vec2::new(800.0, 600.0), 0.0, 0.0, true);
|
|
||||||
let expected = expected_layout.card_size.x * FONT_SIZE_FRAC;
|
let expected = expected_layout.card_size.x * FONT_SIZE_FRAC;
|
||||||
assert!(
|
assert!(
|
||||||
(after - expected).abs() < 1e-3,
|
(after - expected).abs() < 1e-3,
|
||||||
@@ -1046,7 +1039,8 @@ fn shadow_offset_increases_during_drag() {
|
|||||||
q.iter(app.world())
|
q.iter(app.world())
|
||||||
.next()
|
.next()
|
||||||
.expect("fixture should spawn at least one CardEntity")
|
.expect("fixture should spawn at least one CardEntity")
|
||||||
.card.clone()
|
.card
|
||||||
|
.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pick a *different* card to act as the negative control —
|
// Pick a *different* card to act as the negative control —
|
||||||
@@ -1101,9 +1095,7 @@ fn shadow_offset_increases_during_drag() {
|
|||||||
fn shadow_offset_for_card(app: &mut App, card: &Card) -> Vec2 {
|
fn shadow_offset_for_card(app: &mut App, card: &Card) -> Vec2 {
|
||||||
// Map every CardEntity to its (Entity, card).
|
// Map every CardEntity to its (Entity, card).
|
||||||
let card_entity = {
|
let card_entity = {
|
||||||
let mut q = app
|
let mut q = app.world_mut().query::<(Entity, &CardEntity)>();
|
||||||
.world_mut()
|
|
||||||
.query::<(Entity, &CardEntity)>();
|
|
||||||
q.iter(app.world())
|
q.iter(app.world())
|
||||||
.find(|(_, c)| c.card == *card)
|
.find(|(_, c)| c.card == *card)
|
||||||
.map(|(e, _)| e)
|
.map(|(e, _)| e)
|
||||||
@@ -1198,8 +1190,7 @@ fn stock_badge_updates_when_stock_count_changes() {
|
|||||||
assert_eq!(stock_badge_text(&mut app), "24");
|
assert_eq!(stock_badge_text(&mut app), "24");
|
||||||
{
|
{
|
||||||
let mut game = app.world_mut().resource_mut::<GameStateResource>();
|
let mut game = app.world_mut().resource_mut::<GameStateResource>();
|
||||||
let mut stock: Vec<Card> =
|
let mut stock: Vec<Card> = game.0.stock_cards().into_iter().map(|(c, _)| c).collect();
|
||||||
game.0.stock_cards().into_iter().map(|(c, _)| c).collect();
|
|
||||||
let _ = stock.pop();
|
let _ = stock.pop();
|
||||||
game.0.set_test_stock_cards(stock);
|
game.0.set_test_stock_cards(stock);
|
||||||
}
|
}
|
||||||
@@ -1234,8 +1225,7 @@ fn image_set_with_distinct_back_handles() -> CardImageSet {
|
|||||||
// distinct dummy `Image`. We never render these; we only
|
// distinct dummy `Image`. We never render these; we only
|
||||||
// compare ids.
|
// compare ids.
|
||||||
let mut images = Assets::<Image>::default();
|
let mut images = Assets::<Image>::default();
|
||||||
let backs: [Handle<Image>; 5] =
|
let backs: [Handle<Image>; 5] = std::array::from_fn(|_| images.add(Image::default()));
|
||||||
std::array::from_fn(|_| images.add(Image::default()));
|
|
||||||
CardImageSet {
|
CardImageSet {
|
||||||
faces: std::array::from_fn(|_| std::array::from_fn(|_| Handle::default())),
|
faces: std::array::from_fn(|_| std::array::from_fn(|_| Handle::default())),
|
||||||
backs,
|
backs,
|
||||||
@@ -1493,8 +1483,7 @@ fn waste_pile_cards_have_strictly_increasing_z() {
|
|||||||
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
let positions = card_positions(&g, &layout);
|
||||||
|
|
||||||
let waste_ids: HashSet<Card> =
|
let waste_ids: HashSet<Card> = g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
||||||
g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
|
||||||
|
|
||||||
let mut waste_zs: Vec<f32> = positions
|
let mut waste_zs: Vec<f32> = positions
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1543,8 +1532,7 @@ fn waste_cards_do_not_overlap_stock_column_on_portrait() {
|
|||||||
|
|
||||||
let stock_x = layout.pile_positions[&KlondikePile::Stock].x;
|
let stock_x = layout.pile_positions[&KlondikePile::Stock].x;
|
||||||
|
|
||||||
let waste_ids: HashSet<Card> =
|
let waste_ids: HashSet<Card> = g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
||||||
g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
|
||||||
|
|
||||||
let mut waste_positions: Vec<_> = card_positions(&g, &layout)
|
let mut waste_positions: Vec<_> = card_positions(&g, &layout)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -1573,8 +1561,7 @@ fn waste_pile_draw_one_cards_have_distinct_z() {
|
|||||||
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
let positions = card_positions(&g, &layout);
|
||||||
|
|
||||||
let waste_ids: HashSet<Card> =
|
let waste_ids: HashSet<Card> = g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
||||||
g.waste_cards().iter().map(|c| c.0.clone()).collect();
|
|
||||||
|
|
||||||
let mut waste_zs: Vec<f32> = positions
|
let mut waste_zs: Vec<f32> = positions
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon};
|
use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon};
|
||||||
use solitaire_core::Card;
|
use solitaire_core::Card;
|
||||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
|
||||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||||
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
|
|
||||||
use crate::card_plugin::RightClickHighlight;
|
use crate::card_plugin::RightClickHighlight;
|
||||||
use crate::layout::{Layout, LayoutResource};
|
use crate::layout::{Layout, LayoutResource};
|
||||||
@@ -437,7 +437,8 @@ fn tableau_or_stack_pos(
|
|||||||
base.x,
|
base.x,
|
||||||
base.y - layout.card_size.y * layout.tableau_fan_frac * (index as f32),
|
base.y - layout.card_size.y * layout.tableau_fan_frac * (index as f32),
|
||||||
)
|
)
|
||||||
} else if matches!(pile, KlondikePile::Stock) && game.draw_mode() == DrawStockConfig::DrawThree {
|
} else if matches!(pile, KlondikePile::Stock) && game.draw_mode() == DrawStockConfig::DrawThree
|
||||||
|
{
|
||||||
let pile_len = game.waste_cards().len();
|
let pile_len = game.waste_cards().len();
|
||||||
let visible_start = pile_len.saturating_sub(3);
|
let visible_start = pile_len.saturating_sub(3);
|
||||||
let slot = index.saturating_sub(visible_start) as f32;
|
let slot = index.saturating_sub(visible_start) as f32;
|
||||||
@@ -581,7 +582,10 @@ mod tests {
|
|||||||
|
|
||||||
use crate::layout::compute_layout;
|
use crate::layout::compute_layout;
|
||||||
use solitaire_core::{Card, Deck, Rank, Suit};
|
use solitaire_core::{Card, Deck, Rank, Suit};
|
||||||
use solitaire_core::{DrawStockConfig, game_state::{GameMode, GameState}};
|
use solitaire_core::{
|
||||||
|
DrawStockConfig,
|
||||||
|
game_state::{GameMode, GameState},
|
||||||
|
};
|
||||||
|
|
||||||
/// Builds an `App` with `MinimalPlugins` and the overlay system
|
/// Builds an `App` with `MinimalPlugins` and the overlay system
|
||||||
/// registered, plus the resources the system needs. Callers
|
/// registered, plus the resources the system needs. Callers
|
||||||
@@ -630,11 +634,7 @@ mod tests {
|
|||||||
// — same colour family, illegal. Tableau(2) must NOT be
|
// — same colour family, illegal. Tableau(2) must NOT be
|
||||||
// highlighted.
|
// highlighted.
|
||||||
let mut game = GameState::new_with_mode(7, DrawStockConfig::DrawOne, GameMode::Classic);
|
let mut game = GameState::new_with_mode(7, DrawStockConfig::DrawOne, GameMode::Classic);
|
||||||
set_tableau_top(
|
set_tableau_top(&mut game, 2, Card::new(Deck::Deck1, Suit::Clubs, Rank::Six));
|
||||||
&mut game,
|
|
||||||
2,
|
|
||||||
Card::new(Deck::Deck1, Suit::Clubs, Rank::Six),
|
|
||||||
);
|
|
||||||
let dragged = Card::new(Deck::Deck1, Suit::Spades, Rank::Five);
|
let dragged = Card::new(Deck::Deck1, Suit::Spades, Rank::Five);
|
||||||
|
|
||||||
let mut app = overlay_test_app(game);
|
let mut app = overlay_test_app(game);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
use bevy::prelude::Message;
|
use bevy::prelude::Message;
|
||||||
use solitaire_core::KlondikePile;
|
use solitaire_core::KlondikePile;
|
||||||
use solitaire_core::{Card, Suit};
|
|
||||||
use solitaire_core::game_state::GameMode;
|
use solitaire_core::game_state::GameMode;
|
||||||
|
use solitaire_core::{Card, Suit};
|
||||||
use solitaire_data::AchievementRecord;
|
use solitaire_data::AchievementRecord;
|
||||||
use solitaire_sync::SyncResponse;
|
use solitaire_sync::SyncResponse;
|
||||||
|
|
||||||
|
|||||||
@@ -852,7 +852,10 @@ mod tests {
|
|||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
app.add_plugins(MinimalPlugins)
|
app.add_plugins(MinimalPlugins)
|
||||||
.add_plugins(FeedbackAnimPlugin);
|
.add_plugins(FeedbackAnimPlugin);
|
||||||
app.insert_resource(GameStateResource(GameState::new(1, DrawStockConfig::DrawOne)));
|
app.insert_resource(GameStateResource(GameState::new(
|
||||||
|
1,
|
||||||
|
DrawStockConfig::DrawOne,
|
||||||
|
)));
|
||||||
app.insert_resource(SettingsResource(Settings {
|
app.insert_resource(SettingsResource(Settings {
|
||||||
reduce_motion_mode: true,
|
reduce_motion_mode: true,
|
||||||
..Settings::default()
|
..Settings::default()
|
||||||
@@ -906,7 +909,10 @@ mod tests {
|
|||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
app.add_plugins(MinimalPlugins)
|
app.add_plugins(MinimalPlugins)
|
||||||
.add_plugins(FeedbackAnimPlugin);
|
.add_plugins(FeedbackAnimPlugin);
|
||||||
app.insert_resource(GameStateResource(GameState::new(1, DrawStockConfig::DrawOne)));
|
app.insert_resource(GameStateResource(GameState::new(
|
||||||
|
1,
|
||||||
|
DrawStockConfig::DrawOne,
|
||||||
|
)));
|
||||||
app.insert_resource(SettingsResource(Settings {
|
app.insert_resource(SettingsResource(Settings {
|
||||||
reduce_motion_mode: true,
|
reduce_motion_mode: true,
|
||||||
..Settings::default()
|
..Settings::default()
|
||||||
|
|||||||
@@ -14,8 +14,13 @@ use bevy::prelude::*;
|
|||||||
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||||
use bevy::window::AppLifecycle;
|
use bevy::window::AppLifecycle;
|
||||||
use solitaire_core::KlondikePile;
|
use solitaire_core::KlondikePile;
|
||||||
use solitaire_core::{DrawStockConfig, game_state::{GameMode, GameState}};
|
use solitaire_core::{
|
||||||
use solitaire_core::{DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, KlondikeInstruction};
|
DEFAULT_SOLVE_MOVES_BUDGET, DEFAULT_SOLVE_STATES_BUDGET, KlondikeInstruction,
|
||||||
|
};
|
||||||
|
use solitaire_core::{
|
||||||
|
DrawStockConfig,
|
||||||
|
game_state::{GameMode, GameState},
|
||||||
|
};
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
use solitaire_data::latest_replay_path;
|
use solitaire_data::latest_replay_path;
|
||||||
use solitaire_data::{
|
use solitaire_data::{
|
||||||
@@ -188,7 +193,9 @@ impl Plugin for GamePlugin {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
saved.unwrap_or_else(|| GameState::new(seed_from_system_time(), DrawStockConfig::DrawOne)),
|
saved.unwrap_or_else(|| {
|
||||||
|
GameState::new(seed_from_system_time(), DrawStockConfig::DrawOne)
|
||||||
|
}),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -1324,7 +1331,10 @@ fn auto_save_game_state(
|
|||||||
// or there's a pending restore the player hasn't answered — saving
|
// or there's a pending restore the player hasn't answered — saving
|
||||||
// the fresh-deal placeholder we seeded GameStateResource with at
|
// the fresh-deal placeholder we seeded GameStateResource with at
|
||||||
// startup would clobber the real saved game on disk.
|
// startup would clobber the real saved game on disk.
|
||||||
if paused.is_some_and(|p| p.0) || game.0.is_won() || game.0.move_count() == 0 || pending.0.is_some()
|
if paused.is_some_and(|p| p.0)
|
||||||
|
|| game.0.is_won()
|
||||||
|
|| game.0.move_count() == 0
|
||||||
|
|| pending.0.is_some()
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,9 +372,7 @@ fn moving_cards_off_face_up_card_does_not_fire_card_flipped_event() {
|
|||||||
});
|
});
|
||||||
app.update();
|
app.update();
|
||||||
|
|
||||||
let events = app
|
let events = app.world().resource::<Messages<CardFlippedEvent>>();
|
||||||
.world()
|
|
||||||
.resource::<Messages<CardFlippedEvent>>();
|
|
||||||
let mut cursor = events.get_cursor();
|
let mut cursor = events.get_cursor();
|
||||||
let fired: Vec<_> = cursor.read(events).collect();
|
let fired: Vec<_> = cursor.read(events).collect();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -800,7 +798,10 @@ fn replay_recording_skips_undo() {
|
|||||||
1,
|
1,
|
||||||
"only the draw is recorded; the undo does not erase it nor add a new entry",
|
"only the draw is recorded; the undo does not erase it nor add a new entry",
|
||||||
);
|
);
|
||||||
assert!(matches!(recording.moves[0], KlondikeInstruction::RotateStock));
|
assert!(matches!(
|
||||||
|
recording.moves[0],
|
||||||
|
KlondikeInstruction::RotateStock
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starting a new game wipes the recording so the next deal begins
|
/// Starting a new game wipes the recording so the next deal begins
|
||||||
@@ -872,8 +873,8 @@ fn replay_recording_freezes_into_replay_on_game_won() {
|
|||||||
});
|
});
|
||||||
app.update();
|
app.update();
|
||||||
|
|
||||||
let history = load_replay_history_from(&path)
|
let history =
|
||||||
.expect("a winning replay must be persisted to ReplayPath");
|
load_replay_history_from(&path).expect("a winning replay must be persisted to ReplayPath");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
history.replays.len(),
|
history.replays.len(),
|
||||||
1,
|
1,
|
||||||
@@ -1059,7 +1060,10 @@ fn new_game_with_solver_toggle_off_random_seed_path() {
|
|||||||
app.update();
|
app.update();
|
||||||
|
|
||||||
// Game state was reseeded — move_count is 0 on the new game.
|
// Game state was reseeded — move_count is 0 on the new game.
|
||||||
assert_eq!(app.world().resource::<GameStateResource>().0.move_count(), 0);
|
assert_eq!(
|
||||||
|
app.world().resource::<GameStateResource>().0.move_count(),
|
||||||
|
0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1133,7 +1137,10 @@ fn new_game_with_solver_toggle_on_retries_until_winnable() {
|
|||||||
// The chosen seed is non-deterministic (system time),
|
// The chosen seed is non-deterministic (system time),
|
||||||
// but the new game must have been started cleanly:
|
// but the new game must have been started cleanly:
|
||||||
// move_count back to 0, undo stack empty.
|
// move_count back to 0, undo stack empty.
|
||||||
assert_eq!(app.world().resource::<GameStateResource>().0.move_count(), 0);
|
assert_eq!(
|
||||||
|
app.world().resource::<GameStateResource>().0.move_count(),
|
||||||
|
0
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.world()
|
app.world()
|
||||||
.resource::<GameStateResource>()
|
.resource::<GameStateResource>()
|
||||||
|
|||||||
@@ -432,7 +432,9 @@ fn build_home_context<'a>(
|
|||||||
zen_best: stats.map_or(0, |s| s.0.zen_best_score),
|
zen_best: stats.map_or(0, |s| s.0.zen_best_score),
|
||||||
challenge_best: stats.map_or(0, |s| s.0.challenge_best_score),
|
challenge_best: stats.map_or(0, |s| s.0.challenge_best_score),
|
||||||
daily_today,
|
daily_today,
|
||||||
draw_mode: settings.map(|s| s.0.draw_mode).unwrap_or(DrawStockConfig::DrawOne),
|
draw_mode: settings
|
||||||
|
.map(|s| s.0.draw_mode)
|
||||||
|
.unwrap_or(DrawStockConfig::DrawOne),
|
||||||
font_res,
|
font_res,
|
||||||
difficulty_expanded,
|
difficulty_expanded,
|
||||||
last_difficulty: settings.and_then(|s| s.0.last_difficulty),
|
last_difficulty: settings.and_then(|s| s.0.last_difficulty),
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Auto-fade state for the action button bar. The bar fades out when
|
/// Auto-fade state for the action button bar. The bar fades out when
|
||||||
/// the cursor is in the play area (below the HUD band) and back in when
|
/// the cursor is in the play area (below the HUD band) and back in when
|
||||||
/// the cursor approaches the top of the window — same UX as a video
|
/// the cursor approaches the top of the window — same UX as a video
|
||||||
@@ -49,7 +47,11 @@ const ACTION_FADE_RATE_PER_SEC: f32 = 6.0;
|
|||||||
/// `target` at a fixed rate so the visual transition is smooth across
|
/// `target` at a fixed rate so the visual transition is smooth across
|
||||||
/// variable framerates.
|
/// variable framerates.
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
pub(super) fn update_action_fade(windows: Query<&Window>, time: Res<Time>, mut fade: ResMut<HudActionFade>) {
|
pub(super) fn update_action_fade(
|
||||||
|
windows: Query<&Window>,
|
||||||
|
time: Res<Time>,
|
||||||
|
mut fade: ResMut<HudActionFade>,
|
||||||
|
) {
|
||||||
let Ok(window) = windows.single() else {
|
let Ok(window) = windows.single() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -427,4 +429,3 @@ pub(super) fn lerp_text_color(from: Color, to: Color, t: f32) -> Color {
|
|||||||
from.alpha + (to.alpha - from.alpha) * t,
|
from.alpha + (to.alpha - from.alpha) * t,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// `Changed<Interaction>` filter ensures we only react on the frame the
|
/// `Changed<Interaction>` filter ensures we only react on the frame the
|
||||||
/// interaction state transitions, avoiding repeat events while the button
|
/// interaction state transitions, avoiding repeat events while the button
|
||||||
/// is held down. Each click handler fires the corresponding request event,
|
/// is held down. Each click handler fires the corresponding request event,
|
||||||
@@ -613,4 +611,3 @@ pub(super) fn toggle_hud_on_tap(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ use interaction::*;
|
|||||||
use spawn::*;
|
use spawn::*;
|
||||||
use updates::*;
|
use updates::*;
|
||||||
|
|
||||||
|
|
||||||
// On wasm32 AvatarPlugin is gated out; define a placeholder type so the
|
// On wasm32 AvatarPlugin is gated out; define a placeholder type so the
|
||||||
// Option<Res<AvatarResource>> parameters below compile without changes.
|
// Option<Res<AvatarResource>> parameters below compile without changes.
|
||||||
// The resource is never inserted on wasm, so every call resolves to None.
|
// The resource is never inserted on wasm, so every call resolves to None.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use crate::avatar_plugin::AvatarResource;
|
use crate::avatar_plugin::AvatarResource;
|
||||||
|
|
||||||
@@ -292,7 +291,9 @@ pub(super) fn spawn_avatar_child(
|
|||||||
const SIZE: f32 = 32.0;
|
const SIZE: f32 = 32.0;
|
||||||
if let Some(handle) = avatar.and_then(|a| a.0.clone()) {
|
if let Some(handle) = avatar.and_then(|a| a.0.clone()) {
|
||||||
// Logged-in with a downloaded avatar: keep the accent disc behind it.
|
// Logged-in with a downloaded avatar: keep the accent disc behind it.
|
||||||
commands.entity(parent).insert(BackgroundColor(ACCENT_PRIMARY));
|
commands
|
||||||
|
.entity(parent)
|
||||||
|
.insert(BackgroundColor(ACCENT_PRIMARY));
|
||||||
// Image fills the circle container; border_radius clips it to a disc.
|
// Image fills the circle container; border_radius clips it to a disc.
|
||||||
commands.entity(parent).with_children(|b| {
|
commands.entity(parent).with_children(|b| {
|
||||||
b.spawn((
|
b.spawn((
|
||||||
@@ -549,4 +550,3 @@ pub(super) fn spawn_action_button<M: Component>(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,16 @@ fn read_hud_text<M: Component>(app: &mut App) -> String {
|
|||||||
#[test]
|
#[test]
|
||||||
fn score_reflects_game_state() {
|
fn score_reflects_game_state() {
|
||||||
let mut app = headless_app();
|
let mut app = headless_app();
|
||||||
let score = app.world_mut().resource_mut::<GameStateResource>().0.force_test_score(20);
|
let score = app
|
||||||
|
.world_mut()
|
||||||
|
.resource_mut::<GameStateResource>()
|
||||||
|
.0
|
||||||
|
.force_test_score(20);
|
||||||
app.update();
|
app.update();
|
||||||
assert_eq!(read_hud_text::<HudScore>(&mut app), format!("Score: {score}"));
|
assert_eq!(
|
||||||
|
read_hud_text::<HudScore>(&mut app),
|
||||||
|
format!("Score: {score}")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -192,7 +199,9 @@ fn challenge_time_color_zero_is_danger() {
|
|||||||
fn challenge_hud_empty_when_no_daily_resource() {
|
fn challenge_hud_empty_when_no_daily_resource() {
|
||||||
// No DailyChallengeResource inserted → HudChallenge must be empty.
|
// No DailyChallengeResource inserted → HudChallenge must be empty.
|
||||||
let mut app = headless_app();
|
let mut app = headless_app();
|
||||||
app.world_mut().resource_mut::<GameStateResource>().set_changed();
|
app.world_mut()
|
||||||
|
.resource_mut::<GameStateResource>()
|
||||||
|
.set_changed();
|
||||||
app.update();
|
app.update();
|
||||||
assert_eq!(read_hud_text::<HudChallenge>(&mut app), "");
|
assert_eq!(read_hud_text::<HudChallenge>(&mut app), "");
|
||||||
}
|
}
|
||||||
@@ -207,7 +216,9 @@ fn challenge_hud_shows_time_limit_when_resource_present() {
|
|||||||
target_score: None,
|
target_score: None,
|
||||||
max_time_secs: Some(300),
|
max_time_secs: Some(300),
|
||||||
});
|
});
|
||||||
app.world_mut().resource_mut::<GameStateResource>().set_changed();
|
app.world_mut()
|
||||||
|
.resource_mut::<GameStateResource>()
|
||||||
|
.set_changed();
|
||||||
app.update();
|
app.update();
|
||||||
assert_eq!(read_hud_text::<HudChallenge>(&mut app), "Limit: 5:00");
|
assert_eq!(read_hud_text::<HudChallenge>(&mut app), "Limit: 5:00");
|
||||||
}
|
}
|
||||||
@@ -222,7 +233,9 @@ fn challenge_hud_shows_score_goal_when_resource_present() {
|
|||||||
target_score: Some(4000),
|
target_score: Some(4000),
|
||||||
max_time_secs: None,
|
max_time_secs: None,
|
||||||
});
|
});
|
||||||
app.world_mut().resource_mut::<GameStateResource>().set_changed();
|
app.world_mut()
|
||||||
|
.resource_mut::<GameStateResource>()
|
||||||
|
.set_changed();
|
||||||
app.update();
|
app.update();
|
||||||
assert_eq!(read_hud_text::<HudChallenge>(&mut app), "Goal: 4000 pts");
|
assert_eq!(read_hud_text::<HudChallenge>(&mut app), "Goal: 4000 pts");
|
||||||
}
|
}
|
||||||
@@ -238,7 +251,10 @@ fn challenge_hud_clears_on_win() {
|
|||||||
max_time_secs: Some(300),
|
max_time_secs: Some(300),
|
||||||
});
|
});
|
||||||
// Mark the game as won — HudChallenge should be empty.
|
// Mark the game as won — HudChallenge should be empty.
|
||||||
app.world_mut().resource_mut::<GameStateResource>().0.set_test_won(true);
|
app.world_mut()
|
||||||
|
.resource_mut::<GameStateResource>()
|
||||||
|
.0
|
||||||
|
.set_test_won(true);
|
||||||
app.update();
|
app.update();
|
||||||
assert_eq!(read_hud_text::<HudChallenge>(&mut app), "");
|
assert_eq!(read_hud_text::<HudChallenge>(&mut app), "");
|
||||||
}
|
}
|
||||||
@@ -384,7 +400,10 @@ fn score_increase_above_threshold_spawns_floater_in_accent_primary() {
|
|||||||
set_manual_time_step(&mut app, 0.0);
|
set_manual_time_step(&mut app, 0.0);
|
||||||
// Initial state has score=0; bumping by 50 (the threshold)
|
// Initial state has score=0; bumping by 50 (the threshold)
|
||||||
// is the smallest jump that triggers the floater.
|
// is the smallest jump that triggers the floater.
|
||||||
app.world_mut().resource_mut::<GameStateResource>().0.force_test_score(50);
|
app.world_mut()
|
||||||
|
.resource_mut::<GameStateResource>()
|
||||||
|
.0
|
||||||
|
.force_test_score(50);
|
||||||
app.update();
|
app.update();
|
||||||
|
|
||||||
// One floater should now exist.
|
// One floater should now exist.
|
||||||
@@ -405,7 +424,10 @@ fn score_increase_above_threshold_spawns_floater_in_accent_primary() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn score_floater_despawns_after_full_lifetime() {
|
fn score_floater_despawns_after_full_lifetime() {
|
||||||
let mut app = headless_app();
|
let mut app = headless_app();
|
||||||
app.world_mut().resource_mut::<GameStateResource>().0.force_test_score(50);
|
app.world_mut()
|
||||||
|
.resource_mut::<GameStateResource>()
|
||||||
|
.0
|
||||||
|
.force_test_score(50);
|
||||||
app.update();
|
app.update();
|
||||||
assert_eq!(count_with::<ScoreFloater>(&mut app), 1);
|
assert_eq!(count_with::<ScoreFloater>(&mut app), 1);
|
||||||
|
|
||||||
@@ -431,7 +453,10 @@ fn score_increase_below_threshold_does_not_spawn_floater() {
|
|||||||
let mut app = headless_app();
|
let mut app = headless_app();
|
||||||
// +5 mirrors a single tableau-to-foundation move; well below
|
// +5 mirrors a single tableau-to-foundation move; well below
|
||||||
// the 50-point threshold so the floater path stays dormant.
|
// the 50-point threshold so the floater path stays dormant.
|
||||||
app.world_mut().resource_mut::<GameStateResource>().0.force_test_score(5);
|
app.world_mut()
|
||||||
|
.resource_mut::<GameStateResource>()
|
||||||
|
.0
|
||||||
|
.force_test_score(5);
|
||||||
app.update();
|
app.update();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
count_with::<ScoreFloater>(&mut app),
|
count_with::<ScoreFloater>(&mut app),
|
||||||
@@ -507,7 +532,10 @@ fn score_change_skips_pulse_and_floater_under_reduce_motion() {
|
|||||||
..Settings::default()
|
..Settings::default()
|
||||||
}));
|
}));
|
||||||
// +100 would normally create both a ScorePulse and a ScoreFloater.
|
// +100 would normally create both a ScorePulse and a ScoreFloater.
|
||||||
app.world_mut().resource_mut::<GameStateResource>().0.force_test_score(50);
|
app.world_mut()
|
||||||
|
.resource_mut::<GameStateResource>()
|
||||||
|
.0
|
||||||
|
.force_test_score(50);
|
||||||
app.update();
|
app.update();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
count_with::<ScorePulse>(&mut app),
|
count_with::<ScorePulse>(&mut app),
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use bevy::window::WindowResized;
|
use bevy::window::WindowResized;
|
||||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
|
||||||
use solitaire_core::Suit;
|
use solitaire_core::Suit;
|
||||||
use solitaire_core::{DrawStockConfig, game_state::GameMode};
|
use solitaire_core::{DrawStockConfig, game_state::GameMode};
|
||||||
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
|
|
||||||
use crate::auto_complete_plugin::AutoCompleteState;
|
use crate::auto_complete_plugin::AutoCompleteState;
|
||||||
|
|
||||||
@@ -609,4 +609,3 @@ pub(super) fn resize_action_bar_labels(
|
|||||||
font.font_size = new_size;
|
font.font_size = new_size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ use bevy::prelude::*;
|
|||||||
use bevy::window::PrimaryWindow;
|
use bevy::window::PrimaryWindow;
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
use bevy::window::{MonitorSelection, WindowMode};
|
use bevy::window::{MonitorSelection, WindowMode};
|
||||||
use solitaire_core::{Foundation, KlondikeInstruction, KlondikePile, Tableau};
|
|
||||||
use solitaire_core::{FOUNDATIONS, TABLEAUS};
|
|
||||||
use solitaire_core::{Card, Suit};
|
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
|
use solitaire_core::{Card, Suit};
|
||||||
|
use solitaire_core::{FOUNDATIONS, TABLEAUS};
|
||||||
|
use solitaire_core::{Foundation, KlondikeInstruction, KlondikePile, Tableau};
|
||||||
|
|
||||||
use crate::auto_complete_plugin::AutoCompleteState;
|
use crate::auto_complete_plugin::AutoCompleteState;
|
||||||
use crate::card_animation::tuning::AnimationTuning;
|
use crate::card_animation::tuning::AnimationTuning;
|
||||||
@@ -394,7 +394,10 @@ pub fn emit_hint_visuals(
|
|||||||
|
|
||||||
// Find the top face-up card in the source pile and highlight it.
|
// Find the top face-up card in the source pile and highlight it.
|
||||||
let source_cards = pile_cards(game, from);
|
let source_cards = pile_cards(game, from);
|
||||||
let top_card = source_cards.last().filter(|(_, face_up)| *face_up).map(|(c, _)| c.clone());
|
let top_card = source_cards
|
||||||
|
.last()
|
||||||
|
.filter(|(_, face_up)| *face_up)
|
||||||
|
.map(|(c, _)| c.clone());
|
||||||
if let Some(card) = top_card {
|
if let Some(card) = top_card {
|
||||||
for (entity, card_entity, mut sprite) in card_entities.iter_mut() {
|
for (entity, card_entity, mut sprite) in card_entities.iter_mut() {
|
||||||
if card_entity.card == card {
|
if card_entity.card == card {
|
||||||
@@ -831,9 +834,7 @@ fn end_drag(
|
|||||||
let origin_cards = pile_cards(&game.0, &origin);
|
let origin_cards = pile_cards(&game.0, &origin);
|
||||||
if !origin_cards.is_empty() {
|
if !origin_cards.is_empty() {
|
||||||
for card in &drag.cards {
|
for card in &drag.cards {
|
||||||
let Some(stack_index) =
|
let Some(stack_index) = origin_cards.iter().position(|(c, _)| c == card) else {
|
||||||
origin_cards.iter().position(|(c, _)| c == card)
|
|
||||||
else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let target_pos = card_position(&game.0, &layout.0, &origin, stack_index);
|
let target_pos = card_position(&game.0, &layout.0, &origin, stack_index);
|
||||||
@@ -1070,8 +1071,7 @@ fn touch_end_drag(
|
|||||||
let origin_cards = pile_cards(&game.0, &origin);
|
let origin_cards = pile_cards(&game.0, &origin);
|
||||||
if !origin_cards.is_empty() {
|
if !origin_cards.is_empty() {
|
||||||
for card in &drag.cards {
|
for card in &drag.cards {
|
||||||
let Some(stack_index) =
|
let Some(stack_index) = origin_cards.iter().position(|(c, _)| c == card)
|
||||||
origin_cards.iter().position(|(c, _)| c == card)
|
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -1174,7 +1174,8 @@ fn card_position(
|
|||||||
y_offset -= layout.card_size.y * step;
|
y_offset -= layout.card_size.y * step;
|
||||||
}
|
}
|
||||||
Vec2::new(base.x, base.y + y_offset)
|
Vec2::new(base.x, base.y + y_offset)
|
||||||
} else if matches!(pile, KlondikePile::Stock) && game.draw_mode() == DrawStockConfig::DrawThree {
|
} else if matches!(pile, KlondikePile::Stock) && game.draw_mode() == DrawStockConfig::DrawThree
|
||||||
|
{
|
||||||
// In Draw-Three mode the top 3 waste cards are fanned in X to match
|
// In Draw-Three mode the top 3 waste cards are fanned in X to match
|
||||||
// card_plugin::card_positions(). Hit-testing uses the same `waste_fan_step`
|
// card_plugin::card_positions(). Hit-testing uses the same `waste_fan_step`
|
||||||
// so clicking the visually rightmost (top) card actually registers — a
|
// so clicking the visually rightmost (top) card actually registers — a
|
||||||
@@ -1248,7 +1249,10 @@ fn find_draggable_at(
|
|||||||
}
|
}
|
||||||
(i, i + 1)
|
(i, i + 1)
|
||||||
};
|
};
|
||||||
let cards: Vec<Card> = pile_cards[start..end].iter().map(|(c, _)| c.clone()).collect();
|
let cards: Vec<Card> = pile_cards[start..end]
|
||||||
|
.iter()
|
||||||
|
.map(|(c, _)| c.clone())
|
||||||
|
.collect();
|
||||||
return Some((pile, start, cards));
|
return Some((pile, start, cards));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1550,8 +1554,7 @@ fn handle_double_tap(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some((found_card, found_face_up)) =
|
let Some((found_card, found_face_up)) = pile_cards.iter().find(|(c, _)| c == top_card)
|
||||||
pile_cards.iter().find(|(c, _)| c == top_card)
|
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1783,8 +1786,6 @@ fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<(Card, bool)> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const fn tableau_number(tableau: Tableau) -> u8 {
|
const fn tableau_number(tableau: Tableau) -> u8 {
|
||||||
match tableau {
|
match tableau {
|
||||||
Tableau::Tableau1 => 1,
|
Tableau::Tableau1 => 1,
|
||||||
|
|||||||
@@ -89,9 +89,11 @@ fn find_draggable_picks_waste_top_with_multiple_cards() {
|
|||||||
let mut game = GameState::new(42, DrawStockConfig::DrawOne);
|
let mut game = GameState::new(42, DrawStockConfig::DrawOne);
|
||||||
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
clear_test_piles(&mut game);
|
clear_test_piles(&mut game);
|
||||||
let waste = vec![Card::new(Deck::Deck1, Suit::Clubs, Rank::Two),
|
let waste = vec![
|
||||||
Card::new(Deck::Deck1, Suit::Hearts, Rank::Five),
|
Card::new(Deck::Deck1, Suit::Clubs, Rank::Two),
|
||||||
Card::new(Deck::Deck1, Suit::Spades, Rank::Nine)];
|
Card::new(Deck::Deck1, Suit::Hearts, Rank::Five),
|
||||||
|
Card::new(Deck::Deck1, Suit::Spades, Rank::Nine),
|
||||||
|
];
|
||||||
game.set_test_waste_cards(waste.clone());
|
game.set_test_waste_cards(waste.clone());
|
||||||
|
|
||||||
let top_index = waste.len() - 1; // 2 = the visible top
|
let top_index = waste.len() - 1; // 2 = the visible top
|
||||||
@@ -99,7 +101,11 @@ fn find_draggable_picks_waste_top_with_multiple_cards() {
|
|||||||
let result = find_draggable_at(top_pos, &game, &layout).expect("waste top is draggable");
|
let result = find_draggable_at(top_pos, &game, &layout).expect("waste top is draggable");
|
||||||
assert_eq!(result.0, KlondikePile::Stock, "origin is the waste pile");
|
assert_eq!(result.0, KlondikePile::Stock, "origin is the waste pile");
|
||||||
assert_eq!(result.1, top_index, "picks the top index, not the buffer");
|
assert_eq!(result.1, top_index, "picks the top index, not the buffer");
|
||||||
assert_eq!(result.2, vec![waste[top_index].clone()], "drags the top card only");
|
assert_eq!(
|
||||||
|
result.2,
|
||||||
|
vec![waste[top_index].clone()],
|
||||||
|
"drags the top card only"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -176,8 +182,7 @@ fn find_draggable_skips_face_down_cards() {
|
|||||||
// face-up card, but the iterator should skip face-down cards and
|
// face-up card, but the iterator should skip face-down cards and
|
||||||
// the cursor sits above the face-up card's AABB, so the result
|
// the cursor sits above the face-up card's AABB, so the result
|
||||||
// is None.
|
// is None.
|
||||||
let face_down_pos =
|
let face_down_pos = card_position(&game, &layout, &KlondikePile::Tableau(Tableau::Tableau7), 0);
|
||||||
card_position(&game, &layout, &KlondikePile::Tableau(Tableau::Tableau7), 0);
|
|
||||||
let result = find_draggable_at(face_down_pos, &game, &layout);
|
let result = find_draggable_at(face_down_pos, &game, &layout);
|
||||||
assert!(result.is_none(), "face-down cards should not be draggable");
|
assert!(result.is_none(), "face-down cards should not be draggable");
|
||||||
}
|
}
|
||||||
@@ -195,8 +200,7 @@ fn find_draggable_hits_face_up_card_with_face_down_cards_above_it() {
|
|||||||
// Tableau 6 starts with 6 face-down + 1 face-up. The face-up card
|
// Tableau 6 starts with 6 face-down + 1 face-up. The face-up card
|
||||||
// sits at base.y - 6 * TABLEAU_FACEDOWN_FAN_FRAC * card_h, NOT at
|
// sits at base.y - 6 * TABLEAU_FACEDOWN_FAN_FRAC * card_h, NOT at
|
||||||
// base.y - 6 * TABLEAU_FAN_FRAC * card_h. Click the centre.
|
// base.y - 6 * TABLEAU_FAN_FRAC * card_h. Click the centre.
|
||||||
let face_up_pos =
|
let face_up_pos = card_position(&game, &layout, &KlondikePile::Tableau(Tableau::Tableau7), 6);
|
||||||
card_position(&game, &layout, &KlondikePile::Tableau(Tableau::Tableau7), 6);
|
|
||||||
let result = find_draggable_at(face_up_pos, &game, &layout)
|
let result = find_draggable_at(face_up_pos, &game, &layout)
|
||||||
.expect("clicking the face-up card's visible centre must initiate a drag");
|
.expect("clicking the face-up card's visible centre must initiate a drag");
|
||||||
assert_eq!(result.0, KlondikePile::Tableau(Tableau::Tableau7));
|
assert_eq!(result.0, KlondikePile::Tableau(Tableau::Tableau7));
|
||||||
@@ -213,18 +217,14 @@ fn find_draggable_returns_run_when_picking_mid_stack() {
|
|||||||
let king = Card::new(D::Deck1, Suit::Spades, Rank::King);
|
let king = Card::new(D::Deck1, Suit::Spades, Rank::King);
|
||||||
let queen = Card::new(D::Deck1, Suit::Hearts, Rank::Queen);
|
let queen = Card::new(D::Deck1, Suit::Hearts, Rank::Queen);
|
||||||
let jack = Card::new(D::Deck1, Suit::Clubs, Rank::Jack);
|
let jack = Card::new(D::Deck1, Suit::Clubs, Rank::Jack);
|
||||||
game.set_test_tableau_cards(
|
game.set_test_tableau_cards(Tableau::Tableau1, vec![king, queen.clone(), jack.clone()]);
|
||||||
Tableau::Tableau1,
|
|
||||||
vec![king, queen.clone(), jack.clone()],
|
|
||||||
);
|
|
||||||
|
|
||||||
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
let layout = compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
// The Queen's geometric center (index 1) is inside the Jack's bounding box
|
// The Queen's geometric center (index 1) is inside the Jack's bounding box
|
||||||
// (Jack fans 0.5h below base; its box spans [base-h, base]). To hit the
|
// (Jack fans 0.5h below base; its box spans [base-h, base]). To hit the
|
||||||
// Queen we click in her visible strip: the 0.25h band above the Jack's top
|
// Queen we click in her visible strip: the 0.25h band above the Jack's top
|
||||||
// edge (base.y to base.y+0.25h). Midpoint = queen_center + 0.375*card_h.
|
// edge (base.y to base.y+0.25h). Midpoint = queen_center + 0.375*card_h.
|
||||||
let queen_center =
|
let queen_center = card_position(&game, &layout, &KlondikePile::Tableau(Tableau::Tableau1), 1);
|
||||||
card_position(&game, &layout, &KlondikePile::Tableau(Tableau::Tableau1), 1);
|
|
||||||
let pos = queen_center + Vec2::new(0.0, layout.card_size.y * 0.375);
|
let pos = queen_center + Vec2::new(0.0, layout.card_size.y * 0.375);
|
||||||
let (pile, start, ids) = find_draggable_at(pos, &game, &layout).expect("hit");
|
let (pile, start, ids) = find_draggable_at(pos, &game, &layout).expect("hit");
|
||||||
assert_eq!(pile, KlondikePile::Tableau(Tableau::Tableau1));
|
assert_eq!(pile, KlondikePile::Tableau(Tableau::Tableau1));
|
||||||
@@ -322,7 +322,11 @@ fn find_draggable_draw_three_waste_top_card_hit_at_fanned_position() {
|
|||||||
);
|
);
|
||||||
let (pile, _start, ids) = result.unwrap();
|
let (pile, _start, ids) = result.unwrap();
|
||||||
assert_eq!(pile, KlondikePile::Stock);
|
assert_eq!(pile, KlondikePile::Stock);
|
||||||
assert_eq!(ids, vec![four_clubs], "only the top card is draggable from waste");
|
assert_eq!(
|
||||||
|
ids,
|
||||||
|
vec![four_clubs],
|
||||||
|
"only the top card is draggable from waste"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -558,8 +562,7 @@ fn rejected_drag_inserts_card_animation_on_each_dragged_card() {
|
|||||||
fn rejected_drag_animation_targets_origin_resting_position() {
|
fn rejected_drag_animation_targets_origin_resting_position() {
|
||||||
let drag_pos = Vec2::new(640.0, 200.0); // somewhere mid-screen
|
let drag_pos = Vec2::new(640.0, 200.0); // somewhere mid-screen
|
||||||
let target_pos = Vec2::new(123.5, -50.0); // origin pile slot
|
let target_pos = Vec2::new(123.5, -50.0); // origin pile slot
|
||||||
let anim =
|
let anim = build_drag_reject_animation(drag_pos, DRAG_Z, target_pos, /* stack_index */ 3);
|
||||||
build_drag_reject_animation(drag_pos, DRAG_Z, target_pos, /* stack_index */ 3);
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(anim.end - target_pos).length() < 1e-6,
|
(anim.end - target_pos).length() < 1e-6,
|
||||||
@@ -577,8 +580,7 @@ fn rejected_drag_animation_targets_origin_resting_position() {
|
|||||||
fn rejected_drag_animation_starts_from_drag_position() {
|
fn rejected_drag_animation_starts_from_drag_position() {
|
||||||
let drag_pos = Vec2::new(640.0, 200.0);
|
let drag_pos = Vec2::new(640.0, 200.0);
|
||||||
let target_pos = Vec2::new(80.0, -120.0);
|
let target_pos = Vec2::new(80.0, -120.0);
|
||||||
let anim =
|
let anim = build_drag_reject_animation(drag_pos, DRAG_Z, target_pos, /* stack_index */ 0);
|
||||||
build_drag_reject_animation(drag_pos, DRAG_Z, target_pos, /* stack_index */ 0);
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
(anim.start - drag_pos).length() < 1e-6,
|
(anim.start - drag_pos).length() < 1e-6,
|
||||||
@@ -601,12 +603,8 @@ fn rejected_drag_animation_starts_from_drag_position() {
|
|||||||
/// the call site honest.
|
/// the call site honest.
|
||||||
#[test]
|
#[test]
|
||||||
fn rejected_drag_animation_uses_correct_duration() {
|
fn rejected_drag_animation_uses_correct_duration() {
|
||||||
let anim = build_drag_reject_animation(
|
let anim =
|
||||||
Vec2::new(640.0, 200.0),
|
build_drag_reject_animation(Vec2::new(640.0, 200.0), DRAG_Z, Vec2::new(80.0, -120.0), 0);
|
||||||
DRAG_Z,
|
|
||||||
Vec2::new(80.0, -120.0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
assert!(
|
assert!(
|
||||||
(anim.duration - MOTION_DRAG_REJECT_SECS).abs() < 1e-6,
|
(anim.duration - MOTION_DRAG_REJECT_SECS).abs() < 1e-6,
|
||||||
"drag-rejection tween duration must match MOTION_DRAG_REJECT_SECS \
|
"drag-rejection tween duration must match MOTION_DRAG_REJECT_SECS \
|
||||||
@@ -620,12 +618,8 @@ fn rejected_drag_animation_uses_correct_duration() {
|
|||||||
/// jittery rather than forgiving.
|
/// jittery rather than forgiving.
|
||||||
#[test]
|
#[test]
|
||||||
fn rejected_drag_animation_uses_responsive_curve() {
|
fn rejected_drag_animation_uses_responsive_curve() {
|
||||||
let anim = build_drag_reject_animation(
|
let anim =
|
||||||
Vec2::new(640.0, 200.0),
|
build_drag_reject_animation(Vec2::new(640.0, 200.0), DRAG_Z, Vec2::new(80.0, -120.0), 0);
|
||||||
DRAG_Z,
|
|
||||||
Vec2::new(80.0, -120.0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
anim.curve,
|
anim.curve,
|
||||||
MotionCurve::Responsive,
|
MotionCurve::Responsive,
|
||||||
@@ -683,10 +677,16 @@ fn pressing_h_spawns_pending_hint_task() {
|
|||||||
app.init_resource::<HintSolverConfig>();
|
app.init_resource::<HintSolverConfig>();
|
||||||
app.init_resource::<crate::pending_hint::PendingHintTask>();
|
app.init_resource::<crate::pending_hint::PendingHintTask>();
|
||||||
app.init_resource::<ButtonInput<KeyCode>>();
|
app.init_resource::<ButtonInput<KeyCode>>();
|
||||||
app.insert_resource(LayoutResource(
|
app.insert_resource(LayoutResource(compute_layout(
|
||||||
compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true),
|
Vec2::new(1280.0, 800.0),
|
||||||
));
|
0.0,
|
||||||
app.insert_resource(GameStateResource(GameState::new(42, DrawStockConfig::DrawOne)));
|
0.0,
|
||||||
|
true,
|
||||||
|
)));
|
||||||
|
app.insert_resource(GameStateResource(GameState::new(
|
||||||
|
42,
|
||||||
|
DrawStockConfig::DrawOne,
|
||||||
|
)));
|
||||||
app.add_systems(Update, handle_keyboard_hint);
|
app.add_systems(Update, handle_keyboard_hint);
|
||||||
|
|
||||||
// Simulate the H key being pressed this frame.
|
// Simulate the H key being pressed this frame.
|
||||||
|
|||||||
@@ -862,10 +862,8 @@ fn handle_display_name_confirm(
|
|||||||
.leaderboard_display_name
|
.leaderboard_display_name
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
if let SyncBackend::SolitaireServer {
|
if let SyncBackend::SolitaireServer { ref username, .. } =
|
||||||
ref username,
|
settings.0.sync_backend
|
||||||
..
|
|
||||||
} = settings.0.sync_backend
|
|
||||||
{
|
{
|
||||||
username.chars().take(32).collect()
|
username.chars().take(32).collect()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use solitaire_data::{Settings, save_settings_to};
|
|||||||
|
|
||||||
use crate::font_plugin::FontResource;
|
use crate::font_plugin::FontResource;
|
||||||
use crate::settings_plugin::{SettingsResource, SettingsStoragePath};
|
use crate::settings_plugin::{SettingsResource, SettingsStoragePath};
|
||||||
|
use crate::splash_plugin::SplashRoot;
|
||||||
use crate::ui_modal::{
|
use crate::ui_modal::{
|
||||||
ButtonVariant, spawn_modal, spawn_modal_actions, spawn_modal_body_text, spawn_modal_button,
|
ButtonVariant, spawn_modal, spawn_modal_actions, spawn_modal_body_text, spawn_modal_button,
|
||||||
spawn_modal_header,
|
spawn_modal_header,
|
||||||
@@ -36,7 +37,6 @@ use crate::ui_theme::{
|
|||||||
BORDER_SUBTLE, HighContrastBorder, RADIUS_SM, TEXT_PRIMARY, TYPE_BODY, TYPE_CAPTION,
|
BORDER_SUBTLE, HighContrastBorder, RADIUS_SM, TEXT_PRIMARY, TYPE_BODY, TYPE_CAPTION,
|
||||||
VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_3,
|
VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_3,
|
||||||
};
|
};
|
||||||
use crate::splash_plugin::SplashRoot;
|
|
||||||
use crate::ui_theme::{TEXT_SECONDARY, Z_ONBOARDING};
|
use crate::ui_theme::{TEXT_SECONDARY, Z_ONBOARDING};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -969,7 +969,10 @@ mod tests {
|
|||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
app.add_plugins(MinimalPlugins).add_plugins(PausePlugin);
|
app.add_plugins(MinimalPlugins).add_plugins(PausePlugin);
|
||||||
app.init_resource::<ButtonInput<KeyCode>>();
|
app.init_resource::<ButtonInput<KeyCode>>();
|
||||||
app.insert_resource(GameStateResource(GameState::new(1, DrawStockConfig::DrawOne)));
|
app.insert_resource(GameStateResource(GameState::new(
|
||||||
|
1,
|
||||||
|
DrawStockConfig::DrawOne,
|
||||||
|
)));
|
||||||
app.update();
|
app.update();
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,9 +178,9 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::events::HintVisualEvent;
|
use crate::events::HintVisualEvent;
|
||||||
use crate::input_plugin::HintSolverConfig;
|
use crate::input_plugin::HintSolverConfig;
|
||||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
|
||||||
use solitaire_core::{Card, Deck, Rank, Suit};
|
use solitaire_core::{Card, Deck, Rank, Suit};
|
||||||
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
use solitaire_core::{DrawStockConfig, game_state::GameState};
|
||||||
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
|
|
||||||
/// Build a minimal Bevy app exercising only the polling system
|
/// Build a minimal Bevy app exercising only the polling system
|
||||||
/// and the resources/messages it touches.
|
/// and the resources/messages it touches.
|
||||||
@@ -249,10 +249,7 @@ mod tests {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(suits.iter())
|
.zip(suits.iter())
|
||||||
{
|
{
|
||||||
game.set_test_tableau_cards(
|
game.set_test_tableau_cards(tableau, vec![Card::new(Deck::Deck1, *suit, Rank::King)]);
|
||||||
tableau,
|
|
||||||
vec![Card::new(Deck::Deck1, *suit, Rank::King)],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
game
|
game
|
||||||
}
|
}
|
||||||
@@ -267,9 +264,11 @@ mod tests {
|
|||||||
let mut app = pending_hint_app();
|
let mut app = pending_hint_app();
|
||||||
app.insert_resource(GameStateResource(near_finished_state()));
|
app.insert_resource(GameStateResource(near_finished_state()));
|
||||||
let cfg = *app.world().resource::<HintSolverConfig>();
|
let cfg = *app.world().resource::<HintSolverConfig>();
|
||||||
app.world_mut()
|
app.world_mut().resource_mut::<PendingHintTask>().spawn(
|
||||||
.resource_mut::<PendingHintTask>()
|
near_finished_state(),
|
||||||
.spawn(near_finished_state(), cfg.moves_budget, cfg.states_budget);
|
cfg.moves_budget,
|
||||||
|
cfg.states_budget,
|
||||||
|
);
|
||||||
|
|
||||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);
|
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);
|
||||||
while app.world().resource::<PendingHintTask>().is_pending() {
|
while app.world().resource::<PendingHintTask>().is_pending() {
|
||||||
@@ -306,9 +305,11 @@ mod tests {
|
|||||||
let mut app = pending_hint_app();
|
let mut app = pending_hint_app();
|
||||||
app.insert_resource(GameStateResource(near_finished_state()));
|
app.insert_resource(GameStateResource(near_finished_state()));
|
||||||
let cfg = *app.world().resource::<HintSolverConfig>();
|
let cfg = *app.world().resource::<HintSolverConfig>();
|
||||||
app.world_mut()
|
app.world_mut().resource_mut::<PendingHintTask>().spawn(
|
||||||
.resource_mut::<PendingHintTask>()
|
near_finished_state(),
|
||||||
.spawn(near_finished_state(), cfg.moves_budget, cfg.states_budget);
|
cfg.moves_budget,
|
||||||
|
cfg.states_budget,
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
app.world().resource::<PendingHintTask>().is_pending(),
|
app.world().resource::<PendingHintTask>().is_pending(),
|
||||||
"task is in flight after spawn",
|
"task is in flight after spawn",
|
||||||
@@ -344,18 +345,22 @@ mod tests {
|
|||||||
let cfg = *app.world().resource::<HintSolverConfig>();
|
let cfg = *app.world().resource::<HintSolverConfig>();
|
||||||
|
|
||||||
// First spawn.
|
// First spawn.
|
||||||
app.world_mut()
|
app.world_mut().resource_mut::<PendingHintTask>().spawn(
|
||||||
.resource_mut::<PendingHintTask>()
|
near_finished_state(),
|
||||||
.spawn(near_finished_state(), cfg.moves_budget, cfg.states_budget);
|
cfg.moves_budget,
|
||||||
|
cfg.states_budget,
|
||||||
|
);
|
||||||
let first_handle_present = app.world().resource::<PendingHintTask>().is_pending();
|
let first_handle_present = app.world().resource::<PendingHintTask>().is_pending();
|
||||||
assert!(first_handle_present);
|
assert!(first_handle_present);
|
||||||
|
|
||||||
// Second spawn. The `spawn` helper drops the prior task
|
// Second spawn. The `spawn` helper drops the prior task
|
||||||
// before assigning the new one — at no point are two tasks
|
// before assigning the new one — at no point are two tasks
|
||||||
// in flight.
|
// in flight.
|
||||||
app.world_mut()
|
app.world_mut().resource_mut::<PendingHintTask>().spawn(
|
||||||
.resource_mut::<PendingHintTask>()
|
near_finished_state(),
|
||||||
.spawn(near_finished_state(), cfg.moves_budget, cfg.states_budget);
|
cfg.moves_budget,
|
||||||
|
cfg.states_budget,
|
||||||
|
);
|
||||||
// Resource still pending (the second task), but the first
|
// Resource still pending (the second task), but the first
|
||||||
// is gone. We can't directly observe the first handle once
|
// is gone. We can't directly observe the first handle once
|
||||||
// it's been overwritten — what we *can* assert is that the
|
// it's been overwritten — what we *can* assert is that the
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ use bevy::input::touch::Touches;
|
|||||||
use bevy::math::Vec2;
|
use bevy::math::Vec2;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::PrimaryWindow;
|
use bevy::window::PrimaryWindow;
|
||||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
|
||||||
use solitaire_core::{FOUNDATIONS, TABLEAUS};
|
|
||||||
use solitaire_core::Card;
|
use solitaire_core::Card;
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
|
use solitaire_core::{FOUNDATIONS, TABLEAUS};
|
||||||
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
|
|
||||||
use crate::card_plugin::TABLEAU_FACEDOWN_FAN_FRAC;
|
use crate::card_plugin::TABLEAU_FACEDOWN_FAN_FRAC;
|
||||||
use crate::events::{MoveRejectedEvent, MoveRequestEvent};
|
use crate::events::{MoveRejectedEvent, MoveRequestEvent};
|
||||||
@@ -360,9 +360,6 @@ fn pile_cards(game: &GameState, pile: &KlondikePile) -> Vec<(Card, bool)> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Builds the `(destination, anchor)` list for a fresh radial open.
|
/// Builds the `(destination, anchor)` list for a fresh radial open.
|
||||||
///
|
///
|
||||||
/// `half_extents` is the window half-size in world space — icons are clamped
|
/// `half_extents` is the window half-size in world space — icons are clamped
|
||||||
@@ -381,8 +378,10 @@ fn build_radial_destinations(
|
|||||||
.map(|(i, d)| {
|
.map(|(i, d)| {
|
||||||
let raw = radial_anchor_for_index(centre, count, i, RADIAL_RADIUS_PX);
|
let raw = radial_anchor_for_index(centre, count, i, RADIAL_RADIUS_PX);
|
||||||
let clamped = Vec2::new(
|
let clamped = Vec2::new(
|
||||||
raw.x.clamp(-half_extents.x + margin, half_extents.x - margin),
|
raw.x
|
||||||
raw.y.clamp(-half_extents.y + margin, half_extents.y - margin),
|
.clamp(-half_extents.x + margin, half_extents.x - margin),
|
||||||
|
raw.y
|
||||||
|
.clamp(-half_extents.y + margin, half_extents.y - margin),
|
||||||
);
|
);
|
||||||
(d, clamped)
|
(d, clamped)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -246,7 +246,11 @@ pub(crate) fn format_suit_glyph(suit: Suit) -> &'static str {
|
|||||||
/// known card, or `"--"` for an absent top card (empty pile).
|
/// known card, or `"--"` for an absent top card (empty pile).
|
||||||
pub(crate) fn format_card_short(card: Option<&(Card, bool)>) -> String {
|
pub(crate) fn format_card_short(card: Option<&(Card, bool)>) -> String {
|
||||||
match card {
|
match card {
|
||||||
Some((c, _)) => format!("{}{}", format_rank_short(c.rank()), format_suit_glyph(c.suit())),
|
Some((c, _)) => format!(
|
||||||
|
"{}{}",
|
||||||
|
format_rank_short(c.rank()),
|
||||||
|
format_suit_glyph(c.suit())
|
||||||
|
),
|
||||||
None => "--".to_string(),
|
None => "--".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use std::sync::Arc;
|
|||||||
use bevy::math::Vec2;
|
use bevy::math::Vec2;
|
||||||
use bevy::prelude::Resource;
|
use bevy::prelude::Resource;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use solitaire_core::KlondikePile;
|
|
||||||
use solitaire_core::Card;
|
use solitaire_core::Card;
|
||||||
|
use solitaire_core::KlondikePile;
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
|
|
||||||
/// Wraps the currently active `GameState`. Single source of truth for the in-progress game.
|
/// Wraps the currently active `GameState`. Single source of truth for the in-progress game.
|
||||||
|
|||||||
@@ -90,9 +90,7 @@ mod tests {
|
|||||||
.and_then(|prefix| prefix.split_whitespace().last())
|
.and_then(|prefix| prefix.split_whitespace().last())
|
||||||
.and_then(|n| n.parse::<usize>().ok());
|
.and_then(|n| n.parse::<usize>().ok());
|
||||||
parsed.unwrap_or_else(|| {
|
parsed.unwrap_or_else(|| {
|
||||||
panic!(
|
panic!("ambiguity panic message no longer parseable (Bevy upgrade?): {msg}")
|
||||||
"ambiguity panic message no longer parseable (Bevy upgrade?): {msg}"
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,9 +37,9 @@
|
|||||||
|
|
||||||
use bevy::input::ButtonInput;
|
use bevy::input::ButtonInput;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
|
||||||
use solitaire_core::Card;
|
use solitaire_core::Card;
|
||||||
use solitaire_core::game_state::GameState;
|
use solitaire_core::game_state::GameState;
|
||||||
|
use solitaire_core::{Foundation, KlondikePile, Tableau};
|
||||||
|
|
||||||
use crate::card_plugin::CardEntityIndex;
|
use crate::card_plugin::CardEntityIndex;
|
||||||
use crate::events::{InfoToastEvent, MoveRequestEvent, StateChangedEvent};
|
use crate::events::{InfoToastEvent, MoveRequestEvent, StateChangedEvent};
|
||||||
@@ -487,8 +487,10 @@ fn handle_selection_keys(
|
|||||||
1
|
1
|
||||||
};
|
};
|
||||||
let start = source_cards.len().saturating_sub(count);
|
let start = source_cards.len().saturating_sub(count);
|
||||||
let lifted_cards: Vec<Card> =
|
let lifted_cards: Vec<Card> = source_cards[start..]
|
||||||
source_cards[start..].iter().map(|(c, _)| c.clone()).collect();
|
.iter()
|
||||||
|
.map(|(c, _)| c.clone())
|
||||||
|
.collect();
|
||||||
let Some((bottom, _)) = source_cards.get(start) else {
|
let Some((bottom, _)) = source_cards.get(start) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -597,10 +599,7 @@ fn face_up_run_len(cards: &[(Card, bool)]) -> usize {
|
|||||||
/// This is intentionally separated from [`best_destination`] so the Enter
|
/// This is intentionally separated from [`best_destination`] so the Enter
|
||||||
/// handler can attempt a foundation move first and fall through to a
|
/// handler can attempt a foundation move first and fall through to a
|
||||||
/// multi-card stack move rather than accepting a single-card tableau move.
|
/// multi-card stack move rather than accepting a single-card tableau move.
|
||||||
fn try_foundation_dest(
|
fn try_foundation_dest(card: &Card, game: &GameState) -> Option<KlondikePile> {
|
||||||
card: &Card,
|
|
||||||
game: &GameState,
|
|
||||||
) -> Option<KlondikePile> {
|
|
||||||
let source = game.pile_containing_card(card.clone())?;
|
let source = game.pile_containing_card(card.clone())?;
|
||||||
for foundation in [
|
for foundation in [
|
||||||
Foundation::Foundation1,
|
Foundation::Foundation1,
|
||||||
@@ -697,13 +696,7 @@ fn update_selection_highlight(
|
|||||||
if let Some(ref pile) = source_pile
|
if let Some(ref pile) = source_pile
|
||||||
&& let Some(card) = top_face_up_card(pile, &game.0)
|
&& let Some(card) = top_face_up_card(pile, &game.0)
|
||||||
{
|
{
|
||||||
spawn_highlight_on_card(
|
spawn_highlight_on_card(&mut commands, &card_index, &card, card_size, source_color);
|
||||||
&mut commands,
|
|
||||||
&card_index,
|
|
||||||
&card,
|
|
||||||
card_size,
|
|
||||||
source_color,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destination highlight while lifted.
|
// Destination highlight while lifted.
|
||||||
@@ -714,13 +707,7 @@ fn update_selection_highlight(
|
|||||||
// in destination-pick mode and the focused index is observable
|
// in destination-pick mode and the focused index is observable
|
||||||
// via the resource.
|
// via the resource.
|
||||||
if let Some(card) = top_face_up_card(dest, &game.0) {
|
if let Some(card) = top_face_up_card(dest, &game.0) {
|
||||||
spawn_highlight_on_card(
|
spawn_highlight_on_card(&mut commands, &card_index, &card, card_size, dest_color);
|
||||||
&mut commands,
|
|
||||||
&card_index,
|
|
||||||
&card,
|
|
||||||
card_size,
|
|
||||||
dest_color,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1047,10 +1034,7 @@ mod tests {
|
|||||||
press_key(&mut app, KeyCode::Tab);
|
press_key(&mut app, KeyCode::Tab);
|
||||||
app.update();
|
app.update();
|
||||||
|
|
||||||
let selected = app
|
let selected = app.world().resource::<SelectionState>().selected_pile;
|
||||||
.world()
|
|
||||||
.resource::<SelectionState>()
|
|
||||||
.selected_pile;
|
|
||||||
// The cycle order starts at Waste, but Waste is empty so the next
|
// The cycle order starts at Waste, but Waste is empty so the next
|
||||||
// available pile (Tableau(0)) is selected.
|
// available pile (Tableau(0)) is selected.
|
||||||
assert_eq!(selected, Some(KlondikePile::Tableau(Tableau::Tableau1)));
|
assert_eq!(selected, Some(KlondikePile::Tableau(Tableau::Tableau1)));
|
||||||
@@ -1216,16 +1200,10 @@ mod tests {
|
|||||||
drag.active_touch_id = None;
|
drag.active_touch_id = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let before = app
|
let before = app.world().resource::<SelectionState>().selected_pile;
|
||||||
.world()
|
|
||||||
.resource::<SelectionState>()
|
|
||||||
.selected_pile;
|
|
||||||
press_key(&mut app, KeyCode::Tab);
|
press_key(&mut app, KeyCode::Tab);
|
||||||
app.update();
|
app.update();
|
||||||
let after = app
|
let after = app.world().resource::<SelectionState>().selected_pile;
|
||||||
.world()
|
|
||||||
.resource::<SelectionState>()
|
|
||||||
.selected_pile;
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
before, after,
|
before, after,
|
||||||
|
|||||||
@@ -1500,10 +1500,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn zen_win_event_updates_zen_best_score_only() {
|
fn zen_win_event_updates_zen_best_score_only() {
|
||||||
let mut app = headless_app();
|
let mut app = headless_app();
|
||||||
app.world_mut()
|
app.world_mut().resource_mut::<GameStateResource>().0.mode =
|
||||||
.resource_mut::<GameStateResource>()
|
solitaire_core::game_state::GameMode::Zen;
|
||||||
.0
|
|
||||||
.mode = solitaire_core::game_state::GameMode::Zen;
|
|
||||||
|
|
||||||
app.world_mut().write_message(GameWonEvent {
|
app.world_mut().write_message(GameWonEvent {
|
||||||
score: 1800,
|
score: 1800,
|
||||||
|
|||||||
@@ -301,9 +301,9 @@ fn push_on_exit(
|
|||||||
exit_events.clear();
|
exit_events.clear();
|
||||||
|
|
||||||
let payload = build_payload(&stats.0, &achievements.0, &progress.0);
|
let payload = build_payload(&stats.0, &achievements.0, &progress.0);
|
||||||
let result = rt
|
let result = rt.0.block_on(async {
|
||||||
.0
|
tokio::time::timeout(EXIT_PUSH_TIMEOUT, provider.0.push(&payload)).await
|
||||||
.block_on(async { tokio::time::timeout(EXIT_PUSH_TIMEOUT, provider.0.push(&payload)).await });
|
});
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(_)) | Ok(Err(SyncError::UnsupportedPlatform)) => {}
|
Ok(Ok(_)) | Ok(Err(SyncError::UnsupportedPlatform)) => {}
|
||||||
Ok(Err(e)) => warn!("sync push on exit failed: {e}"),
|
Ok(Err(e)) => warn!("sync push on exit failed: {e}"),
|
||||||
@@ -667,9 +667,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// In-memory contract: replays[0].share_url is now Some(url).
|
// In-memory contract: replays[0].share_url is now Some(url).
|
||||||
let live = app
|
let live = app.world().resource::<ReplayHistoryResource>();
|
||||||
.world()
|
|
||||||
.resource::<ReplayHistoryResource>();
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
live.0.replays.first().and_then(|r| r.share_url.clone()),
|
live.0.replays.first().and_then(|r| r.share_url.clone()),
|
||||||
Some(url.clone()),
|
Some(url.clone()),
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::WindowResized;
|
use bevy::window::WindowResized;
|
||||||
use solitaire_core::KlondikePile;
|
use solitaire_core::KlondikePile;
|
||||||
use solitaire_core::{FOUNDATIONS, TABLEAUS};
|
|
||||||
use solitaire_core::Suit;
|
use solitaire_core::Suit;
|
||||||
|
use solitaire_core::{FOUNDATIONS, TABLEAUS};
|
||||||
|
|
||||||
use crate::events::{HintVisualEvent, StateChangedEvent};
|
use crate::events::{HintVisualEvent, StateChangedEvent};
|
||||||
use crate::game_plugin::GameMutation;
|
use crate::game_plugin::GameMutation;
|
||||||
@@ -385,7 +385,11 @@ fn on_window_resized(
|
|||||||
>,
|
>,
|
||||||
mut marker_outlines: Query<
|
mut marker_outlines: Query<
|
||||||
&mut Sprite,
|
&mut Sprite,
|
||||||
(Without<PileMarker>, Without<TableBackground>, Without<Text2d>),
|
(
|
||||||
|
Without<PileMarker>,
|
||||||
|
Without<TableBackground>,
|
||||||
|
Without<Text2d>,
|
||||||
|
),
|
||||||
>,
|
>,
|
||||||
mut marker_labels: Query<&mut TextFont, With<Text2d>>,
|
mut marker_labels: Query<&mut TextFont, With<Text2d>>,
|
||||||
) {
|
) {
|
||||||
@@ -437,8 +441,7 @@ fn on_window_resized(
|
|||||||
// must be re-derived here too or a resize (fold/unfold, rotation)
|
// must be re-derived here too or a resize (fold/unfold, rotation)
|
||||||
// leaves them at the stale size — visible as oversized grey
|
// leaves them at the stale size — visible as oversized grey
|
||||||
// frames on empty piles.
|
// frames on empty piles.
|
||||||
let outline_size =
|
let outline_size = new_layout.card_size + Vec2::splat(PILE_MARKER_OUTLINE_WIDTH * 2.0);
|
||||||
new_layout.card_size + Vec2::splat(PILE_MARKER_OUTLINE_WIDTH * 2.0);
|
|
||||||
let font_size = new_layout.card_size.x * 0.28;
|
let font_size = new_layout.card_size.x * 0.28;
|
||||||
for child in children.into_iter().flatten() {
|
for child in children.into_iter().flatten() {
|
||||||
if let Ok(mut outline) = marker_outlines.get_mut(*child) {
|
if let Ok(mut outline) = marker_outlines.get_mut(*child) {
|
||||||
@@ -593,8 +596,6 @@ fn pile_cards(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -703,7 +704,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert_eq!(outlines, 12, "all 12 markers carry an outline child");
|
assert_eq!(outlines, 12, "all 12 markers carry an outline child");
|
||||||
assert!(labels >= 11, "tableau + foundation markers carry watermarks");
|
assert!(
|
||||||
|
labels >= 11,
|
||||||
|
"tableau + foundation markers carry watermarks"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -939,10 +943,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn suit_symbol_all_four_are_distinct() {
|
fn suit_symbol_all_four_are_distinct() {
|
||||||
let symbols: Vec<&str> = Suit::SUITS
|
let symbols: Vec<&str> = Suit::SUITS.iter().map(suit_symbol).collect();
|
||||||
.iter()
|
|
||||||
.map(suit_symbol)
|
|
||||||
.collect();
|
|
||||||
let unique: std::collections::HashSet<&&str> = symbols.iter().collect();
|
let unique: std::collections::HashSet<&&str> = symbols.iter().collect();
|
||||||
assert_eq!(unique.len(), 4, "all four suit symbols must be distinct");
|
assert_eq!(unique.len(), 4, "all four suit symbols must be distinct");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,8 +252,6 @@ fn apply_theme_to_card_image_set(theme: &CardTheme, image_set: &mut CardImageSet
|
|||||||
image_set.theme_back = Some(theme.back.clone());
|
image_set.theme_back = Some(theme.back.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Switches the active theme to the one served at
|
/// Switches the active theme to the one served at
|
||||||
/// `themes://<theme_id>/theme.ron`. Returns the new `Handle<CardTheme>`
|
/// `themes://<theme_id>/theme.ron`. Returns the new `Handle<CardTheme>`
|
||||||
/// so callers can poll `Assets<CardTheme>` if they want to wait for
|
/// so callers can poll `Assets<CardTheme>` if they want to wait for
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
|
|
||||||
use bevy::ecs::message::MessageReader;
|
use bevy::ecs::message::MessageReader;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use solitaire_core::KlondikePile;
|
|
||||||
use solitaire_core::Card;
|
use solitaire_core::Card;
|
||||||
|
use solitaire_core::KlondikePile;
|
||||||
|
|
||||||
use crate::card_plugin::CardEntity;
|
use crate::card_plugin::CardEntity;
|
||||||
use crate::events::StateChangedEvent;
|
use crate::events::StateChangedEvent;
|
||||||
|
|||||||
@@ -304,10 +304,7 @@ impl HighContrastBackground {
|
|||||||
/// [`BORDER_SUBTLE_HC`]. Currently used by the WIN MOVE scrub-bar
|
/// [`BORDER_SUBTLE_HC`]. Currently used by the WIN MOVE scrub-bar
|
||||||
/// marker which bumps `STATE_SUCCESS` → `STATE_SUCCESS_HC` rather
|
/// marker which bumps `STATE_SUCCESS` → `STATE_SUCCESS_HC` rather
|
||||||
/// than to a neutral gray.
|
/// than to a neutral gray.
|
||||||
pub const fn with_hc(
|
pub const fn with_hc(default_color: Color, hc_color: Color) -> Self {
|
||||||
default_color: Color,
|
|
||||||
hc_color: Color,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
default_color,
|
default_color,
|
||||||
hc_color,
|
hc_color,
|
||||||
|
|||||||
+25
-14
@@ -19,11 +19,14 @@
|
|||||||
//! is the contract.
|
//! is the contract.
|
||||||
|
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use solitaire_core::{KlondikeInstruction, KlondikePile};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use solitaire_core::{Card, Deck, Rank, Suit};
|
|
||||||
use solitaire_core::error::MoveError;
|
use solitaire_core::error::MoveError;
|
||||||
use solitaire_core::{DrawStockConfig, game_state::{GameMode, GameState}};
|
use solitaire_core::{Card, Deck, Rank, Suit};
|
||||||
|
use solitaire_core::{
|
||||||
|
DrawStockConfig,
|
||||||
|
game_state::{GameMode, GameState},
|
||||||
|
};
|
||||||
|
use solitaire_core::{KlondikeInstruction, KlondikePile};
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
/// Mirrors `solitaire_data::Replay` v3.
|
/// Mirrors `solitaire_data::Replay` v3.
|
||||||
@@ -145,8 +148,8 @@ impl ReplayPlayer {
|
|||||||
let pile_cards = |t: KlondikePile| -> Vec<CardSnapshot> {
|
let pile_cards = |t: KlondikePile| -> Vec<CardSnapshot> {
|
||||||
self.game.pile(t).iter().map(CardSnapshot::from).collect()
|
self.game.pile(t).iter().map(CardSnapshot::from).collect()
|
||||||
};
|
};
|
||||||
let foundations: [Vec<CardSnapshot>; 4] = solitaire_core::FOUNDATIONS
|
let foundations: [Vec<CardSnapshot>; 4] =
|
||||||
.map(|f| pile_cards(KlondikePile::Foundation(f)));
|
solitaire_core::FOUNDATIONS.map(|f| pile_cards(KlondikePile::Foundation(f)));
|
||||||
let tableaus: [Vec<CardSnapshot>; 7] =
|
let tableaus: [Vec<CardSnapshot>; 7] =
|
||||||
solitaire_core::TABLEAUS.map(|t| pile_cards(KlondikePile::Tableau(t)));
|
solitaire_core::TABLEAUS.map(|t| pile_cards(KlondikePile::Tableau(t)));
|
||||||
StateSnapshot {
|
StateSnapshot {
|
||||||
@@ -342,8 +345,7 @@ fn legal_moves_for_game(game: &GameState) -> Vec<DebugMove> {
|
|||||||
fn invariant_report_for_game(game: &GameState, legal_moves: &[DebugMove]) -> DebugInvariantReport {
|
fn invariant_report_for_game(game: &GameState, legal_moves: &[DebugMove]) -> DebugInvariantReport {
|
||||||
let stock = game.stock_cards();
|
let stock = game.stock_cards();
|
||||||
let waste = game.waste_cards();
|
let waste = game.waste_cards();
|
||||||
let foundations =
|
let foundations = solitaire_core::FOUNDATIONS.map(|f| game.pile(KlondikePile::Foundation(f)));
|
||||||
solitaire_core::FOUNDATIONS.map(|f| game.pile(KlondikePile::Foundation(f)));
|
|
||||||
let tableaus = solitaire_core::TABLEAUS.map(|t| game.pile(KlondikePile::Tableau(t)));
|
let tableaus = solitaire_core::TABLEAUS.map(|t| game.pile(KlondikePile::Tableau(t)));
|
||||||
|
|
||||||
let mut seen: std::collections::HashSet<Card> = std::collections::HashSet::new();
|
let mut seen: std::collections::HashSet<Card> = std::collections::HashSet::new();
|
||||||
@@ -403,7 +405,8 @@ fn invariant_report_for_game(game: &GameState, legal_moves: &[DebugMove]) -> Deb
|
|||||||
false
|
false
|
||||||
});
|
});
|
||||||
|
|
||||||
let soft_lock = !game.is_won() && stock.is_empty() && waste.is_empty() && legal_moves.is_empty();
|
let soft_lock =
|
||||||
|
!game.is_won() && stock.is_empty() && waste.is_empty() && legal_moves.is_empty();
|
||||||
|
|
||||||
let state_ok = duplicate_cards.is_empty()
|
let state_ok = duplicate_cards.is_empty()
|
||||||
&& missing_cards.is_empty()
|
&& missing_cards.is_empty()
|
||||||
@@ -465,8 +468,7 @@ impl SolitaireGame {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(CardSnapshot::from)
|
.map(CardSnapshot::from)
|
||||||
.collect(),
|
.collect(),
|
||||||
foundations: solitaire_core::FOUNDATIONS
|
foundations: solitaire_core::FOUNDATIONS.map(|f| cards(KlondikePile::Foundation(f))),
|
||||||
.map(|f| cards(KlondikePile::Foundation(f))),
|
|
||||||
tableaus: solitaire_core::TABLEAUS.map(|t| cards(KlondikePile::Tableau(t))),
|
tableaus: solitaire_core::TABLEAUS.map(|t| cards(KlondikePile::Tableau(t))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -888,7 +890,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let idx = pick_move_index(&legal_moves).unwrap_or_default();
|
let idx = pick_move_index(&legal_moves).unwrap_or_default();
|
||||||
if let Err(e) = game.apply_legal_move_native(idx) {
|
if let Err(e) = game.apply_legal_move_native(idx) {
|
||||||
panic!("failed to advance game before replay export (seed={seed}, step={step}, idx={idx}): {e}");
|
panic!(
|
||||||
|
"failed to advance game before replay export (seed={seed}, step={step}, idx={idx}): {e}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1073,9 +1077,14 @@ mod tests {
|
|||||||
let stock_before = game.game.stock_cards().len();
|
let stock_before = game.game.stock_cards().len();
|
||||||
let waste_before = game.game.waste_cards().len();
|
let waste_before = game.game.waste_cards().len();
|
||||||
|
|
||||||
assert!(stock_before > 0, "seed {seed}: stock must be non-empty at start");
|
assert!(
|
||||||
|
stock_before > 0,
|
||||||
|
"seed {seed}: stock must be non-empty at start"
|
||||||
|
);
|
||||||
|
|
||||||
game.game.draw().expect("draw must succeed when stock is non-empty");
|
game.game
|
||||||
|
.draw()
|
||||||
|
.expect("draw must succeed when stock is non-empty");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.game.stock_cards().len(),
|
game.game.stock_cards().len(),
|
||||||
@@ -1104,7 +1113,9 @@ mod tests {
|
|||||||
"seed {seed}: stock must have at least 3 cards for this test"
|
"seed {seed}: stock must have at least 3 cards for this test"
|
||||||
);
|
);
|
||||||
|
|
||||||
game.game.draw().expect("draw must succeed when stock has cards");
|
game.game
|
||||||
|
.draw()
|
||||||
|
.expect("draw must succeed when stock has cards");
|
||||||
|
|
||||||
let expected_drawn = stock_before.min(3);
|
let expected_drawn = stock_before.min(3);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ pub fn start() {
|
|||||||
// texture-dimension limit is now taken from the adapter (see
|
// texture-dimension limit is now taken from the adapter (see
|
||||||
// the RenderPlugin below), so this is purely a quality/perf
|
// the RenderPlugin below), so this is purely a quality/perf
|
||||||
// choice, no longer a crash-avoidance hack.
|
// choice, no longer a crash-avoidance hack.
|
||||||
resolution: WindowResolution::default()
|
resolution: WindowResolution::default().with_scale_factor_override(1.0),
|
||||||
.with_scale_factor_override(1.0),
|
|
||||||
..default()
|
..default()
|
||||||
}),
|
}),
|
||||||
..default()
|
..default()
|
||||||
|
|||||||
Reference in New Issue
Block a user