How can a game build spells from typed text without an LLM?

The interactive below is a crafting bench next to a small arena. Type a spell in your own words, or pick one of the ten presets, and press Craft. Watch the inspector fill in seven typed answers, check which gates pass in the composition panel, then press Cast and see what the game built. Try the freezing fire preset a few times in a row: the same description can land on a different element each cast.

Figure 1Jev spellcrafting: seven typed questions build one spell

Type a spell and a simulated System One model answers seven typed questions about it. Game code gates it, prices it, and builds it from designer-authored tables, then casts it at training dummies.

Built with vanilla JavaScript, Canvas 2D, and CSS. Answers are simulated: presets use hand-written distributions and free text uses a keyword heuristic; nothing calls Jev. Request and answer shapes follow the TypeSafe API docs. Render and interaction by Pro Trailblazer.

New tab

What you're seeing

The crafting bench takes up to 200 characters of free text. That text is the whole input. When you press Craft, the page builds one request in the shape TypeSafe documents for its endpoint: the description as state, the model jev-latest, and a map of seven questions. You can open the exact request and response JSON at the bottom of the inspector.

The inspector shows one row per question. Element, shape, targeting, and control are choice questions, so each row is a set of probability bars over the options the designer wrote, with the winner in red and a confidence pill beside it. Power and duration are score questions over ordered levels (a cantrip up to a catastrophe, instant up to minutes), so their rows show a distribution across levels and a score that can sit between them. The last row is a noul question: one probability that the description asks for something the world forbids, with a red tick at the 0.60 refusal line.

The composition panel is ordinary game code reading those answers. It runs three gates (the legality check, the element confidence, and the mana budget), shows the mana formula with the live numbers, and prints the assembled spell object. Every field in that object is a key into a table the designer filled in by hand. The arena then casts it: a caster on the left, three training dummies with health bars at near, middle, and far range, and a stone wall that blocks bolts and beams aimed at the far one.

A few things to try:

  • Cast the freezing fire preset three or four times. Fire and ice come back at 0.45 and 0.41, the element confidence is 0.41, and the spell is marked unstable. Each cast rolls its element from those probabilities, and the inspector prints the roll.
  • Pick the pillar of holy light. It asks for a lot: power scores 2.97 and the shape is rain, so the cost is 12 + 24 + 6 = 42 against a pool of 40, and the bench refuses with the numbers. Change it to "a pillar of holy light that dazes one foe" and it comes back as a 27 mana light rain that stuns a single target.
  • Pick "bring my brother back", or type "stop time for everyone but me". The forbidden check comes back at 0.94 and 0.74. Both trip the 0.60 gate, the runes go dark, and no table is ever read.
  • Type something plain, like "throw a fireball at that goblin". One clear keyword per question is enough for a fire bolt aimed at a single enemy, with element confidence around 0.69.
  • Cast the ice wall twice in a row. It costs 32 of your 40 mana, so the second cast fails with the exact shortfall until the pool refills at 3 per second.

How seven questions become one spell

The request is the whole interface. Each question is either a choice with named options, a score with ordered levels, or a noul statement to check. The model never sees the ids (element, shape, and so on). Those keys exist for the code that reads the answers. Trimmed to three of the seven questions, the request looks like this:

{
  "state": "a slow wall of ice that shoves enemies back and holds a while",
  "model": "jev-latest",
  "questions": {
    "element": {
      "type": "choice",
      "instructions": "Which element does this spell draw on?",
      "criteria": {
        "fire": "Flame, heat, burning, embers. Not for electric sparks or lightning.",
        "ice": "Frost, freezing cold, solid ice, snow. Not for liquid water or rain.",
        "...": "six more elements"
      }
    },
    "power": {
      "type": "score",
      "instructions": "How much raw damage or force does the description imply?",
      "criteria": [
        "a cantrip, barely a spark",
        "a light hit, a sting",
        "a solid combat spell",
        "a heavy blow that breaks shields",
        "a catastrophe that would level a building"
      ]
    },
    "forbidden": {
      "type": "noul",
      "instructions": "The description asks for something the world's laws forbid: raising the dead, stopping or reversing time, mind control, or harming the caster's own allies on purpose"
    }
  }
}

The "not for" clauses in the element criteria are doing real work. Fire and lightning both describe sparks and heat, and ice and water are the same substance at different temperatures. Telling the model what each option excludes is cheaper than hoping it guesses the boundary the designer had in mind. The full request in the demo is about 580 input tokens, which at $0.042 per million comes to roughly $0.000024 per craft. Output tokens are free.

Every answer comes back with its full distribution. A choice answer carries the winning option, a probability for every option, and a confidence between 0 and 1. A score answer carries a score that is the probability-weighted mean of the level indices, so a description that is half "solid" and half "heavy" lands at 2.5 rather than being forced into one bucket. A noul answer is a single probability. The game code then composes the spell:

if (a.forbidden.noul >= 0.6) return refuse("The runes go dark. That is not permitted here.");

const cost = BASE[a.shape.choice]
           + Math.round(a.power.score * 8)
           + Math.round(a.duration.score * 5);
if (cost > mana) return fail(cost, mana);

const element = a.element.confidence >= 0.5
  ? a.element.choice
  : sample(a.element.probabilities);          // unstable: re-rolled on every cast

cast({
  element,
  shape: a.shape.choice,
  targeting: a.targeting.choice,
  control: a.control.choice,
  power_level: Math.round(a.power.score),
  duration_sec: DURATION_SEC[Math.round(a.duration.score)],
  mana_cost: cost,
});

Nothing in that block parses language. It compares numbers and indexes tables. That is the whole point of asking typed questions: the hard part (reading loose prose) happens once, inside the model, and everything after it is the kind of code a game already has.

Why one big question loses to seven small ones

"What spell is this?" has no fixed answer set. A model asked that question has to write its answer out, and the game has to parse whatever comes back, including answers that name an effect the engine has no code for. Seven atomic questions each have a closed set the designer wrote: eight elements, six shapes, four targeting modes, six control effects, five power levels, four duration levels, and one legality check. Every one of them can be answered without knowing the answer to any other, which is what lets them all run in parallel inside one request.

Decomposition also makes the system debuggable. When a spell comes out wrong, you can see which question went wrong. If "a slow wall of ice" ever came back as a beam, the shape row would show it, and the fix is a sharper shape criterion, not a rewritten prompt that might break the other six answers.

Why the probabilities matter

The demo uses more than the top choice. When the element distribution is split, as it is for "freezing fire", the game does not pick fire and pretend. It marks the spell unstable and samples the element from the -shaped distribution on every cast. An ambiguous description becomes a mechanic the player can see and play with, and a confident description behaves the same way every time.

Confidence is the switch. The demo shows it as 1 minus the normalized entropy of the distribution, which is an approximation; TypeSafe documents that confidence reflects how spread the distribution is but does not publish its formula. The useful property is the same either way: a near-even split reads near 0, a clear winner reads high, and the game can put a threshold on it. Here the threshold is 0.5. Below it, the element is rolled. Above it, the choice stands.

The designer still owns every spell

The model only ever picks from criteria the designer wrote, and the game only ever builds from tables the designer filled: a sprite shape, a speed, a size, a particle count, a damage number, and a knockback vector for every entry. It cannot invent a ninth element or a status effect the engine never shipped. The worst it can do is pick a valid entry the player did not expect, and the probabilities show exactly how likely that was.

Classic text parsers needed exact verbs ("cast fireball at goblin") and broke on anything nobody anticipated. Chat-style LLMs understand loose descriptions, but they are slow for a loop the player repeats every few seconds, they cost per output token, and they can describe effects that do not exist, which someone then has to catch. A typed call sits between the two. It reads free text like a language model and answers like a lookup table, in the 70 to 500 millisecond range TypeSafe reports, priced on input alone. The same pattern runs the typed preflight gate and the batch judgment race in the TypeSafe series, and it is the opposite approach to constrained decoding, where a text model is forced to emit valid JSON one token at a time.

Frequently asked questions

Can AI generate spells in a game?

Yes, but the safer design is for the model to choose rather than generate. In this pattern the model reads the player's description and answers typed questions about it (which element, which shape, how strong), and the game assembles the spell from parts the designer already built. The player gets freeform input, and the designer keeps control over every effect that can exist.

What is a System One model?

It is a model that answers typed questions about some input instead of writing text. TypeSafe's Jev takes a state (a string or JSON) and a set of choice, score, and yes or no questions, and returns a probability for every option plus a confidence value, typically in tens to hundreds of milliseconds.

How do you build freeform crafting in a game?

Break the outcome into independent decisions, give each one a closed set of options that map to content you have built, and ask a model to pick an option for each from the player's description. Then compose the result in ordinary code with gates for legality and cost. Keep the probabilities: they let you turn ambiguous input into a visible mechanic, like the unstable element in the demo.

What happens if the description is ambiguous?

The probabilities show it. When two options split the distribution, confidence drops, and the game decides what that means. The demo marks the spell unstable and rolls the element on each cast. Another game might ask the player to clarify, fall back to a default, or charge more mana for an unstable spell. The point is that the decision lives in game code, where a designer can tune it.

Why not use a chat model like an LLM for this?

A chat model could read the description, but it would have to write its answer out, the game would have to parse it, and the answer could name an effect the engine cannot render. It would also be slower and priced per output token. A typed call returns that index straight into the designer's tables, with no parsing and nothing invented.

Key takeaways

  • A System One model reads free text and answers typed questions about it, so the game gets numbers to compare instead of prose to parse.
  • Ask several small questions with closed answer sets instead of one open question. Each answer maps to a table the designer built, and a wrong answer points at the one criterion to fix.
  • Keep the whole distribution, not just the winner. Low confidence is information, and a game can turn it into a mechanic like an unstable element that re-rolls every cast.
  • Legality and cost belong in code, not in the prompt. A noul check with a threshold refuses forbidden spells before any table is read, and a plain formula prices what is left.
  • The model can only ever choose what the designer already made, which is what makes freeform input safe to ship.

Written by Ryan Murray. Published , updated .