Refactor circuit-tracing to be more in algorithms

This should make it more usable in the editor.
This commit is contained in:
Eevee (Evelyn Woods) 2024-04-21 00:39:23 -06:00
parent c45ebe60e1
commit 04d6b3dddb
4 changed files with 104 additions and 92 deletions

View File

@ -61,10 +61,33 @@ export function* find_terrain_diamond(levelish, start_cell, type_names) {
export function find_implicit_connection() { export function find_implicit_connection() {
} }
export function trace_floor_circuit(level, start_cell, start_edge, on_wire, on_dead_end) { export class Circuit {
constructor() {
this.is_powered = null;
this.tiles = new Map;
this.inputs = new Map;
}
add_tile_edge(tile, edgebits) {
this.tiles.set(tile, (this.tiles.get(tile) ?? 0) | edgebits);
}
add_input_edge(tile, edgebits) {
this.inputs.set(tile, (this.inputs.get(tile) ?? 0) | edgebits);
}
}
// Traces a wire circuit and calls the given callbacks when finding either a new wire or an ending.
// actor_mode describes how to handle circuit blocks:
// - still: Actor wires are examined only for actors with a zero cooldown. (Normal behavior.)
// - always: Actor wires are always examined. (compat.tiles_react_instantly behavior.)
// - ignore: Skip actors entirely. (Editor behavior.)
// Returns a Circuit.
export function trace_floor_circuit(levelish, actor_mode, start_cell, start_edge, on_wire, on_dead_end) {
let is_first = true; let is_first = true;
let pending = [[start_cell, start_edge]]; let pending = [[start_cell, start_edge]];
let seen_cells = new Map; let seen_cells = new Map;
let circuit = new Circuit;
while (pending.length > 0) { while (pending.length > 0) {
let next = []; let next = [];
for (let [cell, edge] of pending) { for (let [cell, edge] of pending) {
@ -80,7 +103,7 @@ export function trace_floor_circuit(level, start_cell, start_edge, on_wire, on_d
let tile = terrain; let tile = terrain;
let actor = cell.get_actor(); let actor = cell.get_actor();
if (actor && actor.type.contains_wire && ( if (actor && actor.type.contains_wire && (
actor.movement_cooldown === 0 || level.compat.tiles_react_instantly)) (actor_mode === 'still' && actor.movement_cooldown === 0) || actor_mode === 'always'))
{ {
tile = actor; tile = actor;
} }
@ -90,10 +113,27 @@ export function trace_floor_circuit(level, start_cell, start_edge, on_wire, on_d
let connections = edgeinfo.bit; let connections = edgeinfo.bit;
let mode = tile.wire_propagation_mode ?? tile.type.wire_propagation_mode; let mode = tile.wire_propagation_mode ?? tile.type.wire_propagation_mode;
if (! is_first && ((tile.wire_directions ?? 0) & edgeinfo.bit) === 0) { if (! is_first && ((tile.wire_directions ?? 0) & edgeinfo.bit) === 0) {
// There's not actually a wire here (but not if this is our starting cell, in which // There's not actually a wire here, so check for things that respond to receiving
// case we trust the caller) // power... but if this is the starting cell, we trust the caller and skip it (XXX why)
if (on_dead_end) { for (let tile2 of cell) {
on_dead_end(cell, edge); if (! tile2)
continue;
if (tile2.type.name === 'logic_gate') {
// Logic gates are technically not wired, but still attached to
// circuits, mostly so blue teleporters can follow them
let wire = tile2.type._gate_types[tile2.gate_type][
(DIRECTIONS[edge].index - DIRECTIONS[tile2.direction].index + 4) % 4];
if (! wire)
continue;
circuit.add_tile_edge(tile2, DIRECTIONS[edge].bit);
if (wire.match(/^out/)) {
circuit.add_input_edge(tile2, DIRECTIONS[edge].bit);
}
}
else if (tile2.type.on_power) {
circuit.add_tile_edge(tile2, DIRECTIONS[edge].bit);
}
} }
continue; continue;
} }
@ -113,8 +153,11 @@ export function trace_floor_circuit(level, start_cell, start_edge, on_wire, on_d
seen_cells.set(cell, seen_edges | connections); seen_cells.set(cell, seen_edges | connections);
if (on_wire) { circuit.add_tile_edge(tile, connections);
on_wire(tile, connections);
if (tile.type.is_power_source) {
// TODO could just do this in a pass afterwards?
circuit.add_input_edge(tile, connections);
} }
for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) { for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) {
@ -130,10 +173,10 @@ export function trace_floor_circuit(level, start_cell, start_edge, on_wire, on_d
// Search in this direction for a matching tunnel // Search in this direction for a matching tunnel
// Note that while actors (the fuckin circuit block) can be wired, tunnels ONLY // Note that while actors (the fuckin circuit block) can be wired, tunnels ONLY
// appear on terrain, and are NOT affected by actors on top // appear on terrain, and are NOT affected by actors on top
neighbor = find_matching_wire_tunnel(level, cell.x, cell.y, direction); neighbor = find_matching_wire_tunnel(levelish, cell.x, cell.y, direction);
} }
else { else {
neighbor = level.get_neighboring_cell(cell, direction); neighbor = levelish.get_neighboring_cell(cell, direction);
} }
/* /*
@ -151,16 +194,18 @@ export function trace_floor_circuit(level, start_cell, start_edge, on_wire, on_d
pending = next; pending = next;
is_first = false; is_first = false;
} }
return circuit;
} }
export function find_matching_wire_tunnel(level, x, y, direction) { export function find_matching_wire_tunnel(levelish, x, y, direction) {
let dirinfo = DIRECTIONS[direction]; let dirinfo = DIRECTIONS[direction];
let [dx, dy] = dirinfo.movement; let [dx, dy] = dirinfo.movement;
let nesting = 0; let nesting = 0;
while (true) { while (true) {
x += dx; x += dx;
y += dy; y += dy;
let candidate = level.cell(x, y); let candidate = levelish.cell(x, y);
if (! candidate) if (! candidate)
return null; return null;

View File

@ -77,6 +77,10 @@ export class LevelInterface {
return x + y * this.size_x; return x + y * this.size_x;
} }
cell_to_scalar(cell) {
return this.coords_to_scalar(cell.x, cell.y);
}
is_point_within_bounds(x, y) { is_point_within_bounds(x, y) {
return (x >= 0 && x < this.size_x && y >= 0 && y < this.size_y); return (x >= 0 && x < this.size_x && y >= 0 && y < this.size_y);
} }

View File

@ -520,21 +520,10 @@ export class Level extends LevelInterface {
// Build circuits out of connected wires // Build circuits out of connected wires
// TODO document this idea // TODO document this idea
if (!first_time) {
for (let circuit of this.circuits) {
for (let tile of circuit.tiles) {
tile[0].circuits = [null, null, null, null];
}
}
}
this.circuits = []; this.circuits = [];
this.power_sources = []; this.power_sources = [];
let wired_outputs = new Set; let wired_outputs = new Set;
this.wired_outputs = []; let seen_edges = new Map;
let add_to_edge_map = (map, item, edges) => {
map.set(item, (map.get(item) ?? 0) | edges);
};
for (let cell of this.linear_cells) { for (let cell of this.linear_cells) {
// We're interested in static circuitry, which means terrain // We're interested in static circuitry, which means terrain
// OR circuit blocks on top // OR circuit blocks on top
@ -585,73 +574,49 @@ export class Level extends LevelInterface {
if (! ((wire_directions | terrain.wire_tunnel_directions) & dirinfo.bit)) if (! ((wire_directions | terrain.wire_tunnel_directions) & dirinfo.bit))
continue; continue;
if (terrain.circuits && terrain.circuits[dirinfo.index]) if ((seen_edges.get(terrain) ?? 0) & dirinfo.bit)
continue; continue;
let circuit = {
is_powered: first_time ? false : null,
tiles: new Map,
inputs: new Map,
};
this.circuits.push(circuit);
// At last, a wired cell edge we have not yet handled. Floodfill from here // At last, a wired cell edge we have not yet handled. Floodfill from here
algorithms.trace_floor_circuit( let circuit = algorithms.trace_floor_circuit(
this, terrain.cell, direction, this, this.compat.tiles_react_instantly ? 'always' : 'still',
// Wire handling terrain.cell, direction,
(tile, edges) => {
if (! tile.circuits) {
tile.circuits = [null, null, null, null];
}
for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) {
if (edges & dirinfo.bit) {
tile.circuits[dirinfo.index] = circuit;
}
}
add_to_edge_map(circuit.tiles, tile, edges);
if (tile.type.on_power) {
// Red teleporters contain wires and /also/ have an on_power
// FIXME this isn't quite right since there's seemingly a 1-frame delay
wired_outputs.add(tile);
}
if (tile.type.is_power_source) {
// TODO could just do this in a pass afterwards
add_to_edge_map(circuit.inputs, tile, edges);
}
},
// Dead end handling (potentially logic gates, etc.)
(cell, edge) => {
for (let tile of cell) {
if (! tile) {
continue;
}
else if (tile.type.name === 'logic_gate') {
// Logic gates are the one non-wired tile that get attached to circuits,
// mostly so blue teleporters can follow them
if (! tile.circuits) {
tile.circuits = [null, null, null, null];
}
tile.circuits[DIRECTIONS[edge].index] = circuit;
let wire = tile.type._gate_types[tile.gate_type][
(DIRECTIONS[edge].index - DIRECTIONS[tile.direction].index + 4) % 4];
if (! wire)
return;
add_to_edge_map(circuit.tiles, tile, DIRECTIONS[edge].bit);
if (wire.match(/^out/)) {
add_to_edge_map(circuit.inputs, tile, DIRECTIONS[edge].bit);
}
}
else if (tile.type.on_power) {
// FIXME this isn't quite right since there's seemingly a 1-frame delay
add_to_edge_map(circuit.tiles, tile, DIRECTIONS[edge].bit);
wired_outputs.add(tile);
}
}
},
); );
this.circuits.push(circuit);
// All wires are explicitly off when the level starts
// TODO what does this mean for recomputing due to circuit blocks? might we send a
// pulse despite the wires never visibly turning off??
// TODO wait what happens if you push a circuit block on a pink button...
if (first_time) {
circuit.is_powered = false;
}
// Search the circuit for tiles that act as outputs, so we can check whether to
// update them during each wire phase
for (let [tile, edges] of circuit.tiles) {
seen_edges.set((seen_edges.get(tile) ?? 0) | edges);
if (tile.type.on_power) {
wired_outputs.add(tile);
} }
} }
}
}
// Make an index of cell indices to the circuits they belong to
this.cells_to_circuits = new Map;
for (let circuit of this.circuits) {
for (let tile of circuit.tiles.keys()) {
let n = this.cell_to_scalar(tile.cell);
let set = this.cells_to_circuits.get(n);
if (! set) {
set = new Set;
this.cells_to_circuits.set(n, set);
}
set.add(circuit);
}
}
this.wired_outputs = Array.from(wired_outputs); this.wired_outputs = Array.from(wired_outputs);
this.wired_outputs.sort((a, b) => this.coords_to_scalar(b.cell.x, b.cell.y) - this.coords_to_scalar(a.cell.x, a.cell.y)); this.wired_outputs.sort((a, b) => this.coords_to_scalar(b.cell.x, b.cell.y) - this.coords_to_scalar(a.cell.x, a.cell.y));
@ -2349,13 +2314,11 @@ export class Level extends LevelInterface {
let wired_tile = actor.cell.get_wired_tile(); let wired_tile = actor.cell.get_wired_tile();
if (wired_tile && (wired_tile === actor || wired_tile.type.name === 'floor' || wired_tile.type.name === 'electrified_floor')) { if (wired_tile && (wired_tile === actor || wired_tile.type.name === 'floor' || wired_tile.type.name === 'electrified_floor')) {
emitting = wired_tile.wire_directions; emitting = wired_tile.wire_directions;
for (let circuit of wired_tile.circuits) { for (let circuit of this.cells_to_circuits.get(this.cell_to_scalar(wired_tile.cell))) {
if (circuit) {
externally_powered_circuits.add(circuit); externally_powered_circuits.add(circuit);
} }
} }
} }
}
if (emitting !== actor.emitting_edges) { if (emitting !== actor.emitting_edges) {
any_changed = true; any_changed = true;
this._set_tile_prop(actor, 'emitting_edges', emitting); this._set_tile_prop(actor, 'emitting_edges', emitting);

View File

@ -1808,14 +1808,14 @@ const TILE_TYPES = {
// Anyway, let's do a breadth-first search for teleporters. // Anyway, let's do a breadth-first search for teleporters.
let walked_circuits = new Set; let walked_circuits = new Set;
let candidate_teleporters = new Set; let candidate_teleporters = new Set;
let circuits = me.circuits; let circuits = [...level.cells_to_circuits.get(level.cell_to_scalar(me.cell))];
for (let i = 0; i < circuits.length; i++) { for (let i = 0; i < circuits.length; i++) {
let circuit = circuits[i]; let circuit = circuits[i];
if (! circuit || walked_circuits.has(circuit)) if (! circuit || walked_circuits.has(circuit))
continue; continue;
walked_circuits.add(circuit); walked_circuits.add(circuit);
for (let [tile, edges] of circuit.tiles.entries()) { for (let tile of circuit.tiles.keys()) {
if (tile.type === me.type || tile.type.name === 'teleport_blue_exit') { if (tile.type === me.type || tile.type.name === 'teleport_blue_exit') {
candidate_teleporters.add(tile); candidate_teleporters.add(tile);
} }
@ -1823,7 +1823,7 @@ const TILE_TYPES = {
// This logic gate is functioning as an output, so walk through it and also // This logic gate is functioning as an output, so walk through it and also
// trace any circuits that treat it as an input (as long as those circuits // trace any circuits that treat it as an input (as long as those circuits
// are currently powered) // are currently powered)
for (let subcircuit of tile.circuits) { for (let subcircuit of level.cells_to_circuits.get(level.cell_to_scalar(tile.cell))) {
if (subcircuit && subcircuit.is_powered && subcircuit.inputs.get(tile)) { if (subcircuit && subcircuit.is_powered && subcircuit.inputs.get(tile)) {
circuits.push(subcircuit); circuits.push(subcircuit);
} }