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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -136,12 +136,11 @@ const btnBoardUndo = document.getElementById("btn-board-undo");
|
|||||||
const btnNew = document.getElementById("btn-new");
|
const btnNew = document.getElementById("btn-new");
|
||||||
const chkDraw3 = document.getElementById("chk-draw3");
|
const chkDraw3 = document.getElementById("chk-draw3");
|
||||||
const btnTheme = document.getElementById("btn-theme");
|
const btnTheme = document.getElementById("btn-theme");
|
||||||
const winOverlay = document.getElementById("win-overlay");
|
const winOverlay = document.getElementById("win-overlay");
|
||||||
const winScore = document.getElementById("win-score");
|
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;
|
||||||
|
|||||||
@@ -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