Two parts of a game often need to coordinate without reaching into each other's data: a shop and a player's wallet, a spawner and the things it spawns, an inventory and the UI. In SCUA each of these is a partition, an isolated actor with its own private state. Partitions never share memory; they coordinate by sending messages. This page builds a shop and a player that trade with each other, so each one owns its own state and the only thing that crosses between them is a message.
#A partition with state and handlers
A partition declares some state and some
handlers. An on handler is fire-and-forget. An
ask handler replies to the sender with return.
You create an instance by calling the partition name, and you send to it
method-style.
partition Shop
state prices = { sword = 50, potion = 10 }
ask Price(item)
return prices[item]
end
end
partition Buyer
on Ask(shop, item)
let p = ask shop.Price(item) timeout 2s
print(`{item} costs {p}`)
end
end
let shop = Shop()
let buyer = Buyer()
tell buyer.Ask(shop, "sword")
tell buyer.Ask(shop, "potion")
Run it:
$ scua shop.scua
sword costs 50
potion costs 10
A few things are happening. Shop() and
Buyer() create instances.
tell buyer.Ask(shop, "sword") sends the Ask
message and moves on without waiting. Inside that handler,
ask shop.Price(item) timeout 2s sends a request and
suspends until the shop replies or the timeout passes, then continues
with the reply bound to p. The shop reference travels
inside the message, which is how the buyer knows who to talk to.
These programs use a simulated clock, so the 2s timeout
costs no real wall-clock time. Run them with --fast so the
clock fast-forwards instead of waiting. See Partitions and the actor
model and Concurrency and time
for the full model.
#Two systems coordinating
Now give the shop something to change and the player a budget to spend. The player asks for a price, decides whether it can afford the item, and only then tells the shop to complete the sale. The shop owns the stock and the sale count; the player owns its gold and its bag. Neither touches the other's state.
-- Two systems that each own their state and coordinate only by messages.
partition Shop
state prices = { sword = 50, potion = 10 }
state sold = 0
-- request/response: reply with `return`
ask Price(item)
return prices[item]
end
ask Buy(item)
sold = sold + 1
print(`[shop] sold {item} (sale #{sold})`)
return prices[item]
end
end
partition Player
state shop = nil
state gold = 0
state bag = []
on Start(s, g)
shop = s
gold = g
end
on TryBuy(item)
let price = ask shop.Price(item) timeout 2s -- suspends until the shop replies
if gold >= price then
let paid = ask shop.Buy(item) timeout 2s
gold = gold - paid
bag.push(item)
print(`[player] bought {item} for {paid}; gold left {gold}`)
else
print(`[player] can't afford {item} ({price} > {gold})`)
end
end
on Report()
print(`[player] bag = {bag}, gold = {gold}`)
end
end
let shop = Shop()
let hero = Player()
tell hero.Start(shop, 60)
tell hero.TryBuy("sword") -- 50: affordable
tell hero.TryBuy("potion") -- 10: affordable, spends the rest
tell hero.TryBuy("sword") -- 50: too expensive now
tell hero.Report()
Run it:
$ scua trade.scua --fast
[shop] sold sword (sale #1)
[player] bought sword for 50; gold left 10
[shop] sold potion (sale #2)
[player] bought potion for 10; gold left 0
[player] can't afford sword (50 > 0)
[player] bag = [sword, potion], gold = 0
Read the output top to bottom and you can see the conversation. The
player asks the shop's price, the shop replies, the player checks its
own gold, and if it can pay it tells the shop to Buy, which
mutates the shop's stock and replies with the cost. The third
TryBuy finds the wallet empty and never sends a
Buy at all, so the shop's sale count stays at two. The
ordering is clean because ask suspends the player's handler
until the reply arrives, so each purchase finishes before the next
begins.
The payoff is in what isn't shared. The player never reads
prices and the shop never reads gold. The only
coupling is the set of messages they agree on: Price,
Buy. You could move the shop to another machine, persist
it, or swap its implementation, and as long as it answers the same
messages the player doesn't notice. Each system is responsible for
exactly its own state, and a bug in one can't corrupt the other's
memory, because there is no shared memory to corrupt.
From here, the same shape scales to the patterns you'd actually
build: a spawner that tells newly created enemies their
starting orders, an inventory that a UI partition asks to
render, a matchmaker that hands players references to a game-session
partition. The mechanics are the ones above: state for what
a system owns, on and ask for what it responds
to, and tell and ask to drive it.