Implement main game mechanic

This commit is contained in:
Mathias Wagner 2025-03-01 00:18:01 +01:00
parent 0a4a9a9d0e
commit 7d7dd263fe
8 changed files with 991 additions and 65 deletions

View File

@ -0,0 +1,117 @@
import { createContext, useState, useEffect, useCallback, useRef } from 'react';
import { io } from 'socket.io-client';
export const SocketContext = createContext();
export const SocketProvider = ({ children }) => {
const [socket, setSocket] = useState(null);
const [connected, setConnected] = useState(false);
const connectAttempts = useRef(0);
const maxAttempts = 3;
const reconnectDelay = 2000; // ms
// Connect to socket server
const connect = useCallback(() => {
if (socket) return;
// Determine server URL based on environment
const serverUrl = process.env.NODE_ENV === 'production'
? window.location.origin
: 'http://localhost:5287';
try {
const newSocket = io(serverUrl, {
reconnectionAttempts: 3,
timeout: 10000,
reconnectionDelay: 1000
});
newSocket.on('connect', () => {
console.log('Socket connected with ID:', newSocket.id);
setConnected(true);
connectAttempts.current = 0;
// Store player ID in localStorage for persistence
localStorage.setItem('playerId', newSocket.id);
});
newSocket.on('disconnect', (reason) => {
console.log('Socket disconnected:', reason);
setConnected(false);
});
newSocket.on('connect_error', (error) => {
console.error('Connection error:', error);
connectAttempts.current += 1;
if (connectAttempts.current >= maxAttempts) {
console.error('Max connection attempts reached, giving up');
newSocket.disconnect();
}
});
setSocket(newSocket);
} catch (err) {
console.error('Error creating socket connection:', err);
}
}, [socket]);
// Disconnect socket
const disconnect = useCallback(() => {
if (socket) {
socket.disconnect();
setSocket(null);
setConnected(false);
console.log('Socket manually disconnected');
}
}, [socket]);
// Send event through socket
const send = useCallback((event, data) => {
if (!socket || !connected) {
console.warn(`Cannot send event "${event}": socket not connected`);
return false;
}
try {
socket.emit(event, data);
return true;
} catch (err) {
console.error(`Error sending event "${event}":`, err);
return false;
}
}, [socket, connected]);
// Register event listener
const on = useCallback((event, callback) => {
if (!socket) {
console.warn(`Cannot listen for event "${event}": socket not initialized`);
return () => {};
}
socket.on(event, callback);
return () => socket.off(event, callback);
}, [socket]);
// Clean up socket on component unmount
useEffect(() => {
return () => {
if (socket) {
socket.disconnect();
}
};
}, [socket]);
return (
<SocketContext.Provider value={{
socket,
connected,
connect,
disconnect,
send,
on
}}>
{children}
</SocketContext.Provider>
);
};

View File

@ -1,19 +1,144 @@
import "./styles.sass";
import {StateContext} from "@/common/contexts/StateContext";
import {SocketContext} from "@/common/contexts/SocketContext";
import {useContext, useState, useEffect, useRef} from "react";
import MusicSlider from "@/pages/Game/components/MusicSlider";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faMessage} from "@fortawesome/free-solid-svg-icons";
import {faMessage, faMusic, faHeadphones, faClock, faCrown} from "@fortawesome/free-solid-svg-icons";
export const Game = () => {
const {setCurrentState} = useContext(StateContext);
const {send, on, socket} = useContext(SocketContext);
const [role, setRole] = useState(null); // 'composer' or 'guesser'
const [round, setRound] = useState(1);
const [phase, setPhase] = useState("waiting"); // waiting, composing, guessing, results
const [timeLeft, setTimeLeft] = useState(30);
const [frequency, setFrequency] = useState(440);
const [currentSong, setCurrentSong] = useState(null);
const [songOptions, setSongOptions] = useState([]);
const [scores, setScores] = useState({});
const [selectedSong, setSelectedSong] = useState(null);
const [guessResult, setGuessResult] = useState(null);
const [isHost, setIsHost] = useState(false);
// Chat state
const [messages, setMessages] = useState([]);
const [inputValue, setInputValue] = useState("");
const [connectedUsers, setConnectedUsers] = useState([]);
const [username, setUsername] = useState("");
const messageEndRef = useRef(null);
const timerIntervalRef = useRef(null);
useEffect(() => {
setPhase("waiting");
const handleRolesAssigned = (roles) => {
console.log("Roles assigned:", roles);
const myRole = roles[socket?.id];
if (myRole) {
setRole(myRole);
setMessages(prev => [...prev, {
system: true,
text: myRole === "composer"
? "Du bist der Komponist! Spiele den Song mit dem Tonregler."
: "Du bist ein Rater! Höre die Frequenzen und versuche, den Song zu erkennen."
}]);
} else {
console.error("No role assigned to this player!");
}
};
const handleSongSelected = (song) => {
console.log("Song selected:", song);
setCurrentSong(song);
};
const handleRoundStarted = (data) => {
console.log("Round started:", data);
setRound(data.round);
setPhase("composing");
setTimeLeft(data.timeRemaining);
};
const handleGuessingPhaseStarted = (data) => {
console.log("Guessing phase started:", data);
setPhase("guessing");
setTimeLeft(data.timeRemaining);
setSongOptions(data.songOptions);
};
const handleGuessResult = (result) => {
console.log("Guess result:", result);
setGuessResult(result.isCorrect);
setCurrentSong(result.correctSong);
};
const handleRoundResults = (results) => {
console.log("Round results:", results);
setPhase("results");
setScores(results.scores);
if (!currentSong) {
setCurrentSong(results.selectedSong);
}
};
const handleRoomUsers = (users) => {
console.log("Room users:", users);
setConnectedUsers(users);
const currentUser = users.find(u => u.id === socket?.id);
if (currentUser) {
setUsername(currentUser.name);
}
};
const handleHostStatus = (status) => {
setIsHost(status.isHost);
};
const cleanupRolesAssigned = on("roles-assigned", handleRolesAssigned);
const cleanupSongSelected = on("song-selected", handleSongSelected);
const cleanupRoundStarted = on("round-started", handleRoundStarted);
const cleanupGuessingPhaseStarted = on("guessing-phase-started", handleGuessingPhaseStarted);
const cleanupGuessResult = on("guess-result", handleGuessResult);
const cleanupRoundResults = on("round-results", handleRoundResults);
const cleanupRoomUsers = on("room-users-update", handleRoomUsers);
const cleanupHostStatus = on("host-status", handleHostStatus);
send("get-room-users");
send("check-host-status");
return () => {
cleanupRolesAssigned();
cleanupSongSelected();
cleanupRoundStarted();
cleanupGuessingPhaseStarted();
cleanupGuessResult();
cleanupRoundResults();
cleanupRoomUsers();
cleanupHostStatus();
};
}, [socket, on, send]);
useEffect(() => {
if (timerIntervalRef.current) {
clearInterval(timerIntervalRef.current);
}
if (phase !== "waiting" && phase !== "results") {
timerIntervalRef.current = setInterval(() => {
setTimeLeft(prev => Math.max(0, prev - 1));
}, 1000);
}
return () => {
if (timerIntervalRef.current) {
clearInterval(timerIntervalRef.current);
}
};
}, [phase]);
useEffect(() => {
const handleChatMessage = (messageData) => {
@ -41,31 +166,36 @@ export const Game = () => {
});
};
if (socket && socket.id) {
send("get-user-info");
const handleFrequencyUpdate = (data) => {
if (role === "guesser") {
setFrequency(data.frequency);
}
const handleUserInfo = (userInfo) => {
setUsername(userInfo.name);
};
const cleanupChatMessage = on("chat-message", handleChatMessage);
const cleanupUserConnected = on("user-connected", handleUserConnected);
const cleanupUserDisconnected = on("user-disconnected", handleUserDisconnected);
const cleanupUserInfo = on("user-info", handleUserInfo);
const cleanupFrequencyUpdate = on("frequency-update", handleFrequencyUpdate);
return () => {
cleanupChatMessage();
cleanupUserConnected();
cleanupUserDisconnected();
cleanupUserInfo();
cleanupFrequencyUpdate();
};
}, [on, send, socket]);
}, [on, role]);
useEffect(() => {
messageEndRef.current?.scrollIntoView({behavior: "smooth"});
}, [messages]);
const handleFrequencyChange = (newFrequency) => {
setFrequency(newFrequency);
if (role === "composer") {
send("submit-frequency", { frequency: newFrequency });
}
};
const handleSendMessage = () => {
if (inputValue.trim()) {
const messageData = {
@ -78,23 +208,191 @@ export const Game = () => {
}
};
const handleSongSelect = (song) => {
setSelectedSong(song);
};
const handleNextRound = () => {
send("next-round");
setSelectedSong(null);
setGuessResult(null);
setTimeLeft(0);
};
const renderPhaseContent = () => {
switch (phase) {
case "waiting":
return (
<div className="waiting-phase">
<h3>Warten auf Spielstart...</h3>
</div>
);
case "composing":
return (
<div className="composing-phase">
<div className="phase-header">
<h3>Runde {round}: {role === "composer" ? "Spielen" : "Zuhören"}</h3>
<div className="timer">
<FontAwesomeIcon icon={faClock} /> {timeLeft}s
</div>
</div>
{role === "composer" && currentSong && (
<div className="song-display">
<div className="song-card">
<img src={currentSong.coverUrl} alt={currentSong.title} />
<div className="song-info">
<div className="song-names">{currentSong.title}</div>
<div className="song-description">von {currentSong.artist}</div>
</div>
</div>
<p className="instruction">Spiele diesen Song mit dem Tonregler!</p>
</div>
)}
{role === "guesser" && (
<div className="listening-display">
<div className="listening-icon">
<FontAwesomeIcon icon={faHeadphones} size="4x" />
</div>
<p className="instruction">Höre genau zu und versuche, den Song zu erkennen!</p>
</div>
)}
</div>
);
case "guessing":
return (
<div className="guessing-phase">
<div className="phase-header">
<h3>Runde {round}: Song erraten</h3>
<div className="timer">
<FontAwesomeIcon icon={faClock} /> {timeLeft}s
</div>
</div>
{role === "composer" && (
<div className="waiting-for-guessers">
<p>Die Rater versuchen nun, deinen Song zu erraten...</p>
</div>
)}
{role === "guesser" && (
<div className="song-options">
<p className="instruction">Welchen Song hat der Komponist gespielt?</p>
<div className="song-grid">
{songOptions.map(song => (
<div
key={song.id}
className={`song-option ${selectedSong?.id === song.id ? 'selected' : ''}`}
onClick={() => handleSongSelect(song)}
>
<img src={song.coverUrl} alt={song.title} />
<div className="song-details">
<div className="song-title">{song.title}</div>
<div className="song-artist">{song.artist}</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
case "results":
return (
<div className="results-phase">
<h3>Runde {round}: Ergebnisse</h3>
<div className="round-results">
{role === "composer" && (
<div className="composer-results">
<p>Die Rater haben versucht, deinen Song zu erraten.</p>
</div>
)}
{role === "guesser" && (
<div className="guesser-results">
{currentSong && (
<div className="correct-song">
<p>Der richtige Song war:</p>
<div className="song-card highlight">
<img src={currentSong.coverUrl} alt={currentSong.title} />
<div className="song-info">
<div className="song-names">{currentSong.title}</div>
<div className="song-description">von {currentSong.artist}</div>
</div>
</div>
</div>
)}
{guessResult !== null && (
<div className={`guess-result ${guessResult ? 'correct' : 'incorrect'}`}>
{guessResult
? 'Richtig! Du erhälst 10 Punkte.'
: 'Leider falsch. Kein Punkt.'}
</div>
)}
</div>
)}
<div className="scoreboard">
<h4>Punktestand</h4>
<div className="scores">
{Object.entries(scores).map(([userId, score]) => {
const user = connectedUsers.find(u => u.id === userId) || { name: 'Unbekannt' };
return (
<div key={userId} className="score-entry">
<span className="player-name">
{user.id === (socket?.id || "you") && "👉 "}
{user.name}
{userId === connectedUsers.find(u => u.id === "user1")?.id && (
<FontAwesomeIcon icon={faCrown} className="host-icon" />
)}
</span>
<span className="player-score">{score}</span>
</div>
);
})}
</div>
</div>
{isHost && (
<button className="next-round-button" onClick={handleNextRound}>
Nächste Runde
</button>
)}
{!isHost && <p className="waiting-message">Warten auf Rundenwechsel durch Host...</p>}
</div>
</div>
);
default:
return <div>Unknown phase</div>;
}
};
return (
<div className="game-page">
<div className="background-overlay">
<div className="rotating-gradient"></div>
</div>
<div className="main-content">
<div className="song-display">
<div className="game-header">
<div className="game-role">
<FontAwesomeIcon icon={role === "composer" ? faMusic : faHeadphones} />
<span>{role === "composer" ? "Komponist" : "Rater"}</span>
</div>
<h2>ToneGuessr</h2>
<div className="song-card">
<img src="https://mir-s3-cdn-cf.behance.net/project_modules/1400/fe529a64193929.5aca8500ba9ab.jpg" alt="Song"/>
<div className="song-info">
<div className="song-names">Black Steam</div>
<div className="song-description">von Carrot Quest GmbH</div>
</div>
</div>
<div className="game-round">Runde {round}</div>
</div>
<div className="main-content">
{renderPhaseContent()}
<div className="chat-window">
<div className="chat-header">
<FontAwesomeIcon icon={faMessage} />
@ -125,7 +423,12 @@ export const Game = () => {
</div>
</div>
</div>
<MusicSlider/>
<MusicSlider
isReadOnly={role !== "composer" || phase !== "composing"}
onFrequencyChange={handleFrequencyChange}
frequency={frequency}
/>
</div>
);
}

View File

@ -1,8 +1,8 @@
import {useEffect, useRef, useState} from "react";
import "./styles.sass";
export const MusicSlider = () => {
const [frequency, setFrequency] = useState(440);
export const MusicSlider = ({ isReadOnly = false, onFrequencyChange, frequency: externalFrequency }) => {
const [frequency, setFrequency] = useState(externalFrequency || 440);
const [isPlaying, setIsPlaying] = useState(false);
const [dragging, setDragging] = useState(false);
const audioContextRef = useRef(null);
@ -10,7 +10,19 @@ export const MusicSlider = () => {
const gainNodeRef = useRef(null);
const sliderRef = useRef(null);
useEffect(() => {
if (externalFrequency !== undefined && !dragging) {
setFrequency(externalFrequency);
if (!isPlaying && !isReadOnly) {
startAudio();
}
}
}, [externalFrequency, dragging, isPlaying, isReadOnly]);
const handleMouseDown = (e) => {
if (isReadOnly) return;
setDragging(true);
startAudio();
handleFrequencyChange(e);
@ -18,15 +30,23 @@ export const MusicSlider = () => {
const handleMouseUp = () => {
setDragging(false);
if (!isReadOnly) {
stopAudio();
}
};
const handleFrequencyChange = (e) => {
if (isReadOnly) return;
const rect = sliderRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const clampedX = Math.max(0, Math.min(x, rect.width));
const newFrequency = 20 + (clampedX / rect.width) * 1980;
setFrequency(newFrequency);
if (onFrequencyChange) {
onFrequencyChange(newFrequency);
}
};
useEffect(() => {
@ -80,9 +100,25 @@ export const MusicSlider = () => {
setIsPlaying(false);
};
useEffect(() => {
return () => {
if (oscillatorRef.current) {
oscillatorRef.current.stop();
oscillatorRef.current.disconnect();
}
if (audioContextRef.current && audioContextRef.current.state !== 'closed') {
audioContextRef.current.close();
}
};
}, []);
return (
<div className="otamatone-container">
<div className="otamatone" onMouseDown={handleMouseDown}>
<div className={`otamatone-container ${isReadOnly ? 'read-only' : ''}`}>
<div
className="otamatone"
onMouseDown={handleMouseDown}
style={{ cursor: isReadOnly ? 'default' : 'pointer' }}
>
<div className="otamatone-face">
<div className="otamatone-mouth" style={{
height: `${10 + (frequency / 2000) * 40}px`,

View File

@ -281,6 +281,86 @@
opacity: 1
animation: rotate-background 5s linear infinite
.game-header
display: flex
justify-content: space-between
align-items: center
width: 100%
padding: 10px 20px
margin-bottom: 20px
border-bottom: 1px solid rgba(255, 255, 255, 0.2)
.game-role
display: flex
align-items: center
background: rgba(255, 255, 255, 0.1)
padding: 8px 15px
border-radius: 20px
border: 1px solid rgba(255, 255, 255, 0.2)
svg
margin-right: 8px
color: $yellow
.game-round
background: rgba(255, 255, 255, 0.1)
padding: 8px 15px
border-radius: 20px
border: 1px solid rgba(255, 255, 255, 0.2)
.phase-header
display: flex
justify-content: space-between
align-items: center
width: 100%
margin-bottom: 20px
.timer
background: rgba(255, 255, 255, 0.1)
padding: 10px 15px
border-radius: 20px
font-size: 18px
display: flex
align-items: center
svg
margin-right: 8px
color: $yellow
.song-grid
display: grid
grid-template-columns: repeat(3, 1fr)
gap: 20px
margin-top: 20px
.song-option
background: rgba(255, 255, 255, 0.1)
border-radius: 15px
overflow: hidden
cursor: pointer
transition: all 0.3s ease
border: 2px solid transparent
&:hover
transform: translateY(-5px)
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3)
&.selected
border: 2px solid $yellow
box-shadow: 0 0 20px rgba(255, 255, 0, 0.5)
img
width: 100%
height: 120px
object-fit: cover
.song-details
padding: 10px
.song-title
font-weight: bold
font-size: 16px
@keyframes subtle-text-glow
0%, 100%
text-shadow: 0 0 10px rgba(255, 255, 255, 0.4)

198
server/controller/game.js Normal file
View File

@ -0,0 +1,198 @@
const roomController = require('./room');
const SONGS = [
{ id: 1, title: "Black Steam", artist: "Carrot Quest GmbH", coverUrl: "https://mir-s3-cdn-cf.behance.net/project_modules/1400/fe529a64193929.5aca8500ba9ab.jpg" },
{ id: 2, title: "Sunset Dreams", artist: "Ocean Waves", coverUrl: "https://place-hold.it/500x500/" },
{ id: 3, title: "Neon Nights", artist: "Electric Avenue", coverUrl: "https://place-hold.it/500x500/" },
{ id: 4, title: "Mountain Echo", artist: "Wild Terrain", coverUrl: "https://place-hold.it/500x500/" },
{ id: 5, title: "Urban Jungle", artist: "City Dwellers", coverUrl: "https://place-hold.it/500x500/" },
{ id: 6, title: "Cosmic Journey", artist: "Stargazers", coverUrl: "https://place-hold.it/500x500/" }
];
const gameStates = {};
const initializeGameState = (roomId) => {
if (!gameStates[roomId]) {
gameStates[roomId] = {
round: 0,
phase: 'waiting',
roles: {},
selectedSong: null,
scores: {},
songOptions: [],
guessResults: {},
currentComposer: null,
roundStartTime: null,
phaseTimeLimit: {
composing: 30,
guessing: 30
},
lastFrequency: 440,
};
const users = roomController.getRoomUsers(roomId);
users.forEach(user => {
gameStates[roomId].scores[user.id] = 0;
});
}
return gameStates[roomId];
};
const startNewRound = (roomId) => {
const gameState = gameStates[roomId];
if (!gameState) return false;
gameState.round += 1;
gameState.phase = 'composing';
gameState.guessResults = {};
gameState.roundStartTime = Date.now();
const users = roomController.getRoomUsers(roomId);
if (users.length < 2) return false;
console.log(`Starting round ${gameState.round} in room ${roomId} with ${users.length} users`);
gameState.roles = {};
if (gameState.round === 1 || !gameState.currentComposer) {
const composerIndex = Math.floor(Math.random() * users.length);
gameState.currentComposer = users[composerIndex].id;
} else {
const currentIndex = users.findIndex(user => user.id === gameState.currentComposer);
if (currentIndex === -1) {
const composerIndex = Math.floor(Math.random() * users.length);
gameState.currentComposer = users[composerIndex].id;
} else {
const nextIndex = (currentIndex + 1) % users.length;
gameState.currentComposer = users[nextIndex].id;
}
}
users.forEach(user => {
gameState.roles[user.id] = user.id === gameState.currentComposer ? 'composer' : 'guesser';
console.log(`User ${user.name} (${user.id}) assigned role: ${gameState.roles[user.id]}`);
});
gameState.selectedSong = SONGS[Math.floor(Math.random() * SONGS.length)];
const songOptions = [...SONGS].sort(() => Math.random() - 0.5).slice(0, 5);
if (!songOptions.includes(gameState.selectedSong)) {
songOptions[Math.floor(Math.random() * songOptions.length)] = gameState.selectedSong;
}
gameState.songOptions = songOptions;
return true;
};
const getTimeRemaining = (roomId) => {
const gameState = gameStates[roomId];
if (!gameState || !gameState.roundStartTime) return 0;
const phaseDuration = gameState.phaseTimeLimit[gameState.phase] * 1000;
const timeElapsed = Date.now() - gameState.roundStartTime;
const timeRemaining = Math.max(0, phaseDuration - timeElapsed);
return Math.ceil(timeRemaining / 1000);
};
const advancePhase = (roomId) => {
const gameState = gameStates[roomId];
if (!gameState) return false;
if (gameState.phase === 'composing') {
gameState.phase = 'guessing';
gameState.roundStartTime = Date.now();
return true;
} else if (gameState.phase === 'guessing') {
gameState.phase = 'results';
return true;
} else if (gameState.phase === 'results') {
return startNewRound(roomId);
}
return false;
};
const updateFrequency = (roomId, frequency) => {
if (!gameStates[roomId]) return false;
gameStates[roomId].lastFrequency = frequency;
return true;
};
const getCurrentFrequency = (roomId) => {
return gameStates[roomId]?.lastFrequency || 440;
};
const submitGuess = (roomId, userId, songId) => {
const gameState = gameStates[roomId];
if (!gameState || gameState.phase !== 'guessing' || gameState.roles[userId] !== 'guesser') {
return false;
}
const isCorrect = gameState.selectedSong.id === songId;
gameState.guessResults[userId] = {
songId,
isCorrect,
points: isCorrect ? 10 : 0
};
if (isCorrect) {
gameState.scores[userId] = (gameState.scores[userId] || 0) + 10;
}
return {
isCorrect,
correctSong: gameState.selectedSong,
points: isCorrect ? 10 : 0
};
};
const getRoundResults = (roomId) => {
const gameState = gameStates[roomId];
if (!gameState) return null;
return {
round: gameState.round,
selectedSong: gameState.selectedSong,
guessResults: gameState.guessResults,
scores: gameState.scores
};
};
const getRoles = (roomId) => {
return gameStates[roomId]?.roles || {};
};
const getUserRole = (roomId, userId) => {
return gameStates[roomId]?.roles[userId] || null;
};
const getSongOptions = (roomId) => {
return gameStates[roomId]?.songOptions || [];
};
const getSelectedSong = (roomId) => {
return gameStates[roomId]?.selectedSong || null;
};
const cleanupGameState = (roomId) => {
delete gameStates[roomId];
};
module.exports = {
initializeGameState,
startNewRound,
getTimeRemaining,
advancePhase,
updateFrequency,
getCurrentFrequency,
submitGuess,
getRoundResults,
getRoles,
getUserRole,
getSongOptions,
getSelectedSong,
cleanupGameState
};

View File

@ -1,3 +1,9 @@
let cleanupGameState;
const setCleanupGameState = (cleanupFunction) => {
cleanupGameState = cleanupFunction;
};
let rooms = {};
module.exports.roomExists = (roomId) => rooms[roomId] !== undefined;
@ -76,6 +82,7 @@ module.exports.disconnectUser = (userId) => {
if (memberIndex !== -1) {
if (room.members[memberIndex].creator && room.members.length > 1) {
// Transfer host status to the next user
room.members[1].creator = true;
}
@ -83,6 +90,9 @@ module.exports.disconnectUser = (userId) => {
console.log(`User ${userId} disconnected from room ${roomId}`);
if (room.members.length === 0) {
if (cleanupGameState) {
cleanupGameState(roomId);
}
delete rooms[roomId];
console.log(`Room ${roomId} deleted because it's empty`);
}
@ -91,3 +101,24 @@ module.exports.disconnectUser = (userId) => {
}
}
}
module.exports.validateRoomMembers = (io, roomId) => {
if (!rooms[roomId]) return [];
const validMembers = [];
const connectedSockets = io.sockets.adapter.rooms.get(roomId) || new Set();
rooms[roomId].members = rooms[roomId].members.filter(member => {
const stillConnected = connectedSockets.has(member.id);
if (stillConnected) {
validMembers.push(member);
} else {
console.log(`Removing disconnected user ${member.name} (${member.id}) from room ${roomId}`);
}
return stillConnected;
});
return validMembers;
}
module.exports.setCleanupGameState = setCleanupGameState;

View File

@ -1,23 +1,79 @@
const {
connectUserToRoom,
roomExists,
disconnectUser,
getUserRoom,
getRoomUsers,
isRoomOpen,
startGame,
isUserHost
} = require("../controller/room");
const roomController = require("../controller/room");
const gameController = require("../controller/game");
module.exports = (io) => (socket) => {
let currentRoomId = null;
let currentUser = null;
let phaseTimers = {};
const clearRoomTimers = (roomId) => {
if (phaseTimers[roomId]) {
clearTimeout(phaseTimers[roomId]);
delete phaseTimers[roomId];
}
};
const startPhaseTimer = (roomId) => {
clearRoomTimers(roomId);
const timeRemaining = gameController.getTimeRemaining(roomId) * 1000;
phaseTimers[roomId] = setTimeout(() => {
const advanced = gameController.advancePhase(roomId);
if (advanced) {
const currentPhase = advanced.phase || 'results';
if (currentPhase === 'guessing') {
const roles = gameController.getRoles(roomId);
const songOptions = gameController.getSongOptions(roomId);
const timeLeft = gameController.getTimeRemaining(roomId);
Object.entries(roles).forEach(([userId, role]) => {
if (role === 'guesser') {
io.to(userId).emit('guessing-phase-started', {
timeRemaining: timeLeft,
songOptions
});
}
});
startPhaseTimer(roomId);
}
else if (currentPhase === 'results') {
const results = gameController.getRoundResults(roomId);
io.to(roomId).emit('round-results', results);
}
else if (typeof advanced === 'boolean' && advanced) {
const roles = gameController.getRoles(roomId);
const selectedSong = gameController.getSelectedSong(roomId);
const timeLeft = gameController.getTimeRemaining(roomId);
io.to(roomId).emit('roles-assigned', roles);
Object.entries(roles).forEach(([userId, role]) => {
if (role === 'composer') {
io.to(userId).emit('song-selected', selectedSong);
}
});
io.to(roomId).emit('round-started', {
round: gameController.getRoundResults(roomId).round,
timeRemaining: timeLeft
});
startPhaseTimer(roomId);
}
}
}, timeRemaining);
};
socket.on("disconnect", () => {
const roomId = getUserRoom(socket.id);
const roomId = roomController.getUserRoom(socket.id);
if (roomId) socket.to(roomId).emit("user-disconnected", socket.id);
disconnectUser(socket.id);
roomController.disconnectUser(socket.id);
});
socket.on("join-room", ({roomId, name}) => {
@ -25,16 +81,16 @@ module.exports = (io) => (socket) => {
roomId = roomId.toString().toUpperCase();
if (roomExists(roomId)) {
if (!isRoomOpen(roomId)) {
if (roomController.roomExists(roomId)) {
if (!roomController.isRoomOpen(roomId)) {
return socket.emit("room-closed", roomId);
}
currentUser = {id: socket.id, name: name.toString()};
connectUserToRoom(roomId, currentUser);
roomController.connectUserToRoom(roomId, currentUser);
socket.join(roomId);
const users = getRoomUsers(roomId);
const users = roomController.getRoomUsers(roomId);
io.to(roomId).emit("room-users-update", users);
socket.to(roomId).emit("user-connected", currentUser);
@ -49,26 +105,51 @@ module.exports = (io) => (socket) => {
socket.on("create-room", ({name}) => {
if (!name) return socket.emit("room-name-required");
const roomId = Math.random().toString(36).substring(7).toUpperCase();
currentUser = {id: socket.id, name: name?.toString()};
connectUserToRoom(roomId, currentUser);
currentUser = {id: socket.id, name: name?.toString(), creator: true};
roomController.connectUserToRoom(roomId, currentUser);
socket.join(roomId);
socket.emit("room-created", roomId);
currentRoomId = roomId;
});
socket.on("start-game", () => {
const roomId = getUserRoom(socket.id);
if (roomId && isUserHost(socket.id)) {
if (startGame(roomId)) {
const roomId = roomController.getUserRoom(socket.id);
if (roomId && roomController.isUserHost(socket.id)) {
roomController.validateRoomMembers(io, roomId);
const roomUsers = roomController.getRoomUsers(roomId);
if (roomController.startGame(roomId)) {
gameController.initializeGameState(roomId);
if (gameController.startNewRound(roomId)) {
const roles = gameController.getRoles(roomId);
const selectedSong = gameController.getSelectedSong(roomId);
io.to(roomId).emit("game-started");
io.to(roomId).emit("roles-assigned", roles);
Object.entries(roles).forEach(([userId, role]) => {
if (role === 'composer') {
io.to(userId).emit("song-selected", selectedSong);
}
});
io.to(roomId).emit("round-started", {
round: 1,
timeRemaining: gameController.getTimeRemaining(roomId)
});
startPhaseTimer(roomId);
}
} else {
socket.emit("not-authorized");
}
}
});
socket.on("send-message", (messageData) => {
const roomId = getUserRoom(socket.id);
const roomId = roomController.getUserRoom(socket.id);
if (roomId) {
socket.to(roomId).emit("chat-message", messageData);
}
@ -81,15 +162,15 @@ module.exports = (io) => (socket) => {
});
socket.on("get-room-users", () => {
const roomId = getUserRoom(socket.id);
const roomId = roomController.getUserRoom(socket.id);
if (roomId) {
const users = getRoomUsers(roomId);
const users = roomController.getRoomUsers(roomId);
socket.emit("room-users", users);
}
});
socket.on("check-host-status", () => {
const isHost = isUserHost(socket.id);
const isHost = roomController.isUserHost(socket.id);
socket.emit("host-status", { isHost });
});
@ -98,4 +179,63 @@ module.exports = (io) => (socket) => {
socket.emit("room-code", currentRoomId);
}
});
socket.on("submit-frequency", ({ frequency }) => {
const roomId = roomController.getUserRoom(socket.id);
if (!roomId) return;
const userRole = gameController.getUserRole(roomId, socket.id);
if (userRole === 'composer') {
if (gameController.updateFrequency(roomId, frequency)) {
socket.to(roomId).emit("frequency-update", { frequency });
}
}
});
socket.on("submit-guess", ({ songId }) => {
const roomId = roomController.getUserRoom(socket.id);
if (!roomId) return;
const result = gameController.submitGuess(roomId, socket.id, songId);
if (result) {
socket.emit("guess-result", result);
}
});
socket.on("next-round", () => {
const roomId = roomController.getUserRoom(socket.id);
if (!roomId || !roomController.isUserHost(socket.id)) return;
roomController.validateRoomMembers(io, roomId);
const roomUsers = roomController.getRoomUsers(roomId);
if (gameController.startNewRound(roomId)) {
const roles = gameController.getRoles(roomId);
const selectedSong = gameController.getSelectedSong(roomId);
const timeLeft = gameController.getTimeRemaining(roomId);
io.to(roomId).emit("roles-assigned", roles);
Object.entries(roles).forEach(([userId, role]) => {
if (role === 'composer') {
io.to(userId).emit("song-selected", selectedSong);
}
});
io.to(roomId).emit("round-started", {
round: gameController.getRoundResults(roomId).round,
timeRemaining: timeLeft
});
startPhaseTimer(roomId);
}
});
socket.on("get-current-frequency", () => {
const roomId = roomController.getUserRoom(socket.id);
if (!roomId) return;
const frequency = gameController.getCurrentFrequency(roomId);
socket.emit("current-frequency", { frequency });
});
};

View File

@ -5,17 +5,38 @@ const app = express();
const path = require("path");
app.use(express.static(path.join(__dirname, './dist')));
app.disable("x-powered-by");
app.get('*', (req, res) => res.sendFile(path.join(__dirname, './dist', 'index.html')));
app.disable("x-powered-by");
const server = http.createServer(app);
const io = new Server(server, {cors: {origin: "*"}});
// Pass io to the connection handler
const io = new Server(server, {
cors: {origin: "*"},
pingTimeout: 30000,
pingInterval: 10000
});
const roomController = require('./controller/room');
const gameController = require('./controller/game');
roomController.setCleanupGameState(gameController.cleanupGameState);
// Handle socket connections
io.on("connection", require("./handler/connection")(io));
server.listen(5287, () => {
console.log("Server running on port 5287");
server.on('error', (error) => {
console.error('Server error:', error);
});
const PORT = process.env.PORT || 5287;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
process.on('SIGINT', () => {
console.log('Server shutting down...');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});