Compare commits

..

18 Commits

Author SHA1 Message Date
Gitea CI 956a7777e2 chore(deploy): bump image to 32400356 [skip ci] 2026-06-09 02:14:53 +00:00
Gitea CI d0fa79fa1a chore(deploy): bump image to 159774f8 [skip ci] 2026-06-09 02:10:35 +00:00
Gitea CI cac85156d8 chore(deploy): bump image to 7fe6ac6c [skip ci] 2026-06-09 02:09:09 +00:00
Gitea CI 9372c9cba6 chore(deploy): bump image to 7dbf34c1 [skip ci] 2026-06-08 18:14:35 +00:00
Gitea CI f4f2ef7b7d chore(deploy): bump image to 2cf72821 [skip ci] 2026-06-02 20:45:47 +00:00
Gitea CI 9ae940dff6 chore(deploy): bump image to 8b262afc [skip ci] 2026-06-02 20:36:31 +00:00
Gitea CI b966708228 chore(deploy): bump image to 8b736cae [skip ci] 2026-06-02 20:27:59 +00:00
Gitea CI 31fc0eb9ec chore(deploy): bump image to de7ae168 [skip ci] 2026-06-02 20:04:37 +00:00
Gitea CI e80ac5e636 chore(deploy): bump image to d45b7cb8 [skip ci] 2026-06-02 19:44:37 +00:00
Gitea CI f8d15d39f2 chore(deploy): bump image to 763fdb48 [skip ci] 2026-06-02 19:43:43 +00:00
Gitea CI 2f9cd1a32d chore(deploy): bump image to 1cdb78ca [skip ci] 2026-06-02 19:26:23 +00:00
Gitea CI 4dc5956552 chore(deploy): bump image to 20e52221 [skip ci] 2026-06-01 22:30:58 +00:00
Gitea CI 627b116c12 chore(deploy): bump image to 44e90ff5 [skip ci] 2026-06-01 22:03:04 +00:00
Gitea CI dc0ce8cd02 chore(deploy): bump image to 7eb1181e [skip ci] 2026-05-28 21:45:02 +00:00
Gitea CI 4c517f4ccd chore(deploy): bump image to 6e407a3e [skip ci] 2026-05-28 20:45:06 +00:00
Gitea CI 7a523f3963 chore(deploy): bump image to 561395fc [skip ci] 2026-05-28 00:34:41 +00:00
Gitea CI e46b3fce2e chore(deploy): bump image to 25c43db6 [skip ci] 2026-05-27 21:44:57 +00:00
Gitea CI 07c05179c3 chore(deploy): bump image to ecab227b [skip ci] 2026-05-19 23:58:50 +00:00
9 changed files with 17 additions and 149 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ spec:
project: default
source:
repoURL: https://git.aleshym.co/funman300/Ferrous-Solitaire.git
targetRevision: deploy
targetRevision: master
path: deploy
destination:
server: https://kubernetes.default.svc
+1 -1
View File
@@ -20,4 +20,4 @@ resources:
images:
- name: solitaire-server
newName: git.aleshym.co/funman300/solitaire-server
newTag: da601beb
newTag: "32400356"
+6 -34
View File
@@ -19,8 +19,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
use bevy::prelude::*;
use bevy::window::{MonitorSelection, PresentMode, WindowPosition};
#[cfg(target_os = "android")]
use bevy::winit::{UpdateMode, WinitSettings};
#[cfg(not(target_os = "android"))]
use bevy::window::{Monitor, PrimaryMonitor, PrimaryWindow};
#[cfg(not(target_os = "android"))]
@@ -114,22 +112,12 @@ pub fn run() {
name: Some("ferrous-solitaire".into()),
resolution: window_resolution,
position: window_position,
// On Android, AutoVsync caps the GPU at the display
// refresh rate (~60-90 fps). Without it the renderer
// spins as fast as the hardware allows, keeping the
// GPU fully loaded and draining the battery even when
// the game is completely idle.
//
// On desktop (X11 / Wayland) AutoNoVsync prefers
// Mailbox (triple-buffered) and falls back to
// Immediate, eliminating the vsync stall that
// AutoVsync produces during continuous window resize.
// The game's frame budget is small enough that a few
// stray dropped frames from disabling vsync are
// imperceptible on desktop.
#[cfg(target_os = "android")]
present_mode: PresentMode::AutoVsync,
#[cfg(not(target_os = "android"))]
// AutoNoVsync prefers Mailbox (triple-buffered) and
// falls back to Immediate, eliminating the vsync stall
// that AutoVsync produces during continuous window
// resize on X11 / Wayland. The game's frame budget is
// small enough that a few stray dropped frames from
// disabling vsync are imperceptible.
present_mode: PresentMode::AutoNoVsync,
// Android windows always fill the screen; max_width/max_height
// default to 0.0, which panics Bevy's clamp when min > max.
@@ -216,22 +204,6 @@ pub fn run() {
.add_plugins(SplashPlugin)
.add_plugins(DiagnosticsHudPlugin);
// On Android the default WinitSettings use UpdateMode::Continuous for
// the focused window, which means Bevy renders as fast as possible even
// when the game is completely idle. Switching to reactive_low_power with
// a 1-second ceiling when the app is backgrounded cuts wake-up frequency
// from ~60 Hz to ≤1 Hz, dramatically reducing background battery drain.
//
// The focused mode stays Continuous so that card-slide animations remain
// smooth. PresentMode::AutoVsync (set above) keeps the GPU capped at the
// display refresh rate (~60 Hz) when foregrounded, which already prevents
// the GPU from spinning at 200+ fps between vsync intervals.
#[cfg(target_os = "android")]
app.insert_resource(WinitSettings {
focused_mode: UpdateMode::Continuous,
unfocused_mode: UpdateMode::reactive_low_power(std::time::Duration::from_secs(1)),
});
// Wire the runtime window icon. Bevy 0.18 has no first-class
// `Window::icon` field; the icon is set through the underlying
// `winit::window::Window` via `WinitWindows`. Android draws its
-23
View File
@@ -1680,29 +1680,6 @@ mod tests {
);
}
#[test]
fn possible_instructions_includes_foundation_to_tableau_when_enabled() {
// Reuse the Foundation→Tableau board setup (Foundation(0): A♠,2♠; Tableau(0): 3♥).
let g = setup_take_from_foundation_game();
assert!(g.take_from_foundation);
let moves = g.possible_instructions();
assert!(
moves.contains(&(PileType::Foundation(0), PileType::Tableau(0), 1)),
"possible_instructions must include Foundation→Tableau when take_from_foundation is on; got {moves:?}"
);
}
#[test]
fn possible_instructions_excludes_foundation_to_tableau_when_disabled() {
let mut g = setup_take_from_foundation_game();
g.take_from_foundation = false;
let moves = g.possible_instructions();
assert!(
!moves.iter().any(|(from, _, _)| matches!(from, PileType::Foundation(_))),
"possible_instructions must not include any Foundation source when take_from_foundation is off; got {moves:?}"
);
}
// --- P2: waste multi-card move must be rejected ---
#[test]
+1 -1
View File
@@ -81,7 +81,7 @@ const VOLUME_TOAST_SECS: f32 = 1.4;
///
/// 50.0 sits comfortably above the highest pile depth (~1.04) and well below
/// `DRAG_Z` (500), so a dragged card always renders above an animated one.
pub const CARD_ANIM_Z_LIFT: f32 = 50.0;
const CARD_ANIM_Z_LIFT: f32 = 50.0;
/// Per-card stagger interval for the win cascade at Normal speed (seconds).
///
+2 -7
View File
@@ -23,7 +23,7 @@ use solitaire_core::pile::PileType;
use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
use crate::animation_plugin::{CardAnim, EffectiveSlideDuration, CARD_ANIM_Z_LIFT};
use crate::animation_plugin::{CardAnim, EffectiveSlideDuration};
use crate::card_animation::CardAnimation;
use crate::events::{CardFaceRevealedEvent, CardFlippedEvent, StateChangedEvent};
use crate::game_plugin::GameMutation;
@@ -963,12 +963,7 @@ fn update_card_entity(
if !has_card_animation {
// Slide to the new position when it differs meaningfully; snap otherwise.
if (cur.truncate() - target.truncate()).length() > 1.0 && slide_secs > 0.0 {
// Lift the card immediately on the first frame of the animation so
// it never appears behind a card that is already resting at the
// destination slot. `advance_card_anims` will maintain this lift
// throughout the tween and snap to `target` (without lift) on
// completion.
let start = Vec3::new(cur.x, cur.y, z + CARD_ANIM_Z_LIFT);
let start = Vec3::new(cur.x, cur.y, z); // update Z immediately
commands
.entity(entity)
.insert(Transform::from_translation(start))
+4 -34
View File
@@ -734,13 +734,8 @@ fn end_drag(
.is_some_and(|p| can_place_on_foundation(&bottom_card, p))
}
PileType::Tableau(_) => {
// Enforce the take-from-foundation rule at the input layer so the
// engine never fires a MoveRequestEvent that game_state would reject.
let foundation_allowed = !matches!(&origin, PileType::Foundation(_))
|| game.0.take_from_foundation;
foundation_allowed
&& game.0.piles.get(&target)
.is_some_and(|p| can_place_on_tableau(&bottom_card, p))
game.0.piles.get(&target)
.is_some_and(|p| can_place_on_tableau(&bottom_card, p))
}
_ => false,
};
@@ -993,13 +988,8 @@ fn touch_end_drag(
.is_some_and(|p| can_place_on_foundation(&bottom_card, p))
}
PileType::Tableau(_) => {
// Enforce the take-from-foundation rule at the input layer so the
// engine never fires a MoveRequestEvent that game_state would reject.
let foundation_allowed = !matches!(&origin, PileType::Foundation(_))
|| game.0.take_from_foundation;
foundation_allowed
&& game.0.piles.get(&target)
.is_some_and(|p| can_place_on_tableau(&bottom_card, p))
game.0.piles.get(&target)
.is_some_and(|p| can_place_on_tableau(&bottom_card, p))
}
_ => false,
};
@@ -1601,26 +1591,6 @@ pub fn all_hints(game: &GameState) -> Vec<(PileType, PileType, usize)> {
}
}
// Pass 2b — Foundation → Tableau moves (only when the rule allows it).
// Foundation piles are excluded from Pass 1 & 2's source list because they
// should never hint Foundation→Foundation. Here we handle the return path
// separately so the guarded `take_from_foundation` rule is respected.
if game.take_from_foundation {
for slot in 0..4_u8 {
let from = PileType::Foundation(slot);
let Some(from_pile) = game.piles.get(&from) else { continue };
let Some(card) = from_pile.cards.last().filter(|c| c.face_up) else { continue };
for i in 0..7_usize {
let dest = PileType::Tableau(i);
if let Some(dest_pile) = game.piles.get(&dest)
&& can_place_on_tableau(card, dest_pile) {
hints.push((from.clone(), dest, 1));
break;
}
}
}
}
// Pass 3 — suggest drawing from the stock when no other hint was found.
if hints.is_empty() {
let stock_non_empty = game.piles.get(&PileType::Stock)
+2 -48
View File
@@ -1,3 +1,5 @@
/* @ts-self-types="./solitaire_wasm.d.ts" */
/**
* Browser-side replay state machine. Owns a live `GameState` and the
* replay's move list; each `step()` applies the next move.
@@ -92,12 +94,6 @@ if (Symbol.dispose) ReplayPlayer.prototype[Symbol.dispose] = ReplayPlayer.protot
* full pile snapshot at any time without mutating state.
*/
export class SolitaireGame {
static __wrap(ptr) {
const obj = Object.create(SolitaireGame.prototype);
obj.__wbg_ptr = ptr;
SolitaireGameFinalization.register(obj, obj.__wbg_ptr, obj);
return obj;
}
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
@@ -129,23 +125,6 @@ export class SolitaireGame {
const ret = wasm.solitairegame_draw(this.__wbg_ptr);
return ret;
}
/**
* Restore a game from a JSON string previously produced by [`SolitaireGame::serialize`].
*
* Returns an error string if the JSON is malformed or describes a state
* that can't be deserialised (e.g. from a future schema version).
* @param {string} json
* @returns {SolitaireGame}
*/
static from_saved(json) {
const ptr0 = passStringToWasm0(json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.solitairegame_from_saved(ptr0, len0);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return SolitaireGame.__wrap(ret[0]);
}
/**
* Move `count` cards from pile `from` to pile `to`.
*
@@ -188,31 +167,6 @@ export class SolitaireGame {
const ret = wasm.solitairegame_seed(this.__wbg_ptr);
return ret;
}
/**
* Serialise the full game state as a JSON string for `localStorage`.
*
* Use [`SolitaireGame::from_saved`] to restore it. The returned string is
* opaque callers should treat it as a blob and store/restore it verbatim.
* @returns {string}
*/
serialize() {
let deferred2_0;
let deferred2_1;
try {
const ret = wasm.solitairegame_serialize(this.__wbg_ptr);
var ptr1 = ret[0];
var len1 = ret[1];
if (ret[3]) {
ptr1 = 0; len1 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred2_0 = ptr1;
deferred2_1 = len1;
return getStringFromWasm0(ptr1, len1);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* Full pile snapshot as a JS object.
*
Binary file not shown.