Skip to content

Commit

Permalink
Feat/game data to db (#31)
Browse files Browse the repository at this point in the history
## How does this PR impact the user?

<!-- Add "before" and "after" screenshots or screen recordings; we like
loom for screen recordings https://www.loom.com/ -->

## Description

Updated Prisma scheme:
- Added `food_eaten` field in GameStats
- Changed some fileds name
- Changed `player_id` field type `Int` -> `String`

Updated end game functuion:
- Separated end game code and created new function `endGame`
- Added function that writes game stats to the db

## Limitations

<!-- Anything related to this PR that wasn't "done" in this PR -->

## Checklist

- [x] my PR is focused and contains one wholistic change
- [ ] I have added screenshots or screen recordings to show the changes
  • Loading branch information
PlutoniumSoup authored Sep 14, 2024
1 parent a547c19 commit 5e1deb5
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 21 deletions.
21 changes: 21 additions & 0 deletions migrations/20240908164726_update_game_fields_types/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
Warnings:
- You are about to alter the column `end_time` on the `Game` table. The data in that column could be lost. The data in that column will be cast from `String` to `DateTime`.
- You are about to alter the column `start_time` on the `Game` table. The data in that column could be lost. The data in that column will be cast from `String` to `DateTime`.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Game" (
"game_id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"start_time" DATETIME NOT NULL,
"end_time" DATETIME NOT NULL,
"end_reason" TEXT NOT NULL
);
INSERT INTO "new_Game" ("end_reason", "end_time", "game_id", "start_time") SELECT "end_reason", "end_time", "game_id", "start_time" FROM "Game";
DROP TABLE "Game";
ALTER TABLE "new_Game" RENAME TO "Game";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
30 changes: 30 additions & 0 deletions migrations/20240914132517_update_fields/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
Warnings:
- You are about to drop the column `num_ate` on the `GameStats` table. All the data in the column will be lost.
- You are about to drop the column `num_eaten` on the `GameStats` table. All the data in the column will be lost.
- You are about to drop the column `player_id` on the `GameStats` table. All the data in the column will be lost.
- Added the required column `deaths` to the `GameStats` table without a default value. This is not possible if the table is not empty.
- Added the required column `food_eaten` to the `GameStats` table without a default value. This is not possible if the table is not empty.
- Added the required column `kills` to the `GameStats` table without a default value. This is not possible if the table is not empty.
- Added the required column `user_id` to the `GameStats` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_GameStats" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"game_id" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL,
"size" REAL NOT NULL,
"food_eaten" INTEGER NOT NULL,
"kills" INTEGER NOT NULL,
"deaths" INTEGER NOT NULL,
CONSTRAINT "GameStats_game_id_fkey" FOREIGN KEY ("game_id") REFERENCES "Game" ("game_id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_GameStats" ("game_id", "id", "size") SELECT "game_id", "id", "size" FROM "GameStats";
DROP TABLE "GameStats";
ALTER TABLE "new_GameStats" RENAME TO "GameStats";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
19 changes: 19 additions & 0 deletions migrations/20240914132735_update_fields/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_GameStats" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"game_id" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL,
"size" REAL NOT NULL,
"food_eaten" INTEGER NOT NULL,
"kills" INTEGER NOT NULL,
"deaths" INTEGER NOT NULL,
CONSTRAINT "GameStats_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "GameStats_game_id_fkey" FOREIGN KEY ("game_id") REFERENCES "Game" ("game_id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_GameStats" ("deaths", "food_eaten", "game_id", "id", "kills", "size", "user_id") SELECT "deaths", "food_eaten", "game_id", "id", "kills", "size", "user_id" FROM "GameStats";
DROP TABLE "GameStats";
ALTER TABLE "new_GameStats" RENAME TO "GameStats";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
27 changes: 15 additions & 12 deletions schema.prisma
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
}

generator client {
provider = "prisma-client-js"
}

model User {
id Int @id @default(autoincrement())
password String
username String @unique
id Int @id @default(autoincrement())
password String
username String @unique
game_stats GameStats[]
}

model Game {
game_id Int @id @default(autoincrement())
start_time String
end_time String
game_id Int @id @default(autoincrement())
start_time DateTime
end_time DateTime
end_reason String
game_stats GameStats[]
}

model GameStats {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
game_id Int
player_id Int
user_id Int
size Float
num_eaten Int
num_ate Int
food_eaten Int
kills Int
deaths Int
user User @relation(fields: [user_id], references: [id])
games Game @relation(fields: [game_id], references: [game_id])
}
}
1 change: 1 addition & 0 deletions server/api/bot/submit.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default defineEventHandler(async (event) => {

botCodeStore.submitBotCode({
username: event.context.user.username,
userId: event.context.user.id,
code: result.data.code,
});

Expand Down
25 changes: 23 additions & 2 deletions server/plugins/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ivm from "isolated-vm";
import prepareBotCode from "../../utils/prepareBotCode";
import * as botCodeStore from "~/utils/botCodeStore";
import World from "~/utils/world";
import { recordGameEnd, type GameStat } from "~/utils/recordGamedb";

const MEMORY_LIMIT_MB = 64;
const TIME_LIMIT_MS = 75;
Expand Down Expand Up @@ -70,18 +71,38 @@ function startEngine({ botApi }: StartEngineArgs) {
const worldState = WORLD_REF.world.getState();
for (const bot of worldState.bots.values()) {
if (bot.radius > worldState.height / 2) {
WORLD_REF.world = new World ({ width: 600, height: 600 });
endGame("Player overdominating");
console.debug(`The World was restarted cus ${bot.botId} was oversized`);
}
}
}, 250);

setInterval(() => {
WORLD_REF.world = new World ({ width: 600, height: 600 });
endGame("Time's up");
console.debug(`The World was restarted after ${MAX_ROUND_TIME_MS} ms`);
}, MAX_ROUND_TIME_MS);
}

function endGame(reason: string) {
const endTime = new Date();
const worldState = WORLD_REF.world.getState();
recordGameEnd({
startTime: worldState.startTime,
endTime: endTime,
endReason: reason,
stats: Array.from(worldState.stats.entries()).map(([botId, stat]) => {
return {
userId: botCodeStore.getBots()[botId]?.userId,
size: worldState.bots.get(WORLD_REF.world.getSpawnId(botId))?.radius,
foodEaten: stat.foodEaten,
kills: stat.kills,
deaths: stat.deaths,
};
}).filter((entry): entry is GameStat => entry.userId !== undefined && entry.size !== undefined),
});
WORLD_REF.world = new World ({ width: 600, height: 600 });
}

export default defineNitroPlugin(async () => {
const level01Api = await useStorage("assets:botApis").getItem("level01.js");

Expand Down
17 changes: 10 additions & 7 deletions utils/botCodeStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface BotCode {
id: string;
code: string;
username: string;
userId: number;
}

export type BotCodes = Record<string, BotCode>;
Expand All @@ -18,12 +19,13 @@ const botEventEmitter = new EventEmitter();
type SubmitBotCodeArgs = {
code: string;
username: string;
userId: number;
};

export function submitBotCode({ code, username }: SubmitBotCodeArgs) {
export function submitBotCode({ code, username, userId }: SubmitBotCodeArgs) {
// ensure each user has only one bot
const id = Object.values(STORE).find(botCode => botCode.username === username)?.id || Math.random().toString(36).substring(5);
const botCode = { id, code, username };
const botCode = { id, code, username, userId };
STORE[id] = botCode;
saveBot(botCode);
botEventEmitter.emit("update", STORE);
Expand Down Expand Up @@ -52,19 +54,20 @@ function loadBots() {
for (const file of files) {
const code = fs.readFileSync(`${BOT_CODE_DIR}/${file}`, "utf8");
const username = file.split("-")[0];
const id = file.split("-")[1]?.replace(".js", "");
if (!id || !code || !username) {
const id = file.split("-")[1];
const userId = file.split("-")[2]?.replace(".js", "");
if (!id || !code || !username || !userId) {
throw new Error(`Invalid bot code file: ${file}`);
}
STORE[id] = { id, code, username };
STORE[id] = { id, code, username, userId: +userId };
}
}

function saveBot({ id, code, username }: BotCode) {
function saveBot({ id, code, username, userId }: BotCode) {
if (!fs.existsSync(BOT_CODE_DIR)) {
fs.mkdirSync(BOT_CODE_DIR);
}
const botCodeFile = `${BOT_CODE_DIR}/${username}-${id}.js`;
const botCodeFile = `${BOT_CODE_DIR}/${username}-${id}-${userId}.js`;

fs.writeFileSync(botCodeFile, code);
}
Expand Down
3 changes: 3 additions & 0 deletions utils/prepareBotCode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ it("prepares bot code correctly when there is one bot in the world", () => {
id: "1",
code: "console.log('hello world');",
username: "yurij",
userId: 1,
};

const state = {
Expand All @@ -27,6 +28,7 @@ it("prepares bot code correctly when there are two bots in the world", () => {
id: "1",
code: "console.log('hello world');",
username: "yurij",
userId: 1,
};

const state = {
Expand All @@ -51,6 +53,7 @@ it("prepares bot code correctly when there are three bots and some food in the w
id: "1",
code: "console.log('hello world');",
username: "yurij",
userId: 1,
};

const state = {
Expand Down
45 changes: 45 additions & 0 deletions utils/recordGamedb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import prisma from "~/utils/db";

export type GameStat = {
userId: number;
size: number;
foodEaten: number;
kills: number;
deaths: number;
};

type RecordGameArgs = {
startTime: Date;
endTime: Date;
endReason: string;
stats: GameStat[];
};

export async function recordGameEnd({ startTime, endTime, endReason, stats }: RecordGameArgs) {
try {
// Создание записи о завершении игры
const game = await prisma.game.create({
data: {
start_time: startTime,
end_time: endTime,
end_reason: endReason,
game_stats: {
create: stats.map(stat => ({
user_id: stat.userId,
size: stat.size,
food_eaten: stat.foodEaten,
kills: stat.kills,
deaths: stat.deaths,
})),
},
},
include: {
game_stats: true,
},
});

console.log("Game record added:", game);
} catch (error) {
console.error("Error recording game end:", error);
}
}
7 changes: 7 additions & 0 deletions utils/world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface WorldState {
width: number;
height: number;
stats: Map<string, Stats>;
startTime: Date;
}

type BotSprites = Map<string, BotSprite>;
Expand Down Expand Up @@ -60,6 +61,7 @@ export default class World {
private minSpawnDistance = 10;
private maxMoveDistance = 2;
private newBotRadius = 5;
private startTime = new Date();

constructor({ width, height }: WorldArgs) {
this.width = width;
Expand Down Expand Up @@ -163,6 +165,7 @@ export default class World {
width: this.width,
height: this.height,
stats: this.stats,
startTime: this.startTime,
};
}

Expand Down Expand Up @@ -284,4 +287,8 @@ export default class World {

return Boolean(botIdsToRemove.length || foodIdxToRemove.length);
}

getSpawnId(botId: string) {
return String(this.botIdToSpawnId.get(botId));
}
}

0 comments on commit 5e1deb5

Please sign in to comment.