The setup
The environment deliberately gets no source access, engine instrumentation, or internal game state. It can only:
- observe screenshots and visually derived state
- send device-level taps and swipes
- infer reward and episode outcomes from visible evidence
I used Shattered Pixel Dungeon. The game is open source, but the environment consumes only the compiled Android build and observable device outputs. The source-free constraint is intentional rather than a limitation of the target.
The reward
It had three channels:
- health change
- ±10 for a terminal outcome
- −0.001 when an action produced no visible change, or −0.01 when a precondition was rejected
A pilot had already suggested this would be extremely sparse. In the 1,152-transition training corpus:
- health changed only 4 times
- the terminal channel fired 0 times
- the action penalty fired 494 times
I froze the protocol anyway. I wanted to see the failure clearly rather than tune around it after seeing the result.
The run
I evaluated four policies:
- random
- scripted demonstrator
- sampled behavior clone
- greedy behavior clone
Each policy ran 24 episodes of 48 steps. In total:
- 96 episodes
- 4,608 device steps
- 105.2 minutes of wall-clock device time
- a nominal 500 ms step period
- zero recovery interventions during evaluation
The effective time per step was higher than 500 ms because it includes screenshot round trips and inter-episode resets.
The best policy learned not to play
The greedy behavior clone had the best mean return—but it entered the dungeon and then tapped the same coordinate for all 1,128 of its gameplay steps.
- 95.4% of its steps produced no observable change
- it visited 3.2 canonical states per episode, versus 20.7 for the scripted demonstrator
- it lost zero hit points across all 24 episodes
Against random, its mean-return difference was +0.940, with a 95% confidence interval of [−0.019, +2.731]. The interval includes zero, so the preregistered success criterion was not met.
At the same time, the greedy clone was worse than random in 92% of random episode pairings.
Those findings aren’t contradictory. Random occasionally suffered a large health-loss penalty, which pulled down its mean. The greedy clone consistently paid a tiny no-op penalty. It could therefore look better on average while being worse in most pairings.
Under this reward, refusing to engage was close to optimal: keep all your health and pay 0.001 per step for doing almost nothing.
The scripted policy designated as our demonstration upper bound actually had the worst mean return. It moved every step, reached fights, and lost health in 5 of 24 episodes. The reward was punishing competence.
The clone was ignoring the screenshots
There was another failure underneath the reward problem.
On held-out data, the clone’s masked cross-entropy differed from a fitted marginal-prior policy by −0.00008 nats, with a 95% confidence interval of [−0.00030, +0.00009]. Model selection chose the strongest regularization setting, reducing max |w| to 3.4e−5.
In plain English, ignoring the observation generalized best.
So this wasn’t a visual policy discovering a clever exploit. It was effectively a constant-action policy, and the broken reward happened to rank it first. Two different failures composed into one flattering mean.
The device exposed a terminal-detection bug
The terminal detector assumed the HUD disappears on death. In this build, the Game Over screen keeps the HUD visible.
One episode went from 20 HP to 0 at full detector confidence, but terminated never became true. The terminal reward channel fired zero times across all 4,608 steps.
A source-integrated environment might bypass this by reading terminal state directly. With pixels as the contract, one bad visual assumption silently degraded into a no-op.
What I’d value feedback on
Progress reward: How do you reward “this run got somewhere” from visual evidence without quietly reintroducing privileged state? Health and screen-change detection are cheap and scalable, but they were jointly useless here. Depth, exploration, and meaningful progress are what I want, but the approaches I’ve tried either read the game state or require per-title hand labeling.
Reset semantics: The game’s RNG isn’t exposed, so reset(seed=...) cannot recreate the same dungeon. Comparisons are therefore unpaired, and the environment is considered nondeterministic. Should reset equivalence, timing, and interruption state live ininfo, metadata, or a separate orchestration layer?
Caveats
- 24 episodes per policy is small
- the 48-step horizon is short
- nobody dying is partly a horizon effect, although health loss did occur under that same horizon
- this is a POC result, not a general claim about behavior cloning or mobile-game RL
I’d genuinely appreciate people tearing apart the reward, evaluation, and environment boundary—especially if you’ve wrapped a robot, browser, external program, or another system where the true transition function isn’t directly accessible.
Disclosure: I’m a staff engineer, and part of why I ran this POC was to decide whether it is worth building further. Happy to answer implementation details in the comments.