Owns and invariants
owns gives a field to its system, so a write from elsewhere is an error. invariant states a rule that shot run checks after every frame.
You'll learn owned fields, rules that always hold, and how a broken rule reads.
A field one system owns
Wallet.Balance should only change through Wallet:Add, which also keeps the change in the history. Today that is a comment, and a write from elsewhere quietly skips the history. One line in the system's Types.shot gives the field to the system:
-- Change Balance only through-- Wallet:Add: Add also keeps the-- change in the history.function Wallet.Add(self: Wallet, amount: number, reason: Types.Reason)
doc { Add also keeps the change in the history; a write to Balance does not}owns Wallet.Balance via Wallet:Add
owns Wallet.Balance via Wallet:AddWallet/the constructor, Add, VipWallet's overridewrite Balance freelywallet.Balance = 0an error: change it with Wallet:Addowns Class.Field via Module.Functionnames a class or module of this folder, its field, and the function that changes it (Class:Methodfor a method). One line per field.- The doc above it is the why: the error quotes it.
- It compiles to nothing; it only adds a check, on
=,+=,-=and the rest.
Write it from Stats, with a function that empties a wallet (and Wallet added to the import in Stats/Types.shot):
doc { Empties a wallet } function Reset(wallet : Wallet) wallet.Balance = 0 end
./Stats/Stats.shot:13:10 Wallet.Balance belongs to Wallet: change it with Wallet:Add instead (Add also keeps the change in the history; a write to Balance does not) 1 errors in 1 file (16 files checked, 3.7 s)
Watch out A write is matched by the object's type: a typed parameter or local, self, module, a use name. An untyped local, a loop variable or a map element (Bank.Wallets[player].Balance = 0) is not followed.
A rule that always holds
No wallet should hold less than 0 coins. Instead of an assert after the writes you remember, state the rule once, in words and as a function:
function ShopServer.Buy(player, item) -- ... wallet:Add(-price, "Shop") assert(wallet.Balance >= 0)end
doc { No wallet holds less than 0 coins: the shop sells only what the player can pay}invariant Bank.NoneBelowZero
The function is the module's own, and the game never calls it:
doc { Rule: no wallet is below 0 } function NoneBelowZero() : (boolean, string?) for player, wallet in module.Wallets do if wallet.Balance < 0 then return false, player.Name .. " has " .. wallet.Balance end end return true, nil end
shot run checks every rule after every frame, and fails the scenario at the first frame one breaks.invariant Module.Functiongoes in aTypes.shot, with its doc above it: the rule in words, printed when it breaks.- The module is a server or shared module of the system's own folder, so rules are checked on the server.
- The function takes nothing and returns
booleanor(boolean, string?): false and a short text with the values that broke it. - It only reads:
shot checkrefuses a rule that writes a field, fires a signal or waits.shot buildcompiles nothing for the line.
Break it
A player with 5 coins asks for a Sword priced at 10:
-- Broke: a player with 5 coins asks for a Sword priced at 10. return function(world) local Bank = world.Server.Require("Wallet.Bank") world:Boot() local ana, client = world:AddPlayer("Ana") world:Step(0.1) Bank.Give(ana, 5) client.Require("Wallet.Shop").Client.Buy("Sword") world:Step(0.1) world.Logf("Ana has %s", Bank.Wallets[ana]:Describe()) end
==== Broke [client:Ana print] client started Ana has 5 coins ==== summary Broke OK (2 lines, 0 errors, 0.2s virtual) 1 of 1 scenario(s) passed
Now someone drops the if wallet.Balance >= price then line and its end from ShopServer.Buy. The types are fine, shot check says 0 errors, and the scenario has no assert. The rule catches it:
==== Broke
[client:Ana print] client started
Ana has -5 coins
---- 1 invariant(s) broken: a rule declared in a Types.shot failed between two frames (each once, at its first frame)
invariant broken: No wallet holds less than 0 coins: the shop sells only what the player can pay (Bank.NoneBelowZero, Wallet/Types.shot:26)
first at frame 7 (0.12s virtual), after log line 1, scenario Scenarios/Broke.luau:9: Ana has -5
==== summary
Broke FAILED (2 lines, 0 errors, 1 invariant(s) broken, 0.2s virtual)
0 of 1 scenario(s) passedTip The scenario line is the world:Step that ran the frame; the change that broke the rule ran after the log line it names. Fix the code that broke the rule, never the rule, unless it is no longer true.
Put module.Count = 0 at the top of NoneBelowZero and run shot check: the rule writes module.Count, and a rule only reads, because a rule that changes the game would change what it checks.
