diff --git a/back/volume/src/pong/dtos/PlayerNamesDto.ts b/back/volume/src/pong/dtos/PlayerNamesDto.ts index ddd3c20..8b4faa3 100644 --- a/back/volume/src/pong/dtos/PlayerNamesDto.ts +++ b/back/volume/src/pong/dtos/PlayerNamesDto.ts @@ -1,8 +1,8 @@ -import { ArrayMaxSize, ArrayMinSize, IsString } from 'class-validator'; +import { ArrayMaxSize, ArrayMinSize, IsString } from 'class-validator' export class PlayerNamesDto { - @IsString({ each: true }) - @ArrayMaxSize(2) - @ArrayMinSize(2) - playerNames: string[]; + @IsString({ each: true }) + @ArrayMaxSize(2) + @ArrayMinSize(2) + playerNames: string[] } diff --git a/back/volume/src/pong/dtos/UserDto.ts b/back/volume/src/pong/dtos/UserDto.ts new file mode 100644 index 0000000..4abc1f4 --- /dev/null +++ b/back/volume/src/pong/dtos/UserDto.ts @@ -0,0 +1,12 @@ +import { ArrayMaxSize, ArrayMinSize, IsString } from 'class-validator' + +export class UserDto { + @IsString() + username: string + + @IsString() + avatar: string + + @IsString() + status: string +} diff --git a/back/volume/src/pong/game/Game.ts b/back/volume/src/pong/game/Game.ts index 2f253c2..1731f48 100644 --- a/back/volume/src/pong/game/Game.ts +++ b/back/volume/src/pong/game/Game.ts @@ -9,6 +9,7 @@ import { GAME_EVENTS } from './constants' import { randomUUID } from 'crypto' +import { Spectator } from './Spectator' const GAME_TICKS = 30 @@ -47,6 +48,7 @@ export class Game { timer: NodeJS.Timer ball: Ball players: Player[] = [] + spectators: Spectator[] = [] playing: boolean constructor (sockets: WebSocket[], uuids: string[], names: string[]) { @@ -63,8 +65,8 @@ export class Game { } } - getGameInfo (uuid: string): GameInfo { - const yourPaddleIndex = this.players.findIndex((p) => p.uuid == uuid) + getGameInfo (name: string): GameInfo { + const yourPaddleIndex = this.players.findIndex((p) => p.name == name) return { ...gameInfoConstants, yourPaddleIndex, @@ -72,6 +74,11 @@ export class Game { } } + addSpectator (socket: WebSocket, uuid: string, name: string) { + this.spectators.push(new Spectator(socket, uuid, name)) + console.log(`Added spectator ${name}`) + } + private addPlayer (socket: WebSocket, uuid: string, name: string) { let paddleCoords = new Point( gameInfoConstants.playerXOffset, @@ -88,8 +95,8 @@ export class Game { ) } - removePlayer (uuid: string) { - const player_index = this.players.findIndex((p) => p.uuid == uuid) + removePlayer (name: string) { + const player_index = this.players.findIndex((p) => p.name == name) if (player_index != -1) { this.players.splice(player_index, 1) if (this.players.length < 2) { @@ -98,8 +105,8 @@ export class Game { } } - ready (uuid: string) { - const player_index = this.players.findIndex((p) => p.uuid == uuid) + ready (name: string) { + const player_index = this.players.findIndex((p) => p.name == name) if (player_index != -1) { this.players[player_index].ready = true console.log(`${this.players[player_index].name} is ready!`) @@ -140,8 +147,8 @@ export class Game { } } - movePaddle (uuid: string, position: Point) { - const playerIndex = this.players.findIndex((p) => p.uuid == uuid) + movePaddle (name: string, position: Point) { + const playerIndex = this.players.findIndex((p) => p.name == name) if (this.timer && playerIndex != -1) { this.players[playerIndex].paddle.move(position.y) @@ -152,6 +159,9 @@ export class Game { this.players.forEach((p) => { p.socket.send(data) }) + this.spectators.forEach((s) => { + s.socket.send(data) + }) } isPlaying (): boolean { diff --git a/back/volume/src/pong/game/Spectator.ts b/back/volume/src/pong/game/Spectator.ts new file mode 100644 index 0000000..fae2c5b --- /dev/null +++ b/back/volume/src/pong/game/Spectator.ts @@ -0,0 +1,13 @@ +import { type WebSocket } from 'ws' + +export class Spectator { + socket: WebSocket + uuid: string + name: string + + constructor (socket: WebSocket, uuid: string, name: string) { + this.socket = socket + this.uuid = uuid + this.name = name + } +} diff --git a/back/volume/src/pong/game/constants.ts b/back/volume/src/pong/game/constants.ts index 0788e71..2eccf50 100644 --- a/back/volume/src/pong/game/constants.ts +++ b/back/volume/src/pong/game/constants.ts @@ -7,7 +7,8 @@ export const GAME_EVENTS = { PLAYER_MOVE: 'PLAYER_MOVE', GET_GAME_INFO: 'GET_GAME_INFO', CREATE_GAME: 'CREATE_GAME', - REGISTER_PLAYER: 'REGISTER_PLAYER' + REGISTER_PLAYER: 'REGISTER_PLAYER', + SPECTATE: 'SPECTATE' } export interface GameInfo extends GameInfoConstants { @@ -26,7 +27,7 @@ export const gameInfoConstants: GameInfoConstants = { paddleSize: new Point(6, 50), playerXOffset: 50, ballSize: new Point(20, 20), - winScore: 2 + winScore: 9999 } export interface GameUpdate { diff --git a/back/volume/src/pong/pong.gateway.ts b/back/volume/src/pong/pong.gateway.ts index cb1ead6..090052a 100644 --- a/back/volume/src/pong/pong.gateway.ts +++ b/back/volume/src/pong/pong.gateway.ts @@ -8,11 +8,11 @@ import { WebSocketGateway } from '@nestjs/websockets' import { randomUUID } from 'crypto' -import { Pong } from './pong' +import { Games } from './pong' import { formatWebsocketData, Point } from './game/utils' import { GAME_EVENTS } from './game/constants' -import { PlayerNamesDto } from './dtos/PlayerNamesDto'; -import { UsePipes, ValidationPipe } from '@nestjs/common'; +import { PlayerNamesDto } from './dtos/PlayerNamesDto' +import { UsePipes, ValidationPipe } from '@nestjs/common' interface WebSocketWithId extends WebSocket { id: string @@ -20,7 +20,7 @@ interface WebSocketWithId extends WebSocket { @WebSocketGateway() export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect { - private readonly pong: Pong = new Pong() + private readonly games: Games = new Games() private readonly socketToPlayerName = new Map() handleConnection (client: WebSocketWithId) { @@ -32,10 +32,11 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect { @ConnectedSocket() client: WebSocketWithId ) { - if (this.pong.isInAGame(client.id)) { + const name: string = this.socketToPlayerName.get(client) + if (this.games.isInAGame(name)) { console.log(`Disconnected ${this.socketToPlayerName.get(client)}`) - if (this.pong.playerGame(client.id).isPlaying()) { - this.pong.playerGame(client.id).stop() + if (this.games.playerGame(name).isPlaying()) { + this.games.playerGame(name).stop() } this.socketToPlayerName.delete(client) } @@ -53,12 +54,15 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect { @SubscribeMessage(GAME_EVENTS.GET_GAME_INFO) getPlayerCount (@ConnectedSocket() client: WebSocketWithId) { - client.send( - formatWebsocketData( - GAME_EVENTS.GET_GAME_INFO, - this.pong.getGameInfo(client.id) + const name: string = this.socketToPlayerName.get(client) + if (name) { + client.send( + formatWebsocketData( + GAME_EVENTS.GET_GAME_INFO, + this.games.getGameInfo(name) + ) ) - ) + } } @SubscribeMessage(GAME_EVENTS.PLAYER_MOVE) @@ -67,47 +71,66 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect { client: WebSocketWithId, @MessageBody('position') position: Point ) { - this.pong.movePlayer(client.id, position) + const name: string = this.socketToPlayerName.get(client) + this.games.movePlayer(name, position) } - @UsePipes(new ValidationPipe({ whitelist: true })) - @SubscribeMessage(GAME_EVENTS.CREATE_GAME) - createGame( - @ConnectedSocket() - client: WebSocketWithId, - @MessageBody() playerNames: PlayerNamesDto - ) { - console.log(playerNames); - const allPlayerNames: Array = Array.from(this.socketToPlayerName.values()); - if (allPlayerNames && allPlayerNames.length >= 2) { - const player1Socket: WebSocketWithId = Array.from(this.socketToPlayerName.keys()).find( - (key) => this.socketToPlayerName.get(key) === playerNames[0] - ); - const player2Socket: WebSocketWithId = Array.from(this.socketToPlayerName.keys()).find( - (key) => this.socketToPlayerName.get(key) === playerNames[1] - ); + @UsePipes(new ValidationPipe({ whitelist: true })) + @SubscribeMessage(GAME_EVENTS.CREATE_GAME) + createGame ( + @ConnectedSocket() + client: WebSocketWithId, + @MessageBody() playerNamesDto: PlayerNamesDto + ) { + if (this.socketToPlayerName.size >= 2) { + const player1Socket: WebSocketWithId = Array.from( + this.socketToPlayerName.keys() + ).find( + (key) => + this.socketToPlayerName.get(key) === playerNamesDto.playerNames[0] + ) + const player2Socket: WebSocketWithId = Array.from( + this.socketToPlayerName.keys() + ).find( + (key) => + this.socketToPlayerName.get(key) === playerNamesDto.playerNames[1] + ) - if ( - player1Socket && - player2Socket && - (client.id === player1Socket.id || client.id === player2Socket.id) && - player1Socket.id !== player2Socket.id - ) { - this.pong.newGame( - [player1Socket, player2Socket], - [player1Socket.id, player2Socket.id], - playerNames.playerNames - ); - } - } - return { event: GAME_EVENTS.CREATE_GAME }; - } + if ( + player1Socket && + player2Socket && + (client.id === player1Socket.id || client.id === player2Socket.id) && + player1Socket.id !== player2Socket.id + ) { + this.games.newGame( + [player1Socket, player2Socket], + [player1Socket.id, player2Socket.id], + playerNamesDto.playerNames + ) + } + } + } @SubscribeMessage(GAME_EVENTS.READY) ready ( @ConnectedSocket() client: WebSocketWithId ) { - this.pong.ready(client.id) + const name: string = this.socketToPlayerName.get(client) + if (name) { + this.games.ready(name) + } + } + + @SubscribeMessage(GAME_EVENTS.SPECTATE) + spectate ( + @ConnectedSocket() + client: WebSocketWithId, + @MessageBody('playerToSpectate') playerToSpectate: string + ) { + const name: string = this.socketToPlayerName.get(client) + if (name) { + this.games.spectateGame(playerToSpectate, client, client.id, name) + } } } diff --git a/back/volume/src/pong/pong.spec.ts b/back/volume/src/pong/pong.spec.ts index 4f68326..bd9cfae 100644 --- a/back/volume/src/pong/pong.spec.ts +++ b/back/volume/src/pong/pong.spec.ts @@ -1,15 +1,15 @@ import { Test, type TestingModule } from '@nestjs/testing' -import { Pong } from './pong' +import { Games } from './pong' describe('Pong', () => { - let provider: Pong + let provider: Games beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [Pong] + providers: [Games] }).compile() - provider = module.get(Pong) + provider = module.get(Games) }) it('should be defined', () => { diff --git a/back/volume/src/pong/pong.ts b/back/volume/src/pong/pong.ts index 740ae5c..63e64e4 100644 --- a/back/volume/src/pong/pong.ts +++ b/back/volume/src/pong/pong.ts @@ -3,57 +3,69 @@ import { type GameInfo } from './game/constants' import { Game } from './game/Game' import { type Point } from './game/utils' -export class Pong { - private playerUUIDToGameIndex = new Map() +export class Games { + private readonly playerNameToGameIndex = new Map() private readonly games = new Array() newGame (sockets: WebSocket[], uuids: string[], names: string[]) { this.games.push(new Game(sockets, uuids, names)) - this.playerUUIDToGameIndex[uuids[0]] = this.games.length - 1 - this.playerUUIDToGameIndex[uuids[1]] = this.games.length - 1 + this.playerNameToGameIndex.set(names[0], this.games.length - 1) + this.playerNameToGameIndex.set(names[1], this.games.length - 1) console.log(`Created game ${names[0]} vs ${names[1]}`) } - removePlayer (uuid: string) { - this.playerGame(uuid).removePlayer(uuid) + removePlayer (name: string) { + this.playerGame(name).removePlayer(name) } - ready (uuid: string) { - if (this.isInAGame(uuid)) { - this.playerGame(uuid).ready(uuid) + ready (name: string) { + if (this.isInAGame(name)) { + this.playerGame(name).ready(name) } } stopGame (uuid: string) { - if (this.isInAGame(uuid)) { - this.playerGame(uuid).stop() - delete this.playerUUIDToGameIndex[uuid] - delete this.games[this.playerUUIDToGameIndex[uuid]] - } + // if (this.isInAGame(uuid)) { + // this.playerGame(uuid).stop() + // delete this.playerNameToGameIndex[uuid] + // delete this.games[this.playerNameToGameIndex[uuid]] + // } } - getGameInfo (uuid: string): GameInfo { - if (this.isInAGame(uuid)) { - return this.playerGame(uuid).getGameInfo(uuid) + getGameInfo (name: string): GameInfo { + if (this.isInAGame(name)) { + return this.playerGame(name).getGameInfo(name) } } - movePlayer (uuid: string, position: Point) { - if (this.isInAGame(uuid)) { - this.playerGame(uuid).movePaddle(uuid, position) + movePlayer (name: string, position: Point) { + if (this.isInAGame(name)) { + this.playerGame(name).movePaddle(name, position) } } - isInAGame (uuid: string): boolean { - if (this.playerUUIDToGameIndex[uuid] === undefined) { - return false + isInAGame (name: string): boolean { + return this.playerNameToGameIndex.get(name) !== undefined + } + + playerGame (name: string): Game { + if (this.isInAGame(name)) { + return this.games[this.playerNameToGameIndex.get(name)] } - return true } - playerGame (uuid: string): Game { - if (this.isInAGame(uuid)) { - return this.games[this.playerUUIDToGameIndex[uuid]] + spectateGame ( + nameToSpectate: string, + socket: WebSocket, + uuid: string, + name: string + ) { + if (this.isInAGame(nameToSpectate)) { + this.playerNameToGameIndex.set( + name, + this.playerNameToGameIndex.get(nameToSpectate) + ) + this.playerGame(nameToSpectate).addSpectator(socket, uuid, name) } } } diff --git a/front/volume/src/components/Pong/Game.ts b/front/volume/src/components/Pong/Game.ts index 2e2ffe4..8e0691b 100644 --- a/front/volume/src/components/Pong/Game.ts +++ b/front/volume/src/components/Pong/Game.ts @@ -40,7 +40,8 @@ export class Game { data.paddleSize ); this.players = [new Player(paddle1), new Player(paddle2)]; - this.my_paddle = this.players[data.yourPaddleIndex].paddle; + if (data.yourPaddleIndex != -1) + this.my_paddle = this.players[data.yourPaddleIndex].paddle; this.id = data.gameId; } diff --git a/front/volume/src/components/Pong/Pong.svelte b/front/volume/src/components/Pong/Pong.svelte index 891a9d1..22ce2ca 100644 --- a/front/volume/src/components/Pong/Pong.svelte +++ b/front/volume/src/components/Pong/Pong.svelte @@ -7,9 +7,11 @@ const SERVER_URL = "ws://localhost:3001"; let connected: boolean = false; + let loggedIn: boolean = false; let socket: WebSocket; let username: string = "John"; let otherUsername: string = "Garfield"; + let spectateUsername: string = "Garfield"; //Get canvas and its context window.onload = () => { @@ -66,14 +68,31 @@ socket.send(formatWebsocketData(GAME_EVENTS.GET_GAME_INFO)); } - function connectToServer() { + function spectate() { + socket.send( + formatWebsocketData(GAME_EVENTS.SPECTATE, { + playerToSpectate: spectateUsername, + }) + ); + } + + function logIn() { socket.send( formatWebsocketData(GAME_EVENTS.REGISTER_PLAYER, { playerName: username }) ); + loggedIn = true; setInterval(() => { updateGameInfo(); }, 1000); } + + function createGame() { + socket.send( + formatWebsocketData(GAME_EVENTS.CREATE_GAME, { + playerNames: [username, otherUsername], + }) + ); + }
@@ -81,28 +100,24 @@ Your name:
- +
Other player name: - +
-
- socket.send(formatWebsocketData(GAME_EVENTS.READY))} + disabled={!loggedIn}>Ready
+ +
{:else} Connecting to game server... diff --git a/front/volume/src/components/Pong/constants.ts b/front/volume/src/components/Pong/constants.ts index d9f1d7c..f66d300 100644 --- a/front/volume/src/components/Pong/constants.ts +++ b/front/volume/src/components/Pong/constants.ts @@ -8,6 +8,7 @@ export const GAME_EVENTS = { GET_GAME_INFO: "GET_GAME_INFO", CREATE_GAME: "CREATE_GAME", REGISTER_PLAYER: "REGISTER_PLAYER", + SPECTATE: "SPECTATE", }; export interface GameInfo extends GameInfoConstants { @@ -26,7 +27,7 @@ export const gameInfoConstants: GameInfoConstants = { paddleSize: new Point(6, 50), playerXOffset: 50, ballSize: new Point(20, 20), - winScore: 2, + winScore: 9999, }; export interface GameUpdate {