Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Words and data

Games are full of words: names on a high-score table, lines of dialog, what someone typed into a chat. And they’re full of data: levels, item lists, scores. This lesson is the toolbox for both.

Cutting and shaping text

len is how many characters a string has. left, right and mid cut a piece out of one, counting from 1. ucase and lcase change its case, and trim takes the spaces off both ends (ltrim and rtrim take them off one).

SCREEN 320, 240
typed = "   crash-tan   "
name = trim(typed)
PRINT "[" + name + "] has", len(name), "letters"
PRINT ucase(left(name, 1)) + mid(name, 2)
PRINT "initials:", ucase(left(name, 1) + mid(name, 7, 1))
PRINT "ends with", right(name, 3)
[crash-tan] has 9 letters
Crash-tan
initials: CT
ends with tan

str turns a number into text and val turns text back into a number. string(n, c) repeats a character and space(n) is that many spaces, which lines up a table of scores nicely:

SCREEN 320, 240
names = ["Ada", "Bob", "Crash-tan"]
points = [1200, 900, 15000]
FOR i = 0 ... 2
    dots = string(16 - len(names(i)) - len(str(points(i))), ".")
    PRINT names(i) + " " + dots + " " + str(points(i))
NEXT
PRINT "next level at", val("2000") + points(0)
Ada ......... 1200
Bob .......... 900
Crash-tan .. 15000
next level at 3200

Finding and changing

instr finds one string in another and says where, or 0 when it isn’t there. replace swaps every copy of one string for another. split cuts a string into a list at a separator, and join glues a list back together.

Here they are reading what a player typed into a chat box:

SCREEN 320, 240
FUNCTION chat(line)
    IF instr(line, "/") = 1
        words = split(mid(line, 2), " ")
        PRINT "command:", words(0), "with", len(words) - 1, "more:", join(words, "+")
    ELSE
        PRINT "says:", replace(line, "heck", "h*ck")
    END IF
END FUNCTION

chat("/give sword 3")
chat("what the heck is that")
command: give with 2 more: give+sword+3
says: what the h*ck is that

Letters as numbers

Every character has a number. asc gives a character’s number and chr the character for a number, so going from one letter to the next is adding one. That’s how an arcade machine lets you pick your initials with the joystick:

SCREEN 320, 240
letter = "A"
FOR press = 1 ... 3
    letter = chr(asc(letter) + 1)
NEXT
PRINT "three presses up from A is", letter, "which is number", asc(letter)
PRINT "255 in hex is", hex(255)
three presses up from A is D which is number 68
255 in hex is FF

hex writes a number in hexadecimal, which is handy for showing colors the way 0xFF8000 writes them.

Is it the right shape?

match checks a string against a pattern where * is anything and ? is any one character: good for file names. regex takes a full regular expression, for checking what someone typed. This one checks a name for the high-score table: a letter, then two to eleven more letters, digits or dashes.

SCREEN 320, 240
FOR name = ["Ada", "x", "Crash-tan", "99 Bob"]
    ok = regex(name, "^[A-Za-z][A-Za-z0-9-]{2,11}$")
    PRINT name, "fits the table?", ok
NEXT
PRINT match("level3.map.json", "level?.map.json"), match("hero.png", "*.wav")
Ada fits the table? 1
x fits the table? 0
Crash-tan fits the table? 1
99 Bob fits the table? 0
1 0

What day is it?

date is today, as month/day/year, and time is the time now, as hours:minutes:seconds, on the player’s clock.

SCREEN 320, 240
PRINT "Today is", date, "and it's", time
// The same seed all day, a new one tomorrow.
RANDOMIZE val(replace(date, "/", ""))
PRINT "today's lucky number:", int(rnd * 100)

That’s a daily challenge: everyone who plays today gets the same lucky number, and tomorrow it changes. It’s the seed trick from Handy maths for games.

Data from a file

loadjson reads a JSON file into maps and lists. The level in your game’s folder is one:

SCREEN 320, 240
level = loadjson("level.map.json")
PRINT "tiles", level.tilewidth, "by", level.tileheight
PRINT "map", level.mapwidth, "by", level.mapheight, "tiles"
PRINT "first row:", len(level.layers(0).data(0)), "tiles"
tiles 32 by 32
map 20 by 23 tiles
first row: 20 tiles

The file says mapWidth. I don’t mind how a field’s name is capitalized, so level.mapwidth finds it.

Asking data questions

When data gets deep, jmes asks it questions in JMESPath, a little language for JSON. [?points > `1000`] keeps the items that pass a test, .name picks one field out of each, and max_by and sort_by find the best and put them in order.

SCREEN 320, 240
scores = [{name: "Ada", points: 1200}, {name: "Bob", points: 900}, {name: "Cy", points: 1500}]
PRINT "over 1000:", jmes(scores, "[?points > `1000`].name")
PRINT "the best:", jmes(scores, "max_by(@, &points).name")
PRINT "low to high:", jmes(scores, "sort_by(@, &points)[*].name")

level = loadjson("level.map.json")
PRINT "layers:", jmes(level, "layers[*].name")
over 1000: [Ada, Cy]
the best: Cy
low to high: [Bob, Ada, Cy]
layers: [Layer 1, Layer 2, Metadata]

A high-score table

For lots of records that you’d like to sort and search, a database does the work. My databases speak SQL. An SQL block names a statement: USE lines say what it’s given and of what kind, and in the SQL, @name is where each goes. open_database opens the file (!! writable to change it as well as read it), and call_sql runs a statement, with what it’s given after !!.

SCREEN 320, 240
SQL "make"
    CREATE TABLE IF NOT EXISTS scores (name TEXT, points INTEGER)
END SQL
SQL "add"
    USE STRING name
    USE NUMBER points
    INSERT INTO scores (name, points) VALUES (@name, @points)
END SQL
SQL "top"
    USE NUMBER how_many DEFAULT 3
    SELECT name, points FROM scores ORDER BY points DESC LIMIT @how_many
END SQL

db = open_database("scores.db") !! writable
call_sql(db, "make")
call_sql db, "add" !! name("Ada") points(1200)
call_sql db, "add" !! name("Bob") points(900)
call_sql db, "add" !! name("Cy") points(1500)
call_sql db, "add" !! name("Dee") points(300)

PRINT "TOP THREE"
FOR row = call_sql(db, "top")
    PRINT row.name, row.points
NEXT
best = call_sql(db, "top") !! how_many(1)
PRINT "champion:", best(0).name
CLOSE db
TOP THREE
Cy 1500
Ada 1200
Bob 900
champion: Cy

A statement that reads gives back its rows, a map each. how_many has a DEFAULT, so it can be left out. The table is kept in the file scores.db, so the next time the game opens it, the scores are still there, and new ones join them.

Crash-tan’s Tip: Always give values with USE and @name rather than gluing them into the SQL yourself. A player called Bob'); DROP TABLE scores; -- is then just a player with a strange name.