Skip to content

Commit

Permalink
fix(engine): populate api.otherPlayers, ensure bots can eat other b…
Browse files Browse the repository at this point in the history
…ots, and fix crash that can sometimes happen when the food with the latest index is eaten (#12)

## How does this PR impact the user?

This PR fixes the following issues:
- [x] `api.otherPlayers` was always empty
- [x] bots weren't able to eat other bots
- [x] world collision computation can crash when the bot eats multiple
foods and on of them has the latest index

### Now – `api.otherPlayers` is correctly populated and the bots can eat
each other


https://github.com/move-fast-and-break-things/aibyss/assets/4187729/b7c8a438-b3a1-43ae-be11-c8df36e45797

### Before – `api.otherPlayers` is always empty, and the second bot is
stuck


https://github.com/move-fast-and-break-things/aibyss/assets/4187729/9725cc6b-46d4-4222-aeb2-c9a87aa9c0b3

## Description

- [x] move bot code preparation logic from `engine.ts` to
`prepareBotCode.ts`
- [x] fix bugs
- [x] add tests to prevent regressions

## Limitations

N/A

## Checklist

- [x] my PR is focused and contains one wholistic change
- [x] I have added screenshots or screen recordings to show the changes
  • Loading branch information
yurijmikhalevich authored Jul 9, 2024
1 parent b640f1b commit a0e9030
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 31 deletions.
32 changes: 6 additions & 26 deletions server/plugins/engine.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ivm from "isolated-vm";
import prepareBotCode from "../../utils/prepareBotCode";
import * as botUtils from "~/utils/botstore";
import World from "~/utils/world";

Expand Down Expand Up @@ -33,33 +34,12 @@ async function runBots({ bots, world, botApi }: RunBotArgs) {
}

for (const bot of Object.values(bots)) {
const { code } = bot;

const botObject = state.bots.get(bot.id);
if (!botObject) {
// TODO(yurij): add better logging
console.error(`Bot with id ${bot.id} not found in the world`);
continue;
}

const me = { x: botObject.x, y: botObject.y, radius: botObject.radius };
const otherPlayers = Object.values(state.bots)
.filter(b => b.id !== 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 }));

try {
const preparedCode = `
global._player = ${JSON.stringify(me)};
global._otherPlayers = ${JSON.stringify(otherPlayers)};
global._food = ${JSON.stringify(food)};
global._worldWidth = ${state.width};
global._worldHeight = ${state.height};
${code}
${botApi}
`;
const preparedCode = prepareBotCode({ bot, state, botApi });
if (!preparedCode) {
console.error(`Failed to prepare code for bot ${bot.id}`);
continue;
}

const actions = await runBot(preparedCode);
if (actions?.[0]?.type === "move") {
Expand Down
43 changes: 43 additions & 0 deletions utils/__snapshots__/prepareBotCode.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`prepares bot code correctly when there are three bots and some food in the world 1`] = `
"
global._player = {"x":0,"y":0,"radius":5};
global._otherPlayers = [{"x":10,"y":10,"radius":5},{"x":20,"y":20,"radius":5}];
global._food = [{"x":30,"y":30,"radius":5},{"x":40,"y":40,"radius":5}];
global._worldWidth = 100;
global._worldHeight = 100;
console.log('hello world');
console.log('hello bot api');
"
`;

exports[`prepares bot code correctly when there are two bots in the world 1`] = `
"
global._player = {"x":0,"y":0,"radius":5};
global._otherPlayers = [{"x":10,"y":10,"radius":5}];
global._food = [];
global._worldWidth = 100;
global._worldHeight = 100;
console.log('hello world');
console.log('hello bot api');
"
`;

exports[`prepares bot code correctly when there is one bot in the world 1`] = `
"
global._player = {"x":0,"y":0,"radius":5};
global._otherPlayers = [];
global._food = [];
global._worldWidth = 100;
global._worldHeight = 100;
console.log('hello world');
console.log('hello bot api');
"
`;
2 changes: 1 addition & 1 deletion utils/botstore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import EventEmitter from "node:events";

interface Bot {
export interface Bot {
id: string;
code: string;
}
Expand Down
69 changes: 69 additions & 0 deletions utils/prepareBotCode.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { it, expect } from "vitest";
import prepareBotCode from "./prepareBotCode";

it("prepares bot code correctly when there is one bot in the world", () => {
const bot = {
id: "1",
code: "console.log('hello world');",
};

const state = {
bots: new Map([["1", { x: 0, y: 0, radius: 5, id: "1", color: "#00FF00" }]]),
food: [],
width: 100,
height: 100,
};

const botApi = "console.log('hello bot api');";

const preparedCode = prepareBotCode({ bot, state, botApi });

expect(preparedCode).toMatchSnapshot();
});

it("prepares bot code correctly when there are two bots in the world", () => {
const bot = {
id: "1",
code: "console.log('hello 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" }],
]),
food: [],
width: 100,
height: 100,
};

const botApi = "console.log('hello bot api');";

const preparedCode = prepareBotCode({ bot, state, botApi });

expect(preparedCode).toMatchSnapshot();
});

it("prepares bot code correctly when there are three bots and some food in the world", () => {
const bot = {
id: "1",
code: "console.log('hello 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" }],
["3", { x: 20, y: 20, radius: 5, id: "3", color: "#00FF00" }],
]),
food: [{ x: 30, y: 30, radius: 5 }, { x: 40, y: 40, radius: 5 }],
width: 100,
height: 100,
};

const botApi = "console.log('hello bot api');";

const preparedCode = prepareBotCode({ bot, state, botApi });

expect(preparedCode).toMatchSnapshot();
});
39 changes: 39 additions & 0 deletions utils/prepareBotCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Bot } from "~/utils/botstore";
import type { WorldState } from "~/utils/world";

type PrepareBotCodeArgs = {
bot: Bot;
state: WorldState;
botApi: string;
};

export default function prepareBotCode({ bot, state, botApi }: PrepareBotCodeArgs): string | undefined {
const { code } = bot;

const botObject = state.bots.get(bot.id);
if (!botObject) {
// TODO(yurij): handle this better
console.error(`Bot with id ${bot.id} not found in the world`);
return;
}

const me = { x: botObject.x, y: botObject.y, radius: botObject.radius };
const otherPlayers = [...state.bots.values()]
.filter(b => b.id !== 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 }));

const preparedCode = `
global._player = ${JSON.stringify(me)};
global._otherPlayers = ${JSON.stringify(otherPlayers)};
global._food = ${JSON.stringify(food)};
global._worldWidth = ${state.width};
global._worldHeight = ${state.height};
${code}
${botApi}
`;

return preparedCode;
}
78 changes: 78 additions & 0 deletions utils/world.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { it, expect } from "vitest";
import World, { type BotSprite, type Sprite } from "./world";

it("correctly removes the smaller bot when the bigger bot eats it", () => {
const world = new World({ width: 100, height: 100 });
world.addBot("1");
world.addBot("2");

// @ts-expect-error - world.bots is private
const bot1 = world.bots.get("1") as BotSprite;
bot1.radius = 10;
bot1.x = 10;
bot1.y = 10;

// @ts-expect-error - world.bots is private
const bot2 = world.bots.get("2") as BotSprite;
bot2.radius = 5;
bot2.x = 10;
bot2.y = 10;

world.checkCollisions("1");

// @ts-expect-error - world.bots is private
expect(world.bots.has("1")).toBe(true);
// @ts-expect-error - world.bots is private
expect(world.bots.has("2")).toBe(false);
});

it("correctly removes food when the bot eats it", () => {
const world = new World({ width: 100, height: 100 });
world.addBot("1");

// @ts-expect-error - world.bots is private
const bot = world.bots.get("1") as BotSprite;
bot.x = 10;
bot.y = 10;

// @ts-expect-error - world.food is private
expect(world.food.length).toBe(100);

// @ts-expect-error - world.food is private
const food = world.food[0] as Sprite;
food.x = 10;
food.y = 10;

world.checkCollisions("1");

// @ts-expect-error - world.food is private
expect(world.food.length).toBeLessThan(100);
});

it("shouldn't crash when bot eats two food items and one of them with the last index", () => {
const world = new World({ width: 100, height: 100 });
world.addBot("1");

// @ts-expect-error - world.bots is private
const bot = world.bots.get("1") as BotSprite;
bot.x = 10;
bot.y = 10;

// @ts-expect-error - world.food is private
expect(world.food.length).toBe(100);

// @ts-expect-error - world.food is private
const food1 = world.food[0] as Sprite;
food1.x = 10;
food1.y = 10;

// @ts-expect-error - world.food is private
const food2 = world.food[99] as Sprite;
food2.x = 10;
food2.y = 10;

world.checkCollisions("1");

// @ts-expect-error - world.food is private
expect(world.food.length).toBeLessThan(100);
});
17 changes: 13 additions & 4 deletions utils/world.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import getRandomElement from "./getRandomElement";

interface Sprite {
export interface Sprite {
x: number;
y: number;
radius: number;
Expand All @@ -16,6 +16,13 @@ export interface BotSprite extends Sprite {
color: string;
}

export interface WorldState {
bots: BotSprites;
food: Sprite[];
width: number;
height: number;
}

type BotSprites = Map<string, BotSprite>;

type WorldArgs = {
Expand Down Expand Up @@ -117,7 +124,7 @@ export default class World {
return newBot;
}

getState() {
getState(): WorldState {
return {
bots: this.bots,
food: this.food,
Expand Down Expand Up @@ -171,7 +178,7 @@ export default class World {
// check if bot eats other bots
const bots = this.bots;
const botIdsToRemove: string[] = [];
for (const [otherBotId, otherBot] of Object.entries(bots)) {
for (const [otherBotId, otherBot] of bots.entries()) {
if (otherBot.id === botId) {
continue;
}
Expand Down Expand Up @@ -203,8 +210,10 @@ export default class World {
bots.delete(botIdToRemove);
}

// sort in descending order to avoid index shifting when removing elements
foodIdxToRemove.sort((a, b) => b - a);
for (const foodIdx of foodIdxToRemove) {
// it's safe to cast to Sprite because know that `foodIdx` is within the bounds of the array
// it's safe to cast to Sprite because we know that `foodIdx` is within the bounds of the array
bot.radius += (food[foodIdx] as Sprite).radius;
food.splice(foodIdx, 1);
}
Expand Down

0 comments on commit a0e9030

Please sign in to comment.