Skip to content

Commit

Permalink
Merge pull request #31 from AnoldGH/room
Browse files Browse the repository at this point in the history
Room game functionalities
  • Loading branch information
AnoldGH authored Dec 1, 2024
2 parents 0f05740 + 1588f41 commit 042764c
Show file tree
Hide file tree
Showing 16 changed files with 808 additions and 103 deletions.
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

0 comments on commit 042764c

Please sign in to comment.