Projecthackathon

Built at JEVATHON (w/ The AI Collective)

Reinforcement Learning for Enemy ai

a 2D platformer where every decision and every physics tick goes through jev, a physics/AI backend (currently a local mock, swappable for the real service). You have 30 seconds to reach the flag as many times as possible. Deaths respawn you — the clock doesn't stop. High score persists across sessions. Everything runs through jev: - step — all physics: player, bullets, monsters (gravity, velocity, AABB collision) - aim — two turrets that study your recent movement history and adapt after misses (leading runners, ambushing campers, shooting landing spots of jumpers) - decide — two flying monsters that dash every second in 2D, intercepting where your velocity says you'll be - reset — wipes all learned state on a new game (N); respawns keep the memory — the enemies keep learning mid-run Controls: ←→/A/D move · Space jump · R respawn · N new game How the game talks to jev The context stream — history Every frame, game.js appends one sample: ```js { t: 12.483, x: 340.2, y: 464, vx: 320, vy: 0 } ``` It's a rolling 12-second ring buffer (HISTORY_SECS), ~720 samples of your position and velocity — the raw material for every enemy decision. Nothing else about you goes over the wire; habits are computed from this stream, on jev's side. The three decision channels (all async, all JSON request/response): ``` game.js jev ───────────────────────────────────────────────────────── step({ body, solids, dt }) → { x, y, vx, vy, onGround, touchingWall, stats } aim({ id, muzzle, history, now, { fire, dir, target, mode } report }) decide({ id, offset, pos, → { action:'dash'|'idle', history, now }) dir:{x,y}, vx, vy } reset({}) → {} ``` step — physics. The game never moves anything itself. Player, both monsters, and every bullet are each submitted as {x, y, w, h, vx, vy, noGravity?} plus the level's solid rects and dt. jev returns resolved positions and contact flags; the game applies them verbatim. noGravity: true on a body = flight (monsters, bullets share the same solver, gravity skipped). aim — the shooters. Per frame, per turret: the request carries the turret's id and muzzle, the full history buffer, now, and report — the outcome of its last shot ({hit: true|false}). That's the feedback loop: jev answers fire: false while its 2 s cooldown runs, or {fire: true, dir, target, mode} where mode names the tactic it picked (lead runner-prediction, zone bombardment of your favorite territory, anti-jump at your landing spot). Each turret has an independent cooldown by id, but hit/miss learning is shared — what one learns, both use. decide — the dashers. Per frame, per monster: id + offset give each its own 1-second cadence, phase-staggered (m1 is offset 0.5 s so they don't dash in lockstep). The request has the monster's pos and the same history. jev reads the last ~1 s of samples, averages your vx/vy, and returns a normalized 2D dir plus the impulse — it intercepts where you're heading, including vertically (swoops up at jumpers, dives at fallers). idle between pulses; the game dampens velocity 0.94×/frame. The loop in one breath: the game is a sensor array and a renderer — it records what you do, asks jev what everything should do next, and draws the verdict. The mock computes the brains locally; set endpoint in jev.config.js and the identical JSON POSTs to {endpoint}/step|aim|decide|reset. The tactic word in the HUD — zone, lead, or anti-jump changes based on what you just did (standing still → zone, running → lead, jumping → anti-jump) - Shots get more accurate over a run — every miss feeds back to jev, which adjusts where the next bullet aims; the target coordinates in the HUD shift after each miss - N resets it — new game wipes the memory and the enemies go back to firing dumb straight shots That's the whole story: the turret's aim line in the HUD is literally jev telling you what it learned.

View source
// latest_project_recording.mp4
// Project brief

a 2D platformer where every decision and every physics tick goes through jev, a physics/AI backend (currently a local mock, swappable for the real service). You have 30 seconds to reach the flag as many times as possible. Deaths respawn you — the clock doesn't stop. High score persists across sessions. Everything runs through jev: - step — all physics: player, bullets, monsters (gravity, velocity, AABB collision) - aim — two turrets that study your recent movement history and adapt after misses (leading runners, ambushing campers, shooting landing spots of jumpers) - decide — two flying monsters that dash every second in 2D, intercepting where your velocity says you'll be - reset — wipes all learned state on a new game (N); respawns keep the memory — the enemies keep learning mid-run Controls: ←→/A/D move · Space jump · R respawn · N new game How the game talks to jev The context stream — history Every frame, game.js appends one sample: ```js { t: 12.483, x: 340.2, y: 464, vx: 320, vy: 0 } ``` It's a rolling 12-second ring buffer (HISTORY_SECS), ~720 samples of your position and velocity — the raw material for every enemy decision. Nothing else about you goes over the wire; habits are computed from this stream, on jev's side. The three decision channels (all async, all JSON request/response): ``` game.js jev ───────────────────────────────────────────────────────── step({ body, solids, dt }) → { x, y, vx, vy, onGround, touchingWall, stats } aim({ id, muzzle, history, now, { fire, dir, target, mode } report }) decide({ id, offset, pos, → { action:'dash'|'idle', history, now }) dir:{x,y}, vx, vy } reset({}) → {} ``` step — physics. The game never moves anything itself. Player, both monsters, and every bullet are each submitted as {x, y, w, h, vx, vy, noGravity?} plus the level's solid rects and dt. jev returns resolved positions and contact flags; the game applies them verbatim. noGravity: true on a body = flight (monsters, bullets share the same solver, gravity skipped). aim — the shooters. Per frame, per turret: the request carries the turret's id and muzzle, the full history buffer, now, and report — the outcome of its last shot ({hit: true|false}). That's the feedback loop: jev answers fire: false while its 2 s cooldown runs, or {fire: true, dir, target, mode} where mode names the tactic it picked (lead runner-prediction, zone bombardment of your favorite territory, anti-jump at your landing spot). Each turret has an independent cooldown by id, but hit/miss learning is shared — what one learns, both use. decide — the dashers. Per frame, per monster: id + offset give each its own 1-second cadence, phase-staggered (m1 is offset 0.5 s so they don't dash in lockstep). The request has the monster's pos and the same history. jev reads the last ~1 s of samples, averages your vx/vy, and returns a normalized 2D dir plus the impulse — it intercepts where you're heading, including vertically (swoops up at jumpers, dives at fallers). idle between pulses; the game dampens velocity 0.94×/frame. The loop in one breath: the game is a sensor array and a renderer — it records what you do, asks jev what everything should do next, and draws the verdict. The mock computes the brains locally; set endpoint in jev.config.js and the identical JSON POSTs to {endpoint}/step|aim|decide|reset. The tactic word in the HUD — zone, lead, or anti-jump changes based on what you just did (standing still → zone, running → lead, jumping → anti-jump) - Shots get more accurate over a run — every miss feeds back to jev, which adjusts where the next bullet aims; the target coordinates in the HUD shift after each miss - N resets it — new game wipes the memory and the enemies go back to firing dumb straight shots That's the whole story: the turret's aim line in the HUD is literally jev telling you what it learned.

// Built with
// Watch & explore
// More from this event

Keep exploring what builders shipped.

All projects →