Rewrite wiring code and fix basically all issues with it; faster, undoable, etc.

This commit is contained in:
Eevee (Evelyn Woods) 2020-12-18 19:58:12 -07:00
parent 48f085d0df
commit 78f59b38c1
5 changed files with 370 additions and 212 deletions

109
js/algorithms.js Normal file
View File

@ -0,0 +1,109 @@
import { DIRECTIONS, DIRECTION_ORDER } from './defs.js';
export function trace_floor_circuit(level, start_cell, start_edge, on_wire, on_dead_end) {
let is_first = true;
let pending = [[start_cell, start_edge]];
let seen_cells = new Map;
while (pending.length > 0) {
let next = [];
for (let [cell, edge] of pending) {
let terrain = cell.get_terrain();
if (! terrain)
continue;
let edgeinfo = DIRECTIONS[edge];
let seen_edges = seen_cells.get(cell) ?? 0;
if (seen_edges & edgeinfo.bit)
continue;
// The wire comes in from this edge towards the center; see how it connects within this
// cell, then check for any neighbors
let connections = edgeinfo.bit;
if (! is_first && ((terrain.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(terrain.cell, edge);
}
continue;
}
else if (terrain.type.wire_propagation_mode === 'none') {
// The wires in this tile never connect to each other
}
else if (terrain.wire_directions === 0x0f && terrain.type.wire_propagation_mode !== 'all') {
// This is a cross pattern, so only opposite edges connect
connections |= edgeinfo.opposite_bit;
}
else {
// Everything connects
connections |= terrain.wire_directions;
}
seen_cells.set(cell, seen_edges | connections);
if (on_wire) {
on_wire(terrain, connections);
}
for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) {
// Obviously don't go backwards, but that doesn't apply if this is our first pass
if (direction === edge && ! is_first)
continue;
if ((connections & dirinfo.bit) === 0)
continue;
let neighbor;
if ((terrain.wire_tunnel_directions ?? 0) & dirinfo.bit) {
// Search in this direction for a matching tunnel
neighbor = find_matching_wire_tunnel(level, cell.x, cell.y, direction);
}
else {
neighbor = level.get_neighboring_cell(cell, direction);
}
/*
if (! neighbor || (((neighbor.get_terrain().wire_directions ?? 0) & dirinfo.opposite_bit) === 0)) {
console.log("bailing here", neighbor, direction);
continue;
}
*/
if (! neighbor)
continue;
next.push([neighbor, dirinfo.opposite]);
}
}
pending = next;
is_first = false;
}
}
export function find_matching_wire_tunnel(level, 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);
if (! candidate)
return null;
let neighbor = candidate.get_terrain();
if (! neighbor)
continue;
if ((neighbor.wire_tunnel_directions ?? 0) & dirinfo.opposite_bit) {
if (nesting === 0) {
return candidate;
}
else {
nesting -= 1;
}
}
if ((neighbor.wire_tunnel_directions ?? 0) & dirinfo.bit) {
nesting += 1;
}
}
}

View File

@ -4,6 +4,7 @@ export const DIRECTIONS = {
north: {
movement: [0, -1],
bit: 0x01,
opposite_bit: 0x04,
index: 0,
action: 'up',
left: 'west',
@ -13,6 +14,7 @@ export const DIRECTIONS = {
south: {
movement: [0, 1],
bit: 0x04,
opposite_bit: 0x01,
index: 2,
action: 'down',
left: 'east',
@ -22,6 +24,7 @@ export const DIRECTIONS = {
west: {
movement: [-1, 0],
bit: 0x08,
opposite_bit: 0x02,
index: 3,
action: 'left',
left: 'south',
@ -31,6 +34,7 @@ export const DIRECTIONS = {
east: {
movement: [1, 0],
bit: 0x02,
opposite_bit: 0x08,
index: 1,
action: 'right',
left: 'north',

View File

@ -1,3 +1,4 @@
import * as algorithms from './algorithms.js';
import { DIRECTIONS, DIRECTION_ORDER, INPUT_BITS, TICS_PER_SECOND } from './defs.js';
import { LevelInterface } from './format-base.js';
import TILE_TYPES from './tiletypes.js';
@ -117,6 +118,9 @@ export class Tile {
}
}
Tile.prototype.emitting_edges = 0;
Tile.prototype.powered_edges = 0;
Tile.prototype.wire_directions = 0;
Tile.prototype.wire_tunnel_directions = 0;
export class Cell extends Array {
constructor(x, y) {
@ -294,8 +298,6 @@ export class Cell extends Array {
return direction;
}
}
Cell.prototype.prev_powered_edges = 0;
Cell.prototype.powered_edges = 0;
// The undo stack is implemented with a ring buffer, and this is its size. One entry per tic.
// Based on Chrome measurements made against the pathological level CCLP4 #40 (Periodic Lasers) and
@ -376,7 +378,6 @@ export class Level extends LevelInterface {
let n = 0;
let connectables = [];
this.power_sources = [];
this.players = [];
// FIXME handle traps correctly:
// - if an actor is in the cell, set the trap to open and unstick everything in it
@ -397,10 +398,6 @@ export class Level extends LevelInterface {
tile.hint_text = template_tile.hint_text ?? null;
}
if (tile.type.is_power_source) {
this.power_sources.push(tile);
}
if (tile.type.is_real_player) {
this.players.push(tile);
}
@ -484,6 +481,111 @@ export class Level extends LevelInterface {
}
}
// Build circuits out of connected wires
// TODO document this idea
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);
};
for (let cell of this.linear_cells) {
// We're interested in static circuitry, which means terrain
let terrain = cell.get_terrain();
if (! terrain) // ?!
continue;
if (terrain.type.is_power_source) {
this.power_sources.push(terrain);
}
let wire_directions = terrain.wire_directions;
if (! wire_directions && ! terrain.wire_tunnel_directions) {
// No wires, not interesting... unless it's a logic gate, which defines its own
// wires! We only care about outgoing ones here, on the off chance that they point
// directly into a non-wired tile, in which case a wire scan won't find them
if (terrain.type.name === 'logic_gate') {
let dir = terrain.direction;
let cxns = terrain.type._gate_types[terrain.gate_type];
for (let i = 0; i < 4; i++) {
let cxn = cxns[i];
if (cxn && cxn.match(/^out/)) {
wire_directions |= DIRECTIONS[dir].bit;
}
dir = DIRECTIONS[dir].right;
}
}
else {
continue;
}
}
for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) {
if (! ((wire_directions | terrain.wire_tunnel_directions) & dirinfo.bit))
continue;
if (terrain.circuits && terrain.circuits[dirinfo.index])
continue;
let circuit = {
is_powered: false,
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.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.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) {
add_to_edge_map(circuit.tiles, tile, DIRECTIONS[edge].bit);
wired_outputs.add(tile);
}
}
},
);
}
}
this.wired_outputs = Array.from(wired_outputs);
this.wired_outputs.sort((a, b) => this.coords_to_scalar(a.cell.x, a.cell.y) - this.coords_to_scalar(b.cell.x, b.cell.y));
// Finally, let all tiles do any custom init behavior
for (let cell of this.linear_cells) {
for (let tile of cell) {
@ -569,9 +671,8 @@ export class Level extends LevelInterface {
this.p1_released |= ~p1_input; // Action keys released since we last checked them
this.swap_player1 = false;
// Used for various tic-local effects; don't need to be undoable
// TODO maybe this should be undone anyway so rewind looks better?
this.player.is_blocked = false;
// This effect only lasts one tic, after which we can move again
this._set_tile_prop(this.player, 'is_blocked', false);
this.sfx.set_player_position(this.player.cell);
@ -720,7 +821,7 @@ export class Level extends LevelInterface {
// Track whether the player is blocked, for visual effect
if (actor === this.player && actor.decision && ! success) {
this.sfx.play_once('blocked');
actor.is_blocked = true;
this._set_tile_prop(actor, 'is_blocked', true);
}
}
@ -1397,178 +1498,130 @@ export class Level extends LevelInterface {
}
}
// Update the state of all wired tiles in the game.
// XXX need to be clear on the order of events here. say everything starts out unpowered.
// then:
// 1. you step on a pink button, which flags itself as going to be powered next frame
// 2. this pass happens. every unpowered-but-wired cell is inspected. if a powered one is
// found, floodfill from there
// FIXME can probably skip this if we know there are no wires at all, like in a CCL, or just an
// unwired map
// FIXME this feels inefficient. most of the time none of the inputs have changed so none of
// this needs to happen at all
// FIXME none of this is currently undoable
update_wiring() {
// FIXME:
// - make this undoable :(
// - blue tele, red tele, and pink button have different connections
// - would like to reuse the walk for blue teles
if (this.circuits.length === 0)
return;
// Gather every tile that's emitting power. Along the way, check whether any of them have
// changed since last tic, so we can skip this work entirely if none did
let neighbors = [];
// Prepare a big slab of undo. The only thing we directly change here (aside from
// emitting_edges, a normal tile property) is Tile.powered_edges, which tends to change for
// large numbers of tiles at a time, so store it all in one map and undo it in one shot.
let powered_edges_changes = new Map;
let _set_edges = (tile, new_edges) => {
if (powered_edges_changes.has(tile)) {
if (powered_edges_changes.get(tile) === new_edges) {
powered_edges_changes.delete(tile);
}
}
else {
powered_edges_changes.set(tile, tile.powered_edges);
}
tile.powered_edges = new_edges;
};
let power_edges = (tile, edges) => {
let new_edges = tile.powered_edges | edges;
_set_edges(tile, new_edges);
};
let depower_edges = (tile, edges) => {
let new_edges = tile.powered_edges & ~edges;
_set_edges(tile, new_edges);
};
// Update the state of any tiles that can generate power. If none of them changed since
// last wiring update, stop here. First, static power sources.
let any_changed = false;
for (let tile of this.power_sources) {
if (! tile.cell)
continue;
let emitting = tile.type.get_emitting_edges(tile, this);
if (emitting) {
neighbors.push([tile.cell, emitting]);
}
if (emitting !== tile.emitting_edges) {
any_changed = true;
tile.emitting_edges = emitting;
this._set_tile_prop(tile, 'emitting_edges', emitting);
}
}
// Also check actors, since any of them might be holding a lightning bolt (argh)
// Next, actors who are standing still, on floor, and holding a lightning bolt
let externally_powered_circuits = new Set;
for (let actor of this.actors) {
if (! actor.cell)
continue;
// Only count when they're on a floor tile AND not in transit!
let emitting = null;
let emitting = 0;
if (actor.movement_cooldown === 0 && actor.has_item('lightning_bolt')) {
let wired_tile = actor.cell.get_wired_tile();
if (wired_tile && (wired_tile === actor || wired_tile.type.name === 'floor')) {
emitting = wired_tile.wire_directions;
neighbors.push([actor.cell, wired_tile.wire_directions]);
for (let circuit of wired_tile.circuits) {
if (circuit) {
externally_powered_circuits.add(circuit);
}
}
}
}
if (emitting !== actor.emitting_edges) {
any_changed = true;
actor.emitting_edges = emitting;
this._set_tile_prop(actor, 'emitting_edges', emitting);
}
}
// If none changed, we're done
if (! any_changed)
return;
// Turn off power to every cell
for (let cell of this.linear_cells) {
cell.prev_powered_edges = cell.powered_edges;
cell.powered_edges = 0;
for (let tile of this.wired_outputs) {
// This is only used within this function, no need to undo
// TODO if this can overlap with power_sources then this is too late?
tile._prev_powered_edges = tile.powered_edges;
}
// Iterate over emitters and flood-fill outwards one edge at a time
// propagated it via flood-fill through neighboring wires
while (neighbors.length > 0) {
let [cell, source_direction] = neighbors.shift();
let wire = cell.get_wired_tile();
// Now go through every circuit, compute whether it's powered, and if that changed, inform
// its outputs
let circuit_changes = new Map;
for (let circuit of this.circuits) {
let is_powered = false;
// Power this cell
if (typeof(source_direction) === 'number') {
// This cell is emitting power itself, and the source direction is actually a
// bitmask of directions
cell.powered_edges = source_direction;
if (externally_powered_circuits.has(circuit)) {
is_powered = true;
}
else {
let bit = DIRECTIONS[source_direction].bit;
if (wire === null || (wire.wire_directions & bit) === 0) {
// No wire on this side, so the power doesn't actually propagate, but it DOES
// stay on this edge (so if this is e.g. a purple tile, it'll be powered)
cell.powered_edges |= bit;
continue;
}
// Common case: power entering a wired edge and propagating outwards. There are a
// couple special cases:
if (wire.type.wire_propagation_mode === 'none') {
// This tile type has wires, but none of them connect to each other
cell.powered_edges |= bit;
continue;
}
else if (wire.wire_directions === 0x0f && wire.type.wire_propagation_mode !== 'all') {
// If all four wires are present, they don't actually make a four-way
// connection, but two straight wires that don't connect to each other (with the
// exception of blue teleporters)
cell.powered_edges |= bit;
cell.powered_edges |= DIRECTIONS[DIRECTIONS[source_direction].opposite].bit;
}
else {
cell.powered_edges = wire.wire_directions;
for (let [input_tile, edges] of circuit.inputs.entries()) {
if (input_tile.emitting_edges & edges) {
is_powered = true;
break;
}
}
}
// Propagate current to neighbors
for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) {
if (direction === source_direction)
continue;
if ((cell.powered_edges & dirinfo.bit) === 0)
continue;
let was_powered = circuit.is_powered;
if (is_powered === was_powered)
continue;
let neighbor, neighbor_wire;
let opposite_bit = DIRECTIONS[dirinfo.opposite].bit;
if (wire && (wire.wire_tunnel_directions & dirinfo.bit)) {
// Search in the given direction until we find a matching tunnel
let x = cell.x;
let y = cell.y;
let nesting = 0;
while (true) {
x += dirinfo.movement[0];
y += dirinfo.movement[1];
let candidate = this.cell(x, y);
if (! candidate)
break;
neighbor_wire = candidate.get_wired_tile();
if (neighbor_wire) {
if ((neighbor_wire.wire_tunnel_directions ?? 0) & opposite_bit) {
if (nesting === 0) {
neighbor = candidate;
break;
}
else {
nesting -= 1;
}
}
if ((neighbor_wire.wire_tunnel_directions ?? 0) & dirinfo.bit) {
nesting += 1;
}
}
}
circuit.is_powered = is_powered;
circuit_changes.set(circuit, was_powered);
for (let [tile, edges] of circuit.tiles.entries()) {
if (is_powered) {
power_edges(tile, edges);
}
else {
// No tunnel; this is easy
neighbor = this.get_neighboring_cell(cell, direction);
if (neighbor) {
neighbor_wire = neighbor.get_wired_tile();
}
}
if (neighbor && (neighbor.powered_edges & opposite_bit) === 0 &&
// Unwired tiles are OK; they might be something activated by power.
// Wired tiles that do NOT connect to us are ignored.
(! neighbor_wire || neighbor_wire.wire_directions & opposite_bit))
{
neighbors.push([neighbor, dirinfo.opposite]);
depower_edges(tile, edges);
}
}
}
// Inform any affected cells of power changes
for (let cell of this.linear_cells) {
if ((cell.prev_powered_edges === 0) !== (cell.powered_edges === 0)) {
let method = cell.powered_edges ? 'on_power' : 'on_depower';
for (let tile of cell) {
if (tile.type[method]) {
tile.type[method](tile, this);
}
}
for (let tile of this.wired_outputs) {
if (tile.powered_edges && ! tile._prev_powered_edges && tile.type.on_power) {
tile.type.on_power(tile, this);
}
else if (! tile.powered_edges && tile._prev_powered_edges && tile.type.on_depower) {
tile.type.on_depower(tile, this);
}
}
}
// Performs a depth-first search for connected wires and wire objects, extending out from the
// given starting cell
*follow_circuit(cell) {
this.pending_undo.push(() => {
for (let [tile, edges] of powered_edges_changes.entries()) {
tile.powered_edges = edges;
}
for (let [circuit, is_powered] of circuit_changes.entries()) {
circuit.is_powered = is_powered;
}
});
}
// -------------------------------------------------------------------------
@ -1630,9 +1683,14 @@ export class Level extends LevelInterface {
}
}
is_cell_wired(cell) {
for (let direction of Object.keys(DIRECTIONS)) {
let neighbor = this.get_neighboring_cell(cell, direction);
// FIXME require_stub should really just care whether we ourselves /can/ contain wire, and also
// we should check that on our neighbor
is_tile_wired(tile, require_stub = true) {
for (let [direction, dirinfo] of Object.entries(DIRECTIONS)) {
if (require_stub && (tile.wire_directions & dirinfo.bit) === 0)
continue;
let neighbor = this.get_neighboring_cell(tile.cell, direction);
if (! neighbor)
continue;
@ -1640,7 +1698,11 @@ export class Level extends LevelInterface {
if (! wired)
continue;
if (wired.wire_directions & DIRECTIONS[DIRECTIONS[direction].opposite].bit)
if (wired.type.wire_propagation_mode === 'none' && ! wired.type.is_power_source)
// Being next to e.g. a red teleporter doesn't count (but pink button is ok)
continue;
if (wired.wire_directions & dirinfo.opposite_bit)
return true;
}
return false;

View File

@ -985,18 +985,18 @@ export class Tileset {
this._draw_fourway_tile_power(tile, 0x0f, blit);
}
else {
if (tile.cell.powered_edges & DIRECTIONS[tile.direction].bit) {
if (tile.powered_edges & DIRECTIONS[tile.direction].bit) {
// Output (on top)
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0.5 - r, 0, 0.5 + r, 0.5);
blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
}
if (tile.cell.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].right].bit) {
if (tile.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].right].bit) {
// Right input, which includes the middle
// This actually covers the entire lower right corner, for bent inputs.
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0.5 - r, 0.5 - r, 1, 1);
blit(powered_coords[0], powered_coords[1], x0, y0, x1 - x0, y1 - y0);
}
if (tile.cell.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].left].bit) {
if (tile.powered_edges & DIRECTIONS[DIRECTIONS[tile.direction].left].bit) {
// Left input, which does not include the middle
// This actually covers the entire lower left corner, for bent inputs.
let [x0, y0, x1, y1] = this._rotate(tile.direction, 0, 0.5 - r, 0.5 - r, 1);
@ -1017,7 +1017,7 @@ export class Tileset {
_draw_fourway_tile_power(tile, wires, blit) {
// Draw the unpowered tile underneath, if any edge is unpowered (and in fact if /none/ of it
// is powered then we're done here)
let powered = (tile.cell ? tile.cell.powered_edges : 0) & wires;
let powered = (tile.cell ? tile.powered_edges : 0) & wires;
if (! tile.cell || powered !== tile.wire_directions) {
this._draw_fourway_power_underlay(this.layout['#unpowered'], wires, blit);
if (! tile.cell || powered === 0)

View File

@ -466,7 +466,7 @@ const TILE_TYPES = {
level._set_tile_prop(me, 'entered_direction', other.direction);
},
on_depart(me, level, other) {
if (! level.is_cell_wired(me.cell)) {
if (! level.is_tile_wired(me, false)) {
me.type._switch_track(me, level);
}
},
@ -1104,6 +1104,8 @@ const TILE_TYPES = {
},
transmogrifier: {
draw_layer: DRAW_LAYERS.terrain,
// C2M technically supports wires in transmogrifiers, but they don't do anything
wire_propagation_mode: 'none',
_mogrifications: {
player: 'player2',
player2: 'player',
@ -1128,15 +1130,12 @@ const TILE_TYPES = {
teeth_timid: 'teeth',
},
_blob_mogrifications: ['ball', 'walker', 'fireball', 'glider', 'paramecium', 'bug', 'tank_blue', 'teeth', 'teeth_timid'],
on_ready(me, level) {
me.is_powered = false;
},
on_arrive(me, level, other) {
// Note: Transmogrifiers technically contain wires the way teleports do, and CC2 uses
// the presence and poweredness of those wires to determine whether the transmogrifier
// should appear to be on or off, but the /functionality/ is controlled entirely by
// whether an adjoining cell carries current to our edge, like a railroad or cloner
if (level.is_cell_wired(me.cell) && ! me.is_powered)
if (level.is_tile_wired(me, false) && ! me.powered_edges)
return;
let name = other.type.name;
if (me.type._mogrifications[name]) {
@ -1149,30 +1148,33 @@ const TILE_TYPES = {
}
},
on_power(me, level) {
level._set_tile_prop(me, 'is_powered', true);
},
on_depower(me, level) {
level._set_tile_prop(me, 'is_powered', false);
// No need to do anything, we just need this here as a signal that our .powered_edges
// needs to be updated
},
},
// FIXME blue teleporters transmit current 4 ways. red don't transmit it at all
teleport_blue: {
draw_layer: DRAW_LAYERS.terrain,
wire_propagation_mode: 'all',
*teleport_dest_order(me, level, other) {
let exit_direction = other.direction;
// Note that unlike other tiles that care about whether they're wired, a blue teleporter
// considers itself part of a network if it contains any wires at all, regardless of
// whether they connect to anything
if (! me.wire_directions) {
// TODO cc2 has a bug where, once it wraps around to the bottom right, it seems to
// forget that it was ever looking for an unwired teleport and will just grab the
// first one it sees
for (let dest of level.iter_tiles_in_reading_order(me.cell, 'teleport_blue', true)) {
yield [dest, exit_direction];
if (! dest.wire_directions) {
yield [dest, exit_direction];
}
}
return;
}
// Wired blue teleports form a network, which means we have to walk all wires from this
// point, collect a list of all possible blue teleports, and then sort them so we can
// try them in the right order.
// Wired blue teleports form an isolated network, so we have to walk the circuit we're
// on, collect a list of all possible blue teleports, and then sort them so we can try
// them in the right order.
// Complicating this somewhat, logic gates act as diodes: we can walk through a logic
// gate if we're connected to one of its inputs AND its output is enabled, but we can't
// walk "backwards" through it.
@ -1185,66 +1187,45 @@ const TILE_TYPES = {
// behavior is not and will never be emulated. No level in CC2 or even CC2LP1 uses blue
// teleporters wired into logic gates, so even the ordering is not interesting imo.)
// Anyway, let's do a breadth-first search for teleporters.
let seeds = [me.cell];
let found = [];
let seen = new Set;
while (seeds.length > 0) {
let next_seeds = [];
for (let cell of seeds) {
if (seen.has(cell))
continue;
seen.add(cell);
let walked_circuits = new Set;
let candidate_teleporters = new Set;
let circuits = me.circuits;
for (let i = 0; i < circuits.length; i++) {
let circuit = circuits[i];
if (! circuit || walked_circuits.has(circuit))
continue;
walked_circuits.add(circuit);
let wired = cell.get_wired_tile();
if (! wired || wired.wire_directions === 0)
continue;
// Check for a blue teleporter
let dest;
for (let tile of cell) {
if (tile.type.name === 'teleport_blue') {
found.push(tile);
break;
for (let [tile, edges] of circuit.tiles.entries()) {
if (tile.type === me.type) {
candidate_teleporters.add(tile);
}
else if (tile.type.name === 'logic_gate' && ! circuit.inputs.get(tile)) {
// 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) {
if (subcircuit && subcircuit.is_powered && subcircuit.inputs.get(tile)) {
circuits.push(subcircuit);
}
}
}
// Search our neighbors
for (let direction of Object.keys(DIRECTIONS)) {
if (! (wired.wire_directions & DIRECTIONS[direction].bit))
continue;
let neighbor = level.get_neighboring_cell(cell, direction);
if (! neighbor || seen.has(neighbor))
continue;
let neighbor_wired = neighbor.get_wired_tile();
if (! neighbor_wired)
continue;
if (! (neighbor_wired.wire_directions & DIRECTIONS[DIRECTIONS[direction].opposite].bit))
continue;
// TODO check for logic gate
// TODO need to know the direction we came in so we can get the right ones
// going out!
// FIXME this needs to understand crossings, and both this and basic wiring
// need to understand how blue/red teleports convey current
next_seeds.push(neighbor);
}
}
seeds = next_seeds;
}
// Now that we have a list of candidate exits, sort it in reverse reading order,
// Now that we have a set of candidate destinations, sort it in reverse reading order,
// starting from ourselves. Easiest way to do this is to make a map of cell indices,
// shifted so that we're at zero, then sort in reverse
let dest_indices = new Map;
let our_index = me.cell.x + me.cell.y * level.size_x;
let level_size = level.size_x * level.size_y;
for (let dest of found) {
for (let dest of candidate_teleporters) {
dest_indices.set(dest, (
(dest.cell.x + dest.cell.y * level.size_x)
- our_index + level_size
) % level_size);
}
let found = Array.from(candidate_teleporters);
found.sort((a, b) => dest_indices.get(b) - dest_indices.get(a));
for (let dest of found) {
yield [dest, exit_direction];
@ -1255,8 +1236,11 @@ const TILE_TYPES = {
draw_layer: DRAW_LAYERS.terrain,
wire_propagation_mode: 'none',
teleport_allow_override: true,
_is_active(me) {
return ! (me.wire_directions && (me.cell.powered_edges & me.wire_directions) === 0);
_is_active(me, level) {
// FIXME must be connected to something that can convey current: a wire, a switch, a
// blue teleporter, etc; NOT nothing, a wall, a transmogrifier, a force floor, etc.
// this is also how blue teleporters, transmogrifiers, and railroads work!
return me.powered_edges || ! level.is_tile_wired(me);
},
*teleport_dest_order(me, level, other) {
// Wired red teleporters can be turned off, which disconnects them from every other red
@ -1264,9 +1248,8 @@ const TILE_TYPES = {
// A red teleporter is considered wired only if it has wires itself. However, CC2 also
// has the bizarre behavior of NOT considering a red teleporter wired if none of its
// wires are directly connected to another neighboring wire.
// FIXME implement that, merge current code with is_cell_wired
let iterable;
if (this._is_active(me)) {
if (this._is_active(me, level)) {
iterable = level.iter_tiles_in_reading_order(me.cell, 'teleport_red');
}
else {
@ -1274,7 +1257,7 @@ const TILE_TYPES = {
}
let exit_direction = other.direction;
for (let tile of iterable) {
if (tile === me || this._is_active(tile)) {
if (tile === me || this._is_active(tile, level)) {
// Red teleporters allow exiting in any direction, searching clockwise
yield [tile, exit_direction];
yield [tile, DIRECTIONS[exit_direction].right];
@ -1283,7 +1266,7 @@ const TILE_TYPES = {
}
}
},
// TODO inactive ones don't animate
// TODO inactive ones don't animate; transmogrifiers too
/*
visual_state(me) {
return this._is_active(me) ? 'active' : 'inactive';
@ -1615,10 +1598,10 @@ const TILE_TYPES = {
let cxn = me.gate_def[i];
let dirinfo = DIRECTIONS[dir];
if (cxn === 'in0') {
input0 = (me.cell.powered_edges & dirinfo.bit) !== 0;
input0 = (me.powered_edges & dirinfo.bit) !== 0;
}
else if (cxn === 'in1') {
input1 = (me.cell.powered_edges & dirinfo.bit) !== 0;
input1 = (me.powered_edges & dirinfo.bit) !== 0;
}
else if (cxn === 'out0') {
outbit0 = dirinfo.bit;