Browse Source

* Added match spectating

master
vvandenb 2 years ago
parent
commit
c9670e6f66
  1. 4
      back/volume/src/pong/dtos/PlayerNamesDto.ts
  2. 12
      back/volume/src/pong/dtos/UserDto.ts
  3. 26
      back/volume/src/pong/game/Game.ts
  4. 13
      back/volume/src/pong/game/Spectator.ts
  5. 5
      back/volume/src/pong/game/constants.ts
  6. 71
      back/volume/src/pong/pong.gateway.ts
  7. 8
      back/volume/src/pong/pong.spec.ts
  8. 66
      back/volume/src/pong/pong.ts
  9. 1
      front/volume/src/components/Pong/Game.ts
  10. 45
      front/volume/src/components/Pong/Pong.svelte
  11. 3
      front/volume/src/components/Pong/constants.ts

4
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 { export class PlayerNamesDto {
@IsString({ each: true }) @IsString({ each: true })
@ArrayMaxSize(2) @ArrayMaxSize(2)
@ArrayMinSize(2) @ArrayMinSize(2)
playerNames: string[]; playerNames: string[]
} }

12
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
}

26
back/volume/src/pong/game/Game.ts

@ -9,6 +9,7 @@ import {
GAME_EVENTS GAME_EVENTS
} from './constants' } from './constants'
import { randomUUID } from 'crypto' import { randomUUID } from 'crypto'
import { Spectator } from './Spectator'
const GAME_TICKS = 30 const GAME_TICKS = 30
@ -47,6 +48,7 @@ export class Game {
timer: NodeJS.Timer timer: NodeJS.Timer
ball: Ball ball: Ball
players: Player[] = [] players: Player[] = []
spectators: Spectator[] = []
playing: boolean playing: boolean
constructor (sockets: WebSocket[], uuids: string[], names: string[]) { constructor (sockets: WebSocket[], uuids: string[], names: string[]) {
@ -63,8 +65,8 @@ export class Game {
} }
} }
getGameInfo (uuid: string): GameInfo { getGameInfo (name: string): GameInfo {
const yourPaddleIndex = this.players.findIndex((p) => p.uuid == uuid) const yourPaddleIndex = this.players.findIndex((p) => p.name == name)
return { return {
...gameInfoConstants, ...gameInfoConstants,
yourPaddleIndex, 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) { private addPlayer (socket: WebSocket, uuid: string, name: string) {
let paddleCoords = new Point( let paddleCoords = new Point(
gameInfoConstants.playerXOffset, gameInfoConstants.playerXOffset,
@ -88,8 +95,8 @@ export class Game {
) )
} }
removePlayer (uuid: string) { removePlayer (name: string) {
const player_index = this.players.findIndex((p) => p.uuid == uuid) const player_index = this.players.findIndex((p) => p.name == name)
if (player_index != -1) { if (player_index != -1) {
this.players.splice(player_index, 1) this.players.splice(player_index, 1)
if (this.players.length < 2) { if (this.players.length < 2) {
@ -98,8 +105,8 @@ export class Game {
} }
} }
ready (uuid: string) { ready (name: string) {
const player_index = this.players.findIndex((p) => p.uuid == uuid) const player_index = this.players.findIndex((p) => p.name == name)
if (player_index != -1) { if (player_index != -1) {
this.players[player_index].ready = true this.players[player_index].ready = true
console.log(`${this.players[player_index].name} is ready!`) console.log(`${this.players[player_index].name} is ready!`)
@ -140,8 +147,8 @@ export class Game {
} }
} }
movePaddle (uuid: string, position: Point) { movePaddle (name: string, position: Point) {
const playerIndex = this.players.findIndex((p) => p.uuid == uuid) const playerIndex = this.players.findIndex((p) => p.name == name)
if (this.timer && playerIndex != -1) { if (this.timer && playerIndex != -1) {
this.players[playerIndex].paddle.move(position.y) this.players[playerIndex].paddle.move(position.y)
@ -152,6 +159,9 @@ export class Game {
this.players.forEach((p) => { this.players.forEach((p) => {
p.socket.send(data) p.socket.send(data)
}) })
this.spectators.forEach((s) => {
s.socket.send(data)
})
} }
isPlaying (): boolean { isPlaying (): boolean {

13
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
}
}

5
back/volume/src/pong/game/constants.ts

@ -7,7 +7,8 @@ export const GAME_EVENTS = {
PLAYER_MOVE: 'PLAYER_MOVE', PLAYER_MOVE: 'PLAYER_MOVE',
GET_GAME_INFO: 'GET_GAME_INFO', GET_GAME_INFO: 'GET_GAME_INFO',
CREATE_GAME: 'CREATE_GAME', CREATE_GAME: 'CREATE_GAME',
REGISTER_PLAYER: 'REGISTER_PLAYER' REGISTER_PLAYER: 'REGISTER_PLAYER',
SPECTATE: 'SPECTATE'
} }
export interface GameInfo extends GameInfoConstants { export interface GameInfo extends GameInfoConstants {
@ -26,7 +27,7 @@ export const gameInfoConstants: GameInfoConstants = {
paddleSize: new Point(6, 50), paddleSize: new Point(6, 50),
playerXOffset: 50, playerXOffset: 50,
ballSize: new Point(20, 20), ballSize: new Point(20, 20),
winScore: 2 winScore: 9999
} }
export interface GameUpdate { export interface GameUpdate {

71
back/volume/src/pong/pong.gateway.ts

@ -8,11 +8,11 @@ import {
WebSocketGateway WebSocketGateway
} from '@nestjs/websockets' } from '@nestjs/websockets'
import { randomUUID } from 'crypto' import { randomUUID } from 'crypto'
import { Pong } from './pong' import { Games } from './pong'
import { formatWebsocketData, Point } from './game/utils' import { formatWebsocketData, Point } from './game/utils'
import { GAME_EVENTS } from './game/constants' import { GAME_EVENTS } from './game/constants'
import { PlayerNamesDto } from './dtos/PlayerNamesDto'; import { PlayerNamesDto } from './dtos/PlayerNamesDto'
import { UsePipes, ValidationPipe } from '@nestjs/common'; import { UsePipes, ValidationPipe } from '@nestjs/common'
interface WebSocketWithId extends WebSocket { interface WebSocketWithId extends WebSocket {
id: string id: string
@ -20,7 +20,7 @@ interface WebSocketWithId extends WebSocket {
@WebSocketGateway() @WebSocketGateway()
export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect { export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
private readonly pong: Pong = new Pong() private readonly games: Games = new Games()
private readonly socketToPlayerName = new Map<WebSocketWithId, string>() private readonly socketToPlayerName = new Map<WebSocketWithId, string>()
handleConnection (client: WebSocketWithId) { handleConnection (client: WebSocketWithId) {
@ -32,10 +32,11 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
@ConnectedSocket() @ConnectedSocket()
client: WebSocketWithId 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)}`) console.log(`Disconnected ${this.socketToPlayerName.get(client)}`)
if (this.pong.playerGame(client.id).isPlaying()) { if (this.games.playerGame(name).isPlaying()) {
this.pong.playerGame(client.id).stop() this.games.playerGame(name).stop()
} }
this.socketToPlayerName.delete(client) this.socketToPlayerName.delete(client)
} }
@ -53,13 +54,16 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
@SubscribeMessage(GAME_EVENTS.GET_GAME_INFO) @SubscribeMessage(GAME_EVENTS.GET_GAME_INFO)
getPlayerCount (@ConnectedSocket() client: WebSocketWithId) { getPlayerCount (@ConnectedSocket() client: WebSocketWithId) {
const name: string = this.socketToPlayerName.get(client)
if (name) {
client.send( client.send(
formatWebsocketData( formatWebsocketData(
GAME_EVENTS.GET_GAME_INFO, GAME_EVENTS.GET_GAME_INFO,
this.pong.getGameInfo(client.id) this.games.getGameInfo(name)
) )
) )
} }
}
@SubscribeMessage(GAME_EVENTS.PLAYER_MOVE) @SubscribeMessage(GAME_EVENTS.PLAYER_MOVE)
movePlayer ( movePlayer (
@ -67,7 +71,8 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
client: WebSocketWithId, client: WebSocketWithId,
@MessageBody('position') position: Point @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 })) @UsePipes(new ValidationPipe({ whitelist: true }))
@ -75,17 +80,21 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
createGame ( createGame (
@ConnectedSocket() @ConnectedSocket()
client: WebSocketWithId, client: WebSocketWithId,
@MessageBody() playerNames: PlayerNamesDto @MessageBody() playerNamesDto: PlayerNamesDto
) { ) {
console.log(playerNames); if (this.socketToPlayerName.size >= 2) {
const allPlayerNames: Array<string> = Array.from(this.socketToPlayerName.values()); const player1Socket: WebSocketWithId = Array.from(
if (allPlayerNames && allPlayerNames.length >= 2) { this.socketToPlayerName.keys()
const player1Socket: WebSocketWithId = Array.from(this.socketToPlayerName.keys()).find( ).find(
(key) => this.socketToPlayerName.get(key) === playerNames[0] (key) =>
); this.socketToPlayerName.get(key) === playerNamesDto.playerNames[0]
const player2Socket: WebSocketWithId = Array.from(this.socketToPlayerName.keys()).find( )
(key) => this.socketToPlayerName.get(key) === playerNames[1] const player2Socket: WebSocketWithId = Array.from(
); this.socketToPlayerName.keys()
).find(
(key) =>
this.socketToPlayerName.get(key) === playerNamesDto.playerNames[1]
)
if ( if (
player1Socket && player1Socket &&
@ -93,14 +102,13 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
(client.id === player1Socket.id || client.id === player2Socket.id) && (client.id === player1Socket.id || client.id === player2Socket.id) &&
player1Socket.id !== player2Socket.id player1Socket.id !== player2Socket.id
) { ) {
this.pong.newGame( this.games.newGame(
[player1Socket, player2Socket], [player1Socket, player2Socket],
[player1Socket.id, player2Socket.id], [player1Socket.id, player2Socket.id],
playerNames.playerNames playerNamesDto.playerNames
); )
} }
} }
return { event: GAME_EVENTS.CREATE_GAME };
} }
@SubscribeMessage(GAME_EVENTS.READY) @SubscribeMessage(GAME_EVENTS.READY)
@ -108,6 +116,21 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
@ConnectedSocket() @ConnectedSocket()
client: WebSocketWithId 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)
}
} }
} }

8
back/volume/src/pong/pong.spec.ts

@ -1,15 +1,15 @@
import { Test, type TestingModule } from '@nestjs/testing' import { Test, type TestingModule } from '@nestjs/testing'
import { Pong } from './pong' import { Games } from './pong'
describe('Pong', () => { describe('Pong', () => {
let provider: Pong let provider: Games
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [Pong] providers: [Games]
}).compile() }).compile()
provider = module.get<Pong>(Pong) provider = module.get<Games>(Games)
}) })
it('should be defined', () => { it('should be defined', () => {

66
back/volume/src/pong/pong.ts

@ -3,57 +3,69 @@ import { type GameInfo } from './game/constants'
import { Game } from './game/Game' import { Game } from './game/Game'
import { type Point } from './game/utils' import { type Point } from './game/utils'
export class Pong { export class Games {
private playerUUIDToGameIndex = new Map<string, number>() private readonly playerNameToGameIndex = new Map<string, number>()
private readonly games = new Array<Game>() private readonly games = new Array<Game>()
newGame (sockets: WebSocket[], uuids: string[], names: string[]) { newGame (sockets: WebSocket[], uuids: string[], names: string[]) {
this.games.push(new Game(sockets, uuids, names)) this.games.push(new Game(sockets, uuids, names))
this.playerUUIDToGameIndex[uuids[0]] = this.games.length - 1 this.playerNameToGameIndex.set(names[0], this.games.length - 1)
this.playerUUIDToGameIndex[uuids[1]] = this.games.length - 1 this.playerNameToGameIndex.set(names[1], this.games.length - 1)
console.log(`Created game ${names[0]} vs ${names[1]}`) console.log(`Created game ${names[0]} vs ${names[1]}`)
} }
removePlayer (uuid: string) { removePlayer (name: string) {
this.playerGame(uuid).removePlayer(uuid) this.playerGame(name).removePlayer(name)
} }
ready (uuid: string) { ready (name: string) {
if (this.isInAGame(uuid)) { if (this.isInAGame(name)) {
this.playerGame(uuid).ready(uuid) this.playerGame(name).ready(name)
} }
} }
stopGame (uuid: string) { stopGame (uuid: string) {
if (this.isInAGame(uuid)) { // if (this.isInAGame(uuid)) {
this.playerGame(uuid).stop() // this.playerGame(uuid).stop()
delete this.playerUUIDToGameIndex[uuid] // delete this.playerNameToGameIndex[uuid]
delete this.games[this.playerUUIDToGameIndex[uuid]] // delete this.games[this.playerNameToGameIndex[uuid]]
} // }
} }
getGameInfo (uuid: string): GameInfo { getGameInfo (name: string): GameInfo {
if (this.isInAGame(uuid)) { if (this.isInAGame(name)) {
return this.playerGame(uuid).getGameInfo(uuid) return this.playerGame(name).getGameInfo(name)
} }
} }
movePlayer (uuid: string, position: Point) { movePlayer (name: string, position: Point) {
if (this.isInAGame(uuid)) { if (this.isInAGame(name)) {
this.playerGame(uuid).movePaddle(uuid, position) this.playerGame(name).movePaddle(name, position)
}
} }
isInAGame (name: string): boolean {
return this.playerNameToGameIndex.get(name) !== undefined
} }
isInAGame (uuid: string): boolean { playerGame (name: string): Game {
if (this.playerUUIDToGameIndex[uuid] === undefined) { if (this.isInAGame(name)) {
return false return this.games[this.playerNameToGameIndex.get(name)]
} }
return true
} }
playerGame (uuid: string): Game { spectateGame (
if (this.isInAGame(uuid)) { nameToSpectate: string,
return this.games[this.playerUUIDToGameIndex[uuid]] 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)
} }
} }
} }

1
front/volume/src/components/Pong/Game.ts

@ -40,6 +40,7 @@ export class Game {
data.paddleSize data.paddleSize
); );
this.players = [new Player(paddle1), new Player(paddle2)]; this.players = [new Player(paddle1), new Player(paddle2)];
if (data.yourPaddleIndex != -1)
this.my_paddle = this.players[data.yourPaddleIndex].paddle; this.my_paddle = this.players[data.yourPaddleIndex].paddle;
this.id = data.gameId; this.id = data.gameId;
} }

45
front/volume/src/components/Pong/Pong.svelte

@ -7,9 +7,11 @@
const SERVER_URL = "ws://localhost:3001"; const SERVER_URL = "ws://localhost:3001";
let connected: boolean = false; let connected: boolean = false;
let loggedIn: boolean = false;
let socket: WebSocket; let socket: WebSocket;
let username: string = "John"; let username: string = "John";
let otherUsername: string = "Garfield"; let otherUsername: string = "Garfield";
let spectateUsername: string = "Garfield";
//Get canvas and its context //Get canvas and its context
window.onload = () => { window.onload = () => {
@ -66,14 +68,31 @@
socket.send(formatWebsocketData(GAME_EVENTS.GET_GAME_INFO)); socket.send(formatWebsocketData(GAME_EVENTS.GET_GAME_INFO));
} }
function connectToServer() { function spectate() {
socket.send(
formatWebsocketData(GAME_EVENTS.SPECTATE, {
playerToSpectate: spectateUsername,
})
);
}
function logIn() {
socket.send( socket.send(
formatWebsocketData(GAME_EVENTS.REGISTER_PLAYER, { playerName: username }) formatWebsocketData(GAME_EVENTS.REGISTER_PLAYER, { playerName: username })
); );
loggedIn = true;
setInterval(() => { setInterval(() => {
updateGameInfo(); updateGameInfo();
}, 1000); }, 1000);
} }
function createGame() {
socket.send(
formatWebsocketData(GAME_EVENTS.CREATE_GAME, {
playerNames: [username, otherUsername],
})
);
}
</script> </script>
<div> <div>
@ -81,28 +100,24 @@
Your name: Your name:
<input bind:value={username} /> <input bind:value={username} />
<br /> <br />
<button on:click={connectToServer}> Connect </button> <button on:click={logIn}> Log in </button>
<br /> <br />
Other player name: Other player name:
<input bind:value={otherUsername} /> <input bind:value={otherUsername} disabled={!loggedIn} />
<br /> <br />
<button <button on:click={createGame} disabled={!loggedIn}>
on:click={() => {
socket.send(
formatWebsocketData(GAME_EVENTS.CREATE_GAME, {
playerNames: [username, otherUsername],
})
);
updateGameInfo();
}}
>
Create game vs {otherUsername} Create game vs {otherUsername}
</button> </button>
<br /> <br />
<button on:click={() => socket.send(formatWebsocketData(GAME_EVENTS.READY))} <button
>Ready</button on:click={() => socket.send(formatWebsocketData(GAME_EVENTS.READY))}
disabled={!loggedIn}>Ready</button
> >
<br /> <br />
<input bind:value={spectateUsername} disabled={!loggedIn} />
<button on:click={spectate} disabled={!loggedIn}
>Spectate {spectateUsername}</button
>
<br /> <br />
{:else} {:else}
Connecting to game server... Connecting to game server...

3
front/volume/src/components/Pong/constants.ts

@ -8,6 +8,7 @@ export const GAME_EVENTS = {
GET_GAME_INFO: "GET_GAME_INFO", GET_GAME_INFO: "GET_GAME_INFO",
CREATE_GAME: "CREATE_GAME", CREATE_GAME: "CREATE_GAME",
REGISTER_PLAYER: "REGISTER_PLAYER", REGISTER_PLAYER: "REGISTER_PLAYER",
SPECTATE: "SPECTATE",
}; };
export interface GameInfo extends GameInfoConstants { export interface GameInfo extends GameInfoConstants {
@ -26,7 +27,7 @@ export const gameInfoConstants: GameInfoConstants = {
paddleSize: new Point(6, 50), paddleSize: new Point(6, 50),
playerXOffset: 50, playerXOffset: 50,
ballSize: new Point(20, 20), ballSize: new Point(20, 20),
winScore: 2, winScore: 9999,
}; };
export interface GameUpdate { export interface GameUpdate {

Loading…
Cancel
Save