Skip to content

Commit

Permalink
Create history when game ends (#177)
Browse files Browse the repository at this point in the history
* Remove unnecessary logging

* Add authentication to game gateway

* Use state update event

* Refactor to use update-status for join/leave message

* Add a guard for unauthenticated users

* Remove log event completely

* Create a game history when game ends
  • Loading branch information
takumihara authored Dec 26, 2023
1 parent 07efc2d commit b777c2e
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 39 deletions.
101 changes: 91 additions & 10 deletions backend/src/events/events.gateway.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Logger } from '@nestjs/common';
import { Logger, UseGuards } from '@nestjs/common';
import {
ConnectedSocket,
MessageBody,
Expand All @@ -7,10 +7,25 @@ import {
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { User } from '@prisma/client';
import { Namespace, Socket } from 'socket.io';
import { AuthService } from 'src/auth/auth.service';
import { HistoryService } from 'src/history/history.service';
import { UserGuardWs } from 'src/user/user.guard-ws';

const POINT_TO_WIN = 3;

type Status =
| 'too-many-players'
| 'joined-as-player'
| 'joined-as-viewer'
| 'ready'
| 'login-required'
| 'friend-joined'
| 'friend-left'
| 'won'
| 'lost';

type Scores = {
[key: string]: number;
};
Expand Down Expand Up @@ -49,36 +64,57 @@ const isPlayer = (players: Players, roomId: string, socketId: string) => {
namespace: '/pong',
})
export class EventsGateway implements OnGatewayDisconnect {
constructor(
private readonly authService: AuthService,
private readonly historyService: HistoryService,
) {}

@WebSocketServer()
private server: Namespace;
private logger: Logger = new Logger('EventsGateway');
private lostPoints: Scores = {};
private players: Players = {};
private users: { [socketId: string]: User } = {};

handleConnection(client: Socket) {
async handleConnection(client: Socket) {
this.logger.log(`connect: ${client.id} `);

const gameId = client.handshake.query['game_id'] as string;
const isPlayer = client.handshake.query['is_player'] == 'true';
const token = client.request.headers.cookie?.split('token=')[1];
let user;

if (token) {
try {
user = await this.authService.verifyAccessToken(token);
(client as any).user = user;
this.users[client.id] = user;
} catch {}
}

// Both of viewers and players join the Socket.io room
client.join(gameId);

if (!isPlayer) {
this.emitUpdateStatus(client, 'joined-as-viewer');
return;
}

if (!user) {
this.emitUpdateStatus(client, 'login-required');
return;
}

if (this.players[gameId] && Object.keys(this.players[gameId]).length == 2) {
this.logger.log(`full: ${gameId} ${client.id}`);
client.emit('log', 'The game is full. You joined as a viewer.');
this.emitUpdateStatus(client, 'too-many-players');
return;
}
addPlayer(this.players, gameId, client.id);
this.broadcastToRooms(client, 'join');
client.emit('log', 'You joined as a player.');
this.broadcastUpdateStatus(client, 'friend-joined');
this.emitUpdateStatus(client, 'joined-as-player');
if (Object.keys(this.players[gameId]).length == 2) {
console.log('here');
client.emit('log', 'Your friend is already here. You can start.');
this.emitUpdateStatus(client, 'ready');
}
this.lostPoints[client.id] = 0;
return;
Expand All @@ -88,14 +124,16 @@ export class EventsGateway implements OnGatewayDisconnect {
this.logger.log(`disconnect: ${client.id} `);
const roomId = client.handshake.query['game_id'] as string;
client.leave(roomId);
delete this.users[client.id];

if (isPlayer(this.players, roomId, client.id)) {
this.broadcastToRoom(client, roomId, 'leave');
this.broadcastUpdateStatus(client, 'friend-left');
removePlayer(this.players, roomId, client.id);
delete this.lostPoints[client.id];
}
}

@UseGuards(UserGuardWs)
@SubscribeMessage('start')
async start(
@MessageBody() data: { vx: number; vy: number },
Expand All @@ -109,6 +147,7 @@ export class EventsGateway implements OnGatewayDisconnect {
return;
}

@UseGuards(UserGuardWs)
@SubscribeMessage('left')
async left(
@MessageBody() data: string,
Expand All @@ -125,6 +164,7 @@ export class EventsGateway implements OnGatewayDisconnect {
return;
}

@UseGuards(UserGuardWs)
@SubscribeMessage('right')
async right(
@MessageBody() data: string,
Expand All @@ -139,6 +179,7 @@ export class EventsGateway implements OnGatewayDisconnect {
return;
}

@UseGuards(UserGuardWs)
@SubscribeMessage('bounce')
async bounce(
@MessageBody() data: string,
Expand All @@ -153,6 +194,7 @@ export class EventsGateway implements OnGatewayDisconnect {
return;
}

@UseGuards(UserGuardWs)
@SubscribeMessage('collide')
async collide(
@MessageBody() data: string,
Expand All @@ -167,9 +209,13 @@ export class EventsGateway implements OnGatewayDisconnect {
this.lostPoints[client.id]++;
if (this.lostPoints[client.id] == POINT_TO_WIN) {
this.broadcastToRooms(client, 'finish');
this.broadcastToRooms(client, 'log', 'You won the game.');
client.emit('finish');
client.emit('log', 'You lost the game.');

// TODO: handle viewers
this.broadcastUpdateStatus(client, 'won');
this.emitUpdateStatus(client, 'lost');

await this.createHistory(client);
}
return;
}
Expand All @@ -192,4 +238,39 @@ export class EventsGateway implements OnGatewayDisconnect {
if (data) socket.to(roomId).emit(eventName, data);
else socket.to(roomId).emit(eventName);
}

emitUpdateStatus(socket: Socket, status: Status) {
socket.emit('update-status', status);
}

broadcastUpdateStatus(socket: Socket, status: Status) {
const roomId = socket.handshake.query['game_id'];
socket.to(roomId).emit('update-status', status);
}

async createHistory(socket: Socket) {
const roomId = socket.handshake.query['game_id'] as string;
const loserSocketId = socket.id;
const loserUserId = this.users[loserSocketId].id;

const winnerSocketId = Object.keys(this.players[roomId]).find(
(sockedId) => sockedId != loserSocketId,
);

// TODO: handle invalid game. The opponent must have been disconnected.
if (!winnerSocketId) return;

const winnerUserId = this.users[winnerSocketId].id;

return await this.historyService.create({
winner: {
userId: winnerUserId,
score: this.lostPoints[loserSocketId],
},
loser: {
userId: loserUserId,
score: this.lostPoints[winnerSocketId],
},
});
}
}
3 changes: 2 additions & 1 deletion backend/src/events/events.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { EventsGateway } from './events.gateway';
import { PongMatchGateway } from './pong-match.gateway';
import { AuthModule } from 'src/auth/auth.module';
import { HistoryModule } from 'src/history/history.module';

@Module({
providers: [EventsGateway, PongMatchGateway],
imports: [AuthModule],
imports: [AuthModule, HistoryModule],
})
export class EventsModule {}
1 change: 1 addition & 0 deletions backend/src/history/history.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ import { HistoryService } from './history.service';
controllers: [HistoryController],
providers: [HistoryService],
imports: [PrismaModule],
exports: [HistoryService],
})
export class HistoryModule {}
109 changes: 109 additions & 0 deletions backend/test/pong-gateway.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Socket, io } from 'socket.io-client';
import { AppModule } from 'src/app.module';
import { TestApp } from './utils/app';
import { expectHistoryResponse } from './utils/matcher';

async function createNestApp(): Promise<INestApplication> {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

const app = moduleFixture.createNestApplication();
return app;
}

describe('ChatGateway and ChatController (e2e)', () => {
let app: TestApp;
let ws1: Socket; // Client socket 1
let ws2: Socket; // Client socket 2
let user1, user2;

beforeAll(async () => {
//app = await initializeApp();
const _app = await createNestApp();
await _app.listen(3000);
app = new TestApp(_app);
const dto1 = {
name: 'test-user1',
email: '[email protected]',
password: 'test-password',
};
const dto2 = {
name: 'test-user2',
email: '[email protected]',
password: 'test-password',
};
user1 = await app.createAndLoginUser(dto1);
user2 = await app.createAndLoginUser(dto2);
});

afterAll(async () => {
await app.deleteUser(user1.id, user1.accessToken).expect(204);
await app.deleteUser(user2.id, user2.accessToken).expect(204);
await app.close();
ws1.close();
ws2.close();
});

const connect = (ws: Socket) => {
return new Promise<void>((resolve) => {
ws.on('connect', () => {
resolve();
});
});
};

describe('connect', () => {
it('Connects to chat server', async () => {
ws1 = io('ws://localhost:3000/pong', {
extraHeaders: { cookie: 'token=' + user1.accessToken },
query: { game_id: 'test-game-id', is_player: true },
});
ws2 = io('ws://localhost:3000/pong', {
extraHeaders: { cookie: 'token=' + user2.accessToken },
query: { game_id: 'test-game-id', is_player: true },
});
expect(ws1).toBeDefined();
expect(ws2).toBeDefined();

// Wait for connection
await connect(ws1);
await connect(ws2);

expect(ws1.connected).toBeTruthy();
expect(ws2.connected).toBeTruthy();
});
});

describe('collide', () => {
it('creates a match result', async () => {
ws1.emit('collide');
ws1.emit('collide');
ws1.emit('collide');

await app
.getHistory(user1.id, user1.accessToken)
.expect(200)
.expect(expectHistoryResponse)
.expect((res) => {
expect(res.body).toHaveLength(1);
expect(res.body[0].result).toBe('COMPLETE');
expect(res.body[0].players).toHaveLength(2);
const result1 = res.body[0].players.find(
(player) => player.user.id === user1.id,
);
const result2 = res.body[0].players.find(
(player) => player.user.id === user2.id,
);
expect(result1).toBeDefined();
expect(result2).toBeDefined();
expect(result1.score).toBe(0);
expect(result2.score).toBe(3);
expect(result1.winLose).toBe('LOSE');
expect(result2.winLose).toBe('WIN');
});
});
});
});
Loading

0 comments on commit b777c2e

Please sign in to comment.