From 50f2b8f96d96dccaf189cdfd2f15d78d7ac987ac Mon Sep 17 00:00:00 2001 From: Milo Date: Tue, 26 Aug 2025 17:25:20 +0200 Subject: [PATCH] solitaire moves hist and undo and hard mode --- src/game/solitaire.js | 153 +++++++++++++++++++++++++++++++++ src/server/routes/solitaire.js | 30 ++++++- 2 files changed, 179 insertions(+), 4 deletions(-) diff --git a/src/game/solitaire.js b/src/game/solitaire.js index 50e8250..faf0e82 100644 --- a/src/game/solitaire.js +++ b/src/game/solitaire.js @@ -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 = []; } \ No newline at end of file diff --git a/src/server/routes/solitaire.js b/src/server/routes/solitaire.js index ff8d77d..b67d97b 100644 --- a/src/server/routes/solitaire.js +++ b/src/server/routes/solitaire.js @@ -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') {