solitaire moves hist and undo and hard mode

This commit is contained in:
Milo
2025-08-26 17:25:20 +02:00
parent 4f143aa80b
commit 50f2b8f96d
2 changed files with 179 additions and 4 deletions

View File

@@ -212,10 +212,25 @@ export function moveCard(gameState, moveData) {
// Add the stack to the destination pile.
destPile.push(...cardsToMove);
const histMove = {
move: 'move',
sourcePileType: sourcePileType,
sourcePileIndex: sourcePileIndex,
sourceCardIndex: sourceCardIndex,
destPileType: destPileType,
destPileIndex: destPileIndex,
cardsMoved: cardsToMove,
cardWasFlipped: false,
points: destPileType === 'foundationPiles' ? 11 : 1 // Points for moving to foundation
}
// If the source was a tableau pile and there are cards left, flip the new top card.
if (sourcePileType === 'tableauPiles' && sourcePile.length > 0) {
sourcePile[sourcePile.length - 1].faceUp = true;
histMove.cardWasFlipped = true;
}
gameState.hist.push(histMove)
}
/**
@@ -227,14 +242,50 @@ export function drawCard(gameState) {
const card = gameState.stockPile.pop();
card.faceUp = true;
gameState.wastePile.push(card);
gameState.hist.push({
move: 'draw',
card: card
})
} else if (gameState.wastePile.length > 0) {
// When stock is empty, move the entire waste pile back to stock, face down.
gameState.stockPile = gameState.wastePile.reverse();
gameState.stockPile.forEach(card => (card.faceUp = false));
gameState.wastePile = [];
gameState.hist.push({
move: 'draw-reset',
})
}
}
export function draw3Cards(gameState) {
if (gameState.stockPile.length > 0) {
let cards = []
for (let i = 0; i < 3; i++) {
if (gameState.stockPile.length > 0) {
const card = gameState.stockPile.pop();
card.faceUp = true;
gameState.wastePile.push(card);
cards.push(card);
} else {
break; // Stop if stock runs out
}
}
gameState.hist.push({
move: 'draw-3',
cards: cards,
})
} else if (gameState.wastePile.length > 0) {
// When stock is empty, move the entire waste pile back to stock, face down.
gameState.stockPile = gameState.wastePile.reverse();
gameState.stockPile.forEach(card => (card.faceUp = false));
gameState.wastePile = [];
gameState.hist.push({
move: 'draw-reset',
})
}
}
/**
* Checks if the game has been won (all 52 cards are in the foundation piles).
* @param {Object} gameState - The current state of the game.
@@ -243,4 +294,106 @@ export function drawCard(gameState) {
export function checkWinCondition(gameState) {
const foundationCardCount = gameState.foundationPiles.reduce((acc, pile) => acc + pile.length, 0);
return foundationCardCount === 52;
}
/**
* Reverts the game state to its previous state based on the last move in the history.
* This function mutates the gameState object directly.
* @param {Object} gameState - The current game state, which includes a `hist` array.
*/
export function undoMove(gameState) {
if (!gameState.hist || gameState.hist.length === 0) {
console.log("No moves to undo.");
return; // Nothing to undo
}
const lastMove = gameState.hist.pop(); // Get and remove the last move from history
gameState.moves++; // Undoing a move counts as a new move
gameState.score -= lastMove.points || 1; // Revert score based on points from the last move
switch (lastMove.move) {
case 'move':
undoCardMove(gameState, lastMove);
break;
case 'draw':
undoDraw(gameState, lastMove);
break;
case 'draw-3':
undoDraw3(gameState, lastMove);
break;
case 'draw-reset':
undoDrawReset(gameState, lastMove);
break;
default:
// If an unknown move type is found, push it back to avoid corrupting the history
gameState.hist.push(lastMove);
gameState.moves--; // Revert the move count increment
gameState.score += lastMove.points || 1; // Revert the score decrement
console.error("Unknown move type in history:", lastMove);
break;
}
}
// --- Helper functions for undoing specific moves ---
function undoCardMove(gameState, moveData) {
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex, cardsMoved, cardWasFlipped } = moveData;
// 1. Find the destination pile (where the cards are NOW)
let currentPile;
if (destPileType === 'tableauPiles') currentPile = gameState.tableauPiles[destPileIndex];
else if (destPileType === 'foundationPiles') currentPile = gameState.foundationPiles[destPileIndex];
// 2. Remove the moved cards from their current pile
// Using splice with a negative index removes from the end of the array
currentPile.splice(-cardsMoved.length);
// 3. Find the original source pile
let originalPile;
if (sourcePileType === 'tableauPiles') originalPile = gameState.tableauPiles[sourcePileIndex];
else if (sourcePileType === 'wastePile') originalPile = gameState.wastePile;
else if (sourcePileType === 'foundationPiles') originalPile = gameState.foundationPiles[sourcePileIndex];
// 4. Put the cards back where they came from
// Using splice to insert the cards back at their original index
originalPile.splice(sourceCardIndex, 0, ...cardsMoved);
// 5. If a card was flipped during the move, flip it back to face-down
if (cardWasFlipped) {
const cardToUnflip = originalPile[sourceCardIndex - 1];
if (cardToUnflip) {
cardToUnflip.faceUp = false;
}
}
}
function undoDraw(gameState, moveData) {
// A 'draw' move means a card went from stock to waste.
// To undo, move it from waste back to stock and flip it face-down.
const cardToReturn = gameState.wastePile.pop();
if (cardToReturn) {
cardToReturn.faceUp = false;
gameState.stockPile.push(cardToReturn);
}
}
function undoDraw3(gameState, moveData) {
// A 'draw-3' move means up to 3 cards went from stock to
// waste. To undo, move them back to stock and flip them face-down.
const cardsToReturn = moveData.cards || [];
for (let i = 0; i < cardsToReturn.length; i++) {
const card = gameState.wastePile.pop();
if (card) {
card.faceUp = false;
gameState.stockPile.push(card);
}
}
}
function undoDrawReset(gameState, moveData) {
// A 'draw-reset' means the waste pile was moved to the stock pile.
// To undo, move the stock pile back to the waste pile and flip cards face-up.
gameState.wastePile = gameState.stockPile.reverse();
gameState.wastePile.forEach(card => (card.faceUp = true));
gameState.stockPile = [];
}

View File

@@ -3,7 +3,7 @@ import express from 'express';
// --- Game Logic Imports ---
import {
createDeck, shuffle, deal, isValidMove, moveCard, drawCard,
checkWinCondition, createSeededRNG, seededShuffle
checkWinCondition, createSeededRNG, seededShuffle, undoMove, draw3Cards
} from '../../game/solitaire.js';
// --- Game State & Database Imports ---
@@ -28,7 +28,7 @@ export function solitaireRoutes(client, io) {
// --- Game Initialization Endpoints ---
router.post('/start', (req, res) => {
const { userId, userSeed } = req.body;
const { userId, userSeed, hardMode } = req.body;
if (!userId) return res.status(400).json({ error: 'User ID is required.' });
// If a game already exists for the user, return it instead of creating a new one.
@@ -56,6 +56,10 @@ export function solitaireRoutes(client, io) {
const gameState = deal(deck);
gameState.seed = seed;
gameState.isSOTD = false;
gameState.score = 0;
gameState.moves = 0;
gameState.hist = [];
gameState.hardMode = hardMode ?? false;
activeSolitaireGames[userId] = gameState;
res.json({ success: true, gameState });
@@ -88,6 +92,8 @@ export function solitaireRoutes(client, io) {
moves: 0,
score: 0,
seed: sotd.seed,
hist: [],
hardMode: false,
};
activeSolitaireGames[userId] = gameState;
@@ -152,11 +158,27 @@ export function solitaireRoutes(client, io) {
if (!gameState) return res.status(404).json({ error: 'Game not found.' });
if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'});
drawCard(gameState);
if (gameState.hardMode) {
draw3Cards(gameState);
} else {
drawCard(gameState);
}
updateGameStats(gameState, 'draw');
res.json({ success: true, gameState });
});
router.post('/undo', (req, res) => {
const { userId } = req.body;
const gameState = activeSolitaireGames[userId];
if (!gameState) return res.status(404).json({ error: 'Game not found.' });
if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'});
if (gameState.hist.length === 0) return res.status(400).json({ error: 'No moves to undo.'});
undoMove(gameState);
res.json({ success: true, gameState });
})
return router;
}
@@ -165,7 +187,7 @@ export function solitaireRoutes(client, io) {
/** Updates game stats like moves and score after an action. */
function updateGameStats(gameState, actionType, moveData = {}) {
if (!gameState.isSOTD) return; // Only track stats for SOTD
// if (!gameState.isSOTD) return; // Only track stats for SOTD
gameState.moves++;
if (actionType === 'move') {