laya-frogger

Laya fine-tuned to play Frogger from a JSON description of the board. Each tick it picks one of five moves in a single forward pass.

The model clearing an unseen level: five homes, no lives lost

Test level 4, never seen in training, at 12 moves a second. Right: the model's probability for each move, the planner's optimal moves (★) and per-decision latency.

Base model convaiinnovations/laya, ModernBERT-large encoder, 421M parameters
Task choose the next move: up, down, left, right or wait
Input a JSON state in a frog-centred view (at most 313 tokens with the question)
Output one probability per move
Speed 5.6 ms per decision on an RTX 5080 (fast=True)

Results

50 test levels never seen in training, each with 5 homes and 3 lives:

Policy Games Homes Deaths Optimal moves¹ Fatal moves¹
Base Laya, zero-shot 3 0 / 15 9 0.664² 0.306²
Always press up 0.689 0.285
This model 50 208 / 250 91 0.938 0.002

¹ Share of 5,000 test states where the move is one the planner rates optimal, or where it dies or leaves no way home in time.

² On 500 test states. Zero-shot, the base model answers up every time and dies within 13 moves.

Findings

  • The state representation mattered most. On the same states and labels, a version that saw the whole board, one string per row, barely beat always pressing up. A frog-centred window that also shows each row one tick ahead took it to 0.94 optimal moves.
  • Self-play data fixed play, not accuracy. Adding states from the model's own games, labelled by the planner (DAgger), lifted games from 182 to 208 homes and ended drowning, while test-set accuracy barely moved.
  • One failure remains. 79 of the 91 deaths are the same mistake: hopping onto a log that carries the frog off the board on the next tick. That situation is only 0.1% of the training data.

Usage

import laya

QUESTION = {
    "move": {
        "type": "choice",
        "instructions": (
            "Which move gets the frog into an empty home "
            "soonest without dying?"
        ),
        "criteria": {
            "up": "hop one row up, toward the homes",
            "down": "hop one row down, back toward the start",
            "left": "hop one cell left",
            "right": "hop one cell right",
            "wait": "stay in place for this tick",
        },
    }
}

# a real state from a test level (format below)
state = {
    "rows_to_home": 11,
    "time_left": 58,
    "empty_homes_dx": [-6, -3, 0, 3, 6],
    "rows": [
        "+3 road >3: now C C . . . C C . . | next C C C . . . C C .",
        "+2 road >3: now . . C . . . . . C | next . . . C . . . . .",
        "+1 road <3: now . . C . . . . . . | next . C . . . . . . C",
        "+0 road <3: now . . C C F . . . . | next . . C C . . . . .",
        "-1 safe: now . . . . . . . . . | next . . . . . . . . .",
    ],
    "legend": (
        "rows relative to the frog, +1 is the row above. "
        "Each row shows the 9 cells centred on the frog "
        "now and after the next tick. "
        "F frog, C car, = log, ~ water, H empty home, "
        "X filled home, # hedge, | edge of the board"
    ),
}

# fast=True needs tilelang; leave it out to use the stock forward pass
agent = laya.Agent("zsla-oc/laya-frogger", device="cuda", fast=True)
answer = agent.predict(state, QUESTION)["answers"]["move"]
print(answer["choice"], answer["probabilities"])

Send the question and the legend exactly as written; the model was trained on them.

State format

The board is 13 columns wide. Row 0 is the start, rows 1-5 are road, row 6 is a safe median, rows 7-11 are river, and row 12 holds homes at columns 0, 3, 6, 9 and 12. Each lane shifts one cell every 1-4 ticks.

Field Meaning
rows_to_home 12 minus the frog's row
time_left ticks left in this life, out of 60
empty_homes_dx column offset from the frog to each empty home
rows rows from 3 above the frog to 1 below, clipped to the board, top first
legend fixed text, shown above

Each row reads <offset> <kind> <motion>: now <9 cells> | next <9 cells>:

  • offset is the row relative to the frog's (+1 is the row above).
  • kind is road, river, safe or home.
  • motion, on moving lanes only, is <N or >N: one cell left or right every N ticks.
  • now is the 9 cells centred on the frog, with F in the middle of the frog's own row. next is the same cells after the world moves one tick.
Symbol F C = ~ H X # . |
Meaning frog car log water empty home filled home hedge ground off the board

A single | also separates the now and next frames.

Training

Stage Starting point and data Epochs Minutes
1 Base model. 49,946 states from 2,000 random levels, played by the planner with random moves mixed in 2 32
2 Stage 1. The same states plus 32,018 from stage 1's own games (DAgger) 1 24
  • Labels: an exact planner solves each life with one backward pass and marks every move that reaches a home fastest; tied moves share the probability. All data is synthetic, and no test level appears in training.
  • Recipe: Laya's official fine-tuning notebook, ported to one GPU: soft-label cross-entropy plus the RLCD proper-scoring term, encoder learning rate 2.5e-5, head 1e-4, bf16, then a temperature fitted on 400 held-out states.
  • Hardware: one RTX 5080. The run history is also stored under training in rl_agent_config.json.

Latency

Median per decision on an RTX 5080:

laya.Agent option Median
default 14.4 ms
compile=True (torch.compile) 7.7 ms
fast=True (TileLang kernels, CUDA graphs) 5.6 ms

Under WSL2, after about 1.5 s idle the GPU dropped into a power state where decisions took 27 ms, and it stayed there under light load. A 50 ms burst of back-to-back calls restored full speed. If you call the model a few times a second, check for this.

Limitations

  • Trained and evaluated once, with no error bars, on 50 test levels.
  • The frog-centred view hides rows the planner can see, so perfect play isn't reachable with this input.
  • Only meaningful for this Frogger and this state format: a study of fine-tuning a decision model, not a general game-playing model.

Licence

Apache 2.0, like the base model convaiinnovations/laya by Convai Innovations. Its encoder is ModernBERT-large by Answer.AI.

Downloads last month
-
Safetensors
Model size
0.4B params
Tensor type
F16
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for zsla-oc/laya-frogger

Finetuned
(206)
this model