--!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")