Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Room game functionalities #31

Merged
merged 53 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
7c36fd1
Room class for in-server storage
AnoldGH Nov 30, 2024
afe81d3
Basic functionality in creating rooms
AnoldGH Nov 30, 2024
582d3b0
Quicker room code access
AnoldGH Nov 30, 2024
bf9222b
Pass room code to frontend
AnoldGH Nov 30, 2024
90333ce
Join game functionality
AnoldGH Nov 30, 2024
456d1d9
Users correctly shown on each create/join
AnoldGH Nov 30, 2024
c16efaa
Use Player class to record room game player info
AnoldGH Nov 30, 2024
eddba55
Questionset generation
AnoldGH Nov 30, 2024
5d61c68
Room model & saveGameRecords
AnoldGH Nov 30, 2024
7dde818
Server-side verification
AnoldGH Nov 30, 2024
fb92c15
Fixes a saveGameRecords bug
AnoldGH Nov 30, 2024
9516798
Bug fixes related to saveGameRecords
AnoldGH Nov 30, 2024
f0d867b
Refactored proxy organization to better accommodate change of providers
AnoldGH Nov 30, 2024
95104a9
Bug fixes related to questionSet generation
AnoldGH Nov 30, 2024
632617e
Propagation of generated questionSets between front/back ends
AnoldGH Nov 30, 2024
5ac0fc3
Added client submission & server verification
AnoldGH Nov 30, 2024
bc50169
Room class for in-server storage
AnoldGH Nov 30, 2024
8b73337
Basic functionality in creating rooms
AnoldGH Nov 30, 2024
009538b
Quicker room code access
AnoldGH Nov 30, 2024
783f4b5
Pass room code to frontend
AnoldGH Nov 30, 2024
8920c6f
Join game functionality
AnoldGH Nov 30, 2024
919ae23
Users correctly shown on each create/join
AnoldGH Nov 30, 2024
bf7247b
Use Player class to record room game player info
AnoldGH Nov 30, 2024
b991a5f
Questionset generation
AnoldGH Nov 30, 2024
fe46f13
Room model & saveGameRecords
AnoldGH Nov 30, 2024
67a6117
Server-side verification
AnoldGH Nov 30, 2024
30c786c
Fixes a saveGameRecords bug
AnoldGH Nov 30, 2024
6485a7a
Bug fixes related to saveGameRecords
AnoldGH Nov 30, 2024
3bea0f5
Refactored proxy organization to better accommodate change of providers
AnoldGH Nov 30, 2024
efb9cc1
Bug fixes related to questionSet generation
AnoldGH Nov 30, 2024
6a9392d
Propagation of generated questionSets between front/back ends
AnoldGH Nov 30, 2024
aa5ab4c
Added client submission & server verification
AnoldGH Nov 30, 2024
854d3ba
Merge branch 'room' of https://github.com/AnoldGH/MuseGuesser into room
AnoldGH Nov 30, 2024
7f163f1
Room class for in-server storage
AnoldGH Nov 30, 2024
e638334
Basic functionality in creating rooms
AnoldGH Nov 30, 2024
6dafab8
Quicker room code access
AnoldGH Nov 30, 2024
6193869
Pass room code to frontend
AnoldGH Nov 30, 2024
b49ace1
Join game functionality
AnoldGH Nov 30, 2024
70541e0
Users correctly shown on each create/join
AnoldGH Nov 30, 2024
71c379c
Use Player class to record room game player info
AnoldGH Nov 30, 2024
d882693
Questionset generation
AnoldGH Nov 30, 2024
3bad42f
Room model & saveGameRecords
AnoldGH Nov 30, 2024
877681f
Server-side verification
AnoldGH Nov 30, 2024
48f6637
Fixes a saveGameRecords bug
AnoldGH Nov 30, 2024
e18dc36
Bug fixes related to saveGameRecords
AnoldGH Nov 30, 2024
33761a8
Bug fixes related to questionSet generation
AnoldGH Nov 30, 2024
e9e6701
Propagation of generated questionSets between front/back ends
AnoldGH Nov 30, 2024
7cbc36d
Added client submission & server verification
AnoldGH Nov 30, 2024
d60783e
Merge branch 'room' of https://github.com/AnoldGH/MuseGuesser into room
AnoldGH Nov 30, 2024
f0624d5
Timeout integration
AnoldGH Nov 30, 2024
2eb64ce
Disable smoothing effect when resetting
AnoldGH Nov 30, 2024
2859c23
Basic progress tracking - not including timeout
AnoldGH Nov 30, 2024
1588f41
Automatic cleanup of inactive rooms
AnoldGH Dec 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ const Song = require('./models/songModel.js');
const User = require('./models/userModel.js');
const DailyChallenge = require('./models/dailyChallenge.js')
const Match = require('./models/matchModel.js');
const RoomModel = require('./models/roomModel.js')

module.exports = {User, Song, DailyChallenge, Match};
module.exports = {User, Song, DailyChallenge, Match, RoomModel};
22 changes: 22 additions & 0 deletions backend/models/roomModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const mongoose = require("mongoose");

const roomSchema = new mongoose.Schema({
players: {
type: Array,
default: [] // { userId, score }
}
});

// Save game records
roomSchema.methods.saveGameRecords = async function (players) {
const playerRecords = players.map((player) => ({
userId: player.id,
score: player.score
}));

this.players = playerRecords;

return await this.save();
}

module.exports = mongoose.model("RoomModel", roomSchema)
124 changes: 123 additions & 1 deletion backend/routes/gameRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,135 @@ const { getAccessToken } = require("../utility/tokenManager");
const DailyChallenge = require("../models/dailyChallenge");
const User = require("../models/userModel");
const mongoose = require("mongoose");
const { Room } = require("../utility/room");
const { Player } = require("../utility/player");
const { generateQuestionSets } = require("../utility/gameManager");

function normalizeDate(date) {
const normalized = new Date(date);
normalized.setUTCHours(0, 0, 0, 0);
return normalized;
}

router.post("/joinRoom", async (req, res) => {
try {
const { code, userId } = req.body // Get room code & player infos

console.log("User %s requests to join room %s", userId, code) // complete log

let room = Room.getRoom(code)
if (room !== undefined) { // room exists
console.log("Room %s found.", code)
console.log(room)

let player;

// Player exists?
if (room.players.has(userId)) {
console.log("Player %s found.", userId)
player = room.players.get(userId)
} else {
console.log("Player %s not found. Creating new instance.", userId)
player = new Player(userId)
room.join(player) // join
}

// Response
const updatedQuestionSets = room.questionSets.map(({ correct, ...rest }) => rest);
console.log("Question sets generated", room.questionSets)

res.status(200).json({
code: code,
players: Array.from(room.players),
questionSets: updatedQuestionSets, // hide correct answers
progress: player.progress, // idx of question answered
score: player.score // player's current score
})
}
else {
console.log("Room %s not found, bad request.", code)

res.status(400) // bad request, room doesn't exist
throw new Error("Room doesn't exist")
// TODO: we can choose to create room with specific code
}

} catch (error) {
console.log("Error fetch room status:", error);
res.status(500).json({ error: "Internal server error." });
}
})

router.post("/createRoom", async (req, res) => {
try {
const { userId } = req.body // Get room code & player infos

// Create room
let newRoom = new Room()

let player = new Player(userId)
newRoom.join(player)

console.log("Created room with code", newRoom.getRoomCode())

// Generate question sets
newRoom.addQuestionSets(await generateQuestionSets(5)) // TODO: more options

console.log("Question sets generated", newRoom.questionSets)

// Response
const updatedQuestionSets = newRoom.questionSets.map(({ correct, ...rest }) => rest);

res.status(200).json({
code: newRoom.getRoomCode(),
questionSets: updatedQuestionSets // hide correct answers
})
} catch(error) {
console.error("Error creating room:", error);
res.status(500).json({ error: "Internal server error." });
}
})

router.post("/submitAnswer", async (req, res) => {
try {
const { code, userId, idx, choice, score } = req.body // Room code | userId | questionSet index | choice index

// Verify answer
let room = Room.getRoom(code)
let questionSet = room.questionSets[idx] // TODO: error handling

let player = room.players.get(userId)
if (player.answered(idx)) { // duplicate answer
player.pulse()
throw new Error("Duplicate submission")
} else {
let correct = questionSet.isCorrect(choice)

if (correct) { // correct answer
let player = room.players.get(userId)
player.addScore(score)
console.log("Room %s, player %s add %d score", code, userId, score)
} else { // wrong answer
let player = room.players.get(userId)
player.pulse()
console.log("Room %s, player %s wrong answer", code, userId)
}

player.updateProgress(idx) // player has answered question up to idx
console.log("Player %s progress updated to %d", userId, player.progress)

res.status(200).json({
correct: correct,
score: score
})
}

} catch(error) {
console.error("Error verifying answer:", error);
res.status(500).json({ error: "Internal server error." });
}
})

router.get("/dailyChallenge", async (req, res) => {
try {

Expand Down Expand Up @@ -42,7 +164,7 @@ router.post("/updateDailyScore", async(req, res) => {

// if (user.dailyScore !== -1) {
// return res.status(400).json({ error: "Daily challenge already completed." });
// }
// }

user.dailyScore = score;
await user.save();
Expand Down
4 changes: 4 additions & 0 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const server = http.createServer(app);

const db = require('./db.js'); // connect to database
const matchModel = require('./models/matchModel.js');
const { Room } = require('./utility/room.js');

// Routes
// --- Spotify access token
Expand All @@ -48,5 +49,8 @@ cron.schedule("0 0 * * *", async () => {
timezone: "America/Los_Angeles",
});

// Room Game Cleanup
Room.periodicCleanup() // by default 60 seconds

// uncomment this and run server to generate new dailychallenge questions
//runCronJob();
59 changes: 59 additions & 0 deletions backend/utility/gameManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const express = require('express')
require("dotenv").config()

const DumbProxy = require("../controller/dumbProxy")
// import { SpotifyProxy } from "../controller/spotifyProxy";
const { QuestionSet } = require("./questionSet")

// Spotify proxy
const proxy = DumbProxy.getInstance()

// Generate a question set
const generateQuestionSet = async(genre='pop', choices=4) => {
let tracks = []
let withPreview = []

while (withPreview.length == 0) {
tracks = await proxy.recommendTracks(genre, choices*2) // increase the hit rate of preview_url
withPreview = tracks.filter((track) => track.preview_url)
}

const correctTrack = withPreview[0]
const incorrectTracks = tracks
.filter((track) => track !== correctTrack)
.sort(() => 0.5 - Math.random())
.slice(0, choices - 1);
const correct = Math.floor(Math.random() * choices)
const finalTracks = [
...incorrectTracks.slice(0, correct),
correctTrack,
...incorrectTracks.slice(correct)
]

let qs = new QuestionSet(finalTracks, correct) // generate question sets
return qs
}

// Generate question sets
const generateQuestionSets = async(count, genre='pop', choices=4) => {
let qss = []

for (let i = 0; i < count; i++) {
qss.push(await generateQuestionSet(genre, choices))
}

return qss
}

const test = async() => {
let proxy = DumbProxy.getInstance()
let qss = await generateQuestionSets(5)
console.log(qss)
}

// test()

module.exports = {
generateQuestionSet,
generateQuestionSets
}
46 changes: 46 additions & 0 deletions backend/utility/player.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Player model used in Rooms
class Player { // player is specific to room
static STATE = {
ACTIVE: 1,
INACTIVE: 0
}

constructor(userId) {
this.id = userId // unique identifier
this.score = 0 // starts with 0 score
this.progress = 0 // idx of question set the player has reached
this.state = Player.STATE.ACTIVE // 0 - inactive, 1 - active
this.lastUpdate = Date.now() // last update
}

// Is the player expired?
isExpired(expiration = 1000 * 60 * 5) { // default to be 5 minutes
return Date.now() > this.lastUpdate + expiration
}

// Add score
addScore(delta) {
this.score += delta
this.pulse()
}

// Answer question
updateProgress(idx) {
this.progress = idx + 1
this.pulse()
}

// Update the index of question set the player has reached
answered(idx) {
return idx < this.progress
}

// Pulse - update lastUpdate
pulse() {
this.lastUpdate = Date.now()
}
}

module.exports = {
Player
}
26 changes: 26 additions & 0 deletions backend/utility/questionSet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class QuestionSet { // Class for multiple-choice with one answer
static QID = 0 // unique identifier

constructor(tracks, correct) {
this.id = QuestionSet.QID++

this.options = tracks.filter((track) => {
return {
name: track.name,
artist: track.artists[0].name
}
})

this.correct = correct
this.url = tracks[correct].preview_url // url
}

// If choice is correct
isCorrect(choice) {
return choice == this.correct
}
}

module.exports = {
QuestionSet
}
Loading