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
You, the knight50/50 · 1 potion
Grumble the ogre50/50 · 1 potion
- 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
- Your code
List the moves
Each move is an option with a
descriptionof 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.", }, ] Each turn, send the game state as
content: both health bars, who is guarding, potions left and the last few turns. The personality goes ininstructions, followed by "What do you do next?".Offer only the moves that are legal right now. With no potion left,
drink_potionis not inoptions, so Sage2 can't pick it.Sage2 returns the move in
chosen, with aprobabilityfor every move.When two moves are too close to call,
chosenisnull. A person can't step in mid-fight. This code takes the most likely move fromprobabilities.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 } } }- 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
- Offer only legal moves. Leave
drink_potionout when no potion is left. Sage2 picks only from the moves you offer. - 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.
- Keep the state short. The last few turns are enough.
- Let the player talk. Add the player's taunt to
content, as the live demo does. - Fast enough for a turn. A move comes back in about 70 to 150 ms. Ask while the player's attack plays.