From 12eac37226c99475f9a769273d00d78f76aee7e2 Mon Sep 17 00:00:00 2001 From: Milo Date: Wed, 28 Jan 2026 11:57:15 +0100 Subject: [PATCH 1/2] snake --- src/game/elo.js | 67 +++++++++++++++++------ src/game/state.js | 6 +++ src/server/socket.js | 123 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 175 insertions(+), 21 deletions(-) diff --git a/src/game/elo.js b/src/game/elo.js index c42b8fa..0e4c980 100644 --- a/src/game/elo.js +++ b/src/game/elo.js @@ -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(), + }); + } + + } /** diff --git a/src/game/state.js b/src/game/state.js index eefdb00..21f2f5a 100644 --- a/src/game/state.js +++ b/src/game/state.js @@ -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 --- diff --git a/src/server/socket.js b/src/server/socket.js index 611269e..a085569 100644 --- a/src/server/socket.js +++ b/src/server/socket.js @@ -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,6 +33,7 @@ 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)); @@ -68,6 +71,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 +199,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; + 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 }); + } +} + +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 +254,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 +263,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 +273,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 +309,7 @@ async function createGame(client, gameType) { gameOver: false, lastmove: Date.now(), }; - } else { - // connect4 + } else if (gameType === "connect4") { lobby = { p1: { id: p1Id, @@ -272,6 +329,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 +410,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 +464,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 +511,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.name} 🐍** (${game.p2.score})`; } if (resultText) description += `\n### ${resultText}`; @@ -438,6 +550,7 @@ function cleanupStaleGames() { }; cleanup(activeTicTacToeGames, "TicTacToe"); cleanup(activeConnect4Games, "Connect4"); + cleanup(activeSnakeGames, "Snake"); } /* EMITS */ From 4dd5be7e2f2eeeac6862950806e0cf9993275c85 Mon Sep 17 00:00:00 2001 From: milo Date: Wed, 28 Jan 2026 17:17:25 +0100 Subject: [PATCH 2/2] hell yeah --- src/server/routes/api.js | 77 ++++++++++++++++++++++++++++++++++++++- src/server/socket.js | 8 ++-- src/utils/marketNotifs.js | 3 +- 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/server/routes/api.js b/src/server/routes/api.js index de8a493..931e139 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -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) => { diff --git a/src/server/socket.js b/src/server/socket.js index a085569..aa82a73 100644 --- a/src/server/socket.js +++ b/src/server/socket.js @@ -36,6 +36,8 @@ export function initializeSocket(server, 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 () => { @@ -210,7 +212,7 @@ async function onSnakeGameStateUpdate(client, eventData) { player.snake = snake; player.food = food; player.score = score; - player.gameOver = gameOver; + player.gameOver = gameOver === true ? true : false; player.win = win; lobby.lastmove = Date.now(); @@ -245,7 +247,7 @@ async function onSnakeGameStateUpdate(client, eventData) { } } -async function onGameOver(client, gameType, playerId, winnerId, reason = "", scores = null) { +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; @@ -514,7 +516,7 @@ async function updateDiscordMessage(client, game, title, resultText = "") { } 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.name} 🐍** (${game.p2.score})`; + description = `**🐍 ${game.p1.name}** (${game.p1.score}) vs (${game.p2.score}) **${game.p2.name}** `; } if (resultText) description += `\n### ${resultText}`; diff --git a/src/utils/marketNotifs.js b/src/utils/marketNotifs.js index 6f9baec..6413b2d 100644 --- a/src/utils/marketNotifs.js +++ b/src/utils/marketNotifs.js @@ -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(