Skip to content

Auto Play: column and bottom-row targeting - #34

Merged
jpcottin merged 2 commits into
mainfrom
feat/autoplay-column-targeting
Sep 27, 2026
Merged

jpcottin merged 2 commits into
mainfrom
feat/autoplay-column-targeting

Conversation

@jpcottin

Copy link
Copy Markdown
Owner

The follow-up left open in #13: which column the autopilot shoots. Measuring first turned up something bigger than expected — on a portrait phone the autopilot lost three campaigns in four, and not to bombs.

What was wrong

A host build of game.cpp played 500 seeded runs per level with the autopilot on, and logged the second before every life lost.

  • On a portrait phone (1080x2400) the old AI cleared 64% of levels and won 24% of campaigns. From level 5 up it lost at least one life per level.
  • Nearly every hit was a collision with an invader, not a bomb (level 7: 168 of 171 hits).
  • The cause is the screen shape. A full wave is 0.69 wide on a 0.87-wide field, so it bounces, and steps down, about once a second. At level 7 the bottom row is level with the ship after about 10 s, and 40 invaders at one volley per 0.42 s take 17 s.
  • Shooting the nearest column leaves the wave's width untouched until the very end.
  • On wide screens (unfolded Fold, glasses, landscape) the old AI already won every campaign.

What changed

Commit 1 — targeting. pickAlienTarget() chooses the column:

Situation Choice
The wave is no threat Nearest column, as before
The wave would reach the ship before it can be cleared An edge column: the one with fewer aliens left, the nearer one on a tie. A narrower wave crosses more screen per step down
The bottom row is about to come level with the ship That row's aliens, nearest first. This also outranks the saucer
Level 10 The escort first, then the mothership. She takes 16 hits and her escort was marching into the ship meanwhile

The aim point is now computed on the bottom-most alien of a column, the only one a laser can reach.

Commit 2 — dead time. Found in the traces while measuring commit 1, kept separate so it can be dropped:

  • The autopilot parked under a falling power-up for 2 to 3 s without firing. It now leaves for the pickup only when it is about to land.
  • It fired only when lined up on its chosen target. It now also fires at any column overhead on the way there or during a dodge.

Results

300 seeded runs per level and 300 campaigns per row. "Clear time" is the sum of the ten per-level mean clear times.

Screen AI Levels cleared Campaigns won Clear time Lives lost per 10 levels
1080x2400 portrait old 64.5% 24.3% 133 s 11.71
commit 1 98.2% 95.3% 160 s 1.02
commit 2 99.2% 99.0% 146 s 0.70
1080x2640 narrow portrait old 36.4% 0.0% 19.69
commit 2 76.2% 57.3% 7.70
1600x2560 tablet old 99.5% 98.7% 171 s 0.43
commit 2 100% 100% 165 s 0.01
2076x2152 unfolded Fold old 100% 100% 194 s 0.00
commit 2 100% 100% 177 s 0.00
450x394 glasses old 100% 100% 205 s 0.00
commit 2 100% 100% 183 s 0.00
2400x1080 landscape old 100% 100% 267 s 0.00
commit 2 100% 100% 217 s 0.00

The old portrait clear time is lower only because it counts cleared levels, and the old AI cleared the easy ones.

Things measured and rejected:

  • Bottom row first, always: worse than the old AI (7% of campaigns). Every kill speeds the wave up and none of them narrows it.
  • Edge column first, always: same survival on portrait, but 25% slower on wide screens, where the ship keeps trekking back to a far edge.
  • Wider safety margins for the bottom-row rule: lose more games, they give up on narrowing too early. The shipped margins sit in the middle of a broad plateau, checked on a second seed set.
  • Skipping the saucer under pressure: no measurable gain, and it costs score.

Trade-off to know

Commit 2 makes levels end sooner, so fewer saucers cross: the mean campaign score on portrait is 45.0k against 45.6k with commit 1 alone.

Verification

  • 114 native tests pass on the MP36 emulator (arm64, API 36), up from 105.
  • Nine new tests. The eight that assert new behaviour were each verified to fail against the old AI; TakesTheNearestColumnWhenTheWaveIsNoThreat passes on both and guards against over-applying the edge rule.
  • AutoPlay.HoldsThePortraitLineAtLevel6 plays eight full portrait waves end to end. The old AI loses three of the eight.
  • AutoPlay.SteersTowardACollectiblePowerUp now drops its power-up closer to the ship, since a high one is no longer chased at once.
  • assembleDebug and assembleRelease build clean. NDK clang-tidy reports nothing in game.cpp.
  • On MP36 in the real app, Auto Play won a full campaign with 46,345 points. No warnings in logcat.

Not changed

  • VulkanAsteroids: there is no marching wave there, so nothing to port.
  • Dodging and steering are untouched.

On a portrait phone a full wave has almost no room to march: it bounces,
and steps down, about once a second. The autopilot shot whichever column
was nearest, which leaves the wave's width untouched until the end, so
from level 5 up the wave reached the ship before it could be cleared.
Nearly every life lost was a collision with an invader, not a bomb.

pickAlienTarget() now chooses the column:
- nearest column while the wave is no threat (unchanged behaviour, and
  the fastest clear on wide screens);
- an edge column when the wave would reach the ship before it can be
  cleared: a narrower wave crosses more screen per step down. The edge
  column with fewer aliens left goes first, the nearer one on a tie;
- the bottom row when it is about to come level with the ship, where it
  can no longer be shot. This also outranks the saucer.

The aim point is now computed on the bottom-most alien of a column, the
only one a laser can reach.

At level 10 the escort is shot before the mothership: she takes 16 hits,
and her escort was marching into the ship in the meantime.

Portrait 1080x2400, 500 seeded runs per level, host build of game.cpp:
levels cleared 64% -> 98%, full campaigns won 24% -> 96%.
Two sources of dead time found while measuring the targeting change:

- The autopilot steered under a power-up the moment it dropped and
  parked there, not firing, for the two to three seconds of its fall.
  It now leaves for the pickup only when it is about to land.
- It fired only when lined up on its chosen target. It now also fires
  at any column that is overhead on the way there or during a dodge.

Portrait 1080x2400, 500 seeded runs per level: levels 8 and 9 cleared
97% / 84% -> 99% / 93%, full campaigns won 96% -> 99%, and a campaign
takes 129 s instead of 143 s. Wide screens clear 8 to 20% faster.

README: describe the autopilot's column choice, test count 103 -> 114.
@jpcottin
jpcottin merged commit 976140b into main Sep 27, 2026
14 checks passed
@jpcottin
jpcottin deleted the feat/autoplay-column-targeting branch September 27, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant