Ward Seven is a night shift in a veterinary clinic, shipped as a single
.rbxl place. Two bugs nearly killed it, and they failed in opposite directions: one took the
entire server script down before the ward was built, and one left a desk standing inside the player's
head. Neither produced an error a player could read — the game just wasn't there.
The brief was a genre and two reference points, not a design document — a story horror game in a veterinary clinic, in the register of Animal Hospital, with "anomalies" as the mechanic. That is enough to fix the whole shape of it: one shift, one corridor, one thing wrong per night, and a report you have to be right about.
Both messages are the same message. The place opened, Studio ran it, and what the player got was a baseplate with a dead HUD. The explanation for why is the most useful part of this build.
The build stage ran a lighting setup, and the last line of it set the height at which fallen parts are destroyed:
-- Ward.server.lua, in buildLighting() workspace.FallenPartsDestroyHeight = -300
In a live play session that property is capability-restricted — writable by a plugin or an open cloud session, not by a game script. Studio's own log had the whole story:
Error: The current thread cannot write 'FallenPartsDestroyHeight'
(lacking capability PluginOrOpenCloud)
Script 'ServerScriptService.Ward', Line 140 - function buildLighting
Script 'ServerScriptService.Ward', Line 149
Line 149 was a bare top-level call. In Luau an uncaught error terminates the Script it happened in — and that Script was the entire game: player state, remote events, ward construction, the shift loop, the report handler. Every one of those came after line 149 and none of them ever ran. The corridor, the doors, the kennels, the desk — all of it was inside a Script that had already stopped.
The failure is server-side and swallowed. The player still spawns, because spawning is engine
behaviour, not game code. There is no error on screen, no missing-asset warning, no red text — just a
flat baseplate and a HUD with no values in it. The only place the truth existed was
~/Library/Logs/Roblox/*_Studio_last.log.
Guarding the write stops this bug. Making every boot step go through a named stage stops the class of bug:
-- the guard: a capability refusal must never cost the boot
pcall(function() workspace.FallenPartsDestroyHeight = -300 end)
-- the wrapper: every stage is named, every failure is reported
local function stage(name, fn)
local ok, err = pcall(fn)
if not ok then
warn(("[WardSeven] build stage '%s' failed: %s"):format(name, tostring(err)))
end
return ok
end
Now a stage can fail and the ward still builds, and the failure arrives with a name attached. The boot
log ends with a count: WARD SEVEN v1.1.0 loaded — 0 stage(s) failed.
With the ward finally building, the report came back: “theres no desk, wth”. The ward was there. The desk was not visible. It was authored like this:
| What | Value | Consequence |
|---|---|---|
| Position | z = START_Z − 1 | its near face sat 0.5 studs behind the spawn point — the player spawned inside it |
| Helper used | deco() | CanCollide = false — walk-through scenery |
| Camera | third person | sits ~10 studs behind the character → buried in the middle of the block |
Three properties multiplied into an object that could not be seen and could not be felt. Roblox culls
backfaces, so from inside a solid box you see through it; with CanCollide = false there
wasn't even a bump to tell you it existed. A desk that is both invisible and intangible is
indistinguishable from no desk — and a screenshot from inside it would have shown an empty corridor.
Two smaller things were wrong in the same corner, and finding them is why the fix was worth doing
properly: the charge station sat three studs from the spawn point, right in front of the camera, and the
corridor entity spawned inside the intake wall at START_Z + 2 — so "something is
standing at the end of the corridor" had never once been visible from the corridor.
Desk is now a solid counter (slab(), CanCollide = true), 5 × 3.2 × 3,
against the left wall 14 studs down the corridor, with the monitor standing on it. The spawn moved in to
START_Z − 9, so there is corridor in front of you instead of furniture. The entity starts
inside the corridor at START_Z − 2. The terminal desk you report at is solid too, so you
stop at it instead of drifting through.
Both bugs are invisible to the checks that are easy to write. The place file serialises perfectly with a dead script inside it; the source compiles; every class name and property name is real; the desk exists in the instance tree at the right coordinates. What neither check does is run the game.
So the pipeline runs the real scripts, verbatim, against a mock Roblox engine. Lune loads the
.lua files off disk — the same bytes that go into the place — and executes them with a fake
Instance.new, a fake workspace, fake remotes, fake players and a fake
RunService heartbeat. The game does not know the difference.
1/6 rules and story pure logic, no Instances — 139 assertions 2/6 API surface 282 class/property/enum names checked against rbx-dom 3/6 build place file roblox.serializePlace 4/6 verify place file deserialise it again, compile every source, 37 checks 5/6 server smoke test the real server script × 200 heartbeats — 146 checks 6/6 client smoke test the real HUD script against every server state — 40 checks ───── publish to a path Studio has never opened ✓ playcheck deserialise the PUBLISHED file, assert the build id and fixes
The first mock was permissive: workspace accepted any write, so the boot-fatal bug passed
the suite. The fix was to make the mock refuse what a live session refuses:
-- smoke_test.luau: the mock reproduces the capability boundary
if key == "FallenPartsDestroyHeight" then
error("The current thread cannot write 'FallenPartsDestroyHeight' "
.. "(lacking capability PluginOrOpenCloud)", 2)
end
Then the guard was removed on purpose, as a negative control, to prove the suite would have caught the original bug:
FAIL server script executes without error
([string "luau.load(...)"]:170: The current thread cannot write
'FallenPartsDestroyHeight' (lacking capability PluginOrOpenCloud))
FAIL ward built for the joined player
RESULT: FAIL — the server never built a ward; stopping here
Restored, and it passes. A test that has never been seen to fail is not evidence; it is decoration.
The desk regression is geometric, so it needed geometric assertions. Section 10 of the smoke suite walks the ward the way a player would and asks the questions a player would:
| Check | What it proves | Failure text if it breaks |
|---|---|---|
| the desk exists at the intake end | the builder actually ran, and furniture is where the brief says | Desk:missing |
| the desk is solid | it is CanCollide = true — you cannot walk through a counter | the desk is solid, not scenery you walk through |
| nothing occupies the spawn point | no part's volume contains the player's spawn position | nothing is inside the spawn point |
| the desk is not on the walk line | the corridor centreline is clear to the far door | Desk:blocks-the-walk-line |
| the corridor entity starts inside the corridor | the "something standing" anomaly can be seen from inside | the figure starts outside the ward |
Putting the old desk back made two of them fail in the same run — the solidity check and the walk-line check. Both are structural, neither depends on a screenshot, and both run in under a second.
The mechanic is a single-bit puzzle repeated eight times: one thing is wrong, or nothing is.
That only works if the anomaly is unambiguous and the "clean" state is memorable, so the catalogue is
fixed at sixteen entries — each one mutating specific named parts, and each one carrying an explicit
restore so nights cannot leak state into each other.
| Anomaly | Room | Weight | First night | What changes |
|---|---|---|---|---|
| DoorAjar | EXAM 1 | 10 | 1 | door rotates 46° into the corridor |
| DoorShut | SURGERY | 8 | 2 | the normally-ajar door is pulled shut |
| ExtraDoor | CORRIDOR | 7 | 3 | frame, door and sign appear at z = 16 |
| LightsRed | CORRIDOR | 8 | 3 | all fixtures + point lights go red, brightness 1.6 |
| Flicker | CORRIDOR | 9 | 2 | folder attribute FlickerMode = "fast" |
| LightsOff | CORRIDOR | 6 | 4 | housings go ceiling-colour, point lights disabled |
| ChairTurned | EXAM 2 | 10 | 1 | chair rotated to face the wall |
| PosterSwapped | EXAM 2 | 8 | 1 | poster text replaced |
| ClockWrong | INTAKE | 8 | 2 | clock hands moved |
| EmptyKennel | KENNELS | 9 | 1 | kennel 3 emptied, card left up |
| ExtraCage | KENNELS | 7 | 3 | a fifth cage built into a four-cage row |
| Footprints | CORRIDOR | 8 | 4 | wet print trail across the floor |
| Stain | CORRIDOR | 8 | 2 | dark stain on the right wall |
| Figure | CORRIDOR | 6 | 5 | a silhouette at the far end |
| GurneyMoved | CORRIDOR | 7 | 4 | gurney out of surgery, blocking half the width |
| Bell | SURGERY | 6 | 6 | a bell appears above the surgery door |
The smoke suite applies all sixteen and restores all sixteen, asserting that the ward returns to its base state each time — because an anomaly that fails to restore would make the next night's clean state wrong, and every night after it would be unwinnable.
Stand within 11 studs of the desk, press E, answer once. Right answer → the night advances and a page of story arrives. Wrong answer → a strike, the corridor turns red, and something starts walking behind you, slower than a sprint. Three strikes → the log closes and the night restarts at 00:12.
The pure state machine behind that lives in WardLogic.lua with no Instances and no
services, which is why 139 assertions can run against it in milliseconds with a deterministic
math.random stub. Everything the player feels — the pacing, the forgiveness, the ramp of the
eight nights — is tunable in WardConfig.lua and nothing else.
| Constant | Value | Why that number |
|---|---|---|
| ReportRadius | 11 | far enough that you do not have to find the exact desk stud, close enough that you must come back |
| SprintDrain | 3.4 /s | ≈ 22 s of running in a whole night — enough to escape once, not enough to sprint the shift |
| StrikesAllowed | 3 | two wrong guesses are survivable; the third resets the night |
| WARD.Length | 96 | long enough for a walk back to be a decision, short enough to learn in one lap |
| WARD.HalfWidth | 6 | 12 studs wide: two abreast, and the gurney blocks half of it |
| Artifact | Detail | Status |
|---|---|---|
| WardSeven-b3-desk.rbxl | 39,597 bytes · sha256 28cc3afd31d04bfc… | opens in Studio |
| WardSeven-src.zip | 64,670 bytes · sha256 5311f5e1eb7867a6… | 2,812 lines + pipeline |
| rules and story | 139 passed, 0 failed | pass |
| API surface | 282 names · self-test rejects a bogus class and property | pass |
| place file | 37 checks after deserialisation | pass |
| server smoke | 146 checks · 8-night playthrough to an ending | pass |
| client smoke | 40 checks · 71 instances · every server state rendered | pass |
| playcheck | 6 markers read out of the published file | pass |
| negative control: boot guard removed | suite reports the capability refusal and stops | fails as designed |
| negative control: old desk restored | solidity + walk-line assertions fail | fails as designed |
They do not prove it is frightening. A mock engine proves that a report resolves, that a desk is solid and that eight nights can be walked to an ending. It cannot prove the corridor is unnerving, that the fourth night's silence lands, or that the thing behind you reads as a threat rather than a pathing bug. That judgement needs a player, and the only player here is the one who said "theres no desk".
There is no literal type check in Ward's six stages — the Neon Ledge pipeline has one, and it
exists because Trail.WidthScale = NumberSequence.new(1.1) is a name-valid, type-invalid
assignment that the API check happily passes. Ward gets away with it because the client smoke test
renders every state, which catches most of the same class of error indirectly. It is a gap, not a
design choice.
Studio holds a .rbxl.lock for as long as the process lives — closing the window
is not enough. Rebuilding the file underneath an open session silently changes nothing, and the player
reports the old bug again. The build id is in the filename (b3-desk), the HUD stamp and
the playcheck stage for exactly this reason: the file you opened and the build you think you have are
two different questions.