Merge pull request #69 from cassoule/milo-260128

Milo 260128
This commit is contained in:
Milo Gourvest
2026-01-28 17:29:09 +01:00
committed by GitHub
5 changed files with 254 additions and 24 deletions

View File

@@ -10,7 +10,7 @@ import { client } from "../bot/client.js";
* @param {number} p2Score - The score for player 2.
* @param {string} type - The type of game being played (e.g., 'TICTACTOE', 'CONNECT4').
*/
export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type, scores = null) {
// --- 1. Fetch Player Data ---
const p1DB = getUser.get(p1Id);
const p2DB = getUser.get(p2Id);
@@ -43,9 +43,26 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
const expectedP1 = 1 / (1 + Math.pow(10, (p2CurrentElo - p1CurrentElo) / 400));
const expectedP2 = 1 / (1 + Math.pow(10, (p1CurrentElo - p2CurrentElo) / 400));
// Calculate raw Elo changes
const p1Change = K_FACTOR * (p1Score - expectedP1);
const p2Change = K_FACTOR * (p2Score - expectedP2);
// Make losing friendlier: loser loses 80% of what winner gains
let finalP1Change = p1Change;
let finalP2Change = p2Change;
if (p1Score > p2Score) {
// P1 won, P2 lost
finalP2Change = p2Change * 0.8;
} else if (p2Score > p1Score) {
// P2 won, P1 lost
finalP1Change = p1Change * 0.8;
}
// If it's a draw (p1Score === p2Score), keep the original changes
// Calculate new Elo ratings
const p1NewElo = Math.round(p1CurrentElo + K_FACTOR * (p1Score - expectedP1));
const p2NewElo = Math.round(p2CurrentElo + K_FACTOR * (p2Score - expectedP2));
const p1NewElo = Math.round(p1CurrentElo + finalP1Change);
const p2NewElo = Math.round(p2CurrentElo + finalP2Change);
// Ensure Elo doesn't drop below a certain threshold (e.g., 100)
const finalP1Elo = Math.max(0, p1NewElo);
@@ -77,19 +94,37 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
updateElo.run({ id: p1Id, elo: finalP1Elo });
updateElo.run({ id: p2Id, elo: finalP2Elo });
insertGame.run({
id: `${p1Id}-${p2Id}-${Date.now()}`,
p1: p1Id,
p2: p2Id,
p1_score: p1Score,
p2_score: p2Score,
p1_elo: p1CurrentElo,
p2_elo: p2CurrentElo,
p1_new_elo: finalP1Elo,
p2_new_elo: finalP2Elo,
type: type,
timestamp: Date.now(),
});
if (scores) {
insertGame.run({
id: `${p1Id}-${p2Id}-${Date.now()}`,
p1: p1Id,
p2: p2Id,
p1_score: scores.p1,
p2_score: scores.p2,
p1_elo: p1CurrentElo,
p2_elo: p2CurrentElo,
p1_new_elo: finalP1Elo,
p2_new_elo: finalP2Elo,
type: type,
timestamp: Date.now(),
});
} else {
insertGame.run({
id: `${p1Id}-${p2Id}-${Date.now()}`,
p1: p1Id,
p2: p2Id,
p1_score: p1Score,
p2_score: p2Score,
p1_elo: p1CurrentElo,
p2_elo: p2CurrentElo,
p1_new_elo: finalP1Elo,
p2_new_elo: finalP2Elo,
type: type,
timestamp: Date.now(),
});
}
}
/**

View File

@@ -11,6 +11,9 @@ export let activeConnect4Games = {};
// Stores active Tic-Tac-Toe games, keyed by a unique game ID.
export let activeTicTacToeGames = {};
// Stores active Snake games, keyed by a unique game ID.
export let activeSnakeGames = {};
// Stores active Solitaire games, keyed by user ID.
export let activeSolitaireGames = {};
@@ -54,6 +57,9 @@ export let tictactoeQueue = [];
// Stores user IDs waiting to play Connect 4.
export let connect4Queue = [];
// Stores user IDs waiting to play Snake 1v1.
export let snakeQueue = [];
export let queueMessagesEndpoints = [];
// --- Rate Limiting and Caching ---

View File

@@ -24,7 +24,7 @@ import {
} from "../../database/index.js";
// --- Game State Imports ---
import { activePolls, activePredis, activeSlowmodes, skins } from "../../game/state.js";
import { activePolls, activePredis, activeSlowmodes, skins, activeSnakeGames } from "../../game/state.js";
// --- Utility and API Imports ---
import { formatTime, isMeleeSkin, isVCTSkin, isChampionsSkin, getVCTRegion } from "../../utils/index.js";
@@ -32,7 +32,7 @@ import { DiscordRequest } from "../../api/discord.js";
// --- Discord.js Builder Imports ---
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { emitDataUpdated, socketEmit } from "../socket.js";
import { emitDataUpdated, socketEmit, onGameOver } from "../socket.js";
import { handleCaseOpening } from "../../utils/marketNotifs.js";
import { drawCaseContent, drawCaseSkin, getSkinUpgradeProbs } from "../../utils/caseOpening.js";
@@ -1204,6 +1204,79 @@ export function apiRoutes(client, io) {
return res.status(200).json({ message: "Prédi close" });
});
router.post("/snake/reward", async (req, res) => {
const { discordId, score, isWin } = req.body;
console.log(`[SNAKE][SOLO]${discordId}: score=${score}, isWin=${isWin}`);
try {
const user = getUser.get(discordId);
if (!user) return res.status(404).json({ message: "Utilisateur introuvable" });
const reward = isWin ? score * 2 : score;
const newCoins = user.coins + reward;
updateUserCoins.run({ id: discordId, coins: newCoins });
insertLog.run({
id: `${discordId}-snake-reward-${Date.now()}`,
user_id: discordId,
action: "SNAKE_GAME_REWARD",
coins_amount: reward,
user_new_amount: newCoins,
target_user_id: null,
});
await emitDataUpdated({ table: "users", action: "update" });
return res.status(200).json({ message: `Récompense de ${reward} FlopoCoins attribuée !` });
} catch (e) {
console.error("Error rewarding snake game:", e);
return res.status(500).json({ message: "Erreur lors de l'attribution de la récompense" });
}
});
router.post("/queue/leave", async (req, res) => {
const { discordId, game, reason } = req.body;
if (game === "snake" && (reason === "beforeunload" || reason === "route-leave")) {
const lobby = Object.values(activeSnakeGames).find(
(l) => (l.p1.id === discordId || l.p2.id === discordId) && !l.gameOver,
);
if (!lobby) return;
const player = lobby.p1.id === discordId ? lobby.p1 : lobby.p2;
const otherPlayer = lobby.p1.id === discordId ? lobby.p2 : lobby.p1;
if (player.gameOver === true) return res.status(200).json({ message: "Déjà quitté" });
player.gameOver = true;
otherPlayer.win = true;
lobby.lastmove = Date.now();
// Broadcast the updated state to both players
await socketEmit("snakegamestate", {
lobby: {
p1: lobby.p1,
p2: lobby.p2,
},
});
// Check if game should end
if (lobby.p1.gameOver && lobby.p2.gameOver) {
// Both players finished - determine winner
let winnerId = null;
if (lobby.p1.win && !lobby.p2.win) {
winnerId = lobby.p1.id;
} else if (lobby.p2.win && !lobby.p1.win) {
winnerId = lobby.p2.id;
} else if (lobby.p1.score > lobby.p2.score) {
winnerId = lobby.p1.id;
} else if (lobby.p2.score > lobby.p1.score) {
winnerId = lobby.p2.id;
}
// If scores are equal, winnerId remains null (draw)
await onGameOver(client, "snake", discordId, winnerId, "", { p1: lobby.p1.score, p2: lobby.p2.score });
} else if (lobby.p1.win || lobby.p2.win) {
// One player won by filling the grid
const winnerId = lobby.p1.win ? lobby.p1.id : lobby.p2.id;
await onGameOver(client, "snake", discordId, winnerId, "", { p1: lobby.p1.score, p2: lobby.p2.score });
}
}
});
// --- Admin Routes ---
router.post("/buy-coins", (req, res) => {

View File

@@ -2,9 +2,11 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "disc
import {
activeConnect4Games,
activeTicTacToeGames,
activeSnakeGames,
connect4Queue,
queueMessagesEndpoints,
tictactoeQueue,
snakeQueue,
} from "../game/state.js";
import {
C4_ROWS,
@@ -31,8 +33,11 @@ export function initializeSocket(server, client) {
registerTicTacToeEvents(socket, client);
registerConnect4Events(socket, client);
registerSnakeEvents(socket, client);
socket.on("tictactoe:queue:leave", async ({ discordId }) => await refreshQueuesForUser(discordId, client));
socket.on("connect4:queue:leave", async ({ discordId }) => await refreshQueuesForUser(discordId, client));
socket.on("snake:queue:leave", async ({ discordId }) => await refreshQueuesForUser(discordId, client));
// catch tab kills / network drops
socket.on("disconnecting", async () => {
@@ -68,6 +73,13 @@ function registerConnect4Events(socket, client) {
socket.on("connect4NoTime", (e) => onGameOver(client, "connect4", e.playerId, e.winner, "(temps écoulé)"));
}
function registerSnakeEvents(socket, client) {
socket.on("snakeconnection", (e) => refreshQueuesForUser(e.id, client));
socket.on("snakequeue", (e) => onQueueJoin(client, "snake", e.playerId));
socket.on("snakegamestate", (e) => onSnakeGameStateUpdate(client, e));
socket.on("snakegameOver", (e) => onGameOver(client, "snake", e.playerId, e.winner));
}
// --- Core Handlers (Preserving Original Logic) ---
async function onQueueJoin(client, gameType, playerId) {
@@ -189,7 +201,53 @@ async function onConnect4Move(client, eventData) {
await onGameOver(client, "connect4", playerId, winnerId);
}
async function onGameOver(client, gameType, playerId, winnerId, reason = "") {
async function onSnakeGameStateUpdate(client, eventData) {
const { playerId, snake, food, score, gameOver, win } = eventData;
const lobby = Object.values(activeSnakeGames).find(
(l) => (l.p1.id === playerId || l.p2.id === playerId) && !l.gameOver,
);
if (!lobby) return;
const player = lobby.p1.id === playerId ? lobby.p1 : lobby.p2;
player.snake = snake;
player.food = food;
player.score = score;
player.gameOver = gameOver === true ? true : false;
player.win = win;
lobby.lastmove = Date.now();
// Broadcast the updated state to both players
io.emit("snakegamestate", {
lobby: {
p1: lobby.p1,
p2: lobby.p2,
},
});
// Check if game should end
if (lobby.p1.gameOver && lobby.p2.gameOver) {
// Both players finished - determine winner
let winnerId = null;
if (lobby.p1.win && !lobby.p2.win) {
winnerId = lobby.p1.id;
} else if (lobby.p2.win && !lobby.p1.win) {
winnerId = lobby.p2.id;
} else if (lobby.p1.score > lobby.p2.score) {
winnerId = lobby.p1.id;
} else if (lobby.p2.score > lobby.p1.score) {
winnerId = lobby.p2.id;
}
// If scores are equal, winnerId remains null (draw)
await onGameOver(client, "snake", playerId, winnerId, "", { p1: lobby.p1.score, p2: lobby.p2.score });
} else if (lobby.p1.win || lobby.p2.win) {
// One player won by filling the grid
const winnerId = lobby.p1.win ? lobby.p1.id : lobby.p2.id;
await onGameOver(client, "snake", playerId, winnerId, "", { p1: lobby.p1.score, p2: lobby.p2.score });
}
}
export async function onGameOver(client, gameType, playerId, winnerId, reason = "", scores = null) {
const { activeGames, title } = getGameAssets(gameType);
const gameKey = Object.keys(activeGames).find((key) => key.includes(playerId));
const game = gameKey ? activeGames[gameKey] : undefined;
@@ -198,7 +256,7 @@ async function onGameOver(client, gameType, playerId, winnerId, reason = "") {
game.gameOver = true;
let resultText;
if (winnerId === null) {
await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase());
await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase(), scores);
resultText = "Égalité";
} else {
await eloHandler(
@@ -207,6 +265,7 @@ async function onGameOver(client, gameType, playerId, winnerId, reason = "") {
game.p1.id === winnerId ? 1 : 0,
game.p2.id === winnerId ? 1 : 0,
title.toUpperCase(),
scores
);
const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name;
resultText = `Victoire de ${winnerName}`;
@@ -216,6 +275,7 @@ async function onGameOver(client, gameType, playerId, winnerId, reason = "") {
if (gameType === "tictactoe") io.emit("tictactoegameOver", { game, winner: winnerId });
if (gameType === "connect4") io.emit("connect4gameOver", { game, winner: winnerId });
if (gameType === "snake") io.emit("snakegameOver", { game, winner: winnerId });
if (gameKey) {
setTimeout(() => delete activeGames[gameKey], 1000);
@@ -251,8 +311,7 @@ async function createGame(client, gameType) {
gameOver: false,
lastmove: Date.now(),
};
} else {
// connect4
} else if (gameType === "connect4") {
lobby = {
p1: {
id: p1Id,
@@ -272,6 +331,31 @@ async function createGame(client, gameType) {
lastmove: Date.now(),
winningPieces: [],
};
} else if (gameType === "snake") {
lobby = {
p1: {
id: p1Id,
name: p1.globalName,
avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }),
snake: [],
food: null,
score: 0,
gameOver: false,
win: false,
},
p2: {
id: p2Id,
name: p2.globalName,
avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }),
snake: [],
food: null,
score: 0,
gameOver: false,
win: false,
},
gameOver: false,
lastmove: Date.now(),
};
}
const msgId = await updateDiscordMessage(client, lobby, title);
@@ -328,8 +412,29 @@ async function refreshQueuesForUser(userId, client) {
}
}
index = snakeQueue.indexOf(userId);
if (index > -1) {
snakeQueue.splice(index, 1);
try {
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const generalChannel = await guild.channels.fetch(process.env.BOT_CHANNEL_ID);
const user = await client.users.fetch(userId);
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]);
const updatedEmbed = new EmbedBuilder()
.setTitle("Snake 1v1")
.setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`)
.setColor(0xed4245)
.setTimestamp(new Date());
await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
delete queueMessagesEndpoints[userId];
} catch (e) {
console.error("Error updating queue message : ", e);
}
}
await emitQueueUpdate(client, "tictactoe");
await emitQueueUpdate(client, "connect4");
await emitQueueUpdate(client, "snake");
}
async function emitQueueUpdate(client, gameType) {
@@ -361,6 +466,13 @@ function getGameAssets(gameType) {
title: "Puissance 4",
url: "/connect-4",
};
if (gameType === "snake")
return {
queue: snakeQueue,
activeGames: activeSnakeGames,
title: "Snake 1v1",
url: "/snake",
};
return { queue: [], activeGames: {} };
}
@@ -401,8 +513,10 @@ async function updateDiscordMessage(client, game, title, resultText = "") {
if (i % 3 === 0) gridText += "\n";
}
description = `### **❌ ${game.p1.name}** vs **${game.p2.name} ⭕**\n${gridText}`;
} else {
} else if (title === "Puissance 4") {
description = `**🔴 ${game.p1.name}** vs **${game.p2.name} 🟡**\n\n${formatConnect4BoardForDiscord(game.board)}`;
} else if (title === "Snake 1v1") {
description = `**🐍 ${game.p1.name}** (${game.p1.score}) vs (${game.p2.score}) **${game.p2.name}** `;
}
if (resultText) description += `\n### ${resultText}`;
@@ -438,6 +552,7 @@ function cleanupStaleGames() {
};
cleanup(activeTicTacToeGames, "TicTacToe");
cleanup(activeConnect4Games, "Connect4");
cleanup(activeSnakeGames, "Snake");
}
/* EMITS */

View File

@@ -211,7 +211,8 @@ export async function handleMarketOfferClosing(offerId, client) {
// Send notification in guild channel
try {
const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID);
const guild = await client.guilds.fetch(process.env.BOT_GUILD_ID);
const guildChannel = await guild.channels.fetch(process.env.BOT_CHANNEL_ID);
const embed = new EmbedBuilder()
.setTitle("🔔 Fin des enchères")
.setDescription(