Files
tokyo-wangan-racing/tools/verify_shared_ramps.luau
jesse 1d23c187ad Daikoku PA: two perimeter ramps, far side served by the median opening
Rework the PA junction to match the reference drawing: yellow = PA -> highway
(entry), red = highway -> PA (exit). Entry and exit swap sides (entry now
EAST, exit WEST) and both trunks hug the PA perimeter instead of running
concentric spirals.

The simplification: both carriageways sit at the same deck level, adjacent,
so the median opening IS the far-side access path. The under-deck crossing
and merge runs are deleted outright -- 8 runs collapse to 4, and the
girder-soffit ceiling logic that constrained the crossing goes with them.
Part count 24,210 -> 21,691.

The median opening is now load-bearing rather than cosmetic (it is the only
route from the far carriageway to the ramps), so it widens from 6 to 9
samples per junction.

Orientation is a single flip-able constant (ORIENT) so a mirrored result is
a one-line correction, not a rewrite.

Measured on the built map (not asserted):
  - 600 entry + 600 exit ramp parts
  - max grade 8.00% / 8.27%, under the 9% limit
  - 0 collisions with piers/masts/girders (558 structural parts)
  - segment continuity <= 2.5 studs; trunk->merge handoff <= 1.7 studs
  - merge lands at dY 0.0 (exact deck level), 33 studs lateral
  - median open at junctions: 0 parts vs. control ~7 on plain mainline

Two traps worth recording, both caught by controls:

1. Studio's require() cache silently rebuilt STALE code. The first "passing"
   build reported 17,575 parts and zero ramps, and the map contained
   PAOffRamp/PAOnRamp parts whose names exist nowhere in the current source.
   Every measurement taken before this was describing an old map. Added
   tools/rebuild_fresh.luau, which clones the module to bust the cache; use
   it rather than requiring MapBuilder directly.

2. A reported "median never opens" bug was a bad probe, not a defect. It
   counted `Barrier` (the outer edge parapet, which must NEVER open) instead
   of `Median_<i>`. Re-run against the right name: control 7, junctions 0.

Both were only visible because the probes carry controls -- a 0-hit result
with no control is vacuous. The verification harnesses are kept in tools/
with their controls built in.
2026-08-03 11:53:40 -04:00

304 lines
8.8 KiB
Luau

-- Verify the SHARED-TRUNK PA ramps as actually BUILT (not as solved offline).
--
-- The offline solver + /tmp/verify_shared.py agreed on the numbers, but they
-- both worked from the parameter table. This measures the real Roblox parts
-- that MapBuilder emits, which is the only thing the player can hit.
--
-- Runs on a CLONE parented to workspace so spatial queries work, with an
-- Include filter so the LIVE map can never contribute a false collision.
-- SAFETY (learned the hard way, 2026-08-03): MapBuilder.build() begins by
-- destroying Workspace.Map. A verifier that calls build() therefore DELETES
-- THE LIVE MAP as a side effect, and its own map:Destroy() at the end then
-- leaves the workspace with no map at all. Snapshot the live map aside first
-- and restore it in a finally-style block so the user's session survives even
-- if this script errors halfway through.
local ServerScriptService = game:GetService("ServerScriptService")
local live = workspace:FindFirstChild("Map")
local stash = nil
if live then
stash = Instance.new("Folder")
stash.Name = "__MAP_STASH__"
stash.Parent = ServerScriptService
live.Parent = stash -- reparent, do NOT clone: 24k parts
end
local function restoreLive()
if stash then
local held = stash:FindFirstChild("Map")
local rebuilt = workspace:FindFirstChild("Map")
if rebuilt then
rebuilt:Destroy()
end
if held then
held.Parent = workspace
end
stash:Destroy()
end
end
local out = {}
local function log(...)
table.insert(out, table.concat({ ... }, " "))
end
local pass, fail = 0, 0
local function check(name, ok, detail)
if ok then
pass += 1
log("PASS", name, detail or "")
else
fail += 1
log("FAIL", name, detail or "")
end
end
local src = ServerScriptService:FindFirstChild("Server")
src = src and src:FindFirstChild("MapBuilder") or ServerScriptService:FindFirstChild("MapBuilder", true)
if not src then
return "FATAL: MapBuilder module not found"
end
local MapBuilder = require(src:Clone())
local map = MapBuilder.build and MapBuilder.build() or MapBuilder.Build()
map.Name = "VERIFY_CLONE"
map.Parent = workspace
local RUNS = {
"PAEntryRampTrunk",
"PAEntryRampNear",
"PAEntryRampCross",
"PAEntryRampMerge",
"PAExitRampTrunk",
"PAExitRampNear",
"PAExitRampCross",
"PAExitRampMerge",
}
-- Collect the deck parts of each run, in segment order.
local deckOf = {}
for _, r in ipairs(RUNS) do
deckOf[r] = {}
end
local everything = map:GetDescendants()
for _, d in ipairs(everything) do
if d:IsA("BasePart") then
for _, r in ipairs(RUNS) do
local idx = d.Name:match("^" .. r .. "_(%d+)$")
if idx then
table.insert(deckOf[r], { i = tonumber(idx), p = d })
end
end
end
end
for _, r in ipairs(RUNS) do
table.sort(deckOf[r], function(a, b)
return a.i < b.i
end)
end
log("=== 0. RUNS BUILT ===")
local allBuilt = true
for _, r in ipairs(RUNS) do
local n = #deckOf[r]
if n == 0 then
allBuilt = false
end
log(string.format(" %-20s %d segments", r, n))
end
check("all 8 runs emitted", allBuilt)
-- What counts as "our own structure" rather than an obstacle.
local function isRampPart(p)
local n = p.Name
if n:find("PARamp") or n:find("PARing") then
return true
end
for _, r in ipairs(RUNS) do
if n:sub(1, #r) == r then
return true
end
end
return false
end
log("")
log("=== 1. SOLID-OBSTACLE HITS ===")
-- A run's first and last few segments are MOUTHS: they are supposed to touch
-- the ring deck / the mainline carriageway. Everything in between must be in
-- clear air.
local MOUTH = 6
local totalHits = 0
for _, r in ipairs(RUNS) do
local segs = deckOf[r]
local hits, sample = 0, nil
for k, e in ipairs(segs) do
if k > MOUTH and k <= #segs - MOUTH then
local params = OverlapParams.new()
params.FilterType = Enum.RaycastFilterType.Include
params.FilterDescendantsInstances = { map }
params.MaxParts = 40
for _, o in ipairs(workspace:GetPartsInPart(e.p, params)) do
if o ~= e.p and not isRampPart(o) then
hits += 1
if not sample then
sample = string.format("seg %d vs %s", k, o.Name)
end
end
end
end
end
totalHits += hits
check(string.format("%s clear of solid obstacles", r), hits == 0, sample or "0 hits")
end
log("")
log("=== 2. GRADE ===")
-- Limit is 9%; the solve claimed <= 8.28% entry, <= 5.29% exit.
local LIMIT = 9.0
for _, r in ipairs(RUNS) do
local segs = deckOf[r]
local worst, atK = 0, 0
for k = 1, #segs - 1 do
local a, b = segs[k].p.Position, segs[k + 1].p.Position
local horiz = Vector3.new(b.X - a.X, 0, b.Z - a.Z).Magnitude
if horiz > 0.5 then
local g = math.abs(b.Y - a.Y) / horiz * 100
if g > worst then
worst, atK = g, k
end
end
end
check(
string.format("%s grade <= %.0f%%", r, LIMIT),
worst <= LIMIT,
string.format("max %.2f%% at seg %d", worst, atK)
)
end
log("")
log("=== 3. UNDER-DECK CLEARANCE (cross runs) ===")
-- The crossing must fit between the ground and the girder soffit, with a car
-- on it. Measured against the ACTUAL mainline deck parts overhead, not against
-- the assumed profile.
local roadParts = {}
for _, d in ipairs(everything) do
if d:IsA("BasePart") and (d.Name:sub(1, 5) == "Road_" or d.Name:sub(1, 4) == "Deck") then
table.insert(roadParts, d)
end
end
local GIRDER = 8
local CAR = 9
for _, r in ipairs({ "PAEntryRampCross", "PAExitRampCross" }) do
local segs = deckOf[r]
local worst, overhead = math.huge, 0
for _, e in ipairs(segs) do
local p = e.p.Position
for _, rp in ipairs(roadParts) do
local dx = math.abs(p.X - rp.Position.X) - rp.Size.X / 2
local dz = math.abs(p.Z - rp.Position.Z) - rp.Size.Z / 2
if dx < 20 and dz < 20 and rp.Position.Y > p.Y then
overhead += 1
local gap = (rp.Position.Y - GIRDER) - (p.Y + 1)
worst = math.min(worst, gap)
end
end
end
if overhead == 0 then
check(
string.format("%s passes under the deck", r),
false,
"NO overhead deck found -- crossing may miss the mainline"
)
else
check(
string.format("%s clears the soffit with a car (%d st)", r, CAR),
worst >= CAR,
string.format("min gap %.1f st over %d overhead samples", worst, overhead)
)
end
end
log("")
log("=== 4. MERGE ACCURACY ===")
-- The last segment of each slip must land ON its carriageway, not beside it.
local samples = {}
for _, d in ipairs(everything) do
if d:IsA("BasePart") and d.Name:sub(1, 5) == "Road_" then
table.insert(samples, d)
end
end
for _, r in ipairs({ "PAEntryRampNear", "PAEntryRampMerge", "PAExitRampNear", "PAExitRampMerge" }) do
local segs = deckOf[r]
if #segs > 0 then
local tip = segs[#segs].p.Position
local best, bestName = math.huge, "?"
for _, rp in ipairs(samples) do
local d = (Vector3.new(rp.Position.X, 0, rp.Position.Z) - Vector3.new(tip.X, 0, tip.Z)).Magnitude
if d < best then
best, bestName = d, rp.Name
end
end
-- Must be within half a carriageway of a road sample, and at its height.
local dy = math.huge
for _, rp in ipairs(samples) do
if rp.Name == bestName then
dy = math.abs(rp.Position.Y - tip.Y)
end
end
check(
string.format("%s lands on the carriageway", r),
best <= 40 and dy <= 6,
string.format("%.1f st from %s, dY %.1f", best, bestName, dy)
)
end
end
log("")
log("=== 5. TRUNK SEPARATION ===")
-- The two trunks spiral concentrically about the same centre; where they pass
-- close in plan they must be well apart vertically (32-stud decks).
local a, b = deckOf.PAEntryRampTrunk, deckOf.PAExitRampTrunk
local worstSep, closePairs = math.huge, 0
for _, ea in ipairs(a) do
for _, eb in ipairs(b) do
local pa2, pb2 = ea.p.Position, eb.p.Position
local plan = Vector3.new(pa2.X - pb2.X, 0, pa2.Z - pb2.Z).Magnitude
if plan < 40 then
closePairs += 1
worstSep = math.min(worstSep, math.abs(pa2.Y - pb2.Y))
end
end
end
if closePairs == 0 then
check("trunks never pass within 40 st in plan", true, "0 close pairs -- no conflict")
else
check(
"trunks vertically separated where they pass close",
worstSep >= 20,
string.format("min %.1f st vertical over %d close pairs", worstSep, closePairs)
)
end
log("")
log("=== 6. PILLARS NOT IN THE EXPRESSWAY ===")
local badPillars, worstPil = 0, nil
for _, d in ipairs(everything) do
if d:IsA("BasePart") and d.Name == "PARampPillar" then
for _, rp in ipairs(roadParts) do
local dx = math.abs(d.Position.X - rp.Position.X) - rp.Size.X / 2
local dz = math.abs(d.Position.Z - rp.Position.Z) - rp.Size.Z / 2
if dx < 0 and dz < 0 then
badPillars += 1
worstPil = worstPil
or string.format("pillar at %.0f,%.0f under %s", d.Position.X, d.Position.Z, rp.Name)
break
end
end
end
end
check("no ramp pillar planted in the mainline footprint", badPillars == 0, worstPil or "0")
map:Destroy()
restoreLive()
log("")
log(string.format("RESULT %d passed, %d failed (obstacle hits total: %d)", pass, fail, totalHits))
log(string.format("live map restored: %s", tostring(workspace:FindFirstChild("Map") ~= nil)))
return table.concat(out, "\n")