Merge pull request #42 from cassoule/milo-250725

Milo 250725
This commit is contained in:
Milo Gourvest
2025-07-28 15:11:41 +02:00
committed by GitHub
4 changed files with 306 additions and 39 deletions

View File

@@ -9,7 +9,7 @@ function createTimesChoices() {
for (let choice of choices) {
commandChoices.push({
name: capitalize(choice.name),
value: choice.value.toString(),
value: choice.value?.toString(),
});
}

115
game.js
View File

@@ -10,7 +10,7 @@ import {
getUserElo,
insertElos,
updateElo,
getAllSkins
getAllSkins, deleteSOTD, insertSOTD, clearSOTDStats, getAllSOTDStats
} from './init_database.js'
import {C4_COLS, C4_ROWS, skins} from "./index.js";
@@ -164,14 +164,14 @@ export async function eloHandler(p1, p2, p1score, p2score, type) {
if (!p1elo) {
await insertElos.run({
id: p1.toString(),
id: p1?.toString(),
elo: 100,
})
p1elo = await getUserElo.get({ id: p1 })
}
if (!p2elo) {
await insertElos.run({
id: p2.toString(),
id: p2?.toString(),
elo: 100,
})
p2elo = await getUserElo.get({ id: p2 })
@@ -179,7 +179,7 @@ export async function eloHandler(p1, p2, p1score, p2score, type) {
if (p1score === p2score) {
insertGame.run({
id: p1.toString() + '-' + p2.toString() + '-' + Date.now().toString(),
id: p1?.toString() + '-' + p2?.toString() + '-' + Date.now()?.toString(),
p1: p1,
p2: p2,
p1_score: p1score,
@@ -206,7 +206,7 @@ export async function eloHandler(p1, p2, p1score, p2score, type) {
updateElo.run({ id: p2, elo: p2newElo })
insertGame.run({
id: p1.toString() + '-' + p2.toString() + '-' + Date.now().toString(),
id: p1?.toString() + '-' + p2?.toString() + '-' + Date.now()?.toString(),
p1: p1,
p2: p2,
p1_score: p1score,
@@ -263,7 +263,7 @@ export async function pokerEloHandler(room) {
updateElo.run({ id: player.id, elo: newElo })
insertGame.run({
id: player.id + '-' + Date.now().toString(),
id: player.id + '-' + Date.now()?.toString(),
p1: player.id,
p2: null,
p1_score: actualScore,
@@ -368,6 +368,20 @@ export function formatConnect4BoardForDiscord(board) {
return board.map(row => row.map(cell => symbols[cell]).join('')).join('\n');
}
/**
* Creates a seedable pseudorandom number generator (PRNG) using the Mulberry32 algorithm.
* @param {number} seed - An initial number to seed the generator.
* @returns {function} A function that, when called, returns a pseudorandom number between 0 and 1.
*/
export function createSeededRNG(seed) {
return function() {
let t = seed += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
}
}
/**
* Shuffles an array in place using the Fisher-Yates algorithm.
* @param {Array} array - The array to shuffle.
@@ -384,6 +398,31 @@ export function shuffle(array) {
return array;
}
/**
* Shuffles an array in place using a seedable PRNG via the Fisher-Yates algorithm.
* @param {Array} array - The array to shuffle.
* @param {function} rng - A seedable random number generator function.
* @returns {Array} The shuffled array.
*/
export function seededShuffle(array, rng) {
let currentIndex = array.length,
randomIndex;
// While there remain elements to shuffle.
while (currentIndex !== 0) {
// Pick a remaining element using the seeded RNG.
randomIndex = Math.floor(rng() * currentIndex);
currentIndex--;
// And swap it with the current element.
[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.
@@ -542,7 +581,7 @@ export function createDeck() {
* @param {Object} gameState - The current state of the game.
* @param {Object} moveData - The details of the move.
*/
export function moveCard(gameState, moveData) {
export async function moveCard(gameState, moveData) {
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
// Identify the source pile array
@@ -570,6 +609,14 @@ export function moveCard(gameState, moveData) {
// Using the spread operator (...) to add all items from the cardsToMove array.
destPile.push(...cardsToMove);
if (sourcePileType === 'foundationPiles') {
sotdMoveUpdate(gameState, -15)
} else if (destPileType === 'foundationPiles') {
sotdMoveUpdate(gameState, 10)
} else {
sotdMoveUpdate(gameState, 0)
}
// 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) {
@@ -581,7 +628,7 @@ export function moveCard(gameState, moveData) {
* 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) {
export async function drawCard(gameState) {
if (gameState.stockPile.length > 0) {
const card = gameState.stockPile.pop();
card.faceUp = true;
@@ -592,6 +639,7 @@ export function drawCard(gameState) {
gameState.stockPile.forEach(card => (card.faceUp = false));
gameState.wastePile = [];
}
sotdMoveUpdate(gameState, 0)
}
/**
@@ -605,4 +653,55 @@ export function checkWinCondition(gameState) {
0
);
return foundationCardCount === 52;
}
export function initTodaysSOTD() {
const rankings = getAllSOTDStats.all()
const firstPlaceId = rankings > 0 ? rankings[0].user_id : null
if (firstPlaceId) {
const firstPlaceUser = getUser.get(firstPlaceId)
if (firstPlaceUser) {
updateUserCoins.run({ id: firstPlaceId, coins: firstPlaceUser.coins + 1000 });
insertLog.run({
id: firstPlaceId + '-' + Date.now(),
user_id: firstPlaceId,
action: 'SOTD_FIRST_PLACE',
target_user_id: null,
coins_amount: 1000,
user_new_amount: firstPlaceUser.coins + 1000,
})
}
}
const newRandomSeed = Date.now().toString(36) + Math.random().toString(36).substr(2);
let numericSeed = 0;
for (let i = 0; i < newRandomSeed.length; i++) {
numericSeed = (numericSeed + newRandomSeed.charCodeAt(i)) & 0xFFFFFFFF;
}
const rng = createSeededRNG(numericSeed);
const deck = createDeck();
const shuffledDeck = seededShuffle(deck, rng);
const todaysSOTD = deal(shuffledDeck);
todaysSOTD.seed = newRandomSeed;
clearSOTDStats.run()
deleteSOTD.run()
insertSOTD.run({
id: 0,
tableauPiles: JSON.stringify(todaysSOTD.tableauPiles),
foundationPiles: JSON.stringify(todaysSOTD.foundationPiles),
stockPile: JSON.stringify(todaysSOTD.stockPile),
wastePile: JSON.stringify(todaysSOTD.wastePile),
seed: todaysSOTD.seed,
})
console.log('Today\'s SOTD is ready')
}
export function sotdMoveUpdate(gameState, points) {
if (gameState.isSOTD) {
gameState.moves++
gameState.score += points
}
}

192
index.js
View File

@@ -27,7 +27,7 @@ import {
pokerEloHandler,
randomSkinPrice,
slowmodesHandler,
deal, isValidMove, moveCard, shuffle, drawCard, checkWinCondition, createDeck,
deal, isValidMove, moveCard, seededShuffle, drawCard, checkWinCondition, createDeck, initTodaysSOTD, createSeededRNG,
} from './game.js';
import { Client, GatewayIntentBits, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
import cron from 'node-cron';
@@ -50,9 +50,19 @@ import {
getSkin,
getAllAvailableSkins,
getUserInventory,
getTopSkins, updateUserCoins,
insertLog, stmtLogs,
getLogs, getUserLogs, getUserElo, getUserGames, getUsersByElo, resetDailyReward, queryDailyReward,
getTopSkins,
updateUserCoins,
insertLog,
stmtLogs,
getLogs,
getUserLogs,
getUserElo,
getUserGames,
getUsersByElo,
resetDailyReward,
queryDailyReward,
deleteSOTD,
insertSOTD, getSOTD, insertSOTDStats, deleteUserSOTDStats, getUserSOTDStats, getAllSOTDStats,
} from './init_database.js';
import { getValorantSkins, getSkinTiers } from './valo.js';
import {sleep} from "openai/core";
@@ -81,7 +91,7 @@ const activeInventories = {};
const activeSearchs = {};
const activeSlowmodes = {};
const activePredis = {};
let todaysHydrateCron = ''
let todaysSOTD = {};
const SPAM_INTERVAL = process.env.SPAM_INTERVAL
const client = new Client({
@@ -548,6 +558,9 @@ client.on('messageCreate', async (message) => {
}
console.log(`Result for ${amount} skins`)
}
else if (message.content.toLowerCase().startsWith('?sotd')) {
initTodaysSOTD()
}
else if (message.author.id === process.env.DEV_ID) {
const prefix = process.env.DEV_SITE === 'true' ? 'dev' : 'flopo'
if (message.content === prefix + ':add-coins-to-users') {
@@ -635,10 +648,6 @@ client.on('messageCreate', async (message) => {
client.once('ready', async () => {
console.log(`Logged in as ${client.user.tag}`);
console.log(`[Connected with ${FLAPI_URL}]`)
const randomMinute = Math.floor(Math.random() * 60);
const randomHour = Math.floor(Math.random() * (18 - 8 + 1)) + 8;
todaysHydrateCron = `${randomMinute} ${randomHour} * * *`
console.log(todaysHydrateCron)
await getAkhys();
console.log('FlopoBOT marked as ready')
@@ -683,13 +692,8 @@ client.once('ready', async () => {
}
});
// ─── 💀 Midnight Chaos Timer ──────────────────────
// at midnight
cron.schedule(process.env.CRON_EXPR, async () => {
const randomMinute = Math.floor(Math.random() * 60);
const randomHour = Math.floor(Math.random() * (18 - 8 + 1)) + 8;
todaysHydrateCron = `${randomMinute} ${randomHour} * * *`
console.log(todaysHydrateCron)
try {
const akhys = getAllUsers.all()
akhys.forEach((akhy) => {
@@ -698,6 +702,8 @@ client.once('ready', async () => {
} catch (e) {
console.log(e)
}
initTodaysSOTD()
});
// users/skins dayly fetch at 7am
@@ -3148,7 +3154,7 @@ app.post('/slowmode', async (req, res) => {
return res.status(200).json({ message: 'Slowmode retiré'})
} else {
let timeLeft = (activeSlowmodes[userId].endAt - Date.now())/1000
timeLeft = timeLeft > 60 ? (timeLeft/60).toFixed().toString() + 'min' : timeLeft.toFixed().toString() + 'sec'
timeLeft = timeLeft > 60 ? (timeLeft/60).toFixed()?.toString() + 'min' : timeLeft.toFixed()?.toString() + 'sec'
return res.status(403).json({ message: `${user.globalName} est déjà en slowmode (${timeLeft})`})
}
} else if (userId === commandUserId) {
@@ -3196,7 +3202,7 @@ app.post('/start-predi', async (req, res) => {
}
const startTime = Date.now()
const newPrediId = commandUserId.toString() + '-' + startTime.toString()
const newPrediId = commandUserId?.toString() + '-' + startTime?.toString()
let msgId;
try {
@@ -4448,16 +4454,102 @@ async function updatePokerPlayersSolve(roomId) {
for (const playerId in pokerRooms[roomId].players) {
const player = pokerRooms[roomId].players[playerId]
let fullHand = pokerRooms[roomId].tapis
player.solve = Hand.solve(fullHand.concat(player.hand), 'standard', false)?.descr
if (!fullHand && !player.hand) {
player.solve = Hand.solve([], 'standard', false)?.descr
} else if (!fullHand) {
player.solve = Hand.solve(player.hand, 'standard', false)?.descr
} else if (!player.hand) {
player.solve = Hand.solve(fullHand, 'standard', false)?.descr
} else {
player.solve = Hand.solve(fullHand.concat(player.hand), 'standard', false)?.descr
}
}
}
app.get('/solitaire/sotd/rankings', async (req, res) => {
const rankings = getAllSOTDStats.all()
return res.json({ rankings })
})
app.post('/solitaire/start', async (req, res) => {
const userId = req.body.userId;
const deck = shuffle(createDeck());
const gameState = deal(deck);
let userSeed = req.body.userSeed;
if (activeSolitaireGames[userId] && !activeSolitaireGames[userId].isSOTD) {
return res.json({ succes: true, gameState: activeSolitaireGames[userId]})
}
if (userSeed) {
let numericSeed = 0
for (let i = 0; i < userSeed.length; i++) {
numericSeed = (numericSeed + userSeed.charCodeAt(i)) & 0xFFFFFFFF;
}
const rng = createSeededRNG(numericSeed);
const deck = createDeck()
const shuffledDeck = seededShuffle(deck, rng);
const gameState = deal(shuffledDeck);
gameState.seed = userSeed;
activeSolitaireGames[userId] = gameState;
return res.json({ success: true, gameState });
} else {
const newRandomSeed = Date.now()?.toString(36) + Math.random()?.toString(36).substr(2);
let numericSeed = 0;
for (let i = 0; i < newRandomSeed.length; i++) {
numericSeed = (numericSeed + newRandomSeed.charCodeAt(i)) & 0xFFFFFFFF;
}
const rng = createSeededRNG(numericSeed);
const deck = createDeck();
const shuffledDeck = seededShuffle(deck, rng);
const gameState = deal(shuffledDeck);
gameState.seed = newRandomSeed;
activeSolitaireGames[userId] = gameState;
return res.json({ success: true, gameState });
}
});
app.post('/solitaire/start/sotd', async (req, res) => {
const userId = req.body.userId
const sotd = getSOTD.get();
const user = getUser.get(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
if (activeSolitaireGames[userId] && activeSolitaireGames[userId].isSOTD) {
return res.json({ success: true, gameState: activeSolitaireGames[userId]})
}
const gameState = {
tableauPiles: JSON.parse(sotd.tableauPiles),
foundationPiles: JSON.parse(sotd.foundationPiles),
stockPile: JSON.parse(sotd.stockPile),
wastePile: JSON.parse(sotd.wastePile),
isDone: false,
isSOTD: true,
hasFinToday: false,
startTime: Date.now(),
endTime: null,
moves: 0,
score: 0,
seed: sotd.seed,
}
activeSolitaireGames[userId] = gameState
res.json({ success: true, gameState });
})
app.post('/solitaire/reset', async (req, res) => {
const userId = req.body.userId;
delete activeSolitaireGames[userId]
res.json({ success: true });
});
/**
@@ -4467,12 +4559,11 @@ app.post('/solitaire/start', async (req, res) => {
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}`);
/*if (!gameState) {
const deck = shuffle(createDeck());
gameState = deal(deck);
activeSolitaireGames[userId] = gameState;
}
}*/
res.json({ success: true, gameState });
});
@@ -4480,7 +4571,7 @@ app.get('/solitaire/state/:userId', (req, res) => {
* POST /solitaire/move
* Receives all necessary move data from the frontend.
*/
app.post('/solitaire/move', (req, res) => {
app.post('/solitaire/move', async (req, res) => {
// Destructure the complete move data from the request body
// Frontend must send all these properties.
const {
@@ -4501,10 +4592,53 @@ app.post('/solitaire/move', (req, res) => {
// Pass the entire data object to the validation function
if (isValidMove(gameState, req.body)) {
// If valid, mutate the state
moveCard(gameState, req.body);
await moveCard(gameState, req.body);
const win = checkWinCondition(gameState);
if (win) gameState.isDone = true
res.json({ success: true, gameState, win });
if (win) {
gameState.isDone = true
if (gameState.isSOTD) {
gameState.hasFinToday = true;
gameState.endTime = Date.now();
const userStats = getUserSOTDStats.get(userId);
if (userStats) {
if (
(gameState.score > userStats.score) ||
(gameState.score === userStats.score && gameState.moves < userStats.moves) ||
(gameState.score === userStats.score && gameState.moves === userStats.moves && gameState.time < userStats.time)
) {
deleteUserSOTDStats.run(userId);
insertSOTDStats.run({
id: userId,
user_id: userId,
time: gameState.endTime - gameState.startTime,
moves: gameState.moves,
score: gameState.score,
})
}
} else {
insertSOTDStats.run({
id: userId,
user_id: userId,
time: gameState.endTime - gameState.startTime,
moves: gameState.moves,
score: gameState.score,
})
const user = getUser.get(userId)
if (user) {
updateUserCoins.run({ id: userId, coins: user.coins + 1000 });
insertLog.run({
id: userId + '-' + Date.now(),
user_id: userId,
action: 'SOTD_WIN',
target_user_id: null,
coins_amount: 1000,
user_new_amount: user.coins + 1000,
})
}
}
}
}
res.json({ success: true, gameState, win, endTime: win ? Date.now() : null });
} else {
// If the move is invalid, send a specific error message
res.status(400).json({ error: 'Invalid move' });
@@ -4515,13 +4649,13 @@ app.post('/solitaire/move', (req, res) => {
* POST /solitaire/draw
* Draws a card from the stock pile to the waste pile.
*/
app.post('/solitaire/draw', (req, res) => {
app.post('/solitaire/draw', async (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);
await drawCard(gameState);
res.json({ success: true, gameState });
});

View File

@@ -120,4 +120,38 @@ export const getUserElo = flopoDB.prepare(`SELECT * FROM elos WHERE id = @id`);
export const updateElo = flopoDB.prepare('UPDATE elos SET elo = @elo WHERE id = @id');
export const getUsersByElo = flopoDB.prepare('SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC')
export const getUsersByElo = flopoDB.prepare('SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC')
export const stmtSOTD = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS sotd (
id INT PRIMARY KEY,
tableauPiles TEXT,
foundationPiles TEXT,
stockPile TEXT,
wastePile TEXT,
isDone BOOLEAN DEFAULT false,
seed TEXT
)
`);
stmtSOTD.run()
export const getSOTD = flopoDB.prepare(`SELECT * FROM sotd WHERE id = '0'`)
export const insertSOTD = flopoDB.prepare(`INSERT INTO sotd (id, tableauPiles, foundationPiles, stockPile, wastePile, seed) VALUES (@id, @tableauPiles, @foundationPiles, @stockPile, @wastePile, @seed)`)
export const deleteSOTD = flopoDB.prepare(`DELETE FROM sotd WHERE id = '0'`)
export const stmtSOTDStats = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS sotd_stats (
id TEXT PRIMARY KEY,
user_id TEXT REFERENCES users,
time INTEGER,
moves INTEGER,
score INTEGER
)
`);
stmtSOTDStats.run()
export const getAllSOTDStats = flopoDB.prepare(`SELECT sotd_stats.*, users.globalName FROM sotd_stats JOIN users ON users.id = sotd_stats.user_id ORDER BY score DESC, moves ASC, time ASC`);
export const getUserSOTDStats = flopoDB.prepare(`SELECT * FROM sotd_stats WHERE user_id = ?`);
export const insertSOTDStats = flopoDB.prepare(`INSERT INTO sotd_stats (id, user_id, time, moves, score) VALUES (@id, @user_id, @time, @moves, @score)`);
export const clearSOTDStats = flopoDB.prepare(`DELETE FROM sotd_stats`);
export const deleteUserSOTDStats = flopoDB.prepare(`DELETE FROM sotd_stats WHERE user_id = ?`);