Skip to content

Drive game characters

Fight an ogre whose every move is a Sage2 decision.

Most game enemies run on hand-written rules: if health is low, drink a potion. Players learn the rules within a few fights. Here the ogre reads the fight and picks its next move with one Choice call, in character.

The personality is a sentence in the request, so a brute, a tactician and a coward are three strings, not three state machines. Your game code still owns the rules: Sage2 only picks from the moves you offer.

  • Choice
Grumble is a

You, the knight50/50 · 1 potion

Grumble the ogre50/50 · 1 potion

  1. Pick a move. Grumble decides his own.

Get the code

Set SAGE_API_KEY (get a free key), then run it. Python: pip install requests. TypeScript: Node 22+. Save as recipe.mts and run npx tsx recipe.mts.

A null answer is not an error. Sage2 isn't sure, and each step below says what to do with it. Limits, errors and retries: Limits and errors.

import os
import requests

def sage(path, body):
    r = requests.post(
        "https://sage.levanto.ai" + path,
        headers={"Authorization": f"Bearer {os.environ['SAGE_API_KEY']}"},
        json=body,
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

MOVES = [
    {
        "option": "strike",
        "description": "A quick hit for 8 damage. Half damage against a guard.",
    },
    {
        "option": "heavy_swing",
        "description": "A slow hit for 16 damage. Does nothing against a guard.",
    },
    {
        "option": "guard",
        "description": "Cuts the damage of the next hit.",
    },
    {
        "option": "drink_potion",
        "description": "Restores 15 health. Only works if a potion is left.",
    },
]

def next_move(personality, state, potions):
    # Offer only legal moves: with no potion left, leave drink_potion out.
    options = [m for m in MOVES if potions or m["option"] != "drink_potion"]
    result = sage("/decide", {
        "content": state,
        "question": {"id": "next_move", "kind": "choice",
                     "instructions": f"{personality} What do you do next?", "options": options},
    })["result"]
    # A close call returns null: go with the most likely move.
    return result["chosen"] or max(result["probabilities"], key=lambda p: p["probability"])["option"]

print(next_move(
    "You are Grumble, a patient ogre. You wait for the knight to make a mistake, then punish it.",
    """Your health: 22/50. Potions left: 1.
Knight's health: 30/50.
Last turns:
Knight used Heavy swing. Grumble took 16 damage.""",
    1,
))

How it works

  1. Your code

    List the moves

    Each move is an option with a description of what it does. Put the game rules in the descriptions: damage, what a guard blocks, when a potion works. Sage2 weighs the moves by these words. Precise rules give sensible fights.

    Runs with the helper from Get the code.

    MOVES = [
        {
            "option": "strike",
            "description": "A quick hit for 8 damage. Half damage against a guard.",
        },
        {
            "option": "heavy_swing",
            "description": "A slow hit for 16 damage. Does nothing against a guard.",
        },
        {
            "option": "guard",
            "description": "Cuts the damage of the next hit.",
        },
        {
            "option": "drink_potion",
            "description": "Restores 15 health. Only works if a potion is left.",
        },
    ]
  2. Choice

    Ask for the next move

    Choice docs

    Each turn, send the game state as content: both health bars, who is guarding, potions left and the last few turns. The personality goes in instructions, followed by "What do you do next?".

    Offer only the moves that are legal right now. With no potion left, drink_potion is not in options, so Sage2 can't pick it.

    Sage2 returns the move in chosen, with a probability for every move.

    When two moves are too close to call, chosen is null. A person can't step in mid-fight. This code takes the most likely move from probabilities.

    Runs with the helper from Get the code.

    def next_move(personality, state, potions):
        # Offer only legal moves: with no potion left, leave drink_potion out.
        options = [m for m in MOVES if potions or m["option"] != "drink_potion"]
        result = sage("/decide", {
            "content": state,
            "question": {"id": "next_move", "kind": "choice",
                         "instructions": f"{personality} What do you do next?", "options": options},
        })["result"]
        # A close call returns null: go with the most likely move.
        return result["chosen"] or max(result["probabilities"], key=lambda p: p["probability"])["option"]
    Example Sage2 output
    {
      "id": "next_move",
      "kind": "choice",
      "result": {
        "chosen": null,
        "probability": null,
        "probabilities": [
          {
            "option": "strike",
            "probability": 0.2909
          },
          {
            "option": "heavy_swing",
            "probability": 0.1963
          },
          {
            "option": "guard",
            "probability": 0.1573
          },
          {
            "option": "drink_potion",
            "probability": 0.3554
          }
        ]
      },
      "meta": {
        "model": "levanto-sage-2",
        "latency_ms": 99.1,
        "question_count": 1,
        "usage": {
          "billed_input_tokens": 178,
          "image_count": 0,
          "image_tokens": 0,
          "input_tokens": 178,
          "output_tokens": 0
        }
      }
    }
  3. Your code

    Play the turn

    Call the function from step 2 with a personality, the current state and the potions left. Then apply the move with your own game logic. Here the patient ogre has just taken a heavy swing and is down to 22 health.

    Runs with the helper from Get the code.

    print(next_move(
        "You are Grumble, a patient ogre. You wait for the knight to make a mistake, then punish it.",
        """Your health: 22/50. Potions left: 1.
    Knight's health: 30/50.
    Last turns:
    Knight used Heavy swing. Grumble took 16 damage.""",
        1,
    ))

Before you ship

  1. Offer only legal moves. Leave drink_potion out when no potion is left. Sage2 picks only from the moves you offer.
  2. Say who's guarding. Add a line such as "The knight is guarding." to the state. The ogre can then skip a heavy swing that would hit a guard.
  3. Keep the state short. The last few turns are enough.
  4. Let the player talk. Add the player's taunt to content, as the live demo does.
  5. Fast enough for a turn. A move comes back in about 70 to 150 ms. Ask while the player's attack plays.

Sage / Levanto

Start free.

Sign in or create an account.

Continue with Google
or with email

We’ll email you a 6-digit code.