SCUA

How-to

Have systems talk to each other with actors

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.