narnaud
2 years ago
28 changed files with 15889 additions and 8 deletions
@ -1,6 +1,8 @@ |
|||||
FROM alpine:3.15 |
FROM alpine:3.15 |
||||
|
|
||||
RUN apk update && apk upgrade && apk add --no-cache npm \ |
RUN apk update && apk upgrade && apk add npm \ |
||||
&& npm install -g @nestjs/cli |
&& npm install -g @nestjs/cli |
||||
|
|
||||
WORKDIR /var/html/ |
WORKDIR /var/www/html |
||||
|
ENTRYPOINT npm install && npm run build && npm run start |
||||
|
|
||||
|
@ -1,8 +1,8 @@ |
|||||
FROM alpine:3.15 |
FROM alpine:3.15 |
||||
|
|
||||
RUN apk update && apk upgrade && apk add npm nginx |
RUN apk update && apk upgrade && apk add npm |
||||
|
|
||||
RUN mkdir -p /var/ssl |
RUN mkdir -p /var/ssl |
||||
|
|
||||
WORKDIR /var/www/html |
WORKDIR /var/www/html |
||||
ENTRYPOINT npm install && npm build && npm preview |
ENTRYPOINT npm install && npm run build && npm run dev |
||||
|
@ -0,0 +1,131 @@ |
|||||
|
|
||||
|
# Logs |
||||
|
logs |
||||
|
*.log |
||||
|
npm-debug.log* |
||||
|
yarn-debug.log* |
||||
|
yarn-error.log* |
||||
|
lerna-debug.log* |
||||
|
.pnpm-debug.log* |
||||
|
|
||||
|
# Diagnostic reports (https://nodejs.org/api/report.html) |
||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json |
||||
|
|
||||
|
# Runtime data |
||||
|
pids |
||||
|
*.pid |
||||
|
*.seed |
||||
|
*.pid.lock |
||||
|
|
||||
|
# Directory for instrumented libs generated by jscoverage/JSCover |
||||
|
lib-cov |
||||
|
|
||||
|
# Coverage directory used by tools like istanbul |
||||
|
coverage |
||||
|
*.lcov |
||||
|
|
||||
|
# nyc test coverage |
||||
|
.nyc_output |
||||
|
|
||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) |
||||
|
.grunt |
||||
|
|
||||
|
# Bower dependency directory (https://bower.io/) |
||||
|
bower_components |
||||
|
|
||||
|
# node-waf configuration |
||||
|
.lock-wscript |
||||
|
|
||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html) |
||||
|
build/Release |
||||
|
|
||||
|
# Dependency directories |
||||
|
node_modules/ |
||||
|
jspm_packages/ |
||||
|
|
||||
|
# Snowpack dependency directory (https://snowpack.dev/) |
||||
|
web_modules/ |
||||
|
|
||||
|
# TypeScript cache |
||||
|
*.tsbuildinfo |
||||
|
|
||||
|
# Optional npm cache directory |
||||
|
.npm |
||||
|
|
||||
|
# Optional eslint cache |
||||
|
.eslintcache |
||||
|
|
||||
|
# Optional stylelint cache |
||||
|
.stylelintcache |
||||
|
|
||||
|
# Microbundle cache |
||||
|
.rpt2_cache/ |
||||
|
.rts2_cache_cjs/ |
||||
|
.rts2_cache_es/ |
||||
|
.rts2_cache_umd/ |
||||
|
|
||||
|
# Optional REPL history |
||||
|
.node_repl_history |
||||
|
|
||||
|
# Output of 'npm pack' |
||||
|
*.tgz |
||||
|
|
||||
|
# Yarn Integrity file |
||||
|
.yarn-integrity |
||||
|
|
||||
|
# dotenv environment variable files |
||||
|
.env |
||||
|
.env.development.local |
||||
|
.env.test.local |
||||
|
.env.production.local |
||||
|
.env.local |
||||
|
|
||||
|
# parcel-bundler cache (https://parceljs.org/) |
||||
|
.cache |
||||
|
.parcel-cache |
||||
|
|
||||
|
# Next.js build output |
||||
|
.next |
||||
|
out |
||||
|
|
||||
|
# Nuxt.js build / generate output |
||||
|
.nuxt |
||||
|
dist |
||||
|
|
||||
|
# Gatsby files |
||||
|
.cache/ |
||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js |
||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support |
||||
|
# public |
||||
|
|
||||
|
# vuepress build output |
||||
|
.vuepress/dist |
||||
|
|
||||
|
# vuepress v2.x temp and cache directory |
||||
|
.temp |
||||
|
.cache |
||||
|
|
||||
|
# Docusaurus cache and generated files |
||||
|
.docusaurus |
||||
|
|
||||
|
# Serverless directories |
||||
|
.serverless/ |
||||
|
|
||||
|
# FuseBox cache |
||||
|
.fusebox/ |
||||
|
|
||||
|
# DynamoDB Local files |
||||
|
.dynamodb/ |
||||
|
|
||||
|
# TernJS port file |
||||
|
.tern-port |
||||
|
|
||||
|
# Stores VSCode versions used for testing VSCode extensions |
||||
|
.vscode-test |
||||
|
|
||||
|
# yarn v2 |
||||
|
.yarn/cache |
||||
|
.yarn/unplugged |
||||
|
.yarn/build-state.yml |
||||
|
.yarn/install-state.gz |
||||
|
.pnp.* |
@ -0,0 +1,73 @@ |
|||||
|
<p align="center"> |
||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a> |
||||
|
</p> |
||||
|
|
||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 |
||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest |
||||
|
|
||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p> |
||||
|
<p align="center"> |
||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a> |
||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a> |
||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a> |
||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a> |
||||
|
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a> |
||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a> |
||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a> |
||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a> |
||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a> |
||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a> |
||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a> |
||||
|
</p> |
||||
|
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer) |
||||
|
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)--> |
||||
|
|
||||
|
## Description |
||||
|
|
||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. |
||||
|
|
||||
|
## Installation |
||||
|
|
||||
|
```bash |
||||
|
$ npm install |
||||
|
``` |
||||
|
|
||||
|
## Running the app |
||||
|
|
||||
|
```bash |
||||
|
# development |
||||
|
$ npm run start |
||||
|
|
||||
|
# watch mode |
||||
|
$ npm run start:dev |
||||
|
|
||||
|
# production mode |
||||
|
$ npm run start:prod |
||||
|
``` |
||||
|
|
||||
|
## Test |
||||
|
|
||||
|
```bash |
||||
|
# unit tests |
||||
|
$ npm run test |
||||
|
|
||||
|
# e2e tests |
||||
|
$ npm run test:e2e |
||||
|
|
||||
|
# test coverage |
||||
|
$ npm run test:cov |
||||
|
``` |
||||
|
|
||||
|
## Support |
||||
|
|
||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). |
||||
|
|
||||
|
## Stay in touch |
||||
|
|
||||
|
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) |
||||
|
- Website - [https://nestjs.com](https://nestjs.com/) |
||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework) |
||||
|
|
||||
|
## License |
||||
|
|
||||
|
Nest is [MIT licensed](LICENSE). |
@ -0,0 +1,5 @@ |
|||||
|
{ |
||||
|
"$schema": "https://json.schemastore.org/nest-cli", |
||||
|
"collection": "@nestjs/schematics", |
||||
|
"sourceRoot": "src" |
||||
|
} |
File diff suppressed because it is too large
@ -0,0 +1,75 @@ |
|||||
|
{ |
||||
|
"name": "pong_server", |
||||
|
"version": "0.0.1", |
||||
|
"description": "", |
||||
|
"author": "", |
||||
|
"private": true, |
||||
|
"license": "UNLICENSED", |
||||
|
"scripts": { |
||||
|
"prebuild": "rimraf dist", |
||||
|
"build": "nest build", |
||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", |
||||
|
"start": "nest start", |
||||
|
"dev": "nest start --watch", |
||||
|
"start:debug": "nest start --debug --watch", |
||||
|
"start:prod": "node dist/main", |
||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", |
||||
|
"test": "jest", |
||||
|
"test:watch": "jest --watch", |
||||
|
"test:cov": "jest --coverage", |
||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", |
||||
|
"test:e2e": "jest --config ./test/jest-e2e.json" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@nestjs/cli": "^9.0.0", |
||||
|
"@nestjs/common": "^9.0.0", |
||||
|
"@nestjs/core": "^9.0.0", |
||||
|
"@nestjs/platform-express": "^9.0.0", |
||||
|
"@nestjs/platform-ws": "^9.2.0", |
||||
|
"@nestjs/schematics": "^9.0.0", |
||||
|
"@nestjs/testing": "^9.0.0", |
||||
|
"@nestjs/websockets": "^9.2.0", |
||||
|
"@types/express": "^4.17.13", |
||||
|
"@types/jest": "28.1.8", |
||||
|
"@types/node": "^16.0.0", |
||||
|
"@types/supertest": "^2.0.11", |
||||
|
"@typescript-eslint/eslint-plugin": "^5.0.0", |
||||
|
"@typescript-eslint/parser": "^5.0.0", |
||||
|
"eslint": "^8.0.1", |
||||
|
"eslint-config-prettier": "^8.5.0", |
||||
|
"eslint-plugin-prettier": "^4.2.1", |
||||
|
"jest": "28.1.3", |
||||
|
"prettier": "^2.3.2", |
||||
|
"reflect-metadata": "^0.1.13", |
||||
|
"rimraf": "^3.0.2", |
||||
|
"rxjs": "^7.2.0", |
||||
|
"source-map-support": "^0.5.20", |
||||
|
"supertest": "^6.1.3", |
||||
|
"ts-jest": "28.0.8", |
||||
|
"ts-loader": "^9.2.3", |
||||
|
"ts-node": "^10.0.0", |
||||
|
"tsconfig-paths": "4.1.0", |
||||
|
"typescript": "^4.7.4", |
||||
|
"ws": "^8.11.0" |
||||
|
}, |
||||
|
"jest": { |
||||
|
"moduleFileExtensions": [ |
||||
|
"js", |
||||
|
"json", |
||||
|
"ts" |
||||
|
], |
||||
|
"rootDir": "src", |
||||
|
"testRegex": ".*\\.spec\\.ts$", |
||||
|
"transform": { |
||||
|
"^.+\\.(t|j)s$": "ts-jest" |
||||
|
}, |
||||
|
"collectCoverageFrom": [ |
||||
|
"**/*.(t|j)s" |
||||
|
], |
||||
|
"coverageDirectory": "../coverage", |
||||
|
"testEnvironment": "node" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@types/ws": "^8.5.3" |
||||
|
} |
||||
|
} |
@ -0,0 +1,7 @@ |
|||||
|
import { Module } from '@nestjs/common'; |
||||
|
import { PongModule } from './pong/pong.module'; |
||||
|
|
||||
|
@Module({ |
||||
|
imports: [PongModule] |
||||
|
}) |
||||
|
export class AppModule {} |
@ -0,0 +1,10 @@ |
|||||
|
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); |
||||
|
} |
||||
|
bootstrap(); |
@ -0,0 +1,58 @@ |
|||||
|
import { gameInfoConstants } from './constants'; |
||||
|
import { 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; |
||||
|
} |
||||
|
} |
@ -0,0 +1,133 @@ |
|||||
|
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); |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
@ -0,0 +1,28 @@ |
|||||
|
import { gameInfoConstants } from './constants'; |
||||
|
import { Point, Rect } from './utils'; |
||||
|
|
||||
|
export class Paddle { |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,29 @@ |
|||||
|
import { WebSocket } from 'ws'; |
||||
|
import { Paddle } from './Paddle'; |
||||
|
import { Point } from './utils'; |
||||
|
|
||||
|
export class Player { |
||||
|
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; |
||||
|
} |
||||
|
|
||||
|
newGame() { |
||||
|
this.score = 0; |
||||
|
this.paddle = new Paddle(this.paddleCoords, this.mapSize); |
||||
|
} |
||||
|
} |
@ -0,0 +1,36 @@ |
|||||
|
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' |
||||
|
}; |
||||
|
|
||||
|
export interface GameInfo extends GameInfoConstants { |
||||
|
yourPaddleIndex: number; |
||||
|
gameId: string; |
||||
|
} |
||||
|
export interface GameInfoConstants { |
||||
|
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 |
||||
|
}; |
||||
|
|
||||
|
export interface GameUpdate { |
||||
|
paddlesPositions: Point[]; |
||||
|
ballPosition: Point; |
||||
|
scores: number[]; |
||||
|
} |
@ -0,0 +1,88 @@ |
|||||
|
export class Point { |
||||
|
x: number; |
||||
|
y: number; |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
|
||||
|
//Modifies `this` point
|
||||
|
add_inplace(other: Point) { |
||||
|
this.x += other.x; |
||||
|
this.y += other.y; |
||||
|
} |
||||
|
|
||||
|
clone(): Point { |
||||
|
return new Point(this.x, this.y); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class Rect { |
||||
|
center: Point; |
||||
|
size: Point; |
||||
|
|
||||
|
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); |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
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); |
||||
|
|
||||
|
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 |
||||
|
}); |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||
|
import { PongGateway } from './pong.gateway'; |
||||
|
|
||||
|
describe('PongGateway', () => { |
||||
|
let gateway: PongGateway; |
||||
|
|
||||
|
beforeEach(async () => { |
||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||
|
providers: [PongGateway] |
||||
|
}).compile(); |
||||
|
|
||||
|
gateway = module.get<PongGateway>(PongGateway); |
||||
|
}); |
||||
|
|
||||
|
it('should be defined', () => { |
||||
|
expect(gateway).toBeDefined(); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,99 @@ |
|||||
|
import { 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'; |
||||
|
|
||||
|
interface WebSocketWithId extends WebSocket { |
||||
|
id: string; |
||||
|
} |
||||
|
|
||||
|
@WebSocketGateway() |
||||
|
export class PongGateway implements OnGatewayConnection, OnGatewayDisconnect { |
||||
|
private pong: Pong = new Pong(); |
||||
|
private socketToPlayerName: Map<WebSocketWithId, string> = new Map(); |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@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.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] |
||||
|
); |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
@ -0,0 +1,7 @@ |
|||||
|
import { Module } from '@nestjs/common'; |
||||
|
import { PongGateway } from './pong.gateway'; |
||||
|
|
||||
|
@Module({ |
||||
|
providers: [PongGateway] |
||||
|
}) |
||||
|
export class PongModule {} |
@ -0,0 +1,18 @@ |
|||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||
|
import { Pong } from './pong'; |
||||
|
|
||||
|
describe('Pong', () => { |
||||
|
let provider: Pong; |
||||
|
|
||||
|
beforeEach(async () => { |
||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||
|
providers: [Pong] |
||||
|
}).compile(); |
||||
|
|
||||
|
provider = module.get<Pong>(Pong); |
||||
|
}); |
||||
|
|
||||
|
it('should be defined', () => { |
||||
|
expect(provider).toBeDefined(); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,59 @@ |
|||||
|
import { WebSocket } from 'ws'; |
||||
|
import { GameInfo } from './game/constants'; |
||||
|
import { Game } from './game/Game'; |
||||
|
import { 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]]; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
import { Test, TestingModule } from '@nestjs/testing' |
||||
|
import { INestApplication } from '@nestjs/common' |
||||
|
import * as request from 'supertest' |
||||
|
import { AppModule } from './../src/app.module' |
||||
|
|
||||
|
describe('AppController (e2e)', () => { |
||||
|
let app: INestApplication |
||||
|
|
||||
|
beforeEach(async () => { |
||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({ |
||||
|
imports: [AppModule] |
||||
|
}).compile() |
||||
|
|
||||
|
app = moduleFixture.createNestApplication() |
||||
|
await app.init() |
||||
|
}) |
||||
|
|
||||
|
it('/ (GET)', () => { |
||||
|
return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!') |
||||
|
}) |
||||
|
}) |
@ -0,0 +1,9 @@ |
|||||
|
{ |
||||
|
"moduleFileExtensions": ["js", "json", "ts"], |
||||
|
"rootDir": ".", |
||||
|
"testEnvironment": "node", |
||||
|
"testRegex": ".e2e-spec.ts$", |
||||
|
"transform": { |
||||
|
"^.+\\.(t|j)s$": "ts-jest" |
||||
|
} |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
{ |
||||
|
"extends": "./tsconfig.json", |
||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"] |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"module": "commonjs", |
||||
|
"declaration": true, |
||||
|
"removeComments": true, |
||||
|
"emitDecoratorMetadata": true, |
||||
|
"experimentalDecorators": true, |
||||
|
"allowSyntheticDefaultImports": true, |
||||
|
"target": "es2017", |
||||
|
"sourceMap": true, |
||||
|
"outDir": "./dist", |
||||
|
"baseUrl": "./", |
||||
|
"incremental": true, |
||||
|
"skipLibCheck": true, |
||||
|
"strictNullChecks": false, |
||||
|
"noImplicitAny": false, |
||||
|
"strictBindCallApply": false, |
||||
|
"forceConsistentCasingInFileNames": false, |
||||
|
"noFallthroughCasesInSwitch": false |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue