This is a hands-on tour of SCUA. By the end you'll have written a small turn-based battle — fighters, elemental types, a damage formula, and a loop that runs the fight — and you'll have met most of the language along the way.
Every snippet here runs. Press ▶ Run on any block to
execute it right on the page — that's the real compiler, built to
WebAssembly, running in your browser. Want room to experiment, or a
blank slate? The playground is a full editor. Or, to
work locally, install the compiler
and put each block in a .scua file.
No setup required. Every code block has a ▶ Run button that runs it right here.
#1. Printing and values
The smallest SCUA program prints something:
print("hello, battle")
SCUA has the value kinds you'd expect: integers and floats
(3, 3.5), strings, booleans (true
/ false), and nil for "nothing". Only
nil and false are falsey — 0 and
"" are both truthy, which trips up newcomers from other
languages.
The friendliest way to build a string is a backtick
template. Anything inside {...} is an expression,
evaluated and dropped into place:
let name = "Aria"
let level = 3
print(`{name} is level {level}`)
print(`next level needs {level * 100} xp`)
One sharp edge worth learning now: + is arithmetic only.
To join strings, use ..:
print("super" .. "effective") -- supereffective
#2. Functions
Functions are declared with fn ... end.
return hands a value back:
fn damage(attack, defense)
let raw = attack - defense
return if raw > 0 then raw else 1 end -- always chip at least 1
end
print(damage(12, 5)) -- 7
print(damage(4, 20)) -- 1
Notice if ... then ... else used as an
expression — it evaluates to a value. That's the same
if you'd use as a statement; when you use it as a value,
the else is required.
No type annotations so far, and the program is complete without them. That's SCUA's default: dynamic, like a scripting language. When you want a guarantee, you add a type — which is the next step.
#3. Records give your data a shape
A record is a named type with a fixed set of fields.
This is how you model game entities. You build one as a table literal
assigned to a binding with the record's type — that annotation is what
turns an ordinary table into a checked Fighter:
record Fighter { name: string, hp: int, atk: int }
let hero: Fighter = { name = "Aria", hp = 30, atk = 7 }
print(`{hero.name}: {hero.hp} hp, {hero.atk} atk`)
The field types (string, int) are checked.
Give a field the wrong type — or leave one out — and the compiler stops
you before the program runs:
record Fighter { name: string, hp: int, atk: int }
let broken: Fighter = { name = "Glitch", hp = "lots", atk = 7 }
$ scua broken.scua
scua: broken.scua:3: type error: field 'hp' is string, expected int
A record's declared fields are mutable, so a turn is just an assignment. Here one fighter strikes another, lowering the target's health in place:
record Fighter { name: string, hp: int, atk: int }
fn strike(attacker: Fighter, target: Fighter)
target.hp = target.hp - attacker.atk
end
let goblin: Fighter = { name = "Goblin", hp = 18, atk = 4 }
let hero: Fighter = { name = "Aria", hp = 30, atk = 7 }
strike(hero, goblin)
print(`{goblin.name} is down to {goblin.hp} hp`)
#4. Elemental types with enums and match
Real games have rock-paper-scissors type charts. An enum is a closed set of named variants:
enum Element { Fire, Water, Grass }
let attack = Element.Fire
print(attack == Element.Fire) -- true
print(attack == Element.Water) -- false
(Under the hood a variant is just an integer, so
print(attack) would show 0, not
Fire — you compare and match on them rather
than printing them.) The payoff is match, which tests a
value against patterns. Because the enum is a closed set, the
compiler checks that your match covers every case (or has a
_ catch-all). Here's a damage multiplier for a classic
triangle:
enum Element { Fire, Water, Grass }
fn multiplier(attack: Element, defend: Element) -> int
return match [attack, defend]
[Element.Fire, Element.Grass] -> 2
[Element.Water, Element.Fire] -> 2
[Element.Grass, Element.Water] -> 2
_ -> 1
end
end
print(multiplier(Element.Fire, Element.Grass)) -- 2
print(multiplier(Element.Fire, Element.Water)) -- 1
match matched on a two-element array
[attack, defend] and destructured it in each pattern. That
same matching works on records, arrays, and
Ok/Error values.
#5. The battle loop
Now we put it together. A while loop runs turns until
someone drops. We give each fighter an element and fold the type
multiplier into the damage.
enum Element { Fire, Water, Grass }
record Fighter { name: string, hp: int, atk: int, element: Element }
fn multiplier(a: Element, d: Element) -> int
return match [a, d]
[Element.Fire, Element.Grass] -> 2
[Element.Water, Element.Fire] -> 2
[Element.Grass, Element.Water] -> 2
_ -> 1
end
end
fn strike(attacker: Fighter, target: Fighter)
let dmg = attacker.atk * multiplier(attacker.element, target.element)
target.hp = target.hp - dmg
end
let hero: Fighter = { name = "Aria", hp = 30, atk = 7, element = Element.Fire }
let foe: Fighter = { name = "Leafling", hp = 24, atk = 5, element = Element.Grass }
let round = 1
while hero.hp > 0 and foe.hp > 0 do
strike(hero, foe)
if foe.hp <= 0 then break end
strike(foe, hero)
print(`round {round}: {hero.name} {hero.hp} hp {foe.name} {foe.hp} hp`)
round = round + 1
end
let winner = if hero.hp > 0 then hero.name else foe.name end
print(`winner: {winner}`)
Press Run and watch Aria's fire chew through the grass type. Change the elements, the stats, the formula — it's your game now.
#6. Make the fight persistent
Here's the part that makes SCUA different. Wrap the battle's state in a partition and it becomes a self-contained block of bytes — one a host can snapshot to move the live game to another machine, or park and bring back later, with no serialization code.
partition Battle
state hero = "?"
state foe = "?"
state round = 0
on Begin(a, b)
hero = a
foe = b
round = 1
print(`a wild {foe} appears! {hero} steps up.`)
end
on Turn()
round = round + 1
print(`...round {round}: {hero} attacks {foe}...`)
end
end
let game = Battle()
tell game.Begin("Aria", "Leafling")
tell game.Turn()
tell game.Turn()
Battle() creates an instance with its own private state.
tell sends it a message; only the partition's own handlers
touch its state, one message at a time, so there's nothing to lock.
Because that state is a self-contained block of bytes, a host can
snapshot the whole battle and bring it back elsewhere — the same
isolation that makes message passing safe.
That snapshot is the live image of the running game: ideal for moving a session between machines, or parking it and resuming it later in the same build. For saves that must survive your code changing — new fields, reshaped data, a version bump — SCUA has a separate, typed durable-save format that validates and migrates as it loads. The two are different tools; both are covered in Partitions and the actor model and Save, load, and migrate game state.
#Where to go next
You've now used values, functions, records, enums,
match, loops, and partitions — the spine of the
language.
- Core concepts — the four big ideas, explained properly.
- The manual — a feature-by-feature guide, plus the reference.
- How-to guides — money, files, testing, embedding, serving HTTP, running many things at once, and more.
- Playground — keep experimenting; it's the fastest feedback loop there is.