diff --git a/server/prisma/migrations/20241208152036_init/migration.sql b/server/prisma/migrations/20241208192715_init/migration.sql similarity index 70% rename from server/prisma/migrations/20241208152036_init/migration.sql rename to server/prisma/migrations/20241208192715_init/migration.sql index f77bef0..cdc8f74 100644 --- a/server/prisma/migrations/20241208152036_init/migration.sql +++ b/server/prisma/migrations/20241208192715_init/migration.sql @@ -1,5 +1,5 @@ -- CreateEnum -CREATE TYPE "MoneyUpdateType" AS ENUM ('START', 'WIN_CHALLENGE', 'BUY_TRAIN'); +CREATE TYPE "MoneyUpdateType" AS ENUM ('START', 'NEW_RUN', 'WIN_CHALLENGE', 'BUY_TRAIN'); -- CreateTable CREATE TABLE "Player" ( @@ -15,11 +15,22 @@ CREATE TABLE "Player" ( CREATE TABLE "Game" ( "id" SERIAL NOT NULL, "started" BOOLEAN NOT NULL DEFAULT false, - "currentRunnerId" INTEGER, + "currentRunId" INTEGER, CONSTRAINT "Game_pkey" PRIMARY KEY ("id") ); +-- CreateTable +CREATE TABLE "PlayerRun" ( + "id" SERIAL NOT NULL, + "gameId" INTEGER NOT NULL, + "runnerId" INTEGER NOT NULL, + "start" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "end" TIMESTAMPTZ(3), + + CONSTRAINT "PlayerRun_pkey" PRIMARY KEY ("id") +); + -- CreateTable CREATE TABLE "Geolocation" ( "id" SERIAL NOT NULL, @@ -56,6 +67,7 @@ CREATE TABLE "ChallengeAction" ( "end" TIMESTAMPTZ(3), "penaltyStart" TIMESTAMPTZ(3), "penaltyEnd" TIMESTAMPTZ(3), + "runId" INTEGER NOT NULL, CONSTRAINT "ChallengeAction_pkey" PRIMARY KEY ("id") ); @@ -70,6 +82,7 @@ CREATE TABLE "TrainTrip" ( "departureTime" TIMESTAMPTZ(3) NOT NULL, "arrivalTime" TIMESTAMPTZ(3) NOT NULL, "infoJson" JSONB NOT NULL, + "runId" INTEGER NOT NULL, CONSTRAINT "TrainTrip_pkey" PRIMARY KEY ("id") ); @@ -82,6 +95,7 @@ CREATE TABLE "MoneyUpdate" ( "reason" "MoneyUpdateType" NOT NULL, "actionId" INTEGER, "tripId" TEXT, + "runId" INTEGER, "timestamp" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "MoneyUpdate_pkey" PRIMARY KEY ("id") @@ -91,7 +105,7 @@ CREATE TABLE "MoneyUpdate" ( CREATE UNIQUE INDEX "Player_name_key" ON "Player"("name"); -- CreateIndex -CREATE UNIQUE INDEX "Game_currentRunnerId_key" ON "Game"("currentRunnerId"); +CREATE UNIQUE INDEX "Game_currentRunId_key" ON "Game"("currentRunId"); -- CreateIndex CREATE UNIQUE INDEX "Challenge_title_key" ON "Challenge"("title"); @@ -105,8 +119,17 @@ CREATE UNIQUE INDEX "MoneyUpdate_actionId_key" ON "MoneyUpdate"("actionId"); -- CreateIndex CREATE UNIQUE INDEX "MoneyUpdate_tripId_key" ON "MoneyUpdate"("tripId"); +-- CreateIndex +CREATE UNIQUE INDEX "MoneyUpdate_runId_key" ON "MoneyUpdate"("runId"); + -- AddForeignKey -ALTER TABLE "Game" ADD CONSTRAINT "Game_currentRunnerId_fkey" FOREIGN KEY ("currentRunnerId") REFERENCES "Player"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "Game" ADD CONSTRAINT "Game_currentRunId_fkey" FOREIGN KEY ("currentRunId") REFERENCES "PlayerRun"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PlayerRun" ADD CONSTRAINT "PlayerRun_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PlayerRun" ADD CONSTRAINT "PlayerRun_runnerId_fkey" FOREIGN KEY ("runnerId") REFERENCES "Player"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "Geolocation" ADD CONSTRAINT "Geolocation_playerId_fkey" FOREIGN KEY ("playerId") REFERENCES "Player"("id") ON DELETE RESTRICT ON UPDATE CASCADE; @@ -117,9 +140,15 @@ ALTER TABLE "ChallengeAction" ADD CONSTRAINT "ChallengeAction_playerId_fkey" FOR -- AddForeignKey ALTER TABLE "ChallengeAction" ADD CONSTRAINT "ChallengeAction_challengeId_fkey" FOREIGN KEY ("challengeId") REFERENCES "Challenge"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +-- AddForeignKey +ALTER TABLE "ChallengeAction" ADD CONSTRAINT "ChallengeAction_runId_fkey" FOREIGN KEY ("runId") REFERENCES "PlayerRun"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE "TrainTrip" ADD CONSTRAINT "TrainTrip_playerId_fkey" FOREIGN KEY ("playerId") REFERENCES "Player"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +-- AddForeignKey +ALTER TABLE "TrainTrip" ADD CONSTRAINT "TrainTrip_runId_fkey" FOREIGN KEY ("runId") REFERENCES "PlayerRun"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + -- AddForeignKey ALTER TABLE "MoneyUpdate" ADD CONSTRAINT "MoneyUpdate_playerId_fkey" FOREIGN KEY ("playerId") REFERENCES "Player"("id") ON DELETE RESTRICT ON UPDATE CASCADE; @@ -128,3 +157,6 @@ ALTER TABLE "MoneyUpdate" ADD CONSTRAINT "MoneyUpdate_actionId_fkey" FOREIGN KEY -- AddForeignKey ALTER TABLE "MoneyUpdate" ADD CONSTRAINT "MoneyUpdate_tripId_fkey" FOREIGN KEY ("tripId") REFERENCES "TrainTrip"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MoneyUpdate" ADD CONSTRAINT "MoneyUpdate_runId_fkey" FOREIGN KEY ("runId") REFERENCES "PlayerRun"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 8a257f8..ad59b4d 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -16,14 +16,29 @@ model Player { geolocations Geolocation[] moneyUpdates MoneyUpdate[] trips TrainTrip[] - runningGame Game? + runs PlayerRun[] } model Game { - id Int @id @default(autoincrement()) - started Boolean @default(false) - currentRunner Player? @relation(fields: [currentRunnerId], references: [id]) - currentRunnerId Int? @unique + id Int @id @default(autoincrement()) + started Boolean @default(false) + currentRun PlayerRun? @relation("CurrentRun", fields: [currentRunId], references: [id]) + currentRunId Int? @unique + runs PlayerRun[] @relation("GameRuns") +} + +model PlayerRun { + id Int @id @default(autoincrement()) + game Game @relation("GameRuns", fields: [gameId], references: [id]) + runningGame Game? @relation("CurrentRun") + gameId Int + runner Player @relation(fields: [runnerId], references: [id]) + runnerId Int + start DateTime @default(now()) @db.Timestamptz(3) + end DateTime? @db.Timestamptz(3) + moneyUpdate MoneyUpdate? + challengeActions ChallengeAction[] + trains TrainTrip[] } model Geolocation { @@ -59,6 +74,8 @@ model ChallengeAction { end DateTime? @db.Timestamptz(3) penaltyStart DateTime? @db.Timestamptz(3) penaltyEnd DateTime? @db.Timestamptz(3) + run PlayerRun @relation(fields: [runId], references: [id]) + runId Int moneyUpdate MoneyUpdate? } @@ -72,6 +89,8 @@ model TrainTrip { departureTime DateTime @db.Timestamptz(3) arrivalTime DateTime @db.Timestamptz(3) infoJson Json + run PlayerRun @relation(fields: [runId], references: [id]) + runId Int moneyUpdate MoneyUpdate? } @@ -85,11 +104,14 @@ model MoneyUpdate { actionId Int? @unique trip TrainTrip? @relation(fields: [tripId], references: [id]) tripId String? @unique + run PlayerRun? @relation(fields: [runId], references: [id]) + runId Int? @unique timestamp DateTime @default(now()) @db.Timestamptz(3) } enum MoneyUpdateType { START + NEW_RUN WIN_CHALLENGE BUY_TRAIN } diff --git a/server/src/challenge-actions/challenge-actions.service.ts b/server/src/challenge-actions/challenge-actions.service.ts index 04b032d..fa49230 100644 --- a/server/src/challenge-actions/challenge-actions.service.ts +++ b/server/src/challenge-actions/challenge-actions.service.ts @@ -12,7 +12,8 @@ export class ChallengeActionsService { constructor(private prisma: PrismaService) { } async create(authenticatedPlayer: Player, createChallengeActionDto: CreateChallengeActionDto): Promise { - const data = { ...createChallengeActionDto, playerId: authenticatedPlayer.id } + const game = await this.prisma.game.findUnique({ where: { id: 1 } }) + const data = { ...createChallengeActionDto, playerId: authenticatedPlayer.id, runId: game.currentRunId } return await this.prisma.challengeAction.create({ data: data, }) diff --git a/server/src/challenge-actions/entities/challenge-action.entity.ts b/server/src/challenge-actions/entities/challenge-action.entity.ts index 5e555cc..650c68d 100644 --- a/server/src/challenge-actions/entities/challenge-action.entity.ts +++ b/server/src/challenge-actions/entities/challenge-action.entity.ts @@ -7,33 +7,55 @@ export class ChallengeActionEntity implements ChallengeAction { Object.assign(this, partial) } - @ApiProperty({ description: "Identifiant unique" }) + /** + * Identifiant unique + */ id: number - @ApiProperty({ description: "Identifiant de læ joueur⋅se effectuant le défi" }) + /** + * Identifiant de læ joueur⋅se effectuant le défi + */ playerId: number - @ApiProperty({ description: "Identifiant du défi rattaché à l'action" }) + /** + * Identifiant du défi rattaché à l'action + */ challengeId: number - @ApiProperty({ description: "Est-ce que le défi est actuellement en train d'être réalisé" }) + /** + * Est-ce que le défi est actuellement en train d'être réalisé + */ active: boolean - @ApiProperty({ description: "Est-ce que le défi a été réussi" }) + /** + * Est-ce que le défi a été réussi + */ success: boolean - @ApiProperty({ description: "Heure à laquelle le défi a été démarré" }) + /** + * Heure à laquelle le défi a été démarré + */ start: Date + /** + * Heure à laquelle le défi a été terminé + */ @IsOptional() - @ApiProperty({ description: "Heure à laquelle le défi a été terminé", required: false, nullable: true }) - end: Date + end: Date | null = null - @IsOptional() - @ApiProperty({ description: "Heure à laquelle la pénalité a commencé, si applicable", required: false, nullable: true }) - penaltyStart: Date + /** + * Heure à laquelle la pénalité a commencé, si applicable + */ + penaltyStart: Date | null = null + /** + * Heure à laquelle la pénalité s'est terminée, si applicable + */ @IsOptional() - @ApiProperty({ description: "Heure à laquelle la pénalité s'est terminée, si applicable", required: false, nullable: true }) - penaltyEnd: Date + penaltyEnd: Date | null = null + + /** + * Identifiant de la course pendant laquelle le challenge est réalisé + */ + runId: number } diff --git a/server/src/challenges/challenges.service.ts b/server/src/challenges/challenges.service.ts index dd48bfd..c34d80a 100644 --- a/server/src/challenges/challenges.service.ts +++ b/server/src/challenges/challenges.service.ts @@ -61,8 +61,8 @@ export class ChallengesService { } async drawRandom(player: Player): Promise { - const game = await this.prisma.game.findUnique({ where: { id: 1 } }) - if (game.currentRunnerId != player.id) + const game = await this.prisma.game.findUnique({ where: { id: 1 }, include: { currentRun: true } }) + if (game.currentRun?.runnerId !== player.id) throw new ConflictException("Vous n'êtes pas en course, ce n'est pas à vous de tirer un défi.") const currentChallengeAction = await this.prisma.challengeAction.findFirst({ where: { @@ -91,6 +91,7 @@ export class ChallengesService { data: { playerId: player.id, challengeId: challenge.id, + runId: game.currentRunId, active: true, success: false, } diff --git a/server/src/game/entities/game.entity.ts b/server/src/game/entities/game.entity.ts index 17d334e..e766eed 100644 --- a/server/src/game/entities/game.entity.ts +++ b/server/src/game/entities/game.entity.ts @@ -6,12 +6,18 @@ export class GameEntity implements Game { Object.assign(this, partial) } - @ApiProperty({ description: "Identifiant unique du jeu" }) + /** + * Identifiant unique du jeu + */ id: number - @ApiProperty({ description: "Est-ce que la partie est en cours" }) + /** + * Est-ce que la partie est en cours + */ started: boolean - @ApiProperty({ description: "Identifiant de læ joueur⋅se en course" }) - currentRunnerId: number + /** + * Identifiant de læ joueur⋅se en course + */ + currentRunId: number } diff --git a/server/src/game/game.service.ts b/server/src/game/game.service.ts index 3b0290a..9f5159f 100644 --- a/server/src/game/game.service.ts +++ b/server/src/game/game.service.ts @@ -7,8 +7,8 @@ import { RepairGame } from './dto/repair-game.dto' export class GameService { constructor (private prisma: PrismaService) {} - async find(): Promise { - return await this.prisma.game.findUnique({ where: { id: 1 } }) + async find() { + return await this.prisma.game.findUnique({ where: { id: 1 }, include: { currentRun: true } }) } async start(): Promise { @@ -16,7 +16,8 @@ export class GameService { if (game.started) throw new ConflictException("La partie a déjà démarré.") const players = await this.prisma.player.findMany() - const alreadyStarted = game.currentRunnerId !== null + const alreadyStarted = game.currentRunId !== null + let run if (!alreadyStarted) { for (const player of players) { await this.prisma.moneyUpdate.create({ @@ -27,30 +28,50 @@ export class GameService { } }) } + const runnerId = players[Math.floor(players.length * Math.random())].id + run = await this.prisma.playerRun.create({ + data: { + gameId: game.id, + runnerId: runnerId, + } + }) + } + else { + run = game.currentRun } - const runnerId = alreadyStarted ? game.currentRunnerId : players[Math.floor(players.length * Math.random())].id return await this.prisma.game.update({ where: { id: 1 }, data: { started: true, - currentRunnerId: runnerId, + currentRunId: run.id, }, }) } async switchRunningPlayer(): Promise { const game = await this.find() - const newRunnerId = game.currentRunnerId == 1 ? 2 : 1 - await this.prisma.moneyUpdate.create({ + const newRunnerId = game.currentRun.runnerId == 1 ? 2 : 1 + const firstRun = await this.prisma.playerRun.findFirst({ where: { runnerId: newRunnerId } }) !== null + const newRun = await this.prisma.playerRun.create({ data: { - playerId: newRunnerId, - amount: 300, - reason: MoneyUpdateType.START, + gameId: game.id, + runnerId: newRunnerId, } }) + if (!firstRun) { + // Si c'est une nouvelle course (pas la première), on accorde un bonus de points de départ + await this.prisma.moneyUpdate.create({ + data: { + playerId: newRunnerId, + amount: 300, + reason: MoneyUpdateType.NEW_RUN, + runId: newRun.id, + } + }) + } return await this.prisma.game.update({ where: { id: game.id }, - data: { currentRunnerId: newRunnerId }, + data: { currentRunId: newRun.id }, }) } @@ -67,18 +88,19 @@ export class GameService { } async reset(): Promise { - await this.prisma.moneyUpdate.deleteMany() - await this.prisma.challengeAction.deleteMany() - await this.prisma.trainTrip.deleteMany() - await this.prisma.geolocation.deleteMany() - await this.prisma.player.updateMany({ data: { money: 0 } }) await this.prisma.game.update({ where: { id: 1 }, data: { started: false, - currentRunnerId: null, + currentRunId: null, }, }) + await this.prisma.player.updateMany({ data: { money: 0 } }) + await this.prisma.moneyUpdate.deleteMany() + await this.prisma.challengeAction.deleteMany() + await this.prisma.trainTrip.deleteMany() + await this.prisma.playerRun.deleteMany() + await this.prisma.geolocation.deleteMany() return await this.find() } diff --git a/server/src/money-updates/entities/money-update.entity.ts b/server/src/money-updates/entities/money-update.entity.ts index 395eba1..0abe1b2 100644 --- a/server/src/money-updates/entities/money-update.entity.ts +++ b/server/src/money-updates/entities/money-update.entity.ts @@ -1,29 +1,49 @@ import { ApiProperty } from "@nestjs/swagger" import { MoneyUpdate, MoneyUpdateType } from "@prisma/client" +import { IsOptional } from "class-validator" export class MoneyUpdateEntity implements MoneyUpdate { constructor (partial: Partial) { Object.assign(this, partial) } - @ApiProperty({ description: "Identifiant unique de la mise à jour de solde" }) + /** + * Identifiant unique de la mise à jour de solde + */ id: number - @ApiProperty({ description: "Joueur⋅se concerné⋅e par la mise à jour de solde" }) + /** + * Joueur⋅se concerné⋅e par la mise à jour de solde + */ playerId: number - @ApiProperty({ description: "Montant de la modification du solde" }) + /** + * Montant de la modification du solde + */ amount: number - @ApiProperty({ description: "Type de modification de solde" }) + /** + * Type de modification de solde + */ reason: MoneyUpdateType - @ApiProperty({ description: "Identifiant de la réalisation de défi, si la mise à jour est liée à un défi", nullable: true }) - actionId: number + /** + * Identifiant de la réalisation de défi, si la mise à jour est liée à un défi + */ + actionId: number | null = null - @ApiProperty({ description: "Identifiant du trajet acheté, si la mise à jour est liée à la réservation d'un train", nullable: true }) - tripId: string + /** + * Identifiant du trajet acheté, si la mise à jour est liée à la réservation d'un train + */ + tripId: string | null = null - @ApiProperty({ description: "Date et heure de la modification de solde" }) + /** + * Idenifiant de la course rattachée, si la mise à jour est liée à une nouvelle tentative de course + */ + runId: number | null = null + + /** + * Date et heure de la modification de solde + */ timestamp: Date } diff --git a/server/src/trains/entities/train.entity.ts b/server/src/trains/entities/train.entity.ts index 561fd21..9f5d2f0 100644 --- a/server/src/trains/entities/train.entity.ts +++ b/server/src/trains/entities/train.entity.ts @@ -7,27 +7,48 @@ export class TrainEntity implements TrainTrip { Object.assign(this, partial) } - @ApiProperty({ description: "Identifiant du train, donné par l'identifiant de partage Interrail" }) + /** + * Identifiant du train, donné par l'identifiant de partage Interrail + */ id: string - @ApiProperty({ description: "Identifiant de læ joueur⋅se effectuant le trajet" }) + /** + * Identifiant de læ joueur⋅se effectuant le trajet + */ playerId: number - @ApiProperty({ description: "Distance estimée en mètres du trajet" }) + /** + * Distance estimée en mètres du trajet + */ distance: number - @ApiProperty({ description: "Nom de la gare de départ" }) + /** + * Nom de la gare de départ + */ from: string - @ApiProperty({ description: "Nom de la gare d'arrivée" }) + /** + * Nom de la gare d'arrivée + */ to: string - @ApiProperty({ description: "Date et heure de départ du train" }) + /** + * Date et heure de départ du train + */ departureTime: Date - @ApiProperty({ description: "Date et heure d'arrivée du train" }) + /** + * Date et heure d'arrivée du train" + */ arrivalTime: Date - @ApiProperty({ description: "Informations JSON supplémentaires transmises par Interrail" }) + /** + * Informations JSON supplémentaires transmises par Interrail + */ infoJson: JsonValue + + /** + * Identifiant de la course pendant laquelle ce train a été emprunté + */ + runId: number } diff --git a/server/src/trains/trains.service.ts b/server/src/trains/trains.service.ts index 283b26b..f000147 100644 --- a/server/src/trains/trains.service.ts +++ b/server/src/trains/trains.service.ts @@ -15,7 +15,9 @@ export class TrainsService { constructor(private prisma: PrismaService) { } async create(createTrainDto: CreateTrainDto): Promise { - return await this.prisma.trainTrip.create({ data: createTrainDto }) + const game = await this.prisma.game.findUnique({ where: { id: 1 } }) + const data = { ...createTrainDto, runId: game.currentRunId } + return await this.prisma.trainTrip.create({ data: data }) } async findAll(queryPagination: QueryPaginationDto, playerFilter: PlayerFilterDto): Promise<[TrainTrip[], number]> { @@ -54,6 +56,8 @@ export class TrainsService { } async import(player: Player, { id: trainId }: ImportTrainDto): Promise { + const game = await this.prisma.game.findUnique({ where: { id: 1 } }) + if (this.findOne(trainId)) throw new ConflictException(`Le train avec l'identifiant ${trainId} est déjà importé`) @@ -95,6 +99,7 @@ export class TrainsService { departureTime: departure, arrivalTime: arrival, infoJson: leg.infoJson, + runId: game.currentRunId, }, }) // Ajout du trajet en points