Every transport piece moves material at belt speed and capacity (t105)
This commit is contained in:
@@ -98,6 +98,26 @@ Details and numbers live in the code and in `docs/systems/`.
|
|||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
|
- **Transport matches the t45 belt (t105, 2026-10-30).** The owner approved the flat belt
|
||||||
|
and asked for the other transport pieces to keep the same pace and flow through mixed lines.
|
||||||
|
The flat belt stays unchanged: 30 cells/s, 30 grains/s with one layer and 90 grains/s with a
|
||||||
|
heavy feed. Straight screws and flat filters match it. Diagonal transport uses three steps
|
||||||
|
every eight ticks: 31.8 cells/s along its path (the integer cadence keeps four screw lanes at
|
||||||
|
90 grains/s). Bars follow the same distance clock. This replaces the old faster diagonal
|
||||||
|
screw/belt pace and slower bar-only screen pace.
|
||||||
|
Downhill belts and screens carry a bed six vertical grains deep, about four grains across
|
||||||
|
their path; the mesh holds the contact layer between steps. Screens still drop the chosen
|
||||||
|
grains through, with those grains waiting to sift instead of riding onto the outlet belt.
|
||||||
|
Uphill sidewalls keep real pushing pockets and rollback, with 10-grain cleats spaced eight
|
||||||
|
face cells apart and a boot scoop reaching nine cells back. Their 96-cell push bound stays.
|
||||||
|
Belts, filters and screws run receivers before feeders; screw mouths preserve all four lanes
|
||||||
|
through turns and place their output into the receiving belt's bed. Rising diagonal outlets
|
||||||
|
throw their load clear of the raised belt's lip, which otherwise spills it underneath. These join rules avoid
|
||||||
|
squeezing a full belt into one screw lane. The measured mixed chain carries about 90 grains/s
|
||||||
|
without growing heaps or spills. This supersedes the earlier reduced uphill/screen rates,
|
||||||
|
six-grain cleats and screen contact-only nudges below. World generation is unchanged.
|
||||||
|
|
||||||
|
|
||||||
The newest are at the bottom. The dates are when the decision was made or built.
|
The newest are at the bottom. The dates are when the decision was made or built.
|
||||||
|
|
||||||
**Up to 2026-10-19** (early design, stated by the owner): everything in "What the owner wants"
|
**Up to 2026-10-19** (early design, stated by the owner): everything in "What the owner wants"
|
||||||
|
|||||||
+14
-11
@@ -259,7 +259,7 @@ Exploration only: never changes grains or the random stream.
|
|||||||
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
|
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
|
||||||
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
|
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
|
||||||
|
|
||||||
### src/sim/cells.js (322 lines)
|
### src/sim/cells.js (332 lines)
|
||||||
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
|
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
|
||||||
- functions: setCell, swapCells, swapCellData, move, enter, enterFree, powderOpen, powderFall, powderSpill, avalancheMove, powder, passable, liquid, liquidSearch, gas, wakeBotPowder, simMove, moveChunk
|
- functions: setCell, swapCells, swapCellData, move, enter, enterFree, powderOpen, powderFall, powderSpill, avalancheMove, powder, passable, liquid, liquidSearch, gas, wakeBotPowder, simMove, moveChunk
|
||||||
- variables: SORTP, SORTP_FUEL, LSTEP, LFILM, CORNER_SEAL, POWDER_FAN
|
- variables: SORTP, SORTP_FUEL, LSTEP, LFILM, CORNER_SEAL, POWDER_FAN
|
||||||
@@ -269,7 +269,7 @@ Heat: conduction, melting and freezing, reactions, fire and air, steel.
|
|||||||
- functions: curAir, airflowAt, alloyMix, airy, hasO2, condPair, heatPass, heatChunk, neighborDir, react, char, quench, burningNear, burn, steelCheck
|
- functions: curAir, airflowAt, alloyMix, airy, hasO2, condPair, heatPass, heatChunk, neighborDir, react, char, quench, burningNear, burn, steelCheck
|
||||||
- variables: POROUS, SOIL, HOTLIQ, REACT_MIN, ALLOY
|
- variables: POROUS, SOIL, HOTLIQ, REACT_MIN, ALLOY
|
||||||
|
|
||||||
### src/sim/machines.js (157 lines)
|
### src/sim/machines.js (179 lines)
|
||||||
Machines: the power network, footprints, placing and removing machines.
|
Machines: the power network, footprints, placing and removing machines.
|
||||||
- functions: markPow, partLost, rebuildPower, netSat, evictCells, cellBuildable, machineFootprint, canPlaceMachine, addMachine, removeMachine
|
- functions: markPow, partLost, rebuildPower, netSat, evictCells, cellBuildable, machineFootprint, canPlaceMachine, addMachine, removeMachine
|
||||||
- variables: nextPowerBuild
|
- variables: nextPowerBuild
|
||||||
@@ -289,7 +289,7 @@ Pipes, pumps and outlets: each joined line holds one liquid as a count, not as g
|
|||||||
- functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick
|
- functions: sideOf, pipeClosedSide, pipeJoins, rebuildPipes, pipeNetOf, pipeTakeShare, faceCell, pumpDraw, outletPour, pipesTick
|
||||||
- variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty
|
- variables: PIPE_CAP, PUMP_RATE, OUTLET_RATE, PUMP_MAX_T, SIDE_V, PUMP_V, OUTLET_V, pipeNets, pipeNetAt, pipesDirty
|
||||||
|
|
||||||
### src/sim/machine-tick.js (160 lines)
|
### src/sim/machine-tick.js (97 lines)
|
||||||
Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick).
|
Per-tick machine work: steam engines, fans, belts, filters, screw conveyors (machinesTick).
|
||||||
- functions: engineIntake, fanBlow, machinesTick
|
- functions: engineIntake, fanBlow, machinesTick
|
||||||
|
|
||||||
@@ -298,25 +298,25 @@ Travel-directed transport ends. Precomputed probes; world/plan queries allocate
|
|||||||
- functions: pieceEnds, joinCandidate, endJoin, pieceJoins, endAt, joinIsDrop, joinSnap, nearMiss
|
- functions: pieceEnds, joinCandidate, endJoin, pieceJoins, endAt, joinIsDrop, joinSnap, nearMiss
|
||||||
- variables: JOIN_ENDS, NO_PIECE, NEAR_MISS_VIEW
|
- variables: JOIN_ENDS, NO_PIECE, NEAR_MISS_VIEW
|
||||||
|
|
||||||
### src/sim/conveyors.js (215 lines)
|
### src/sim/conveyors.js (344 lines)
|
||||||
How belts, filters and screw conveyors move what is on or in them. What rests on a belt or filter rides along: the bottom four grains standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear). If a powder grain is blocked, the load above it stands still and the belt slips underneath. A filter's own material drops through it instead, unless it is already full.
|
How belts, filters and screw conveyors move what is on or in them. What rests on a belt or filter rides along: the bottom four grains standing on the carrying surface steps one cell toward d together (belts run front of the line first, so the way ahead is clear). If a powder grain is blocked, the load above it stands still and the belt slips underneath. A filter's own material drops through it instead, unless it is already full. Six vertical grains give a 45° face roughly the same depth across its path.
|
||||||
- functions: conveyorStep, carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, screwSpread, screwIntake, screwAccepts, straightScrewNext, diagonalScrewNext
|
- functions: conveyorStep, carryColumn, beltDrain, launchGrain, rebuildFilters, filterCarry, carryBeltsAndFilters, setFilter, filterRun, roomFor, screwSpread, transportBed, screwIntake, screwAccepts, straightScrewNext, diagonalScrewNext, screwCarry
|
||||||
- variables: CARRY_DEPTH, filterR, filterL, filtersDirty
|
- variables: CARRY_DEPTH, SLOPE_CARRY_DEPTH, filterR, filterL, filtersDirty, screwCarryR, screwCarryL
|
||||||
|
|
||||||
### src/sim/flow.js (26 lines)
|
### src/sim/flow.js (26 lines)
|
||||||
Throughput for the info overlay (t49): how many grains each belt, filter and screw conveyor block hands on, per second of game time. Counting runs only while the overlay is on, so normal play pays one flag check per carried grain. Bars are not counted.
|
Throughput for the info overlay (t49): how many grains each belt, filter and screw conveyor block hands on, per second of game time. Counting runs only while the overlay is on, so normal play pays one flag check per carried grain. Bars are not counted.
|
||||||
- functions: setFlowCounting, flowNote, flowCross, flowSlopeUnder, flowTick
|
- functions: setFlowCounting, flowNote, flowCross, flowSlopeUnder, flowTick
|
||||||
- variables: flowOn, flowCount, flowRate
|
- variables: flowOn, flowCount, flowRate
|
||||||
|
|
||||||
### src/sim/transport-status.js (38 lines)
|
### src/sim/transport-status.js (39 lines)
|
||||||
Local backpressure: only loaded transport that cannot advance warns after half a second.
|
Local backpressure: only loaded transport that cannot advance warns after half a second.
|
||||||
- functions: transportNote, transportStalled, transportClock, transportBegin, transportEnd, transportCleat, transportBar
|
- functions: transportNote, transportStalled, transportClock, transportBegin, transportEnd, transportCleat, transportBar
|
||||||
- variables: STALL_TICKS, transportState
|
- variables: STALL_TICKS, transportState
|
||||||
|
|
||||||
### src/sim/slope-conveyors.js (237 lines)
|
### src/sim/slope-conveyors.js (232 lines)
|
||||||
Cleats are rubber ribs standing up from a sloped belt's face, one every CLEAT_SPACING face cells. Loose grains treat them as solid (cleatMask). Nothing grips the bare face: grains slide down it until a cleat stops them. Each belt step moves every cleat one face cell along the belt, and the rib pushes whatever is in its way ahead of it, so the pocket in front rides along while anything heaped above the rib is left to roll back. If a rib can't push its way, its whole run stalls. Only uphill runs have cleats: downhill, the belt carries its load like a flat one.
|
Cleats are rubber ribs standing up from a sloped belt's face, one every CLEAT_SPACING face cells. Loose grains treat them as solid (cleatMask). Nothing grips the bare face: grains slide down it until a cleat stops them. Each belt step moves every cleat one face cell along the belt, and the rib pushes whatever is in its way ahead of it, so the pocket in front rides along while anything heaped above the rib is left to roll back. If a rib can't push its way, its whole run stalls. Only uphill runs have cleats: downhill, the belt carries its load like a flat one.
|
||||||
- functions: ribMove, cleatFoot, ribCells, slopeUphill, slopeFaceAt, pushRow, scoop, pocketRoom, advanceCleat, slopeRunKey, slopeCleatsTick, rebuildCleatMask, expelFromRib, screenNudge, wakeCleatLoad
|
- functions: diagonalBeat, ribMove, cleatFoot, ribCells, slopeUphill, slopeFaceAt, pushRow, scoop, pocketRoom, advanceCleat, slopeRunKey, slopeCleatsTick, rebuildCleatMask, expelFromRib, screenNudge, wakeCleatLoad
|
||||||
- variables: CLEAT_SPACING, CLEAT_HEIGHT, CLEAT_REACH, SCREEN_NUDGE_TICKS, cleatChunks, cleatMask, cleatCells, slopePhase, ribLog
|
- variables: CLEAT_SPACING, CLEAT_HEIGHT, CLEAT_REACH, cleatChunks, cleatMask, cleatCells, slopePhase, ribLog
|
||||||
|
|
||||||
### src/sim/blocks.js (268 lines)
|
### src/sim/blocks.js (268 lines)
|
||||||
Building blocks: placing, integrity, removing, lines and rectangles of blocks.
|
Building blocks: placing, integrity, removing, lines and rectangles of blocks.
|
||||||
@@ -701,6 +701,7 @@ The frame loop and boot.
|
|||||||
- `test/t_stalls.py`: t45: real transport warnings, hover, frozen animation and recovery in both looks.
|
- `test/t_stalls.py`: t45: real transport warnings, hover, frozen animation and recovery in both looks.
|
||||||
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
|
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
|
||||||
- `test/t_threads.js`: Shared allocation, checkerboard independence, and real worker-count determinism.
|
- `test/t_threads.js`: Shared allocation, checkerboard independence, and real worker-count determinism.
|
||||||
|
- `test/t_transport_balance.js`: t105: compare every direction with the unchanged t45 belt and exercise the complete line.
|
||||||
- `test/t_vacuum_collapse.js`: Real nozzle cuts into settled heaps, inline and through the real Node worker pool.
|
- `test/t_vacuum_collapse.js`: Real nozzle cuts into settled heaps, inline and through the real Node worker pool.
|
||||||
- `test/t_vacuum_collapse.py`: Vacuum cuts in the real pixel frame loop with active browser workers and hardware graphics.
|
- `test/t_vacuum_collapse.py`: Vacuum cuts in the real pixel frame loop with active browser workers and hardware graphics.
|
||||||
- `test/t_water.js`: Water conservation, settling, ledge pouring, boundaries, pipes and phase changes. Tall worlds keep geothermal heat out of the ponds.
|
- `test/t_water.js`: Water conservation, settling, ledge pouring, boundaries, pipes and phase changes. Tall worlds keep geothermal heat out of the ponds.
|
||||||
@@ -729,6 +730,7 @@ The frame loop and boot.
|
|||||||
- `tools/experiments/slope-flow.js`: Reproducible generous-feed throughput and settled pile angles for t42.
|
- `tools/experiments/slope-flow.js`: Reproducible generous-feed throughput and settled pile angles for t42.
|
||||||
- `tools/experiments/smelter.js`: Smelter design experiments, built with the same block pieces a player uses. Layout (block rows, 4x4 grains each), on flat ground at row 25: roof with a vent ........ row g-CH-1 chamber, CH blocks tall .. charge of coal + copper ore inside grate floor ............. row g = 23 (liquids pass, powders do not) ingot mold .............. row 24, sitting on the ground side walls run from the roof down past the mold. Usage: node t_smelter.js [key=value ...]
|
- `tools/experiments/smelter.js`: Smelter design experiments, built with the same block pieces a player uses. Layout (block rows, 4x4 grains each), on flat ground at row 25: roof with a vent ........ row g-CH-1 chamber, CH blocks tall .. charge of coal + copper ore inside grate floor ............. row g = 23 (liquids pass, powders do not) ingot mold .............. row 24, sitting on the ground side walls run from the roof down past the mold. Usage: node t_smelter.js [key=value ...]
|
||||||
- `tools/experiments/starter.js`: Plays a starter design the way a player would: paste the blueprint, let the bot build it from the bag, blow an even ore/coal mix inside, light it, wait, knock out the castings. Usage: node tools/experiments/starter.js [design id] [ore] [coal]
|
- `tools/experiments/starter.js`: Plays a starter design the way a player would: paste the blueprint, let the bot build it from the bag, blow an even ore/coal mix inside, light it, wait, knock out the castings. Usage: node tools/experiments/starter.js [design id] [ore] [coal]
|
||||||
|
- `tools/experiments/transport-balance.js`: t105: same belt feed, open sink, central single-grain/bar travel. Run with node.
|
||||||
- `tools/experiments/worldgen.js`: World generation report: time to generate, how many cells of each material, where the bot starts, and what idle ticks and ticks after digging cost.
|
- `tools/experiments/worldgen.js`: World generation report: time to generate, how many cells of each material, where the bot starts, and what idle ticks and ticks after digging cost.
|
||||||
- `tools/scenes/bar-lift.js`: A mold drops copper bars onto a belt, through a screw lift and onto the upper belt.
|
- `tools/scenes/bar-lift.js`: A mold drops copper bars onto a belt, through a screw lift and onto the upper belt.
|
||||||
- `tools/scenes/belt-heap.js`: A belt feeding an upward screw: sand underneath a deep heap of copper ore. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/belt-heap.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/belt-heap.js)" --ticks 240 --zoom 7
|
- `tools/scenes/belt-heap.js`: A belt feeding an upward screw: sand underneath a deep heap of copper ore. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/belt-heap.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/belt-heap.js)" --ticks 240 --zoom 7
|
||||||
@@ -739,6 +741,7 @@ The frame loop and boot.
|
|||||||
- `tools/scenes/slope-conveyors.js`: Loaded sidewall conveyor and screen. Use --js 'slopeScene(false)' (belt) or 'slopeScene(true)' (screen).
|
- `tools/scenes/slope-conveyors.js`: Loaded sidewall conveyor and screen. Use --js 'slopeScene(false)' (belt) or 'slopeScene(true)' (screen).
|
||||||
- `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7
|
- `tools/scenes/stone-uses.js`: The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g. python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7
|
||||||
- `tools/scenes/t-screen-joint.js`: Downhill screen whose outlet belt sits level with its last block (the jam the owner saw).
|
- `tools/scenes/t-screen-joint.js`: Downhill screen whose outlet belt sits level with its last block (the jam the owner saw).
|
||||||
|
- `tools/scenes/t105-chain.js`: Load test/fixtures/t105-transport.js first. The measured chain, running at heavy feed.
|
||||||
- `tools/scenes/t33-ground.js`: Paused dug pocket: copper ore, rubble and dirt against stone, granite and dirt. Use --js "..." to set daylight or R.lens after this setup; CAM frames the pocket.
|
- `tools/scenes/t33-ground.js`: Paused dug pocket: copper ore, rubble and dirt against stone, granite and dirt. Use --js "..." to set daylight or R.lens after this setup; CAM frames the pocket.
|
||||||
- `tools/scenes/t35-diagonal.js`: Run after clear-sky.js: a belt takes sand from a pile, up a diagonal screw to an upper belt.
|
- `tools/scenes/t35-diagonal.js`: Run after clear-sky.js: a belt takes sand from a pile, up a diagonal screw to an upper belt.
|
||||||
- `tools/scenes/t35-joins.js`: Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect.
|
- `tools/scenes/t35-joins.js`: Run after clear-sky.js. Paused, empty pipes make casing, flights and mouths easy to inspect.
|
||||||
|
|||||||
@@ -345,3 +345,12 @@ and [Chromium Dawn defaults](https://dawn.googlesource.com/dawn.git/+/refs/heads
|
|||||||
All game pipelines use async creation during capability checking. World loading submits both
|
All game pipelines use async creation during capability checking. World loading submits both
|
||||||
looks before `GAME.running` becomes true. Shader and loading timings are printed by `t_gpu.py`;
|
looks before `GAME.running` becomes true. Shader and loading timings are printed by `t_gpu.py`;
|
||||||
look/zoom switches must not call synchronous pipeline creation.
|
look/zoom switches must not call synchronous pipeline creation.
|
||||||
|
|
||||||
|
|
||||||
|
`t105` transport checks: `node tools/experiments/transport-balance.js` prints single-grain and
|
||||||
|
bar path speeds, normal/heavy flow for both directions of every carrier, and the heavy mixed
|
||||||
|
chain's output, conservation and per-join load counts. It uses `test/fixtures/t105-transport.js`;
|
||||||
|
`t_transport_balance.js` pins the belt reference and requires 89–91 grains/s through the chain.
|
||||||
|
`t_threads.js` also compares that chain with 0/1/2/8 real workers. To photograph the same chain,
|
||||||
|
pass the fixture followed by `tools/scenes/t105-chain.js` together as `tools/shot.py --js`
|
||||||
|
(they must share one setup scope); use zoom 5, no lighting and a 1280×760 viewport. Check both looks when changing cleat geometry.
|
||||||
|
|||||||
@@ -4,6 +4,22 @@ What agents have done, newest first. Add an entry for every piece of work: the d
|
|||||||
note or request it was for, what changed, the files touched, the tests run, and anything left
|
note or request it was for, what changed, the files touched, the tests run, and anything left
|
||||||
open or not verified. Keep each entry short; the git history has the details.
|
open or not verified. Keep each entry short; the git history has the details.
|
||||||
|
|
||||||
|
## 2026-10-30: t105 transport matches the t45 belt (Codex)
|
||||||
|
|
||||||
|
- Shared diagonal cadence, six-grain vertical slope beds, taller cleat pockets and four-lane
|
||||||
|
screw handoffs; raised outlets throw clear of the belt lip. Flat belt and worldgen unchanged.
|
||||||
|
Full before/after table in t105: mixed chain 30.0 → 89.9 grains/s, no spills, 7216/7216 grains
|
||||||
|
accounted for. Diagonal path pace is 31.8 versus 30 cells/s straight (integer cadence).
|
||||||
|
- Files: sim/{cells,conveyors,items,machine-tick,machines,pieces,slope-conveyors,transport-status};
|
||||||
|
test/{t_conveyors,t_diag_screws,t_slope_belts,t_threads,t_transport_balance,fixtures/t105-transport};
|
||||||
|
tools/{experiments/transport-balance,scenes/t105-chain}; docs/{DESIGN,TESTING,PROJECT_MAP}, t105.
|
||||||
|
- All 44 headless suites pass, including 91 transport checks and 0/1/2/8-worker chain equality;
|
||||||
|
42 new checks fail on original code. Two generated-world hashes match the original revision.
|
||||||
|
Host-GPU t_stalls.py, t_smooth.py, play.py, t_gpu_golden.py and t_screw_heat.py pass.
|
||||||
|
- Inspected shots/t105-chain{,-smooth}.png: full pockets and beds, connected joins, clear outlet.
|
||||||
|
Map regenerated; measured transport cost in the task report. Sandbox browser startup timed out.
|
||||||
|
Full browser suite not run; owner playtest remains. No commit.
|
||||||
|
|
||||||
## 2026-10-29: new belts stood still until reload; golden test after t45 (Claude)
|
## 2026-10-29: new belts stood still until reload; golden test after t45 (Claude)
|
||||||
|
|
||||||
- Owner bug: in a world started after a long session, belts built after loading never moved
|
- Owner bug: in a world started after a long session, belts built after loading never moved
|
||||||
|
|||||||
@@ -168,9 +168,19 @@ function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) {
|
|||||||
// packs too tight for that, so a mixed furnace charge stays mixed until the fire gets in.
|
// packs too tight for that, so a mixed furnace charge stays mixed until the fire gets in.
|
||||||
const t = mat[b];
|
const t = mat[b];
|
||||||
if (PH[t] === PH_POWDER && (!barriers || (!cleatMask[i] && !cleatMask[b])) && DENS[m] >= DENS[t] * 2 && (!FUEL[t] || (flags[b] & F_BURN)) && stamp[b] !== tickStamp && !occ[b] && part[b] !== P_GRATE && part[b] !== P_FILTER && rnd() < (FUEL[t] ? SORTP_FUEL : SORTP)) { move(i, b, x, y, x, y + 1); chunkWake((y >> 5) * CW + (x >> 5), 40); return; }
|
if (PH[t] === PH_POWDER && (!barriers || (!cleatMask[i] && !cleatMask[b])) && DENS[m] >= DENS[t] * 2 && (!FUEL[t] || (flags[b] & F_BURN)) && stamp[b] !== tickStamp && !occ[b] && part[b] !== P_GRATE && part[b] !== P_FILTER && rnd() < (FUEL[t] ? SORTP_FUEL : SORTP)) { move(i, b, x, y, x, y + 1); chunkWake((y >> 5) * CW + (x >> 5), 40); return; }
|
||||||
|
// Selected grains above a screen sift down; they do not slide onto its outlet belt.
|
||||||
|
if ((y + SLOPE_CARRY_DEPTH < H && isFilter(blockP[((y + SLOPE_CARRY_DEPTH) >> 2) * BW + (x >> 2)])) ||
|
||||||
|
isFilter(blockP[(y >> 2) * BW + (x >> 2)])) {
|
||||||
|
const face = transportBed(i);
|
||||||
|
if (face >= 0 && part[face] === P_FILTER && filterPassAt(m, face)) return;
|
||||||
|
}
|
||||||
// a grain lying right on a belt or filter is held by it: the surface carries it, it does not slide off the edge
|
// a grain lying right on a belt or filter is held by it: the surface carries it, it does not slide off the edge
|
||||||
if ((t === BELTL || t === BELTR || part[b] === P_FILTER) &&
|
if ((t === BELTL || t === BELTR || part[b] === P_FILTER) &&
|
||||||
!isSlopeBelt(blockP[((y + 1) >> 2) * BW + (x >> 2)])) return;
|
!isSlopeBelt(blockP[((y + 1) >> 2) * BW + (x >> 2)])) return;
|
||||||
|
if(t===BELTL||t===BELTR||part[b]===P_FILTER) {
|
||||||
|
const owner=((y+1)>>2)*BW+(x>>2),code=blockP[owner];
|
||||||
|
if(isSlopeBelt(code)&&(isFilter(code)||!slopeUphill(code,blockV[owner])))return;
|
||||||
|
}
|
||||||
// Feed a receiving screw mouth sideways instead of falling under its bottom casing.
|
// Feed a receiving screw mouth sideways instead of falling under its bottom casing.
|
||||||
if (t === BELTL || t === BELTR) {
|
if (t === BELTL || t === BELTR) {
|
||||||
const b = ((y + 1) >> 2) * BW + (x >> 2);
|
const b = ((y + 1) >> 2) * BW + (x >> 2);
|
||||||
|
|||||||
+138
-9
@@ -3,7 +3,8 @@
|
|||||||
// steps one cell toward d together (belts run front of the line first, so the way ahead is
|
// steps one cell toward d together (belts run front of the line first, so the way ahead is
|
||||||
// clear). If a powder grain is blocked, the load above it stands still and the belt slips
|
// clear). If a powder grain is blocked, the load above it stands still and the belt slips
|
||||||
// underneath. A filter's own material drops through it instead, unless it is already full.
|
// underneath. A filter's own material drops through it instead, unless it is already full.
|
||||||
const CARRY_DEPTH = 4;
|
// Six vertical grains give a 45° face roughly the same depth across its path.
|
||||||
|
const CARRY_DEPTH = 4, SLOPE_CARRY_DEPTH = 6;
|
||||||
// Find the next carrying face at the same height or one grain above/below it.
|
// Find the next carrying face at the same height or one grain above/below it.
|
||||||
function conveyorStep(x, y, d) {
|
function conveyorStep(x, y, d) {
|
||||||
const nx = x + d;
|
const nx = x + d;
|
||||||
@@ -23,7 +24,8 @@ function carryColumn(a, d, pass) {
|
|||||||
if (a < 0 || mat[a] === EMPTY) return;
|
if (a < 0 || mat[a] === EMPTY) return;
|
||||||
const sy = ((a / W) | 0) + 1, step = conveyorStep(a % W, sy, d), carrier = (sy >> 2) * BW + ((a % W) >> 2);
|
const sy = ((a / W) | 0) + 1, step = conveyorStep(a % W, sy, d), carrier = (sy >> 2) * BW + ((a % W) >> 2);
|
||||||
let blocked = false;
|
let blocked = false;
|
||||||
for (let L = 0; L < CARRY_DEPTH; L++, a -= W) {
|
const depth=isSlopeBelt(blockP[carrier])?SLOPE_CARRY_DEPTH:CARRY_DEPTH;
|
||||||
|
for (let L = 0; L < depth; L++, a -= W) {
|
||||||
if (a < 0) return;
|
if (a < 0) return;
|
||||||
const m = mat[a];
|
const m = mat[a];
|
||||||
if (m === EMPTY || (PH[m] === PH_SOLID && !part[a])) return; // the top of the heap, or a ceiling
|
if (m === EMPTY || (PH[m] === PH_SOLID && !part[a])) return; // the top of the heap, or a ceiling
|
||||||
@@ -35,10 +37,36 @@ function carryColumn(a, d, pass) {
|
|||||||
const u = mat[a + W];
|
const u = mat[a + W];
|
||||||
if (L > 0 && !pass[u] && PH[u] === PH_POWDER && stamp[a + W] !== tickStamp && !occ[a + W] && rnd() < 0.5) { const x = a % W, y = (a / W) | 0; move(a, a + W, x, y, x, y + 1); transportNote(carrier, true); continue; }
|
if (L > 0 && !pass[u] && PH[u] === PH_POWDER && stamp[a + W] !== tickStamp && !occ[a + W] && rnd() < 0.5) { const x = a % W, y = (a / W) | 0; move(a, a + W, x, y, x, y + 1); transportNote(carrier, true); continue; }
|
||||||
}
|
}
|
||||||
|
if(pass && pass[m])continue;
|
||||||
if (blocked) continue; // filters still sift above a stopped load
|
if (blocked) continue; // filters still sift above a stopped load
|
||||||
const x = a % W, nx = x + d;
|
const x = a % W, nx = x + d;
|
||||||
if (nx < 0 || nx >= W) return;
|
if (nx < 0 || nx >= W) return;
|
||||||
const j = a + d + step * W;
|
let j = a + d + step * W;
|
||||||
|
// Turn the incoming bed across a screw mouth without squeezing it into one lane.
|
||||||
|
let sp=part[j];
|
||||||
|
const down=sy<H?part[sy*W+nx]:0;
|
||||||
|
if(!sp && IS_SCREW[down] && (SCREW_DY[down]>0 || SCREW_DX[down]) && screwAccepts(down,d,0)) {
|
||||||
|
sp=down;
|
||||||
|
j=(sy+(SCREW_DX[sp]?((a/W)|0)&3:0))*W+nx;
|
||||||
|
}
|
||||||
|
if(IS_SCREW[sp] && !SCREW_DX[sp]) {
|
||||||
|
const row=((j/W)|0),lane=d>0?(((a/W)|0)&3):3-(((a/W)|0)&3);
|
||||||
|
j=((row&~3)+(SCREW_DY[sp]<0?3:0))*W+(nx&~3)+lane;
|
||||||
|
}
|
||||||
|
if(IS_SCREW[sp] && SCREW_DX[sp] && SCREW_DY[sp]) {
|
||||||
|
const by=((j/W)|0)&~3,xx=nx;
|
||||||
|
for(let yy=by+3;yy>=by;yy--){const target=yy*W+xx;if(roomFor(target)&&!occ[target]){j=target;break;}}
|
||||||
|
} else if(isSlopeBelt(blockP[((sy+step)>>2)*BW+(nx>>2)]) && !isSlopeBelt(blockP[carrier])) {
|
||||||
|
const face=(sy+step)*W+nx;
|
||||||
|
for(let h=1;h<=SLOPE_CARRY_DEPTH;h++){const target=face-h*W;if(target>=0&&roomFor(target)&&!occ[target]){j=target;break;}}
|
||||||
|
}
|
||||||
|
const receiver=blockP[((sy+step)>>2)*BW+(nx>>2)];
|
||||||
|
if(isSlopeBelt(blockP[carrier]) && (receiver===BP_BELT || receiver===BP_FILTER)) {
|
||||||
|
const face=(sy+step)*W+nx;
|
||||||
|
let target=-1;for(let h=1;h<=CARRY_DEPTH;h++){const q=face-h*W;if(roomFor(q)&&!occ[q]){target=q;break;}}
|
||||||
|
if(target<0)return;
|
||||||
|
j=target;
|
||||||
|
}
|
||||||
if (j < 0 || j >= N) return;
|
if (j < 0 || j >= N) return;
|
||||||
const tj = mat[j];
|
const tj = mat[j];
|
||||||
// grains on a wet belt push through the water rather than stopping at it
|
// grains on a wet belt push through the water rather than stopping at it
|
||||||
@@ -50,7 +78,10 @@ function carryColumn(a, d, pass) {
|
|||||||
const y = (a / W) | 0, bj = j + W;
|
const y = (a / W) | 0, bj = j + W;
|
||||||
// carried off the end with nothing under it: it leaves at belt speed and falls
|
// carried off the end with nothing under it: it leaves at belt speed and falls
|
||||||
if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, nx, y + step, d * 0.5, step * 0.5);
|
if (mat[j] === EMPTY && !part[j] && bj < N && (mat[bj] === EMPTY || PH[mat[bj]] === PH_GAS) && !occ[bj] && !part[bj]) launchGrain(a, nx, y + step, d * 0.5, step * 0.5);
|
||||||
else move(a, j, x, y, nx, y + step);
|
else {
|
||||||
|
move(a,j,x,y,j%W,(j/W)|0);
|
||||||
|
if(IS_SCREW[part[j]] && SCREW_DX[part[j]])screwSpread(j,j%W,(j/W)|0,d,step);
|
||||||
|
}
|
||||||
transportNote(carrier, true);
|
transportNote(carrier, true);
|
||||||
stats.moved++;
|
stats.moved++;
|
||||||
if (flowOn && (x >> 2) !== (nx >> 2)) flowNote(carrier);
|
if (flowOn && (x >> 2) !== (nx >> 2)) flowNote(carrier);
|
||||||
@@ -81,6 +112,7 @@ function rebuildFilters() {
|
|||||||
}
|
}
|
||||||
function filterCarry(b, d, carrying = true) {
|
function filterCarry(b, d, carrying = true) {
|
||||||
if (!isFilter(blockP[b])) return;
|
if (!isFilter(blockP[b])) return;
|
||||||
|
if(isSlopeBelt(blockP[b]) && !diagonalBeat())return;
|
||||||
const bx = b % BW, by = (b / BW) | 0, code = blockP[b], y = by * BS;
|
const bx = b % BW, by = (b / BW) | 0, code = blockP[b], y = by * BS;
|
||||||
if (y < 1) return;
|
if (y < 1) return;
|
||||||
if (!carrying && !isSlopeBelt(code)) return;
|
if (!carrying && !isSlopeBelt(code)) return;
|
||||||
@@ -93,16 +125,21 @@ function filterCarry(b, d, carrying = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Belts and filters share one front-first order, including their flat/slope joins.
|
// Belts, filters and screws share a front-first order, so receivers clear before feeders.
|
||||||
|
let screwCarryR=[], screwCarryL=[];
|
||||||
function carryBeltsAndFilters(belts, filters, d, bm, carrying) {
|
function carryBeltsAndFilters(belts, filters, d, bm, carrying) {
|
||||||
let k = 0, f = 0;
|
const screws=d>0?screwCarryR:screwCarryL;
|
||||||
while (k < belts.length || f < filters.length) {
|
let k = 0, f = 0, s=0;
|
||||||
|
while (k < belts.length || f < filters.length || (carrying && s<screws.length)) {
|
||||||
|
const bx=k<belts.length?belts[k]%W:(d>0?-1:W);
|
||||||
|
const fx0=f<filters.length?(filters[f]%BW)*BS+(d>0?BS-1:0):(d>0?-1:W);
|
||||||
|
if(carrying && s<screws.length && (((screws[s]%W)>>2)-(bx>>2))*d>=0 && (((screws[s]%W)>>2)-(fx0>>2))*d>=0){screwCarry(screws[s++]);continue;}
|
||||||
const fx = f < filters.length ? (filters[f] % BW) * BS + (d > 0 ? BS - 1 : 0) : 0;
|
const fx = f < filters.length ? (filters[f] % BW) * BS + (d > 0 ? BS - 1 : 0) : 0;
|
||||||
if (f < filters.length && (k >= belts.length || (fx - belts[k] % W) * d > 0)) { filterCarry(filters[f++], d, carrying); continue; }
|
if (f < filters.length && (k >= belts.length || (fx - belts[k] % W) * d > 0)) { filterCarry(filters[f++], d, carrying); continue; }
|
||||||
const b = belts[k++];
|
const b = belts[k++];
|
||||||
if (mat[b] !== bm || b < W) continue;
|
if (mat[b] !== bm || b < W) continue;
|
||||||
const x = b % W, y = (b / W) | 0, code = blockP[(y >> 2) * BW + (x >> 2)];
|
const x = b % W, y = (b / W) | 0, code = blockP[(y >> 2) * BW + (x >> 2)];
|
||||||
if (!carrying && !isSlopeBelt(code)) continue;
|
if (isSlopeBelt(code) ? !diagonalBeat() : !carrying) continue;
|
||||||
if (isSlopeBelt(code) && beltFaceY(code, x >> 2, y >> 2, x) !== y) continue;
|
if (isSlopeBelt(code) && beltFaceY(code, x >> 2, y >> 2, x) !== y) continue;
|
||||||
if (slopeSealOwner(b) >= 0) continue;
|
if (slopeSealOwner(b) >= 0) continue;
|
||||||
const above = mat[b - W];
|
const above = mat[b - W];
|
||||||
@@ -153,6 +190,15 @@ function screwSpread(c, x, y, sx, sy, intake = false) {
|
|||||||
}
|
}
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
// Carrying face below a grain, including the deeper vertical bed on a slope.
|
||||||
|
function transportBed(i) {
|
||||||
|
const x=i%W, y=(i/W)|0;
|
||||||
|
for(let h=1;h<=SLOPE_CARRY_DEPTH && y+h<H;h++) {
|
||||||
|
const j=i+h*W,b=((y+h)>>2)*BW+(x>>2),c=blockP[b];
|
||||||
|
if((isSlopeBelt(c)||h<=CARRY_DEPTH)&&(isBelt(c)||isFilter(c))&&beltFaceY(c,x>>2,(y+h)>>2,x)===y+h)return j;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
// An empty screw cell draws in loose material from behind it or from its sides
|
// An empty screw cell draws in loose material from behind it or from its sides
|
||||||
// (not from the cell it pushes into), so a screw end sitting in a pile feeds itself.
|
// (not from the cell it pushes into), so a screw end sitting in a pile feeds itself.
|
||||||
function screwIntake(i, p, dx, dy) {
|
function screwIntake(i, p, dx, dy) {
|
||||||
@@ -172,7 +218,7 @@ function screwIntake(i, p, dx, dy) {
|
|||||||
if (stamp[i] === tickStamp) return;
|
if (stamp[i] === tickStamp) return;
|
||||||
const pj = part[j];
|
const pj = part[j];
|
||||||
if (IS_SCREW[pj]) continue; // other screw cells move their own contents
|
if (IS_SCREW[pj]) continue; // other screw cells move their own contents
|
||||||
if (pj === P_GRATE || pj === P_FILTER) continue;
|
if (pj === P_GRATE || pj === P_FILTER || transportBed(j)>=0) continue;
|
||||||
move(j, i, nx, ny, x, y);
|
move(j, i, nx, ny, x, y);
|
||||||
if (diagonal) screwSpread(i, x, y, 0, (y & 3) < 2 ? 1 : -1, true);
|
if (diagonal) screwSpread(i, x, y, 0, (y & 3) < 2 ? 1 : -1, true);
|
||||||
else if (ox !== -dx || oy !== -dy) screwSpread(i, x, y, -ox, -oy, true);
|
else if (ox !== -dx || oy !== -dy) screwSpread(i, x, y, -ox, -oy, true);
|
||||||
@@ -190,6 +236,10 @@ function straightScrewNext(i, p) {
|
|||||||
const xx = x + dx, yy = y + dy, next = yy * W + xx;
|
const xx = x + dx, yy = y + dy, next = yy * W + xx;
|
||||||
if ((xx >> 2) === (x >> 2) && (yy >> 2) === (y >> 2)) return next;
|
if ((xx >> 2) === (x >> 2) && (yy >> 2) === (y >> 2)) return next;
|
||||||
const q = part[next];
|
const q = part[next];
|
||||||
|
if(IS_SCREW[q] && !(SCREW_DX[q] && SCREW_DY[q]) && !!dx !== !!SCREW_DX[q]) {
|
||||||
|
const bx=xx&~3,by=yy&~3;
|
||||||
|
return dx ? (by+(SCREW_DY[q]>0?0:3))*W+bx+(y&3) : (by+(x&3))*W+bx+(SCREW_DX[q]>0?0:3);
|
||||||
|
}
|
||||||
if (!(SCREW_DX[q] && SCREW_DY[q])) return next;
|
if (!(SCREW_DX[q] && SCREW_DY[q])) return next;
|
||||||
const join = endJoin(x >> 2, y >> 2, dx + dy > 0 ? 1 : 0);
|
const join = endJoin(x >> 2, y >> 2, dx + dy > 0 ? 1 : 0);
|
||||||
return join >= 0 && (join >> 1) === (yy >> 2) * BW + (xx >> 2) ? next : -1;
|
return join >= 0 && (join >> 1) === (yy >> 2) * BW + (xx >> 2) ? next : -1;
|
||||||
@@ -213,3 +263,82 @@ function diagonalScrewNext(i, p) {
|
|||||||
}
|
}
|
||||||
return yy >= 0 && yy < H && !IS_SCREW[part[yy * W + xx]] ? yy * W + xx : -1;
|
return yy >= 0 && yy < H && !IS_SCREW[part[yy * W + xx]] ? yy * W + xx : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move one screw cell after the cells ahead of it, including adjoining belts.
|
||||||
|
function screwCarry(i) {
|
||||||
|
const p=part[i];
|
||||||
|
if(!IS_SCREW[p] || occ[i])return;
|
||||||
|
const dx=SCREW_DX[p], dy=SCREW_DY[p], flat=dy===0;
|
||||||
|
|
||||||
|
const m = mat[i];
|
||||||
|
if (m === EMPTY || PH[m] !== PH_POWDER) { if ((!dx || !dy || diagonalBeat()) && (m === EMPTY || PH[m] !== PH_SOLID)) screwIntake(i, p, dx, dy); return; }
|
||||||
|
if (stamp[i] === tickStamp) return;
|
||||||
|
const carrier = (Math.floor(i / W) >> 2) * BW + ((i % W) >> 2);
|
||||||
|
transportNote(carrier);
|
||||||
|
if (temp[i] > 1000 && rnd() < 0.02) {
|
||||||
|
part[i] = 0; markPow(i, false); emit({ t: 'warn', x: i % W, y: (i / W) | 0, text: 'melted' }); wakeIdx(i);
|
||||||
|
if (!ledger.screwMelt) { ledger.screwMelt = true; emit({ t: 'screw_melt' }); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(dx && dy && !diagonalBeat())return;
|
||||||
|
const s = netSat(i);
|
||||||
|
if (s <= 0 || rnd() > s) return;
|
||||||
|
const x = i % W, y = (i / W) | 0;
|
||||||
|
if(flat && !(y&3) && y>0 && PH[mat[i-W]]===PH_POWDER && !part[i-W] && roomFor(i+W) && !occ[i+W]) {
|
||||||
|
move(i,i+W,x,y,x,y+1);transportNote(carrier,true);return;
|
||||||
|
}
|
||||||
|
if(flat && x+dx>=0 && x+dx<W && !roomFor(i+dx) && y+1<H && part[i+W]===p && roomFor(i+W) && !occ[i+W]) {
|
||||||
|
move(i,i+W,x,y,x,y+1);transportNote(carrier,true);return;
|
||||||
|
}
|
||||||
|
if (!flat && !dx) {
|
||||||
|
let own = 0;
|
||||||
|
const end = dy < 0 ? (y & ~3) : (y | 3);
|
||||||
|
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + x]] === PH_POWDER) own++;
|
||||||
|
const side = ((tick >> 1) + i) & 1 ? 1 : -1;
|
||||||
|
for (let c = 0; c < 2; c++) {
|
||||||
|
const sx = c ? -side : side, xx = x + sx, j = i + sx;
|
||||||
|
if ((xx >> 2) !== (x >> 2) || part[j] !== p || !roomFor(j) || mat[j] !== EMPTY || occ[j] || stamp[j] === tickStamp) continue;
|
||||||
|
let load = 0;
|
||||||
|
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + xx]] === PH_POWDER) load++;
|
||||||
|
// Include this grain: a strict gap prevents a sideways step being undone next beat.
|
||||||
|
if (own <= load + 1) continue;
|
||||||
|
move(i,j,x,y,xx,y);stats.moved++;transportNote(carrier,true);return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (x + dx < 0 || x + dx >= W || y + dy < 0 || y + dy >= H) return;
|
||||||
|
let dest = dx && dy ? diagonalScrewNext(i, p) : straightScrewNext(i, p);
|
||||||
|
if (dest < 0) return;
|
||||||
|
const bed=transportBed(dest);
|
||||||
|
if(bed>=0 && !IS_SCREW[part[dest]]) {
|
||||||
|
for(let j=bed-W;j>=bed-CARRY_DEPTH*W;j-=W)if(roomFor(j)&&!occ[j]){dest=j;break;}
|
||||||
|
|
||||||
|
}
|
||||||
|
const nx = dest % W, ny = (dest / W) | 0;
|
||||||
|
if (nx < 0 || ny < 0 || nx >= W || ny >= H) return;
|
||||||
|
const j = dest;
|
||||||
|
if (occ[j]) return;
|
||||||
|
const pj = part[j], screw = IS_SCREW[pj];
|
||||||
|
if (screw && !screwAccepts(pj, dx, dy)) return;
|
||||||
|
const t = mat[j];
|
||||||
|
if (!roomFor(j)) return;
|
||||||
|
if (pj === P_GRATE || pj === P_INTAKE || (pj === P_FILTER && !filterPassAt(m, j))) return;
|
||||||
|
if (!screw && !pj && t === EMPTY && (bed < 0 || dx && dy < 0)) {
|
||||||
|
if (bed >= 0) {
|
||||||
|
// Clear the raised belt's open edge; landing on its lip lets the load tumble underneath.
|
||||||
|
const mouth=bed-W-(y&3)*W;
|
||||||
|
if(!roomFor(mouth)||occ[mouth])return;
|
||||||
|
launchGrain(i,nx,(mouth/W)|0,dx*0.9,-0.5);
|
||||||
|
} else {
|
||||||
|
// out of the mouth: the screw spits it clear, so the next grain is never blocked by the last
|
||||||
|
launchGrain(i, nx, ny, dy && !dx ? (rnd() - 0.5) * (dy < 0 ? 0.9 : 0.1) : dx * (0.42 + rnd() * 0.16), dy ? dy * (0.5 + rnd() * 0.2) : rnd() * 0.08);
|
||||||
|
}
|
||||||
|
stats.moved++; transportNote(carrier, true); if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j); return;
|
||||||
|
}
|
||||||
|
move(i, j, x, y, nx, ny); stats.moved++; transportNote(carrier, true);
|
||||||
|
if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j);
|
||||||
|
if (screw && pj !== p && (dx && dy || SCREW_DX[pj] && SCREW_DY[pj])) {
|
||||||
|
// turning a corner: keep going across the new screw's width so every lane fills
|
||||||
|
screwSpread(j, nx, ny, dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
+6
-6
@@ -120,9 +120,9 @@ function entScrewTick(e, p) {
|
|||||||
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) if (part[yy * W + xx]) return;
|
for (let yy = y; yy < y + BAR_H; yy++) for (let xx = x; xx < x + BAR_W; xx++) if (part[yy * W + xx]) return;
|
||||||
if (entScrewMove(e, x, y)) { e.vx = dx ? dx * 0.5 : (rnd() - 0.5) * 0.4; e.vy = dy * 0.6; stats.moved++; }
|
if (entScrewMove(e, x, y)) { e.vx = dx ? dx * 0.5 : (rnd() - 0.5) * 0.4; e.vy = dy * 0.6; stats.moved++; }
|
||||||
}
|
}
|
||||||
// A horizontal bar stays whole: one diagonal block per four screw beats.
|
// A horizontal bar stays whole; its block steps follow the same distance clock as grains.
|
||||||
function entDiagonalScrewTick(e, p) {
|
function entDiagonalScrewTick(e, p) {
|
||||||
if (tick & 7) return;
|
if (!diagonalBeat(tick,4)) return;
|
||||||
const dx = SCREW_DX[p], dy = SCREW_DY[p], bx = e.ix & ~3, by = e.iy & ~3, lane = e.iy & 3;
|
const dx = SCREW_DX[p], dy = SCREW_DY[p], bx = e.ix & ~3, by = e.iy & ~3, lane = e.iy & 3;
|
||||||
const edge = (by + lane) * W + bx + (dx > 0 ? BS - 1 : 0);
|
const edge = (by + lane) * W + bx + (dx > 0 ? BS - 1 : 0);
|
||||||
const dest = diagonalScrewNext(edge, p);
|
const dest = diagonalScrewNext(edge, p);
|
||||||
@@ -163,12 +163,12 @@ function entBeltScrewEnter(e) {
|
|||||||
function entSlopeBeltTick(e) {
|
function entSlopeBeltTick(e) {
|
||||||
const bottom = e.iy + BAR_H;
|
const bottom = e.iy + BAR_H;
|
||||||
if (bottom >= H) return false;
|
if (bottom >= H) return false;
|
||||||
let d = 0, slope = false, screen = false;
|
let d = 0, slope = false;
|
||||||
for (let x = e.ix; x < e.ix + BAR_W; x++) {
|
for (let x = e.ix; x < e.ix + BAR_W; x++) {
|
||||||
const i = bottom * W + x, b = (bottom >> 2) * BW + (x >> 2), c = blockP[b];
|
const i = bottom * W + x, b = (bottom >> 2) * BW + (x >> 2), c = blockP[b];
|
||||||
if ((isBelt(c) || isFilter(c)) && entSolidCell(i)) {
|
if ((isBelt(c) || isFilter(c)) && entSolidCell(i)) {
|
||||||
d = isFilter(c) ? (blockV[b] & 128 ? 1 : -1) : (blockV[b] ? 1 : -1);
|
d = isFilter(c) ? (blockV[b] & 128 ? 1 : -1) : (blockV[b] ? 1 : -1);
|
||||||
if (isSlopeBelt(c)) { slope = true; if (isFilter(c)) screen = true; }
|
if (isSlopeBelt(c)) slope = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!d) return false;
|
if (!d) return false;
|
||||||
@@ -179,12 +179,12 @@ function entSlopeBeltTick(e) {
|
|||||||
const b = (y >> 2) * BW + (x >> 2), c = blockP[b];
|
const b = (y >> 2) * BW + (x >> 2), c = blockP[b];
|
||||||
if (!(isBelt(c) || isFilter(c)) || (isFilter(c) && filterPassAt(e.m, y * W + x))) continue;
|
if (!(isBelt(c) || isFilter(c)) || (isFilter(c) && filterPassAt(e.m, y * W + x))) continue;
|
||||||
if (beltFaceY(c, x >> 2, y >> 2, x) !== y) continue;
|
if (beltFaceY(c, x >> 2, y >> 2, x) !== y) continue;
|
||||||
if (isSlopeBelt(c)) { slope = true; if (isFilter(c)) screen = true; }
|
if (isSlopeBelt(c)) slope = true;
|
||||||
face = Math.min(face, y);
|
face = Math.min(face, y);
|
||||||
}
|
}
|
||||||
if (!slope) return false;
|
if (!slope) return false;
|
||||||
e.x = e.ix; e.y = e.iy; e.vx = e.vy = 0;
|
e.x = e.ix; e.y = e.iy; e.vx = e.vy = 0;
|
||||||
if (screen ? tick % SCREEN_NUDGE_TICKS : tick & 1) return true;
|
if (!diagonalBeat()) return true;
|
||||||
const y = face === H ? e.iy : face - BAR_H, x = e.ix + d;
|
const y = face === H ? e.iy : face - BAR_H, x = e.ix + d;
|
||||||
if (entFreeAt(x, y)) { e.vx = d * 0.5; e.vy = (y - e.iy) * 0.5; e.x = e.ix = x; e.y = e.iy = y; stats.moved++; }
|
if (entFreeAt(x, y)) { e.vx = d * 0.5; e.vy = (y - e.iy) * 0.5; e.x = e.ix = x; e.y = e.iy = y; stats.moved++; }
|
||||||
else if (!entScrewEnter(e, d, 0)) entScrewEnter(e, d, Math.sign(y - e.iy), true);
|
else if (!entScrewEnter(e, d, 0)) entScrewEnter(e, d, Math.sign(y - e.iy), true);
|
||||||
|
|||||||
@@ -94,68 +94,4 @@ function machinesTick() {
|
|||||||
slopeCleatsTick();
|
slopeCleatsTick();
|
||||||
carryBeltsAndFilters(beltL, filterL, -1, BELTL, (tick & 1) === 0);
|
carryBeltsAndFilters(beltL, filterL, -1, BELTL, (tick & 1) === 0);
|
||||||
carryBeltsAndFilters(beltR, filterR, 1, BELTR, (tick & 1) === 0);
|
carryBeltsAndFilters(beltR, filterR, 1, BELTR, (tick & 1) === 0);
|
||||||
// screws: every loose grain inside steps one cell along, front of the line first, on the same
|
|
||||||
// beat as belts (every other tick), so a block-wide screw carries as much as a block-deep belt
|
|
||||||
if ((tick & 1) === 0) for (let p = 1; p < IS_SCREW.length; p++) {
|
|
||||||
if (!IS_SCREW[p]) continue;
|
|
||||||
const list = screwLists[p], dx = SCREW_DX[p], dy = SCREW_DY[p], flat = dy === 0;
|
|
||||||
for (let k = 0; k < list.length; k++) {
|
|
||||||
const i = list[k];
|
|
||||||
if (part[i] !== p || occ[i]) continue;
|
|
||||||
const m = mat[i];
|
|
||||||
if (m === EMPTY || PH[m] !== PH_POWDER) { if (m === EMPTY || PH[m] !== PH_SOLID) screwIntake(i, p, dx, dy); continue; }
|
|
||||||
if (stamp[i] === tickStamp) continue;
|
|
||||||
const carrier = (Math.floor(i / W) >> 2) * BW + ((i % W) >> 2);
|
|
||||||
transportNote(carrier);
|
|
||||||
if (temp[i] > 1000 && rnd() < 0.02) {
|
|
||||||
part[i] = 0; markPow(i, false); emit({ t: 'warn', x: i % W, y: (i / W) | 0, text: 'melted' }); wakeIdx(i);
|
|
||||||
if (!ledger.screwMelt) { ledger.screwMelt = true; emit({ t: 'screw_melt' }); }
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const s = netSat(i);
|
|
||||||
if (s <= 0 || rnd() > s) continue;
|
|
||||||
const x = i % W, y = (i / W) | 0;
|
|
||||||
// a sideways screw carries its load along the bottom: grains settle into empty lanes below
|
|
||||||
if (flat && y + 1 < H && part[i + W] === p && roomFor(i + W) && !occ[i + W]) { move(i, i + W, x, y, x, y + 1); transportNote(carrier, true); continue; }
|
|
||||||
if (!flat && !dx) {
|
|
||||||
let own = 0, shifted = false;
|
|
||||||
const end = dy < 0 ? (y & ~3) : (y | 3);
|
|
||||||
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + x]] === PH_POWDER) own++;
|
|
||||||
const side = ((tick >> 1) + i) & 1 ? 1 : -1;
|
|
||||||
for (let c = 0; c < 2; c++) {
|
|
||||||
const sx = c ? -side : side, xx = x + sx, j = i + sx;
|
|
||||||
if ((xx >> 2) !== (x >> 2) || part[j] !== p || !roomFor(j) || mat[j] !== EMPTY || occ[j] || stamp[j] === tickStamp) continue;
|
|
||||||
let load = 0;
|
|
||||||
for (let yy = y; dy < 0 ? yy >= end : yy <= end; yy += dy) if (PH[mat[yy * W + xx]] === PH_POWDER) load++;
|
|
||||||
// Include this grain: a strict gap prevents a sideways step being undone next beat.
|
|
||||||
if (own <= load + 1) continue;
|
|
||||||
move(i, j, x, y, xx, y); stats.moved++; transportNote(carrier, true); shifted = true; break;
|
|
||||||
}
|
|
||||||
if (shifted) continue;
|
|
||||||
}
|
|
||||||
if (x + dx < 0 || x + dx >= W || y + dy < 0 || y + dy >= H) continue;
|
|
||||||
const dest = dx && dy ? diagonalScrewNext(i, p) : straightScrewNext(i, p);
|
|
||||||
if (dest < 0) continue;
|
|
||||||
const nx = dest % W, ny = (dest / W) | 0;
|
|
||||||
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
|
||||||
const j = dest;
|
|
||||||
if (occ[j]) continue;
|
|
||||||
const pj = part[j], screw = IS_SCREW[pj];
|
|
||||||
if (screw && !screwAccepts(pj, dx, dy)) continue;
|
|
||||||
const t = mat[j];
|
|
||||||
if (!roomFor(j)) continue;
|
|
||||||
if (pj === P_GRATE || pj === P_INTAKE || (pj === P_FILTER && !filterPassAt(m, j))) continue;
|
|
||||||
if (!screw && !pj && t === EMPTY) {
|
|
||||||
// out of the mouth: the screw spits it clear, so the next grain is never blocked by the last
|
|
||||||
launchGrain(i, nx, ny, dy && !dx ? (rnd() - 0.5) * (dy < 0 ? 0.9 : 0.1) : dx * (0.42 + rnd() * 0.16), dy ? dy * (0.5 + rnd() * 0.2) : rnd() * 0.08);
|
|
||||||
stats.moved++; transportNote(carrier, true); if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j); continue;
|
|
||||||
}
|
|
||||||
move(i, j, x, y, nx, ny); stats.moved++; transportNote(carrier, true);
|
|
||||||
if (flowOn) flowCross((y >> 2) * BW + (x >> 2), i, j);
|
|
||||||
if (screw && pj !== p) {
|
|
||||||
// turning a corner: keep going across the new screw's width so every lane fills
|
|
||||||
screwSpread(j, nx, ny, dx, dy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,28 @@ function rebuildPower() {
|
|||||||
if (!net) continue;
|
if (!net) continue;
|
||||||
if (mm.type === 'fan') net.fans.push(mm); else if (mm.type === 'wheel' || mm.type === 'engine') net.prods.push(mm);
|
if (mm.type === 'fan') net.fans.push(mm); else if (mm.type === 'wheel' || mm.type === 'engine') net.prods.push(mm);
|
||||||
}
|
}
|
||||||
|
screwCarryR=[];screwCarryL=[];
|
||||||
|
for(const p of SCREW_PARTS)for(const i of screwLists[p]) {
|
||||||
|
let d=SCREW_DX[p];
|
||||||
|
if(!d){
|
||||||
|
const x=i%W,y=(i/W)|0,by=y>>2;
|
||||||
|
// A vertical run follows its outlet corner, or the belt feeding its side.
|
||||||
|
for(let yy=by;yy>=0&&yy<BH;yy+=SCREW_DY[p]) {
|
||||||
|
const b=yy*BW+(x>>2);
|
||||||
|
if(blockP[b]!==BP_SCREW)break;
|
||||||
|
const v=blockV[b];if(SCREW_DIRS[v][0]){d=SCREW_DIRS[v][0];break;}
|
||||||
|
if((x>>2)>0 && isBelt(blockP[b-1]) && (blockV[b-1]&1))d=1;
|
||||||
|
if((x>>2)+1<BW && isBelt(blockP[b+1]) && !(blockV[b+1]&1))d=-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(d<0?screwCarryL:screwCarryR).push(i);
|
||||||
|
}
|
||||||
|
const vertical=new Int8Array(BW);
|
||||||
|
for(const p of SCREW_PARTS)if(!SCREW_DX[p])for(const i of screwLists[p])vertical[(i%W)>>2]=SCREW_DY[p];
|
||||||
|
const order=(a,b,d)=>(((a%W)>>2)-((b%W)>>2))*d ||
|
||||||
|
(((a/W)>>2)-((b/W)>>2))*(vertical[(a%W)>>2]||1)*-1 ||
|
||||||
|
((a%W)-(b%W))*d || (SCREW_DY[part[a]]<0?a-b:b-a);
|
||||||
|
screwCarryR.sort((a,b)=>order(a,b,-1));screwCarryL.sort((a,b)=>order(a,b,1));
|
||||||
beltL.sort((a, b) => (a % W) - (b % W) || a - b); beltR.sort((a, b) => (b % W) - (a % W) || b - a);
|
beltL.sort((a, b) => (a % W) - (b % W) || a - b); beltR.sort((a, b) => (b % W) - (a % W) || b - a);
|
||||||
for (const p of SCREW_PARTS) {
|
for (const p of SCREW_PARTS) {
|
||||||
screwLists[p].sort((a, b) => {
|
screwLists[p].sort((a, b) => {
|
||||||
|
|||||||
+3
-3
@@ -73,11 +73,11 @@ const PIECES = [
|
|||||||
{ id: 'wall', code: BP_WALL, name: 'Wall block', kind: 'block', variants: 'wall',
|
{ id: 'wall', code: BP_WALL, name: 'Wall block', kind: 'block', variants: 'wall',
|
||||||
desc: 'A solid block. Diagonal drags make a sloped face; T flips it. For one block, T cycles square and four slope corners. R picks the material: a square costs 16 grains or 2 bars; a slope costs 8 grains or 1 bar.' },
|
desc: 'A solid block. Diagonal drags make a sloped face; T flips it. For one block, T cycles square and four slope corners. R picks the material: a square costs 16 grains or 2 bars; a slope costs 8 grains or 1 bar.' },
|
||||||
{ id: 'belt', code: BP_BELT, name: 'Belt', kind: 'block', cost: [[CASH, 12]], dirs: ['left', 'right'],
|
{ id: 'belt', code: BP_BELT, name: 'Belt', kind: 'block', cost: [[CASH, 12]], dirs: ['left', 'right'],
|
||||||
desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it. Drag diagonally for a sidewall belt. Moving cleats lift pockets of grains; extra grains roll back.' },
|
desc: 'Carries loose grains and bars resting on top of it, left or right, in a layer up to a block deep. Water and other liquids drain through it. Drag diagonally for a sidewall belt. Moving cleats lift pockets of grains at belt pace; extra grains roll back.' },
|
||||||
{ id: 'screw', code: BP_SCREW, name: 'Screw conveyor', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', 'left', 'up-right', 'down-right', 'down-left', 'up-left'],
|
{ id: 'screw', code: BP_SCREW, name: 'Screw conveyor', kind: 'block', cost: [[CASH, 25]], dirs: ['up', 'right', 'down', 'left', 'up-right', 'down-right', 'down-left', 'up-left'],
|
||||||
desc: 'A turning screw inside a tube that draws in loose grains and whole bars and pushes them along, four grains wide. Water and other liquids run straight through it; the screw never carries them. Drag a line to set its direction, including diagonals. R turns a single screw conveyor. Join either end to screw conveyors or belts. Copper softens around 1000°C.' },
|
desc: 'A turning screw inside a tube that draws in loose grains and whole bars and pushes them along at belt pace, four grains wide. Water and other liquids run straight through it; the screw never carries them. Drag a line to set its direction, including diagonals. R turns a single screw conveyor. Join either end to screw conveyors or belts. Copper softens around 1000°C.' },
|
||||||
{ id: 'filter', code: BP_FILTER, name: 'Filter', kind: 'block', cost: [[CASH, 40]], dirs: ['left', 'right'], filter: true,
|
{ id: 'filter', code: BP_FILTER, name: 'Filter', kind: 'block', cost: [[CASH, 40]], dirs: ['left', 'right'], filter: true,
|
||||||
desc: 'A shaking mesh that joins belts. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through. Drag diagonally for a downhill screen. Grains slide down while it shakes; R keeps a picked slope downhill.' },
|
desc: 'A shaking mesh that joins belts. Pick materials and a mode: drop them through and carry everything else along, or carry them and drop the rest. Liquids drain through. Drag diagonally for a downhill screen. Grains and bars travel downhill at belt pace while it shakes; R keeps a picked slope downhill.' },
|
||||||
{ id: 'shaft', code: BP_SHAFT, name: 'Shaft', kind: 'block', cost: [[LWOOD, 8]], hidden: POWER_FREE,
|
{ id: 'shaft', code: BP_SHAFT, name: 'Shaft', kind: 'block', cost: [[LWOOD, 8]], hidden: POWER_FREE,
|
||||||
desc: 'Carries power between machines. Wooden, so keep it away from fire.' },
|
desc: 'Carries power between machines. Wooden, so keep it away from fire.' },
|
||||||
{ id: 'grate', code: BP_GRATE, name: 'Grate', kind: 'block', cost: [[LCLAY, 8]],
|
{ id: 'grate', code: BP_GRATE, name: 'Grate', kind: 'block', cost: [[LCLAY, 8]],
|
||||||
|
|||||||
+11
-16
@@ -4,9 +4,12 @@
|
|||||||
// rib pushes whatever is in its way ahead of it, so the pocket in front rides along while anything
|
// rib pushes whatever is in its way ahead of it, so the pocket in front rides along while anything
|
||||||
// heaped above the rib is left to roll back. If a rib can't push its way, its whole run stalls.
|
// heaped above the rib is left to roll back. If a rib can't push its way, its whole run stalls.
|
||||||
// Only uphill runs have cleats: downhill, the belt carries its load like a flat one.
|
// Only uphill runs have cleats: downhill, the belt carries its load like a flat one.
|
||||||
const CLEAT_SPACING = 8, CLEAT_HEIGHT = 6; // spacing in face cells, height in grains
|
const CLEAT_SPACING = 8, CLEAT_HEIGHT = 10; // spacing in face cells, height in grains
|
||||||
const CLEAT_REACH = Math.floor(CLEAT_HEIGHT * Math.SQRT1_2);
|
const CLEAT_REACH = Math.floor(CLEAT_HEIGHT * Math.SQRT1_2);
|
||||||
const SCREEN_NUDGE_TICKS = 4; // one downhill contact step per four ticks (15 steps/s), plus gravity
|
// Three diagonal face steps per eight ticks: 31.8 cells/s along a 45° path.
|
||||||
|
function diagonalBeat(t = tick, blocks = 1) {
|
||||||
|
return !(t & 1) && Math.floor(t*3/(8*blocks)) !== Math.floor((t-2)*3/(8*blocks));
|
||||||
|
}
|
||||||
let cleatChunks = new Uint8Array(0);
|
let cleatChunks = new Uint8Array(0);
|
||||||
let cleatMask = new Uint8Array(0), cleatCells = [], slopePhase = new Uint8Array(0);
|
let cleatMask = new Uint8Array(0), cleatCells = [], slopePhase = new Uint8Array(0);
|
||||||
let ribLog = []; // the swaps one run's ribs made this step, undone if the run stalls
|
let ribLog = []; // the swaps one run's ribs made this step, undone if the run stalls
|
||||||
@@ -55,9 +58,9 @@ function pushRow(p, dx, dy) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// At the foot of an uphill run, a rib coming up around the end roller scoops what lies on the
|
// At the foot of an uphill run, a rib coming up around the end roller scoops what lies on the
|
||||||
// feeding belt behind it, one pocket long and as high as the rib, into the pocket in front of it.
|
// feeding belt behind it, about one pocket long and as high as the rib, into the pocket in front of it.
|
||||||
function scoop(x, y, d, rib) {
|
function scoop(x, y, d, rib) {
|
||||||
for (let k = 1; k < CLEAT_SPACING; k++) {
|
for (let k = 1; k <= CLEAT_SPACING + 1; k++) {
|
||||||
const xx = x - d * k;
|
const xx = x - d * k;
|
||||||
if (xx < 0 || xx >= W) break;
|
if (xx < 0 || xx >= W) break;
|
||||||
for (let yy = y; yy >= y - Math.min(k, CLEAT_REACH) && yy >= 0; yy--) {
|
for (let yy = y; yy >= y - Math.min(k, CLEAT_REACH) && yy >= 0; yy--) {
|
||||||
@@ -137,7 +140,7 @@ function slopeCleatsTick(advance = true) {
|
|||||||
if (slopePhase[b] !== p) { slopePhase[b] = p; resynced = true; }
|
if (slopePhase[b] !== p) { slopePhase[b] = p; resynced = true; }
|
||||||
}
|
}
|
||||||
if (resynced) rebuildCleatMask();
|
if (resynced) rebuildCleatMask();
|
||||||
if (advance && !(tick & 1)) {
|
if (advance && diagonalBeat()) {
|
||||||
// each run moves all its ribs or none: if one can't clear its path, the run stalls and every
|
// each run moves all its ribs or none: if one can't clear its path, the run stalls and every
|
||||||
// grain its ribs moved goes back. Front of the line first, so a rib pushes into room the rib
|
// grain its ribs moved goes back. Front of the line first, so a rib pushes into room the rib
|
||||||
// ahead of it has just made.
|
// ahead of it has just made.
|
||||||
@@ -171,7 +174,7 @@ function slopeCleatsTick(advance = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
rebuildCleatMask();
|
rebuildCleatMask();
|
||||||
if (advance && !(tick & 1)) {
|
if (advance && diagonalBeat()) {
|
||||||
// the running face drags the grains lying right on it; unless something below holds them,
|
// the running face drags the grains lying right on it; unless something below holds them,
|
||||||
// they slide back down between steps, which on 45° is faster than the belt runs
|
// they slide back down between steps, which on 45° is faster than the belt runs
|
||||||
for (const list of [beltR, beltL]) for (const i of list) {
|
for (const list of [beltR, beltL]) for (const i of list) {
|
||||||
@@ -215,17 +218,9 @@ function expelFromRib(i) {
|
|||||||
if (PH[mat[q]] !== PH_POWDER) return;
|
if (PH[mat[q]] !== PH_POWDER) return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A screen shakes only the grains touching its mesh, never the whole column above it.
|
// A screen shakes a finite bed, on the same distance clock as the other diagonal carriers.
|
||||||
function screenNudge(top,d,pass) {
|
function screenNudge(top,d,pass) {
|
||||||
if (tick % SCREEN_NUDGE_TICKS) return;
|
if(diagonalBeat())carryColumn(top-W,d,pass);
|
||||||
const i = top - W, m = mat[i];
|
|
||||||
if (i < 0 || PH[m] !== PH_POWDER || stamp[i] === tickStamp || occ[i]) return;
|
|
||||||
const carrier = ((top / W) >> 2) * BW + ((top % W) >> 2);
|
|
||||||
transportNote(carrier);
|
|
||||||
if (pass && pass[m] && enter(m, PH_POWDER, top, 0, 1)) { transportNote(carrier, true); return; }
|
|
||||||
const j = i + d + W;
|
|
||||||
if (i % W + d < 0 || i % W + d >= W || j >= N) return;
|
|
||||||
if (enter(m, PH_POWDER, j, d, 1)) { move(i, j, i % W, (i / W) | 0, j % W, (j / W) | 0); transportNote(carrier, true); if (flowOn) flowCross(carrier, i, j); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only loose load can react to a barrier appearing or disappearing.
|
// Only loose load can react to a barrier appearing or disappearing.
|
||||||
|
|||||||
@@ -12,14 +12,15 @@ function transportClock(b) { return transportState.get(b)?.clock ?? tick; }
|
|||||||
function transportBegin() {
|
function transportBegin() {
|
||||||
for (const [b, s] of transportState) {
|
for (const [b, s] of transportState) {
|
||||||
if (blockP[b] !== s.code || blockV[b] !== s.v || !pieceEnds(s.code, s.v).length) { transportState.delete(b); continue; }
|
if (blockP[b] !== s.code || blockV[b] !== s.v || !pieceEnds(s.code, s.v).length) { transportState.delete(b); continue; }
|
||||||
if (isFilter(s.code) && isSlopeBelt(s.code) && tick % SCREEN_NUDGE_TICKS) continue;
|
if ((isSlopeBelt(s.code)||s.code===BP_SCREW&&s.v>=4) && !diagonalBeat()) continue;
|
||||||
s.loaded = s.moved = s.jam = false;
|
s.loaded = s.moved = s.jam = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function transportEnd() {
|
function transportEnd() {
|
||||||
for (const s of transportState.values()) {
|
for (const s of transportState.values()) {
|
||||||
const cadence = isFilter(s.code) && isSlopeBelt(s.code) ? SCREEN_NUDGE_TICKS : 2;
|
const diagonal=isSlopeBelt(s.code)||s.code===BP_SCREW&&s.v>=4;
|
||||||
if (tick % cadence) continue;
|
if (diagonal ? !diagonalBeat() : tick&1)continue;
|
||||||
|
const cadence=diagonal&&!diagonalBeat(tick-2)?4:2;
|
||||||
s.age = s.jam || (s.loaded && !s.moved) ? Math.min(STALL_TICKS, s.age + cadence) : 0;
|
s.age = s.jam || (s.loaded && !s.moved) ? Math.min(STALL_TICKS, s.age + cadence) : 0;
|
||||||
if (s.age < STALL_TICKS) s.clock += cadence;
|
if (s.age < STALL_TICKS) s.clock += cadence;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+61
@@ -0,0 +1,61 @@
|
|||||||
|
// Shared t105 measurement and screenshot layout. No generated terrain.
|
||||||
|
function transportFixture(kind, reverse = false) {
|
||||||
|
RS=7; nextPowerBuild=0; money=1e6; AMB.fill(20); temp.fill(20); hot.fill(0); weather.next=1e15;
|
||||||
|
const blocks=[];
|
||||||
|
const put=(c,v,x,y)=>blocks.push([c,v,x,y]);
|
||||||
|
const belt=(a,b,y)=>{for(let x=a;x<b;x++)put(BP_BELT,1,x,y);};
|
||||||
|
let inlet=24, outlet=24, start=[72,95], axis=0, sign=1, diagonal=false;
|
||||||
|
if(kind==='belt'||kind==='filter') {
|
||||||
|
belt(10,16,24);for(let x=16;x<26;x++)put(kind==='belt'?BP_BELT:BP_FILTER,kind==='belt'?1:128,x,24);belt(26,32,24);
|
||||||
|
} else if(kind==='uphill'||kind==='downhill'||kind==='screen') {
|
||||||
|
const g=kind==='uphill'?-1:1, by=g<0?34:20;
|
||||||
|
inlet=by+(g<0?1:0);outlet=by+9*g+(g>0?1:0);diagonal=true;
|
||||||
|
belt(10,16,inlet);for(let k=0;k<10;k++)put(kind==='screen'?BP_FILTER_DOWN:g<0?BP_BELT_UP:BP_BELT_DOWN,kind==='screen'?128:1,16+k,by+k*g);belt(26,32,outlet);
|
||||||
|
start=[72,beltFaceY(g<0?BP_BELT_UP:BP_BELT_DOWN,18,by+2*g,72)-1];
|
||||||
|
} else if(kind==='screw-right') {
|
||||||
|
inlet=25;outlet=25;belt(10,16,25);for(let x=16;x<26;x++)put(BP_SCREW,1,x,24);belt(26,32,25);start=[72,99];
|
||||||
|
} else if(kind==='screw-up'||kind==='screw-down') {
|
||||||
|
const up=kind==='screw-up';inlet=up?35:20;outlet=up?24:32;axis=1;sign=up?-1:1;
|
||||||
|
belt(10,16,inlet);for(let y=up?24:20;y<(up?35:31);y++)put(BP_SCREW,up?0:2,16,y);
|
||||||
|
for(let x=16;x<20;x++)put(BP_SCREW,1,x,up?23:31);belt(20,32,outlet);start=[65,up?128:88];
|
||||||
|
} else {
|
||||||
|
const up=kind==='screw-up-right';inlet=up?35:20;outlet=up?25:30;diagonal=true;
|
||||||
|
belt(10,16,inlet);for(let k=0;k<10;k++)put(BP_SCREW,up?4:5,16+k,(up?35:20)+k*(up?-1:1));belt(26,32,outlet);start=[72,up?133:89];
|
||||||
|
}
|
||||||
|
const shift=24-inlet;for(const b of blocks)b[3]+=shift;start[1]+=shift*4;outlet+=shift;inlet=24;
|
||||||
|
if(reverse)for(const b of blocks){b[2]=41-b[2];if(b[0]===BP_SCREW)b[1]=[0,3,2,1,7,6,5,4][b[1]];else {if(isSlopeBelt(b[0]))b[0]+=b[0]===BP_BELT_UP||b[0]===BP_FILTER_UP?1:-1;b[1]=isFilter(b[0])?0:0;}}
|
||||||
|
for(const [c,v,x,y] of blocks)placeBlock(c,v,x,y,true,{m:[CUORE],inv:0});
|
||||||
|
if(reverse)start[0]=167-start[0];
|
||||||
|
return {kind,reverse,inlet,outlet,start,axis,sign,diagonal,feedX:reverse?123:44,sink:reverse?40:128};
|
||||||
|
}
|
||||||
|
function transportFeed(s,depth) {
|
||||||
|
let added=0;
|
||||||
|
for(let l=1;l<=depth;l++){const i=(s.inlet*4-l)*W+s.feedX;if(mat[i]===EMPTY){setCell(i,SAND,20,0);added++;}}
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
function transportCollect(s) {
|
||||||
|
let out=0;
|
||||||
|
for(let y=0;y<H;y++)for(let x=s.reverse?0:s.sink;x<(s.reverse?s.sink:W);x++){const i=y*W+x;if(mat[i]===SAND){out++;setCell(i,EMPTY,20,0);}}
|
||||||
|
for(let j=flyers.length-1;j>=0;j--)if(flyers[j].m===SAND&&(s.reverse?flyers[j].x<s.sink:flyers[j].x>=s.sink)){out++;flyers.splice(j,1);}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transportChainFixture() {
|
||||||
|
RS=7; nextPowerBuild=0; money=1e6; AMB.fill(20); temp.fill(20); hot.fill(0); weather.next=1e15;
|
||||||
|
const belt=(a,b,y)=>{for(let x=a;x<b;x++)placeBlock(BP_BELT,1,x,y,true);};
|
||||||
|
belt(10,16,48);
|
||||||
|
for(let k=0;k<6;k++)placeBlock(BP_BELT_UP,1,16+k,47-k,true);
|
||||||
|
belt(22,30,42);
|
||||||
|
for(let y=32;y<42;y++)placeBlock(BP_SCREW,0,30,y,true);
|
||||||
|
for(let x=30;x<34;x++)placeBlock(BP_SCREW,1,x,31,true);
|
||||||
|
belt(34,40,32);
|
||||||
|
for(let x=40;x<46;x++)placeBlock(BP_FILTER,128,x,32,true,{m:[CUORE],inv:0});
|
||||||
|
belt(46,52,32);
|
||||||
|
for(let k=0;k<6;k++)placeBlock(BP_SCREW,4,52+k,32-k,true);
|
||||||
|
belt(58,66,26);
|
||||||
|
return {kind:'chain',reverse:false,inlet:48,feedX:44,sink:264,
|
||||||
|
joins:[['sidewall',64,192],['top belt',88,168],['lift',120,168],['lift outlet',136,128],['filter',160,128],['filter outlet',184,128],['diagonal screw',208,128],['final belt',232,104]]};
|
||||||
|
}
|
||||||
|
function transportPiles(s) {
|
||||||
|
return s.joins.map(([name,x,y])=>[name,(()=>{let n=0;for(let yy=y-20;yy<y;yy++)for(let xx=x-8;xx<x+4;xx++)if(mat[yy*W+xx]===SAND)n++;return n;})()]);
|
||||||
|
}
|
||||||
+8
-5
@@ -5,11 +5,11 @@ const { check, done } = suite();
|
|||||||
function fresh() { flatWorld(run, 320, 200, 120); run('bot.x = 20; bot.y = 40; money = 1e6; globalThis.__got = 0;'); }
|
function fresh() { flatWorld(run, 320, 200, 120); run('bot.x = 20; bot.y = 40; money = 1e6; globalThis.__got = 0;'); }
|
||||||
const screwLiquid = () => run('(() => { let n = 0; for (let i = 0; i < N; i++) if (part[i] >= P_SCREW_U && part[i] <= P_SCREW_R && PH[mat[i]] === PH_LIQUID) n++; return n; })()');
|
const screwLiquid = () => run('(() => { let n = 0; for (let i = 0; i < N; i++) if (part[i] >= P_SCREW_U && part[i] <= P_SCREW_R && PH[mat[i]] === PH_LIQUID) n++; return n; })()');
|
||||||
// grains that land in the sink region are counted and taken away, so outlets never back up
|
// grains that land in the sink region are counted and taken away, so outlets never back up
|
||||||
function rate(x0, x1, y0, y1, warm, meas) {
|
function rate(x0, x1, y0, y1, warm, meas, sample = '') {
|
||||||
const sink = `for (let y=${y0};y<=${y1};y++) for (let x=${x0};x<=${x1};x++) { const i=y*W+x; if (mat[i]===SAND) { mat[i]=EMPTY; __got++; } }`;
|
const sink = `for (let y=${y0};y<=${y1};y++) for (let x=${x0};x<=${x1};x++) { const i=y*W+x; if (mat[i]===SAND) { mat[i]=EMPTY; __got++; } }`;
|
||||||
for (let k = 0; k < warm; k++) run(`simTick(); ${sink}`);
|
for (let k = 0; k < warm; k++) run(`simTick(); ${sample} ${sink}`);
|
||||||
run('__got = 0');
|
run('__got = 0');
|
||||||
for (let k = 0; k < meas; k++) run(`simTick(); ${sink}`);
|
for (let k = 0; k < meas; k++) run(`simTick(); ${sample} ${sink}`);
|
||||||
return run('__got') / (meas / 60);
|
return run('__got') / (meas / 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,8 +116,11 @@ fresh();
|
|||||||
run('for (let y = 120; y < 196; y++) for (let x = 90; x < 130; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
|
run('for (let y = 120; y < 196; y++) for (let x = 90; x < 130; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
|
||||||
rect(run, 90, 130, 129, 195, run('SAND'));
|
rect(run, 90, 130, 129, 195, run('SAND'));
|
||||||
run('for (let by = 20; by <= 37; by++) placeBlock(BP_SCREW, 0, 26, by, true); for (let bx = 26; bx <= 40; bx++) placeBlock(BP_SCREW, 1, bx, 19, true);');
|
run('for (let by = 20; by <= 37; by++) placeBlock(BP_SCREW, 0, 26, by, true); for (let bx = 26; bx <= 40; bx++) placeBlock(BP_SCREW, 1, bx, 19, true);');
|
||||||
const lr = rate(164, 319, 80, 119, 400, 300);
|
// Observe while the finite pit feeds the lift: faster turns can empty it before the final tick.
|
||||||
const liftLanes = run('(() => { const s = new Set(); for (let y = 76; y < 80; y++) for (let x = 120; x < 160; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
|
run('globalThis.__liftLanes = new Set();');
|
||||||
|
const lr = rate(164, 319, 80, 119, 400, 300,
|
||||||
|
'for(let y=76;y<80;y++)for(let x=120;x<160;x++)if(mat[y*W+x]===SAND)__liftLanes.add(y);');
|
||||||
|
const liftLanes = run('__liftLanes.size');
|
||||||
check('a lift turns its corner without squeezing down to one lane', liftLanes === 4 && lr > 30, `${lr.toFixed(0)}/s, ${liftLanes} lanes at the top`);
|
check('a lift turns its corner without squeezing down to one lane', liftLanes === 4 && lr > 30, `${lr.toFixed(0)}/s, ${liftLanes} lanes at the top`);
|
||||||
fresh();
|
fresh();
|
||||||
run('for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
|
run('for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ for (const v of [4,5,6,7]) for (const side of [-1,1]) for (const code of ['BP_BE
|
|||||||
check(`${code} at diagonal ${v} end ${end} draws the shared join`,run(`(diagonalScrewJoins(20,20,${dv})>>${end*3}&7)===${kind}`));
|
check(`${code} at diagonal ${v} end ${end} draws the shared join`,run(`(diagonalScrewJoins(20,20,${dv})>>${end*3}&7)===${kind}`));
|
||||||
check(`${code} at diagonal ${v} end ${end} receives every grain lane above its face`,run(`(()=>{for(let l=0;l<4;l++){const i=(80+l)*W+${side>0?83:80};if(diagonalScrewNext(i,SCREW_PARTS[${dv}])!==${ny*4-1}*W+${side>0?84:79})return false;}return true;})()`));
|
check(`${code} at diagonal ${v} end ${end} receives every grain lane above its face`,run(`(()=>{for(let l=0;l<4;l++){const i=(80+l)*W+${side>0?83:80};if(diagonalScrewNext(i,SCREW_PARTS[${dv}])!==${ny*4-1}*W+${side>0?84:79})return false;}return true;})()`));
|
||||||
putBar(80,81);
|
putBar(80,81);
|
||||||
check(`${code} at diagonal ${v} end ${end} receives a bar on its face`,run(`(()=>{const e=ents[0];tick=8;entMark(e,0);entDiagonalScrewTick(e,SCREW_PARTS[${dv}]);entMark(e,1);return e.ix===${nx*4}&&e.iy===${ny*4-2};})()`));
|
check(`${code} at diagonal ${v} end ${end} receives a bar on its face`,run(`(()=>{const e=ents[0];tick=12;entMark(e,0);entDiagonalScrewTick(e,SCREW_PARTS[${dv}]);entMark(e,1);return e.ix===${nx*4}&&e.iy===${ny*4-2};})()`));
|
||||||
}
|
}
|
||||||
// Flat carriers enter every diagonal at its same-row inlet; blocked mouths must wait safely.
|
// Flat carriers enter every diagonal at its same-row inlet; blocked mouths must wait safely.
|
||||||
for(const v of [4,5,6,7])for(const filter of [false,true]) {
|
for(const v of [4,5,6,7])for(const filter of [false,true]) {
|
||||||
@@ -137,7 +137,8 @@ for (const v of [0, 4, 5]) {
|
|||||||
rates.push(result.out/10);
|
rates.push(result.out/10);
|
||||||
}
|
}
|
||||||
console.log('Saturated throughput, ten blocks, grains/s: straight/up-diagonal/down-diagonal '+rates.join('/'));
|
console.log('Saturated throughput, ten blocks, grains/s: straight/up-diagonal/down-diagonal '+rates.join('/'));
|
||||||
check('diagonals match straight throughput per block',rates.every(n=>n===120),rates);
|
// t105 slows diagonal path travel; four lanes now carry the belt's heavy rate.
|
||||||
|
check('straight screw retains its full lanes; diagonals match the belt heavy rate',rates[0]===120 && rates.slice(1).every(n=>n>=81&&n<=99),rates);
|
||||||
// A belt's last carrying cell feeds the tail side of an upward diagonal screw.
|
// A belt's last carrying cell feeds the tail side of an upward diagonal screw.
|
||||||
const belt=line(4);
|
const belt=line(4);
|
||||||
run('for(let bx=10;bx<16;bx++)placeBlock(BP_BELT,1,bx,32,true);');
|
run('for(let bx=10;bx<16;bx++)placeBlock(BP_BELT,1,bx,32,true);');
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ for(const grade of [-1,1]) {
|
|||||||
const released=collect(s,800);
|
const released=collect(s,800);
|
||||||
check(`grade ${grade} resumes after the head clears`,released.out===160 && released.left===0,JSON.stringify(released));
|
check(`grade ${grade} resumes after the head clears`,released.out===160 && released.left===0,JSON.stringify(released));
|
||||||
line(grade);
|
line(grade);
|
||||||
run(`for(let bx=26;bx<32;bx++)removeBlock(bx,${s.outlet});setCell((${s.by+9*grade}*4+${grade<0?0:3}-1)*W+103,SAND,20,0);tick=2;tickStamp++;machinesTick();`);
|
run(`for(let bx=26;bx<32;bx++)removeBlock(bx,${s.outlet});setCell((${s.by+9*grade}*4+${grade<0?0:3}-1)*W+103,SAND,20,0);tick=4;tickStamp++;machinesTick();`);
|
||||||
check(`grade ${grade} launches off an open end along its face`,run(`flyers.length===1 && flyers[0].vx>0 && Math.sign(flyers[0].vy)===${grade}`));
|
check(`grade ${grade} launches off an open end along its face`,run(`flyers.length===1 && flyers[0].vx>0 && Math.sign(flyers[0].vy)===${grade}`));
|
||||||
run(`spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);
|
run(`spawnBar(BARCU,${s.feedX || 44},${s.inlet*4-2},0,0);`);
|
||||||
const barLaunch=run('(()=>{for(let k=0;k<400;k++){simTick();if(ents[0].ix>=104)return ents[0].vx>0;}return false;})()');
|
const barLaunch=run('(()=>{for(let k=0;k<400;k++){simTick();if(ents[0].ix>=104)return ents[0].vx>0;}return false;})()');
|
||||||
@@ -179,22 +179,22 @@ for (const grade of [-1,1]) {
|
|||||||
run('rebuildPower();slopeCleatsTick(false);');
|
run('rebuildPower();slopeCleatsTick(false);');
|
||||||
if(grade>0) check('a downhill run has no cleats',run('cleatCells.length===0'));
|
if(grade>0) check('a downhill run has no cleats',run('cleatCells.length===0'));
|
||||||
else check(`grade ${grade} cleats block every downhill crossing`,run(`cleatCells.length>0 && cleatCells.every(i=>!enter(SAND,PH_POWDER,i,${grade},1) && !enter(SAND,PH_POWDER,i+W+${grade},${grade},1))`));
|
else check(`grade ${grade} cleats block every downhill crossing`,run(`cleatCells.length>0 && cleatCells.every(i=>!enter(SAND,PH_POWDER,i,${grade},1) && !enter(SAND,PH_POWDER,i+W+${grade},${grade},1))`));
|
||||||
check(`grade ${grade} bare face slides between steps without stamping the load`,run(`(()=>{
|
check(`grade ${grade} ${grade<0?'bare face slides':'carrying bed waits'} between steps without stamping the load`,run(`(()=>{
|
||||||
for(let x=72;x<96;x++){
|
for(let x=72;x<96;x++){
|
||||||
const by=${s.by}+((x>>2)-16)*${grade}, y=beltFaceY(blockP[by*BW+(x>>2)],x>>2,by,x)-1,i=y*W+x;
|
const by=${s.by}+((x>>2)-16)*${grade}, y=beltFaceY(blockP[by*BW+(x>>2)],x>>2,by,x)-1,i=y*W+x;
|
||||||
if(cleatMask[i] || cleatMask[i+W+${grade}])continue;
|
if(cleatMask[i] || cleatMask[i+W+${grade}])continue;
|
||||||
setCell(i,SAND,20,0);tick=3;tickStamp++;machinesTick();
|
setCell(i,SAND,20,0);tick=3;tickStamp++;machinesTick();
|
||||||
const unstamped=stamp[i]!==tickStamp;
|
const unstamped=stamp[i]!==tickStamp;
|
||||||
for(let k=0;k<200 && mat[i]===SAND;k++)powder(i,x,y,SAND);
|
for(let k=0;k<200 && mat[i]===SAND;k++)powder(i,x,y,SAND);
|
||||||
return unstamped && mat[i]===EMPTY;
|
return unstamped && ${grade<0?"mat[i]===EMPTY":"mat[i]===SAND"};
|
||||||
} return false;
|
} return false;
|
||||||
})()`));
|
})()`));
|
||||||
}
|
}
|
||||||
fresh();
|
fresh();
|
||||||
run('placeBlock(BP_FILTER_DOWN,128,20,20,true,{m:[],inv:0});setCell(79*W+80,GRAVEL,20,0);tickStamp++;tick=SCREEN_NUDGE_TICKS-1;screenNudge(80*W+80,1,null);');
|
run('placeBlock(BP_FILTER_DOWN,128,20,20,true,{m:[],inv:0});setCell(79*W+80,GRAVEL,20,0);tickStamp++;tick=2;screenNudge(80*W+80,1,null);');
|
||||||
check('screen waits between its configured nudges',run('mat[79*W+80]===GRAVEL && stamp[79*W+80]!==tickStamp'));
|
check('screen waits between its configured nudges',run('mat[79*W+80]===GRAVEL && stamp[79*W+80]!==tickStamp'));
|
||||||
run('tick++;screenNudge(80*W+80,1,null);');
|
run('tick+=2;screenNudge(80*W+80,1,null);');
|
||||||
check('screen nudges just its mesh contact downhill at the configured rate',run('mat[80*W+81]===GRAVEL && mat[79*W+80]===EMPTY'));
|
check('screen carries its mesh contact downhill at the shared diagonal rate',run('mat[80*W+81]===GRAVEL && mat[79*W+80]===EMPTY'));
|
||||||
fresh();run('placeBlock(BP_BELT_UP,1,20,32,true);');
|
fresh();run('placeBlock(BP_BELT_UP,1,20,32,true);');
|
||||||
run(fs.readFileSync('src/render/slope-belts.js','utf8'));
|
run(fs.readFileSync('src/render/slope-belts.js','utf8'));
|
||||||
// The renderer prepares joins and power first; stripe pitch is measured along the rotated band.
|
// The renderer prepares joins and power first; stripe pitch is measured along the rotated band.
|
||||||
|
|||||||
@@ -67,6 +67,21 @@ function hash() {
|
|||||||
run('const forwardChunks=runChunks;runChunks=(chunks,pass,work)=>forwardChunks(chunks.slice().reverse(),pass,work);for(let k=0;k<320;k++)simTick();runChunks=forwardChunks;');
|
run('const forwardChunks=runChunks;runChunks=(chunks,pass,work)=>forwardChunks(chunks.slice().reverse(),pass,work);for(let k=0;k<320;k++)simTick();runChunks=forwardChunks;');
|
||||||
assert.strictEqual(hash(),expected,'chunk order within a colour must not matter');
|
assert.strictEqual(hash(),expected,'chunk order within a colour must not matter');
|
||||||
console.log('PASS reversing chunks within every pass preserves all bytes and effects');
|
console.log('PASS reversing chunks within every pass preserves all bytes and effects');
|
||||||
|
run(require('fs').readFileSync('test/fixtures/t105-transport.js','utf8'));
|
||||||
|
let chainExpected;
|
||||||
|
for(const n of [0,1,2,8]) {
|
||||||
|
flatWorld(run,320,256,240);
|
||||||
|
run(`WS=456;resetWeather();seen.fill(0);ledger={};EV.length=0;
|
||||||
|
for(const k of Object.keys(stats))stats[k]=0;resetEconomy();
|
||||||
|
globalThis.ts=transportChainFixture();`);
|
||||||
|
const stop=n?await enableThreads(run,n):null;
|
||||||
|
try {
|
||||||
|
run('for(let k=0;k<1000;k++){transportFeed(ts,16);simTick();transportCollect(ts);}');
|
||||||
|
const actual=hash();if(!chainExpected)chainExpected=actual;
|
||||||
|
assert.strictEqual(actual,chainExpected,`mixed transport chain differs with ${n} workers`);
|
||||||
|
console.log(`PASS mixed transport chain is identical with ${n} workers (${actual.slice(0,16)})`);
|
||||||
|
} finally {if(stop)await stop();}
|
||||||
|
}
|
||||||
const {execFileSync}=require('child_process');
|
const {execFileSync}=require('child_process');
|
||||||
const fallback=execFileSync(process.execPath,['-e',`
|
const fallback=execFileSync(process.execPath,['-e',`
|
||||||
global.SharedArrayBuffer=undefined;
|
global.SharedArrayBuffer=undefined;
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// t105: compare every direction with the unchanged t45 belt and exercise the complete line.
|
||||||
|
const {kinds,speed,throughput,chain,fresh,run}=require('../tools/experiments/transport-balance');
|
||||||
|
const {suite}=require('./harness');
|
||||||
|
const {check,done}=suite();
|
||||||
|
for(const kind of kinds)for(const reverse of [false,true]){
|
||||||
|
const name=kind+(reverse?' mirrored':'');
|
||||||
|
if(kind==='screw-up-right'||kind==='screw-down-right') {
|
||||||
|
fresh(kind,reverse);
|
||||||
|
check(`${name}: both belt joins are mutual`,run(`(()=>{let n=0;for(let b=0;b<NB;b++)if(blockP[b]===BP_SCREW)for(let e=0;e<2;e++){const j=endJoin(b%BW,(b/BW)|0,e);if(j>=0&&blockP[j>>1]===BP_BELT)n++;}return n===2;})()`));
|
||||||
|
}
|
||||||
|
const v=speed(kind,reverse),bar=speed(kind,reverse,true);
|
||||||
|
const normal=throughput(kind,reverse,1),heavy=throughput(kind,reverse,16);
|
||||||
|
check(`${name}: grain path speed matches belt`,Math.abs(v-30)<=3,v);
|
||||||
|
check(`${name}: bar path speed matches belt`,Math.abs(bar-30)<=3,bar);
|
||||||
|
check(`${name}: normal feed matches belt`,Math.abs(normal-30)<=3,normal);
|
||||||
|
check(`${name}: heavy feed matches belt`,Math.abs(heavy-90)<=9,heavy);
|
||||||
|
}
|
||||||
|
fresh('screw-up-right',false);
|
||||||
|
run('placeBlock(BP_WALL,RSTONE,26,13,true);for(let k=0;k<1000;k++){transportFeed(ts,16);simTick();transportCollect(ts);}');
|
||||||
|
check('a blocked raised outlet holds its load and warns',run('transportStalled(15*BW+25)'));
|
||||||
|
run('removeBlock(26,13);');
|
||||||
|
const released=run('(()=>{let n=0;for(let k=0;k<600;k++){transportFeed(ts,16);simTick();n+=transportCollect(ts);}return n;})()');
|
||||||
|
check('clearing the raised outlet resumes delivery',released>0,released);
|
||||||
|
const line=chain();
|
||||||
|
check('mixed chain carries the full heavy belt feed',line.heavy>=89&&line.heavy<=91,JSON.stringify(line));
|
||||||
|
check('mixed chain has mutual diagonal joins',line.joined);
|
||||||
|
check('mixed chain conserves every supplied grain',line.added===line.accounted,`${line.added}/${line.accounted}`);
|
||||||
|
check('mixed chain spills no grains below its joins',line.stray===0,line.stray);
|
||||||
|
check('mixed chain has no growing piles at joins',line.late.every(([name,n],i)=>n<=line.early[i][1]+12),JSON.stringify(line.late));
|
||||||
|
done();
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// t105: same belt feed, open sink, central single-grain/bar travel. Run with node.
|
||||||
|
const {load,flatWorld}=require('../../test/harness');
|
||||||
|
const fs=require('node:fs');
|
||||||
|
const {run}=load();
|
||||||
|
run(fs.readFileSync('test/fixtures/t105-transport.js','utf8'));
|
||||||
|
const kinds=['belt','filter','uphill','downhill','screen','screw-right','screw-up','screw-down','screw-up-right','screw-down-right'];
|
||||||
|
function fresh(kind,reverse){flatWorld(run,256,224,208);return run(`globalThis.ts=transportFixture(${JSON.stringify(kind)},${reverse});ts`);}
|
||||||
|
function speed(kind,reverse,bar=false){
|
||||||
|
fresh(kind,reverse);
|
||||||
|
return run(`(()=>{let [x,y]=ts.start;if(${bar}&&ts.kind.startsWith('screw')){x=x&~3;y=(y&~3)+2;}if(${bar}&&ts.diagonal&&!ts.kind.startsWith('screw')){let face=H;for(let xx=x;xx<x+BAR_W;xx++)for(let yy=y-8;yy<=y+8;yy++){const b=(yy>>2)*BW+(xx>>2);if(isSlopeBelt(blockP[b])&&beltFaceY(blockP[b],xx>>2,yy>>2,xx)===yy)face=Math.min(face,yy);}y=face-1;}if(${bar}){spawnBar(BARCU,20,40,0,0);const e=ents[0];entMark(e,0);e.x=e.ix=x;e.y=e.iy=y-1;entMark(e,1);}else setCell(y*W+x,SAND,20,0);
|
||||||
|
let begin=-1,first=0;
|
||||||
|
for(let k=0;k<180;k++){simTick();let px,py;
|
||||||
|
if(${bar}){px=ents[0].ix;py=ents[0].iy;}else{const i=mat.indexOf(SAND);if(i<0)continue;px=i%W;py=(i/W)|0;}
|
||||||
|
const p=ts.axis?(py-y)*ts.sign:(px-x)*(ts.reverse?-1:1);
|
||||||
|
if(begin<0&&p>=4){begin=k;first=p;}
|
||||||
|
if(begin>=0&&p>=20)return (p-first)*60/(k-begin)*(ts.diagonal?Math.SQRT2:1);
|
||||||
|
}return 0;})()`);
|
||||||
|
}
|
||||||
|
function throughput(kind,reverse,depth){fresh(kind,reverse);return run(`(()=>{let out=0;for(let k=0;k<2400;k++){transportFeed(ts,${depth});simTick();const n=transportCollect(ts);if(k>=1200)out+=n;}return out/20;})()`);}
|
||||||
|
function chain(){flatWorld(run,320,256,240);run('globalThis.ts=transportChainFixture();');return run(`(()=>{let out=0,total=0,added=0,early;for(let k=0;k<4800;k++){added+=transportFeed(ts,16);simTick();const n=transportCollect(ts);total+=n;if(k>=3600)out+=n;if(k===3599)early=transportPiles(ts);}return {heavy:out/20,added,accounted:total+mat.reduce((n,m)=>n+(m===SAND),0)+flyers.filter(f=>f.m===SAND).length,early,late:transportPiles(ts),joined:endJoin(51,32,1)>=0&&endJoin(57,27,1)>=0,stray:mat.reduce((n,m,i)=>n+(m===SAND&&i/W>=200),0)};})()`);}
|
||||||
|
function measure(){const rows=[];for(const kind of kinds)for(const reverse of [false,true]){const row={piece:kind+(reverse?' mirrored':''),speed:speed(kind,reverse),normal:throughput(kind,reverse,1),heavy:throughput(kind,reverse,16),bar:speed(kind,reverse,true)};rows.push(row);console.log(JSON.stringify(row));}return rows;}
|
||||||
|
if(require.main===module){measure();console.log(JSON.stringify({piece:'chain',...chain()}));}
|
||||||
|
module.exports={fresh,speed,throughput,kinds,chain,run};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Load test/fixtures/t105-transport.js first. The measured chain, running at heavy feed.
|
||||||
|
for(let y=64;y<248;y++)for(let x=24;x<288;x++) {
|
||||||
|
const i=y*W+x;
|
||||||
|
part[i]=0;mach[i]=0;bgm[i]=0;
|
||||||
|
setCell(i,y>=240?STONE:EMPTY,20,y>=240?F_NAT:0);
|
||||||
|
}
|
||||||
|
for(let x=24;x<288;x++){surfY[x]=240;dirtY[x]=250;}
|
||||||
|
updateRoof();
|
||||||
|
const chain=transportChainFixture();
|
||||||
|
for(let k=0;k<4800;k++){
|
||||||
|
transportFeed(chain,16);simTick();transportCollect(chain);
|
||||||
|
}
|
||||||
|
window.CAM=[152,144];
|
||||||
Reference in New Issue
Block a user