From 79df29d141fb4e071edbf9617f270cd23a10d2ef Mon Sep 17 00:00:00 2001 From: Ibinola Date: Sat, 22 Feb 2025 19:25:06 +0100 Subject: [PATCH] feature: implement tournament system --- backend/package-lock.json | 39 +++++++++ backend/package.json | 1 + backend/src/app.module.ts | 4 + backend/src/tournament/scheduling.service.ts | 26 ++++++ backend/src/tournament/tournament.entity.ts | 33 ++++++++ backend/src/tournament/tournament.module.ts | 13 +++ .../src/tournament/tournament.service.spec.ts | 18 +++++ backend/src/tournament/tournament.service.ts | 80 +++++++++++++++++++ 8 files changed, 214 insertions(+) create mode 100644 backend/src/tournament/scheduling.service.ts create mode 100644 backend/src/tournament/tournament.entity.ts create mode 100644 backend/src/tournament/tournament.module.ts create mode 100644 backend/src/tournament/tournament.service.spec.ts create mode 100644 backend/src/tournament/tournament.service.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index b758730a..3ea96798 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,6 +15,7 @@ "@nestjs/event-emitter": "^3.0.0", "@nestjs/jwt": "^11.0.0", "@nestjs/platform-express": "^11.0.5", + "@nestjs/schedule": "^5.0.1", "@nestjs/swagger": "^11.0.3", "@nestjs/typeorm": "^11.0.0", "@types/bcrypt": "^5.0.2", @@ -2077,6 +2078,19 @@ "@nestjs/core": "^11.0.0" } }, + "node_modules/@nestjs/schedule": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-5.0.1.tgz", + "integrity": "sha512-kFoel84I4RyS2LNPH6yHYTKxB16tb3auAEciFuc788C3ph6nABkUfzX5IE+unjVaRX+3GuruJwurNepMlHXpQg==", + "license": "MIT", + "dependencies": { + "cron": "3.5.0" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/schematics": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.0.tgz", @@ -2599,6 +2613,12 @@ "@types/node": "*" } }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -4411,6 +4431,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/cron": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-3.5.0.tgz", + "integrity": "sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.4.0", + "luxon": "~3.5.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -7528,6 +7558,15 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", diff --git a/backend/package.json b/backend/package.json index 61eefb3a..8eae98c7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,6 +26,7 @@ "@nestjs/event-emitter": "^3.0.0", "@nestjs/jwt": "^11.0.0", "@nestjs/platform-express": "^11.0.5", + "@nestjs/schedule": "^5.0.1", "@nestjs/swagger": "^11.0.3", "@nestjs/typeorm": "^11.0.0", "@types/bcrypt": "^5.0.2", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 1176db71..7e7f8077 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -18,6 +18,8 @@ import { GlobalInterceptor } from './interceptors/global.interceptor'; import { SongsModule } from './songs/songs.module'; import { ScoringModule } from './scoring/scoring.module'; import { ChatRoomModule } from './chat-room/chat-room.module'; +import { TournamentService } from './tournament/tournament.service'; +import { TournamentModule } from './tournament/tournament.module'; @Module({ imports: [ @@ -39,6 +41,7 @@ import { ChatRoomModule } from './chat-room/chat-room.module'; SongsModule, ChatRoomModule, ScoringModule, + TournamentModule, ], controllers: [AppController], @@ -52,6 +55,7 @@ import { ChatRoomModule } from './chat-room/chat-room.module'; provide: APP_INTERCEPTOR, useClass: GlobalInterceptor, }, + TournamentService, ], }) export class AppModule {} diff --git a/backend/src/tournament/scheduling.service.ts b/backend/src/tournament/scheduling.service.ts new file mode 100644 index 00000000..26d4f7d5 --- /dev/null +++ b/backend/src/tournament/scheduling.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { GameSession } from 'src/game-session/game-session.entity'; +import { Player } from '../player/player.entity'; + +@Injectable() +export class SchedulingService { + schedule(tournament: any): GameSession[] { + const participants: Player[] = tournament.participants; + const matches: GameSession[] = []; + + for (let i = 0; i < participants.length; i++) { + for (let j = i + 1; j < participants.length; j++) { + const match = new GameSession(); + match.players = [participants[i], participants[j]]; + match.startTime = this.getMatchTime(); + matches.push(match); + } + } + + return matches; + } + + private getMatchTime(): Date { + return new Date(); + } +} diff --git a/backend/src/tournament/tournament.entity.ts b/backend/src/tournament/tournament.entity.ts new file mode 100644 index 00000000..41a16cdf --- /dev/null +++ b/backend/src/tournament/tournament.entity.ts @@ -0,0 +1,33 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToMany, + OneToMany, +} from 'typeorm'; +import { User } from '../user/user.entity'; +import { GameSession } from '../game-session/game-session.entity'; + +@Entity() +export class Tournament { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column('timestamp') + startTime: Date; + + @Column('timestamp') + endTime: Date; + + @Column('json', { nullable: true }) + rules: Record; + + @ManyToMany(() => User, { eager: true }) + participants: User[]; + + @OneToMany(() => GameSession, (gameSession) => gameSession) + matches: GameSession[]; +} diff --git a/backend/src/tournament/tournament.module.ts b/backend/src/tournament/tournament.module.ts new file mode 100644 index 00000000..57fd15d1 --- /dev/null +++ b/backend/src/tournament/tournament.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TournamentService } from './tournament.service'; +import { Tournament } from './tournament.entity'; +import { User } from '../user/user.entity'; +import { GameSession } from '../game-session/game-session.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Tournament, User, GameSession])], + providers: [TournamentService], + exports: [TournamentService], +}) +export class TournamentModule {} diff --git a/backend/src/tournament/tournament.service.spec.ts b/backend/src/tournament/tournament.service.spec.ts new file mode 100644 index 00000000..c53da047 --- /dev/null +++ b/backend/src/tournament/tournament.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TournamentService } from './tournament.service'; + +describe('TournamentService', () => { + let service: TournamentService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [TournamentService], + }).compile(); + + service = module.get(TournamentService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/tournament/tournament.service.ts b/backend/src/tournament/tournament.service.ts new file mode 100644 index 00000000..8b3ad467 --- /dev/null +++ b/backend/src/tournament/tournament.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Tournament } from './tournament.entity'; +import { Player } from '../player/player.entity'; +import { GameSession } from 'src/game-session/game-session.entity'; +import { SchedulingService } from './scheduling.service'; + +@Injectable() +export class TournamentService { + constructor( + @InjectRepository(Tournament) + private tournamentRepository: Repository, + + @InjectRepository(Player) + private playerRepository: Repository, + + @InjectRepository(GameSession) + private gameSessionRepository: Repository, + + private schedulingService: SchedulingService, + ) {} + + async createTournament( + name: string, + startTime: Date, + endTime: Date, + rules: Record, + participantIds: string[], + ): Promise { + const participants = await this.playerRepository.find({ + where: participantIds.map((id) => ({ id })), + }); + const tournament = this.tournamentRepository.create({ + name, + startTime, + endTime, + rules, + participants, + }); + return this.tournamentRepository.save(tournament); + } + + async scheduleMatches(tournamentId: string): Promise { + const tournament = await this.tournamentRepository.findOne({ + where: { id: tournamentId }, + relations: ['participants'], + }); + + if (!tournament) { + throw new Error('Tournament not found'); + } + + const matches = await this.schedulingService.schedule(tournament); + return this.gameSessionRepository.save(matches); + } + + async applyScoring(tournamentId: string): Promise { + const tournament = await this.tournamentRepository.findOne({ + where: { id: tournamentId }, + relations: ['matches'], + }); + + if (!tournament) { + throw new Error('Tournament not found'); + } + + tournament.matches.forEach((match) => { + this.applyMatchScoringRules(match); + }); + + await this.gameSessionRepository.save(tournament.matches); + } + + private applyMatchScoringRules(match: GameSession) { + if (match.players.length === 2) { + const [player1, player2] = match.players; + } + } +}