mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-03-18 21:40:27 +01:00
228
game.js
228
game.js
@@ -366,4 +366,232 @@ export function formatConnect4BoardForDiscord(board) {
|
||||
null: '⚪'
|
||||
};
|
||||
return board.map(row => row.map(cell => symbols[cell]).join('')).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffles an array in place using the Fisher-Yates algorithm.
|
||||
* @param {Array} array - The array to shuffle.
|
||||
* @returns {Array} The shuffled array.
|
||||
*/
|
||||
export function shuffle(array) {
|
||||
let currentIndex = array.length,
|
||||
randomIndex;
|
||||
while (currentIndex !== 0) {
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deals a shuffled deck into the initial Solitaire game state.
|
||||
* @param {Array} deck - A shuffled deck of cards.
|
||||
* @returns {Object} The initial gameState object.
|
||||
*/
|
||||
export function deal(deck) {
|
||||
const gameState = {
|
||||
tableauPiles: [[], [], [], [], [], [], []],
|
||||
foundationPiles: [[], [], [], []],
|
||||
stockPile: [],
|
||||
wastePile: [],
|
||||
};
|
||||
|
||||
// Deal cards to the tableau piles
|
||||
for (let i = 0; i < 7; i++) {
|
||||
for (let j = i; j < 7; j++) {
|
||||
gameState.tableauPiles[j].push(deck.shift());
|
||||
}
|
||||
}
|
||||
|
||||
// Flip the top card of each tableau pile
|
||||
gameState.tableauPiles.forEach(pile => {
|
||||
if (pile.length > 0) {
|
||||
pile[pile.length - 1].faceUp = true;
|
||||
}
|
||||
});
|
||||
|
||||
// The rest of the deck becomes the stock
|
||||
gameState.stockPile = deck;
|
||||
|
||||
return gameState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a proposed move is valid according to the rules of Klondike Solitaire.
|
||||
* @param {Object} gameState - The current state of the game.
|
||||
* @param {Object} moveData - The details of the move.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isValidMove(gameState, moveData) {
|
||||
// Use more descriptive names to avoid confusion
|
||||
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
|
||||
|
||||
let sourcePile;
|
||||
// Get the actual source pile array based on its type and index
|
||||
if (sourcePileType === 'tableauPiles') {
|
||||
sourcePile = gameState.tableauPiles[sourcePileIndex];
|
||||
} else if (sourcePileType === 'wastePile') {
|
||||
sourcePile = gameState.wastePile;
|
||||
} else {
|
||||
return false; // Cannot drag from foundation or stock
|
||||
}
|
||||
|
||||
// Get the actual card being dragged (the top of the stack)
|
||||
const sourceCard = sourcePile[sourceCardIndex];
|
||||
|
||||
// A card must exist and be face-up to be moved
|
||||
if (!sourceCard || !sourceCard.faceUp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Validate move TO a Tableau Pile ---
|
||||
if (destPileType === 'tableauPiles') {
|
||||
const destinationPile = gameState.tableauPiles[destPileIndex];
|
||||
const topCard = destinationPile.length > 0 ? destinationPile[destinationPile.length - 1] : null;
|
||||
|
||||
if (!topCard) {
|
||||
// If the destination tableau pile is empty, only a King can be moved there.
|
||||
return sourceCard.rank === 'K';
|
||||
}
|
||||
|
||||
// If the destination pile is not empty, check game rules
|
||||
const sourceColor = getCardColor(sourceCard.suit);
|
||||
const destColor = getCardColor(topCard.suit);
|
||||
const sourceValue = getRankValue(sourceCard.rank);
|
||||
const destValue = getRankValue(topCard.rank);
|
||||
|
||||
// Card being moved must be opposite color and one rank lower than the destination top card.
|
||||
return sourceColor !== destColor && destValue - sourceValue === 1;
|
||||
}
|
||||
|
||||
// --- Validate move TO a Foundation Pile ---
|
||||
if (destPileType === 'foundationPiles') {
|
||||
// You can only move one card at a time to a foundation pile.
|
||||
const stackBeingMoved = sourcePile.slice(sourceCardIndex);
|
||||
if (stackBeingMoved.length > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const destinationPile = gameState.foundationPiles[destPileIndex];
|
||||
const topCard = destinationPile.length > 0 ? destinationPile[destinationPile.length - 1] : null;
|
||||
|
||||
if (!topCard) {
|
||||
// If the foundation is empty, only an Ace can be moved there.
|
||||
return sourceCard.rank === 'A';
|
||||
}
|
||||
|
||||
// If not empty, card must be same suit and one rank higher.
|
||||
const sourceValue = getRankValue(sourceCard.rank);
|
||||
const destValue = getRankValue(topCard.rank);
|
||||
|
||||
return sourceCard.suit === topCard.suit && sourceValue - destValue === 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of suits and ranks to create a deck.
|
||||
*/
|
||||
const SUITS = ['h', 'd', 's', 'c']; // Hearts, Diamonds, Spades, Clubs
|
||||
const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K'];
|
||||
|
||||
/**
|
||||
* Gets the numerical value of a card's rank.
|
||||
* @param {string} rank - e.g., 'A', 'K', '7'
|
||||
* @returns {number}
|
||||
*/
|
||||
function getRankValue(rank) {
|
||||
if (rank === 'A') return 1;
|
||||
if (rank === 'T') return 10;
|
||||
if (rank === 'J') return 11;
|
||||
if (rank === 'Q') return 12;
|
||||
if (rank === 'K') return 13;
|
||||
return parseInt(rank, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the color of a card's suit.
|
||||
* @param {string} suit - e.g., 'h', 's'
|
||||
* @returns {string} 'red' or 'black'
|
||||
*/
|
||||
function getCardColor(suit) {
|
||||
return suit === 'h' || suit === 'd' ? 'red' : 'black';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standard 52-card deck.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function createDeck() {
|
||||
const deck = [];
|
||||
for (const suit of SUITS) {
|
||||
for (const rank of RANKS) {
|
||||
deck.push({ suit, rank, faceUp: false });
|
||||
}
|
||||
}
|
||||
return deck;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutates the game state by performing a valid move, correctly handling stacks.
|
||||
* @param {Object} gameState - The current state of the game.
|
||||
* @param {Object} moveData - The details of the move.
|
||||
*/
|
||||
export function moveCard(gameState, moveData) {
|
||||
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
|
||||
|
||||
// Identify the source pile array
|
||||
const sourcePile = sourcePileType === 'tableauPiles'
|
||||
? gameState.tableauPiles[sourcePileIndex]
|
||||
: gameState.wastePile;
|
||||
|
||||
// Identify the destination pile array
|
||||
const destPile = destPileType === 'tableauPiles'
|
||||
? gameState.tableauPiles[destPileIndex]
|
||||
: gameState.foundationPiles[destPileIndex];
|
||||
|
||||
// Using splice(), cut the entire stack of cards to be moved from the source pile.
|
||||
const cardsToMove = sourcePile.splice(sourceCardIndex);
|
||||
|
||||
// Add the stack of cards to the destination pile.
|
||||
// Using the spread operator (...) to add all items from the cardsToMove array.
|
||||
destPile.push(...cardsToMove);
|
||||
|
||||
// After moving, if the source was a tableau pile and it's not empty,
|
||||
// flip the new top card to be face-up.
|
||||
if (sourcePileType === 'tableauPiles' && sourcePile.length > 0) {
|
||||
sourcePile[sourcePile.length - 1].faceUp = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a card from the stock to the waste pile. If stock is empty, resets it from the waste.
|
||||
* @param {Object} gameState - The current state of the game.
|
||||
*/
|
||||
export function drawCard(gameState) {
|
||||
if (gameState.stockPile.length > 0) {
|
||||
const card = gameState.stockPile.pop();
|
||||
card.faceUp = true;
|
||||
gameState.wastePile.push(card);
|
||||
} else if (gameState.wastePile.length > 0) {
|
||||
// When stock is empty, move waste pile back to stock, face down
|
||||
gameState.stockPile = gameState.wastePile.reverse();
|
||||
gameState.stockPile.forEach(card => (card.faceUp = false));
|
||||
gameState.wastePile = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the game has been won (all cards are in the foundation piles).
|
||||
* @param {Object} gameState - The current state of the game.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function checkWinCondition(gameState) {
|
||||
const foundationCardCount = gameState.foundationPiles.reduce(
|
||||
(acc, pile) => acc + pile.length,
|
||||
0
|
||||
);
|
||||
return foundationCardCount === 52;
|
||||
}
|
||||
76
index.js
76
index.js
@@ -26,7 +26,8 @@ import {
|
||||
eloHandler, formatConnect4BoardForDiscord,
|
||||
pokerEloHandler,
|
||||
randomSkinPrice,
|
||||
slowmodesHandler
|
||||
slowmodesHandler,
|
||||
deal, isValidMove, moveCard, shuffle, drawCard, checkWinCondition, createDeck,
|
||||
} from './game.js';
|
||||
import { Client, GatewayIntentBits, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
|
||||
import cron from 'node-cron';
|
||||
@@ -74,6 +75,7 @@ app.use((req, res, next) => {
|
||||
});
|
||||
// To keep track of our active games
|
||||
const activeGames = {};
|
||||
const activeSolitaireGames = {};
|
||||
const activePolls = {};
|
||||
const activeInventories = {};
|
||||
const activeSearchs = {};
|
||||
@@ -4446,6 +4448,78 @@ async function updatePokerPlayersSolve(roomId) {
|
||||
}
|
||||
}
|
||||
|
||||
app.post('/solitaire/start', async (req, res) => {
|
||||
const userId = req.body.userId;
|
||||
const deck = shuffle(createDeck());
|
||||
const gameState = deal(deck);
|
||||
activeSolitaireGames[userId] = gameState
|
||||
res.json({ success: true, gameState });
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /solitaire/state/:userId
|
||||
* Gets the current game state for a user. If no game exists, creates a new one.
|
||||
*/
|
||||
app.get('/solitaire/state/:userId', (req, res) => {
|
||||
const { userId } = req.params;
|
||||
let gameState = activeSolitaireGames[userId];
|
||||
if (!gameState) {
|
||||
console.log(`Creating new Solitaire game for user: ${userId}`);
|
||||
const deck = shuffle(createDeck());
|
||||
gameState = deal(deck);
|
||||
activeSolitaireGames[userId] = gameState;
|
||||
}
|
||||
res.json({ success: true, gameState });
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /solitaire/move
|
||||
* Receives all necessary move data from the frontend.
|
||||
*/
|
||||
app.post('/solitaire/move', (req, res) => {
|
||||
// Destructure the complete move data from the request body
|
||||
// Frontend must send all these properties.
|
||||
const {
|
||||
userId,
|
||||
sourcePileType,
|
||||
sourcePileIndex,
|
||||
sourceCardIndex,
|
||||
destPileType,
|
||||
destPileIndex
|
||||
} = req.body;
|
||||
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
|
||||
if (!gameState) {
|
||||
return res.status(404).json({ error: 'Game not found for this user.' });
|
||||
}
|
||||
|
||||
// Pass the entire data object to the validation function
|
||||
if (isValidMove(gameState, req.body)) {
|
||||
// If valid, mutate the state
|
||||
moveCard(gameState, req.body);
|
||||
const win = checkWinCondition(gameState);
|
||||
res.json({ success: true, gameState, win });
|
||||
} else {
|
||||
// If the move is invalid, send a specific error message
|
||||
res.status(400).json({ error: 'Invalid move' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /solitaire/draw
|
||||
* Draws a card from the stock pile to the waste pile.
|
||||
*/
|
||||
app.post('/solitaire/draw', (req, res) => {
|
||||
const { userId } = req.body;
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
if (!gameState) {
|
||||
return res.status(404).json({ error: `Game not found for user ${userId}` });
|
||||
}
|
||||
drawCard(gameState);
|
||||
res.json({ success: true, gameState });
|
||||
});
|
||||
|
||||
import http from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import * as test from "node:test";
|
||||
|
||||
Reference in New Issue
Block a user