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 {
@IsString({ each: true })
@ArrayMaxSize(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
} 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 {

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',
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 {

71
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<WebSocketWithId, string>()
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,13 +54,16 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
@SubscribeMessage(GAME_EVENTS.GET_GAME_INFO)
getPlayerCount (@ConnectedSocket() client: WebSocketWithId) {
const name: string = this.socketToPlayerName.get(client)
if (name) {
client.send(
formatWebsocketData(
GAME_EVENTS.GET_GAME_INFO,
this.pong.getGameInfo(client.id)
this.games.getGameInfo(name)
)
)
}
}
@SubscribeMessage(GAME_EVENTS.PLAYER_MOVE)
movePlayer (
@ -67,7 +71,8 @@ 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 }))
@ -75,17 +80,21 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
createGame (
@ConnectedSocket()
client: WebSocketWithId,
@MessageBody() playerNames: PlayerNamesDto
@MessageBody() playerNamesDto: PlayerNamesDto
) {
console.log(playerNames);
const allPlayerNames: Array<string> = 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]
);
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 &&
@ -93,14 +102,13 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
(client.id === player1Socket.id || client.id === player2Socket.id) &&
player1Socket.id !== player2Socket.id
) {
this.pong.newGame(
this.games.newGame(
[player1Socket, player2Socket],
[player1Socket.id, player2Socket.id],
playerNames.playerNames
);
playerNamesDto.playerNames
)
}
}
return { event: GAME_EVENTS.CREATE_GAME };
}
@SubscribeMessage(GAME_EVENTS.READY)
@ -108,6 +116,21 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
@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)
}
}
}

8
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>(Pong)
provider = module.get<Games>(Games)
})
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 { type Point } from './game/utils'
export class Pong {
private playerUUIDToGameIndex = new Map<string, number>()
export class Games {
private readonly playerNameToGameIndex = new Map<string, number>()
private readonly games = new Array<Game>()
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 (name: string): boolean {
return this.playerNameToGameIndex.get(name) !== undefined
}
isInAGame (uuid: string): boolean {
if (this.playerUUIDToGameIndex[uuid] === undefined) {
return false
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)
}
}
}

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

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

45
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],
})
);
}
</script>
<div>
@ -81,28 +100,24 @@
Your name:
<input bind:value={username} />
<br />
<button on:click={connectToServer}> Connect </button>
<button on:click={logIn}> Log in </button>
<br />
Other player name:
<input bind:value={otherUsername} />
<input bind:value={otherUsername} disabled={!loggedIn} />
<br />
<button
on:click={() => {
socket.send(
formatWebsocketData(GAME_EVENTS.CREATE_GAME, {
playerNames: [username, otherUsername],
})
);
updateGameInfo();
}}
>
<button on:click={createGame} disabled={!loggedIn}>
Create game vs {otherUsername}
</button>
<br />
<button on:click={() => socket.send(formatWebsocketData(GAME_EVENTS.READY))}
>Ready</button
<button
on:click={() => socket.send(formatWebsocketData(GAME_EVENTS.READY))}
disabled={!loggedIn}>Ready</button
>
<br />
<input bind:value={spectateUsername} disabled={!loggedIn} />
<button on:click={spectate} disabled={!loggedIn}
>Spectate {spectateUsername}</button
>
<br />
{:else}
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",
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 {

Loading…
Cancel
Save