diff --git a/components/GameScreen.vue b/components/GameScreen.vue
index dc8106d..6521997 100644
--- a/components/GameScreen.vue
+++ b/components/GameScreen.vue
@@ -1,20 +1,13 @@
diff --git a/package-lock.json b/package-lock.json
index 9dad633..aba3d0e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"bcrypt": "^5.1.1",
"isolated-vm": "^4.6.0",
"lru-cache": "^10.2.0",
+ "pixi.js": "^8.2.5",
"prisma": "^5.9.1",
"zod": "^3.22.4"
},
@@ -2841,6 +2842,11 @@
"node": "^16 || ^18 || >= 20"
}
},
+ "node_modules/@pixi/colord": {
+ "version": "2.9.6",
+ "resolved": "https://registry.npmjs.org/@pixi/colord/-/colord-2.9.6.tgz",
+ "integrity": "sha512-nezytU2pw587fQstUu1AsJZDVEynjskwOL+kibwcdxsMBFqPsFFNA7xl0ii/gXuDi6M0xj3mfRJj8pBSc2jCfA=="
+ },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -3527,6 +3533,16 @@
"@types/node": "*"
}
},
+ "node_modules/@types/css-font-loading-module": {
+ "version": "0.0.12",
+ "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.12.tgz",
+ "integrity": "sha512-x2tZZYkSxXqWvTDgveSynfjq/T2HyiZHXb00j/+gy19yp70PHCizM48XFdjBCWH7eHBD0R5i/pw9yMBP/BH5uA=="
+ },
+ "node_modules/@types/earcut": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz",
+ "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ=="
+ },
"node_modules/@types/eslint": {
"version": "8.56.10",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz",
@@ -4419,6 +4435,19 @@
"vue-component-type-helpers": "^2.0.0"
}
},
+ "node_modules/@webgpu/types": {
+ "version": "0.1.43",
+ "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.43.tgz",
+ "integrity": "sha512-HoP+d+m+Kuq8CsE63BZ3+BYBKAemrqbHUNrCalxrUju5XW+q/094Q3oeIa+2pTraEbO8ckJmGpibzyGT4OV4YQ=="
+ },
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.10",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
+ "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -6298,6 +6327,11 @@
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
"dev": true
},
+ "node_modules/earcut": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
+ "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ=="
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -7125,8 +7159,7 @@
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
- "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
- "dev": true
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"node_modules/events": {
"version": "3.3.0",
@@ -8251,6 +8284,11 @@
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true
},
+ "node_modules/ismobilejs": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz",
+ "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw=="
+ },
"node_modules/isolated-vm": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/isolated-vm/-/isolated-vm-4.7.2.tgz",
@@ -10907,6 +10945,11 @@
"protocols": "^2.0.0"
}
},
+ "node_modules/parse-svg-path": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/parse-svg-path/-/parse-svg-path-0.1.2.tgz",
+ "integrity": "sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ=="
+ },
"node_modules/parse-url": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz",
@@ -11060,6 +11103,22 @@
"node": ">= 6"
}
},
+ "node_modules/pixi.js": {
+ "version": "8.2.5",
+ "resolved": "https://registry.npmjs.org/pixi.js/-/pixi.js-8.2.5.tgz",
+ "integrity": "sha512-cpN4f4Duj2mDPQeDQJQ33LXA8wIGMpWbhld1SklEwESRULYPUsY84zXC8B+Cyl+zojcXH7eY1OMEdbbIPMb+rA==",
+ "dependencies": {
+ "@pixi/colord": "^2.9.6",
+ "@types/css-font-loading-module": "^0.0.12",
+ "@types/earcut": "^2.1.4",
+ "@webgpu/types": "^0.1.40",
+ "@xmldom/xmldom": "^0.8.10",
+ "earcut": "^2.2.4",
+ "eventemitter3": "^5.0.1",
+ "ismobilejs": "^1.1.1",
+ "parse-svg-path": "^0.1.2"
+ }
+ },
"node_modules/pkg-types": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz",
diff --git a/package.json b/package.json
index c4ee73c..6fe418f 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
"bcrypt": "^5.1.1",
"isolated-vm": "^4.6.0",
"lru-cache": "^10.2.0",
+ "pixi.js": "^8.2.5",
"prisma": "^5.9.1",
"zod": "^3.22.4"
}
diff --git a/server/plugins/engine.ts b/server/plugins/engine.ts
index 732471e..1543e13 100644
--- a/server/plugins/engine.ts
+++ b/server/plugins/engine.ts
@@ -4,7 +4,7 @@ import * as botUtils from "~/utils/botstore";
import World from "~/utils/world";
const MEMORY_LIMIT_MB = 64;
-const TIME_LIMIT_MS = 100;
+const TIME_LIMIT_MS = 75;
export const WORLD = new World ({ width: 600, height: 600 });
@@ -14,6 +14,7 @@ async function runBot(code: string) {
const jail = context.global;
await jail.set("global", jail.derefInto());
const result = await context.eval(code, { timeout: TIME_LIMIT_MS });
+ isolate.dispose();
return JSON.parse(result);
}
@@ -28,7 +29,7 @@ async function runBots({ bots, world, botApi }: RunBotArgs) {
const botIds = Object.keys(bots);
for (const botId of botIds) {
- if (!state.bots.get(botId)) {
+ if (!world.hasBot(botId)) {
world.addBot(botId);
}
}
diff --git a/utils/prepareBotCode.spec.ts b/utils/prepareBotCode.spec.ts
index 7fa285b..45a1d04 100644
--- a/utils/prepareBotCode.spec.ts
+++ b/utils/prepareBotCode.spec.ts
@@ -8,7 +8,7 @@ it("prepares bot code correctly when there is one bot in the world", () => {
};
const state = {
- bots: new Map([["1", { x: 0, y: 0, radius: 5, id: "1", color: "#00FF00" }]]),
+ bots: new Map([["1", { x: 0, y: 0, radius: 5, botId: "1", color: "#00FF00", spawnId: "s1" }]]),
food: [],
width: 100,
height: 100,
@@ -29,8 +29,8 @@ it("prepares bot code correctly when there are two bots in the world", () => {
const state = {
bots: new Map([
- ["1", { x: 0, y: 0, radius: 5, id: "1", color: "#00FF00" }],
- ["2", { x: 10, y: 10, radius: 5, id: "2", color: "#00FF00" }],
+ ["1", { x: 0, y: 0, radius: 5, botId: "1", color: "#00FF00", spawnId: "s1" }],
+ ["2", { x: 10, y: 10, radius: 5, botId: "2", color: "#00FF00", spawnId: "s2" }],
]),
food: [],
width: 100,
@@ -52,9 +52,9 @@ it("prepares bot code correctly when there are three bots and some food in the w
const state = {
bots: new Map([
- ["1", { x: 0, y: 0, radius: 5, id: "1", color: "#00FF00" }],
- ["2", { x: 10, y: 10, radius: 5, id: "2", color: "#00FF00" }],
- ["3", { x: 20, y: 20, radius: 5, id: "3", color: "#00FF00" }],
+ ["1", { x: 0, y: 0, radius: 5, botId: "1", color: "#00FF00", spawnId: "s1" }],
+ ["2", { x: 10, y: 10, radius: 5, botId: "2", color: "#00FF00", spawnId: "s2" }],
+ ["3", { x: 20, y: 20, radius: 5, botId: "3", color: "#00FF00", spawnId: "s3" }],
]),
food: [{ x: 30, y: 30, radius: 5 }, { x: 40, y: 40, radius: 5 }],
width: 100,
diff --git a/utils/prepareBotCode.ts b/utils/prepareBotCode.ts
index 9de91e0..e06a75f 100644
--- a/utils/prepareBotCode.ts
+++ b/utils/prepareBotCode.ts
@@ -10,7 +10,7 @@ type PrepareBotCodeArgs = {
export default function prepareBotCode({ bot, state, botApi }: PrepareBotCodeArgs): string | undefined {
const { code } = bot;
- const botObject = state.bots.get(bot.id);
+ const botObject = [...state.bots.values()].find(b => bot.id === b.botId);
if (!botObject) {
// TODO(yurij): handle this better
console.error(`Bot with id ${bot.id} not found in the world`);
@@ -19,7 +19,7 @@ export default function prepareBotCode({ bot, state, botApi }: PrepareBotCodeArg
const me = { x: botObject.x, y: botObject.y, radius: botObject.radius };
const otherPlayers = [...state.bots.values()]
- .filter(b => b.id !== bot.id)
+ .filter(b => b.botId !== bot.id)
.map(b => ({ x: b.x, y: b.y, radius: b.radius }));
const food = state.food.map(f => ({ x: f.x, y: f.y, radius: f.radius }));
diff --git a/utils/world.ts b/utils/world.ts
index 7eb65fc..89788f8 100644
--- a/utils/world.ts
+++ b/utils/world.ts
@@ -12,8 +12,9 @@ interface Position {
}
export interface BotSprite extends Sprite {
- id: string;
+ botId: string;
color: string;
+ spawnId: string;
}
export interface WorldState {
@@ -43,7 +44,8 @@ const BOT_COLORS = [
];
export default class World {
- private bots: BotSprites = new Map();
+ private botSpawns: BotSprites = new Map();
+ private botIdToSpawnId: Map = new Map();
private food: Sprite[] = [];
private width: number;
private height: number;
@@ -75,7 +77,7 @@ export default class World {
const takenPositions: Set = new Set();
// to avoid computing distances between points when spawning new bots, assume that bots are square
- for (const bot of this.bots.values()) {
+ for (const bot of this.botSpawns.values()) {
const { x, y, radius } = bot;
const safeRadius = radius + this.minSpawnDistance + newBotRadius;
@@ -109,7 +111,11 @@ export default class World {
return availablePositions;
}
- addBot(id: string): BotSprite {
+ hasBot(botId: string) {
+ return this.botIdToSpawnId.has(botId);
+ }
+
+ addBot(botId: string): BotSprite {
const availablePositions = this.getAvailableBotPositions(this.newBotRadius);
if (availablePositions.length === 0) {
@@ -119,14 +125,25 @@ export default class World {
const randomPosition = getRandomElement(availablePositions);
const color = getRandomElement(BOT_COLORS);
- const newBot = { ...randomPosition, id, radius: this.newBotRadius, color };
- this.bots.set(id, newBot);
+ // spawnId is unique per bot live
+ // useful to track respawns on the UI
+ const spawnId = Math.random().toString(36).substring(5);
+
+ const newBot = {
+ ...randomPosition,
+ botId,
+ radius: this.newBotRadius,
+ color,
+ spawnId,
+ };
+ this.botSpawns.set(spawnId, newBot);
+ this.botIdToSpawnId.set(botId, spawnId);
return newBot;
}
getState(): WorldState {
return {
- bots: this.bots,
+ bots: this.botSpawns,
food: this.food,
width: this.width,
height: this.height,
@@ -146,10 +163,15 @@ export default class World {
return { x: newX, y: newY };
}
- moveBot(id: string, x: number, y: number) {
- const bot = this.bots.get(id);
+ moveBot(botId: string, x: number, y: number) {
+ const spawnId = this.botIdToSpawnId.get(botId);
+ if (!spawnId) {
+ throw new Error(`Bot with id ${botId} not found`);
+ }
+
+ const bot = this.botSpawns.get(spawnId);
if (!bot) {
- throw new Error(`Bot with id ${id} not found`);
+ throw new Error(`Bot with id ${botId} not found`);
}
const distance = World.distance(bot, { x, y });
@@ -164,28 +186,33 @@ export default class World {
bot.y = y;
let checkLimit = 5;
- while (this.checkCollisions(id) && --checkLimit) {
+ while (this.checkCollisions(botId) && --checkLimit) {
continue;
}
}
checkCollisions(botId: string) {
- const bot = this.bots.get(botId);
+ const spawnId = this.botIdToSpawnId.get(botId);
+ if (!spawnId) {
+ throw new Error(`Bot with id ${botId} not found`);
+ }
+
+ const bot = this.botSpawns.get(spawnId);
if (!bot) {
throw new Error(`Bot with id ${botId} not found`);
}
// check if bot eats other bots
- const bots = this.bots;
+ const botSpawns = this.botSpawns;
const botIdsToRemove: string[] = [];
- for (const [otherBotId, otherBot] of bots.entries()) {
- if (otherBot.id === botId) {
+ for (const [otherBotSpawnId, otherBot] of botSpawns.entries()) {
+ if (otherBotSpawnId === spawnId) {
continue;
}
const distance = World.distance(bot, otherBot);
if (distance < bot.radius && bot.radius > otherBot.radius) {
- botIdsToRemove.push(otherBotId);
+ botIdsToRemove.push(otherBotSpawnId);
}
}
@@ -201,13 +228,14 @@ export default class World {
}
// remove food and bots
- for (const botIdToRemove of botIdsToRemove) {
- const botToRemove = bots.get(botIdToRemove);
+ for (const botSpawnIdToRemove of botIdsToRemove) {
+ const botToRemove = botSpawns.get(botSpawnIdToRemove);
if (!botToRemove) {
continue;
}
bot.radius += botToRemove.radius;
- bots.delete(botIdToRemove);
+ this.botIdToSpawnId.delete(botToRemove.botId);
+ botSpawns.delete(botSpawnIdToRemove);
}
// sort in descending order to avoid index shifting when removing elements