The rules¶
The reference of the functions and constants a game sees. The same API runs on the board: if the simulator and the board diverge, it is a bug of the simulator.
Four tables, gfx, pad, sys, phy, and require. Nothing else is reachable: the VM is sandboxed, there is no file system, no wall clock, no arbitrary loading. The tables are described on their own pages: gfx, phy, pad, sys and require; the limits in Constants and limits.
Five rules¶
1. Tables, not flat names. gfx.sprite, not gfx_spr. The flat form only saved one GETFIELD on an interned string, about 0.1 % of a frame with 200 sprites, and a game that writes local spr = ... no longer shadows anything. For a hot loop, local sprite = gfx.sprite gives a GETUPVAL and is worth it in either scheme.
2. The logical frame is the call to _update(), not the clock. There is no dt. Speeds are in pixels per frame, rates in frames per image. The console slows down, it never skips.
3. A game must replay bit for bit. The leaderboard server replays the trace of the inputs with the same engine and must find the same score, on three different machines. Hence the integer arithmetic of the engine, the absence of the transcendental functions, and the refusal of floats at the boundary.
4. The order of the calls to gfx.sprite() IS the paint order, the last entry is in front. There is no other priority mechanism between sprites.
5. The game folder. It holds all its art, in palette groups; gfx.bundle() installs it in one go, once. Everything after that is a choice made in the game: the active palette, the background, a sprite.
The skeleton¶
gfx.bundle() -- game.pal, game.atlas, game-*.png, game-*.map
local ship = gfx.id('game/ship') -- the id of a region: <group>/<name>
local flame = phy.anim_load('game/flame', 6)
function _update() -- the logic, once per frame
local p = pad.get() -- any pad
...
phy.step()
end
function _draw() -- DESCRIBES the frame: nothing is drawn here
gfx.sprite(ship, x, y)
gfx.print(8, 8, 'SCORE ' .. score, 2)
end
_update() then _draw() are called at every frame; the display list restarts between the two, only the gfx.sprite calls of _draw() show. _draw() does not modify the state: under load the console may skip it. A walkthrough is in your first game.
The sandbox¶
Open libraries: base, table, string, math, and nothing else. io, os, package and debug are never opened, rather than removed afterwards.
Erased: dofile, loadfile, load, and the standard require, replaced by the one described in sys and require.
Removed from math: sin, cos, tan, asin, acos, atan, exp, log. They call the platform's libm, and musl's sinf does not return the same bits as glibc's; one bit is enough for a game to no longer replay. Use sys.sin and sys.cos.
Remain: sqrt, fmod, floor, ceil, abs, min, max, tointeger, type, modf, ult, random, randomseed. All exact to the bit: math.random is a fully integer xoshiro256** since Lua 5.4.
math.randomseed() without an argument is refused
It is the form that makes a game impossible to rank: a seed nobody chose cannot be replayed. Seed explicitly.
Two known holes, which cannot be closed without forking the interpreter: the ^ operator goes through pow() as soon as the exponent is not 2, and tostring() of a float goes through the libc's %.14g. A game that branches on either is not guaranteed to replay.
Integers and floats¶
Lua 5.4 distinguishes the two, and the console is compiled in 32 bits: integer is an int32, float a float32, 24 bits of mantissa only.
| expression | type | why |
|---|---|---|
3 |
integer | |
3.0 |
float | the point is enough |
4 / 2 |
float | / always returns a float |
7 // 2 |
integer | integer division |
2 ^ 3 |
float | ^ too |
math.floor(3.7) |
integer | |
math.sqrt(4) |
float |
math.type(x) returns "integer", "float" or nil. When in doubt, it decides.
Beyond 16 777 216, every other integer no longer exists as a float. A position of 400.5 px is 26 247 168 in Q16.16 fixed point, well above: passing it through a float would cost it its low bits, silently. That is why all the phy.* functions refuse a float, even one that lands exactly, with a message that names the remedy.