The editor

Python

The box on the right is real Python with a fence around it. Lessons only ask for one call at a time. This page is the rest of what that box will run: the buttons, the two ways a snippet is treated, every builtin, and every word it refuses.

The panel

Creation, town, the wilds, and a fight all use the same box. It sits on the right. The text area blinks a caret. A grey line is the current lesson’s example. It is a hint, not code, until you type it. The equip lesson is the exception: it writes your class weapon into the box for you.

Under the text is an output pane. Run starts it with the word Running and a copy of what you sent. A failure turns that pane red and says Code Error. The longer story — look, combat, gather — also lands in the status log under the map. You can select that log and copy it. When you change rooms, the last snippet is put back in the box.

F5, or the Run button

Sends the whole box, top to bottom. F5 works while you are still typing.

Get Help

Prints the command list for the room you are in: town, wilds, or combat. It does not run your snippet. hero.help() prints the same list into the status log.

Docs

The button is on the panel. It is not connected, so it does not open this codex or any other page.

F1, Toggle Editor

Settings lists this key. Pressing it does not hide the box. The editor stays on screen. Escape, F5, and F1 can all be rebound under Settings, then Apply. Reset to Defaults puts them back.

Escape pauses in town, the wilds, and combat. It does nothing on the title, on creation, or inside Settings. Pause offers Resume, Settings, and Quit to Title. Settings opened from pause returns you to the same room and opens pause again. Quit on the title closes the window. Quit to Title does not.

Two ways a snippet runs

The box does not treat every script the same. A stack of plain calls is checked one line at a time. The moment the box contains an assignment, an if, a for, a while, a def, a lambda, or a comprehension, the whole snippet switches to the Python runner. A comment or a blank line does not switch it. Creation never switches: that screen only accepts the four lesson calls, and a variable will not stand in for a quoted word.

Plain calls

Each line is one call. They run from the top. Creation allows print, set_name, choose_race, and choose_class, and the words must be in quotes. After that, a line has to be a hero call that belongs in the room. The command book is that list. print("hello") by itself is refused in town, the wilds, and a fight.

Python runner

Used for decisions, loops, names, and the builtins below. Hero calls inside it still have to make sense for the room: rest works in town, attack works in a fight. Each hero call counts toward a cap of 200 in one snippet. The 201st stops the script.
hero.look()
hero.move_right(2)

Those two lines stay on the plain path. Add one name and the same calls run as Python instead:

steps = 2
hero.look()
hero.move_right(steps)

Shape of a Python snippet

Lines run from top to bottom. A colon starts a block, and the next lines indent four spaces: if, elif, else, for, while, def, and try. Mix tabs and spaces and the box reports a syntax error. A line that starts with # is a comment and does nothing. Blank lines are fine.

Names you invent live only until the snippet ends. The hero does not. A function you define is gone the next time you press Run, unless you paste it again. If a line fails, the rest of that snippet does not run. Steps that already happened stay happened. Fix the line and send it again.

The name box on creation cannot be typed in. set_name("Aldric") writes it. Back to Menu on that screen throws away a half-finished hero and returns to the title.

Values you can write

Numbers

12 is a whole number. 1.5 is a fraction. Quotes make it text, so "12" is not 12. Arithmetic is + − * / . // drops the fraction. % is the remainder. ** is a power. 7 // 3 is 2. 7 % 3 is 1.

Text

Quotes, single or double. "bread" and 'bread' are the same. A quote of the other kind can sit inside. An f-string inserts a value: f"HP {hero.hp}".

Yes, no, nothing

True, False, and None. Capital letters matter. and, or, and not combine tests. Compare with ==, !=, less than, greater than, and the or-equal forms. in asks whether a value is inside a list or a string.

Collections

A list uses square brackets, like [1, 2]. A dict pairs a key with a value, id with bread and qty with 1. A tuple is a fixed pair in parentheses. A set keeps each value once. Square brackets pick an item: row["id"], bag[0], steps[1:3].

Decisions

if runs a block only when the test is true. elif tries the next test. else runs when none of them were. The test itself is not a call you have to wrap.

hp = hero.hp
if hp < 8:
    hero.use("minor_hp_potion")
elif hero.in_combat():
    hero.attack()
else:
    hero.look()

Repeating

for walks a list or a range. while repeats until its test is false. break leaves the loop early. continue skips to the next pass. A while that can never become false, and that never calls the hero, does not hit the 200-call stop. It will sit there. Give every while a counter or a test that can fail.

for n in range(3):
    hero.move_right()

steps = 0
while steps < 4 and hero.hp > 0:
    hero.move_down()
    steps = steps + 1

range(3) yields 0, then 1, then 2. It does not include 3. hero.move_right(3) is different: that one call walks three tiles.

Functions you define

def names a block you can run more than once in the same snippet. return hands a value back and leaves the function. Arguments can have defaults. lambda is a one-line function, useful for sorted or filter. It cannot hold a hero call comfortably; use def for that.

def walk(n):
    for i in range(n):
        hero.move_right()

walk(3)

double = lambda n: n * 2
print(double(4))

try and a bare except catch a failure so the snippet can choose another call. The name Exception is not in the box, so write except: with nothing after the colon.

try:
    hero.use("minor_hp_potion")
except:
    hero.attack()

Lists the hero hands back

Several calls return data you can store, but only on the Python path, because storing uses an assignment. inventory_list returns rows of id, name, and qty. foes returns name, hp, and max_hp, in the order attack uses. recipe_available, recipe_list, enchant_list, shop, look_stations, and skills return ids. look_nodes returns names. gather, attack, skill, and use return True or False.

Read a field with square brackets: row["id"]. A dot on that row is refused, so row.get("id") and items.append(x) do not work. Build a new list with + . A method on a value written right there is allowed, such as "Bread".lower() or (1, 2, 3)[0], because the quotes or the brackets are in the line. A method on a name you created is not.

bag = hero.inventory_list()
ids = []
for row in bag:
    ids = ids + [row["id"]]
    print(row["name"], row["qty"])

recipes = hero.recipe_available()
if recipes:
    hero.craft(recipes[0])

Builtins

These names exist only on the Python path. They are not hero calls, and they do not count toward the 200. map and filter and reversed hand back a walker, not a list. Wrap them in list() if you want to index them.

WordUse
printWrite a line into the status log. print(hero.hp) or print("ready").
rangeA count for a for loop. range(3) is 0, 1, 2. range(1, 4) is 1, 2, 3. range(0, 8, 2) steps by 2.
lenHow many items are in a string, list, dict, tuple, or set.
intWhole number. int("3") is 3. int(2.9) cuts the fraction.
floatA number with a fraction. float("1.5") is 1.5.
strTurn a value into text. str(hero.gold) is a string of digits.
boolTrue or False. bool(0) and bool("") are False. Anything else is usually True.
minThe smaller of the values. min(hero.hp, 10).
maxThe larger of the values. max(1, hero.level).
absDrop the minus sign. abs(-4) is 4.
roundNearest whole number, or round(1.234, 2) for two decimals.
divmodPair of quotient and remainder. divmod(7, 3) is (2, 1).
powPower. pow(2, 3) is 8, the same as 2 ** 3.
sumAdd a list of numbers. sum([1, 2, 3]) is 6.
chrThe letter for a number. chr(65) is "A".
ordThe number for a letter. ord("A") is 65.
listMake a list. list(range(3)) is [0, 1, 2].
dictMake a dict. dict(name="Aldric") is {"name": "Aldric"}.
tupleMake a tuple, a list you do not change. tuple([1, 2]) is (1, 2).
setMake a set: each value once, no order you can count on.
enumerateWalk a list with a counter. for i, row in enumerate(bag):
zipWalk two lists in pairs. for a, b in zip(left, right):
sortedA new list, smallest first. sorted(numbers, reverse=True) flips it.
reversedWalk a list from the end. Wrap it with list() if you need to store it.
anyTrue if one item is true. any(row["qty"] > 0 for row in bag) needs a script.
allTrue only if every item is true.
mapApply a function to each item. Store it with list(map(...)).
filterKeep the items that pass a test. Store it with list(filter(...)).
isinstanceAsk the kind. isinstance(hero.hp, int) is True.
TrueThe yes value. Not a call. Written with a capital T.
FalseThe no value. Capital F.
NoneNo value yet. Capital N. A call that has nothing to give back returns None.

What you may read on the hero

A dot is allowed on the word hero, and only one dot deep. hero.hp is your current hit points. Also plain values: hero.gold, hero.level, hero.name, hero.mana, hero.stamina, hero.x, and hero.y. x and y are the column and the row of the tile you stand on. hero.in_combat() is True while a foe is still standing.

hero.race.something and hero.equipment.something are a second dot, so they are refused. Use the list calls and square brackets instead. You can overwrite hero.hp or hero.gold with an assignment. That skips shops and damage. Play with the calls.

if hero.hp < 10 and "minor_hp_potion" in [row["id"] for row in hero.inventory_list()]:
    hero.use("minor_hp_potion")
else:
    hero.attack()

Words the box refuses

WordWhy
importNo modules. Everything you may use is already in the box.
withNot available. Open and close things with a hero call instead.
async, await, yieldNo paused or background functions.
classA class tries to put a dot on self. The box only allows a dot on hero.
Names starting with _Private. _floor, __init__, and hero._hp are refused.
open, inputNo files, and the box cannot stop to ask you to type.
exec, eval, compileYou cannot run a second hidden program.
help, dir, globals, locals, varsThe builtin help is blocked. Use hero.help() or Get Help.
getattr, setattr, hasattr, delattrNo looking up a name from a string.
type, object, super, propertyNo building new kinds of objects.
id, hash, repr, ascii, formatInspection and low-level text tools are blocked.
bytes, bytearray, memoryview, sliceRaw memory tools are blocked. Use a list and square brackets.
exit, quit, breakpointThose stop a terminal, not this game. hero.quit() closes the window.
classmethod, staticmethod, callableNot part of the fenced Python.

A dot on any name except hero fails with “Only attributes on hero are available.” That includes a string you stored, a list you stored, and self inside a class.

How a failure reads

Syntax error

The line is not Python yet: a missing colon, a broken quote, mixed indentation. Nothing in the snippet runs.

Code Error

Python started, then a line failed. The pane names the kind of error. “is not defined” means that word is not hero and not a builtin. Fix it and run again.

Not in combat

attack, skill, and use only work after you have walked onto a foe. The log says so.

200 hero actions

The script stops itself. Split a long walk into more than one Run, or walk with hero.move_right(3) instead of three calls if one call is enough.

Three snippets

Each one is a Python snippet because it uses a name or a for. Paste one, press F5, and read the status log.

Walk a short path

for step in range(3):
    hero.move_right()
hero.move_down()

Drink, or swing

if hero.hp < 8:
    hero.use("minor_hp_potion")
else:
    hero.attack()

Make the first recipe the bag can afford

recipes = hero.recipe_available()
if recipes:
    hero.craft(recipes[0])
else:
    print("Nothing to craft.")

The lesson order, and what each screen is asking for, stays on Lessons. Every hero call, room by room, stays in the command book.