Browse Source

* Pong: Added matchmaking

* Frontend: cleaned up main content and made buttons functional

fix merge issues
master
vvandenb 2 years ago
committed by nicolas-arnaud
parent
commit
4f4d20ac19
  1. 3
      back/volume/src/pong/dtos/MatchmakingDto.ts
  2. 7
      back/volume/src/pong/dtos/MatchmakingDtoValidated.ts
  3. 2
      back/volume/src/pong/game/Games.ts
  4. 59
      back/volume/src/pong/game/MatchmakingQueue.ts
  5. 3
      back/volume/src/pong/game/constants.ts
  6. 36
      back/volume/src/pong/pong.gateway.ts
  7. 7
      front/volume/src/components/Play.svelte
  8. 11
      front/volume/src/components/Pong/Game.ts
  9. 32
      front/volume/src/components/Pong/GameComponent.svelte
  10. 55
      front/volume/src/components/Pong/GameCreation.svelte
  11. 2
      front/volume/src/components/Pong/MapCustomization.svelte
  12. 32
      front/volume/src/components/Pong/Matchmaking.svelte
  13. 162
      front/volume/src/components/Pong/Pong.svelte
  14. 44
      front/volume/src/components/Pong/SpectateFriend.svelte
  15. 1
      front/volume/src/components/Pong/constants.ts
  16. 3
      front/volume/src/components/Pong/dtos/MatchmakingDto.ts

3
back/volume/src/pong/dtos/MatchmakingDto.ts

@ -0,0 +1,3 @@
export class MatchmakingDto {
matchmaking!: boolean
}

7
back/volume/src/pong/dtos/MatchmakingDtoValidated.ts

@ -0,0 +1,7 @@
import { IsBoolean } from 'class-validator'
import { MatchmakingDto } from './MatchmakingDto'
export class MatchmakingDtoValidated extends MatchmakingDto {
@IsBoolean()
matchmaking!: boolean
}

2
back/volume/src/pong/game/Games.ts

@ -73,7 +73,7 @@ export class Games {
return game.getGameInfo(name) return game.getGameInfo(name)
} }
return { return {
yourPaddleIndex: 0, yourPaddleIndex: -2,
gameId: '', gameId: '',
mapSize: new Point(0, 0), mapSize: new Point(0, 0),
walls: [], walls: [],

59
back/volume/src/pong/game/MatchmakingQueue.ts

@ -0,0 +1,59 @@
import { type WebSocket } from 'ws'
import { type GameCreationDtoValidated } from '../dtos/GameCreationDtoValidated'
import { DEFAULT_MAP_SIZE } from './constants'
import { type Games } from './Games'
export class MatchmakingQueue {
games: Games
queue: Array<{ name: string, socket: WebSocket, uuid: string }>
constructor (games: Games) {
this.games = games
this.queue = []
}
addPlayer (name: string, socket: WebSocket, uuid: string): boolean {
let succeeded: boolean = false
if (!this.alreadyInQueue(name)) {
this.queue.push({ name, socket, uuid })
if (this.canCreateGame()) {
this.createGame()
}
succeeded = true
}
return succeeded
}
removePlayer (name: string): void {
this.queue = this.queue.filter((player) => player.name !== name)
}
alreadyInQueue (name: string): boolean {
return this.queue.some((player) => player.name === name)
}
canCreateGame (): boolean {
return this.queue.length >= 2
}
createGame (): void {
const player1 = this.queue.shift()
const player2 = this.queue.shift()
if (player1 === undefined || player2 === undefined) {
return
}
const gameCreationDto: GameCreationDtoValidated = {
playerNames: [player1.name, player2.name],
map: {
size: DEFAULT_MAP_SIZE,
walls: []
}
}
this.games.newGame(
[player1.socket, player2.socket],
[player1.uuid, player2.uuid],
gameCreationDto
)
}
}

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

@ -8,7 +8,8 @@ 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' SPECTATE: 'SPECTATE',
MATCHMAKING: 'MATCHMAKING'
} }
export const DEFAULT_MAP_SIZE = new Point(600, 400) export const DEFAULT_MAP_SIZE = new Point(600, 400)

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

@ -18,6 +18,8 @@ import { type Game } from './game/Game'
import { plainToClass } from 'class-transformer' import { plainToClass } from 'class-transformer'
import { PointDtoValidated } from './dtos/PointDtoValidated' import { PointDtoValidated } from './dtos/PointDtoValidated'
import { StringDtoValidated } from './dtos/StringDtoValidated' import { StringDtoValidated } from './dtos/StringDtoValidated'
import { MatchmakingQueue } from './game/MatchmakingQueue'
import { MatchmakingDtoValidated } from './dtos/MatchmakingDtoValidated'
interface WebSocketWithId extends WebSocket { interface WebSocketWithId extends WebSocket {
id: string id: string
@ -27,6 +29,7 @@ interface WebSocketWithId extends WebSocket {
export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect { export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
private readonly games: Games = new Games() private readonly games: Games = new Games()
private readonly socketToPlayerName = new Map<WebSocketWithId, string>() private readonly socketToPlayerName = new Map<WebSocketWithId, string>()
private readonly matchmakingQueue = new MatchmakingQueue(this.games)
handleConnection (client: WebSocketWithId): void { handleConnection (client: WebSocketWithId): void {
const uuid = randomUUID() const uuid = randomUUID()
@ -93,7 +96,7 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
@ConnectedSocket() @ConnectedSocket()
client: WebSocketWithId, client: WebSocketWithId,
@MessageBody() gameCreationDto: GameCreationDtoValidated @MessageBody() gameCreationDto: GameCreationDtoValidated
): void { ): { event: string, data: boolean } {
const realGameCreationDto: GameCreationDtoValidated = plainToClass( const realGameCreationDto: GameCreationDtoValidated = plainToClass(
GameCreationDtoValidated, GameCreationDtoValidated,
gameCreationDto gameCreationDto
@ -126,8 +129,10 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
[player1Socket.id, player2Socket.id], [player1Socket.id, player2Socket.id],
realGameCreationDto realGameCreationDto
) )
return { event: GAME_EVENTS.CREATE_GAME, data: true }
} }
} }
return { event: GAME_EVENTS.CREATE_GAME, data: false }
} }
@SubscribeMessage(GAME_EVENTS.READY) @SubscribeMessage(GAME_EVENTS.READY)
@ -147,10 +152,37 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
@ConnectedSocket() @ConnectedSocket()
client: WebSocketWithId, client: WebSocketWithId,
@MessageBody() playerToSpectate: StringDtoValidated @MessageBody() playerToSpectate: StringDtoValidated
): void { ): { event: string, data: boolean } {
let succeeded: boolean = false
const name: string | undefined = this.socketToPlayerName.get(client) const name: string | undefined = this.socketToPlayerName.get(client)
if (name !== undefined) { if (name !== undefined) {
this.games.spectateGame(playerToSpectate.value, client, client.id, name) this.games.spectateGame(playerToSpectate.value, client, client.id, name)
succeeded = true
}
return { event: GAME_EVENTS.SPECTATE, data: succeeded }
}
@UsePipes(new ValidationPipe({ whitelist: true }))
@SubscribeMessage(GAME_EVENTS.MATCHMAKING)
updateMatchmaking (
@ConnectedSocket()
client: WebSocketWithId,
@MessageBody() matchmakingUpdateData: MatchmakingDtoValidated
): { event: string, data: MatchmakingDtoValidated } {
let isMatchmaking: boolean = false
const name: string | undefined = this.socketToPlayerName.get(client)
if (name !== undefined) {
if (matchmakingUpdateData.matchmaking) {
if (this.matchmakingQueue.addPlayer(name, client, client.id)) {
isMatchmaking = true
}
} else {
this.matchmakingQueue.removePlayer(name)
}
}
return {
event: GAME_EVENTS.MATCHMAKING,
data: { matchmaking: isMatchmaking }
} }
} }
} }

7
front/volume/src/components/Play.svelte

@ -1,10 +1,15 @@
<script lang="ts"> <script lang="ts">
let creatingMatch: boolean = false;
</script> </script>
<main> <main>
<h1>Choose a gamemode</h1> <h1>Choose a gamemode</h1>
<button>Matchmaking</button> <button>Matchmaking</button>
<button>Play with a friend</button> <button on:click={() => creatingMatch}>Play with a friend</button>
{#if creatingMatch}
<CreateMatch />
{/if}
</main> </main>
<style> <style>

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

@ -7,6 +7,7 @@ import { Player } from "./Player";
import { formatWebsocketData, Point, Rect } from "./utils"; import { formatWebsocketData, Point, Rect } from "./utils";
const BG_COLOR = "black"; const BG_COLOR = "black";
const FPS = import.meta.env.VITE_FRONT_FPS;
export class Game { export class Game {
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
@ -16,6 +17,7 @@ export class Game {
my_paddle: Paddle; my_paddle: Paddle;
id: string; id: string;
walls: Rect[]; walls: Rect[];
drawInterval: NodeJS.Timer;
constructor(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D) { constructor(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D) {
this.canvas = canvas; this.canvas = canvas;
@ -23,17 +25,16 @@ export class Game {
this.players = []; this.players = [];
this.my_paddle = null; this.my_paddle = null;
this.walls = []; this.walls = [];
this.drawInterval = null;
} }
setInfo(data: GameInfo) { setInfo(data: GameInfo) {
this.canvas.width = data.mapSize.x; this.canvas.width = data.mapSize.x;
this.canvas.height = data.mapSize.y; this.canvas.height = data.mapSize.y;
this.ball = new Ball( this.ball = new Ball(
new Point(this.canvas.width / 2, this.canvas.height / 2), new Point(this.canvas.width / 2, this.canvas.height / 2),
data.ballSize data.ballSize
); );
const paddle1: Paddle = new Paddle( const paddle1: Paddle = new Paddle(
new Point(data.playerXOffset, this.canvas.height / 2), new Point(data.playerXOffset, this.canvas.height / 2),
data.paddleSize data.paddleSize
@ -53,6 +54,12 @@ export class Game {
new Point(w.size.x, w.size.y) new Point(w.size.x, w.size.y)
) )
); );
if (this.drawInterval === null) {
this.drawInterval = setInterval(() => {
this.draw();
}, 1000 / FPS);
}
console.log("Game updated!");
} }
start(socket: WebSocket) { start(socket: WebSocket) {

32
front/volume/src/components/Pong/GameComponent.svelte

@ -0,0 +1,32 @@
<script lang="ts">
import { onMount } from "svelte";
import { GAME_EVENTS } from "./constants";
import { formatWebsocketData } from "./utils";
export let gameCanvas: HTMLCanvasElement;
export let gamePlaying: boolean;
export let setupSocket: (
canvas: HTMLCanvasElement,
context: CanvasRenderingContext2D
) => void;
export let socket: WebSocket;
//Get canvas and its context
onMount(() => {
if (gameCanvas) {
const context: CanvasRenderingContext2D = gameCanvas.getContext(
"2d"
) as CanvasRenderingContext2D;
if (context) {
setupSocket(gameCanvas, context);
}
}
});
</script>
<div hidden={!gamePlaying}>
<button on:click={() => socket.send(formatWebsocketData(GAME_EVENTS.READY))}
>Ready</button
>
<canvas bind:this={gameCanvas} />
</div>

55
front/volume/src/components/Pong/GameCreation.svelte

@ -0,0 +1,55 @@
<script lang="ts">
import { formatWebsocketData } from "./utils";
import { Map } from "./Map";
import { DEFAULT_MAP_SIZE, GAME_EVENTS } from "./constants";
import MapCustomization from "./MapCustomization.svelte";
import type { GameCreationDto } from "./dtos/GameCreationDto";
export let username: string;
export let socket: WebSocket;
let map: Map = new Map(DEFAULT_MAP_SIZE.clone(), []);
let otherUsername: string = "Garfield";
function createGame() {
const data: GameCreationDto = {
playerNames: [username, otherUsername],
map,
};
socket.send(formatWebsocketData(GAME_EVENTS.CREATE_GAME, data));
}
</script>
<div class="overlay">
<div class="window" on:click|stopPropagation on:keydown|stopPropagation>
Friend:
<input bind:value={otherUsername} />
<button on:click={createGame}>
Create game vs {otherUsername}
</button>
<MapCustomization {map} />
</div>
</div>
<style>
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9998;
display: flex;
justify-content: center;
align-items: center;
}
.window {
background-color: #fff;
border: 1px solid #ccc;
border-radius: 5px;
padding: 1rem;
width: 80vw;
height: 80vh;
}
</style>

2
front/volume/src/components/Pong/MapCustomization.svelte

@ -65,7 +65,7 @@
</script> </script>
<div> <div>
<h2>Map Customization:</h2> <h1>Map Customization:</h1>
<div> <div>
Width: Width:
<input type="range" min="400" max="1000" bind:value={map.size.x} /> <input type="range" min="400" max="1000" bind:value={map.size.x} />

32
front/volume/src/components/Pong/Matchmaking.svelte

@ -0,0 +1,32 @@
<script lang="ts">
export let stopMatchmaking: () => void;
</script>
<div class="overlay">
<div class="window" on:click|stopPropagation on:keydown|stopPropagation>
<h1>Matchmaking...</h1>
<button on:click={stopMatchmaking}>Cancel</button>
</div>
</div>
<style>
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9998;
display: flex;
justify-content: center;
align-items: center;
}
.window {
background-color: #fff;
border: 1px solid #ccc;
border-radius: 5px;
padding: 1rem;
}
</style>

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

@ -1,38 +1,27 @@
<script lang="ts"> <script lang="ts">
import { DEFAULT_MAP_SIZE, GAME_EVENTS } from "./constants"; import { GAME_EVENTS } from "./constants";
import type { GameCreationDto } from "./dtos/GameCreationDto";
import { Game } from "./Game"; import { Game } from "./Game";
import MapCustomization from "./MapCustomization.svelte";
import { formatWebsocketData } from "./utils"; import { formatWebsocketData } from "./utils";
import { Map } from "./Map"; import GameCreation from "./GameCreation.svelte";
import { onMount } from "svelte"; import GameComponent from "./GameComponent.svelte";
import type { StringDto } from "./dtos/StringDto"; import type { StringDto } from "./dtos/StringDto";
import SpectateFriend from "./SpectateFriend.svelte";
import Matchmaking from "./Matchmaking.svelte";
import type { MatchmakingDto } from "./dtos/MatchmakingDto";
const FPS = import.meta.env.VITE_FRONT_FPS;
const SERVER_URL = `ws://${import.meta.env.VITE_HOST}:${ const SERVER_URL = `ws://${import.meta.env.VITE_HOST}:${
import.meta.env.VITE_BACK_PORT import.meta.env.VITE_BACK_PORT
}`; }`;
let createMatchWindow: boolean = false;
let spectateWindow: boolean = false;
let gamePlaying: boolean = false;
let matchmaking: boolean = false;
let gameCanvas: HTMLCanvasElement; let gameCanvas: HTMLCanvasElement;
let connected: boolean = false; let connected: boolean = false;
let loggedIn: 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 spectateUsername: string = "Garfield";
let map: Map = new Map(DEFAULT_MAP_SIZE.clone(), []);
//Get canvas and its context
onMount(() => {
if (gameCanvas) {
const context: CanvasRenderingContext2D = gameCanvas.getContext(
"2d"
) as CanvasRenderingContext2D;
if (context) {
setupSocket(gameCanvas, context);
}
}
});
function setupSocket( function setupSocket(
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
@ -51,20 +40,29 @@
game.update(data); game.update(data);
} else if (event == GAME_EVENTS.GET_GAME_INFO) { } else if (event == GAME_EVENTS.GET_GAME_INFO) {
if (data && data.gameId != game.id) { if (data && data.gameId != game.id) {
game.setInfo(data); if (data.yourPaddleIndex !== -2) {
setInterval(() => { gamePlaying = true;
game.draw(); game.setInfo(data);
}, 1000 / FPS); } else gamePlaying = false;
console.log("Game updated!");
} }
} else if (event == GAME_EVENTS.REGISTER_PLAYER) { } else if (event == GAME_EVENTS.REGISTER_PLAYER) {
console.log("Registered player: " + data.value);
if (data.value == username) { if (data.value == username) {
loggedIn = true; loggedIn = true;
setInterval(() => { setInterval(() => {
updateGameInfo(); updateGameInfo();
}, 1000); }, 1000);
} }
} else if (event == GAME_EVENTS.CREATE_GAME) {
if (data) gamePlaying = true;
} else if (event == GAME_EVENTS.MATCHMAKING) {
matchmaking = data.matchmaking;
} else if (event == GAME_EVENTS.SPECTATE) {
if (data) {
gamePlaying = true;
setInterval(() => {
updateGameInfo();
}, 1000);
}
} else { } else {
console.log( console.log(
"Unknown event from server: " + event + " with data " + data "Unknown event from server: " + event + " with data " + data
@ -84,54 +82,84 @@
socket.send(formatWebsocketData(GAME_EVENTS.GET_GAME_INFO)); socket.send(formatWebsocketData(GAME_EVENTS.GET_GAME_INFO));
} }
function spectate() {
const data: StringDto = { value: spectateUsername };
socket.send(formatWebsocketData(GAME_EVENTS.SPECTATE, data));
}
function logIn() { function logIn() {
const data: StringDto = { value: username }; const data: StringDto = { value: username };
socket.send(formatWebsocketData(GAME_EVENTS.REGISTER_PLAYER, data)); socket.send(formatWebsocketData(GAME_EVENTS.REGISTER_PLAYER, data));
} }
function createGame() { function startMatchmaking() {
const data: GameCreationDto = { const data: MatchmakingDto = { matchmaking: true };
playerNames: [username, otherUsername], socket.send(formatWebsocketData(GAME_EVENTS.MATCHMAKING, data));
map, }
};
socket.send(formatWebsocketData(GAME_EVENTS.CREATE_GAME, data)); function stopMatchmaking() {
const data: MatchmakingDto = { matchmaking: false };
socket.send(formatWebsocketData(GAME_EVENTS.MATCHMAKING, data));
} }
</script> </script>
<div> <div>
<div> {#if !loggedIn}
{#if connected} Log in:
Your name: <input bind:value={username} />
<input bind:value={username} /> <button on:click={logIn} disabled={!connected}> Log in </button>
<br /> <br />
<button on:click={logIn}> Log in </button> {/if}
<br /> <div hidden={!loggedIn}>
Other player name: <main>
<input bind:value={otherUsername} disabled={!loggedIn} /> <GameComponent {gameCanvas} {gamePlaying} {setupSocket} {socket} />
<br /> {#if gamePlaying}
<button on:click={createGame} disabled={!loggedIn}> <div />
Create game vs {otherUsername} {:else if connected}
</button> <h1>Choose a gamemode</h1>
<br /> <button on:click={startMatchmaking}>Matchmaking</button>
<button <button on:click={() => (createMatchWindow = true)}
on:click={() => socket.send(formatWebsocketData(GAME_EVENTS.READY))} >Play with a friend</button
disabled={!loggedIn}>Ready</button >
> <button on:click={() => (spectateWindow = true)}
<br /> >Spectate a friend</button
<input bind:value={spectateUsername} disabled={!loggedIn} /> >
<button on:click={spectate} disabled={!loggedIn}
>Spectate {spectateUsername}</button {#if matchmaking}
> <div on:click={stopMatchmaking} on:keydown={stopMatchmaking}>
<br /> <Matchmaking {stopMatchmaking} />
{:else} </div>
Connecting to game server... {:else if createMatchWindow}
{/if} <div
<canvas bind:this={gameCanvas} /> on:click={() => (createMatchWindow = false)}
on:keydown={() => (createMatchWindow = false)}
>
<GameCreation {socket} {username} />
</div>
{:else if spectateWindow}
<div
on:click={() => (spectateWindow = false)}
on:keydown={() => (spectateWindow = false)}
>
<SpectateFriend {socket} />
</div>
{/if}
{:else}
Connecting to game server...
{/if}
</main>
</div> </div>
<MapCustomization {map} />
</div> </div>
<style>
main {
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
margin-bottom: 2rem;
}
button {
font-size: 1.5rem;
padding: 1rem 2rem;
margin-bottom: 1rem;
}
</style>

44
front/volume/src/components/Pong/SpectateFriend.svelte

@ -0,0 +1,44 @@
<script lang="ts">
import { formatWebsocketData } from "./utils";
import { GAME_EVENTS } from "./constants";
import type { StringDto } from "./dtos/StringDto";
export let socket: WebSocket;
let spectateUsername: string = "Garfield";
function spectate() {
const data: StringDto = { value: spectateUsername };
socket.send(formatWebsocketData(GAME_EVENTS.SPECTATE, data));
}
</script>
<div class="overlay">
<div class="window" on:click|stopPropagation on:keydown|stopPropagation>
<input bind:value={spectateUsername} />
<button on:click={spectate}>Spectate</button>
</div>
</div>
<style>
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9998;
display: flex;
justify-content: center;
align-items: center;
}
.window {
background-color: #fff;
border: 1px solid #ccc;
border-radius: 5px;
padding: 1rem;
width: 400px;
}
</style>

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

@ -9,6 +9,7 @@ export const GAME_EVENTS = {
CREATE_GAME: "CREATE_GAME", CREATE_GAME: "CREATE_GAME",
REGISTER_PLAYER: "REGISTER_PLAYER", REGISTER_PLAYER: "REGISTER_PLAYER",
SPECTATE: "SPECTATE", SPECTATE: "SPECTATE",
MATCHMAKING: "MATCHMAKING",
}; };
export const DEFAULT_MAP_SIZE = new Point(600, 400); export const DEFAULT_MAP_SIZE = new Point(600, 400);

3
front/volume/src/components/Pong/dtos/MatchmakingDto.ts

@ -0,0 +1,3 @@
export class MatchmakingDto {
matchmaking!: boolean;
}
Loading…
Cancel
Save