How I Used Jev Model to Play Plants vs. Zombies at 3.9 Hz (Without Seeing a Pixel)

Share
How I Used Jev Model to Play Plants vs. Zombies at 3.9 Hz (Without Seeing a Pixel)

Everyone knows ChatGPT. You ask a question, and it slowly types out a conversational answer. That’s great for writing emails, but terrible for driving real-time software. If ChatGPT is a slow, deliberate "System 2" thinker, we now have models built for fast reflexes.

Meet Jev, a brand-new System One AI model by TypeSafe. Instead of generating paragraphs token by token, Jev takes a structured state and instantly returns a definitive choice with a calibrated confidence score. It doesn't write text; it evaluates typed choices.

To stress-test this new architecture, I plugged Jev directly into a game of Plants vs. Zombies on a real Android phone over ADB at 3.9 Hz. The wildest part? The model playing the game never sees a single pixel.

The Architecture: JSON to Touch Events

Architecture

The control loop relies on a lightweight stack, heavily utilizing TypeScript to enforce schema validation before execution.

The pipeline runs like this:

  • scrcpy captures H.264 video frames from the Android device.
  • OpenCV parses the frame into a normalized JSON board state.
  • The system constructs typed questions and sends them to Jev.
  • The response is converted into a 32-byte touch message sent back over scrcpy's control socket.

The interesting part isn't the game itself, but the request shape. Instead of feeding it images, the system gives Jev the board state as data and asks strict multiple-choice questions:

  • Priority (Choice): Do we build economy or answer the lawn?
  • Most threatened lane (Choice): A rubric per lane naming current threats.
  • Threat severity (Score): Scored from low to critical.
  • Optimal placement (Choice): The single best plant and tile combination to play.

The Request: State and Questions

Instead of sending the model screenshots, the perception layer converts the screen into a structured JSON representation of the board. This explicit payload tracks available sun, plant cooldowns, live enemy threats, and free tiles on the lawn.

jev_request_state.json

{
  "sun": 150,
  "collectible_suns": [],
  "seed_slots": [
    {
      "name": "peashooter",
      "cost": 100,
      "ready": true,
      "brightness": 180.2,
      "index": 0,
      "cooling": false
    },
    {
      "name": "sunflower",
      "cost": 50,
      "ready": true,
      "brightness": 164.9,
      "index": 1,
      "cooling": false
    },
    {
      "name": "cherrybomb",
      "cost": 150,
      "ready": true,
      "brightness": 156.0,
      "index": 2,
      "cooling": false
    },
    {
      "name": "wallnut",
      "cost": 50,
      "ready": false,
      "brightness": 90.3,
      "index": 3,
      "cooling": true
    }
  ],
  "threats": [
    {
      "row": 4,
      "col": 7,
      "distance_to_house": 7,
      "threat_type": "basic",
      "coverage": 0.53,
      "confidence": 0.849
    },
    {
      "row": 1,
      "col": 9,
      "distance_to_house": 9,
      "threat_type": "basic",
      "coverage": 0.254,
      "confidence": 0.92
    },
    {
      "row": 4,
      "col": 9,
      "distance_to_house": 9,
      "threat_type": "basic",
      "coverage": 0.324,
      "confidence": 0.783
    },
    {
      "row": 5,
      "col": 9,
      "distance_to_house": 9,
      "threat_type": "basic",
      "coverage": 0.136,
      "confidence": 0.831
    }
  ],
  "lawn": {
    "r1_c1": "sunflower",
    "r1_c3": "peashooter",
    "r2_c1": "sunflower",
    "r2_c3": "peashooter",
    "r2_c8": "unknown",
    "r3_c1": "sunflower",
    "r3_c3": "peashooter",
    "r3_c4": "peashooter",
    "r3_c5": "peashooter",
    "r3_c6": "unknown",
    "r3_c7": "unknown",
    "r4_c1": "sunflower",
    "r4_c2": "sunflower",
    "r4_c4": "peashooter",
    "r4_c6": "unknown",
    "r5_c1": "sunflower",
    "r5_c2": "sunflower",
    "r5_c3": "peashooter",
    "r5_c4": "unknown"
  },
  "free_tiles": {
    "r1": [2, 4, 5, 6, 7, 8, 9],
    "r2": [2, 4, 5, 6, 7, 8, 9],
    "r3": [2, 6, 7, 8, 9],
    "r4": [3, 5, 6, 7, 8, 9],
    "r5": [4, 5, 6, 7, 8, 9]
  },
  "placement_options": {
    "peashooter": [
      { "tile": "r4_c3", "rank": 0, "arrival_s": 19.0 },
      { "tile": "r4_c5", "rank": 1, "arrival_s": 9.5 },
      { "tile": "r1_c4", "rank": 2, "arrival_s": 23.8 },
      { "tile": "r1_c5", "rank": 3, "arrival_s": 19.0 },
      { "tile": "r1_c6", "rank": 4, "arrival_s": 14.3 }
    ],
    "sunflower": [
      { "tile": "r1_c2", "rank": 0, "in_ramp": true, "arrival_s": 33.3 },
      { "tile": "r2_c2", "rank": 1, "in_ramp": true },
      { "tile": "r3_c2", "rank": 2, "in_ramp": true }
    ],
    "cherrybomb": [
      { "tile": "r4_c7", "rank": 0, "zombie": true }
    ]
  }
}

state

The system then asks strict multiple-choice questions where Jev must pick from a validated menu. Jev doesn't guess raw grid coordinates. We give it a list of legal moves with the mathematical trade-offs explicitly written out so it understands the tactical buffer of each choice.

jev_request_questions.json

{
  "priority": {
    "type": "choice",
    "instructions": "Plants vs Zombies, one decision: should the next action build sun production or answer what is on the lawn?",
    "criteria": {
      "economy": "Plant a sunflower: nothing on the lawn needs an immediate answer.",
      "defense": "Plant a defender or an instant kill: a lane is threatened or undefended."
    }
  },
  "most_threatened_lane": {
    "type": "choice",
    "instructions": "Which lane is in the most danger right now?",
    "criteria": {
      "row_1": "Row 1: basic at column 9 — our plants in columns [1, 3].",
      "row_4": "Row 4: basic at column 7, basic at column 9 — our plants in columns [1, 2, 4].",
      "row_5": "Row 5: basic at column 9 — our plants in columns [1, 2, 3].",
      "none": "No lane stands out; any threatened lane is equally urgent."
    }
  },
  "optimal_placement": {
    "type": "choice",
    "instructions": "Which single (plant, tile) pair is the best move right now? Every option listed is legal and affordable; pick the one that produces the best board position. Consider how far the zombie is from each tile.",
    "criteria": {
      "peashooter_r4_c3": "Peashooter (100 sun, shooter): r4_c3: 4 tile(s) left of the basic at column 7, so it shoots with 4 tile(s) of buffer before it is reached.",
      "sunflower_r1_c2": "Sunflower (50 sun, economy): r1_c2: 7 tile(s) in front of the basic at column 9, which reaches it in 33 s.",
      "cherrybomb_r4_c7": "Cherrybomb (150 sun, instant): r4_c7: the basic stands here now, so the instant lands on it.",
      "peashooter_r4_c5": "Peashooter (100 sun, shooter): r4_c5: 2 tile(s) left of the basic at column 7, so it shoots with 2 tile(s) of buffer before it is reached.",
      "sunflower_r2_c2": "Sunflower (50 sun, economy): r2_c2 is clear and inside the economy reserve (columns 1-2).",
      "peashooter_r1_c4": "Peashooter (100 sun, shooter): r1_c4: 5 tile(s) left of the basic at column 9, so it shoots with 5 tile(s) of buffer before it is reached.",
      "sunflower_r3_c2": "Sunflower (50 sun, economy): r3_c2 is clear and inside the economy reserve (columns 1-2).",
      "peashooter_r1_c5": "Peashooter (100 sun, shooter): r1_c5: 4 tile(s) left of the basic at column 9, so it shoots with 4 tile(s) of buffer before it is reached.",
      "peashooter_r1_c6": "Peashooter (100 sun, shooter): r1_c6: 3 tile(s) left of the basic at column 9, so it shoots with 3 tile(s) of buffer before it is reached."
    }
  }
}

question

The Response: Typed Decisions

Jev returns exactly what is requested: a single best choice for each category alongside a calibrated confidence score and a probability distribution across all valid options. Because it only selects from the provided keys, it cannot hallucinate an illegal move or malformed syntax.

jev_response_answers.json

{
  "priority": {
    "type": "choice",
    "choice": "defense",
    "confidence": 0.92,
    "probabilities": {
      "economy": 0.04,
      "defense": 0.96
    }
  },
  "most_threatened_lane": {
    "type": "choice",
    "choice": "row_4",
    "confidence": 0.95,
    "probabilities": {
      "row_4": 0.96,
      "row_1": 0.02,
      "none": 0.0,
      "row_5": 0.02
    }
  },
  "optimal_placement": {
    "type": "choice",
    "choice": "cherrybomb_r4_c7",
    "confidence": 0.9,
    "probabilities": {
      "peashooter_r1_c5": 0.0,
      "peashooter_r4_c3": 0.02,
      "sunflower_r1_c2": 0.0,
      "sunflower_r2_c2": 0.0,
      "peashooter_r1_c6": 0.0,
      "cherrybomb_r4_c7": 0.91,
      "sunflower_r3_c2": 0.0,
      "peashooter_r4_c5": 0.06,
      "peashooter_r1_c4": 0.01
    }
  }
}

jev response

Typed Decisions Over Raw Prose

The secret is how we ask that last question. Jev doesn't guess raw grid coordinates. We give it a menu of legal moves with the mathematical trade-offs already spelled out.

For example:

  • "peashooter_r3_c5": "Peashooter (100 sun, shooter): r3_c5: 2 tile(s) left of basic at col 7; shoots with 2 tiles of buffer before reached."
  • "sunflower_r3_c2": "Sunflower (50 sun, economy): r3_c2: 5 tile(s) in front of basic at col 7; reached in 24 s."

Only legal tiles are offered. Everything deterministic stays in code: costs, cooldowns, and which tiles are free. Even collecting sun never reaches the model—it's handled by a fast coordinate lookup (178 taps in this run). If an answer expires mid-flight, the system falls back to the deterministic cascade instead of trying to execute an illegal action.

Latency, Cost, and The Confidence Gate

Because the game moves fast, the AI's strategy runs in the background. While the control loop has a strict 250 ms tick budget, Jev's round trips ran between 293 and 1034 ms (median 356 ms). The strategy is cached and refreshed asynchronously, while every tick re-checks affordability and availability against the live state.

The economics of this architecture are just as important as the speed. Because Jev is a specialized decision engine and not a chat LLM, TypeSafe prices it at a fraction of standard frontier models: $0.042 per 1 million input tokens, and $0 for output tokens.

To put that in perspective: the entire 5-minute PvZ run consumed 182,669 input tokens and 14,127 output tokens. The total cost for the full run? $0.007. Less than a single penny.

If you isolate a standard agent decision loop using roughly 10,000 input tokens, it costs exactly $0.00042. By eliminating the "token tax" of streaming text, this architecture makes continuous, high-frequency agent loops financially viable in production.

The most powerful feature of this setup is the confidence gate. In this run, 34 out of 142 answers came back with a confidence score under 0.4 on the priority question. When Jev's confidence drops, the system safely ignores the model and falls through to deterministic rules rather than acting on a hedge. A model whose "I'm not sure" can be acted on is far more useful than one that is always confidently wrong.

What generalizes past a lawn full of zombies to any real-world AI agent:

  1. Give models typed decisions over a code-validated action set, never raw prose.
  2. Describe options in their domain context and keep the arithmetic on your side.
  3. If round trips exceed your control loop, cache the strategy and recompute tactics.
  4. Decide up front what confidence threshold means an action is not worth taking.

You can dive into the complete TypeScript control loop, the event dashboard, and the full run logs over on my GitHub at https://github.com/kunal52/plants_vs_zombies_jev