From 4d75fec2d1122c4df4fe9d1a0cd26f4bc7f6e659 Mon Sep 17 00:00:00 2001 From: vvandenb Date: Mon, 27 Feb 2023 16:58:11 +0100 Subject: [PATCH] * Added map customization --- back/volume/src/pong/dtos/GameCreationDto.ts | 25 ++++++ back/volume/src/pong/dtos/PlayerNamesDto.ts | 8 -- back/volume/src/pong/game/Ball.ts | 21 +++-- back/volume/src/pong/game/Game.ts | 46 ++++++----- .../src/pong/{pong.ts => game/Games.ts} | 32 +++++--- back/volume/src/pong/game/Map.ts | 15 ++++ back/volume/src/pong/game/constants.ts | 3 +- back/volume/src/pong/pong.gateway.ts | 21 +++-- back/volume/src/pong/pong.spec.ts | 2 +- front/volume/src/components/Pong/Game.ts | 12 ++- front/volume/src/components/Pong/Map.ts | 11 +++ .../components/Pong/MapCustomization.svelte | 82 +++++++++++++++++++ front/volume/src/components/Pong/Pong.svelte | 78 ++++++++++-------- front/volume/src/components/Pong/constants.ts | 3 +- .../components/Pong/dtos/GameCreationDto.ts | 6 ++ 15 files changed, 274 insertions(+), 91 deletions(-) create mode 100644 back/volume/src/pong/dtos/GameCreationDto.ts delete mode 100644 back/volume/src/pong/dtos/PlayerNamesDto.ts rename back/volume/src/pong/{pong.ts => game/Games.ts} (69%) create mode 100644 back/volume/src/pong/game/Map.ts create mode 100644 front/volume/src/components/Pong/Map.ts create mode 100644 front/volume/src/components/Pong/MapCustomization.svelte create mode 100644 front/volume/src/components/Pong/dtos/GameCreationDto.ts diff --git a/back/volume/src/pong/dtos/GameCreationDto.ts b/back/volume/src/pong/dtos/GameCreationDto.ts new file mode 100644 index 0000000..a7784ca --- /dev/null +++ b/back/volume/src/pong/dtos/GameCreationDto.ts @@ -0,0 +1,25 @@ +import { Type } from 'class-transformer' +import { + ArrayMaxSize, + ArrayMinSize, + IsDefined, + IsNotEmptyObject, + IsObject, + IsString, + ValidateNested +} from 'class-validator' +import { Map } from '../game/Map' + +export class GameCreationDto { + @IsString({ each: true }) + @ArrayMaxSize(2) + @ArrayMinSize(2) + playerNames!: string[] + + @IsDefined() + @IsObject() + @IsNotEmptyObject() + @ValidateNested() + @Type(() => Map) + map!: Map +} diff --git a/back/volume/src/pong/dtos/PlayerNamesDto.ts b/back/volume/src/pong/dtos/PlayerNamesDto.ts deleted file mode 100644 index d03d682..0000000 --- a/back/volume/src/pong/dtos/PlayerNamesDto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ArrayMaxSize, ArrayMinSize, IsString } from 'class-validator' - -export class PlayerNamesDto { - @IsString({ each: true }) - @ArrayMaxSize(2) - @ArrayMinSize(2) - playerNames!: string[] -} diff --git a/back/volume/src/pong/game/Ball.ts b/back/volume/src/pong/game/Ball.ts index 949a019..4e327a0 100644 --- a/back/volume/src/pong/game/Ball.ts +++ b/back/volume/src/pong/game/Ball.ts @@ -1,6 +1,7 @@ import { gameInfoConstants } from './constants' import { type Paddle } from './Paddle' import { Point, Rect } from './utils' +import { type Map } from './Map' export class Ball { rect: Rect @@ -23,16 +24,16 @@ export class Ball { return this.indexPlayerScored } - update (canvasRect: Rect, paddles: Paddle[]): void { + update (canvasRect: Rect, paddles: Paddle[], map: Map): void { if (!canvasRect.contains_x(this.rect)) { - this.indexPlayerScored = this.score() + this.indexPlayerScored = this.playerScored() } else { this.indexPlayerScored = -1 - this.move(canvasRect, paddles) + this.move(canvasRect, paddles, map) } } - move (canvasRect: Rect, paddles: Paddle[]): void { + move (canvasRect: Rect, paddles: Paddle[], map: Map): void { for (const paddle of paddles) { if (paddle.rect.collides(this.rect)) { if (this.speed.x < 0) { @@ -45,12 +46,20 @@ export class Ball { break } } + + for (const wall of map.walls) { + if (wall.collides(this.rect)) { + this.speed.x = this.speed.x * -1 + this.speed.y = this.speed.y * -1 + break + } + } + if (!canvasRect.contains_y(this.rect)) this.speed.y = this.speed.y * -1 this.rect.center.add_inplace(this.speed) } - // A player scored: return his index and reposition the ball - score (): number { + playerScored (): number { let indexPlayerScored: number if (this.rect.center.x <= this.spawn.x) { indexPlayerScored = 1 diff --git a/back/volume/src/pong/game/Game.ts b/back/volume/src/pong/game/Game.ts index e2962ac..9527206 100644 --- a/back/volume/src/pong/game/Game.ts +++ b/back/volume/src/pong/game/Game.ts @@ -10,17 +10,20 @@ import { } from './constants' import { randomUUID } from 'crypto' import { Spectator } from './Spectator' +import { type Map } from './Map' const GAME_TICKS = 30 function gameLoop (game: Game): void { - const canvasRect = new Rect( - new Point(gameInfoConstants.mapSize.x / 2, gameInfoConstants.mapSize.y / 2), - new Point(gameInfoConstants.mapSize.x, gameInfoConstants.mapSize.y) + const canvasRect: Rect = new Rect( + new Point(game.map.size.x / 2, game.map.size.y / 2), + new Point(game.map.size.x, game.map.size.y) ) + game.ball.update( canvasRect, - game.players.map((p) => p.paddle) + game.players.map((p) => p.paddle), + game.map ) const indexPlayerScored: number = game.ball.getIndexPlayerScored() if (indexPlayerScored !== -1) { @@ -46,21 +49,23 @@ function gameLoop (game: Game): void { export class Game { id: string timer: NodeJS.Timer | null + map: Map ball: Ball players: Player[] = [] spectators: Spectator[] = [] playing: boolean - constructor (sockets: WebSocket[], uuids: string[], names: string[]) { + constructor ( + sockets: WebSocket[], + uuids: string[], + names: string[], + map: Map + ) { this.id = randomUUID() this.timer = null this.playing = false - this.ball = new Ball( - new Point( - gameInfoConstants.mapSize.x / 2, - gameInfoConstants.mapSize.y / 2 - ) - ) + this.map = map + this.ball = new Ball(new Point(this.map.size.x / 2, this.map.size.y / 2)) for (let i = 0; i < uuids.length; i++) { this.addPlayer(sockets[i], uuids[i], names[i]) } @@ -70,8 +75,10 @@ export class Game { const yourPaddleIndex = this.players.findIndex((p) => p.name === name) return { ...gameInfoConstants, + mapSize: this.map.size, yourPaddleIndex, - gameId: this.id + gameId: this.id, + walls: this.map.walls } } @@ -83,16 +90,16 @@ export class Game { private addPlayer (socket: WebSocket, uuid: string, name: string): void { let paddleCoords = new Point( gameInfoConstants.playerXOffset, - gameInfoConstants.mapSize.y / 2 + this.map.size.y / 2 ) if (this.players.length === 1) { paddleCoords = new Point( - gameInfoConstants.mapSize.x - gameInfoConstants.playerXOffset, - gameInfoConstants.mapSize.y / 2 + this.map.size.x - gameInfoConstants.playerXOffset, + this.map.size.y / 2 ) } this.players.push( - new Player(socket, uuid, name, paddleCoords, gameInfoConstants.mapSize) + new Player(socket, uuid, name, paddleCoords, this.map.size) ) } @@ -119,12 +126,7 @@ export class Game { private start (): boolean { if (this.timer === null && this.players.length === 2) { - this.ball = new Ball( - new Point( - gameInfoConstants.mapSize.x / 2, - gameInfoConstants.mapSize.y / 2 - ) - ) + this.ball = new Ball(new Point(this.map.size.x / 2, this.map.size.y / 2)) this.players.forEach((p) => { p.newGame() }) diff --git a/back/volume/src/pong/pong.ts b/back/volume/src/pong/game/Games.ts similarity index 69% rename from back/volume/src/pong/pong.ts rename to back/volume/src/pong/game/Games.ts index f199195..473a04d 100644 --- a/back/volume/src/pong/pong.ts +++ b/back/volume/src/pong/game/Games.ts @@ -1,18 +1,28 @@ import { type WebSocket } from 'ws' -import { type GameInfo } from './game/constants' -import { Game } from './game/Game' -import { type Point } from './game/utils' -import { gameInfoConstants } from './game/constants' +import { type GameInfo } from './constants' +import { Game } from './Game' +import { Point } from './utils' +import { gameInfoConstants } from './constants' +import { type Map as GameMap } from './Map' +import { type GameCreationDto } from '../dtos/GameCreationDto' export class Games { private readonly playerNameToGameIndex = new Map() private readonly games = new Array() - newGame (sockets: WebSocket[], uuids: string[], names: string[]): void { - this.games.push(new Game(sockets, uuids, names)) - 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]}`) + newGame ( + sockets: WebSocket[], + uuids: string[], + gameCreationDto: GameCreationDto + ): void { + const names: string[] = gameCreationDto.playerNames + const map: GameMap = gameCreationDto.map + if (!this.isInAGame(names[0]) && !this.isInAGame(names[1])) { + this.games.push(new Game(sockets, uuids, names, map)) + 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 (name: string): void { @@ -45,7 +55,9 @@ export class Games { return { ...gameInfoConstants, yourPaddleIndex: 0, - gameId: '' + gameId: '', + mapSize: new Point(0, 0), + walls: [] } } diff --git a/back/volume/src/pong/game/Map.ts b/back/volume/src/pong/game/Map.ts new file mode 100644 index 0000000..48bece8 --- /dev/null +++ b/back/volume/src/pong/game/Map.ts @@ -0,0 +1,15 @@ +import { Type } from 'class-transformer' +import { ArrayMaxSize, IsArray, IsDefined, IsObject } from 'class-validator' +import { Point, Rect } from './utils' + +export class Map { + @IsObject() + @IsDefined() + @Type(() => Point) + size!: Point + + @IsArray() + @ArrayMaxSize(5) + @Type(() => Rect) + walls!: Rect[] +} diff --git a/back/volume/src/pong/game/constants.ts b/back/volume/src/pong/game/constants.ts index 2eccf50..c808d9a 100644 --- a/back/volume/src/pong/game/constants.ts +++ b/back/volume/src/pong/game/constants.ts @@ -1,4 +1,4 @@ -import { Point } from './utils' +import { Point, type Rect } from './utils' export const GAME_EVENTS = { START_GAME: 'START_GAME', @@ -14,6 +14,7 @@ export const GAME_EVENTS = { export interface GameInfo extends GameInfoConstants { yourPaddleIndex: number gameId: string + walls: Rect[] } export interface GameInfoConstants { mapSize: Point diff --git a/back/volume/src/pong/pong.gateway.ts b/back/volume/src/pong/pong.gateway.ts index 28476ff..05d2647 100644 --- a/back/volume/src/pong/pong.gateway.ts +++ b/back/volume/src/pong/pong.gateway.ts @@ -8,10 +8,10 @@ import { WebSocketGateway } from '@nestjs/websockets' import { randomUUID } from 'crypto' -import { Games } from './pong' -import { formatWebsocketData, Point } from './game/utils' +import { Games } from './game/Games' +import { formatWebsocketData, Point, Rect } from './game/utils' import { GAME_EVENTS } from './game/constants' -import { PlayerNamesDto } from './dtos/PlayerNamesDto' +import { GameCreationDto } from './dtos/GameCreationDto' import { UsePipes, ValidationPipe } from '@nestjs/common' import { type Game } from './game/Game' @@ -82,20 +82,27 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect { createGame ( @ConnectedSocket() client: WebSocketWithId, - @MessageBody() playerNamesDto: PlayerNamesDto + @MessageBody() gameCreationDto: GameCreationDto ): void { + gameCreationDto.map.walls = gameCreationDto.map.walls.map((wall) => { + return new Rect( + new Point(wall.center.x, wall.center.y), + new Point(wall.size.x, wall.size.y) + ) + }) + if (this.socketToPlayerName.size >= 2) { const player1Socket: WebSocketWithId | undefined = Array.from( this.socketToPlayerName.keys() ).find( (key) => - this.socketToPlayerName.get(key) === playerNamesDto.playerNames[0] + this.socketToPlayerName.get(key) === gameCreationDto.playerNames[0] ) const player2Socket: WebSocketWithId | undefined = Array.from( this.socketToPlayerName.keys() ).find( (key) => - this.socketToPlayerName.get(key) === playerNamesDto.playerNames[1] + this.socketToPlayerName.get(key) === gameCreationDto.playerNames[1] ) if ( @@ -107,7 +114,7 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect { this.games.newGame( [player1Socket, player2Socket], [player1Socket.id, player2Socket.id], - playerNamesDto.playerNames + gameCreationDto ) } } diff --git a/back/volume/src/pong/pong.spec.ts b/back/volume/src/pong/pong.spec.ts index bd9cfae..b11fd5b 100644 --- a/back/volume/src/pong/pong.spec.ts +++ b/back/volume/src/pong/pong.spec.ts @@ -1,5 +1,5 @@ import { Test, type TestingModule } from '@nestjs/testing' -import { Games } from './pong' +import { Games } from './game/Games' describe('Pong', () => { let provider: Games diff --git a/front/volume/src/components/Pong/Game.ts b/front/volume/src/components/Pong/Game.ts index 8e0691b..8f9e56d 100644 --- a/front/volume/src/components/Pong/Game.ts +++ b/front/volume/src/components/Pong/Game.ts @@ -3,7 +3,7 @@ import { GAME_EVENTS } from "./constants"; import type { GameInfo, GameUpdate } from "./constants"; import { Paddle } from "./Paddle"; import { Player } from "./Player"; -import { formatWebsocketData, Point } from "./utils"; +import { formatWebsocketData, Point, Rect } from "./utils"; const BG_COLOR = "black"; @@ -14,12 +14,14 @@ export class Game { players: Player[]; my_paddle: Paddle; id: string; + walls: Rect[]; constructor(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D) { this.canvas = canvas; this.context = context; this.players = []; this.my_paddle = null; + this.walls = []; } setInfo(data: GameInfo) { @@ -43,6 +45,13 @@ export class Game { if (data.yourPaddleIndex != -1) this.my_paddle = this.players[data.yourPaddleIndex].paddle; this.id = data.gameId; + this.walls = data.walls.map( + (w) => + new Rect( + new Point(w.center.x, w.center.y), + new Point(w.size.x, w.size.y) + ) + ); } start(socket: WebSocket) { @@ -76,6 +85,7 @@ export class Game { this.context.fillStyle = BG_COLOR; this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); + this.walls.forEach((w) => w.draw(this.context, "white")); this.players.forEach((p) => p.draw(this.context)); this.ball.draw(this.context); diff --git a/front/volume/src/components/Pong/Map.ts b/front/volume/src/components/Pong/Map.ts new file mode 100644 index 0000000..8e40473 --- /dev/null +++ b/front/volume/src/components/Pong/Map.ts @@ -0,0 +1,11 @@ +import type { Point, Rect } from "./utils"; + +export class Map { + size: Point; + walls: Rect[]; + + constructor(size: Point, walls: Rect[]) { + this.size = size; + this.walls = walls; + } +} diff --git a/front/volume/src/components/Pong/MapCustomization.svelte b/front/volume/src/components/Pong/MapCustomization.svelte new file mode 100644 index 0000000..82a1baf --- /dev/null +++ b/front/volume/src/components/Pong/MapCustomization.svelte @@ -0,0 +1,82 @@ + + +
+

Map Customization:

+
+ Width: + + Height: + +
+ click(e, false)} + on:contextmenu={(e) => click(e, true)} + /> +
diff --git a/front/volume/src/components/Pong/Pong.svelte b/front/volume/src/components/Pong/Pong.svelte index 22ce2ca..2cb0d8d 100644 --- a/front/volume/src/components/Pong/Pong.svelte +++ b/front/volume/src/components/Pong/Pong.svelte @@ -1,7 +1,10 @@
- {#if connected} - Your name: - -
- -
- Other player name: - -
- -
- -
- - -
- {:else} - Connecting to game server... - {/if} - +
+ {#if connected} + Your name: + +
+ +
+ Other player name: + +
+ +
+ +
+ + +
+ {:else} + Connecting to game server... + {/if} + +
+
diff --git a/front/volume/src/components/Pong/constants.ts b/front/volume/src/components/Pong/constants.ts index f66d300..5bb83c2 100644 --- a/front/volume/src/components/Pong/constants.ts +++ b/front/volume/src/components/Pong/constants.ts @@ -1,4 +1,4 @@ -import { Point } from "./utils"; +import { Point, Rect } from "./utils"; export const GAME_EVENTS = { START_GAME: "START_GAME", @@ -14,6 +14,7 @@ export const GAME_EVENTS = { export interface GameInfo extends GameInfoConstants { yourPaddleIndex: number; gameId: string; + walls: Rect[]; } export interface GameInfoConstants { mapSize: Point; diff --git a/front/volume/src/components/Pong/dtos/GameCreationDto.ts b/front/volume/src/components/Pong/dtos/GameCreationDto.ts new file mode 100644 index 0000000..6a04ee4 --- /dev/null +++ b/front/volume/src/components/Pong/dtos/GameCreationDto.ts @@ -0,0 +1,6 @@ +import type { Map } from "../Map"; + +export class GameCreationDto { + playerNames: string[]; + map: Map; +}