nicolas-arnaud
2 years ago
22 changed files with 2622 additions and 626 deletions
@ -0,0 +1,16 @@ |
|||
{ |
|||
"env": { |
|||
"browser": true, |
|||
"es2021": true |
|||
}, |
|||
"extends": "standard-with-typescript", |
|||
"overrides": [ |
|||
], |
|||
"parserOptions": { |
|||
"ecmaVersion": "latest", |
|||
"sourceType": "module", |
|||
"project": ["./tsconfig.json"] |
|||
}, |
|||
"rules": { |
|||
} |
|||
} |
File diff suppressed because it is too large
@ -1,7 +1,7 @@ |
|||
import { Module } from '@nestjs/common'; |
|||
import { PongModule } from './pong/pong.module'; |
|||
import { Module } from '@nestjs/common' |
|||
import { PongModule } from './pong/pong.module' |
|||
|
|||
@Module({ |
|||
imports: [PongModule] |
|||
imports: [PongModule] |
|||
}) |
|||
export class AppModule {} |
|||
|
@ -1,10 +1,10 @@ |
|||
import { NestFactory } from '@nestjs/core'; |
|||
import { WsAdapter } from '@nestjs/platform-ws'; |
|||
import { AppModule } from './app.module'; |
|||
import { NestFactory } from '@nestjs/core' |
|||
import { WsAdapter } from '@nestjs/platform-ws' |
|||
import { AppModule } from './app.module' |
|||
|
|||
async function bootstrap() { |
|||
const app = await NestFactory.create(AppModule); |
|||
app.useWebSocketAdapter(new WsAdapter(app)); |
|||
await app.listen(3001); |
|||
async function bootstrap () { |
|||
const app = await NestFactory.create(AppModule) |
|||
app.useWebSocketAdapter(new WsAdapter(app)) |
|||
await app.listen(3001) |
|||
} |
|||
bootstrap(); |
|||
bootstrap() |
|||
|
@ -1,58 +1,65 @@ |
|||
import { gameInfoConstants } from './constants'; |
|||
import { Paddle } from './Paddle'; |
|||
import { Point, Rect } from './utils'; |
|||
import { gameInfoConstants } from './constants' |
|||
import { type Paddle } from './Paddle' |
|||
import { Point, Rect } from './utils' |
|||
|
|||
export class Ball { |
|||
rect: Rect; |
|||
speed: Point; |
|||
spawn: Point; |
|||
indexPlayerScored: number; |
|||
|
|||
constructor(spawn: Point, size: Point = gameInfoConstants.ballSize, speed: Point = new Point(10, 2)) { |
|||
this.rect = new Rect(spawn, size); |
|||
this.speed = speed; |
|||
this.spawn = spawn.clone(); |
|||
} |
|||
|
|||
getIndexPlayerScored(): number { |
|||
return this.indexPlayerScored; |
|||
} |
|||
|
|||
update(canvas_rect: Rect, paddles: Paddle[]) { |
|||
if (!canvas_rect.contains_x(this.rect)) { |
|||
this.indexPlayerScored = this.score(); |
|||
} else { |
|||
this.indexPlayerScored = -1; |
|||
this.move(canvas_rect, paddles); |
|||
} |
|||
} |
|||
|
|||
move(canvas_rect: Rect, paddles: Paddle[]) { |
|||
for (const paddle of paddles) { |
|||
if (paddle.rect.collides(this.rect)) { |
|||
if (this.speed.x < 0) this.rect.center.x = paddle.rect.center.x + paddle.rect.size.x; |
|||
else this.rect.center.x = paddle.rect.center.x - paddle.rect.size.x; |
|||
this.speed.x = this.speed.x * -1; |
|||
this.speed.y = ((this.rect.center.y - paddle.rect.center.y) / paddle.rect.size.y) * 20; |
|||
break; |
|||
} |
|||
} |
|||
if (!canvas_rect.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 { |
|||
let index_player_scored: number; |
|||
if (this.rect.center.x <= this.spawn.x) { |
|||
index_player_scored = 1; |
|||
} else { |
|||
index_player_scored = 0; |
|||
} |
|||
|
|||
this.rect.center = this.spawn.clone(); |
|||
this.speed.x = this.speed.x * -1; |
|||
|
|||
return index_player_scored; |
|||
} |
|||
rect: Rect |
|||
speed: Point |
|||
spawn: Point |
|||
indexPlayerScored: number |
|||
|
|||
constructor ( |
|||
spawn: Point, |
|||
size: Point = gameInfoConstants.ballSize, |
|||
speed: Point = new Point(10, 2) |
|||
) { |
|||
this.rect = new Rect(spawn, size) |
|||
this.speed = speed |
|||
this.spawn = spawn.clone() |
|||
} |
|||
|
|||
getIndexPlayerScored (): number { |
|||
return this.indexPlayerScored |
|||
} |
|||
|
|||
update (canvas_rect: Rect, paddles: Paddle[]) { |
|||
if (!canvas_rect.contains_x(this.rect)) { |
|||
this.indexPlayerScored = this.score() |
|||
} else { |
|||
this.indexPlayerScored = -1 |
|||
this.move(canvas_rect, paddles) |
|||
} |
|||
} |
|||
|
|||
move (canvas_rect: Rect, paddles: Paddle[]) { |
|||
for (const paddle of paddles) { |
|||
if (paddle.rect.collides(this.rect)) { |
|||
if (this.speed.x < 0) { |
|||
this.rect.center.x = paddle.rect.center.x + paddle.rect.size.x |
|||
} else this.rect.center.x = paddle.rect.center.x - paddle.rect.size.x |
|||
this.speed.x = this.speed.x * -1 |
|||
this.speed.y = |
|||
((this.rect.center.y - paddle.rect.center.y) / paddle.rect.size.y) * |
|||
20 |
|||
break |
|||
} |
|||
} |
|||
if (!canvas_rect.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 { |
|||
let index_player_scored: number |
|||
if (this.rect.center.x <= this.spawn.x) { |
|||
index_player_scored = 1 |
|||
} else { |
|||
index_player_scored = 0 |
|||
} |
|||
|
|||
this.rect.center = this.spawn.clone() |
|||
this.speed.x = this.speed.x * -1 |
|||
|
|||
return index_player_scored |
|||
} |
|||
} |
|||
|
@ -1,133 +1,160 @@ |
|||
import { Ball } from './Ball'; |
|||
import { WebSocket } from 'ws'; |
|||
import { formatWebsocketData, Point, Rect } from './utils'; |
|||
import { Player } from './Player'; |
|||
import { GameInfo, gameInfoConstants, GameUpdate, GAME_EVENTS } from './constants'; |
|||
import { randomUUID } from 'crypto'; |
|||
|
|||
const GAME_TICKS = 30; |
|||
|
|||
function gameLoop(game: Game) { |
|||
const canvas_rect = new Rect( |
|||
new Point(gameInfoConstants.mapSize.x / 2, gameInfoConstants.mapSize.y / 2), |
|||
new Point(gameInfoConstants.mapSize.x, gameInfoConstants.mapSize.y) |
|||
); |
|||
game.ball.update( |
|||
canvas_rect, |
|||
game.players.map((p) => p.paddle) |
|||
); |
|||
const index_player_scored: number = game.ball.getIndexPlayerScored(); |
|||
if (index_player_scored != -1) { |
|||
game.players[index_player_scored].score += 1; |
|||
if (game.players[index_player_scored].score >= gameInfoConstants.winScore) { |
|||
console.log(`${game.players[index_player_scored].name} won!`); |
|||
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); |
|||
import { Ball } from './Ball' |
|||
import { type WebSocket } from 'ws' |
|||
import { formatWebsocketData, Point, Rect } from './utils' |
|||
import { Player } from './Player' |
|||
import { |
|||
type GameInfo, |
|||
gameInfoConstants, |
|||
type GameUpdate, |
|||
GAME_EVENTS |
|||
} from './constants' |
|||
import { randomUUID } from 'crypto' |
|||
|
|||
const GAME_TICKS = 30 |
|||
|
|||
function gameLoop (game: Game) { |
|||
const canvas_rect = new Rect( |
|||
new Point(gameInfoConstants.mapSize.x / 2, gameInfoConstants.mapSize.y / 2), |
|||
new Point(gameInfoConstants.mapSize.x, gameInfoConstants.mapSize.y) |
|||
) |
|||
game.ball.update( |
|||
canvas_rect, |
|||
game.players.map((p) => p.paddle) |
|||
) |
|||
const index_player_scored: number = game.ball.getIndexPlayerScored() |
|||
if (index_player_scored != -1) { |
|||
game.players[index_player_scored].score += 1 |
|||
if (game.players[index_player_scored].score >= gameInfoConstants.winScore) { |
|||
console.log(`${game.players[index_player_scored].name} won!`) |
|||
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; |
|||
ball: Ball; |
|||
players: Player[] = []; |
|||
playing: boolean; |
|||
|
|||
constructor(sockets: Array<WebSocket>, uuids: Array<string>, names: Array<string>) { |
|||
this.id = randomUUID(); |
|||
this.timer = null; |
|||
this.ball = new Ball(new Point(gameInfoConstants.mapSize.x / 2, gameInfoConstants.mapSize.y / 2)); |
|||
for (let i = 0; i < uuids.length; i++) { |
|||
this.addPlayer(sockets[i], uuids[i], names[i]); |
|||
} |
|||
} |
|||
|
|||
getGameInfo(uuid: string): GameInfo { |
|||
const yourPaddleIndex = this.players.findIndex((p) => p.uuid == uuid); |
|||
return { |
|||
...gameInfoConstants, |
|||
yourPaddleIndex: yourPaddleIndex, |
|||
gameId: this.id |
|||
}; |
|||
} |
|||
|
|||
private addPlayer(socket: WebSocket, uuid: string, name: string) { |
|||
let paddleCoords = new Point(gameInfoConstants.playerXOffset, gameInfoConstants.mapSize.y / 2); |
|||
if (this.players.length == 1) { |
|||
paddleCoords = new Point( |
|||
gameInfoConstants.mapSize.x - gameInfoConstants.playerXOffset, |
|||
gameInfoConstants.mapSize.y / 2 |
|||
); |
|||
} |
|||
this.players.push(new Player(socket, uuid, name, paddleCoords, gameInfoConstants.mapSize)); |
|||
} |
|||
|
|||
removePlayer(uuid: string) { |
|||
const player_index = this.players.findIndex((p) => p.uuid == uuid); |
|||
if (player_index != -1) { |
|||
this.players.splice(player_index, 1); |
|||
if (this.players.length < 2) { |
|||
this.stop(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
ready(uuid: string) { |
|||
const player_index = this.players.findIndex((p) => p.uuid == uuid); |
|||
if (player_index != -1) { |
|||
this.players[player_index].ready = true; |
|||
console.log(`${this.players[player_index].name} is ready!`); |
|||
if (this.players.every((p) => p.ready)) { |
|||
this.start(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private start(): boolean { |
|||
if (!this.timer && this.players.length == 2) { |
|||
this.ball = new Ball(new Point(gameInfoConstants.mapSize.x / 2, gameInfoConstants.mapSize.y / 2)); |
|||
this.players.forEach((p) => p.newGame()); |
|||
|
|||
this.timer = setInterval(gameLoop, 1000 / GAME_TICKS, this); |
|||
this.broadcastGame(formatWebsocketData(GAME_EVENTS.START_GAME)); |
|||
console.log('Started game'); |
|||
this.playing = true; |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
stop() { |
|||
if (this.timer) { |
|||
clearInterval(this.timer); |
|||
this.timer = null; |
|||
this.players = []; |
|||
this.playing = false; |
|||
console.log('Stopped game'); |
|||
} |
|||
} |
|||
|
|||
movePaddle(uuid: string, position: Point) { |
|||
const playerIndex = this.players.findIndex((p) => p.uuid == uuid); |
|||
|
|||
if (this.timer && playerIndex != -1) { |
|||
this.players[playerIndex].paddle.move(position.y); |
|||
} |
|||
} |
|||
|
|||
broadcastGame(data: string) { |
|||
this.players.forEach((p) => p.socket.send(data)); |
|||
} |
|||
|
|||
isPlaying(): boolean { |
|||
return this.playing; |
|||
} |
|||
id: string |
|||
timer: NodeJS.Timer |
|||
ball: Ball |
|||
players: Player[] = [] |
|||
playing: boolean |
|||
|
|||
constructor (sockets: WebSocket[], uuids: string[], names: string[]) { |
|||
this.id = randomUUID() |
|||
this.timer = null |
|||
this.ball = new Ball( |
|||
new Point( |
|||
gameInfoConstants.mapSize.x / 2, |
|||
gameInfoConstants.mapSize.y / 2 |
|||
) |
|||
) |
|||
for (let i = 0; i < uuids.length; i++) { |
|||
this.addPlayer(sockets[i], uuids[i], names[i]) |
|||
} |
|||
} |
|||
|
|||
getGameInfo (uuid: string): GameInfo { |
|||
const yourPaddleIndex = this.players.findIndex((p) => p.uuid == uuid) |
|||
return { |
|||
...gameInfoConstants, |
|||
yourPaddleIndex, |
|||
gameId: this.id |
|||
} |
|||
} |
|||
|
|||
private addPlayer (socket: WebSocket, uuid: string, name: string) { |
|||
let paddleCoords = new Point( |
|||
gameInfoConstants.playerXOffset, |
|||
gameInfoConstants.mapSize.y / 2 |
|||
) |
|||
if (this.players.length == 1) { |
|||
paddleCoords = new Point( |
|||
gameInfoConstants.mapSize.x - gameInfoConstants.playerXOffset, |
|||
gameInfoConstants.mapSize.y / 2 |
|||
) |
|||
} |
|||
this.players.push( |
|||
new Player(socket, uuid, name, paddleCoords, gameInfoConstants.mapSize) |
|||
) |
|||
} |
|||
|
|||
removePlayer (uuid: string) { |
|||
const player_index = this.players.findIndex((p) => p.uuid == uuid) |
|||
if (player_index != -1) { |
|||
this.players.splice(player_index, 1) |
|||
if (this.players.length < 2) { |
|||
this.stop() |
|||
} |
|||
} |
|||
} |
|||
|
|||
ready (uuid: string) { |
|||
const player_index = this.players.findIndex((p) => p.uuid == uuid) |
|||
if (player_index != -1) { |
|||
this.players[player_index].ready = true |
|||
console.log(`${this.players[player_index].name} is ready!`) |
|||
if (this.players.every((p) => p.ready)) { |
|||
this.start() |
|||
} |
|||
} |
|||
} |
|||
|
|||
private start (): boolean { |
|||
if (!this.timer && this.players.length == 2) { |
|||
this.ball = new Ball( |
|||
new Point( |
|||
gameInfoConstants.mapSize.x / 2, |
|||
gameInfoConstants.mapSize.y / 2 |
|||
) |
|||
) |
|||
this.players.forEach((p) => { |
|||
p.newGame() |
|||
}) |
|||
|
|||
this.timer = setInterval(gameLoop, 1000 / GAME_TICKS, this) |
|||
this.broadcastGame(formatWebsocketData(GAME_EVENTS.START_GAME)) |
|||
console.log('Started game') |
|||
this.playing = true |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
|
|||
stop () { |
|||
if (this.timer) { |
|||
clearInterval(this.timer) |
|||
this.timer = null |
|||
this.players = [] |
|||
this.playing = false |
|||
console.log('Stopped game') |
|||
} |
|||
} |
|||
|
|||
movePaddle (uuid: string, position: Point) { |
|||
const playerIndex = this.players.findIndex((p) => p.uuid == uuid) |
|||
|
|||
if (this.timer && playerIndex != -1) { |
|||
this.players[playerIndex].paddle.move(position.y) |
|||
} |
|||
} |
|||
|
|||
broadcastGame (data: string) { |
|||
this.players.forEach((p) => { |
|||
p.socket.send(data) |
|||
}) |
|||
} |
|||
|
|||
isPlaying (): boolean { |
|||
return this.playing |
|||
} |
|||
} |
|||
|
@ -1,28 +1,32 @@ |
|||
import { gameInfoConstants } from './constants'; |
|||
import { Point, Rect } from './utils'; |
|||
import { gameInfoConstants } from './constants' |
|||
import { type Point, Rect } from './utils' |
|||
|
|||
export class Paddle { |
|||
rect: Rect; |
|||
color: string | CanvasGradient | CanvasPattern = 'white'; |
|||
mapSize: Point; |
|||
rect: Rect |
|||
color: string | CanvasGradient | CanvasPattern = 'white' |
|||
mapSize: Point |
|||
|
|||
constructor(spawn: Point, gameSize: Point, size: Point = gameInfoConstants.paddleSize) { |
|||
this.rect = new Rect(spawn, size); |
|||
this.mapSize = gameSize; |
|||
} |
|||
constructor ( |
|||
spawn: Point, |
|||
gameSize: Point, |
|||
size: Point = gameInfoConstants.paddleSize |
|||
) { |
|||
this.rect = new Rect(spawn, size) |
|||
this.mapSize = gameSize |
|||
} |
|||
|
|||
draw(context: CanvasRenderingContext2D) { |
|||
this.rect.draw(context, this.color); |
|||
} |
|||
draw (context: CanvasRenderingContext2D) { |
|||
this.rect.draw(context, this.color) |
|||
} |
|||
|
|||
move(new_y: number) { |
|||
const offset: number = this.rect.size.y / 2; |
|||
if (new_y - offset < 0) { |
|||
this.rect.center.y = offset; |
|||
} else if (new_y + offset > this.mapSize.y) { |
|||
this.rect.center.y = this.mapSize.y - offset; |
|||
} else { |
|||
this.rect.center.y = new_y; |
|||
} |
|||
} |
|||
move (new_y: number) { |
|||
const offset: number = this.rect.size.y / 2 |
|||
if (new_y - offset < 0) { |
|||
this.rect.center.y = offset |
|||
} else if (new_y + offset > this.mapSize.y) { |
|||
this.rect.center.y = this.mapSize.y - offset |
|||
} else { |
|||
this.rect.center.y = new_y |
|||
} |
|||
} |
|||
} |
|||
|
@ -1,29 +1,35 @@ |
|||
import { WebSocket } from 'ws'; |
|||
import { Paddle } from './Paddle'; |
|||
import { Point } from './utils'; |
|||
import { type WebSocket } from 'ws' |
|||
import { Paddle } from './Paddle' |
|||
import { type Point } from './utils' |
|||
|
|||
export class Player { |
|||
socket: WebSocket; |
|||
uuid: string; |
|||
name: string; |
|||
ready: boolean; |
|||
paddle: Paddle; |
|||
paddleCoords: Point; |
|||
mapSize: Point; |
|||
score: number; |
|||
socket: WebSocket |
|||
uuid: string |
|||
name: string |
|||
ready: boolean |
|||
paddle: Paddle |
|||
paddleCoords: Point |
|||
mapSize: Point |
|||
score: number |
|||
|
|||
constructor(socket: WebSocket, uuid: string, name: string, paddleCoords: Point, mapSize: Point) { |
|||
this.socket = socket; |
|||
this.uuid = uuid; |
|||
this.name = name; |
|||
this.paddle = new Paddle(paddleCoords, mapSize); |
|||
this.paddleCoords = paddleCoords; |
|||
this.mapSize = mapSize; |
|||
this.score = 0; |
|||
} |
|||
constructor ( |
|||
socket: WebSocket, |
|||
uuid: string, |
|||
name: string, |
|||
paddleCoords: Point, |
|||
mapSize: Point |
|||
) { |
|||
this.socket = socket |
|||
this.uuid = uuid |
|||
this.name = name |
|||
this.paddle = new Paddle(paddleCoords, mapSize) |
|||
this.paddleCoords = paddleCoords |
|||
this.mapSize = mapSize |
|||
this.score = 0 |
|||
} |
|||
|
|||
newGame() { |
|||
this.score = 0; |
|||
this.paddle = new Paddle(this.paddleCoords, this.mapSize); |
|||
} |
|||
newGame () { |
|||
this.score = 0 |
|||
this.paddle = new Paddle(this.paddleCoords, this.mapSize) |
|||
} |
|||
} |
|||
|
@ -1,36 +1,36 @@ |
|||
import { Point } from './utils'; |
|||
import { Point } from './utils' |
|||
|
|||
export const GAME_EVENTS = { |
|||
START_GAME: 'START_GAME', |
|||
READY: 'READY', |
|||
GAME_TICK: 'GAME_TICK', |
|||
PLAYER_MOVE: 'PLAYER_MOVE', |
|||
GET_GAME_INFO: 'GET_GAME_INFO', |
|||
CREATE_GAME: 'CREATE_GAME', |
|||
REGISTER_PLAYER: 'REGISTER_PLAYER' |
|||
}; |
|||
START_GAME: 'START_GAME', |
|||
READY: 'READY', |
|||
GAME_TICK: 'GAME_TICK', |
|||
PLAYER_MOVE: 'PLAYER_MOVE', |
|||
GET_GAME_INFO: 'GET_GAME_INFO', |
|||
CREATE_GAME: 'CREATE_GAME', |
|||
REGISTER_PLAYER: 'REGISTER_PLAYER' |
|||
} |
|||
|
|||
export interface GameInfo extends GameInfoConstants { |
|||
yourPaddleIndex: number; |
|||
gameId: string; |
|||
yourPaddleIndex: number |
|||
gameId: string |
|||
} |
|||
export interface GameInfoConstants { |
|||
mapSize: Point; |
|||
paddleSize: Point; |
|||
playerXOffset: number; |
|||
ballSize: Point; |
|||
winScore: number; |
|||
mapSize: Point |
|||
paddleSize: Point |
|||
playerXOffset: number |
|||
ballSize: Point |
|||
winScore: number |
|||
} |
|||
export const gameInfoConstants: GameInfoConstants = { |
|||
mapSize: new Point(600, 400), |
|||
paddleSize: new Point(6, 50), |
|||
playerXOffset: 50, |
|||
ballSize: new Point(20, 20), |
|||
winScore: 2 |
|||
}; |
|||
mapSize: new Point(600, 400), |
|||
paddleSize: new Point(6, 50), |
|||
playerXOffset: 50, |
|||
ballSize: new Point(20, 20), |
|||
winScore: 2 |
|||
} |
|||
|
|||
export interface GameUpdate { |
|||
paddlesPositions: Point[]; |
|||
ballPosition: Point; |
|||
scores: number[]; |
|||
paddlesPositions: Point[] |
|||
ballPosition: Point |
|||
scores: number[] |
|||
} |
|||
|
@ -1,88 +1,99 @@ |
|||
export class Point { |
|||
x: number; |
|||
y: number; |
|||
x: number |
|||
y: number |
|||
|
|||
constructor(x: number, y: number) { |
|||
this.x = x; |
|||
this.y = y; |
|||
} |
|||
constructor (x: number, y: number) { |
|||
this.x = x |
|||
this.y = y |
|||
} |
|||
|
|||
//Returns a new point
|
|||
add(other: Point) { |
|||
return new Point(this.x + other.x, this.y + other.y); |
|||
} |
|||
// Returns a new point
|
|||
add (other: Point) { |
|||
return new Point(this.x + other.x, this.y + other.y) |
|||
} |
|||
|
|||
//Modifies `this` point
|
|||
add_inplace(other: Point) { |
|||
this.x += other.x; |
|||
this.y += other.y; |
|||
} |
|||
// Modifies `this` point
|
|||
add_inplace (other: Point) { |
|||
this.x += other.x |
|||
this.y += other.y |
|||
} |
|||
|
|||
clone(): Point { |
|||
return new Point(this.x, this.y); |
|||
} |
|||
clone (): Point { |
|||
return new Point(this.x, this.y) |
|||
} |
|||
} |
|||
|
|||
export class Rect { |
|||
center: Point; |
|||
size: Point; |
|||
center: Point |
|||
size: Point |
|||
|
|||
constructor(center: Point, size: Point) { |
|||
this.center = center; |
|||
this.size = size; |
|||
} |
|||
constructor (center: Point, size: Point) { |
|||
this.center = center |
|||
this.size = size |
|||
} |
|||
|
|||
draw(context: CanvasRenderingContext2D, color: string | CanvasGradient | CanvasPattern) { |
|||
const offset: Point = new Point(this.size.x / 2, this.size.y / 2); |
|||
draw ( |
|||
context: CanvasRenderingContext2D, |
|||
color: string | CanvasGradient | CanvasPattern |
|||
) { |
|||
const offset: Point = new Point(this.size.x / 2, this.size.y / 2) |
|||
|
|||
context.fillStyle = color; |
|||
context.fillRect(this.center.x - offset.x, this.center.y - offset.y, this.size.x, this.size.y); |
|||
} |
|||
context.fillStyle = color |
|||
context.fillRect( |
|||
this.center.x - offset.x, |
|||
this.center.y - offset.y, |
|||
this.size.x, |
|||
this.size.y |
|||
) |
|||
} |
|||
|
|||
//True if `this` rect contains `other` rect in the x-axis
|
|||
contains_x(other: Rect): boolean { |
|||
const offset: number = this.size.x / 2; |
|||
const offset_other: number = other.size.x / 2; |
|||
// True if `this` rect contains `other` rect in the x-axis
|
|||
contains_x (other: Rect): boolean { |
|||
const offset: number = this.size.x / 2 |
|||
const offset_other: number = other.size.x / 2 |
|||
|
|||
if ( |
|||
this.center.x - offset <= other.center.x - offset_other && |
|||
this.center.x + offset >= other.center.x + offset_other |
|||
) |
|||
return true; |
|||
return false; |
|||
} |
|||
if ( |
|||
this.center.x - offset <= other.center.x - offset_other && |
|||
this.center.x + offset >= other.center.x + offset_other |
|||
) { |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
|
|||
//True if `this` rect contains `other` rect in the y-axis
|
|||
contains_y(other: Rect): boolean { |
|||
const offset: number = this.size.y / 2; |
|||
const offset_other: number = other.size.y / 2; |
|||
// True if `this` rect contains `other` rect in the y-axis
|
|||
contains_y (other: Rect): boolean { |
|||
const offset: number = this.size.y / 2 |
|||
const offset_other: number = other.size.y / 2 |
|||
|
|||
if ( |
|||
this.center.y - offset <= other.center.y - offset_other && |
|||
this.center.y + offset >= other.center.y + offset_other |
|||
) |
|||
return true; |
|||
return false; |
|||
} |
|||
if ( |
|||
this.center.y - offset <= other.center.y - offset_other && |
|||
this.center.y + offset >= other.center.y + offset_other |
|||
) { |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
|
|||
collides(other: Rect): boolean { |
|||
const offset: Point = new Point(this.size.x / 2, this.size.y / 2); |
|||
const offset_other: Point = new Point(other.size.x / 2, other.size.y / 2); |
|||
collides (other: Rect): boolean { |
|||
const offset: Point = new Point(this.size.x / 2, this.size.y / 2) |
|||
const offset_other: Point = new Point(other.size.x / 2, other.size.y / 2) |
|||
|
|||
if ( |
|||
this.center.x - offset.x < other.center.x + offset_other.x && |
|||
this.center.x + offset.x > other.center.x - offset_other.x && |
|||
this.center.y - offset.y < other.center.y + offset_other.y && |
|||
this.center.y + offset.y > other.center.y - offset_other.y |
|||
) |
|||
return true; |
|||
return false; |
|||
} |
|||
if ( |
|||
this.center.x - offset.x < other.center.x + offset_other.x && |
|||
this.center.x + offset.x > other.center.x - offset_other.x && |
|||
this.center.y - offset.y < other.center.y + offset_other.y && |
|||
this.center.y + offset.y > other.center.y - offset_other.y |
|||
) { |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
} |
|||
|
|||
export function formatWebsocketData(event: string, data?: any): string { |
|||
return JSON.stringify({ |
|||
event, |
|||
data |
|||
}); |
|||
export function formatWebsocketData (event: string, data?: any): string { |
|||
return JSON.stringify({ |
|||
event, |
|||
data |
|||
}) |
|||
} |
|||
|
@ -1,18 +1,18 @@ |
|||
import { Test, TestingModule } from '@nestjs/testing'; |
|||
import { PongGateway } from './pong.gateway'; |
|||
import { Test, type TestingModule } from '@nestjs/testing' |
|||
import { PongGateway } from './pong.gateway' |
|||
|
|||
describe('PongGateway', () => { |
|||
let gateway: PongGateway; |
|||
let gateway: PongGateway |
|||
|
|||
beforeEach(async () => { |
|||
const module: TestingModule = await Test.createTestingModule({ |
|||
providers: [PongGateway] |
|||
}).compile(); |
|||
beforeEach(async () => { |
|||
const module: TestingModule = await Test.createTestingModule({ |
|||
providers: [PongGateway] |
|||
}).compile() |
|||
|
|||
gateway = module.get<PongGateway>(PongGateway); |
|||
}); |
|||
gateway = module.get<PongGateway>(PongGateway) |
|||
}) |
|||
|
|||
it('should be defined', () => { |
|||
expect(gateway).toBeDefined(); |
|||
}); |
|||
}); |
|||
it('should be defined', () => { |
|||
expect(gateway).toBeDefined() |
|||
}) |
|||
}) |
|||
|
@ -1,99 +1,115 @@ |
|||
import { WebSocket } from 'ws'; |
|||
import { type WebSocket } from 'ws' |
|||
import { |
|||
ConnectedSocket, |
|||
MessageBody, |
|||
OnGatewayConnection, |
|||
OnGatewayDisconnect, |
|||
SubscribeMessage, |
|||
WebSocketGateway |
|||
} from '@nestjs/websockets'; |
|||
import { randomUUID } from 'crypto'; |
|||
import { Pong } from './pong'; |
|||
import { formatWebsocketData, Point } from './game/utils'; |
|||
import { GAME_EVENTS } from './game/constants'; |
|||
ConnectedSocket, |
|||
MessageBody, |
|||
type OnGatewayConnection, |
|||
type OnGatewayDisconnect, |
|||
SubscribeMessage, |
|||
WebSocketGateway |
|||
} from '@nestjs/websockets' |
|||
import { randomUUID } from 'crypto' |
|||
import { Pong } from './pong' |
|||
import { formatWebsocketData, Point } from './game/utils' |
|||
import { GAME_EVENTS } from './game/constants' |
|||
|
|||
interface WebSocketWithId extends WebSocket { |
|||
id: string; |
|||
id: string |
|||
} |
|||
|
|||
@WebSocketGateway() |
|||
export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect { |
|||
private pong: Pong = new Pong(); |
|||
private socketToPlayerName: Map<WebSocketWithId, string> = new Map(); |
|||
private readonly pong: Pong = new Pong() |
|||
private readonly socketToPlayerName = new Map<WebSocketWithId, string>() |
|||
|
|||
handleConnection(client: WebSocketWithId) { |
|||
const uuid = randomUUID(); |
|||
client.id = uuid; |
|||
} |
|||
handleConnection (client: WebSocketWithId) { |
|||
const uuid = randomUUID() |
|||
client.id = uuid |
|||
} |
|||
|
|||
handleDisconnect( |
|||
@ConnectedSocket() |
|||
client: WebSocketWithId |
|||
) { |
|||
if (this.pong.isInAGame(client.id)) { |
|||
console.log(`Disconnected ${this.socketToPlayerName.get(client)}`); |
|||
if (this.pong.playerGame(client.id).isPlaying()) { |
|||
this.pong.playerGame(client.id).stop(); |
|||
} |
|||
this.socketToPlayerName.delete(client); |
|||
} |
|||
} |
|||
handleDisconnect ( |
|||
@ConnectedSocket() |
|||
client: WebSocketWithId |
|||
) { |
|||
if (this.pong.isInAGame(client.id)) { |
|||
console.log(`Disconnected ${this.socketToPlayerName.get(client)}`) |
|||
if (this.pong.playerGame(client.id).isPlaying()) { |
|||
this.pong.playerGame(client.id).stop() |
|||
} |
|||
this.socketToPlayerName.delete(client) |
|||
} |
|||
} |
|||
|
|||
@SubscribeMessage(GAME_EVENTS.REGISTER_PLAYER) |
|||
registerPlayer( |
|||
@ConnectedSocket() |
|||
client: WebSocketWithId, |
|||
@MessageBody('playerName') playerName: string |
|||
) { |
|||
this.socketToPlayerName.set(client, playerName); |
|||
console.log(`Connected ${this.socketToPlayerName.get(client)}`); |
|||
} |
|||
@SubscribeMessage(GAME_EVENTS.REGISTER_PLAYER) |
|||
registerPlayer ( |
|||
@ConnectedSocket() |
|||
client: WebSocketWithId, |
|||
@MessageBody('playerName') playerName: string |
|||
) { |
|||
this.socketToPlayerName.set(client, playerName) |
|||
console.log(`Connected ${this.socketToPlayerName.get(client)}`) |
|||
} |
|||
|
|||
@SubscribeMessage(GAME_EVENTS.GET_GAME_INFO) |
|||
getPlayerCount(@ConnectedSocket() client: WebSocketWithId) { |
|||
client.send(formatWebsocketData(GAME_EVENTS.GET_GAME_INFO, this.pong.getGameInfo(client.id))); |
|||
} |
|||
@SubscribeMessage(GAME_EVENTS.GET_GAME_INFO) |
|||
getPlayerCount (@ConnectedSocket() client: WebSocketWithId) { |
|||
client.send( |
|||
formatWebsocketData( |
|||
GAME_EVENTS.GET_GAME_INFO, |
|||
this.pong.getGameInfo(client.id) |
|||
) |
|||
) |
|||
} |
|||
|
|||
@SubscribeMessage(GAME_EVENTS.PLAYER_MOVE) |
|||
movePlayer( |
|||
@ConnectedSocket() |
|||
client: WebSocketWithId, |
|||
@MessageBody('position') position: Point |
|||
) { |
|||
this.pong.movePlayer(client.id, position); |
|||
} |
|||
@SubscribeMessage(GAME_EVENTS.PLAYER_MOVE) |
|||
movePlayer ( |
|||
@ConnectedSocket() |
|||
client: WebSocketWithId, |
|||
@MessageBody('position') position: Point |
|||
) { |
|||
this.pong.movePlayer(client.id, position) |
|||
} |
|||
|
|||
@SubscribeMessage(GAME_EVENTS.CREATE_GAME) |
|||
createGame( |
|||
@ConnectedSocket() |
|||
client: WebSocketWithId, |
|||
@MessageBody('playerNames') playerNames: string[] |
|||
) { |
|||
const allPlayerNames: Array<string> = Array.from(this.socketToPlayerName.values()); |
|||
if (playerNames && playerNames.length === 2 && 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] |
|||
); |
|||
@SubscribeMessage(GAME_EVENTS.CREATE_GAME) |
|||
createGame ( |
|||
@ConnectedSocket() |
|||
client: WebSocketWithId, |
|||
@MessageBody('playerNames') playerNames: string[] |
|||
) { |
|||
const allPlayerNames: string[] = Array.from( |
|||
this.socketToPlayerName.values() |
|||
) |
|||
if ( |
|||
playerNames && |
|||
playerNames.length === 2 && |
|||
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 ( |
|||
player1Socket && |
|||
player2Socket && |
|||
(client.id === player1Socket.id || client.id === player2Socket.id) && |
|||
player1Socket.id !== player2Socket.id |
|||
) { |
|||
this.pong.newGame([player1Socket, player2Socket], [player1Socket.id, player2Socket.id], playerNames); |
|||
} |
|||
} |
|||
} |
|||
if ( |
|||
player1Socket && |
|||
player2Socket && |
|||
(client.id === player1Socket.id || client.id === player2Socket.id) && |
|||
player1Socket.id !== player2Socket.id |
|||
) { |
|||
this.pong.newGame( |
|||
[player1Socket, player2Socket], |
|||
[player1Socket.id, player2Socket.id], |
|||
playerNames |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@SubscribeMessage(GAME_EVENTS.READY) |
|||
ready( |
|||
@ConnectedSocket() |
|||
client: WebSocketWithId |
|||
) { |
|||
this.pong.ready(client.id); |
|||
} |
|||
@SubscribeMessage(GAME_EVENTS.READY) |
|||
ready ( |
|||
@ConnectedSocket() |
|||
client: WebSocketWithId |
|||
) { |
|||
this.pong.ready(client.id) |
|||
} |
|||
} |
|||
|
@ -1,7 +1,7 @@ |
|||
import { Module } from '@nestjs/common'; |
|||
import { PongGateway } from './pong.gateway'; |
|||
import { Module } from '@nestjs/common' |
|||
import { PongGateway } from './pong.gateway' |
|||
|
|||
@Module({ |
|||
providers: [PongGateway] |
|||
providers: [PongGateway] |
|||
}) |
|||
export class PongModule {} |
|||
|
@ -1,18 +1,18 @@ |
|||
import { Test, TestingModule } from '@nestjs/testing'; |
|||
import { Pong } from './pong'; |
|||
import { Test, type TestingModule } from '@nestjs/testing' |
|||
import { Pong } from './pong' |
|||
|
|||
describe('Pong', () => { |
|||
let provider: Pong; |
|||
let provider: Pong |
|||
|
|||
beforeEach(async () => { |
|||
const module: TestingModule = await Test.createTestingModule({ |
|||
providers: [Pong] |
|||
}).compile(); |
|||
beforeEach(async () => { |
|||
const module: TestingModule = await Test.createTestingModule({ |
|||
providers: [Pong] |
|||
}).compile() |
|||
|
|||
provider = module.get<Pong>(Pong); |
|||
}); |
|||
provider = module.get<Pong>(Pong) |
|||
}) |
|||
|
|||
it('should be defined', () => { |
|||
expect(provider).toBeDefined(); |
|||
}); |
|||
}); |
|||
it('should be defined', () => { |
|||
expect(provider).toBeDefined() |
|||
}) |
|||
}) |
|||
|
@ -1,59 +1,59 @@ |
|||
import { WebSocket } from 'ws'; |
|||
import { GameInfo } from './game/constants'; |
|||
import { Game } from './game/Game'; |
|||
import { Point } from './game/utils'; |
|||
import { type WebSocket } from 'ws' |
|||
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>(); |
|||
private games = new Array<Game>(); |
|||
|
|||
newGame(sockets: Array<WebSocket>, uuids: Array<string>, names: Array<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; |
|||
console.log(`Created game ${names[0]} vs ${names[1]}`); |
|||
} |
|||
|
|||
removePlayer(uuid: string) { |
|||
this.playerGame(uuid).removePlayer(uuid); |
|||
} |
|||
|
|||
ready(uuid: string) { |
|||
if (this.isInAGame(uuid)) { |
|||
this.playerGame(uuid).ready(uuid); |
|||
} |
|||
} |
|||
|
|||
stopGame(uuid: string) { |
|||
if (this.isInAGame(uuid)) { |
|||
this.playerGame(uuid).stop(); |
|||
delete this.playerUUIDToGameIndex[uuid]; |
|||
delete this.games[this.playerUUIDToGameIndex[uuid]]; |
|||
} |
|||
} |
|||
|
|||
getGameInfo(uuid: string): GameInfo { |
|||
if (this.isInAGame(uuid)) { |
|||
return this.playerGame(uuid).getGameInfo(uuid); |
|||
} |
|||
} |
|||
|
|||
movePlayer(uuid: string, position: Point) { |
|||
if (this.isInAGame(uuid)) { |
|||
this.playerGame(uuid).movePaddle(uuid, position); |
|||
} |
|||
} |
|||
|
|||
isInAGame(uuid: string): boolean { |
|||
if (this.playerUUIDToGameIndex[uuid] === undefined) { |
|||
return false; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
playerGame(uuid: string): Game { |
|||
if (this.isInAGame(uuid)) { |
|||
return this.games[this.playerUUIDToGameIndex[uuid]]; |
|||
} |
|||
} |
|||
private playerUUIDToGameIndex = 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 |
|||
console.log(`Created game ${names[0]} vs ${names[1]}`) |
|||
} |
|||
|
|||
removePlayer (uuid: string) { |
|||
this.playerGame(uuid).removePlayer(uuid) |
|||
} |
|||
|
|||
ready (uuid: string) { |
|||
if (this.isInAGame(uuid)) { |
|||
this.playerGame(uuid).ready(uuid) |
|||
} |
|||
} |
|||
|
|||
stopGame (uuid: string) { |
|||
if (this.isInAGame(uuid)) { |
|||
this.playerGame(uuid).stop() |
|||
delete this.playerUUIDToGameIndex[uuid] |
|||
delete this.games[this.playerUUIDToGameIndex[uuid]] |
|||
} |
|||
} |
|||
|
|||
getGameInfo (uuid: string): GameInfo { |
|||
if (this.isInAGame(uuid)) { |
|||
return this.playerGame(uuid).getGameInfo(uuid) |
|||
} |
|||
} |
|||
|
|||
movePlayer (uuid: string, position: Point) { |
|||
if (this.isInAGame(uuid)) { |
|||
this.playerGame(uuid).movePaddle(uuid, position) |
|||
} |
|||
} |
|||
|
|||
isInAGame (uuid: string): boolean { |
|||
if (this.playerUUIDToGameIndex[uuid] === undefined) { |
|||
return false |
|||
} |
|||
return true |
|||
} |
|||
|
|||
playerGame (uuid: string): Game { |
|||
if (this.isInAGame(uuid)) { |
|||
return this.games[this.playerUUIDToGameIndex[uuid]] |
|||
} |
|||
} |
|||
} |
|||
|
@ -1,21 +1,24 @@ |
|||
import { Test, TestingModule } from '@nestjs/testing' |
|||
import { INestApplication } from '@nestjs/common' |
|||
import { Test, type TestingModule } from '@nestjs/testing' |
|||
import { type INestApplication } from '@nestjs/common' |
|||
import * as request from 'supertest' |
|||
import { AppModule } from './../src/app.module' |
|||
|
|||
describe('AppController (e2e)', () => { |
|||
let app: INestApplication |
|||
let app: INestApplication |
|||
|
|||
beforeEach(async () => { |
|||
const moduleFixture: TestingModule = await Test.createTestingModule({ |
|||
imports: [AppModule] |
|||
}).compile() |
|||
beforeEach(async () => { |
|||
const moduleFixture: TestingModule = await Test.createTestingModule({ |
|||
imports: [AppModule] |
|||
}).compile() |
|||
|
|||
app = moduleFixture.createNestApplication() |
|||
await app.init() |
|||
}) |
|||
app = moduleFixture.createNestApplication() |
|||
await app.init() |
|||
}) |
|||
|
|||
it('/ (GET)', () => { |
|||
return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!') |
|||
}) |
|||
it('/ (GET)', async () => { |
|||
return await request(app.getHttpServer()) |
|||
.get('/') |
|||
.expect(200) |
|||
.expect('Hello World!') |
|||
}) |
|||
}) |
|||
|
Loading…
Reference in new issue