Files
interactive-story/backend/app/seed_data/05-league-championship.json
T
parththakkar106andClaude Opus 5 47c7800903 Report the world-state changes the engine refuses
`apply_delta` records three outcomes for every change the model sends:
`applied`, `clamped`, and `rejected`. Everything downstream read only
`applied`. A refused change reached the player as an ordinary chip, and
reached the model on the next turn as a change that had succeeded.

Five parts:

- `Action.world_changes` reads `clamped` and `rejected` beside `applied`.
  Accepted stats carry a `clamped` flag; refusals become `kind: "rejected"`
  entries. The `fix` key is present only when the engine wrote one, because
  this property runs for every action of every list response.
- The UI separates the three outcomes. A clamp to a standstill reads
  `no change - at its limit` on a dashed chip, a partial clamp is marked
  `(limited)`, and a rejection carries its reason. Dashed and dimmed rather
  than red: a refused change means the rules are working.
- The goals line names the milestone id, as `milestones.<id>`. The ids
  appeared nowhere in the prompt before, so the model could not send one.
- Each rejection, and each clamp that moved nothing, builds a `fix` string
  from the stat definition at the point of refusal. `render_refusals()`
  renders them into the next prompt above `EMIT_REMINDER`.
- `_history_text` replays `applied_delta()` instead of the sent delta, so a
  past turn's state block shows only what the engine accepted.

A clamp that reduced a change but still moved the value reports nothing. If
you tell a model its 80 damage became 30, it can treat the shortfall as a
debt and send the remaining 50 next turn, which is the swing
`max_delta_per_turn` prevents.

In the demo scenario, `pokemon_left` becomes `pokemon_fainted`
(`type: counter`, `initial: 0`). Starting at the ceiling turned a wrong-signed
delta into a silent no-op; counting up puts the wrong sign on the counter
rule, which refuses it out loud. The instructions also now ask for
`world.turn`, which sat at 0 for a whole playtest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PacdRuPXSkQQy4ZYdH32hF
2026-08-28 16:26:34 +05:30

169 lines
13 KiB
JSON

{
"title": "[Demo] League Championship: Round One",
"description": "A full Pokémon battle run on the built-in world-state system. Every member of your team is tracked as its own entity with HP and a status condition, a text field names who is currently out, and your potion supply runs down as you use it. The opponent's active Pokémon is tracked the same way. No scripting — the engine keeps the numbers honest. Open the World State panel on the left to watch the battle play out.",
"prompt": "The floodlights come up over the championship field and the referee raises both flags. Round One of eight. Beat the trainer in front of you or go home.\n\nAcross the packed rock terrain, Trainer Milo tosses his first Poké Ball. Graveler lands hard enough to crack the stone under it, four arms flexing, and settles into a low stance.\n\n\"Rock-types all the way down,\" Milo calls over the noise. \"Hope you brought something that hits harder than it looks.\"\n\nYour Pidgeotto is already in the air, wings locked into a slow circle above the field. It is fast, but Rock-type moves will tear it out of the sky, and Graveler is built to take a hit. Three Hyper Potions sit in your belt pouch.\n\nGraveler grinds one fist into its palm. The referee drops the flag.\n\nWhat's your move?",
"memory": "The player is a Trainer in Round One of an eight-round League Championship. The Round One opponent is Milo, a Rock-type specialist with three Pokémon: Graveler (Rock/Ground), Onix (Rock/Ground), and Kabutops (Rock/Water). The player's team is Pidgeotto (Normal/Flying), Ivysaur (Grass/Poison), Wartortle (Water), Machoke (Fighting), and Pikachu (Electric). Pidgeotto is out first. The player carries Hyper Potions, which restore HP and can be swapped for a status heal when needed. A battle is lost when every Pokémon on one side has fainted.",
"authors_note": "Play it like a real battle. Type matchups decide damage, status conditions stick around and cost the afflicted Pokémon turns, and switching costs the player a turn while the opponent attacks freely. Milo is competent: he switches to punish bad matchups and targets whatever is weakest.",
"ai_instructions": "Write in second person, present tense. Narrate the battle move by move, naming the active Pokémon on each side and the move each one uses. End every reply at a decision point where the player can choose a move, switch Pokémon, or use an item.\n\nReflect type advantage and disadvantage in the damage you narrate and in the HP numbers: a super-effective hit should take roughly double a neutral hit, a resisted hit roughly half. Ground and Rock moves hit Pikachu and Pidgeotto hard. Water, Grass, and Fighting moves are strong against Milo's Rock-types.\n\nEvery turn, update the state:\n- Add 1 to `world.turn` in every reply, including this one.\n- Drop the active Pokémon's HP on both sides when they take damage (`npc.<name>.hp` for the player's own Pokémon, e.g. npc.pidgeotto.hp; `npc.milo.active_hp` for Milo's), and raise it when they are healed.\n- Set a Pokémon's `status` stat (`npc.<name>.status`, or `npc.milo.active_status`) to the condition it just picked up: poisoned, burned, paralyzed, frozen, asleep, or none when it is cured or wakes up. Status matters. Poison and burn shave HP at the end of each turn, paralysis and freeze cost turns, sleep stops a Pokémon acting until it wakes.\n- When the player switches, set `player.active_pokemon` to the incoming Pokémon's name in full (not a delta).\n- When the player uses a potion, decrement `player.potions` by 1 and raise the healed Pokémon's HP. Potions cannot be used once the count reaches 0.\n- When a Pokémon's HP hits 0 it faints, its status becomes `fainted`, and its trainer must send out a replacement (the player picks the next `player.active_pokemon`). A fainted Pokémon cannot be healed or switched back in.\n- Update `npc.milo.active_pokemon` and reset `npc.milo.active_hp` and `npc.milo.active_status` whenever Milo sends out a new Pokémon.\n\nWhen one of Milo's Pokémon faints, do both of these in the same reply: add 1 to `npc.milo.pokemon_fainted`, and set `npc.milo.active_pokemon` with a fresh `npc.milo.active_hp` and `npc.milo.active_status` for the one he sends out next. `npc.milo.pokemon_fainted` counts up from 0 and never goes down.\n\nMark each milestone as it happens, using the id shown beside the goal in the world state. Do not decide the whole battle in one reply; give the player a turn between every exchange.",
"tags": "demo, pokemon, battle, tournament, world-state, combat",
"icon": "⚡",
"stat_schema": {
"world": {
"round": { "type": "counter", "min": 1, "max": 8, "initial": 1, "desc": "Which round of the eight-round championship this is; only ever counts up." },
"turn": { "type": "counter", "min": 0, "initial": 0, "max_delta_per_turn": 1, "desc": "How many turns the current battle has lasted; only ever counts up." }
},
"player": {
"potions": {
"desc": "Hyper Potions left in the player's bag. Each one restores HP or cures a status condition, and is used up when it is used.",
"min": 0, "max": 5, "initial": 3, "max_delta_per_turn": 1,
"bands": [[0, 1, "out of potions"], [1, 2, "one left"], [2, 4, "a few left"], [4, 6, "well stocked"]]
},
"active_pokemon": {
"type": "text", "initial": "Pidgeotto",
"desc": "Which of the player's Pokémon is currently on the field: Pidgeotto, Ivysaur, Wartortle, Machoke, or Pikachu. Replace this the moment the player switches."
}
},
"flags": {
"sandstorm_active": { "desc": "True while a sandstorm is whipping across the field, chipping HP off any Pokémon that is not Rock, Ground, or Steel each turn.", "initial": false }
},
"npcs": {
"pidgeotto": {
"name": "Pidgeotto",
"keys": "Pidgeotto",
"desc": "The player's own Pokémon. Normal/Flying. Fast, but Rock-type moves hit it for four times damage.",
"stats": {
"hp": {
"desc": "Pidgeotto's HP.",
"min": 0, "max": 84, "initial": 84, "max_delta_per_turn": 60,
"bands": [[0, 1, "fainted"], [1, 20, "critical"], [20, 45, "hurt"], [45, 70, "scuffed"], [70, 85, "healthy"]]
},
"status": {
"type": "text", "initial": "none",
"desc": "Status condition: none, poisoned, burned, paralyzed, frozen, asleep, or fainted."
}
}
},
"ivysaur": {
"name": "Ivysaur",
"keys": "Ivysaur",
"desc": "The player's own Pokémon. Grass/Poison. Super effective on Milo's Rock/Ground types; can inflict poison or sleep.",
"stats": {
"hp": {
"desc": "Ivysaur's HP.",
"min": 0, "max": 96, "initial": 96, "max_delta_per_turn": 60,
"bands": [[0, 1, "fainted"], [1, 24, "critical"], [24, 50, "hurt"], [50, 78, "scuffed"], [78, 97, "healthy"]]
},
"status": {
"type": "text", "initial": "none",
"desc": "Status condition: none, poisoned, burned, paralyzed, frozen, asleep, or fainted."
}
}
},
"wartortle": {
"name": "Wartortle",
"keys": "Wartortle",
"desc": "The player's own Pokémon. Water. Bulky, and super effective on the whole of Milo's team.",
"stats": {
"hp": {
"desc": "Wartortle's HP.",
"min": 0, "max": 92, "initial": 92, "max_delta_per_turn": 60,
"bands": [[0, 1, "fainted"], [1, 23, "critical"], [23, 48, "hurt"], [48, 75, "scuffed"], [75, 93, "healthy"]]
},
"status": {
"type": "text", "initial": "none",
"desc": "Status condition: none, poisoned, burned, paralyzed, frozen, asleep, or fainted."
}
}
},
"machoke": {
"name": "Machoke",
"keys": "Machoke",
"desc": "The player's own Pokémon. Fighting. Hits Rock-types hardest of the team but is slow and takes Ground moves badly.",
"stats": {
"hp": {
"desc": "Machoke's HP.",
"min": 0, "max": 104, "initial": 104, "max_delta_per_turn": 65,
"bands": [[0, 1, "fainted"], [1, 26, "critical"], [26, 54, "hurt"], [54, 84, "scuffed"], [84, 105, "healthy"]]
},
"status": {
"type": "text", "initial": "none",
"desc": "Status condition: none, poisoned, burned, paralyzed, frozen, asleep, or fainted."
}
}
},
"pikachu": {
"name": "Pikachu",
"keys": "Pikachu",
"desc": "The player's own Pokémon. Electric. Frail, and its Electric moves do nothing at all to Ground-types.",
"stats": {
"hp": {
"desc": "Pikachu's HP.",
"min": 0, "max": 76, "initial": 76, "max_delta_per_turn": 60,
"bands": [[0, 1, "fainted"], [1, 19, "critical"], [19, 40, "hurt"], [40, 62, "scuffed"], [62, 77, "healthy"]]
},
"status": {
"type": "text", "initial": "none",
"desc": "Status condition: none, poisoned, burned, paralyzed, frozen, asleep, or fainted."
}
}
},
"milo": {
"name": "Trainer Milo",
"keys": "Milo, opponent, Graveler, Onix, Kabutops, rock",
"desc": "The Round One opponent, a Rock-type specialist with three Pokémon: Graveler, Onix, and Kabutops. He leads with Graveler, switches to punish a bad matchup, and aims for whichever of the player's Pokémon is closest to fainting.",
"stats": {
"active_pokemon": {
"type": "text", "initial": "Graveler",
"desc": "Which Pokémon Milo currently has on the field: Graveler, Onix, or Kabutops. Replace this the moment he sends out a different one."
},
"active_hp": {
"desc": "The HP of Milo's active Pokémon. Reset this to the newcomer's full HP whenever he sends out a different Pokémon: Graveler 98, Onix 90, Kabutops 88.",
"min": 0, "max": 98, "initial": 98, "max_delta_per_turn": 65,
"bands": [[0, 1, "fainted"], [1, 25, "critical"], [25, 50, "hurt"], [50, 78, "scuffed"], [78, 99, "healthy"]]
},
"active_status": {
"type": "text", "initial": "none",
"desc": "The status condition on Milo's active Pokémon: none, poisoned, burned, paralyzed, frozen, asleep, or fainted."
},
"pokemon_fainted": {
"desc": "How many of Milo's three Pokémon have fainted. Add 1 each time one goes down. Milo loses the round when this reaches 3.",
"type": "counter",
"min": 0, "max": 3, "initial": 0, "max_delta_per_turn": 1,
"bands": [[0, 1, "full team"], [1, 2, "two left"], [2, 3, "last Pokémon"], [3, 4, "defeated"]]
}
}
}
},
"milestones": {
"graveler_defeated": { "desc": "Knock out Milo's lead Graveler" },
"status_landed": { "desc": "Inflict a status condition on one of Milo's Pokémon" },
"type_advantage_used": { "desc": "Land a super-effective hit by switching in the right Pokémon" },
"round_one_won": { "desc": "Knock out all three of Milo's Pokémon and advance to Round Two" }
}
},
"story_cards": [
{
"type": "location",
"name": "Championship Stadium",
"keys": "stadium, arena, battlefield, field, terrain",
"entry": "The League Championship's main battlefield. The floor is broken rock terrain, which gives Rock- and Ground-type moves solid footing and gives grounded Pokémon cover to duck behind. Eight rounds stand between any Trainer and the Champion's title, and this is Round One.",
"notes": "Rocky terrain. Rock and Ground moves get a slight edge. Boulders can be used as cover or thrown."
},
{
"type": "character",
"name": "Trainer Milo",
"keys": "Milo, opponent, rock trainer",
"entry": "Milo is the Round One opponent, a Rock-type specialist who won his qualifiers on raw defense. His team is Graveler (Rock/Ground), Onix (Rock/Ground), and Kabutops (Rock/Water). He leads with Graveler, likes Sandstorm and Rock Slide, and switches out the moment a matchup turns against him.",
"notes": "Graveler and Onix are weak to Water, Grass, Fighting, Ground, Ice, and Steel, and are immune to Electric. Kabutops is weak to Grass, Fighting, Ground, and Electric. All three resist Normal and Flying."
},
{
"type": "item",
"name": "Hyper Potion",
"keys": "potion, hyper potion, heal, item, bag",
"entry": "A pressurized spray that closes wounds fast. One dose restores a large chunk of a Pokémon's HP, or clears a single status condition instead. Using one costs your turn, and the opponent attacks freely while you do it. You start the round with three and cannot restock mid-battle.",
"notes": "Decrement player.potions by 1 per use. A potion cannot revive a fainted Pokémon."
}
],
"scripts": []
}