Browse Source

Pong:

* Can now decline game invites
* No more "ready" in ranked
master
vvandenb 2 years ago
parent
commit
5a93bbc42d
  1. 4
      back/volume/src/main.ts
  2. 1
      back/volume/src/pong/dtos/GameInfo.ts
  3. 104
      back/volume/src/pong/game/Game.ts
  4. 28
      back/volume/src/pong/game/Games.ts
  5. 4
      back/volume/src/pong/game/MatchmakingQueue.ts
  6. 3
      back/volume/src/pong/game/constants.ts
  7. 23
      back/volume/src/pong/pong.gateway.ts
  8. 52
      front/volume/src/components/MatchHistory.svelte
  9. 20
      front/volume/src/components/NavBar.svelte
  10. 43
      front/volume/src/components/Pong/Game.ts
  11. 17
      front/volume/src/components/Pong/GameComponent.svelte
  12. 1
      front/volume/src/components/Pong/GameCreation.svelte
  13. 9
      front/volume/src/components/Pong/MapCustomization.svelte
  14. 23
      front/volume/src/components/Pong/Pong.svelte
  15. 2
      front/volume/src/components/Pong/constants.ts
  16. 1
      front/volume/src/components/Pong/dtos/GameInfo.ts

4
back/volume/src/main.ts

@ -12,7 +12,7 @@ async function bootstrap (): Promise<void> {
const logger = new Logger()
const app = await NestFactory.create<NestExpressApplication>(AppModule)
const port =
process.env.BACK_PORT && process.env.BACK_PORT !== ''
process.env.BACK_PORT !== undefined && process.env.BACK_PORT !== ''
? +process.env.BACK_PORT
: 3001
const cors = {
@ -28,7 +28,7 @@ async function bootstrap (): Promise<void> {
resave: false,
saveUninitialized: false,
secret:
process.env.JWT_SECRET && process.env.JWT_SECRET !== ''
process.env.JWT_SECRET !== undefined && process.env.JWT_SECRET !== ''
? process.env.JWT_SECRET
: 'secret'
})

1
back/volume/src/pong/dtos/GameInfo.ts

@ -9,4 +9,5 @@ export class GameInfo {
playerXOffset!: number
ballSize!: Point
winScore!: number
ranked!: boolean
}

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

@ -17,38 +17,6 @@ import { type GameUpdate } from '../dtos/GameUpdate'
import { type GameInfo } from '../dtos/GameInfo'
import { type PongService } from '../pong.service'
function gameLoop (game: Game): void {
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.map
)
const indexPlayerScored: number = game.ball.getIndexPlayerScored()
if (indexPlayerScored !== -1) {
game.players[indexPlayerScored].score += 1
if (game.players[indexPlayerScored].score >= DEFAULT_WIN_SCORE) {
console.log(`${game.players[indexPlayerScored].name} won!`)
void game.stop()
}
}
const data: GameUpdate = {
paddlesPositions: game.players.map((p) => p.paddle.rect.center),
ballPosition: game.ball.rect.center,
scores: game.players.map((p) => p.score)
}
const websocketData: string = formatWebsocketData(
GAME_EVENTS.GAME_TICK,
data
)
game.broadcastGame(websocketData)
}
export class Game {
id: string
timer: NodeJS.Timer | null
@ -57,6 +25,7 @@ export class Game {
players: Player[] = []
spectators: Spectator[] = []
playing: boolean
ranked: boolean
gameStoppedCallback: (name: string) => void
constructor (
@ -65,11 +34,13 @@ export class Game {
names: string[],
map: MapDtoValidated,
gameStoppedCallback: (name: string) => void,
private readonly pongService: PongService
private readonly pongService: PongService,
ranked: boolean
) {
this.id = randomUUID()
this.timer = null
this.playing = false
this.ranked = ranked
this.map = map
this.gameStoppedCallback = gameStoppedCallback
this.ball = new Ball(new Point(this.map.size.x / 2, this.map.size.y / 2))
@ -88,7 +59,8 @@ export class Game {
paddleSize: DEFAULT_PADDLE_SIZE,
playerXOffset: DEFAULT_PLAYER_X_OFFSET,
ballSize: DEFAULT_BALL_SIZE,
winScore: DEFAULT_WIN_SCORE
winScore: DEFAULT_WIN_SCORE,
ranked: this.ranked
}
}
@ -108,47 +80,41 @@ export class Game {
this.players.push(
new Player(socket, uuid, name, paddleCoords, this.map.size)
)
}
removePlayer (name: string): void {
const playerIndex: number = this.players.findIndex((p) => p.name === name)
if (playerIndex !== -1) {
this.players.splice(playerIndex, 1)
if (this.players.length < 2) {
void this.stop()
}
if (this.ranked) {
this.ready(name)
}
}
ready (name: string): void {
const playerIndex: number = this.players.findIndex((p) => p.name === name)
if (playerIndex !== -1) {
if (playerIndex !== -1 && !this.players[playerIndex].ready) {
this.players[playerIndex].ready = true
console.log(`${this.players[playerIndex].name} is ready!`)
if (this.players.every((p) => p.ready)) {
console.log(`${this.players[playerIndex].name} is ready`)
if (this.players.length === 2 && this.players.every((p) => p.ready)) {
this.start()
}
}
}
private start (): boolean {
private start (): void {
if (this.timer === null && this.players.length === 2) {
this.ball = new Ball(new Point(this.map.size.x / 2, this.map.size.y / 2))
this.players.forEach((p) => {
p.newGame()
})
this.timer = setInterval(gameLoop, 1000 / GAME_TICKS, this)
this.broadcastGame(formatWebsocketData(GAME_EVENTS.START_GAME))
this.playing = true
return true
this.broadcastGame(formatWebsocketData(GAME_EVENTS.START_GAME))
this.timer = setInterval(this.gameLoop.bind(this), 1000 / GAME_TICKS)
console.log(`Game ${this.id} started`)
}
return false
}
async stop (): Promise<void> {
if (this.timer !== null) {
await this.pongService.saveResult(this.players)
this.gameStoppedCallback(this.players[0].name)
if (this.players.length !== 0) {
this.gameStoppedCallback(this.players[0].name)
}
clearInterval(this.timer)
this.timer = null
@ -165,7 +131,7 @@ export class Game {
}
}
broadcastGame (data: string): void {
private broadcastGame (data: string): void {
this.players.forEach((p) => {
p.socket.send(data)
})
@ -177,4 +143,36 @@ export class Game {
isPlaying (): boolean {
return this.playing
}
private gameLoop (): void {
const canvasRect: Rect = new Rect(
new Point(this.map.size.x / 2, this.map.size.y / 2),
new Point(this.map.size.x, this.map.size.y)
)
this.ball.update(
canvasRect,
this.players.map((p) => p.paddle),
this.map
)
const indexPlayerScored: number = this.ball.getIndexPlayerScored()
if (indexPlayerScored !== -1) {
this.players[indexPlayerScored].score += 1
if (this.players[indexPlayerScored].score >= DEFAULT_WIN_SCORE) {
console.log(`${this.players[indexPlayerScored].name} won`)
void this.stop()
}
}
const data: GameUpdate = {
paddlesPositions: this.players.map((p) => p.paddle.rect.center),
ballPosition: this.ball.rect.center,
scores: this.players.map((p) => p.score)
}
const websocketData: string = formatWebsocketData(
GAME_EVENTS.GAME_TICK,
data
)
this.broadcastGame(websocketData)
}
}

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

@ -21,7 +21,8 @@ export class Games {
newGame (
sockets: WebSocket[],
uuids: string[],
gameCreationDto: GameCreationDtoValidated
gameCreationDto: GameCreationDtoValidated,
ranked: boolean
): void {
const names: string[] = gameCreationDto.playerNames
const map: GameMap = {
@ -35,8 +36,9 @@ export class Games {
uuids,
names,
map,
this.gameStopped.bind(this, names[0]),
this.pongService
this.deleteGame.bind(this, names[0]),
this.pongService,
ranked
)
)
this.playerNameToGameIndex.set(names[0], this.games.length - 1)
@ -49,13 +51,6 @@ export class Games {
}
}
removePlayer (name: string): void {
const game: Game | undefined = this.playerGame(name)
if (game !== undefined) {
game.removePlayer(name)
}
}
ready (name: string): void {
const game: Game | undefined = this.playerGame(name)
if (game !== undefined) {
@ -63,7 +58,7 @@ export class Games {
}
}
private gameStopped (name: string): void {
private deleteGame (name: string): void {
const game: Game | undefined = this.playerGame(name)
if (game !== undefined) {
this.games.splice(this.games.indexOf(game), 1)
@ -87,7 +82,8 @@ export class Games {
paddleSize: DEFAULT_PADDLE_SIZE,
playerXOffset: DEFAULT_PLAYER_X_OFFSET,
ballSize: DEFAULT_BALL_SIZE,
winScore: DEFAULT_WIN_SCORE
winScore: DEFAULT_WIN_SCORE,
ranked: false
}
}
@ -123,4 +119,12 @@ export class Games {
this.games[gameIndex].addSpectator(socket, uuid, name)
}
}
async leaveGame (name: string): Promise<void> {
const game: Game | undefined = this.playerGame(name)
if (game !== undefined && !game.ranked) {
await game.stop()
this.deleteGame(name)
}
}
}

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

@ -49,11 +49,13 @@ export class MatchmakingQueue {
walls: []
}
}
const ranked = true
this.games.newGame(
[player1.socket, player2.socket],
[player1.uuid, player2.uuid],
gameCreationDto
gameCreationDto,
ranked
)
}
}

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

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

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

@ -57,7 +57,7 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
): void {
const name: string | undefined = this.socketToPlayerName.get(client)
const game: Game | undefined = this.games.playerGame(name)
if (game !== undefined && game.isPlaying()) {
if (game?.isPlaying() !== undefined) {
void game.stop()
}
if (name !== undefined) {
@ -152,10 +152,12 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
(client.id === player1Socket.id || client.id === player2Socket.id) &&
player1Socket.id !== player2Socket.id
) {
const ranked = false
this.games.newGame(
[player1Socket, player2Socket],
[player1Socket.id, player2Socket.id],
realGameCreationDto
realGameCreationDto,
ranked
)
return { event: GAME_EVENTS.CREATE_GAME, data: true }
}
@ -167,11 +169,14 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
ready (
@ConnectedSocket()
client: WebSocketWithId
): void {
): { event: string, data: boolean } {
let succeeded: boolean = false
const name: string | undefined = this.socketToPlayerName.get(client)
if (name !== undefined) {
this.games.ready(name)
succeeded = true
}
return { event: GAME_EVENTS.READY, data: succeeded }
}
@UsePipes(new ValidationPipe({ whitelist: true }))
@ -213,4 +218,16 @@ export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect {
data: { matchmaking: isMatchmaking }
}
}
@UsePipes(new ValidationPipe({ whitelist: true }))
@SubscribeMessage(GAME_EVENTS.LEAVE_GAME)
leaveGame (
@ConnectedSocket()
client: WebSocketWithId
): void {
const name: string | undefined = this.socketToPlayerName.get(client)
if (name !== undefined) {
void this.games.leaveGame(name)
}
}
}

52
front/volume/src/components/MatchHistory.svelte

@ -11,7 +11,7 @@
export let matches: Array<Match> = [];
function displayDate(str: string) {
const splitT = str.split("T");
const splitDate = splitT[0].split('-')
const splitDate = splitT[0].split("-");
const splitDot = splitT[1].split(".");
return `${splitDate[1]}/${splitDate[2]}-${splitDot[0]}`;
}
@ -21,27 +21,30 @@
<div class="history" on:click|stopPropagation on:keydown|stopPropagation>
<div>
{#if matches.length > 0}
<table>
<thead>
<tr>
<th colspan="3">Last 10 monkey games</th>
</tr>
</thead>
<tbody>
<tr>
<td>Date</td>
<td>Players</td>
<td>Scores</td>
</tr>
{#each matches.slice(0, 10) as match}
<tr>
<td>{displayDate(match.date.toString())}</td>
<td>{match.players[0].username}<br>{match.players[1].username}</td>
<td>{match.score[0]}<br>{match.score[1]}</td>
</tr>
{/each}
</tbody>
</table>
<table>
<thead>
<tr>
<th colspan="3">Last 10 monkey games</th>
</tr>
</thead>
<tbody>
<tr>
<td>Date</td>
<td>Players</td>
<td>Scores</td>
</tr>
{#each matches.slice(0, 10) as match}
<tr>
<td>{displayDate(match.date.toString())}</td>
<td
>{match.players[0].username}<br />{match.players[1]
.username}</td
>
<td>{match.score[0]}<br />{match.score[1]}</td>
</tr>
{/each}
</tbody>
</table>
{:else}
<p>No matches to display</p>
{/if}
@ -69,15 +72,14 @@
border-radius: 5px;
padding: 1rem;
width: 300px;
display:flex;
display: flex;
justify-content: center;
}
td {
border:1px solid #111;
border: 1px solid #111;
text-align: center;
max-width: 15ch;
overflow: hidden;
}
</style>

20
front/volume/src/components/NavBar.svelte

@ -26,30 +26,22 @@
{#each links as link}
{#if link.text === "Leaderboard"}
<li>
<button on:click={clickLeaderboard}>
Leaderboard
</button>
<button on:click={clickLeaderboard}> Leaderboard </button>
</li>
{/if}
{#if link.text === "Spectate"}
<li>
<button on:click={clickSpectate}>
Spectate
</button>
<button on:click={clickSpectate}> Spectate </button>
</li>
{/if}
{#if link.text === "Channels"}
<li>
<button on:click={clickChannels}>
Channels
</button>
<button on:click={clickChannels}> Channels </button>
</li>
{/if}
{#if link.text === "Friends"}
<li>
<button on:click={clickFriends}>
Friends
</button>
<button on:click={clickFriends}> Friends </button>
</li>
{/if}
{#if link.text === "Profile"}
@ -61,9 +53,7 @@
{/if}
{#if link.text === "History"}
<li>
<button on:click={clickHistory}>
History
</button>
<button on:click={clickHistory}> History </button>
</li>
{/if}
{#if link.text === "Home"}

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

@ -20,6 +20,8 @@ export class Game {
drawInterval: NodeJS.Timer;
elementsColor: string;
backgroundColor: string;
ranked: boolean;
youAreReady: boolean;
constructor(
renderCanvas: HTMLCanvasElement,
@ -33,10 +35,13 @@ export class Game {
this.context = context;
this.players = [];
this.my_paddle = null;
this.id = "";
this.walls = [];
this.drawInterval = null;
this.elementsColor = elementsColor;
this.backgroundColor = backgroundColor;
this.ranked = false;
this.youAreReady = false;
}
setInfo(data: GameInfo) {
@ -44,6 +49,8 @@ export class Game {
this.renderCanvas.height = data.mapSize.y;
this.canvas.width = data.mapSize.x;
this.canvas.height = data.mapSize.y;
this.ranked = data.ranked;
this.youAreReady = false;
this.ball = new Ball(
new Point(this.canvas.width / 2, this.canvas.height / 2),
data.ballSize
@ -76,26 +83,28 @@ export class Game {
}
start(socket: WebSocket) {
if (this.my_paddle) {
this.renderCanvas.addEventListener("pointermove", (e) => {
this.my_paddle.move(e);
const data: Point = this.my_paddle.rect.center;
socket.send(formatWebsocketData(GAME_EVENTS.PLAYER_MOVE, data));
});
console.log("Game started!");
}
// if (this.my_paddle) {
this.renderCanvas.addEventListener("pointermove", (e) => {
this.my_paddle.move(e);
const data: Point = this.my_paddle.rect.center;
socket.send(formatWebsocketData(GAME_EVENTS.PLAYER_MOVE, data));
});
console.log("Game started!");
// }
}
update(data: GameUpdate) {
if (this.players[0].paddle != this.my_paddle) {
this.players[0].paddle.rect.center = data.paddlesPositions[0];
}
if (this.players[1].paddle != this.my_paddle) {
this.players[1].paddle.rect.center = data.paddlesPositions[1];
}
this.ball.rect.center = data.ballPosition;
for (let i = 0; i < data.scores.length; i++) {
this.players[i].score = data.scores[i];
if (this.id !== "") {
if (this.players[0].paddle != this.my_paddle) {
this.players[0].paddle.rect.center = data.paddlesPositions[0];
}
if (this.players[1].paddle != this.my_paddle) {
this.players[1].paddle.rect.center = data.paddlesPositions[1];
}
this.ball.rect.center = data.ballPosition;
for (let i = 0; i < data.scores.length; i++) {
this.players[i].score = data.scores[i];
}
}
}

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

@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from "svelte";
import { GAME_EVENTS } from "./constants";
import type { Game } from "./Game";
import { formatWebsocketData } from "./utils";
export let gamePlaying: boolean;
@ -10,11 +11,11 @@
context: CanvasRenderingContext2D
) => void;
export let socket: WebSocket;
export let game: Game;
let gameCanvas: HTMLCanvasElement;
let renderCanvas: HTMLCanvasElement;
//Get canvas and its context
onMount(() => {
if (gameCanvas && renderCanvas) {
const context: CanvasRenderingContext2D = gameCanvas.getContext("2d");
@ -26,9 +27,17 @@
</script>
<div hidden={!gamePlaying} class="gameDiv">
<button on:click={() => socket.send(formatWebsocketData(GAME_EVENTS.READY))}
>Ready</button
>
{#if game && !game.ranked}
<button
on:click={() => socket.send(formatWebsocketData(GAME_EVENTS.LEAVE_GAME))}
>Leave</button
>
<button
disabled={game.youAreReady}
on:click={() => socket.send(formatWebsocketData(GAME_EVENTS.READY))}
>Ready</button
>
{/if}
<canvas hidden bind:this={gameCanvas} />
<canvas bind:this={renderCanvas} class="renderCanvas" />
</div>

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

@ -7,6 +7,7 @@
export let username: string;
export let socket: WebSocket;
let map: Map = new Map(DEFAULT_MAP_SIZE.clone(), []);
let otherUsername: string = "Garfield";

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

@ -50,10 +50,7 @@
function addWall(e: MouseEvent) {
const rect: any = gameCanvas.getBoundingClientRect();
const wall = new Rect(
getMapXY(e),
new Point(wallWidth, wallHeight)
);
const wall = new Rect(getMapXY(e), new Point(wallWidth, wallHeight));
const ballSpawnArea = new Rect(
new Point(map.size.x / 2, map.size.y / 2),
new Point(DEFAULT_BALL_SIZE.x * 5, DEFAULT_BALL_SIZE.y * 5)
@ -78,8 +75,8 @@
function getMapXY(e: MouseEvent): Point {
const canvasPoint: Point = getCanvasXY(new Point(e.pageX, e.pageY));
const x = canvasPoint.x * gameCanvas.width / gameCanvas.clientWidth;
const y = canvasPoint.y * gameCanvas.height / gameCanvas.clientHeight;
const x = (canvasPoint.x * gameCanvas.width) / gameCanvas.clientWidth;
const y = (canvasPoint.y * gameCanvas.height) / gameCanvas.clientHeight;
return new Point(x, y);
}
</script>

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

@ -39,7 +39,13 @@
renderCanvas = _renderCanvas;
canvas = _canvas;
context = _context;
game = new Game(_renderCanvas, canvas, context, elementsColor, backgroundColor);
game = new Game(
_renderCanvas,
canvas,
context,
elementsColor,
backgroundColor
);
socket.onmessage = function (e) {
const event_json = JSON.parse(e.data);
@ -52,7 +58,7 @@
game.update(data);
} else if (event == GAME_EVENTS.GET_GAME_INFO) {
if (data && data.gameId != game.id) {
if (gamePlaying && data.gameId == '') {
if (gamePlaying && data.gameId == "") {
resetMenus();
gamePlaying = false;
}
@ -81,26 +87,28 @@
updateGameInfo();
}, 1000);
}
} else if (event == GAME_EVENTS.READY) {
game.youAreReady = true;
} else {
console.log(
"Unknown event from server: " + event + " with data " + data
);
}
};
socket.onopen = onSocketOpen
socket.onclose = onSocketClose
socket.onopen = onSocketOpen;
socket.onclose = onSocketClose;
}
async function onSocketOpen() {
await getUser();
void logIn();
connected = true;
};
}
async function onSocketClose() {
connected = false;
setupSocket(renderCanvas, canvas, context);
};
}
function updateGameInfo() {
socket.send(formatWebsocketData(GAME_EVENTS.GET_GAME_INFO));
@ -128,6 +136,7 @@
createMatchWindow = false;
spectateWindow = false;
matchmaking = false;
game.youAreReady = false;
}
$: {
@ -138,7 +147,7 @@
</script>
<main>
<GameComponent {gamePlaying} {setupSocket} {socket} />
<GameComponent {game} {gamePlaying} {setupSocket} {socket} />
{#if gamePlaying}
<div />
{:else if loggedIn}

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

@ -10,6 +10,7 @@ export const GAME_EVENTS = {
REGISTER_PLAYER: "REGISTER_PLAYER",
SPECTATE: "SPECTATE",
MATCHMAKING: "MATCHMAKING",
LEAVE_GAME: "LEAVE_GAME",
};
export const DEFAULT_MAP_SIZE = new Point(600, 400);
@ -17,3 +18,4 @@ export const DEFAULT_PADDLE_SIZE = new Point(6, 50);
export const DEFAULT_BALL_SIZE = new Point(20, 20);
export const DEFAULT_PLAYER_X_OFFSET = 50;
export const DEFAULT_WIN_SCORE = 5;
export const GAME_TICKS = 30;

1
front/volume/src/components/Pong/dtos/GameInfo.ts

@ -9,4 +9,5 @@ export class GameInfo {
playerXOffset!: number;
ballSize!: Point;
winScore!: number;
ranked!: boolean;
}

Loading…
Cancel
Save