126 lines
6.3 KiB
JavaScript
126 lines
6.3 KiB
JavaScript
// Travel-directed transport ends. Precomputed probes; world/plan queries allocate nothing.
|
|
// End indices stay geometric: left/right, or up/down for vertical screws.
|
|
const JOIN_ENDS = [[]];
|
|
for (const [kind, tx, ty] of [
|
|
...SCREW_DIRS.map(([x,y]) => [x && y ? 'screw-diagonal' : x ? 'screw-horizontal' : 'screw-vertical',x,y]),
|
|
['belt-flat',-1,0], ['belt-flat',1,0],
|
|
['belt-sloped',-1,1], ['belt-sloped',1,-1],
|
|
['belt-sloped',-1,-1], ['belt-sloped',1,1]
|
|
]) {
|
|
const outlet = (tx || ty) > 0 ? 1 : 0;
|
|
JOIN_ENDS.push([0,1].map(e => ({
|
|
dx: (e ? 1 : -1) * Math.abs(tx), dy: (e ? 1 : -1) * (tx ? ty * tx : 1),
|
|
diagonal: !!(tx && ty), kind, tx, ty, outlet: e === outlet, targets: []
|
|
})));
|
|
}
|
|
function pieceEnds(code, v) {
|
|
if (code === BP_SCREW) return JOIN_ENDS[v >= 0 && v < 8 ? v + 1 : 0];
|
|
if (!isBelt(code) && !isFilter(code)) return JOIN_ENDS[0];
|
|
const d = isFilter(code) ? (v & 128 ? 1 : 0) : (v & 1);
|
|
return JOIN_ENDS[9 + d + (beltGrade(code) < 0 ? 2 : beltGrade(code) > 0 ? 4 : 0)];
|
|
}
|
|
for (const ends of JOIN_ENDS) for (let e = 0; e < ends.length; e++) {
|
|
const a = ends[e];
|
|
if (!a.outlet) continue;
|
|
for (const others of JOIN_ENDS) for (let n = 0; n < others.length; n++) {
|
|
const b = others[n], screw = a.kind.startsWith('screw'), otherScrew = b.kind.startsWith('screw');
|
|
if (b.outlet || a.tx * b.tx + a.ty * b.ty <= 0) continue;
|
|
if (screw !== otherScrew && (a.kind === 'screw-vertical' || b.kind === 'screw-vertical' ||
|
|
(a.diagonal && b.diagonal))) continue;
|
|
if (a.diagonal && b.diagonal && (a.tx !== b.tx || a.ty !== b.ty)) continue;
|
|
const dy = !screw && a.ty < 0 ? 0 : a.ty;
|
|
const offsets = [[a.tx, dy]];
|
|
// Grains on a flat belt below an uphill low end already reach its carrying face.
|
|
if (!screw && !otherScrew && !a.ty && b.ty < 0) offsets.push([a.tx, -1]);
|
|
for (const [dx, yy] of offsets) {
|
|
a.targets.push({ dx, dy: yy, ends: others, end: n });
|
|
b.targets.push({ dx: -dx, dy: -yy, ends, end: e });
|
|
}
|
|
}
|
|
}
|
|
for (const ends of JOIN_ENDS) for (const end of ends) {
|
|
const sameShape = t => t.ends[t.end].kind === end.kind &&
|
|
t.ends[t.end].tx === end.tx && t.ends[t.end].ty === end.ty;
|
|
end.targets.sort((a,b) => end.outlet ? Math.abs(a.dy - end.ty) - Math.abs(b.dy - end.ty) :
|
|
Number(sameShape(b)) - Number(sameShape(a)) || a.dy - b.dy || a.dx - b.dx);
|
|
}
|
|
// Result: neighbour block index * 2 + its end index; -1 means an open end.
|
|
function joinCandidate(bx, by, endIndex, planned) {
|
|
if (bx < 0 || by < 0 || bx >= BW || by >= BH) return -1;
|
|
const b = by * BW + bx, plan = planned && planned.get(b);
|
|
const ends = pieceEnds(plan ? plan[0] : blockP[b], plan ? plan[1] : blockV[b]);
|
|
const end = ends[endIndex];
|
|
if (!end) return -1;
|
|
for (let k = 0; k < end.targets.length; k++) {
|
|
const target = end.targets[k];
|
|
const x = bx + target.dx, y = by + target.dy;
|
|
if (x < 0 || y < 0 || x >= BW || y >= BH) continue;
|
|
const nb = y * BW + x, next = planned && planned.get(nb);
|
|
if (pieceEnds(next ? next[0] : blockP[nb], next ? next[1] : blockV[nb]) === target.ends)
|
|
return nb * 2 + target.end;
|
|
}
|
|
return -1;
|
|
}
|
|
function endJoin(bx, by, endIndex, planned = null) {
|
|
const join = joinCandidate(bx, by, endIndex, planned);
|
|
if (join < 0) return -1;
|
|
const nb = join >> 1;
|
|
// Competing neighbours cannot claim an end that chose a different partner.
|
|
return joinCandidate(nb % BW, (nb / BW) | 0, join & 1, planned) === (by * BW + bx) * 2 + endIndex ? join : -1;
|
|
}
|
|
function pieceJoins(bx, by, planned = null) {
|
|
return (endJoin(bx, by, 0, planned) >= 0 ? 1 : 0) | (endJoin(bx, by, 1, planned) >= 0 ? 2 : 0);
|
|
}
|
|
function endAt(b, end, planned = null) {
|
|
const p = planned && planned.get(b);
|
|
return pieceEnds(p ? p[0] : blockP[b], p ? p[1] : blockV[b])[end];
|
|
}
|
|
// A same-row join into a sloped belt's low end: the flat surface is level with the top of the
|
|
// slope's first block, so grains drop about 3 grains onto its face. Joined, but not seamless.
|
|
function joinIsDrop(bx, by, end, join, planned = null) {
|
|
if (join < 0 || (((join >> 1) / BW) | 0) !== by) return false;
|
|
const low = e => !!e && e.kind === 'belt-sloped' && e.dy > 0;
|
|
return low(endAt(by * BW + bx, end, planned)) || low(endAt(join >> 1, join & 1, planned));
|
|
}
|
|
// Rows to move a line so its first block joins: 0, -1 or +1. A line that already joins stays
|
|
// put; it moves only when one block up or down gives a seamless join involving a diagonal piece
|
|
// and every block there is free, so straight lines beside straight lines never jump.
|
|
function joinSnap(code, v, cells) {
|
|
if (!cells.length) return 0;
|
|
const joins = dy => {
|
|
const plan = new Map(cells.map(([x, y]) => [(y + dy) * BW + x, [code, v]]));
|
|
const x = cells[0][0], y = cells[0][1] + dy, own = pieceEnds(code, v);
|
|
for (let end = 0; end < own.length; end++) {
|
|
const j = endJoin(x, y, end, plan);
|
|
if (j < 0 || plan.has(j >> 1) || joinIsDrop(x, y, end, j, plan)) continue;
|
|
return own[end].diagonal || endAt(j >> 1, j & 1, plan).diagonal ? 2 : 1;
|
|
}
|
|
return 0;
|
|
};
|
|
if (!pieceEnds(code, v).length || joins(0)) return 0;
|
|
for (const dy of [-1, 1]) {
|
|
if (cells.every(([x, y]) => canPlaceBlock(x, y + dy, code) === '') && joins(dy) === 2) return dy;
|
|
}
|
|
return 0;
|
|
}
|
|
// "Doesn't connect": an end with no join that would join seamlessly, with a diagonal piece
|
|
// involved, if its block sat one higher (-1) or lower (+1) in empty space. 0 when no near miss.
|
|
// A drop join counts as connected. The moved block is tried through a small override view of
|
|
// `planned`, so ghost maps are never copied.
|
|
const NO_PIECE = [0, 0];
|
|
function nearMiss(bx, by, end, planned = null) {
|
|
const b = by * BW + bx, p = planned && planned.get(b), code = p ? p[0] : blockP[b], v = p ? p[1] : blockV[b];
|
|
const own = pieceEnds(code, v);
|
|
if (!own[end] || endJoin(bx, by, end, planned) >= 0) return 0;
|
|
const entry = [code, v];
|
|
for (const dy of [-1, 1]) {
|
|
const y = by + dy, nb = y * BW + bx;
|
|
if (y < 0 || y >= BH || blockP[nb] || (planned && planned.get(nb))) continue;
|
|
const view = { get: k => k === b ? NO_PIECE : k === nb ? entry : planned ? planned.get(k) : undefined };
|
|
view.has = k => view.get(k) !== undefined;
|
|
const j = endJoin(bx, y, end, view);
|
|
if (j >= 0 && !joinIsDrop(bx, y, end, j, view) && (own[end].diagonal || endAt(j >> 1, j & 1, view).diagonal)) return dy;
|
|
}
|
|
return 0;
|
|
}
|