Implement main game mechanic
This commit is contained in:
198
server/controller/game.js
Normal file
198
server/controller/game.js
Normal 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
|
||||
};
|
@ -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`);
|
||||
}
|
||||
@ -90,4 +100,25 @@ module.exports.disconnectUser = (userId) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
@ -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)) {
|
||||
io.to(roomId).emit("game-started");
|
||||
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");
|
||||
}
|
||||
} 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 });
|
||||
});
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user