Files
tokyo-wangan-racing/scripts/verify_map.luau
T
jesse 766fce5088 test: add automated map verification harness
Eight geometric assertions against the built map, runnable from Linux over
the Studio MCP bridge. Each check tests the world that exists rather than
trusting that a build reported success.

Check 1 is the important one: it fresh-requires Circuit and asserts
823/200/474. Studio caches ModuleScripts, so a stale Circuit will happily
build a plausible-looking map of the wrong circuit -- which is exactly what
happened before. The shape checks are meaningless without it.

Found and fixed while writing this: a Sound with a doubled rbxassetid://
prefix in the imported CarLibrary chassis.

Probe note: pillar tops are flush with the deck by design (0.00 clearance),
so the car-envelope probe must ride fully above the road surface or it
reports its own supports as intrusions.
2026-07-31 08:25:57 -04:00

239 lines
7.2 KiB
Luau

--!strict
-- verify_map.luau — automated map regression test.
--
-- Run from Linux:
-- python3 -c "import sys;sys.path.insert(0,'/tmp');import rbx;
-- print(rbx.call('execute_luau',
-- {'code':open('scripts/verify_map.luau').read(),'datamodel_type':'Edit'},
-- wait=90,timeout=600))"
--
-- Exits with a PASS/FAIL summary. Every check is a real geometric assertion
-- against the built map -- no check trusts that a build "ran".
--
-- IMPORTANT: this must be run against a map built from CURRENT source. Studio
-- caches ModuleScripts, so a stale Circuit will silently pass the shape checks
-- while describing the wrong circuit. Check 1 guards exactly that.
local RS = game:GetService("ReplicatedStorage")
local EXPECT_SAMPLES = 823
local EXPECT_TUNNEL = 200
local EXPECT_BRIDGE = 474
local ROAD_HALF = 24 -- ROAD_WIDTH 48 / 2
local CAR_HALF_W = 5 -- 10-stud car
local CAR_HEIGHT = 5.5
local results: { string } = {}
local failures = 0
local function check(name: string, ok: boolean, detail: string)
if ok then
table.insert(results, (" PASS %-34s %s"):format(name, detail))
else
failures += 1
table.insert(results, (" FAIL %-34s %s"):format(name, detail))
end
end
-- Fresh-require the circuit so a cached stale module can't mask a bad build.
local orig = RS.Shared:FindFirstChild("Circuit")
if not orig then
return "FATAL: ReplicatedStorage.Shared.Circuit missing"
end
orig.Name = "_CircuitOld"
local fresh = orig:Clone()
fresh.Name = "Circuit"
fresh.Parent = RS.Shared
local ok, C = pcall(require, fresh)
local function cleanup()
fresh:Destroy()
orig.Name = "Circuit"
end
if not ok then
cleanup()
return "FATAL: Circuit require failed: " .. tostring(C)
end
local s = C.samples
local n = #s
-- 1. Source freshness ------------------------------------------------------
local nTunnel, nBridge = 0, 0
for i = 1, n do
if s[i].tunnel then
nTunnel += 1
end
if s[i].bridge then
nBridge += 1
end
end
check(
"circuit is current source",
n == EXPECT_SAMPLES and nTunnel == EXPECT_TUNNEL and nBridge == EXPECT_BRIDGE,
("samples=%d tunnel=%d bridge=%d"):format(n, nTunnel, nBridge)
)
-- 2. Loop closure + spacing ------------------------------------------------
local minGap, maxGap = math.huge, 0
for i = 1, n do
local d = (s[(i % n) + 1].pos - s[i].pos).Magnitude
minGap = math.min(minGap, d)
maxGap = math.max(maxGap, d)
end
check("loop closed & evenly spaced", maxGap - minGap < 5, ("gap %.1f..%.1f studs"):format(minGap, maxGap))
-- 3. Grade is drivable -----------------------------------------------------
local maxGrade, gradeAt = 0, 0
for i = 1, n do
local a, b = s[i].pos, s[(i % n) + 1].pos
local flat = (Vector3.new(b.X, 0, b.Z) - Vector3.new(a.X, 0, a.Z)).Magnitude
if flat > 0.1 then
local g = math.abs(b.Y - a.Y) / flat
if g > maxGrade then
maxGrade, gradeAt = g, i
end
end
end
check("max grade drivable (<15%)", maxGrade < 0.15, ("%.1f%% at sample %d"):format(maxGrade * 100, gradeAt))
local map = workspace:FindFirstChild("Map")
if not map then
cleanup()
return "FATAL: workspace.Map missing -- run MapBuilder first"
end
local exp = map:FindFirstChild("Expressway")
-- 4. Continuous driving surface -------------------------------------------
local rp = RaycastParams.new()
rp.FilterType = Enum.RaycastFilterType.Include
rp.FilterDescendantsInstances = { exp }
local holes, worstOffset = 0, 0
for i = 1, n do
local hit = workspace:Raycast(s[i].pos + Vector3.new(0, 12, 0), Vector3.new(0, -40, 0), rp)
if not hit then
holes += 1
else
worstOffset = math.max(worstOffset, math.abs(s[i].pos.Y - hit.Position.Y))
end
end
check("no holes in road surface", holes == 0, ("%d gaps, worst offset %.2f studs"):format(holes, worstOffset))
-- 5. Tunnel bore count matches flags --------------------------------------
local walls, ceilings = 0, 0
for _, d in ipairs(map:GetDescendants()) do
if d.Name == "TunnelWall" then
walls += 1
elseif d.Name == "TunnelCeiling" then
ceilings += 1
end
end
check(
"tunnel bore matches flags",
walls == nTunnel * 2 and ceilings == nTunnel,
("%d walls (want %d), %d ceilings"):format(walls, nTunnel * 2, nTunnel)
)
-- 6. Nothing intrudes into the car envelope -------------------------------
-- Probe sits ON the deck: centre lifted by half the car height, so the box
-- never dips below the road and false-positives on the pillars beneath it.
local probe = Instance.new("Part")
probe.Size = Vector3.new(CAR_HALF_W * 2, CAR_HEIGHT, 20)
probe.Anchored = true
probe.CanCollide = false
probe.Transparency = 1
probe.Parent = workspace
local op = OverlapParams.new()
op.FilterType = Enum.RaycastFilterType.Exclude
op.FilterDescendantsInstances = { probe, exp }
op.MaxParts = 8
local expected = {
KillWall = true,
Guardrail = true,
TunnelCeiling = true,
TunnelLight = true,
RoadLight = true,
BoxGirder = true,
PierCap = true,
Cable = true,
Baseplate = true,
}
local intrusions = 0
local firstHit = ""
for i = 1, n do
local a, b = s[i].pos, s[(i % n) + 1].pos
local dir = b - a
if dir.Magnitude > 0.1 then
-- Pillar tops are flush with the deck (0.00 clearance by design), so the
-- probe must sit fully ABOVE the road surface or it clips its own
-- supports. Lift a further 1.5 studs of margin.
local mid = (a + b) / 2 + Vector3.new(0, CAR_HEIGHT / 2 + 2.0, 0)
probe.CFrame = CFrame.lookAt(mid, mid + dir.Unit)
for _, h in ipairs(workspace:GetPartsInPart(probe, op)) do
-- only judge the NEW map; legacy C1/City trees are separate content
local root = h
while root.Parent and root.Parent ~= workspace do
root = root.Parent
end
if root == map and not expected[h.Name] then
intrusions += 1
if firstHit == "" then
firstHit = h:GetFullName() .. " @sample" .. i
end
break
end
end
end
end
probe:Destroy()
check("car envelope clear", intrusions == 0, ("%d intrusions %s"):format(intrusions, firstHit))
-- 7. Supports stay under the deck -----------------------------------------
local badPillars, minGapUnder = 0, math.huge
for _, d in ipairs(map:GetDescendants()) do
if d.Name == "Pillar" and d:IsA("BasePart") then
local top = d.Position.Y + d.Size.Y / 2
local best = math.huge
for i = 1, n do
local flat = (Vector3.new(s[i].pos.X, 0, s[i].pos.Z) - Vector3.new(d.Position.X, 0, d.Position.Z)).Magnitude
if flat < best then
best = flat
if flat < ROAD_HALF then
local gap = s[i].pos.Y - top
minGapUnder = math.min(minGapUnder, gap)
if gap < 0 then
badPillars += 1
end
end
end
end
end
end
check(
"pillars stay below deck",
badPillars == 0,
("%d protruding, min clearance %.2f studs"):format(badPillars, minGapUnder == math.huge and 0 or minGapUnder)
)
-- 8. Asset hygiene ---------------------------------------------------------
local badSounds = 0
for _, svc in ipairs({ game:GetService("ServerStorage"), RS, workspace }) do
for _, d in ipairs(svc:GetDescendants()) do
if d:IsA("Sound") then
local _, count = d.SoundId:gsub("rbxassetid://", "")
if count > 1 then
badSounds += 1
end
end
end
end
check("no malformed sound ids", badSounds == 0, ("%d double-prefixed"):format(badSounds))
cleanup()
local header = failures == 0 and ("MAP VERIFY: ALL %d CHECKS PASSED"):format(#results)
or ("MAP VERIFY: %d/%d CHECKS FAILED"):format(failures, #results)
return header .. "\n" .. table.concat(results, "\n")