Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e50ff02274 |
@@ -60,17 +60,19 @@ jobs:
|
|||||||
curl -sL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.4.3/kustomize_v5.4.3_linux_amd64.tar.gz | tar xz
|
curl -sL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.4.3/kustomize_v5.4.3_linux_amd64.tar.gz | tar xz
|
||||||
sudo mv kustomize /usr/local/bin/kustomize
|
sudo mv kustomize /usr/local/bin/kustomize
|
||||||
|
|
||||||
- name: Pin image tag and push to deploy branch
|
- name: Pin image tag in deploy manifests
|
||||||
|
run: |
|
||||||
|
cd deploy
|
||||||
|
kustomize edit set image solitaire-server=${{ env.IMAGE }}:${{ steps.meta.outputs.sha }}
|
||||||
|
|
||||||
|
- name: Commit and push updated kustomization
|
||||||
run: |
|
run: |
|
||||||
git config user.email "ci@gitea.local"
|
git config user.email "ci@gitea.local"
|
||||||
git config user.name "Gitea CI"
|
git config user.name "Gitea CI"
|
||||||
# Switch to the deploy branch, creating it from the current HEAD if absent.
|
|
||||||
git fetch origin deploy 2>/dev/null && git checkout deploy || git checkout -b deploy
|
|
||||||
# Update the pinned image tag.
|
|
||||||
cd deploy
|
|
||||||
kustomize edit set image solitaire-server=${{ env.IMAGE }}:${{ steps.meta.outputs.sha }}
|
|
||||||
cd ..
|
|
||||||
git add deploy/kustomization.yaml
|
git add deploy/kustomization.yaml
|
||||||
git diff --cached --quiet && exit 0
|
git diff --cached --quiet && exit 0 # nothing to commit — skip push
|
||||||
git commit -m "chore(deploy): bump image to ${{ steps.meta.outputs.sha }} [skip ci]"
|
git commit -m "chore(deploy): bump image to ${{ steps.meta.outputs.sha }} [skip ci]"
|
||||||
git push origin deploy
|
for i in 1 2 3; do
|
||||||
|
git pull --rebase origin master && git push && break
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ spec:
|
|||||||
project: default
|
project: default
|
||||||
source:
|
source:
|
||||||
repoURL: https://git.aleshym.co/funman300/Ferrous-Solitaire.git
|
repoURL: https://git.aleshym.co/funman300/Ferrous-Solitaire.git
|
||||||
targetRevision: deploy
|
targetRevision: master
|
||||||
path: deploy
|
path: deploy
|
||||||
destination:
|
destination:
|
||||||
server: https://kubernetes.default.svc
|
server: https://kubernetes.default.svc
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ resources:
|
|||||||
images:
|
images:
|
||||||
- name: solitaire-server
|
- name: solitaire-server
|
||||||
newName: git.aleshym.co/funman300/solitaire-server
|
newName: git.aleshym.co/funman300/solitaire-server
|
||||||
newTag: da601beb
|
newTag: 90eb5fd2
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::{MonitorSelection, PresentMode, WindowPosition};
|
use bevy::window::{MonitorSelection, PresentMode, WindowPosition};
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
use bevy::winit::{UpdateMode, WinitSettings};
|
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
use bevy::window::{Monitor, PrimaryMonitor, PrimaryWindow};
|
use bevy::window::{Monitor, PrimaryMonitor, PrimaryWindow};
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
@@ -114,22 +112,12 @@ pub fn run() {
|
|||||||
name: Some("ferrous-solitaire".into()),
|
name: Some("ferrous-solitaire".into()),
|
||||||
resolution: window_resolution,
|
resolution: window_resolution,
|
||||||
position: window_position,
|
position: window_position,
|
||||||
// On Android, AutoVsync caps the GPU at the display
|
// AutoNoVsync prefers Mailbox (triple-buffered) and
|
||||||
// refresh rate (~60-90 fps). Without it the renderer
|
// falls back to Immediate, eliminating the vsync stall
|
||||||
// spins as fast as the hardware allows, keeping the
|
// that AutoVsync produces during continuous window
|
||||||
// GPU fully loaded and draining the battery even when
|
// resize on X11 / Wayland. The game's frame budget is
|
||||||
// the game is completely idle.
|
// small enough that a few stray dropped frames from
|
||||||
//
|
// disabling vsync are imperceptible.
|
||||||
// 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"))]
|
|
||||||
present_mode: PresentMode::AutoNoVsync,
|
present_mode: PresentMode::AutoNoVsync,
|
||||||
// Android windows always fill the screen; max_width/max_height
|
// Android windows always fill the screen; max_width/max_height
|
||||||
// default to 0.0, which panics Bevy's clamp when min > max.
|
// default to 0.0, which panics Bevy's clamp when min > max.
|
||||||
@@ -216,22 +204,6 @@ pub fn run() {
|
|||||||
.add_plugins(SplashPlugin)
|
.add_plugins(SplashPlugin)
|
||||||
.add_plugins(DiagnosticsHudPlugin);
|
.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
|
// Wire the runtime window icon. Bevy 0.18 has no first-class
|
||||||
// `Window::icon` field; the icon is set through the underlying
|
// `Window::icon` field; the icon is set through the underlying
|
||||||
// `winit::window::Window` via `WinitWindows`. Android draws its
|
// `winit::window::Window` via `WinitWindows`. Android draws its
|
||||||
|
|||||||
@@ -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 ---
|
// --- P2: waste multi-card move must be rejected ---
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -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
|
/// 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.
|
/// `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).
|
/// Per-card stagger interval for the win cascade at Normal speed (seconds).
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use solitaire_core::pile::PileType;
|
|||||||
|
|
||||||
use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
|
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::card_animation::CardAnimation;
|
||||||
use crate::events::{CardFaceRevealedEvent, CardFlippedEvent, StateChangedEvent};
|
use crate::events::{CardFaceRevealedEvent, CardFlippedEvent, StateChangedEvent};
|
||||||
use crate::game_plugin::GameMutation;
|
use crate::game_plugin::GameMutation;
|
||||||
@@ -963,12 +963,7 @@ fn update_card_entity(
|
|||||||
if !has_card_animation {
|
if !has_card_animation {
|
||||||
// Slide to the new position when it differs meaningfully; snap otherwise.
|
// Slide to the new position when it differs meaningfully; snap otherwise.
|
||||||
if (cur.truncate() - target.truncate()).length() > 1.0 && slide_secs > 0.0 {
|
if (cur.truncate() - target.truncate()).length() > 1.0 && slide_secs > 0.0 {
|
||||||
// Lift the card immediately on the first frame of the animation so
|
let start = Vec3::new(cur.x, cur.y, z); // update Z immediately
|
||||||
// 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);
|
|
||||||
commands
|
commands
|
||||||
.entity(entity)
|
.entity(entity)
|
||||||
.insert(Transform::from_translation(start))
|
.insert(Transform::from_translation(start))
|
||||||
|
|||||||
@@ -1045,7 +1045,9 @@ pub fn record_replay_on_win(
|
|||||||
/// previous heuristic incorrectly did (Quat hit this with 4 cards
|
/// previous heuristic incorrectly did (Quat hit this with 4 cards
|
||||||
/// remaining and the game just sat there).
|
/// remaining and the game just sat there).
|
||||||
pub fn has_legal_moves(game: &GameState) -> bool {
|
pub fn has_legal_moves(game: &GameState) -> bool {
|
||||||
|
use solitaire_core::card::Card;
|
||||||
use solitaire_core::pile::PileType;
|
use solitaire_core::pile::PileType;
|
||||||
|
use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
|
||||||
|
|
||||||
// Drawing from a non-empty stock, and recycling a non-empty waste back to
|
// Drawing from a non-empty stock, and recycling a non-empty waste back to
|
||||||
// stock, are always legal moves in standard Klondike (unlimited recycles).
|
// stock, are always legal moves in standard Klondike (unlimited recycles).
|
||||||
@@ -1056,14 +1058,40 @@ pub fn has_legal_moves(game: &GameState) -> bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stock and waste both exhausted — delegate to the authoritative move
|
// Stock and waste exhausted — check whether any visible card can be placed.
|
||||||
// enumeration in core, which validates tableau sequence structure and
|
let mut sources: Vec<Card> = Vec::new();
|
||||||
// foundation placement correctly. The previous hand-rolled loop only
|
// Top waste card (waste is empty here, but included for completeness).
|
||||||
// checked can_place_on_tableau(card, dest) for individual face-up cards
|
if let Some(p) = game.piles.get(&PileType::Waste)
|
||||||
// without verifying that the cards above them form a valid alternating run,
|
&& let Some(top) = p.cards.last()
|
||||||
// causing false positives when a useful-looking card was buried under an
|
{
|
||||||
// invalid sequence.
|
sources.push(top.clone());
|
||||||
!game.possible_instructions().is_empty()
|
}
|
||||||
|
// Any face-up card in a tableau column can be the base of a movable run.
|
||||||
|
for i in 0..7_usize {
|
||||||
|
if let Some(t) = game.piles.get(&PileType::Tableau(i)) {
|
||||||
|
for card in t.cards.iter().filter(|c| c.face_up) {
|
||||||
|
sources.push(card.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for card in &sources {
|
||||||
|
for slot in 0..4_u8 {
|
||||||
|
if let Some(dest) = game.piles.get(&PileType::Foundation(slot))
|
||||||
|
&& can_place_on_foundation(card, dest)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i in 0..7_usize {
|
||||||
|
if let Some(dest) = game.piles.get(&PileType::Tableau(i))
|
||||||
|
&& can_place_on_tableau(card, dest)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// After each `StateChangedEvent`, check if the game has no legal moves.
|
/// After each `StateChangedEvent`, check if the game has no legal moves.
|
||||||
|
|||||||
@@ -734,12 +734,7 @@ fn end_drag(
|
|||||||
.is_some_and(|p| can_place_on_foundation(&bottom_card, p))
|
.is_some_and(|p| can_place_on_foundation(&bottom_card, p))
|
||||||
}
|
}
|
||||||
PileType::Tableau(_) => {
|
PileType::Tableau(_) => {
|
||||||
// Enforce the take-from-foundation rule at the input layer so the
|
game.0.piles.get(&target)
|
||||||
// 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))
|
.is_some_and(|p| can_place_on_tableau(&bottom_card, p))
|
||||||
}
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
@@ -993,12 +988,7 @@ fn touch_end_drag(
|
|||||||
.is_some_and(|p| can_place_on_foundation(&bottom_card, p))
|
.is_some_and(|p| can_place_on_foundation(&bottom_card, p))
|
||||||
}
|
}
|
||||||
PileType::Tableau(_) => {
|
PileType::Tableau(_) => {
|
||||||
// Enforce the take-from-foundation rule at the input layer so the
|
game.0.piles.get(&target)
|
||||||
// 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))
|
.is_some_and(|p| can_place_on_tableau(&bottom_card, p))
|
||||||
}
|
}
|
||||||
_ => false,
|
_ => 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.
|
// Pass 3 — suggest drawing from the stock when no other hint was found.
|
||||||
if hints.is_empty() {
|
if hints.is_empty() {
|
||||||
let stock_non_empty = game.piles.get(&PileType::Stock)
|
let stock_non_empty = game.piles.get(&PileType::Stock)
|
||||||
|
|||||||
@@ -355,67 +355,6 @@ main {
|
|||||||
animation: illegal-shake 320ms ease;
|
animation: illegal-shake 320ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── No-moves banner ─────────────────────────────────────────────────── */
|
|
||||||
|
|
||||||
#no-moves-banner {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 24px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 900;
|
|
||||||
animation: slide-up 240ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#no-moves-banner.hidden { display: none; }
|
|
||||||
|
|
||||||
.no-moves-card {
|
|
||||||
background: var(--panel);
|
|
||||||
border: 1px solid rgba(255,255,255,0.15);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 20px 32px;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
box-shadow: 0 8px 32px rgba(0,0,0,0.7);
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-moves-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-moves-detail {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-moves-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-moves-actions button.secondary {
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid rgba(255,255,255,0.2);
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-moves-actions button.secondary:hover {
|
|
||||||
background: rgba(255,255,255,0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slide-up {
|
|
||||||
from { opacity: 0; transform: translateX(-50%) translateY(12px); }
|
|
||||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Foundation slot suit hints ──────────────────────────────────────────── */
|
/* ── Foundation slot suit hints ──────────────────────────────────────────── */
|
||||||
|
|
||||||
.slot-hint {
|
.slot-hint {
|
||||||
|
|||||||
@@ -77,17 +77,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="no-moves-banner" class="hidden">
|
|
||||||
<div class="no-moves-card">
|
|
||||||
<div class="no-moves-title">No Moves Available</div>
|
|
||||||
<p class="no-moves-detail">No legal moves remain. Undo to go back or start a new game.</p>
|
|
||||||
<div class="no-moves-actions">
|
|
||||||
<button id="btn-no-moves-undo">↩ Undo</button>
|
|
||||||
<button id="btn-no-moves-new" class="secondary">↺ New Game</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module" src="/web/game.js"></script>
|
<script type="module" src="/web/game.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ const winScore = document.getElementById("win-score");
|
|||||||
const winMoves = document.getElementById("win-moves");
|
const winMoves = document.getElementById("win-moves");
|
||||||
const winTime = document.getElementById("win-time");
|
const winTime = document.getElementById("win-time");
|
||||||
const btnWinNew = document.getElementById("btn-win-new");
|
const btnWinNew = document.getElementById("btn-win-new");
|
||||||
const noMovesBanner = document.getElementById("no-moves-banner");
|
|
||||||
|
|
||||||
// ── Scale to fit ─────────────────────────────────────────────────────────────
|
// ── Scale to fit ─────────────────────────────────────────────────────────────
|
||||||
// Scales #card-area to fill #board without overflowing either dimension.
|
// Scales #card-area to fill #board without overflowing either dimension.
|
||||||
@@ -392,12 +391,9 @@ function render(s) {
|
|||||||
clearSave();
|
clearSave();
|
||||||
stopTimer();
|
stopTimer();
|
||||||
if (acTimer) { clearInterval(acTimer); acTimer = null; }
|
if (acTimer) { clearInterval(acTimer); acTimer = null; }
|
||||||
if (noMovesBanner) noMovesBanner.classList.add("hidden");
|
|
||||||
showWin(s);
|
showWin(s);
|
||||||
} else {
|
} else {
|
||||||
saveState();
|
saveState();
|
||||||
const noMoves = !s.has_moves && !s.is_auto_completable;
|
|
||||||
if (noMovesBanner) noMovesBanner.classList.toggle("hidden", !noMoves);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,8 +479,6 @@ function attachHandlers() {
|
|||||||
btnUndo.addEventListener("click", doUndo);
|
btnUndo.addEventListener("click", doUndo);
|
||||||
btnBoardUndo.addEventListener("click", doUndo);
|
btnBoardUndo.addEventListener("click", doUndo);
|
||||||
btnNew.addEventListener("click", () => startGame(randomSeed()));
|
btnNew.addEventListener("click", () => startGame(randomSeed()));
|
||||||
document.getElementById("btn-no-moves-undo")?.addEventListener("click", doUndo);
|
|
||||||
document.getElementById("btn-no-moves-new")?.addEventListener("click", () => startGame(randomSeed()));
|
|
||||||
btnWinNew.addEventListener("click", () => startGame(randomSeed()));
|
btnWinNew.addEventListener("click", () => startGame(randomSeed()));
|
||||||
chkDraw3.addEventListener("change", () => {
|
chkDraw3.addEventListener("change", () => {
|
||||||
drawThree = chkDraw3.checked;
|
drawThree = chkDraw3.checked;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* @ts-self-types="./solitaire_wasm.d.ts" */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browser-side replay state machine. Owns a live `GameState` and the
|
* Browser-side replay state machine. Owns a live `GameState` and the
|
||||||
* replay's move list; each `step()` applies the next move.
|
* 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.
|
* full pile snapshot at any time without mutating state.
|
||||||
*/
|
*/
|
||||||
export class SolitaireGame {
|
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() {
|
__destroy_into_raw() {
|
||||||
const ptr = this.__wbg_ptr;
|
const ptr = this.__wbg_ptr;
|
||||||
this.__wbg_ptr = 0;
|
this.__wbg_ptr = 0;
|
||||||
@@ -129,23 +125,6 @@ export class SolitaireGame {
|
|||||||
const ret = wasm.solitairegame_draw(this.__wbg_ptr);
|
const ret = wasm.solitairegame_draw(this.__wbg_ptr);
|
||||||
return ret;
|
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`.
|
* Move `count` cards from pile `from` to pile `to`.
|
||||||
*
|
*
|
||||||
@@ -188,31 +167,6 @@ export class SolitaireGame {
|
|||||||
const ret = wasm.solitairegame_seed(this.__wbg_ptr);
|
const ret = wasm.solitairegame_seed(this.__wbg_ptr);
|
||||||
return ret;
|
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.
|
* Full pile snapshot as a JS object.
|
||||||
*
|
*
|
||||||
|
|||||||
Binary file not shown.
@@ -241,8 +241,6 @@ pub struct GameSnapshot {
|
|||||||
pub move_count: u32,
|
pub move_count: u32,
|
||||||
pub is_won: bool,
|
pub is_won: bool,
|
||||||
pub is_auto_completable: bool,
|
pub is_auto_completable: bool,
|
||||||
/// `false` when stock, waste, and all pile-to-pile moves are exhausted.
|
|
||||||
pub has_moves: bool,
|
|
||||||
pub undo_count: u32,
|
pub undo_count: u32,
|
||||||
/// Number of snapshots currently on the undo stack; 0 means undo is unavailable.
|
/// Number of snapshots currently on the undo stack; 0 means undo is unavailable.
|
||||||
pub undo_stack_len: usize,
|
pub undo_stack_len: usize,
|
||||||
@@ -281,17 +279,11 @@ impl SolitaireGame {
|
|||||||
.map(|p| p.cards.iter().map(CardSnapshot::from).collect())
|
.map(|p| p.cards.iter().map(CardSnapshot::from).collect())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
};
|
};
|
||||||
let has_moves = {
|
|
||||||
let stock_empty = self.game.piles.get(&PileType::Stock).is_none_or(|p| p.cards.is_empty());
|
|
||||||
let waste_empty = self.game.piles.get(&PileType::Waste).is_none_or(|p| p.cards.is_empty());
|
|
||||||
!stock_empty || !waste_empty || !self.game.possible_instructions().is_empty()
|
|
||||||
};
|
|
||||||
GameSnapshot {
|
GameSnapshot {
|
||||||
score: self.game.score,
|
score: self.game.score,
|
||||||
move_count: self.game.move_count,
|
move_count: self.game.move_count,
|
||||||
is_won: self.game.is_won,
|
is_won: self.game.is_won,
|
||||||
is_auto_completable: self.game.is_auto_completable,
|
is_auto_completable: self.game.is_auto_completable,
|
||||||
has_moves,
|
|
||||||
undo_count: self.game.undo_count,
|
undo_count: self.game.undo_count,
|
||||||
undo_stack_len: self.game.undo_stack_len(),
|
undo_stack_len: self.game.undo_stack_len(),
|
||||||
stock: cards(PileType::Stock),
|
stock: cards(PileType::Stock),
|
||||||
|
|||||||
Reference in New Issue
Block a user