Skip to content

Commit

Permalink
backend: Complete the event propagation mechanism.
Browse files Browse the repository at this point in the history
- A client can send a polling get request at game/events.
 If there are events for the client, they are flushed immediately.

- A client can post a game event at game/events.
 The active game of the client is fetched, and the event is dispatched
 on the game. The change is then enqueued to be sent to
  all the current players' clients.
  • Loading branch information
kuv2707 committed Jun 12, 2024
1 parent ee3bf21 commit af69bc7
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 31 deletions.
21 changes: 0 additions & 21 deletions backend/src/controllers/gameControllers.js

This file was deleted.

37 changes: 37 additions & 0 deletions backend/src/controllers/gameControllers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Response } from 'express';
import { enqueueForSend } from '../eventRecipients';
import { AuthRequest } from '../middlewares/authMiddleware';
import { retrieveGame } from '../gameStore';

export async function handleGameEvent(req: AuthRequest, res: Response) {
const event = req.body;
const activeGameId = req.user.activeGameId;
if (!activeGameId) {
res.status(404).send({
message: 'User is not actively playing any game',
});
return;
}
const game = retrieveGame(activeGameId);
if (!game) {
res.status(404).send({ message: 'Game not found' });
return;
}
//todo: When game data is retrieved from database, it is not an instance of GameEngine
// so we would need to convert it to an instance of GameEngine
const result = game.dispatchEvent(event);
if (result.type === 'ERROR') {
res.status(400).send({ message: result.message });
return;
} else {
// the game state after a successful event is propagated to all clients
// we can choose to relay the event received, so that the clients apply the event
// to their local game state, but that would be an extra implementation burden.
// Instead, we can just send the new game state to the clients.
// todo: send updated game state rather than event
for (const player of game.players) {
enqueueForSend(player.id, event);
}
res.status(200).send({ message: 'Event propagated to clients.' });
}
}
17 changes: 13 additions & 4 deletions backend/src/eventRecipients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ const clients = new Map<ClientId, Response>();
const eventQueue = new Map<ClientId, AppEvent[]>();

export function addClient(clientId: ClientId, res: Response) {
// todo: We can immediately send any events that are in the queue.
// if there are new events for this client, we will send them immediately
// else we will withhold the response object until there are new events
clients.set(clientId, res);
eventQueue.set(clientId, []);
doSendEvents(clientId);
}

export function removeClient(clientId: ClientId) {
Expand All @@ -32,8 +33,16 @@ export function getAllClients(): ClientId[] {
return Array.from(clients.keys());
}

export function scheduleSend(clientId: ClientId, event: AppEvent) {
eventQueue.get(clientId)?.push(event);
export function enqueueForSend(clientId: ClientId, event: AppEvent) {
if (!eventQueue.has(clientId)) {
// this is most probably not going to happen, as a polling request is expected
// to be made before any app event is sent to the client, but better safe than sorry
eventQueue.set(clientId, [event]);
} else {
eventQueue.get(clientId)?.push(event);
}
// send the events immediately if the client is waiting
doSendEvents(clientId);
}

export function doSendEvents(clientId: ClientId) {
Expand Down
5 changes: 5 additions & 0 deletions backend/src/models/userModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { NextFunction } from 'express';
export interface IUser extends mongoose.Document {
username: string;
password: string;
activeGameId: string | null;
isPasswordCorrect: (password: string) => Promise<boolean>;
}

Expand All @@ -20,6 +21,10 @@ const userSchema = new mongoose.Schema<IUser>(
required: [true, 'Password is required'],
minlength: [6, 'Password must be at least 6 characters long'],
},
activeGameId: {
type: String,
default: null,
},
},
{
timestamps: true,
Expand Down
13 changes: 9 additions & 4 deletions backend/src/routes/gameRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import express from 'express';
import { addClient } from '../eventRecipients';
import handleEvent from '../controllers/gameControllers';
import { handleGameEvent } from '../controllers/gameControllers';
import { AuthRequest, verifyToken } from '../middlewares/authMiddleware';

const router = express.Router();
router.use(verifyToken);

router.get('/events', (req, res) => {
addClient('user_id', res);
router.get('/events', function (req: AuthRequest, res) {
const clientId = req.user.id as string;
// note: we might need to use a different client id if the user is allowed to have multiple clients
// ie, the user is allowed to play multiple games on multiple devices at the same time
addClient(clientId, res);
});

router.post('/events', handleEvent);
router.post('/events', handleGameEvent);

export default router;
4 changes: 2 additions & 2 deletions backend/src/uno-game-engine/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ export class GameEngine {
return { type: 'ERROR', message: (error as Error).message };
}
}
dispatchEvent(event: GameEvent) {
dispatchEvent(event: GameEvent): EventResult {
// handle different types of events based on event.type
handleEvent(this, event);
return handleEvent(this, event);
}
}

0 comments on commit af69bc7

Please sign in to comment.