Skip to content

Commit

Permalink
feat: make bot movements smooth
Browse files Browse the repository at this point in the history
  • Loading branch information
yurijmikhalevich committed Jul 29, 2024
1 parent 1278b08 commit 2acf103
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 65 deletions.
144 changes: 110 additions & 34 deletions components/GameScreen.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
<script setup lang="ts">
// create canvas ref
const canvas = ref<HTMLCanvasElement | null>(null);
const context = ref<CanvasRenderingContext2D | null>(null);
import { Application, Graphics } from "pixi.js";
const { data: gameState, refresh } = await useFetch("/api/state");
const refreshIntervalMs = 1000;
const { data: gameState, refresh } = await useFetch("/api/state");
const intervalRef = ref<number | null>(null);
onMounted(async () => {
const ctx = canvas.value?.getContext("2d", { alpha: false });
if (!ctx) {
window.alert("Can't render the game. Please, refresh the page. If the problem persists, report the issue at https://github.com/move-fast-and-break-things/aibyss/issues. Include as many details as possible.");
return;
}
context.value = ctx;
intervalRef.value = window.setInterval(refresh, 1000);
intervalRef.value = window.setInterval(refresh, refreshIntervalMs);
});
onBeforeUnmount(() => {
Expand All @@ -23,38 +16,121 @@ onBeforeUnmount(() => {
}
});
watch(gameState, (newState) => {
if (!canvas.value || !newState) {
const canvas = ref<HTMLCanvasElement | null>(null);
const appRef = ref<Application | null>(null);
const foodRef = ref<{ x: number; y: number; graphics: Graphics }[]>([]);
const botSpawnsRef = ref<Record<string, Graphics>>({});
const tickFnRef = ref<() => void>();
watch(gameState, async (newState, prevState) => {
if (!canvas.value) {
window.alert("Can't render the game. Please, refresh the page. If the problem persists, report the issue at https://github.com/move-fast-and-break-things/aibyss/issues. Include as many details as possible.");
return;
}
canvas.value.width = newState.width;
canvas.value.height = newState.height;
if (!prevState || !newState) {
return;
}
const ctx = canvas.value.getContext("2d", { alpha: false });
if (!ctx) {
window.alert("Can't render the game. Please, refresh the page. If the problem persists, report the issue at");
throw new Error("Can't render the game");
if (tickFnRef.value) {
appRef.value?.ticker.remove(tickFnRef.value);
}
context.value = ctx;
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, canvas.value.width, canvas.value.height);
if (!appRef.value || appRef.value.renderer.width !== prevState.width || appRef.value.renderer.height !== prevState.height) {
appRef.value?.destroy();
for (const food of newState.food) {
ctx.fillStyle = "#FF0000";
ctx.beginPath();
ctx.arc(food.x, food.y, food.radius, 0, 2 * Math.PI);
ctx.fill();
}
const app = new Application();
appRef.value = app;
await app.init({
width: prevState.width,
height: prevState.height,
canvas: canvas.value,
backgroundColor: "#FFFFFF",
antialias: true,
resolution: 1,
});
// render food
for (const food of prevState.food) {
const graphics = new Graphics();
graphics.circle(food.x, food.y, food.radius);
graphics.fill("#FF0000");
app.stage.addChild(graphics);
foodRef.value.push({ x: food.x, y: food.y, graphics });
}
// render bots
for (const bot of Object.values(prevState.bots)) {
const graphics = new Graphics();
app.stage.addChild(graphics);
graphics.circle(bot.x, bot.y, bot.radius);
graphics.fill(bot.color);
botSpawnsRef.value[bot.spawnId] = graphics;
}
} else {
// remove eaten food
for (const food of foodRef.value) {
if (!prevState.food.find(f => f.x === food.x && f.y === food.y)) {
// @ts-expect-error - food.graphics has some weird type
appRef.value.stage.removeChild(food.graphics);
}
}
// remove eaten bots
for (const spawnId of Object.keys(botSpawnsRef.value)) {
const existingBotGraphics = botSpawnsRef.value[spawnId];
if (existingBotGraphics && !newState.bots[spawnId]) {
appRef.value.stage.removeChild(existingBotGraphics);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete botSpawnsRef.value[spawnId];
}
}
// move or add bots
for (const bot of Object.values(prevState.bots)) {
if (!newState.bots[bot.spawnId]) {
continue;
}
for (const bot of Object.values(newState.bots)) {
ctx.fillStyle = bot.color;
ctx.beginPath();
ctx.arc(bot.x, bot.y, bot.radius, 0, 2 * Math.PI);
ctx.fill();
const existingBot = botSpawnsRef.value[bot.spawnId];
if (existingBot) {
existingBot.clear();
existingBot.circle(bot.x, bot.y, bot.radius);
existingBot.fill(bot.color);
} else {
const graphics = new Graphics();
appRef.value.stage.addChild(graphics);
graphics.circle(bot.x, bot.y, bot.radius);
graphics.fill(bot.color);
botSpawnsRef.value[bot.spawnId] = graphics;
}
}
}
// slowly move the bots from prevState to newState during the refresh interval
const updateTime = Date.now();
tickFnRef.value = () => {
const now = Date.now();
const progress = (now - updateTime) / refreshIntervalMs;
for (const bot of Object.values(newState.bots)) {
const existingBot = botSpawnsRef.value[bot.spawnId];
const prevBot = prevState.bots[bot.spawnId];
if (existingBot && prevBot) {
const x = prevBot.x + (bot.x - prevBot.x) * progress;
const y = prevBot.y + (bot.y - prevBot.y) * progress;
existingBot.clear();
existingBot.circle(x, y, prevBot.radius);
existingBot.fill(bot.color);
}
}
};
appRef.value.ticker.add(tickFnRef.value);
});
</script>

Expand Down
63 changes: 61 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
5 changes: 3 additions & 2 deletions server/plugins/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand All @@ -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);
}

Expand All @@ -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);
}
}
Expand Down
12 changes: 6 additions & 6 deletions utils/prepareBotCode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions utils/prepareBotCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
Expand All @@ -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 }));

Expand Down
Loading

0 comments on commit 2acf103

Please sign in to comment.