Verse Wiki — the interactive handbook for Verse developers
REFERENCE · HANDBOOK

Syntax Cheat Sheet: Every Verse Move on One Page

A one-page companion to the official Verse Quick Reference: the core syntax from all 23 lessons, condensed — each topic gets a minimal snippet, a one-line takeaway, and the lesson number that covers it in depth. Stuck mid-code? Come back for a quick scan, then follow the lesson number to fill the gap.

1. Variables, Constants & Type Annotations

variables.verse
# Constants: defined once, fixed for life; := lets the compiler infer the type
MaxHealth:int = 100
Nickname := "Bubbles"

# Variables: declared with var and a type annotation; every change must go through set
var Health:int = 100
set Health = 80
set Health += 15

▢ Key point: three symbols, three jobs — := only handles the initial definition, set … = only assigns to a var, and a bare = is a failable comparison; there is no ==. Details in Lesson 4.

▢ Walk through it: the top half creates two locked-for-life constants (like creating a variable and never wiring a Set node to it); the bottom half's var Health is a variable you can change — and every change has to go through set (that's your Set node). Remember the three symbols: := only handles the very first definition, set … = is how you change the value afterwards (wiring a Set), and a lone = is an equality check that can fail — and there is no == at all. Details in Lesson 4.

types.verse
Count:int = 42                        # integer
Speed:float = 3.5                     # float; literals must carry the decimal point
Title:string = "UE6"                  # string; interpolate with "HP: {Count}"
var Ready:logic = false               # boolean: true / false
Names:[]string = array{"Bubbles", "Archie"}   # array
Table:[string]int = map{"key" => 1}    # map
var MaybeN:?int = false               # option; false means empty
Pair:tuple(int, float) = (1, 2.0)     # tuple

▢ Key point: annotations are always "name:type"; int and float never convert implicitly — turn an int into a float by multiplying by 1.0, and a float into an int with the failable Floor[X]. Numbers in Lesson 5, strings in Lesson 6, logic in Lesson 7.

▢ Walk through it: every line reads "name:type = value" — you decide up front what the variable holds (integer, float, text, Boolean, array, map, a maybe-empty option box, a tuple), just like picking the variable type first when you create a variable in Blueprint. Two traps: int and float never auto-convert — go int → float by multiplying by 1.0, and float → int with the might-fail Floor[X] (round down); and always write the decimal point in float literals. Numbers in Lesson 5, strings in Lesson 6, logic in Lesson 7.

2. Operators

operators.verse
if (A = B, B <> C):            # equality is =, inequality is <>; there is no ==
    Print("The comma acts as and")
if (X > 0 and Y > 0):          # logic words and / or / not; no && || !
    Print("The whole comparison family is failable expressions")
if (Ready?):                   # postfix ?: succeeds only if the logic is true
    Print("A bare if (Ready) will not compile")

▢ Key point: comparison operators do not return booleans — they participate in control flow through success/failure, which is why they can only live inside a failure context. Details in Lesson 9.

▢ Walk through it: all three ifs look like a Branch, but note what they're asking is not "is this Boolean true" — it's "does the thing in the parentheses go through". So equality is a single =, inequality is <> (there is no ==), logic is spelled and / or / not (no && || !), and a comma works as and too; to use a Boolean as a condition, tack on a ?. In one line: these comparisons don't spit out true/false — they join the flow as "goes through / doesn't", so they can only sit where failure is allowed. Details in Lesson 9.

Symbol What it does Notes
:= Define and initialize First definition only; the type can be omitted
set X = … Assign to a var Compound assignment += -= *= /= (no /= for int)
= <> < <= > >= Comparison, failable Only allowed inside failure contexts
+ - * / Arithmetic int / int is failable and yields rational (a fraction type): if (Half := Floor(X / 2)):
Mod[A, B] Remainder There is no %; square brackets = failable call
and or not Combine success/failure No && || !; effects inside not always roll back
X? Query Succeeds if the logic is true / the option is non-empty

▢ Read this table with Blueprint instincts: := creates the variable and gives it its first value; set X = … is the Set node changing the value (+= -= and friends mean "add to / subtract from what's there"); the whole row of comparison symbols (= for equals, <> for not-equals, < > and so on) only works where failure is allowed; in arithmetic, integer ÷ integer "might fail" and yields a fraction, so use Floor to get an integer back; there is no % for remainder — write Mod[A, B] (square brackets = a call that might fail); logic connectives are only ever the three words and / or / not; and the trailing X? translates a Boolean or a box into "goes through / doesn't".

3. Control Flow: if / for / loop / defer / case

if.verse
# The condition slot is a failure context: if any step in the chain fails, the whole thing takes else
if (Player := player[Agent], Fort := Player.GetFortCharacter[]):
    Print("Got the character, carrying on")
else:
    Print("Some step in the chain failed")

Max := if (A > B) then A else B    # if is an expression; you can take its value

▢ Key point: a failure context is a transaction — any set inside the condition rolls back wholesale on failure, as if it never happened; that's also why non-rollbackable calls like Print are banned inside conditions. Details in Lesson 8.

▢ Walk through it: the upper if is like a chain of Casts — first convert the Agent to a player, then fetch the character entity from it; if any link doesn't go through, the whole thing takes the else. Below it, if (A > B) then A else B uses if as a pick-one-of-two (think Select node) to compute the larger value directly. The key temperament: a "place that allows failure" is rollbackable — any value changed with Set inside the condition is undone wholesale on failure, as if it was never touched, which is also why irreversible actions like Print String aren't allowed inside the condition parentheses. Details in Lesson 8.

for.verse
for (Item : Items):                # iterate over the array's elements
    Print("{Item}")
for (Index -> Elem : Items):       # with the index, starting at 0
    Print("No. {Index}: {Elem}")
for (N := 1..5, N > 2):            # range includes both ends; a failed filter only skips that round
    Print("{N}")
Squares := for (X := 1..5) { X * X }   # for is an expression; collect into a new array

▢ Key point: there is no break/continue inside for; express "skip" with a filter clause, and get list comprehension by taking the value of the for itself. Details in Lesson 10.

▢ Walk through it: four flavors of ForEach — take each element; take the index along with it; run over a numeric range (with an optional filter bolted on — a round that doesn't pass just gets skipped); and collect each round's result into a fresh array. Note that Verse's ForEach has no break / continue: to "skip a round", write it as a filter condition; to "collect as you go", just use the whole for's result as an array. Details in Lesson 10.

loop.verse
var Round:int = 0
OnBegin<override>()<suspends>:void =
    loop:
        set Round += 1
        if (Round > 3):
            break              # break belongs to loop only; not allowed in for
        Sleep(1.0)             # at least one suspension point per round, or it counts as an infinite loop

▢ Key point: Verse has no while — it's always loop plus a conditional break; a synchronous loop that neither breaks nor suspends triggers ErrRuntime_InfiniteLoop and every bit of Verse on the island shuts down. Details in Lesson 11.

▢ Walk through it: loop is an execution wire that circles back on itself and repeats forever (your While Loop); each round bumps the counter by 1, breaks out once it passes 3, and ends with a Sleep(1.0) (a Delay) so it rests a second before the next lap. Two iron rules: break belongs to loop alone — you can't use it in a ForEach; and if a synchronous loop neither breaks nor takes a Delay, you trigger the infinite-loop error ErrRuntime_InfiniteLoop and every bit of Verse on the island goes on strike. Details in Lesson 11.

defer.verse
Countdown()<suspends>:void =
    Print("Countdown started")
    defer:
        Print("Runs only when leaving the scope — even on an early exit")
    Sleep(3.0)
    Print("Three, two, one")

▢ Key point: a defer only counts once control flow has actually passed it and registered it; written in a branch that never runs, it never executes. Details in Lesson 11.

▢ Walk through it: the line inside defer is the wrap-up wire that is guaranteed to run last, right before this function exits — so even though it's written in the middle, it actually executes at the end. The catch: execution has to pass the defer first and check it in for it to count; if it's hiding on a branch that never gets walked, it won't fire. Details in Lesson 11.

case.verse
game_phase := enum{Lobby, Battle, Podium}

PhaseLabel(Phase:game_phase):string =
    case (Phase):
        game_phase.Lobby => "Lobby"
        game_phase.Battle => "Battle"
        game_phase.Podium => "Podium"

▢ Key point: for a default (closed) enum, covering every member in the case means no fallback branch is needed; miss one and the compiler calls it out by name. Details in Lesson 7.

▢ Walk through it: case is Blueprint's Switch on Enum — it branches on whichever member the enum currently is, here turning each of the three game phases into a label to hand back. The payoff: cover every member of the enum and you don't need an "other" fallback branch; miss one, and the compiler names it the moment you hit Compile. Details in Lesson 7.

4. Containers: array / map / option / tuple

array.verse
var Numbers:[]int = array{1, 2, 3}
if (First := Numbers[0]):            # indexing is failable; it must live inside an if
    Print("The first one is {First}")
if (set Numbers[0] = 9) {}           # changing an element can fail too
set Numbers += array{4}              # append: concatenates into a new array
Print("{Numbers.Length} in total")   # .Length never fails

▢ Key point: arrays are immutable values — there is no .Add/.Push; going out of bounds doesn't crash, it just "fails" into the else. Details in Lesson 12.

▢ Walk through it: build an array; take element 0 by index — but that step "might not go through", so it's wrapped in an if; changing an element and appending follow the same pattern (appending actually concatenates a brand-new array); .Length is the steady one — it never fails. Key point: Verse arrays are "read-only values" with no in-place .Add / .Push; an out-of-range index won't crash anything — that step simply fails and takes the else. Details in Lesson 12.

map.verse
var Scores:[string]int = map{"Bubbles" => 10}
if (Score := Scores["Bubbles"]):     # lookups can fail: no such key takes else
    Print("Found {Score} points")
if (set Scores["Archie"] = 0) {}     # even writes go inside an if
for (Name -> Score : Scores):       # iterates in insertion order
    Print("{Name}: {Score}")

▢ Key point: literals pair with =>, iteration reads out with ->; there is no Remove — deletion means filter-and-rebuild. Details in Lesson 13.

▢ Walk through it: build a "name → score" register (a Map); looking up a score by name "might find nothing", so it goes inside an if; even writing a new key goes inside an if; finally a ForEach reads every pair back out in insertion order. Don't mix up the two arrows: => pairs entries in the literal, -> reads them out while iterating; and there is no Remove — to delete, filter out what you want to keep and rebuild the map. Details in Lesson 13.

option.verse
var MaybeScore:?int = false          # false is the "empty box"
set MaybeScore = option{42}          # put a value in with option{}, not option()
if (Score := MaybeScore?):           # postfix ? unwraps; fails if empty
    Print("The box holds {Score}")
MaybeFirst := option{Numbers[0]}     # jar a failable expression up into an option

▢ Key point: option{…} is itself a failure context — failure inside yields an empty box; it is the official bridge between "failure" and "no value". Details in Lesson 14.

▢ Walk through it: ?int is a box that "might hold an integer, might be empty" — false is the empty box; put things in with option{42}; to take it out, append a ? — an empty box doesn't go through and takes the else. The last line stuffs a might-fail lookup straight into option{…} — because option{…} is itself a "place where failure is allowed": if the step inside fails there's no error, you just get an empty box. It is the official bridge between "doesn't go through" and "no value". Details in Lesson 14.

tuple.verse
Point:tuple(int, float) = (1, 2.0)   # fixed-length bundle of mixed types
X := Point(0)                        # parenthesis access, checked at compile time, cannot fail

GetXY():tuple(float, float) = (3.0, 4.0)   # multiple return values = return a tuple
Result := GetXY()
Print("x = {Result(0)}, y = {Result(1)}")

▢ Key point: tuples use parentheses T(0), arrays use square brackets A[0]; tuple indices must be compile-time constants — no variables as indices. Details in Lesson 14.

▢ Walk through it: bundle "one integer + one float" into a little package — parentheses to pack it, and parentheses again (Point(0)) to pull the nth item out; since how many items and each one's type are locked in at compile time, access cannot fail. GetXY() below shows the tuple's biggest job: letting one function hand back several return values at once. Remember: tuples read with parentheses T(0), arrays with square brackets A[0], and a tuple's index must be hardcoded — you can't use a variable as the index. Details in Lesson 14.

5. Function Definitions

functions.verse
Add(A:int, B:int):int = A + B        # implicit return: the last expression is the result

Award(?Points:int = 1):void =        # ? prefix = named optional parameter
    Print("Adding {Points} points")
# Calls: Award() uses the default; Award(?Points := 10) passes by name

Twice(F(X:int):int, N:int):int = F(F(N))   # functions can be passed as parameters too

▢ Key point: parameters are immutable — copy into a local var first if you need to change one; writing Foo without parentheses gives you the function itself, not a call. Details in Lesson 15.

▢ Walk through it: Add is the plainest Blueprint function there is — whatever it computes last automatically becomes the return value (no dedicated Return node needed). Award shows an optional parameter with a default (a ? before the name); callers can skip it or pass it by name. Twice goes further: a function can be handed to another function as a parameter. Two traps: incoming parameters are "read-only" — copy one into a local var before changing it; and writing a function's name Foo without parentheses gives you "the function itself", not the result of running it. Details in Lesson 15.

decides_fn.verse
# <decides> must appear paired with <transacts>: failure has to be able to roll back
IsBig(X:int)<transacts><decides>:void =
    X > 100

if (IsBig[200]):                     # failable functions are called with square brackets
    Print("Parentheses call normal functions, square brackets call failable ones")

▢ Key point: explicit return is banned inside a decides function body; to "exit with a value once found", collect into var Ret:?t = false and unwrap with Ret? on the last line. Details in Lesson 15 and Lesson 18.

▢ Walk through it: hanging <decides> on a function turns it into a Blueprint function that "might not go through" — if the X > 100 step inside doesn't pass, the whole function counts as failed; such a function must also carry <transacts> (rollbackable), and callers switch to square brackets: IsBig[200] (parentheses for normal functions, square brackets for failable ones). One trap: no return is allowed inside such a body; to "exit with the value once you've found it", the idiom is to prepare an empty box var Ret:?t = false to collect into, then crack it open with Ret? on the last line. Details in Lesson 15 and Lesson 18.

6. Type Definitions: class / struct / interface / enum

class.verse
enemy := class:
    Name:string                      # no default → must be filled in at instantiation
    var Health:int = 100

    TakeDamage(Amount:int):void =
        set Health -= Amount         # use Self inside methods to mean "myself"

Grunt := enemy{Name := "Grunt"}      # archetype instantiation; there is no new

▢ Key point: inheritance is child := class(base):, overriding a member requires <override>, calling the parent implementation is (super:)Method(); classes have reference semantics. Details in Lesson 16.

▢ Walk through it: this is defining a Blueprint class — list the fields (any without a default must be filled in when building an instance), write the methods (use Self inside them for "me"), and finally enemy{Name := "Grunt"} builds an instance (no new — just braces with the fields filled in). Inheritance is written child := class(base): (your Child Blueprint Class), overriding a parent member must carry <override> (that Override dropdown), and calling the parent's original is (super:)Method(); classes have "reference" semantics (you pass around the same instance, not a copy). Details in Lesson 16.

struct.verse
point := struct:
    X:float = 0.0
    Y:float = 0.0

P := point{X := 3.0, Y := 4.0}       # value semantics: assignment and passing both copy
Q := point{X := P.X, Y := 9.9}       # "modifying" = constructing a new instance

▢ Key point: structs allow no var fields and no methods — to add behavior, use an extension method: (S:point).Norm():float = …. Details in Lesson 17.

▢ Walk through it: struct is that Structure asset you create from the right-click menu — a small bundle of pure data. It has "value" semantics: a tweak like Q := point{X := P.X, …} actually builds a brand-new instance modeled on the old one; assigning and passing copy the whole bundle, and the copies never affect each other. House rules: no mutable var fields and no methods inside a struct — to give it behavior, bolt an "extension method" on from the outside: (S:point).Norm():float = …. Details in Lesson 17.

interface.verse
rideable := interface:
    Mount():void                     # signature only, no implementation

bicycle := class(rideable):
    Mount<override>():void =         # every interface method you implement needs <override>
        Print("Hop on, let's go")

▢ Key point: a class can inherit only one class but implement several interfaces at once: class(base, iface1, iface2); downcasting with if (C := child_type[Base]): is a failable expression. Details in Lesson 17.

▢ Walk through it: interface is your Blueprint Interface (BPI) — it lists function signatures only, no implementations; the class below signed up for the interface, so it has to fill in Mount, and implementing an interface method also carries <override>. House rules: a class can claim only one parent class but can sign up for several interfaces at once: class(base, iface1, iface2); to "downcast" a parent reference to a concrete subtype, use if (C := child_type[Base]): — a step that "might not go through", which is exactly Blueprint's Cast. Details in Lesson 17.

enum.verse
card_suit := enum{Clubs, Diamonds, Hearts, Spades}

MySuit := card_suit.Hearts           # members are reached through the type name
if (MySuit = card_suit.Hearts):      # only = and <> equality checks are supported
    Print("It's Hearts")             # want text? write your own case mapping

▢ Key point: enum members have no underlying integers — no converting to int/string, no ordering comparisons; definitions belong at module scope. Details in Lesson 7.

▢ Walk through it: enum is that Enumeration asset from the right-click menu — a few well-named options; take one via "type name.member name", and check whether it's a given one with = (only = and <> are supported). Key point: no integer hides behind an enum member — they can't be converted to numbers or text and can't be ordered; for display text, write a little case mapping yourself. Definitions go outside the class (module level). Details in Lesson 7.

7. Specifier Cheat Table

The ones in angle brackets are called specifiers, enforced by the compiler; the ones starting with @ are attributes, written on the line above a declaration, and they shape editor behavior.

The things in angle brackets <…> are called specifiers — hit Compile and the compiler checks you against them; the ones starting with @ are attributes, written on the line above the declaration, and they mostly affect how things behave in the UEFN editor (for example @editable floats a field up into the Details panel). Memorize the rows from this table you'll actually use.

Modifier In one line Where it goes
<public> <internal> <protected> <private> Access control; the default is internal (module-visible), not public After the identifier; on a var it can split read/write access: var<protected> Ammo<public>:int
<override> Overrides a parent member; mandatory After the member name
<abstract> Cannot be instantiated; may contain unimplemented methods class<abstract>
<final> Class can't be inherited / member can't be overridden class or member
<concrete> Every field has a default, so my_class{} instantiates from an empty archetype; required for @editable custom classes class<concrete>
<unique> Instances carry identity — comparable with = and usable as map keys (that's exactly where player comes from) class<unique>
<transacts> Actions can roll back; the only thing failure contexts welcome Effect: after the parameter list, before the return type
<decides> Failable, called with square brackets; must pair with <transacts> Effect
<suspends> Async, can suspend across frames; only directly callable from an async context Effect
<computes> <varies> <converges> Three grades of "no side effects" promise, pick exactly one; no label at all = no_rollback (cannot roll back) Effect
<persistable> Save-file eligible; the class must be <final> with constant fields only After class / struct
<localizes> Wraps an interpolated string into a message (the type UI text wants) Function / constant
@editable Exposes a field to the Details panel; change values without recompiling Attribute: the line above the field declaration

▢ Key point: effect specifiers are Verse's mechanism for writing "what this function may do" into the type signature — when an error mentions no_rollback / decides / suspends, check this table first. Details in Lesson 18.

▢ Every keyword in this table can light up red when you hit Compile. A few picks by Blueprint instinct: <override> = overriding a parent member (that Override dropdown); <concrete> = every field has a default, so you can build an instance bare-handed with {} — required for custom @editable classes; <unique> = instances carry an ID card and can be map keys (that's how players work); <transacts> = the action can roll back — the only kind welcome where failure is allowed; <suspends> = it waits across frames (its body has Delay-type nodes); @editable = the Instance Editable eyeball, lighting the field up in the Details panel. The "effect" family simply writes "what this function will do" into the signature — when an error mentions no_rollback / decides / suspends, come back to this table first. Details in Lesson 18.

8. The Five Concurrency Expressions

Expression Semantics Fate of the other tasks Where it's allowed
sync: Continues only when all finish; results packed into a tuple —— Async context
race: Ends the moment the first finishes; the value is the winner's Canceled at their next suspension point Async context
rush: Returns the moment the first finishes Keep running, wrapped up with the enclosing scope Async context
branch: Starts it; the main line continues Retired when the enclosing scope exits Async context
spawn{ Fn() } Releases an independent task, returns a task Not canceled; runs to completion on its own The only one usable in synchronous code

These five are all ways to "run several wires at once" — read them with Blueprint instinct: sync = wait for every wire to arrive before moving on (results bundled into a little pack); race = whoever finishes first calls it, and the other wires are cut on the spot (use it for timeouts); rush = also takes the first to finish, but the stragglers aren't cut — they get to run to the end; branch = fork off while the main line continues, with automatic cleanup when this stretch ends (a supervised spawn); spawn = fork off a wire whose lifetime nobody manages — the only one usable in ordinary synchronous code. The first four only work inside a "context that can wait".

race_timeout.verse
GateOnce()<suspends>:void =
    race:
        block:
            Button.InteractedWithEvent.Await()   # Button is an @editable reference
            Print("Pressed in time!")
        block:
            Sleep(10.0)
            Print("Timed out — moving on")

▢ Key point: the timeout recipe — race an event against an alarm clock, first one wins; wrap multi-line arms in block:, and the arms' result types must be compatible. Details in Lesson 20.

▢ Walk through it: race sets two wires racing — one waits for the button press (Await() suspends until the event fires; Button is the @editable reference wired up in the panel), the other runs Sleep(10.0) as the alarm clock; whichever arrives first wins, and the loser is cut on the spot. This is the standard timeout recipe: an event racing an alarm clock. When an arm has several lines, wrap them into one wire with block:, and the arms' final results need compatible types. Details in Lesson 20.

spawn.verse
OnPressed(Agent:agent):void =        # a Subscribe callback is not an async function
    spawn{ CloseAfterDelay() }       # a spawn body must be a single async call

CloseAfterDelay()<suspends>:void =
    Sleep(5.0)
    Print("Five seconds up — wrapping up")

▢ Key point: Epic's guidance is to prefer structured concurrency (sync/race/rush/branch) over spawn; Sleep(0.0) suspends until the next frame — the standard "wait one frame". suspends and the flow of time in Lesson 19, the five expressions in Lesson 20.

▢ Walk through it: the button's Subscribe callback OnPressed is not itself a function that can wait, so to do waiting work inside it, fork a new wire with spawn{ CloseAfterDelay() } (the braces take exactly one async call) and return immediately; the new wire runs Sleep(5.0) (a five-second Delay) and then wraps up. Reminder: if one of the four structured ones (sync/race/rush/branch) fits, don't bare-spawn; Sleep(0.0) is the standard way to write "wait one frame, then continue". Waiting and the flow of time in Lesson 19, the five expressions in Lesson 20.

9. Device Boilerplate: @editable + Subscribe

my_device.verse
using { /Fortnite.com/Devices }

my_device := class(creative_device):
    @editable
    Button:button_device = button_device{}    # wire up the real device in the panel

    OnBegin<override>()<suspends>:void =
        Button.InteractedWithEvent.Subscribe(OnPressed)

    OnPressed(Agent:agent):void =
        Print("A player pressed the button")

▢ Key point: the device only takes effect once you drag it from the Content Browser into the level and wire the @editable to a real device in the Details panel; the callback signature must match the event payload (a button sends agent); for async work inside a callback, spawn. First device in Lesson 2, events and @editable in Lesson 21, the full graduation project in Lesson 23.

▢ Walk through it: this is the skeleton of a minimal device — an @editable Button (that little eyeball in the panel; wire it to a real button once it's in the level); OnBegin is Event BeginPlay, and right at the start it uses Subscribe (Bind Event) to bind the button's "interacted" event to its own OnPressed; press the button and OnPressed runs and prints a line. To make it work: the device has to be dragged from the Content Browser into the level, with the @editable connected to the real thing in the Details panel; the callback's parameters must match what the event hands over (a button gives an agent); to do waiting work in the callback, spawn. First device in Lesson 2, events and @editable in Lesson 21, the full graduation project in Lesson 23.

10. Persistence Patterns

persist_coins.verse
# Module scope (outside the class) — the location itself is the persistence switch
var PlayerCoins:weak_map(player, int) = map{}

AddCoins(Player:player, Amount:int):void =
    if (Old := PlayerCoins[Player], set PlayerCoins[Player] = Old + Amount):
        Print("Coins credited — they survive across sessions")

▢ Key point: both reads and writes are failable operations; when a player first joins, lay down the initial value with if (set PlayerCoins[Player] = 0) {} before any accumulating. Details in Lesson 22.

▢ Walk through it: the coin-storing weak_map lives "outside the class" (module level) — that location is itself the save-game switch: the data gets one copy per player, kept automatically across sessions. Inside AddCoins, both reading the old value and writing the new one "might not go through", so the whole thing sits in one if (read the old value and set the new one back in a single stroke). Remember: the first time a player joins, lay the initial value with if (set PlayerCoins[Player] = 0) {} before any accumulating can happen. Details in Lesson 22.

persist_class.verse
player_profile := class<final><persistable>:
    Level:int = 1
    Coins:int = 0

var Profiles:weak_map(player, player_profile) = map{}
# Update = read the old instance → build a new one from its values → set the whole thing back

▢ Key point: weak_map's three can'ts — no Length, no iteration, keys are weak references; the key must be player, so cast an agent first with player[Agent]. Put evolving data in a class (after release, only classes can gain new fields with defaults). Details in Lesson 22.

▢ Walk through it: pack the data you want saved (level, coins) into a <persistable> class, then keep the instances in a player-keyed weak_map; the update pattern is "read the old instance → build a new one from the old values → set the whole thing back". Remember weak_map's three can'ts: you can't ask for a count, can't iterate, and the keys are weak references; the key must be a player — if you're holding an agent, convert it first with player[Agent] (Cast to Player). Data that will grow fields later is safest in a class (after release, only classes can add new fields with defaults). Details in Lesson 22.