mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-01-18 16:37:40 +01:00
flopobot V2.1
This commit is contained in:
1
.idea/sqldialects.xml
generated
1
.idea/sqldialects.xml
generated
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="file://$PROJECT_DIR$/init_database.js" dialect="SQLite" />
|
||||
<file url="PROJECT" dialect="SQLite" />
|
||||
</component>
|
||||
</project>
|
||||
BIN
flopobot.db-shm
BIN
flopobot.db-shm
Binary file not shown.
BIN
flopobot.db-wal
BIN
flopobot.db-wal
Binary file not shown.
711
game.js
711
game.js
@@ -1,711 +0,0 @@
|
||||
import { capitalize } from './utils.js';
|
||||
import pkg from 'pokersolver';
|
||||
const { Hand } = pkg;
|
||||
|
||||
import {
|
||||
updateUserCoins,
|
||||
getUser,
|
||||
insertLog,
|
||||
insertGame,
|
||||
getUserElo,
|
||||
insertElos,
|
||||
updateElo,
|
||||
getAllSkins, deleteSOTD, insertSOTD, clearSOTDStats, getAllSOTDStats
|
||||
} from './init_database.js'
|
||||
import {C4_COLS, C4_ROWS, skins} from "./index.js";
|
||||
|
||||
const messagesTimestamps = new Map();
|
||||
|
||||
const TimesChoices = [
|
||||
{
|
||||
name: '1 minute',
|
||||
value: 60,
|
||||
},
|
||||
{
|
||||
name: '5 minutes',
|
||||
value: 300,
|
||||
},
|
||||
{
|
||||
name: '10 minutes',
|
||||
value: 600,
|
||||
},
|
||||
{
|
||||
name: '15 minutes',
|
||||
value: 900,
|
||||
},
|
||||
{
|
||||
name: '30 minutes',
|
||||
value: 1800,
|
||||
},
|
||||
{
|
||||
name: '1 heure',
|
||||
value: 3600,
|
||||
},
|
||||
{
|
||||
name: '2 heures',
|
||||
value: 3600,
|
||||
},
|
||||
{
|
||||
name: '3 heures',
|
||||
value: 10800,
|
||||
},
|
||||
{
|
||||
name: '6 heures',
|
||||
value: 21600,
|
||||
},
|
||||
{
|
||||
name: '9 heures',
|
||||
value: 32400,
|
||||
},
|
||||
{
|
||||
name: '12 heures',
|
||||
value: 43200,
|
||||
},
|
||||
{
|
||||
name: '16 heures',
|
||||
value: 57600,
|
||||
},
|
||||
{
|
||||
name: '1 jour',
|
||||
value: 86400,
|
||||
},
|
||||
/*{
|
||||
name: '2 journées',
|
||||
value: 172800,
|
||||
},
|
||||
{
|
||||
name: '1 semaine',
|
||||
value: 604800,
|
||||
},
|
||||
{
|
||||
name: '2 semaines',
|
||||
value: 604800 * 2,
|
||||
},*/
|
||||
];
|
||||
|
||||
export function getTimesChoices() {
|
||||
return TimesChoices
|
||||
}
|
||||
|
||||
export function channelPointsHandler(msg) {
|
||||
const author = msg.author
|
||||
const authorDB = getUser.get(author.id)
|
||||
|
||||
if (!authorDB) {
|
||||
console.log("message from an unknown user")
|
||||
return
|
||||
}
|
||||
|
||||
if (msg.content.length < 3 || msg.content.startsWith('.')) return
|
||||
|
||||
const now = Date.now();
|
||||
const timestamps = messagesTimestamps.get(author.id) || [];
|
||||
|
||||
// Remove all timestamps if first one is older than 15 minutes
|
||||
const updatedTimestamps = now - timestamps[0] < 900000 ? timestamps : [];
|
||||
|
||||
updatedTimestamps.push(now);
|
||||
messagesTimestamps.set(author.id, updatedTimestamps);
|
||||
|
||||
if (messagesTimestamps.get(author.id).length <= 10) {
|
||||
// +10 or +50 coins
|
||||
let coins = messagesTimestamps.get(author.id).length === 10
|
||||
? 50
|
||||
: 10
|
||||
updateUserCoins.run({
|
||||
id: author.id,
|
||||
coins: authorDB.coins + coins,
|
||||
})
|
||||
insertLog.run({
|
||||
id: author.id + '-' + Date.now(),
|
||||
user_id: author.id,
|
||||
action: 'AUTOCOINS',
|
||||
target_user_id: null,
|
||||
coins_amount: coins,
|
||||
user_new_amount: authorDB.coins + coins,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export async function slowmodesHandler(msg, activeSlowmodes) {
|
||||
const author = msg.author
|
||||
const authorDB = getUser.get(author.id)
|
||||
const authorSlowmode = activeSlowmodes[author.id]
|
||||
|
||||
if (!authorDB) return false
|
||||
if (!authorSlowmode) return false
|
||||
|
||||
console.log('Message from a slowmode user')
|
||||
|
||||
const now = Date.now();
|
||||
if (now > authorSlowmode.endAt) {
|
||||
console.log('Slow mode is over')
|
||||
delete activeSlowmodes[author.id]
|
||||
return true
|
||||
}
|
||||
|
||||
if (authorSlowmode.lastMessage && (authorSlowmode.lastMessage + 60 * 1000) > now) {
|
||||
await msg.delete()
|
||||
console.log('Message deleted')
|
||||
} else {
|
||||
authorSlowmode.lastMessage = Date.now()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function eloHandler(p1, p2, p1score, p2score, type) {
|
||||
const p1DB = getUser.get(p1)
|
||||
const p2DB = getUser.get(p2)
|
||||
|
||||
if (!p1DB || !p2DB) return
|
||||
|
||||
let p1elo = await getUserElo.get({ id: p1 })
|
||||
let p2elo = await getUserElo.get({ id: p2 })
|
||||
|
||||
if (!p1elo) {
|
||||
await insertElos.run({
|
||||
id: p1?.toString(),
|
||||
elo: 100,
|
||||
})
|
||||
p1elo = await getUserElo.get({ id: p1 })
|
||||
}
|
||||
if (!p2elo) {
|
||||
await insertElos.run({
|
||||
id: p2?.toString(),
|
||||
elo: 100,
|
||||
})
|
||||
p2elo = await getUserElo.get({ id: p2 })
|
||||
}
|
||||
|
||||
if (p1score === p2score) {
|
||||
insertGame.run({
|
||||
id: p1?.toString() + '-' + p2?.toString() + '-' + Date.now()?.toString(),
|
||||
p1: p1,
|
||||
p2: p2,
|
||||
p1_score: p1score,
|
||||
p2_score: p2score,
|
||||
p1_elo: p1elo.elo,
|
||||
p2_elo: p2elo.elo,
|
||||
p1_new_elo: p1elo.elo,
|
||||
p2_new_elo: p2elo.elo,
|
||||
type: type,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const prob1 = 1 / (1 + Math.pow(10, (p2elo.elo - p1elo.elo)/400))
|
||||
const prob2 = 1 / (1 + Math.pow(10, (p1elo.elo - p2elo.elo)/400))
|
||||
|
||||
const p1newElo = Math.max(Math.floor(p1elo.elo + 10 * (p1score - prob1)), 0)
|
||||
const p2newElo = Math.max(Math.floor(p2elo.elo + 10 * (p2score - prob2)), 0)
|
||||
|
||||
console.log(`${p1} elo update : ${p1elo.elo} -> ${p1newElo}`)
|
||||
console.log(`${p2} elo update : ${p2elo.elo} -> ${p2newElo}`)
|
||||
updateElo.run({ id: p1, elo: p1newElo })
|
||||
updateElo.run({ id: p2, elo: p2newElo })
|
||||
|
||||
insertGame.run({
|
||||
id: p1?.toString() + '-' + p2?.toString() + '-' + Date.now()?.toString(),
|
||||
p1: p1,
|
||||
p2: p2,
|
||||
p1_score: p1score,
|
||||
p2_score: p2score,
|
||||
p1_elo: p1elo.elo,
|
||||
p2_elo: p2elo.elo,
|
||||
p1_new_elo: p1newElo,
|
||||
p2_new_elo: p2newElo,
|
||||
type: type,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
export async function pokerEloHandler(room) {
|
||||
if (room.fakeMoney) return
|
||||
|
||||
let DBplayers = []
|
||||
Object.keys(room.players).forEach(playerId => {
|
||||
const DBuser = getUser.get(playerId)
|
||||
if (DBuser) {
|
||||
DBplayers.push(DBuser)
|
||||
}
|
||||
})
|
||||
|
||||
const winnerIds = new Set(room.winners)
|
||||
const playerCount = Object.keys(room.players).length
|
||||
const baseK = 10
|
||||
|
||||
const avgOpponentElo = (player) => {
|
||||
const opponents = DBplayers.filter(p => p.id !== player.id);
|
||||
return opponents.reduce((sum, p) => sum + p.elo, 0) / opponents.length;
|
||||
};
|
||||
|
||||
DBplayers.forEach(player => {
|
||||
const avgElo = avgOpponentElo(player);
|
||||
const expectedScore = 1 / (1 + 10 ** ((avgElo - player.elo) / 400))
|
||||
|
||||
let actualScore;
|
||||
if (winnerIds.has(player.id)) {
|
||||
actualScore = (winnerIds.size === playerCount) ? 0.5 : 1;
|
||||
} else {
|
||||
actualScore = 0;
|
||||
}
|
||||
|
||||
const K = winnerIds.has(player.id) ? (baseK * playerCount) : baseK
|
||||
const delta = K * (actualScore - expectedScore)
|
||||
|
||||
const newElo = Math.max(Math.floor(player.elo + delta), 0)
|
||||
|
||||
|
||||
|
||||
if (!isNaN(newElo)) {
|
||||
console.log(`${player.id} elo update: ${player.elo} -> ${newElo} (K: ${K.toFixed(2)}, Δ: ${delta.toFixed(2)})`);
|
||||
updateElo.run({ id: player.id, elo: newElo })
|
||||
|
||||
insertGame.run({
|
||||
id: player.id + '-' + Date.now()?.toString(),
|
||||
p1: player.id,
|
||||
p2: null,
|
||||
p1_score: actualScore,
|
||||
p2_score: null,
|
||||
p1_elo: player.elo,
|
||||
p2_elo: avgElo,
|
||||
p1_new_elo: newElo,
|
||||
p2_new_elo: null,
|
||||
type: 'POKER_ROUND',
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
} else {
|
||||
console.log(`# ELO UPDATE ERROR -> ${player.id} elo update: ${player.elo} -> ${newElo} (K: ${K.toFixed(2)}, Δ: ${delta.toFixed(2)})`);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function randomSkinPrice(id=0) {
|
||||
const dbSkins = getAllSkins.all();
|
||||
const randomIndex = Math.floor(Math.random() * dbSkins.length);
|
||||
let randomSkin = skins.find((skin) => skin.uuid === dbSkins[randomIndex].uuid);
|
||||
|
||||
// Generate random level and chroma
|
||||
const randomLevel = Math.floor(Math.random() * randomSkin.levels.length + 1);
|
||||
let randomChroma = randomLevel === randomSkin.levels.length
|
||||
? Math.floor(Math.random() * randomSkin.chromas.length + 1)
|
||||
: 1;
|
||||
if (randomChroma === randomSkin.chromas.length && randomSkin.chromas.length >= 2) randomChroma--
|
||||
const selectedLevel = randomSkin.levels[randomLevel - 1]
|
||||
const selectedChroma = randomSkin.chromas[randomChroma - 1]
|
||||
|
||||
|
||||
// Helper functions (unchanged from your original code)
|
||||
const price = () => {
|
||||
let result = dbSkins[randomIndex].basePrice;
|
||||
|
||||
result *= (1 + (randomLevel / Math.max(randomSkin.levels.length, 2)))
|
||||
result *= (1 + (randomChroma / 4))
|
||||
|
||||
return result.toFixed(2);
|
||||
}
|
||||
|
||||
const returnPrice = price()
|
||||
console.log(`#${id} :`, returnPrice)
|
||||
return returnPrice
|
||||
}
|
||||
|
||||
export function createConnect4Board() {
|
||||
return Array(C4_ROWS).fill(null).map(() => Array(C4_COLS).fill(null));
|
||||
}
|
||||
|
||||
export function checkConnect4Win(board, player) {
|
||||
// Check horizontal
|
||||
for (let r = 0; r < C4_ROWS; r++) {
|
||||
for (let c = 0; c <= C4_COLS - 4; c++) {
|
||||
if (board[r][c] === player && board[r][c+1] === player && board[r][c+2] === player && board[r][c+3] === player) {
|
||||
return { win: true, pieces: [{row:r, col:c}, {row:r, col:c+1}, {row:r, col:c+2}, {row:r, col:c+3}] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check vertical
|
||||
for (let r = 0; r <= C4_ROWS - 4; r++) {
|
||||
for (let c = 0; c < C4_COLS; c++) {
|
||||
if (board[r][c] === player && board[r+1][c] === player && board[r+2][c] === player && board[r+3][c] === player) {
|
||||
return { win: true, pieces: [{row:r, col:c}, {row:r+1, col:c}, {row:r+2, col:c}, {row:r+3, col:c}] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check diagonal (down-right)
|
||||
for (let r = 0; r <= C4_ROWS - 4; r++) {
|
||||
for (let c = 0; c <= C4_COLS - 4; c++) {
|
||||
if (board[r][c] === player && board[r+1][c+1] === player && board[r+2][c+2] === player && board[r+3][c+3] === player) {
|
||||
return { win: true, pieces: [{row:r, col:c}, {row:r+1, col:c+1}, {row:r+2, col:c+2}, {row:r+3, col:c+3}] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check diagonal (up-right)
|
||||
for (let r = 3; r < C4_ROWS; r++) {
|
||||
for (let c = 0; c <= C4_COLS - 4; c++) {
|
||||
if (board[r][c] === player && board[r-1][c+1] === player && board[r-2][c+2] === player && board[r-3][c+3] === player) {
|
||||
return { win: true, pieces: [{row:r, col:c}, {row:r-1, col:c+1}, {row:r-2, col:c+2}, {row:r-3, col:c+3}] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { win: false, pieces: [] };
|
||||
}
|
||||
|
||||
export function checkConnect4Draw(board) {
|
||||
return board[0].every(cell => cell !== null);
|
||||
}
|
||||
|
||||
export function formatConnect4BoardForDiscord(board) {
|
||||
const symbols = {
|
||||
'R': '🔴',
|
||||
'Y': '🟡',
|
||||
null: '⚪'
|
||||
};
|
||||
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.
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @returns {Object} The initial gameState object.
|
||||
*/
|
||||
export function deal(deck) {
|
||||
const gameState = {
|
||||
tableauPiles: [[], [], [], [], [], [], []],
|
||||
foundationPiles: [[], [], [], []],
|
||||
stockPile: [],
|
||||
wastePile: [],
|
||||
isDone: false,
|
||||
};
|
||||
|
||||
// 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 if (sourcePileType === 'foundationPiles') {
|
||||
sourcePile = gameState.foundationPiles[sourcePileIndex];
|
||||
} 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' && sourcePileType !== '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 async function moveCard(gameState, moveData) {
|
||||
const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData;
|
||||
|
||||
// Identify the source pile array
|
||||
let sourcePile;
|
||||
if (sourcePileType === 'tableauPiles') {
|
||||
sourcePile = gameState.tableauPiles[sourcePileIndex];
|
||||
} else if (sourcePileType === 'wastePile') {
|
||||
sourcePile = gameState.wastePile;
|
||||
} else if (sourcePileType === 'foundationPiles') {
|
||||
sourcePile = gameState.foundationPiles[sourcePileIndex];
|
||||
}
|
||||
|
||||
// Identify the destination pile array
|
||||
let destPile;
|
||||
if (destPileType === 'tableauPiles') {
|
||||
destPile = gameState.tableauPiles[destPileIndex];
|
||||
} else {
|
||||
destPile = 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);
|
||||
|
||||
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) {
|
||||
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 async 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 = [];
|
||||
}
|
||||
sotdMoveUpdate(gameState, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
export function initTodaysSOTD() {
|
||||
const rankings = getAllSOTDStats.all()
|
||||
const firstPlaceId = rankings.length > 0 ? rankings[0].user_id : null
|
||||
|
||||
console.log(rankings)
|
||||
console.log(firstPlaceId)
|
||||
if (firstPlaceId) {
|
||||
const firstPlaceUser = getUser.get(firstPlaceId)
|
||||
console.log(firstPlaceUser)
|
||||
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,
|
||||
})
|
||||
}
|
||||
console.log(`${firstPlaceId} wins ${new Date().toLocaleDateString()} SOTD`)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
157
init_database.js
157
init_database.js
@@ -1,157 +0,0 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
|
||||
export const flopoDB = new Database('flopobot.db');
|
||||
|
||||
export const stmtUsers = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
globalName TEXT,
|
||||
warned BOOLEAN DEFAULT 0,
|
||||
warns INTEGER DEFAULT 0,
|
||||
allTimeWarns INTEGER DEFAULT 0,
|
||||
totalRequests INTEGER DEFAULT 0,
|
||||
coins INTEGER DEFAULT 0,
|
||||
dailyQueried BOOLEAN DEFAULT 0
|
||||
)
|
||||
`);
|
||||
stmtUsers.run();
|
||||
export const stmtSkins = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS skins (
|
||||
uuid TEXT PRIMARY KEY,
|
||||
displayName TEXT,
|
||||
contentTierUuid TEXT,
|
||||
displayIcon TEXT,
|
||||
user_id TEXT REFERENCES users,
|
||||
tierRank TEXT,
|
||||
tierColor TEXT,
|
||||
tierText TEXT,
|
||||
basePrice TEXT,
|
||||
currentLvl INTEGER DEFAULT NULL,
|
||||
currentChroma INTEGER DEFAULT NULL,
|
||||
currentPrice INTEGER DEFAULT NULL,
|
||||
maxPrice INTEGER DEFAULT NULL
|
||||
)
|
||||
`);
|
||||
stmtSkins.run()
|
||||
|
||||
export const insertUser = flopoDB.prepare('INSERT INTO users (id, username, globalName, warned, warns, allTimeWarns, totalRequests) VALUES (@id, @username, @globalName, @warned, @warns, @allTimeWarns, @totalRequests)');
|
||||
export const updateUser = flopoDB.prepare('UPDATE users SET warned = @warned, warns = @warns, allTimeWarns = @allTimeWarns, totalRequests = @totalRequests WHERE id = @id');
|
||||
export const queryDailyReward = flopoDB.prepare(`UPDATE users SET dailyQueried = 1 WHERE id = ?`);
|
||||
export const resetDailyReward = flopoDB.prepare(`UPDATE users SET dailyQueried = 0 WHERE id = ?`);
|
||||
export const updateUserCoins = flopoDB.prepare('UPDATE users SET coins = @coins WHERE id = @id');
|
||||
export const getUser = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE users.id = ?');
|
||||
export const getAllUsers = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id ORDER BY coins DESC');
|
||||
|
||||
export const insertSkin = flopoDB.prepare('INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText, basePrice, currentLvl, currentChroma, currentPrice, maxPrice) VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText, @basePrice, @currentLvl, @currentChroma, @currentPrice, @maxPrice)');
|
||||
export const updateSkin = flopoDB.prepare('UPDATE skins SET user_id = @user_id, currentLvl = @currentLvl, currentChroma = @currentChroma, currentPrice = @currentPrice WHERE uuid = @uuid');
|
||||
export const getSkin = flopoDB.prepare('SELECT * FROM skins WHERE uuid = ?');
|
||||
export const getAllSkins = flopoDB.prepare('SELECT * FROM skins ORDER BY maxPrice DESC');
|
||||
export const getAllAvailableSkins = flopoDB.prepare('SELECT * FROM skins WHERE user_id IS NULL');
|
||||
export const getUserInventory = flopoDB.prepare('SELECT * FROM skins WHERE user_id = @user_id ORDER BY currentPrice DESC');
|
||||
export const getTopSkins = flopoDB.prepare('SELECT * FROM skins ORDER BY maxPrice DESC LIMIT 10');
|
||||
|
||||
export const insertManyUsers = flopoDB.transaction(async (users) => {
|
||||
for (const user of users) try { await insertUser.run(user) } catch (e) { /**/ }
|
||||
});
|
||||
export const updateManyUsers = flopoDB.transaction(async (users) => {
|
||||
for (const user of users) try { await updateUser.run(user) } catch (e) { console.log('user update failed') }
|
||||
});
|
||||
|
||||
export const insertManySkins = flopoDB.transaction(async (skins) => {
|
||||
for (const skin of skins) try { await insertSkin.run(skin) } catch (e) { console.log('skin insert failed') }
|
||||
});
|
||||
export const updateManySkins = flopoDB.transaction(async (skins) => {
|
||||
for (const skin of skins) try { await updateSkin.run(skin) } catch (e) { console.log('skin insert failed') }
|
||||
});
|
||||
|
||||
|
||||
export const stmtLogs = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
id PRIMARY KEY,
|
||||
user_id TEXT REFERENCES users,
|
||||
action TEXT,
|
||||
target_user_id TEXT REFERENCES users,
|
||||
coins_amount INTEGER,
|
||||
user_new_amount INTEGER
|
||||
)
|
||||
`);
|
||||
stmtLogs.run()
|
||||
|
||||
export const insertLog = flopoDB.prepare('INSERT INTO logs (id, user_id, action, target_user_id, coins_amount, user_new_amount) VALUES (@id, @user_id, @action, @target_user_id, @coins_amount, @user_new_amount)');
|
||||
export const getLogs = flopoDB.prepare('SELECT * FROM logs');
|
||||
export const getUserLogs = flopoDB.prepare('SELECT * FROM logs WHERE user_id = @user_id');
|
||||
|
||||
|
||||
export const stmtGames = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS games (
|
||||
id PRIMARY KEY,
|
||||
p1 TEXT REFERENCES users,
|
||||
p2 TEXT REFERENCES users,
|
||||
p1_score INTEGER,
|
||||
p2_score INTEGER,
|
||||
p1_elo INTEGER,
|
||||
p2_elo INTEGER,
|
||||
p1_new_elo INTEGER,
|
||||
p2_new_elo INTEGER,
|
||||
type TEXT,
|
||||
timestamp TIMESTAMP
|
||||
)
|
||||
`);
|
||||
stmtGames.run()
|
||||
|
||||
export const insertGame = flopoDB.prepare('INSERT INTO games (id, p1, p2, p1_score, p2_score, p1_elo, p2_elo, p1_new_elo, p2_new_elo, type, timestamp) VALUES (@id, @p1, @p2, @p1_score, @p2_score, @p1_elo, @p2_elo, @p1_new_elo, @p2_new_elo, @type, @timestamp)');
|
||||
export const getGames = flopoDB.prepare('SELECT * FROM games');
|
||||
export const getUserGames = flopoDB.prepare('SELECT * FROM games WHERE p1 = @user_id OR p2 = @user_id');
|
||||
|
||||
|
||||
export const stmtElos = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS elos (
|
||||
id PRIMARY KEY REFERENCES users,
|
||||
elo INTEGER
|
||||
)
|
||||
`);
|
||||
stmtElos.run()
|
||||
|
||||
export const insertElos = flopoDB.prepare(`INSERT INTO elos (id, elo) VALUES (@id, @elo)`);
|
||||
export const getElos = flopoDB.prepare(`SELECT * FROM elos`);
|
||||
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 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 = ?`);
|
||||
5167
old_index.js
5167
old_index.js
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ import { DiscordRequest } from '../../api/discord.js';
|
||||
// --- Discord.js Builder Imports ---
|
||||
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
|
||||
import {emitDataUpdated, socketEmit} from "../socket.js";
|
||||
import {formatTime} from "../../../utils.js";
|
||||
import { formatTime } from "../../utils/index.js";
|
||||
|
||||
// Create a new router instance
|
||||
const router = express.Router();
|
||||
|
||||
@@ -38,7 +38,7 @@ export function pokerRoutes(client, io) {
|
||||
});
|
||||
|
||||
router.post('/create', async (req, res) => {
|
||||
const { creatorId } = req.body;
|
||||
const { creatorId, minBet, fakeMoney } = req.body;
|
||||
if (!creatorId) return res.status(400).json({ message: 'Creator ID is required.' });
|
||||
|
||||
if (Object.values(pokerRooms).some(room => room.host_id === creatorId || room.players[creatorId])) {
|
||||
@@ -51,14 +51,15 @@ export function pokerRoutes(client, io) {
|
||||
|
||||
pokerRooms[id] = {
|
||||
id, host_id: creatorId, host_name: creator.globalName || creator.username,
|
||||
name, created_at: Date.now(), last_move_at: Date.now(),
|
||||
name, created_at: Date.now(), last_move_at: null,
|
||||
players: {}, queue: {}, afk: {}, pioche: initialShuffledCards(), tapis: [],
|
||||
dealer: null, sb: null, bb: null, highest_bet: 0, current_player: null,
|
||||
current_turn: null, playing: false, winners: [], waiting_for_restart: false, fakeMoney: false,
|
||||
current_turn: null, playing: false, winners: [], waiting_for_restart: false, fakeMoney: fakeMoney,
|
||||
minBet: minBet,
|
||||
};
|
||||
|
||||
await joinRoom(id, creatorId, io); // Auto-join the creator
|
||||
await emitPokerUpdate({ room: pokerRooms[id] });
|
||||
await emitPokerUpdate({ room: pokerRooms[id], type: 'room-created' });
|
||||
res.status(201).json({ roomId: id });
|
||||
});
|
||||
|
||||
@@ -69,6 +70,9 @@ export function pokerRoutes(client, io) {
|
||||
if (Object.values(pokerRooms).some(r => r.players[userId] || r.queue[userId])) {
|
||||
return res.status(403).json({ message: 'You are already in a room or queue.' });
|
||||
}
|
||||
if (!pokerRooms[roomId].fakeMoney && pokerRooms[roomId].minBet > (getUser.get(userId)?.coins ?? 0)) {
|
||||
return res.status(403).json({ message: 'You do not have enough coins to join this room.' });
|
||||
}
|
||||
|
||||
await joinRoom(roomId, userId, io);
|
||||
res.status(200).json({ message: 'Successfully joined.' });
|
||||
@@ -82,16 +86,99 @@ export function pokerRoutes(client, io) {
|
||||
return res.status(403).json({ message: 'Unauthorized or player not in queue.' });
|
||||
}
|
||||
|
||||
if (!room.fakeMoney) {
|
||||
const userDB = getUser.get(playerId);
|
||||
if (userDB) {
|
||||
updateUserCoins.run({ id: playerId, coins: userDB.coins - room.minBet });
|
||||
insertLog.run({
|
||||
id: `${playerId}-poker-${Date.now()}`,
|
||||
user_id: playerId, target_user_id: null,
|
||||
action: 'POKER_JOIN',
|
||||
coins_amount: -room.minBet, user_new_amount: userDB.coins - room.minBet,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
room.players[playerId] = room.queue[playerId];
|
||||
delete room.queue[playerId];
|
||||
|
||||
await emitPokerUpdate({ room: room });
|
||||
await emitPokerUpdate({ room: room, type: 'player-accepted' });
|
||||
res.status(200).json({ message: 'Player accepted.' });
|
||||
});
|
||||
|
||||
router.post('/leave', (req, res) => {
|
||||
// Implement leave logic...
|
||||
res.status(501).json({ message: "Not Implemented" });
|
||||
router.post('/leave', async (req, res) => {
|
||||
const { userId, roomId } = req.body
|
||||
|
||||
if (!pokerRooms[roomId]) return res.status(404).send({ message: 'Table introuvable' })
|
||||
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: 'Joueur introuvable' })
|
||||
|
||||
if (pokerRooms[roomId].playing && (pokerRooms[roomId].current_turn !== null && pokerRooms[roomId].current_turn !== 4)) {
|
||||
pokerRooms[roomId].afk[userId] = pokerRooms[roomId].players[userId]
|
||||
|
||||
try {
|
||||
pokerRooms[roomId].players[userId].folded = true
|
||||
pokerRooms[roomId].players[userId].last_played_turn = pokerRooms[roomId].current_turn
|
||||
if (pokerRooms[roomId].current_player === userId) {
|
||||
await checkRoundCompletion(pokerRooms[roomId], io);
|
||||
}
|
||||
} catch(e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
await emitPokerUpdate({ type: 'player-afk' });
|
||||
return res.status(200)
|
||||
}
|
||||
|
||||
try {
|
||||
updatePlayerCoins(pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].fakeMoney);
|
||||
delete pokerRooms[roomId].players[userId]
|
||||
|
||||
if (userId === pokerRooms[roomId].host_id) {
|
||||
const newHostId = Object.keys(pokerRooms[roomId].players).find(id => id !== userId)
|
||||
if (!newHostId) {
|
||||
delete pokerRooms[roomId]
|
||||
} else {
|
||||
pokerRooms[roomId].host_id = newHostId
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
await emitPokerUpdate({ type: 'player-left' });
|
||||
return res.status(200)
|
||||
});
|
||||
|
||||
router.post('/kick', async (req, res) => {
|
||||
const { commandUserId, userId, roomId } = req.body
|
||||
|
||||
if (!pokerRooms[roomId]) return res.status(404).send({ message: 'Table introuvable' })
|
||||
if (!pokerRooms[roomId].players[commandUserId]) return res.status(404).send({ message: 'Joueur introuvable' })
|
||||
if (pokerRooms[roomId].host_id !== commandUserId) return res.status(403).send({ message: 'Seul l\'host peut kick' })
|
||||
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: 'Joueur introuvable' })
|
||||
|
||||
if (pokerRooms[roomId].playing && (pokerRooms[roomId].current_turn !== null && pokerRooms[roomId].current_turn !== 4)) {
|
||||
return res.status(403).send({ message: 'Playing' })
|
||||
}
|
||||
|
||||
try {
|
||||
updatePlayerCoins(pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].fakeMoney);
|
||||
delete pokerRooms[roomId].players[userId]
|
||||
|
||||
if (userId === pokerRooms[roomId].host_id) {
|
||||
const newHostId = Object.keys(pokerRooms[roomId].players).find(id => id !== userId)
|
||||
if (!newHostId) {
|
||||
delete pokerRooms[roomId]
|
||||
} else {
|
||||
pokerRooms[roomId].host_id = newHostId
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
await emitPokerUpdate({ type: 'player-kicked' });
|
||||
return res.status(200)
|
||||
});
|
||||
|
||||
// --- Game Action Endpoints ---
|
||||
@@ -152,7 +239,6 @@ export function pokerRoutes(client, io) {
|
||||
player.bank -= callAmount;
|
||||
player.bet += callAmount;
|
||||
if (player.bank === 0) player.allin = true;
|
||||
updatePlayerCoins(player, -callAmount, room.fakeMoney);
|
||||
await emitPokerToast({
|
||||
type: 'player-call',
|
||||
playerId: player.id,
|
||||
@@ -168,7 +254,6 @@ export function pokerRoutes(client, io) {
|
||||
player.bet += amount;
|
||||
if (player.bank === 0) player.allin = true;
|
||||
room.highest_bet = player.bet;
|
||||
updatePlayerCoins(player, -amount, room.fakeMoney);
|
||||
await emitPokerToast({
|
||||
type: 'player-raise',
|
||||
amount: amount,
|
||||
@@ -194,23 +279,29 @@ export function pokerRoutes(client, io) {
|
||||
async function joinRoom(roomId, userId, io) {
|
||||
const user = await client.users.fetch(userId);
|
||||
const userDB = getUser.get(userId);
|
||||
const bank = (userDB?.coins ?? 0) >= 1000 ? userDB.coins : 1000;
|
||||
const isFake = (userDB?.coins ?? 0) < 1000;
|
||||
const room = pokerRooms[roomId];
|
||||
|
||||
const playerObject = {
|
||||
id: userId, globalName: user.globalName || user.username,
|
||||
hand: [], bank: bank, bet: 0, folded: false, allin: false,
|
||||
hand: [], bank: room.minBet, bet: 0, folded: false, allin: false,
|
||||
last_played_turn: null, solve: null
|
||||
};
|
||||
|
||||
const room = pokerRooms[roomId];
|
||||
if (room.playing) {
|
||||
room.queue[userId] = playerObject;
|
||||
} else {
|
||||
room.players[userId] = playerObject;
|
||||
if (!room.fakeMoney) {
|
||||
updateUserCoins.run({ id: userId, coins: userDB.coins - room.minBet });
|
||||
insertLog.run({
|
||||
id: `${userId}-poker-${Date.now()}`,
|
||||
user_id: userId, target_user_id: null,
|
||||
action: 'POKER_JOIN',
|
||||
coins_amount: -room.minBet, user_new_amount: userDB.coins - room.minBet,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isFake) room.fakeMoney = true;
|
||||
await emitPokerUpdate({ room: room, type: 'player-joined' });
|
||||
}
|
||||
|
||||
@@ -218,7 +309,7 @@ async function startNewHand(room, io) {
|
||||
const playerIds = Object.keys(room.players);
|
||||
if (playerIds.length < 2) {
|
||||
room.playing = false; // Not enough players to continue
|
||||
await emitPokerUpdate({ room: room });
|
||||
await emitPokerUpdate({ room: room, type: 'new-hand' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -229,6 +320,7 @@ async function startNewHand(room, io) {
|
||||
room.winners = [];
|
||||
room.waiting_for_restart = false;
|
||||
room.highest_bet = 20;
|
||||
room.last_move_at = Date.now();
|
||||
|
||||
// Rotate dealer
|
||||
const oldDealerIndex = playerIds.indexOf(room.dealer);
|
||||
@@ -248,9 +340,7 @@ async function startNewHand(room, io) {
|
||||
room.bb = bbPlayer.id;
|
||||
|
||||
sbPlayer.bank -= 10; sbPlayer.bet = 10;
|
||||
updatePlayerCoins(sbPlayer, -10, room.fakeMoney);
|
||||
bbPlayer.bank -= 20; bbPlayer.bet = 20;
|
||||
updatePlayerCoins(bbPlayer, -20, room.fakeMoney);
|
||||
|
||||
bbPlayer.last_played_turn = 0;
|
||||
room.current_player = playerIds[(dealerIndex + 3) % playerIds.length];
|
||||
@@ -269,7 +359,7 @@ async function checkRoundCompletion(room, io) {
|
||||
}
|
||||
} else {
|
||||
room.current_player = getNextActivePlayer(room);
|
||||
await emitPokerUpdate({ room: room });
|
||||
await emitPokerUpdate({ room: room, type: 'round-continue' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,19 +383,19 @@ async function advanceToNextPhase(room, io, phase) {
|
||||
await handleShowdown(room, io, checkRoomWinners(room));
|
||||
return;
|
||||
case 'progressive-showdown':
|
||||
await emitPokerUpdate({ room: room });
|
||||
await emitPokerUpdate({ room: room, type: 'progressive-showdown' });
|
||||
while(room.tapis.length < 5) {
|
||||
await sleep(500);
|
||||
room.tapis.push(room.pioche.pop());
|
||||
updatePlayerHandSolves(room);
|
||||
await emitPokerUpdate({ room: room });
|
||||
await emitPokerUpdate({ room: room, type: 'progressive-showdown' });
|
||||
}
|
||||
await handleShowdown(room, io, checkRoomWinners(room));
|
||||
return;
|
||||
}
|
||||
updatePlayerHandSolves(room); // NEW: Update hand strength after new cards
|
||||
room.current_player = getFirstActivePlayerAfterDealer(room);
|
||||
await emitPokerUpdate({ room: room });
|
||||
await emitPokerUpdate({ room: room, type: 'phase-advanced' });
|
||||
}
|
||||
|
||||
async function handleShowdown(room, io, winners) {
|
||||
@@ -313,6 +403,7 @@ async function handleShowdown(room, io, winners) {
|
||||
room.playing = false;
|
||||
room.waiting_for_restart = true;
|
||||
room.winners = winners;
|
||||
room.current_player = null;
|
||||
|
||||
let totalPot = 0;
|
||||
Object.values(room.players).forEach(p => { totalPot += p.bet; });
|
||||
@@ -323,16 +414,20 @@ async function handleShowdown(room, io, winners) {
|
||||
const winnerPlayer = room.players[winnerId];
|
||||
if(winnerPlayer) {
|
||||
winnerPlayer.bank += winAmount;
|
||||
updatePlayerCoins(winnerPlayer, winAmount, room.fakeMoney);
|
||||
}
|
||||
});
|
||||
|
||||
await clearAfkPlayers(room);
|
||||
|
||||
console.log(room)
|
||||
|
||||
//await pokerEloHandler(room);
|
||||
await emitPokerUpdate({ room: room });
|
||||
await emitPokerUpdate({ room: room, type: 'showdown' });
|
||||
await emitPokerToast({
|
||||
type: 'player-winner',
|
||||
playerIds: winners,
|
||||
roomId: room.id,
|
||||
amount: winAmount,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -352,12 +447,22 @@ function updatePlayerCoins(player, amount, isFake) {
|
||||
const user = getUser.get(player.id);
|
||||
if (!user) return;
|
||||
|
||||
const newCoins = player.bank; // The bank is the source of truth now
|
||||
updateUserCoins.run({ id: player.id, coins: newCoins });
|
||||
const userDB = getUser.get(player.id);
|
||||
updateUserCoins.run({ id: player.id, coins: userDB.coins + amount });
|
||||
insertLog.run({
|
||||
id: `${player.id}-poker-${Date.now()}`,
|
||||
user_id: player.id, target_user_id: null,
|
||||
action: `POKER_${amount > 0 ? 'WIN' : 'BET'}`,
|
||||
coins_amount: amount, user_new_amount: newCoins,
|
||||
coins_amount: amount, user_new_amount: userDB.coins + amount,
|
||||
});
|
||||
}
|
||||
|
||||
async function clearAfkPlayers(room) {
|
||||
Object.keys(room.afk).forEach(playerId => {
|
||||
if (room.players[playerId]) {
|
||||
updatePlayerCoins(room.players[playerId], room.players[playerId].bank, room.fakeMoney);
|
||||
delete room.players[playerId];
|
||||
}
|
||||
});
|
||||
room.afk = {};
|
||||
}
|
||||
@@ -62,9 +62,9 @@ export function solitaireRoutes(client, io) {
|
||||
|
||||
router.post('/start/sotd', (req, res) => {
|
||||
const { userId } = req.body;
|
||||
if (!userId || !getUser.get(userId)) {
|
||||
/*if (!userId || !getUser.get(userId)) {
|
||||
return res.status(404).json({ error: 'User not found.' });
|
||||
}
|
||||
}*/
|
||||
|
||||
if (activeSolitaireGames[userId]?.isSOTD) {
|
||||
return res.json({ success: true, gameState: activeSolitaireGames[userId] });
|
||||
@@ -177,7 +177,7 @@ function updateGameStats(gameState, actionType, moveData = {}) {
|
||||
}
|
||||
if(actionType === 'draw' && gameState.wastePile.length === 0) {
|
||||
// Penalty for cycling through an empty stock pile
|
||||
gameState.score -= 100;
|
||||
gameState.score -= 5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +189,7 @@ function handleWin(userId, gameState, io) {
|
||||
const timeTaken = gameState.endTime - gameState.startTime;
|
||||
|
||||
const currentUser = getUser.get(userId);
|
||||
if (!currentUser) return;
|
||||
const existingStats = getUserSOTDStats.get(userId);
|
||||
|
||||
if (!existingStats) {
|
||||
|
||||
314
utils.js
314
utils.js
@@ -1,314 +0,0 @@
|
||||
import 'dotenv/config';
|
||||
import OpenAI from "openai";
|
||||
import {GoogleGenAI} from "@google/genai";
|
||||
import {Mistral} from '@mistralai/mistralai';
|
||||
import pkg from 'pokersolver';
|
||||
import {flopoDB, getAllUsers} from "./init_database.js";
|
||||
|
||||
const { Hand } = pkg;
|
||||
|
||||
export async function DiscordRequest(endpoint, options) {
|
||||
// append endpoint to root API URL
|
||||
const url = 'https://discord.com/api/v10/' + endpoint;
|
||||
// Stringify payloads
|
||||
if (options.body) options.body = JSON.stringify(options.body);
|
||||
// Use fetch to make requests
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bot ${process.env.DISCORD_TOKEN}`,
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'User-Agent': 'DiscordBot (https://github.com/discord/discord-example-app, 1.0.0)',
|
||||
},
|
||||
...options
|
||||
});
|
||||
// throw API errors
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
console.log(res.status);
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
// return original response
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function InstallGlobalCommands(appId, commands) {
|
||||
// API endpoint to overwrite global commands
|
||||
const endpoint = `applications/${appId}/commands`;
|
||||
|
||||
try {
|
||||
// This is calling the bulk overwrite endpoint: https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands
|
||||
await DiscordRequest(endpoint, { method: 'PUT', body: commands });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Simple method that returns a random emoji from list
|
||||
export function getRandomEmoji(list=0) {
|
||||
let emojiList
|
||||
|
||||
switch (list) {
|
||||
case 0:
|
||||
emojiList = ['😭','😄','😌','🤓','😎','😤','🤖','😶🌫️','🌏','📸','💿','👋','🌊','✨']
|
||||
break
|
||||
case 1:
|
||||
emojiList = [
|
||||
'<:CAUGHT:1323810730155446322>',
|
||||
'<:hinhinhin:1072510144933531758>',
|
||||
'<:o7:1290773422451986533>',
|
||||
'<:zhok:1115221772623683686>',
|
||||
'<:nice:1154049521110765759>',
|
||||
'<:nerd:1087658195603951666>',
|
||||
'<:peepSelfie:1072508131839594597>',
|
||||
]
|
||||
break
|
||||
default:
|
||||
emojiList = ['']
|
||||
break
|
||||
}
|
||||
return emojiList[Math.floor(Math.random() * emojiList.length)];
|
||||
}
|
||||
export function getRandomHydrateText() {
|
||||
const texts = [
|
||||
`Hydratez-vous`,
|
||||
`Pensez à vous hydratez`,
|
||||
`Vous vous êtes hydratez aujourd'hui ?`,
|
||||
`Buvez de l'eau la team`,
|
||||
`#etsi vous vous hydratiez`,
|
||||
`Oubliez pas de vous hydratez`,
|
||||
`Hydratez vous la team`,
|
||||
`Hydratez vous c'est important`,
|
||||
`Hydratez-vous`,
|
||||
`Pensez à vous hydratez`,
|
||||
`Vous vous êtes hydratez aujourd'hui ?`,
|
||||
`Buvez de l'eau la team`,
|
||||
`#etsi vous vous hydratiez`,
|
||||
`Oubliez pas de vous hydratez`,
|
||||
`Hydratez vous la team`,
|
||||
`Hydratez vous c'est important`,
|
||||
`Hydratez-vous`,
|
||||
`Pensez à vous hydratez`,
|
||||
`Vous vous êtes hydratez aujourd'hui ?`,
|
||||
`Buvez de l'eau la team`,
|
||||
`#etsi vous vous hydratiez`,
|
||||
`Oubliez pas de vous hydratez`,
|
||||
`Hydratez vous la team`,
|
||||
`Hydratez vous c'est important`,
|
||||
`nsm ojd ça s'hydrate pas`,
|
||||
];
|
||||
return texts[Math.floor(Math.random() * texts.length)];
|
||||
}
|
||||
|
||||
export const initialCards = [
|
||||
'Ad', '2d', '3d', '4d', '5d', '6d', '7d', '8d', '9d', 'Td', 'Jd', 'Qd', 'Kd',
|
||||
'As', '2s', '3s', '4s', '5s', '6s', '7s', '8s', '9s', 'Ts', 'Js', 'Qs', 'Ks',
|
||||
'Ac', '2c', '3c', '4c', '5c', '6c', '7c', '8c', '9c', 'Tc', 'Jc', 'Qc', 'Kc',
|
||||
'Ah', '2h', '3h', '4h', '5h', '6h', '7h', '8h', '9h', 'Th', 'Jh', 'Qh', 'Kh',
|
||||
]
|
||||
|
||||
export function initialShuffledCards() {
|
||||
return initialCards.sort((a, b) => 0.5 - Math.random());
|
||||
}
|
||||
export function capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
export async function getOnlineUsersWithRole(guildId, roleId) {
|
||||
const endpoint = `/guilds/${guildId}/members?limit=1000`;
|
||||
const response = await DiscordRequest(endpoint, { method: 'GET' });
|
||||
|
||||
const members = await response.json();
|
||||
return members.filter(
|
||||
(m) =>
|
||||
m.roles.includes(roleId) &&
|
||||
m.presence?.status !== 'offline'
|
||||
);
|
||||
}
|
||||
|
||||
export function formatTime(time) {
|
||||
const days = Math.floor(time / (24 * 60 * 60));
|
||||
const hours = Math.floor((time % (24 * 60 * 60)) / (60 * 60));
|
||||
const minutes = Math.floor((time % (60 * 60)) / 60);
|
||||
const seconds = time % 60;
|
||||
|
||||
const parts = [];
|
||||
|
||||
if (days > 0) parts.push(`**${days}** jour${days > 1 ? 's' : ''}`);
|
||||
if (hours > 0) parts.push(`**${hours}** heure${hours > 1 ? 's' : ''}`);
|
||||
if (minutes > 0) parts.push(`**${minutes}** minute${minutes > 1 ? 's' : ''}`);
|
||||
if (seconds > 0 || parts.length === 0) parts.push(`**${seconds}** seconde${seconds > 1 ? 's' : ''}`);
|
||||
|
||||
return parts.join(', ').replace(/,([^,]*)$/, ' et$1');
|
||||
}
|
||||
|
||||
const openai = new OpenAI();
|
||||
const gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY})
|
||||
const leChat = new Mistral({apiKey: process.env.MISTRAL_KEY});
|
||||
|
||||
export async function gork(messageHistory) {
|
||||
if (process.env.MODEL === 'OpenAI') {
|
||||
// OPEN AI
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: "gpt-4.1-mini",
|
||||
messages: messageHistory,
|
||||
});
|
||||
|
||||
return completion.choices[0].message.content;
|
||||
}
|
||||
else if (process.env.MODEL === 'Gemini') {
|
||||
//GEMINI
|
||||
const formattedHistory = messageHistory.map(msg => {
|
||||
return `${msg.role}: ${msg.content}`;
|
||||
}).join('\n');
|
||||
const response = await gemini.models.generateContent({
|
||||
model: "gemini-2.0-flash-lite",
|
||||
contents: formattedHistory,
|
||||
})
|
||||
return response.text
|
||||
} else if (process.env.MODEL === 'Mistral') {
|
||||
// MISTRAL
|
||||
const chatResponse = await leChat.chat.complete({
|
||||
model: 'mistral-large-latest',
|
||||
messages: messageHistory,
|
||||
})
|
||||
|
||||
return chatResponse.choices[0].message.content
|
||||
} else {
|
||||
return "Pas d'IA"
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAPOUsers() {
|
||||
const fetchUrl = process.env.APO_BASE_URL + '/users?serverId=' + (process.env.T12_GUILD_ID ?? process.env.GUILD_ID)
|
||||
console.log(fetchUrl)
|
||||
return await fetch(fetchUrl)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Error fetching APO users')
|
||||
}
|
||||
return response.json()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('There was a problem with the fetch operation:', error);
|
||||
});
|
||||
}
|
||||
|
||||
export async function postAPOBuy(userId, amount) {
|
||||
const fetchUrl = process.env.APO_BASE_URL + '/buy?serverId=' + (process.env.T12_GUILD_ID ?? process.env.GUILD_ID) + '&userId=' + userId + '&amount=' + amount
|
||||
console.log(fetchUrl)
|
||||
return await fetch(fetchUrl, {
|
||||
method: 'POST',
|
||||
})
|
||||
.then(response => response)
|
||||
.catch(error => console.log('Post error:', error))
|
||||
}
|
||||
|
||||
export function getFirstActivePlayerAfterDealer(room) {
|
||||
const players = Object.values(room.players);
|
||||
const dealerPosition = players.findIndex((p) => p.id === room.dealer);
|
||||
for (let i = 1; i < players.length; i++) {
|
||||
const nextPos = (dealerPosition + i) % players.length;
|
||||
if (!players[nextPos].folded && !players[nextPos].allin) {
|
||||
return players[nextPos].id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getNextActivePlayer(room) {
|
||||
const players = Object.values(room.players);
|
||||
const currentPlayerPosition = players.findIndex((p) => p.id === room.current_player);
|
||||
for (let i = 1; i < players.length; i++) {
|
||||
const nextPos = (currentPlayerPosition + i) % players.length;
|
||||
if (!players[nextPos].folded && !players[nextPos].allin) {
|
||||
return players[nextPos].id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function checkEndOfBettingRound(room) {
|
||||
const players = Object.values(room.players);
|
||||
const activePlayers = players.filter((p) => !p.folded);
|
||||
|
||||
if (activePlayers.length === 1) {
|
||||
return { endRound: true, winner: activePlayers[0].id, nextPhase: null };
|
||||
}
|
||||
|
||||
const allInPlayers = activePlayers.filter(p => p.allin)
|
||||
if (allInPlayers.length === activePlayers.length) {
|
||||
return { endRound: true, winner: null, nextPhase: 'progressive-showdown' };
|
||||
}
|
||||
|
||||
const allActed = activePlayers.every(p => (p.bet === room.highest_bet || p.allin) && p.last_played_turn === room.current_turn);
|
||||
|
||||
if (allActed) {
|
||||
let nextPhase;
|
||||
switch (room.current_turn) {
|
||||
case 0:
|
||||
nextPhase = 'flop';
|
||||
break;
|
||||
case 1:
|
||||
nextPhase = 'turn';
|
||||
break;
|
||||
case 2:
|
||||
nextPhase = 'river';
|
||||
break;
|
||||
case 3:
|
||||
nextPhase = 'showdown';
|
||||
break;
|
||||
default:
|
||||
nextPhase = null;
|
||||
break;
|
||||
}
|
||||
return { endRound: true, winner: null, nextPhase: nextPhase };
|
||||
}
|
||||
|
||||
return { endRound: false, winner: null, nextPhase: null };
|
||||
}
|
||||
|
||||
export function checkRoomWinners(room) {
|
||||
const players = Object.values(room.players);
|
||||
const activePlayers = players.filter(p => !p.folded);
|
||||
|
||||
const playerSolutions = activePlayers.map(player => ({
|
||||
id: player.id,
|
||||
solution: Hand.solve([...room.tapis, ...player.hand], 'standard', false)
|
||||
}));
|
||||
|
||||
const solutions = playerSolutions.map(ps => ps.solution);
|
||||
|
||||
const winningIndices = Hand.winners(solutions).map(winningHand =>
|
||||
solutions.findIndex(sol => sol === winningHand)
|
||||
);
|
||||
|
||||
return winningIndices.map(index => playerSolutions[index].id);
|
||||
}
|
||||
|
||||
export async function pruneOldLogs() {
|
||||
const users = flopoDB.prepare(`
|
||||
SELECT user_id
|
||||
FROM logs
|
||||
GROUP BY user_id
|
||||
HAVING COUNT(*) > ${process.env.LOGS_BY_USER}
|
||||
`).all();
|
||||
|
||||
const transaction = flopoDB.transaction(() => {
|
||||
for (const { user_id } of users) {
|
||||
flopoDB.prepare(`
|
||||
DELETE FROM logs
|
||||
WHERE id IN (
|
||||
SELECT id FROM (
|
||||
SELECT id,
|
||||
ROW_NUMBER() OVER (ORDER BY id DESC) AS rn
|
||||
FROM logs
|
||||
WHERE user_id = ?
|
||||
)
|
||||
WHERE rn > ${process.env.LOGS_BY_USER}
|
||||
)
|
||||
`).run(user_id);
|
||||
}
|
||||
});
|
||||
|
||||
transaction()
|
||||
}
|
||||
11
valo.js
11
valo.js
@@ -1,11 +0,0 @@
|
||||
export async function getValorantSkins(locale='fr-FR') {
|
||||
const response = await fetch(`https://valorant-api.com/v1/weapons/skins?language=${locale}`, { method: 'GET' });
|
||||
const data = await response.json();
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function getSkinTiers(locale='fr-FR') {
|
||||
const response = await fetch(`https://valorant-api.com/v1/contenttiers?language=${locale}`, { method: 'GET'});
|
||||
const data = await response.json();
|
||||
return data.data
|
||||
}
|
||||
Reference in New Issue
Block a user