Chong
Entry II  ·  Eskiv  ·  filed under games · AI
№ 02 · Entry II

A brute-force AI that plays a dodger.

Eskiv is a 2016 game: you move a square, dodge balls, grab a new square each tick — every pickup spawns another ball, so the field fills up until the agent can’t find a safe path home. Ten thousand games, one brute-force agent with lookahead=100, three plates of how it behaves.

pythonpygamebrute-force searchMMXVI — ongoing
§ I — The game

What you’re watching

FIG. 0 — One game, played at speed
A single 300-second run from the brute-force agent.

Grey square is the player. Blue circles are enemies — they bounce, accelerate, and there are more of them every second. Light grey squares are pickups worth one point each. The agent searches over reachable pickup targets and picks the one whose safe-neighbourhood survives the next hundred ticks of simulation. When no such target exists, the game ends.

◆  § II  ◆
§ II — The setup

10,000 games, three questions

The engine was rewritten headless — vectorised enemies, no pygame in the hot loop — so ten thousand 5-minute games finish in an afternoon. Every frame of every game is saved. Three questions, chosen because I was curious and the answers weren’t obvious from watching one game:

  1. Does the agent move in straight lines? How close to the shortest route does it actually walk, and does that change as the field gets crowded?
  2. Where does it spend its time? Does a brute-force agent with no notion of “danger zones” still discover them empirically?
  3. How does it die? Is it mostly clipped by fast enemies, or does it trap itself in corners when the field fills up?
§ III — Figure I

Path ratio vs clutter

Straight-line distance divided by actual path walked, per pickup. 1.0 means the agent walked a straight line. Bucketed by how many balls were on the field at the time.

FIG. I — 742,146 pickups across 10,000 games
Mean ratio (line), interquartile range (band), sample count (bars).

With an empty field the agent walks at ~76% of the straight line — not 100% because it’s already dodging a few enemies. By the time there are 60+ balls it’s down to 40%, and at 100+ it’s taking routes more than four times longer than the crow flies. The fall-off is smooth and roughly linear from ~20 balls onward. Not surprising, but this is the shape of the concession.

§ IV — Figure II

Where the agent stands

Player-position density by score bucket. Log-smoothed, 16.4 million samples. Auto-scrubs through the game’s life.

FIG. II — 12 score buckets, smoothed density
Top-left is the top-left corner of the game field. Watch it crawl to the walls.

Up to about 200 points the agent hugs the centre — maximum optionality, minimum wall risk. Somewhere between 300 and 400 it flips: the centre becomes so ball-dense that the only safe pockets are against the edges. By the time scores hit 500 it’s spending most of its steps pinned to a single wall. It’s not a strategy the agent was told about — it’s where an agent that only minimises immediate risk ends up.

§ V — Figure III

How it dies

Final score per game. Brute agent, lookahead=100, small field.

FIG. III — 10,000 games · mean 381.1 · median 385
Every death was by trap; zero deaths by direct hit.

The distribution is remarkably tight — stddev 58, p10–p90 range of ~310–445. More striking: not a single game ended by getting hit. Every death was a trap: the search raised DeathError because no target had a safe enough 100-step future. The agent dodges perfectly right up until it corners itself — which is a very specific failure mode of conservative search.

§ VI — The rough edges

Open questions

MaterialsPython, NumPy, SciPy, pygame
VintageMMXVI — analysis MMXXVI
Scale10,000 games · 16.4M steps · 742k pickups
Sourcegithub/xnmp/Eskiv_new