From 04d6b3dddba760b60e0146eef0f0208cf20dec35 Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Sun, 21 Apr 2024 00:39:23 -0600 Subject: [PATCH] Refactor circuit-tracing to be more in algorithms This should make it more usable in the editor. --- js/algorithms.js | 69 ++++++++++++++++++++++----- js/format-base.js | 4 ++ js/game.js | 117 ++++++++++++++++------------------------------ js/tiletypes.js | 6 +-- 4 files changed, 104 insertions(+), 92 deletions(-) diff --git a/js/algorithms.js b/js/algorithms.js index 3b65bac..9c8d46b 100644 --- a/js/algorithms.js +++ b/js/algorithms.js @@ -61,10 +61,33 @@ export function* find_terrain_diamond(levelish, start_cell, type_names) { 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 pending = [[start_cell, start_edge]]; let seen_cells = new Map; + let circuit = new Circuit; while (pending.length > 0) { let next = []; 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 actor = cell.get_actor(); 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; } @@ -90,10 +113,27 @@ export function trace_floor_circuit(level, start_cell, start_edge, on_wire, on_d let connections = edgeinfo.bit; let mode = tile.wire_propagation_mode ?? tile.type.wire_propagation_mode; 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 - // case we trust the caller) - if (on_dead_end) { - on_dead_end(cell, edge); + // There's not actually a wire here, so check for things that respond to receiving + // power... but if this is the starting cell, we trust the caller and skip it (XXX why) + for (let tile2 of cell) { + 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; } @@ -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); - if (on_wire) { - on_wire(tile, connections); + circuit.add_tile_edge(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)) { @@ -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 // Note that while actors (the fuckin circuit block) can be wired, tunnels ONLY // 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 { - 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; 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 [dx, dy] = dirinfo.movement; let nesting = 0; while (true) { x += dx; y += dy; - let candidate = level.cell(x, y); + let candidate = levelish.cell(x, y); if (! candidate) return null; diff --git a/js/format-base.js b/js/format-base.js index 1e3a762..f60717e 100644 --- a/js/format-base.js +++ b/js/format-base.js @@ -77,6 +77,10 @@ export class LevelInterface { 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) { return (x >= 0 && x < this.size_x && y >= 0 && y < this.size_y); } diff --git a/js/game.js b/js/game.js index 2b734f1..4b9209b 100644 --- a/js/game.js +++ b/js/game.js @@ -520,21 +520,10 @@ export class Level extends LevelInterface { // Build circuits out of connected wires // 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.power_sources = []; let wired_outputs = new Set; - this.wired_outputs = []; - let add_to_edge_map = (map, item, edges) => { - map.set(item, (map.get(item) ?? 0) | edges); - }; + let seen_edges = new Map; for (let cell of this.linear_cells) { // We're interested in static circuitry, which means terrain // OR circuit blocks on top @@ -585,73 +574,49 @@ export class Level extends LevelInterface { if (! ((wire_directions | terrain.wire_tunnel_directions) & dirinfo.bit)) continue; - if (terrain.circuits && terrain.circuits[dirinfo.index]) + if ((seen_edges.get(terrain) ?? 0) & dirinfo.bit) 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 - algorithms.trace_floor_circuit( - this, terrain.cell, direction, - // Wire handling - (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); - } - } - }, + let circuit = algorithms.trace_floor_circuit( + this, this.compat.tiles_react_instantly ? 'always' : 'still', + terrain.cell, direction, ); + 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.sort((a, b) => this.coords_to_scalar(b.cell.x, b.cell.y) - this.coords_to_scalar(a.cell.x, a.cell.y)); @@ -2349,10 +2314,8 @@ export class Level extends LevelInterface { 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')) { emitting = wired_tile.wire_directions; - for (let circuit of wired_tile.circuits) { - if (circuit) { - externally_powered_circuits.add(circuit); - } + for (let circuit of this.cells_to_circuits.get(this.cell_to_scalar(wired_tile.cell))) { + externally_powered_circuits.add(circuit); } } } diff --git a/js/tiletypes.js b/js/tiletypes.js index 70d4ef7..f7b569c 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -1808,14 +1808,14 @@ const TILE_TYPES = { // Anyway, let's do a breadth-first search for teleporters. let walked_circuits = 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++) { let circuit = circuits[i]; if (! circuit || walked_circuits.has(circuit)) continue; 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') { 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 // trace any circuits that treat it as an input (as long as those circuits // 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)) { circuits.push(subcircuit); }