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.
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
-- 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")
|
||||
Reference in New Issue
Block a user