#!/usr/bin/env perl
# Embedding of selected Klotski puzzles in Escape.
# https://mattmccutchen.net/escape-klotski/
#
# Usage:
#
# escape-klotski-generate-level.pl NAME
#
# where NAME is red-donkey or trail .  Generates klotski-NAME-uncompressed.esx
# in the current directory.

use strict;
use warnings;
use List::Util qw(max min);

### The Klotski puzzle and Klotski-related definitions

# Note: Some parts of the code have hard-coded dependencies on the width,
# height, number of offsets, and number of piece types, so you cannot expect to
# change these and generate a working puzzle without further modifications.
# Other parts of the code are written in terms of these parameters for clarity.

my @directions = (
  {dx => -1, dy => 0},
  {dx => 1, dy => 0},
  {dx => 0, dy => -1},
  {dx => 0, dy => 1},
);

# It happens that everything from here to `my %puzzles` is common to all
# supported puzzles.
my $kwidth = 4, my $kheight = 5;
my $kwh = $kwidth * $kheight;

# For efficient implementation, we group the pieces in the Klotski puzzles into
# "piece types": two pieces of the same shape are of the same type, except that
# the goal piece always has its own type.  A piece is made of an "anchor" cell
# (the first occupied cell in row-major order, i.e., the leftmost occupied cell
# in the topmost occupied row) plus zero or more cells at certain
# two-dimensional offsets from the anchor cell.  This representation lets us
# build circuitry for each offset and reuse it for all piece types that include
# the offset.

my @k_offsets = (
  {dx => 1, dy => 0},
  {dx => 0, dy => 1},
  {dx => 1, dy => 1}
);
my $k_num_offsets = scalar(@k_offsets);

# $k_piece_types[TYPE]->[OFFSET] answers whether the type contains the offset.
my @k_piece_types = (
  [0, 0, 0],
  [1, 0, 0],
  [0, 1, 0],
  [1, 1, 1]
);
my $k_num_piece_types = scalar(@k_piece_types);

my %puzzles = (
  'red-donkey' => {
    title => 'Red Donkey',
    initial_pieces => [
      {type => 3, anchor => {x => 1, y => 0}},
      {type => 2, anchor => {x => 0, y => 0}},
      {type => 2, anchor => {x => 3, y => 0}},
      {type => 2, anchor => {x => 0, y => 2}},
      {type => 2, anchor => {x => 3, y => 2}},
      {type => 1, anchor => {x => 1, y => 2}},
      {type => 0, anchor => {x => 1, y => 3}},
      {type => 0, anchor => {x => 2, y => 3}},
      {type => 0, anchor => {x => 0, y => 4}},
      {type => 0, anchor => {x => 3, y => 4}},
    ],
    goal => {type => 3, anchor => {x => 1, y => 3}}
  },
  'trail' => {
    title => 'Trail',
    initial_pieces => [
      {type => 3, anchor => {x => 1, y => 0}},
      {type => 2, anchor => {x => 0, y => 0}},
      {type => 2, anchor => {x => 3, y => 0}},
      {type => 0, anchor => {x => 0, y => 2}},
      {type => 0, anchor => {x => 0, y => 3}},
      {type => 0, anchor => {x => 3, y => 2}},
      {type => 0, anchor => {x => 3, y => 3}},
      {type => 1, anchor => {x => 1, y => 2}},
      {type => 1, anchor => {x => 1, y => 3}},
      {type => 1, anchor => {x => 1, y => 4}},
    ],
    goal => {type => 3, anchor => {x => 1, y => 3}}
  }
);

my $puzzle_id = $ARGV[0];
my $puzzle = $puzzles{$puzzle_id};
die "Not a known puzzle: $puzzle_id" unless defined($puzzle);

my $k_title = $puzzle->{title};
my @k_initial_pieces = @{$puzzle->{initial_pieces}};
my $k_goal = $puzzle->{goal};

sub kxy_valid($$) {
  my ($kx, $ky) = @_;
  return ($kx >= 0 && $kx < $kwidth && $ky >= 0 && $ky < $kheight);
}

# We number Klotski cells in row-major order.
sub kcons($$) {
  my ($kx, $ky) = @_;
  return $kwidth * $ky + $kx;
}

### Escape definitions

use constant {
  T_FLOOR => 0,
  T_EXIT => 5,
  T_RIGHT => 11,
  T_LEFT => 12,
  T_UP => 13,
  T_DOWN => 14,
  T_ROUGH => 15,
  T_NS => 25,
  T_NE => 26,
  T_NW => 27,
  T_SE => 28,
  T_SW => 29,
  T_WE => 30,
  T_BUTTON => 31,
  T_BLIGHT => 32,
  T_RLIGHT => 33,
  T_BLACK => 35,
  T_BUP => 36,
  T_BDOWN => 37,
  T_RUP => 38,
  T_RDOWN => 39,
  T_GDOWN => 41,
  # Note: When creating a panel, set flags so Escape doesn't show the level as
  # "corrupted".  Escape is capable of fixing the flags for us, but if we start
  # ignoring the "corrupted" warning, we might miss other real problems.
  T_BPANEL => 48,
  T_BSTEEL => 52,
  T_SLEEPINGDOOR => 56,
  T_NSWE => 58,
  T_REMOTE => 59,

  TF_HASPANEL => 1,
  # blue = L, green = H, red = L|H
  TF_RPANELL => 4,
  TF_RPANELH => 8,

  B_HUGBOT => 2,
};

sub xy($$) {
  my ($x, $y) = @_;
  return { x => $x, y => $y };
}

sub black_tile() {
  return {
    type => T_BLACK,
    alt_type => T_BLACK,
    dest => xy(0, 0),
    flags => 0
  };
  #; # help gedit
}

### Basic setup of Escape level

my $width = 45, my $height = 45;

# Tiles are 2D array by y, x .
# Tile structure:
# {
#   type => ...
#   alt_type => ...
#   dest => { x => ..., y => ... }
#   flags => ???
# }
#
# Bot structure:
# {
#   type => ...
#   loc => { x => ..., y => ... }
# }

my $level = {
  width => $width,
  height => $height,
  title => "Klotski: $k_title",
  author => 'Matt McCutchen (based on Klotski puzzle of uncertain origin)',
  # Will be within "user control" generated below.
  guyx => 1,
  guyy => 1,
  tiles => [ map { [ map { black_tile(); } (0..($width - 1)) ] } (0..($height - 1)) ],
  bots => []
};

# "tile lookup", very short abbreviation since it's used so many times
#
# I'm stridently opposed to MATLAB-style overloading in most situations, but
# here's one where I think the gain in conciseness far outweighs the minimal
# loss in clarity.
sub tl($;$) {
  my ($fst, $snd) = @_;
  my ($x, $y);
  if (ref($fst) eq 'HASH' && !defined($snd)) {
    if (defined($fst->{type})) {
      # Looks like the input was already a tile.
      return $fst;
    } else {
      $x = $fst->{x};
      $y = $fst->{y};
    }
  } elsif (ref($fst) eq '' && ref($snd) eq '') {
    $x = $fst;
    $y = $snd;
  } else {
    die 'Invalid arguments to tl';
  }
  return $level->{tiles}->[$y]->[$x];
}

sub set_tile($$;$) {
  my ($t, $type, $alt_type) = @_;
  $t = tl($t);
  $t->{type} = $type;
  $t->{alt_type} = $alt_type // T_BLACK;
}

sub flip_tile($) {
  my ($t) = @_;
  $t = tl($t);
  ($t->{type}, $t->{alt_type}) = ($t->{alt_type}, $t->{type});
  # XXX A general implementation would need to adjust $t->{flags} accordingly,
  # but we don't call flip_tile on any tile with flags.
}

sub make_remote($$;$) {
  my ($t, $dest, $other_type) = @_;
  $t = tl($t);
  $t->{dest} = $dest;
  $t->{type} = T_REMOTE;
  $t->{alt_type} = $other_type // T_BLACK;
}

sub make_alt_remote($$;$) {
  my ($t) = @_;
  &make_remote(@_);
  flip_tile($t);
}

sub add_bot_right($) {
  my ($button_xy) = @_;
  push(@{$level->{bots}}, {
    type => B_HUGBOT,
    loc => {x => $button_xy->{x} + 1, y => $button_xy->{y}}
  });
}

sub add_bot_below($) {
  my ($button_xy) = @_;
  push(@{$level->{bots}}, {
    type => B_HUGBOT,
    loc => {x => $button_xy->{x}, y => $button_xy->{y} + 1}
  });
}

# Layout of major components of the level:
#
# (One character corresponds to one Escape tile.)
#
# +------------+ +----------------------------+
# |            | |              Ty            |
# |            | ++                           |
# |            |  +---------------------------+
# |     UI     |       +----------------------+
# |            |       |          DS          |
# |            |       |                      |
# |            |       +----------------------+
# +------------+           +------------------+
#                          |                  |
# +------------------+     |                  |
# |                  |     |   A (cont'd.)    |
# |        DF        |     |                  |
# |                  |     |                  |
# +------------------+    ++------------------|
# +------------------+    +------------------+|
# |                  |    |                  ||
# |                  |    |                  ||
# |                  |    |       PSU        ||
# |                  |    |                  ||
# |                  |    |                  ||
# |        PS        |    |                  ||
# |                  |    +------------------+|
# |                  |    +-------------------+
# |                  |    |                   |
# |                  |    |                   |
# |                  |    |        A          |
# |                  |    |                   |
# +------------------+    |                   |
# +------------------+    +-------------------+
# |                  |    +-------------------+
# |                  |    |                   |
# |                  |    |        DC         |
# |        OC        |    |                   |
# |                  |    +-------------------+
# |                  |   +--------------------+
# |                  |   |                    |
# |                  |   |                    |
# +------------------+   |                    |
# +--------AS--------+   |         S          |
# +------------------+   |                    |
# +------------------+   |                    |
# |        T         |   |                    |
# |                  |   |                    |
# +------------------+   +--------------------+

# Before I explain the function of these components, it helps to explain an
# idiom that I've used several times.  When an action (typically, flipping a
# tile somewhere) needs to be taken depending on two variables (call them "a"
# and "b"), I'll use one component "F" that has the 2D grid of remotes
# corresponding to all (a, b) pairs, all initially on the alternate layer.  The
# main layer has wires (for the sake of this discussion, suppose they are
# vertical) corresponding to the possible values of "b".  An auxiliary component
# "S" has one row for each value of "a", containing remotes that flip the tiles
# corresponding to "a" in the main component.  We give "S" a pulse representing
# the value of "a" and then give "F" a pulse representing the value of "b",
# which will go down a wire on which the sole active remote is the one flipped
# by "S", taking the desired action.  Now we give "S" another pulse representing
# "a" to clean up.

# Major components and their functions:
# - UI: User interface.
# - PS: "Primary state".  For each cell, stores the type of the piece anchored
#   there, if any.  When the user requests a move on the cell, the cell is
#   selected as "current" in PS, PSU, OC, and DF and the piece type is
#   transmitted to Ty.  If the cell belongs to a piece anchored elsewhere, PS
#   has a remote that selects the anchor cell as "current".
# - Ty: "Type".  Activates remotes in S and PSU based on the type of the piece
#   currently being moved.  (A second invocation deactivates the remotes to
#   clean up.  Many components are used this way.)
# - DS: "Displacement selector".  Activates remotes in DF based on the direction
#   pressed by the user.
# - DF: "Displacement firer".  Receives the current anchor cell, computes the
#   displaced anchor cell to which the user requests to move the piece, and
#   transmits it to DC.
# - DC: "Displacement copier".  Transmits the displaced anchor cell to PC and
#   OC.
# - S: "Shape".  Activates remotes in OC and PSU based on the offsets belonging
#   to the current piece type.
# - OC: "Occupancy computer".  Given either the original or the displaced
#   anchor cell, computes all cells occupied by the piece and activates the
#   corresponding folds in A.  If one or more cells of the displaced piece are
#   off the board, flips one or more tiles directly in T to ensure that T fails.
# - A: Multipurpose component that handles all operations that need to be
#   performed on multiple cells in parallel, since if we used one pulse per
#   cell, we would quickly run out of bots.  (Named "A" because it was the first
#   such component, but I ended up needing no others.)  It has a backbone with a
#   set of folds, one per cell; a pulse along the backbone will go through the
#   active folds and bypass the others.  /Every/ pulse of A toggles the "test"
#   tiles in T corresponding to the active cells.  By default, every second
#   pulse of A triggers PSU and additionally adds or removes the current piece
#   in the "total" tiles of T and the UI; this only works properly if A is
#   pulsed twice in a row with the same active folds (as it always is).  AS can
#   temporarily disable this behavior.
# - PSU: "Primary state updater".  Adds or removes a piece of the current type
#   in PS at the current anchor (before or after accepting a valid move).  Also,
#   based on the offsets of the current piece type, adds or removes the remotes
#   in PS for the non-anchor cells to point to the anchor cell.
# - T: "Tester".  Tests whether the requested new location of a piece collides
#   with another piece or the edge of the board.  For each Klotski cell, T has a
#   "total" tile that indicates whether the cell is occupied by a piece not
#   involved in the current move, and two "test" tiles that indicate whether
#   the cell belongs to the moving piece in its new location.  If the "test"
#   tiles are set, the pulse has to pass through the "total" tile (which is
#   missing if the cell is taken), otherwise the pulse passes through a tile
#   that is always present.  A pulse will pass all the way through T if and only
#   if the requested location is legal.
# - AS: "A switcher".  Toggles on or off part of the functionality of A as
#   described above.
#
# To simplify the control logic, some components are permanently chained
# together because they are only ever invoked together:
# A -> PSU (on every second invocation when enabled)
# S -> DC

# Some components have multiple entry points:
# PS:
# - PS1: Triggers remotes corresponding to the cell selected by the user via
#   the steel block.
# - PSC: Triggers remotes corresponding to the "current" anchor cell.
# - PSD: Triggers remotes corresponding to the "displaced" anchor cell computed
#   by DF.
# OC:
# - OCC: Toggles folds of A based on the "current" anchor cell.
# - OCD: Toggles folds of A based on the "displaced" anchor cell.

# Sequence of bots on each move:
#
# 1. PS1, DS
# 2. PS1, PSC
#    This is a dirty trick.  If the user-selected cell is an anchor cell, then
#    we'll fire into the same column of PS from both ends, triggering all the
#    remotes twice with no net effect.  If the user-selected cell is an occupied
#    /non-anchor/ cell, then both the user-selected cell and the anchor cell
#    will be selected in PSC, but the anchor cell will catch the pulse first
#    because it's earlier in row-major order and earlier cells are closer to the
#    entry point; thus, the PSC pulse will select the anchor cell and the PS1
#    pulse will unselect the user-selected cell.  In any case, after this step,
#    the selected cell is either unoccupied or an anchor.
# 3. Ty, DF
# 4. S+DC, F1, F2
#    F1 and F2 are minor "components" that simply toggle all blue and red tiles
#    if the current piece is the goal piece.  This way, the current piece always
#    has no bumps while we are manipulating it and we can maintain the invariant
#    that all blue and red tiles in the alternate layer are un-bumped.
# If we found a piece:
#     5. OCC
#     6. A
#     7. A+PSU, OCC, OCD, AS
#        Remove the piece from PS, T total, and UI.
#     8. A
#        Now the new location of the piece is loaded into T test.
#     9. T
#     If T passed:
#         10. PSC, PSD
#     11. A (/no/ PSU because of AS), OCC, OCD, AS
#     12. A
#     13. A+PSU, OCC
#         Add the piece back to PS, T total, and UI, whether or not it moved.
# 14. S+DC, F1, F2
#     (Even if we didn't find a piece, we have to call DC twice to clean up
#     from DF.)
# 15. Ty, PSC, DS

# Hopefully that gives you enough of the high-level structure and terminology to
# understand the nitty-gritty below.  One last detail: what opens the exit?  The
# column of PSU that corresponds to the anchor of the goal zone has an extra
# remote pointing to the exit, but the connection to that remote is present only
# when the selected type is the goal type.  Thus, the exit will open when you
# move the goal piece to the goal zone (and close if for some reason you decide
# to move it out of the goal zone).

### Component position definitions
# So that generation of one component can create remotes to the correct
# destinations in a component that is generated later.

my $ynext;

# Left column:
my $xy_EXIT = xy(0, 1);
my $xy_steel_initial = xy(1, 2);
my $board_startx = $kwidth + 3;

$ynext = max($kheight + 5, 2 * $kheight);

my $y_DF = $ynext;
$ynext += 5;

my $y_PS = $ynext;
my $y_PS1 = $y_PS;
my $y_PS_offsets = $y_PS1 + 1;
my $y_PS_types = $y_PS_offsets + $k_num_offsets;
my $y_PS_misc = $y_PS_types + $k_num_piece_types;
my $y_PSC = $y_PS_misc + 4;
my $y_PSD = $y_PSC + 1;
$ynext = $y_PSD + 1;

my $y_OC = $ynext;
my $y_OCC = $y_OC;
my $y_OCD = $y_OCC + 1 + 2*($k_num_offsets + 1);

my $xy_OCD_invalid = xy(22, 33);

$ynext = $y_OCD + 3;

my $y_T = $ynext;
$ynext += 4;

sub t_test_kc_i_to_xy($$) {
  my ($kc, $i) = @_;
  my $kcr = $kc - $kwh / 2;
  if ($kcr >= 0) {
    return xy(2*$kcr + $i, $y_T + 1 - ($kcr + $i) % 2);
  } else {
    return xy($kwh - 1 - 2*$kc - $i, $y_T + 2 + ($kc + $i) % 2);
  }
}

sub t_total_kc_to_xy($) {
  my ($kc) = @_;
  my $kcr = $kc - $kwh / 2;
  if ($kcr >= 0) {
    return xy(2*$kcr + 1, $y_T + 1 - $kcr % 2);
  } else {
    return xy($kwh - 1 - 2*$kc, $y_T + 2 + ($kc + 1) % 2);
  }
}

sub t_sabotage_xy($) {
  my ($id) = @_;
  return t_test_kc_i_to_xy($id, 0);
}

# Both columns are in fact completely full.
die unless $ynext == $height;

# Right column:
$ynext = 0;

my $y_Ty = $ynext;
$ynext += $k_num_piece_types;
my $x_Ty_psu = $width - $kwh - 2;
my $x_Ty = $x_Ty_psu - 1 - 2*$k_num_offsets;

my $y_DS = $ynext;
my $x_DS_cells = $width - $kwh;
my $x_DS_clear = $x_DS_cells - 1;
my $x_DS_clear_toggle = $x_DS_clear - 1;
my $x_DS_choice = $x_DS_clear_toggle - 1;
$ynext += @directions;

my $A_part_height = 7;
my $y_A = $ynext;
my $y_PSU = $y_A + $A_part_height;
my $y_PSU_offsets = $y_PSU + 1 + $k_num_piece_types;
my $y_A_lower = $y_PSU_offsets + $k_num_offsets;
$ynext = $y_A_lower + $A_part_height;

my $xy_PSU_wincheck_toggle = xy($width - $kwh - 1, $y_A_lower);
my $xy_psu_toggler = xy($width - 1, $y_A_lower - 1);

# $i is 0 or 1
# Decision: $i == 0 is always on the left because this makes it easier to think
# about, even though the loops run in an "inconsistent" direction.
# Note, y and dy are independent of i.  XXX: Refactor to make this clearer?
sub a_kc_i_to_xydy($$) {
  my ($kc, $i) = @_;
  if ($kc >= $kwh / 2) {
    return { x => $width - 2 - (2*$kc - $kwh) + $i, y => $y_A + 6, dy => -1 };
  } else {
    return { x => $width - $kwh + 2*$kc + $i, y => $y_A + 15, dy => 1 };
  }
}

sub axydy_at($$) {
  my ($axydy, $dist) = @_;
  return xy($axydy->{x}, $axydy->{y} + $dist * $axydy->{dy});
}

# Special logic: the goal anchor is moved from its place in sequence to the
# beginning.
sub psu_kc_to_x($) {
  my ($kc) = @_;
  my $x_start = $width - $kwh - 1;
  my $kc_goal = kcons($k_goal->{anchor}->{x}, $k_goal->{anchor}->{y});
  if ($kc == $kc_goal) {
    return $x_start;
  } elsif ($kc > $kc_goal) {
    return $x_start + $kc;
  } else {
    return $x_start + $kc + 1;
  }
}

my $y_S = $ynext;
# NOTE: The height should be approximately 3 * $k_num_offsets + 5, but there
# isn't a simple design that always fits in that space.  I manually worked out
# an irregular design for $k_num_offsets == 3.
my $y_S_bottom = $y_S + 15;
my $x_S = $width - $kwh - 2;
# Irregular... :(
my @y_S_by_off_i = (
  [$y_S_bottom - 1, $y_S_bottom - 4],
  [$y_S_bottom - 5, $y_S_bottom - 8],
  [$y_S_bottom - 9, $y_S_bottom - 10]
);
die unless scalar(@y_S_by_off_i) == scalar($k_num_offsets);
$ynext = $y_S_bottom;

die unless $ynext == $height;

my $xy_button_1 = xy(23, 9);
my $xy_button_2 = xy(21, 16);
my $xy_button_3 = xy(16, 9);
my $xy_button_4_14 = xy(22, 43);
my $xy_button_5_6 = xy(22, 23);
my $xy_button_7_11 = xy(21, 32);
my $xy_button_8_12 = xy(22, 25);
my $xy_button_13 = xy(22, 28);
my $xy_button_9 = xy(20, 44);
my $xy_button_10 = xy(20, 27);
my $xy_button_15 = xy(22, 10);
my $xy_colorflip_0 = xy(21, 41);
my $xy_colorflip_1 = xy(22, 41);

### Component generation

# User control

foreach my $y (2..($kheight + 3)) {
  foreach my $x (0..($kwidth + 1)) {
    set_tile(tl($x, $y), T_ROUGH);
  }
}
foreach my $ky (0..($kheight-1)) {
  foreach my $kx (0..($kwidth-1)) {
    my $t = tl(1+$kx, 3+$ky);
    $t->{type} = T_BPANEL;
    $t->{flags} = TF_HASPANEL | TF_RPANELL;
    $t->{dest} = xy($kwh - 1 - kcons($kx, $ky), $y_PS1);
  }
}
set_tile($xy_steel_initial, T_BSTEEL, T_ROUGH);

sub make_user_button($$$$$) {
  my ($bx, $by, $dx, $dy, $dir) = @_;
  set_tile(tl($bx, $by), T_BUTTON);
  make_remote(tl($bx+$dx, $by+$dy), xy($width - $kwh - 3, $y_DS + $dir));
  make_remote(tl($bx+2*$dx, $by+2*$dy), $xy_button_1);
}

make_user_button(3, 1, -1, 0, 0);
set_tile(tl(3, 0), T_LEFT);
make_user_button($kwidth, 1, 1, 0, 1);
set_tile(tl($kwidth, 0), T_RIGHT);
make_user_button($kwidth+2, 3, 0, -1, 2);
set_tile(tl($kwidth+2, 4), T_UP);
make_user_button($kwidth+2, $kheight+1, 0, 1, 3);
set_tile(tl($kwidth+2, $kheight), T_DOWN);

set_tile($xy_EXIT, T_SLEEPINGDOOR, T_EXIT);

# Board display

sub piece_display_locations($) {
  my ($piece) = @_;
  my @k_locations = ($piece->{anchor});
  foreach my $off_id (0..($k_num_offsets-1)) {
    if ($k_piece_types[$piece->{type}]->[$off_id]) {
      my $delta = $k_offsets[$off_id];
      push(@k_locations,
        xy($piece->{anchor}->{x} + $delta->{dx},
           $piece->{anchor}->{y} + $delta->{dy}));
    }
  }
  my $locations_map;
  foreach my $loc (@k_locations) {
    $locations_map->[$loc->{y}]->[$loc->{x}] = 1;
  }
  my @disp_locations = ();
  foreach my $loc (@k_locations) {
    push(@disp_locations, {x => 2*$loc->{x}, y => 2*$loc->{y}});
    push(@disp_locations, {x => 2*$loc->{x} + 1, y => 2*$loc->{y}})
      if $locations_map->[$loc->{y}]->[$loc->{x}+1];
    push(@disp_locations, {x => 2*$loc->{x}, y => 2*$loc->{y} + 1})
      if $locations_map->[$loc->{y}+1]->[$loc->{x}];    
  }
  return @disp_locations;
}

my $is_goal_zone;
my $occupancy;
foreach my $x (0..(2*$kwidth-2)) {
  foreach my $y (0..(2*$kheight-2)) {
    $is_goal_zone->[$y]->[$x] = 0;
    $occupancy->[$y]->[$x] = 0;
  }
}

foreach my $dloc (piece_display_locations($k_goal)) {
  $is_goal_zone->[$dloc->{y}]->[$dloc->{x}] = 1;
}
foreach my $piece (@k_initial_pieces) {
  foreach my $dloc (piece_display_locations($piece)) {
    $occupancy->[$dloc->{y}]->[$dloc->{x}] =
      ($piece->{type} == $k_goal->{type} ? 2 : 1);
  }
}

my @display_types = (T_FLOOR, T_BDOWN, T_BUP, T_GDOWN, T_RDOWN, T_RUP);
my @display_alt_types = (T_BDOWN, T_FLOOR, T_FLOOR, T_RDOWN, T_GDOWN, T_GDOWN);
foreach my $x (0..(2*$kwidth-2)) {
  foreach my $y (0..(2*$kheight-2)) {
    my $n = 3 * $is_goal_zone->[$y]->[$x] + $occupancy->[$y]->[$x];
    set_tile(tl($board_startx + $x, $y), $display_types[$n], $display_alt_types[$n]);
  }
}

# DF

foreach my $kx (0..($kwidth-1)) {
  foreach my $ky (0..($kheight-1)) {
    my $kc = kcons($kx, $ky);
    my $x = $kwh - 1 - $kc;
    set_tile(tl($x, $y_DF),
      ($kc == 0) ? T_NW : T_WE,
      ($kc == 0) ? T_NS : T_SE);
    foreach my $dir (0..(@directions-1)) {
      my $t = $level->{tiles}->[$y_DF + 1 + $dir]->[$x];
      my $delta = $directions[$dir];
      my $kx2 = $kx + $delta->{dx};
      my $ky2 = $ky + $delta->{dy};
      my $kc2 = kxy_valid($kx2, $ky2) ? kcons($kx2, $ky2) : $kwh;
      make_alt_remote(tl($x, $y_DF + 1 + $dir),
        xy($width - $kwh - 1 + $kc2, $y_S + 4), T_NS);
    }
  }
}

# PS

foreach my $kx (0..($kwidth-1)) {
  foreach my $ky (0..($kheight-1)) {
    my $kc = kcons($kx, $ky);
    my $x = $kwh - 1 - $kc;
    set_tile(xy($x, $y_PS1), T_WE, T_SE);
    set_tile(xy($x, $y_PSC), T_NSWE, T_NE);
    set_tile(xy($x, $y_PSD), T_WE, T_NE);
    foreach my $off (0..($k_num_offsets-1)) {
      my $delta = $k_offsets[$off];
      my $t = tl($x, $y_PS_offsets + $off);
      my $kx2 = $kx - $delta->{dx};
      my $ky2 = $ky - $delta->{dy};
      if (kxy_valid($kx2, $ky2)) {
        my $kc2 = kcons($kx2, $ky2);
        make_alt_remote($t, xy($kwh - 1 - $kc2, $y_PSC), T_NS);
      } else {
        set_tile($t, T_NS);
      }
    }
    foreach my $ptype (0..($k_num_piece_types-1)) {
      # Some of these may be unusable because a piece of the given type anchored
      # at the current cell doesn't fit in the board.  But we don't bother to
      # check for that here.
      make_alt_remote(tl($x, $y_PS_types + $ptype), xy($x_Ty, $y_Ty + $ptype), T_NS);
    }
    # OCC
    make_remote(tl($x, $y_PS_misc), xy($x, $y_OCC));
    # PSC
    make_remote(tl($x, $y_PS_misc+1), xy($x, $y_PSC));
    # PSU
    make_remote(tl($x, $y_PS_misc+2), xy(psu_kc_to_x($kc), $y_PSU));
    # DF
    # TODO: Can DF be merged into PS to save a row?  It's not trivial.
    make_remote(tl($x, $y_PS_misc+3), xy($x, $y_DF));
  }
}

# OC

# need to know: x, y, target kc, whether permanent.
sub oc_set_pair($$$$) {
  my ($x, $y, $tkc, $permanent) = @_;
  foreach my $i (0..1) {
    my $t = $level->{tiles}->[$y+$i]->[$x];
    my $active_type;
    if ($tkc < 0) {
      if ($i == 0) {
        $active_type = T_REMOTE;
        $t->{dest} = t_sabotage_xy(-$tkc - 1);
      } else {
        $active_type = T_NS;
      }
    } else {
      $active_type = T_REMOTE;
      $t->{dest} = axydy_at(a_kc_i_to_xydy($tkc, $i), 0);
    }
    if ($permanent) {
      $t->{type} = $active_type;
    } else {
      $t->{type} = T_NS;
      $t->{alt_type} = $active_type;
    }
  }
}

foreach my $kx (0..($kwidth-1)) {
  foreach my $ky (0..($kheight-1)) {
    my $kc = kcons($kx, $ky);
    my $x = $kwh - 1 - $kc;

    set_tile(tl($x, $y_OCC), T_WE, T_SE);
    set_tile(tl($x, $y_OCD), T_WE, T_NE);

    oc_set_pair($x, $y_OC + 1, $kc, 1);
    foreach my $off (0..($k_num_offsets-1)) {
      my $delta = $k_offsets[$off];
      my $kx2 = $kx + $delta->{dx};
      my $ky2 = $ky + $delta->{dy};
      my $kc2 = kxy_valid($kx2, $ky2) ? kcons($kx2, $ky2) : -1 - $off;
      oc_set_pair($x, $y_OC + 3 + 2*$off, $kc2, 0);
    }

    # Now the AS part.
    foreach my $i (0..1) {
      make_remote(tl($x + 1, $y_OCD + 1 + $i),
        axydy_at(a_kc_i_to_xydy($kc, $i), 3));
    }
    set_tile(tl(0, $y_OCD + 1), T_SE);
    set_tile(tl(0, $y_OCD + 2), T_NE);
  }
}

make_alt_remote($xy_OCD_invalid, t_sabotage_xy(0), T_NS);

# T

sub construct_half_t {
  my ($y_inner, $y_outer, $tile_types) = @_;
  foreach my $unit (0..($kwh/2 - 1)) {
    my $x = 2*$unit;
    my $parity = $unit % 2;
    my $y_start = ($parity == 0) ? $y_inner : $y_outer;
    my $y_end = ($parity == 0) ? $y_outer : $y_inner;
    my $t;
    # First test tile
    $t = tl($x, $y_start);
    if ($unit == 0) {
      # Special case: turn to connect to the other half.
      set_tile($t, $tile_types->{turn}, $tile_types->{turn_alt});
    } else {
      set_tile($t, $tile_types->{test1}->[$parity], $tile_types->{test1_alt}->[$parity]);
    }
    # Connecting tile for when not testing
    set_tile(tl($x, $y_end), $tile_types->{conn}->[$parity]);
    # Total tile
    set_tile(tl($x+1, $y_start), $tile_types->{total}->[$parity]);
    # Second test tile
    set_tile(tl($x+1, $y_end), $tile_types->{test2}->[$parity], $tile_types->{test2_alt}->[$parity]);
  }
}

# Lower half
construct_half_t($y_T + 2, $y_T + 3, {
  turn => T_NS,
  turn_alt => T_NE,
  test1 => [T_SW, T_NW],
  test1_alt => [T_WE, T_WE],
  conn => [T_NE, T_SE],
  total => [T_SW, T_NW],
  test2 => [T_WE, T_WE],
  test2_alt => [T_NE, T_SE]
});
# Upper half
# tile_types is just the vertical reversal of the one for the lower half.
construct_half_t($y_T + 1, $y_T, {
  turn => T_NS,
  turn_alt => T_SE,
  test1 => [T_NW, T_SW],
  test1_alt => [T_WE, T_WE],
  conn => [T_SE, T_NE],
  total => [T_NW, T_SW],
  test2 => [T_WE, T_WE],
  test2_alt => [T_SE, T_NE]
});

# Ty

foreach my $ptype (0..($k_num_piece_types-1)) {
  my $y = $ptype;
  set_tile(tl($x_Ty, $y), ($ptype == 0) ? T_SW : T_NS, T_SE);

  # Toggle offsets in S
  foreach my $off (0..($k_num_offsets-1)) {
    foreach my $i (0..1) {
      my $t = tl($x_Ty + 1 + 2*$off + $i, $y);
      if ($k_piece_types[$ptype]->[$off]) {
        make_remote($t, xy($x_S, $y_S_by_off_i[$off]->[$i]));
      } else {
        set_tile($t, T_WE);
      }
    }
  }

  # Toggle this type for all anchors in PSU
  foreach my $kc (0..($kwh-1)) {
    make_remote(tl($x_Ty_psu + $kc, $y),
      xy(psu_kc_to_x($kc), $y_PSU + 1 + $ptype));
  }
}

# Goal type: enable color flipping and win checking in PSU
my $goal_type = $k_goal->{type};
die unless $goal_type >= 2;
make_remote(tl($width - 2, $goal_type), $xy_PSU_wincheck_toggle);
set_tile(tl($width - 1, $goal_type), T_NW);
make_remote(tl($width - 1, $goal_type - 1), $xy_colorflip_0);
make_remote(tl($width - 1, $goal_type - 2), $xy_colorflip_1);

# If we didn't find a valid anchor cell with a type, skip most of the sequence.
set_tile(tl($x_Ty - 1, 0), T_SE);
make_remote(tl($x_Ty - 1, 1), $xy_button_5_6);
make_remote(tl($x_Ty - 1, 2), $xy_button_9);

# DS

foreach my $dir (0..(@directions-1)) {
  my $y = $y_DS + $dir;

  set_tile(tl($x_DS_choice, $y),
    # Careful not to accidentally feed into Ty!
    ($dir == 0) ? T_BLACK : T_NS,
    T_SE);

  make_remote(tl($x_DS_clear_toggle, $y), xy($x_DS_clear, $y));

  make_alt_remote(tl($x_DS_clear, $y), xy($x_DS_choice, $y), T_WE);

  foreach my $kc (0..($kwh - 1)) {
    make_remote(tl($x_DS_cells + $kc, $y), xy($kc, $y_DF + 1 + $dir));
  }
}

# A + PSU

foreach my $kx (0..($kwidth-1)) {
  foreach my $ky (0..($kheight-1)) {
    my $kc = kcons($kx, $ky);
    foreach my $i (0..1) {
      my $axydy = a_kc_i_to_xydy($kc, $i);

      my $turnback_ttype = ($axydy->{dy} > 0)
        ? ($i == 0 ? T_NE : T_NW)
        : ($i == 0 ? T_SE : T_SW);

      my $t = tl(axydy_at($axydy, 0));
      if ($axydy->{x} == $width - 1) {
        # Edge case
        set_tile($t, ($axydy->{dy} > 0) ? T_NW : T_SW, T_NS);
      } else {
        set_tile($t, T_WE,
          ($axydy->{dy} > 0)
            ? ($i == 0 ? T_SW : T_SE)
            : ($i == 0 ? T_NW : T_NE));
      }

      make_remote(tl(axydy_at($axydy, 1)), t_test_kc_i_to_xy($kc, $i));

      # UI broadcast to cells left and above
      $t = tl(axydy_at($axydy, 2));
      my $dest = ($i == 0)
        ? ($kx == 0 ? undef : axydy_at(a_kc_i_to_xydy(kcons($kx-1, $ky), 0), 5))
        : ($ky == 0 ? undef : axydy_at(a_kc_i_to_xydy(kcons($kx, $ky-1), 1), 5));
      if ($dest) {
        make_remote($t, $dest);
      } else {
        set_tile($t, T_NS);
      }

      # Initially active but can be switched off by AS
      make_remote(axydy_at($axydy, 3), axydy_at($axydy, 4), T_NS);

      # Initially a turnback but will be toggled every time A is pulsed so that
      # the logic here and beyond runs every second pulse of A.
      make_alt_remote(axydy_at($axydy, 4),
        ($i == 0)
          ? xy($board_startx + 2*$kx, 2*$ky)  # Draw center tile on board
          : t_total_kc_to_xy($kc),            # Update total occupancy in T
         $turnback_ttype);

      # Receive UI broadcast from cells right and below, and if so, flip the
      # tile on the board.
      $t = tl(axydy_at($axydy, 5));
      $t->{type} = T_NS;
      $dest = ($i == 0)
        ? ($kx == $kwidth - 1 ? undef : xy($board_startx + 2*$kx + 1, 2*$ky))
        : ($ky == $kheight - 1 ? undef : xy($board_startx + 2*$kx, 2*$ky + 1));
      if ($dest) {
        make_alt_remote($t, $dest, T_NS);
      } else {
        set_tile($t, T_NS);
      }

      set_tile(axydy_at($axydy, 6), $turnback_ttype);
    }
  }
}

# Normally, every second invocation of A should trigger PSU (just like UI).
# However, AS can temporarily shut off this behavior.
my $xy_psu_link = xy($width - $kwh - 1, $y_PSU - 1);
set_tile($xy_psu_link, T_BLACK, T_SE);

# Link the two halves of A.
make_remote($xy_psu_toggler, $xy_psu_link, T_NS);

foreach my $y ($y_PSU..($y_A_lower-2)) {
  set_tile(tl($width - 1, $y), T_NS);
}

# PSU

foreach my $kx (0..($kwidth-1)) {
  foreach my $ky (0..($kheight-1)) {
    my $kc = kcons($kx, $ky);
    my $x = psu_kc_to_x($kc);

    my $t = tl($x, $y_PSU);
    if ($x == $width - $kwh - 1) {
      set_tile($t, T_NE, T_NS);
    } else {
      set_tile($t, T_WE, T_SW);
    }

    foreach my $ptype (0..($k_num_piece_types - 1)) {
      # Remote has to be activated by Ty
      make_alt_remote(tl($x, $y_PSU + 1 + $ptype),
        xy($kwh - 1 - $kc, $y_PS_types + $ptype),
        T_NS);
    }

    foreach my $off (0..($k_num_offsets - 1)) {
      my $t = tl($x, $y_PSU_offsets + $off);
      # Is there a cell at the offset from the current cell?
      my $delta = $k_offsets[$off];
      my $kx2 = $kx + $delta->{dx};
      my $ky2 = $ky + $delta->{dy};
      if (kxy_valid($kx2, $ky2)) {
        my $kc2 = kcons($kx2, $ky2);
        make_alt_remote($t, xy($kwh - 1 - $kc2, $y_PS_offsets + $off), T_NS);
      } else {
        # For simplicity's sake, we let S toggle entire rows of PSU and don't
        # require it to know which offsets make sense for which cells.
        set_tile($t, T_NS, T_NS);
      }
    }
  }
}

# Win check area
set_tile($xy_PSU_wincheck_toggle, T_WE, T_NSWE);
make_remote(tl($width - $kwh - 1, $y_A_lower + 1), $xy_EXIT);

# S + DC

set_tile(tl($x_S, $height - 1), T_NW);
set_tile(tl($x_S, $height - 11), T_SE);
foreach my $i (2..10) {
  set_tile(tl($x_S, $height - $i), T_NS);
}
foreach my $i (1, 5, 9) {
  my $t = tl($x_S, $height - $i);
  $t->{alt_type} = ($i == 1) ? T_WE : T_SE;
}
foreach my $i (4, 8, 10) {
  my $t = tl($x_S, $height - $i);
  $t->{alt_type} = T_NE;
}

foreach my $off (0..2) {
  my @dests = map { (
    xy($kwh - 1 - $_, $y_OC + 3 + 2*$off),
    xy($kwh - 1 - $_, $y_OC + 3 + 2*$off + 1),
    xy(psu_kc_to_x($_), $y_PSU_offsets + $off)
  ) } (0..$kwh-1);
  die unless $kwh == 20 && scalar(@dests) == 60;
  if ($off < 2) {
    my $y = $height - 4 - 4*$off;
    set_tile(tl($x_S+1, $y), T_WE);
    set_tile(tl($x_S+1, $y+1), T_SE);
    set_tile(tl($x_S+1, $y+2), T_NE);
    set_tile(tl($x_S+1, $y+3), T_WE);
    set_tile(tl($x_S+17, $y), T_SW);
    set_tile(tl($x_S+17, $y+1), T_NW);
    set_tile(tl($x_S+17, $y+2), T_SW);
    set_tile(tl($x_S+17, $y+3), T_NW);
    foreach my $i (0..3) {
      foreach my $j (0..14) {
        make_remote(tl($x_S+2+$j, $y+$i), $dests[15*$i + $j]);
      }
    }
  } else {
    my $y = $height - 10;
    foreach my $i (0..19) {
      make_remote(tl($x_S+1+$i, $y), $dests[$i]);
    }
    $level->{tiles}->[$y]->[$x_S+21]->{type} = T_SW;
    $level->{tiles}->[$y+1]->[$x_S+21]->{type} = T_NS;
    $level->{tiles}->[$y+1]->[$x_S+20]->{type} = T_SW;
    $level->{tiles}->[$y+1]->[$x_S+19]->{type} = T_SE;
    $level->{tiles}->[$y+1]->[$x_S+18]->{type} = T_SW;
    $level->{tiles}->[$y+9]->[$x_S+21]->{type} = T_NW;
    $level->{tiles}->[$y+9]->[$x_S+20]->{type} = T_NE;
    $level->{tiles}->[$y+9]->[$x_S+19]->{type} = T_NW;
    $level->{tiles}->[$y+9]->[$x_S+18]->{type} = T_NE;
    foreach my $i (0..3) {
      foreach my $j (0..6) {
        make_remote(tl($x_S+18+$i, $y+2+$j), $dests[20 + 7*$i + $j]);
      }
    }
    foreach my $i (0..11) {
      make_remote(tl($x_S+1+$i, $y+1), $dests[48 + $i]);
    }
    # A few spare cells (($x_S+21, $y+1) is spare too)
    foreach my $x (($x_S+13)..($x_S+17)) {
      set_tile(tl($x, $y+1), T_WE);
    }
  }
}

# DC
# Invalid displaced anchor is at the far right.
foreach my $kc (0..$kwh) {
  my $x = $width - $kwh - 1 + $kc;

  my $choice_xy = xy($x, $y_S + 4);
  set_tile($choice_xy, T_WE, T_NW);

  make_remote(tl($x, $y_S + 3),
    ($kc == $kwh) ? $xy_OCD_invalid : xy($kwh - 1 - $kc, $y_OCD));

  my $t = tl($x, $y_S + 2);
  if ($kc == $kwh) {
    set_tile($t, T_NS);
  } else {
    make_remote($t, xy($kwh - 1 - $kc, $y_PSD));
  }

  my $clearer_xy = xy($x, $y_S);
  make_remote(tl($x, $y_S + 1), $clearer_xy);

  make_alt_remote($clearer_xy, $choice_xy, T_NS);
}

# One last thing: load the initial pieces into PS and T.  (We did the display
# above, but it seems convenient to do these here.)

foreach my $piece (@k_initial_pieces) {
  my $ax = $piece->{anchor}->{x}, my $ay = $piece->{anchor}->{y};
  my $ac = kcons($ax, $ay);
  flip_tile(tl($kwh - 1 - $ac, $y_PS_types + $piece->{type}));
  flip_tile(t_total_kc_to_xy($ac));
  foreach my $off (0..(@k_offsets-1)) {
    my $delta = $k_offsets[$off];
    if ($k_piece_types[$piece->{type}]->[$off]) {
      my $kx2 = $ax + $delta->{dx};
      my $ky2 = $ay + $delta->{dy};
      my $kc2 = kcons($kx2, $ky2);
      # This had better be a valid Klotski cell.
      flip_tile(t_total_kc_to_xy($kc2));
      flip_tile(tl($kwh - 1 - $kc2, $y_PS_offsets + $off));
    }
  }
  # If you really want to make a puzzle that is initially won...
  if ($piece->{type} == $k_goal->{type} &&
    $piece->{anchor}->{x} == $k_goal->{anchor}->{x} &&
    $piece->{anchor}->{y} == $k_goal->{anchor}->{y}) {
    flip_tile($xy_EXIT);
  }
}

# Wiring and control

# Because of the narrow space between the two columns of components, it happens
# that columns of remotes are much more common than rows in the control.
sub make_remote_column($@) {
  my ($xy_top, @dests) = @_;
  foreach my $i (0..(@dests - 1)) {
    my $t = tl($xy_top->{x}, $xy_top->{y} + $i);
    my $dest = $dests[$i];
    if (defined($dest)) {
      make_remote($t, $dest);
    } else {
      # Occasionally necessary to guard against pulses coming from the side.
      set_tile($t, T_NS);
    }
  }
}

# Shared location definitions

my $xy_init_button = xy(21, 11);
my $xy_DS_switch = xy(22, 9);
my $xy_Ty_switch = xy(16, 4);
my $xy_PS_1_15_switch = xy(23, 10);
my $xy_PS1_PSC_switch = xy(20, 15);
my $xy_button_2_switch1 = xy(21, 15);
my $xy_button_2_switch2 = xy(20, 16);
my @button_2_dests = ($xy_button_2, $xy_button_2_switch1, $xy_button_2_switch2);
my $xy_A_disconnect = xy(23, 23);
my $xy_OCC_disconnect = xy(21, 31);
my $xy_10_disconnect = xy(20, 26);

# Initial button

set_tile($xy_init_button, T_BUTTON);
make_remote(tl(21, 12), $xy_steel_initial);
make_remote(tl(21, 13), $xy_init_button);

# Button 1

set_tile($xy_button_1, T_BLACK, T_BUTTON);

# Connection to DS, which can be switched to button 15
set_tile($xy_DS_switch, T_NE, T_NS);
set_tile(tl(22, 8), T_NS);

# Connection to PS1/PSC.  The near end can be switched to button 15, and
# the far end initially goes to PS1 but can be switched to PSC.
set_tile($xy_PS_1_15_switch, T_NS, T_SW);
# Should be safe since this wire is used only by bots 1 and 15.
make_remote_column(xy(23, 11), $xy_button_15, $xy_PS_1_15_switch);
set_tile(tl(23, 13), T_NS);
set_tile(tl(23, 14), T_NW);
set_tile(tl(22, 14), T_WE);
set_tile(tl(21, 14), T_SE);
set_tile($xy_button_2_switch1, T_NW, T_SW);
set_tile($xy_PS1_PSC_switch, T_WE, T_SE);
set_tile($xy_button_2_switch2, T_NS, T_SE);
foreach my $y (17..26) {
  set_tile(tl(20, $y), T_NS);
}
set_tile($xy_button_10, T_NW, T_BUTTON);
set_tile(tl(20, 28), T_NW);

# Control actions for button 1
set_tile(tl(23, 8), T_SE);
make_remote(tl(24, 8), $xy_button_1);
set_tile(tl(24, 9), T_SW);
make_remote_column(xy(24, 10), $xy_DS_switch, @button_2_dests);

# Button 2

set_tile($xy_button_2, T_BLACK, T_BUTTON);
make_remote_column(xy(21, 17), @button_2_dests, $xy_PS1_PSC_switch,
  $xy_button_3, $xy_button_4_14);

# Button 3

set_tile($xy_button_3, T_BLACK, T_BUTTON);
make_remote_column(xy(16, 5), $xy_button_3,
  $xy_Ty_switch, $xy_button_5_6, $xy_button_9);
set_tile($xy_Ty_switch, T_NS, T_NE);
set_tile(tl(17, 9), T_WE);
set_tile(tl(18, 9), T_WE);
set_tile(tl(19, 9), T_SW);

# Button 4/14

set_tile($xy_button_4_14, T_BLACK, T_BUTTON);
set_tile($xy_colorflip_0, T_BLACK, T_BLIGHT);
set_tile($xy_colorflip_1, T_BLACK, T_RLIGHT);
set_tile(tl(21, 42), T_NS);
set_tile(tl(21, 43), T_NE);
make_remote(tl(22, 42), $xy_10_disconnect);
set_tile(tl(22, 44), T_NE);

# Button 5/6

# The other buttons are on the path, so we'll go ahead and create them here.
set_tile($xy_button_5_6, T_SE, T_BUTTON);
set_tile($xy_A_disconnect, T_BLACK, T_WE);  # initially disconnected
set_tile(tl(22, 24), T_NS);
set_tile($xy_button_8_12, T_NS, T_BUTTON);
set_tile(tl(22, 26), T_NS);
set_tile(tl(22, 27), T_NS);
set_tile($xy_button_13, T_NW, T_BUTTON);
set_tile(tl(21, 28), T_SE);
set_tile(tl(21, 29), T_NS);
set_tile(tl(21, 30), T_NS);
set_tile($xy_OCC_disconnect, T_NS, T_BLACK);  # initially connected
set_tile($xy_button_7_11, T_NW, T_BUTTON);
set_tile(tl(20, 32), T_NE);
set_tile(tl(20, 31), T_NS);
set_tile(tl(20, 30), T_NS);
set_tile(tl(20, 29), T_SW);

# Finally, the remotes specific to button 5/6.  The remote column toggles the
# connectivity of part of itself on every invocation, so that alternate
# invocations do different things, so we can use the same button with two bots
# for two different pipeline stages rather than having to lay out separate
# buttons and remote columns.
make_remote_column(xy(22, 17),
  $xy_A_disconnect, $xy_button_5_6, $xy_button_7_11,
  xy(22, 19), $xy_OCC_disconnect, $xy_A_disconnect);
flip_tile(xy(22, 19));

# Button 7/11

set_tile(tl(22, 32), T_SW);
# Note: $xy_OCD_invalid (22, 33) is handled with the OC component.
set_tile(tl(22, 34), T_NW);
set_tile(tl(21, 34), T_SE);
make_remote_column(xy(21, 35), $xy_OCC_disconnect,
  $xy_button_7_11, $xy_button_8_12, $xy_psu_toggler);
set_tile(tl(21, 39), T_NW);

set_tile(tl(21, 33), T_NW);
set_tile(tl(20, 33), T_SE);
foreach my $y (34..37) {
  set_tile(tl(20, $y), T_NS);
}
set_tile(tl(20, 38), T_NW);

# Button 8/12

set_tile(tl(23, 25), T_SW);
make_remote_column(xy(23, 26),
  $xy_button_8_12, $xy_OCC_disconnect,
  undef,  # interference from button 13
  $xy_button_7_11, xy(23, 31), $xy_button_7_11, $xy_button_13);
flip_tile(xy(23, 31));

# Button 13

make_remote_column(xy(22, 29), $xy_A_disconnect, $xy_button_13, $xy_button_5_6);

# Button 9

set_tile($xy_button_9, T_BLACK, T_BUTTON);
set_tile(tl(20, 43), T_SW);
make_remote(tl(20, 42), $xy_button_10);

# Button 10

make_remote(tl(21, 27), $xy_button_10);

# Button 15

set_tile($xy_button_15, T_BLACK, T_BUTTON);

# Connection to Ty, with remotes
set_tile(tl(21, 10), T_NE);
make_remote_column(xy(21, 6), $xy_PS1_PSC_switch, $xy_DS_switch, $xy_Ty_switch,
  $xy_button_4_14);
set_tile(tl(21, 5), T_NS);
set_tile(tl(21, 4), T_SW);
foreach my $x (17..20) {
  set_tile(tl($x, 4), T_WE);
}

# Extra remotes
make_remote_column(xy(22, 11), undef, $xy_button_5_6, $xy_button_9);

# Bots!  (Order is critical!)

add_bot_right($xy_button_1);
add_bot_right($xy_button_2);
add_bot_below($xy_button_3);
add_bot_right($xy_button_4_14);
add_bot_right($xy_button_5_6);
add_bot_below($xy_button_5_6);
add_bot_right($xy_button_7_11);
add_bot_right($xy_button_8_12);
add_bot_right($xy_button_9);
add_bot_right($xy_button_10);
add_bot_below($xy_button_7_11);
add_bot_below($xy_button_8_12);
add_bot_below($xy_button_13);
add_bot_below($xy_button_4_14);
add_bot_below($xy_button_15);

################
# Encode and write to file

sub esc_index($) {
  my ($pos) = @_;
  return $pos->{y} * $level->{width} + $pos->{x};
}

sub simple_rleencode(@) {
  my $out = pack('C', 128 + 32);
  foreach my $elt (@_) {
    $out .= pack('CN', 1, $elt);
  }
  return $out;
}

my $levelstr = 'ESXL';
$levelstr .= pack('NN', $level->{width}, $level->{height});
$levelstr .= pack('Na*', length($level->{title}), $level->{title});
$levelstr .= pack('Na*', length($level->{author}), $level->{author});
$levelstr .= pack('NN', $level->{guyx}, $level->{guyy});

my @flat_tiles = ();
foreach my $row (@{$level->{tiles}}) {
  push(@flat_tiles, @{$row});
}
$levelstr .= simple_rleencode(map { $_->{type} } @flat_tiles);
$levelstr .= simple_rleencode(map { $_->{alt_type} } @flat_tiles);
$levelstr .= simple_rleencode(map { esc_index($_->{dest}) } @flat_tiles);
$levelstr .= simple_rleencode(map { $_->{flags} } @flat_tiles);

my @bots = @{$level->{bots}};
$levelstr .= pack('N', scalar(@bots));
$levelstr .= simple_rleencode(map { esc_index($_->{loc}) } @bots);
$levelstr .= simple_rleencode(map { $_->{type} } @bots);

# `uncompressed`: since we don't implement compression, the level is supposed to
# be opened with Escape and re-saved.
open(my $fh, '>', "klotski-$puzzle_id-uncompressed.esx") or die;
print $fh $levelstr or die;
close $fh or die;

