decompilation-klonoa-lvl1

API Endpoint
Leaderboard
Loading leaderboard...
README

Decompilation — Klonoa, level 1

Matching decompilation as an OpenReward environment.

Level 1 is 60 functions of at most 2,048 bytes each. The level is the
difficulty, and it is the only thing that distinguishes this environment from
its siblings: same machinery, same game, a harder draw. Raising the size cap is
what makes a higher level, since the cap is what decides whether a task's score
sits in work a model can reach.

A task hands the agent a real, complete decompilation project with some function
bodies removed. The agent writes C; the environment compiles it with the game's
original toolchain and diffs the objects against the ones the untouched project
produces. The task is solved when every object matches byte for byte — not
equivalent code, identical code.

The verifier is a compiler and a differ, so there is no judge and no rubric.

Games

gameplatformcompilerupstreamfunctions
Klonoa: Empire of DreamsGBAagbcc (gcc 2.95 + Nintendo's patches)testyourmine/kleod (MIT)457

More projects are added as sibling entries in decompilation/games.py and
games/<slug>/; they share this repo and this environment.

The task

Decompile 60 functions of the game — that is what level 1 means. The other ~400 stay in place as gold: they
are the only worked examples of the codebase's idioms, and reading the
neighbours of what you are writing is the first thing a real contributor does.

There are 100 tasks, differing in which functions they remove. Each is a seeded
draw, stratified by compiled size and held to a byte budget, so tasks are
comparable as work while overlapping in only ~14% of their functions:

functions per task60
function size cap2,048 bytes
task size24,301 ± 536 bytes (2.2% spread)
overlap between two tasks8.5 functions (14.2%)

The size distribution of a typical task, and where its score lives:

size band fns/task share of task bytes 0-32 7.38 0.6% 32-64 8.68 1.6% 64-128 9.42 3.5% 128-256 10.96 8.7% 256-512 7.33 10.7% 512-1024 7.53 23.1% 1024-2048 8.70 51.8%

Two things follow from that shape. Reward is byte-weighted, so the 36 functions
under 512 bytes are 61% of a task by count and 14% of its score, while the 8.7
functions of 1-2 KB are 52% of the score. And the cap matters: without it two or
three functions of 4-28 KB carry half a task, the score sits entirely in work far
beyond what a model completes today, and the reward goes flat long before the
time does. 64% of an uncapped task's score is below 2 KB, so the cap costs little
difficulty and keeps the frontier inside the task rather than behind a wall.

Every knob is an environment variable — DECOMP_FUNCTIONS_PER_TASK,
DECOMP_FUNCTION_SIZE_CAP, DECOMP_TASK_COUNT, DECOMP_REMOVED_FRACTION (used
only when DECOMP_FUNCTIONS_PER_TASK is 0) — so difficulty is a deployment
setting rather than a rebuild.

Measured

glm-5.3-flash, four agents under AgentOrg, against an earlier 46-function draw:
about 30 minutes of orientation, then roughly 16 functions and 1,250 bytes of
byte-exact code per hour. Completions average 78 bytes, so the model is closing
the small end and has not yet broken into the 512-1024 band. On the task above
that projects to a reward around 0.15-0.25 over four hours, still climbing at the
end rather than saturating.

Reward

The fraction of the remaining gap closed:

(matched_code_percent - baseline) / (100 - baseline)

with 0.15 weight on objdiff's fuzzy_match_percent for shaping, so a function
taken from 0% to 85% is not invisible.

The best is tracked per function, and the score is the byte-weighted sum of
those. A project-wide reading is a sample of whatever state the tree happened to
be in when make ran, and with several agents editing several files some file
is almost always half-written — the project figure swings by thousands of bytes
independently of real progress, so a project-wide running maximum ends up
rewarding the luckiest sample. Per function, a match that was once achieved
stays credited, and a half-written file cannot erase a finished function
elsewhere. solved still reflects the real simultaneous state of the tree, not
the per-function bests.

It is emitted as increments over a running best, not as one terminal number. A
run almost always ends on the harness's wall clock rather than by calling
finish, and a terminal-only reward scores such a run at nothing no matter what
it recovered. Emitting the increment instead means the stream sums to the best
ever reached. A step that graded nothing emits None and adds no row; a step
that graded without improving emits 0.0, so a run that earns nothing still
scores zero rather than coming back unscored. Every tool result carries the
run's numbers in its metadata, terminal or not, for the same reason.

solved additionally requires every object to compare equal after
--strip-debug, and the data sections to match too. objdiff's percentages are
the shaped signal; the byte compare is the verifier - it also covers the
inter-function alignment padding objdiff attributes to no symbol.

No ROM

baserom.gba is referenced only from the projects' data assembly. The C
translation units build without it, so tasks, targets and grading never touch
the copyrighted image. The cost is that the final ROM link is out of scope: the
win condition is "every object matches" rather than the ROM SHA-1. Those are
equivalent as long as a solution only edits C, which the guards below enforce.

Guards

  • Edit surface. Every file is hashed when the task is built. Anything
    modified, added or deleted outside the project's C sources scores 0 — the
    linker script, the hand-written assembly and the data objects feed the link
    too.
  • Inline assembly. Scanned on the lines a solution adds, diffed against
    the task's starting state: upstream's own asm("swi 0x2A") is legitimate, an
    added one is not. register x asm("rN") stays allowed — it constrains
    register allocation without emitting instructions. A macro or token pasting
    hides an asm statement from a line scan, so every build is also checked
    after preprocessing: a code-emitting asm statement or a naked attribute
    that upstream's tree does not have (the same template in the same function)
    rejects the build. agbcc marks
    no inline asm in the assembly it writes, which is why the check reads the
    preprocessed source rather than the compiler's output. An asm statement with
    an empty template, such as the barrier asm("" : "+r"(x)), emits no
    instruction and is allowed by both checks.
  • Executable sections. A data object placed in .text pairs its bytes with
    a target function of the same name, so machine code can be smuggled in as a C
    array. The preprocessed scan also rejects a section(".text…") attribute the
    upstream tree does not have; only .text is executable, so data in upstream's
    own sections (.bss.code, common_data, iwram_data, …) stays allowed.
  • Build limits. Every build runs in its own process group under a wall-clock
    timeout and memory, output-size and CPU limits, so an infinite preprocessor
    include or a runaway allocation cannot hang the server or fill its disk. A
    build that exceeds them reads as a failed build the agent can retry.
  • Reference solutions. The pristine trees live at /opt/pristine, outside
    the work area. No tool can read them, and neither can the build: the
    references are root-only, and each session's tree is built by a uid of its
    own, so an #include or INCBIN of /opt/pristine, /opt/target or another
    session's tree fails.

Tools

list_targets, read_file, edit_file, build, diff, progress, finish.
diff renders the target and the current assembly side by side, which is the
environment's real feedback channel.

Resources

CPU only. agbcc, GNU as and objdiff are all single-threaded C; builds run at
make -j4. The image carries the pristine objects, so a session starts from a
built tree and make only recompiles the translation units a task's stubs
touch. Measured at 4 CPUs / 16 GB: session setup 2.9s, the build tool 1.5s,
the diff tool 0.1s. A full 22-object rebuild from cold is about 7 seconds.
Rollouts are token-bound, not build-bound.

Set the deployment's concurrency low — around 4. Unlike most environments every
session runs real compiles, so the default of 500 concurrent requests against
4 CPUs would thrash; scale with instances instead.

Running locally

docker build -t decompilation .
docker run -p 8080:8080 decompilation

Point a harness at it with OPENREWARD_SESSION_URL=http://localhost:8080.

Licence

Upstream kleod is MIT (testyourmine) — see games/klonoa/kleod/LICENSE and
games/klonoa/UPSTREAM_COMMIT.

GeneralReasoning/decompilation-klonoa-lvl1 | OpenReward