### ----------------------------------------------------
### -- Chess
### -- Licenses: Artistic-2.0
### -- Authors: Lucien Grondin
### -- File: lib/Chess/JS.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 2
unit class Chess::JS;
# translated from https://github.com/jhlywa/chess.js.git
#`{{{ ORIGINAL LICENSE
Copyright (c) 2025, Jeff Hlywa (jhlywa@gmail.com)
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
}}}
class Move {...}
trusts Move;

enum Color is export <WHITE BLACK>;

constant PAWN   is export(:pieces) = 'p';
constant KNIGHT is export(:pieces) = 'n';
constant BISHOP is export(:pieces) = 'b';
constant ROOK   is export(:pieces) = 'r';
constant QUEEN  is export(:pieces) = 'q';
constant KING   is export(:pieces) = 'k';

constant DEFAULT-POSITION = q{rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1};

# emulate javascript's void 0??
constant void0 = 0 but role { method defined { False } }

constant %FLAGS = 
  NORMAL       => 'n',
  CAPTURE      => 'c',
  BIG_PAWN     => 'b',
  EP_CAPTURE   => 'e',
  PROMOTION    => 'p',
  KSIDE_CASTLE => 'k',
  QSIDE_CASTLE => 'q'
  ;

constant %BITS =
  NORMAL       => 1,
  CAPTURE      => 2,
  BIG_PAWN     => 4,
  EP_CAPTURE   => 8,
  PROMOTION    => 16,
  KSIDE_CASTLE => 32,
  QSIDE_CASTLE => 64
  ;

# https://en.wikipedia.org/wiki/0x88
our constant @SQUARES = ([1..8] .reverse) X[R~] 'a'..'h';
our constant %Ox88 = (@SQUARES Z=> ((0, 16 ... *) Z[X+] ^8 xx 8).flat);

subset Square          of UInt   where BEGIN %Ox88{@SQUARES}.any;
subset EnPassantSquare of Square where BEGIN %Ox88{@SQUARES.grep: /<[36]>$/}.any;

constant %PAWN_OFFSETS = 
  (BLACK) => [16, 32, 17, 15],
  (WHITE) => [-16, -32, -17, -15]
  ;
constant %PIECE_OFFSETS = 
  n => [-18, -33, -31, -14, 18, 33, 31, 14],
  b => [-17, -15, 17, 15],
  r => [-16, 1, 16, -1],
  q => [-17, -16, -15, 1, 17, 16, 15, -1],
  k => [-17, -16, -15, 1, 17, 16, 15, -1]
;
constant $ATTACKS = blob8.new:
	20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20, 0, 0, 20, 0, 0,
	0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 20, 0, 0, 0, 0, 24, 0, 0,
	0, 0, 20, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0,
	0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20,
	2, 24, 2, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0,
	0, 0, 0, 0, 24, 24, 24, 24, 24, 24, 56, 0, 56, 24, 24, 24, 24, 24, 24,
	0, 0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
	20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20,
	0, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0,
	20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 24,
	0, 0, 0, 0, 0, 20, 0, 0, 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20
;
constant $RAYS = Blob[int8].new:
	17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0, 0, 17, 0, 0, 0,
	0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0,
	0, 15, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0, 0,
	0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0,
	16, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0,
	0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0,
	0, 0, 0, 0, -15, -16, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0,
	-16, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, -16, 0, 0, -17,
	0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, -16, 0, 0, 0, -17, 0, 0, 0, 0, 0,
	0, -15, 0, 0, 0, 0, -16, 0, 0, 0, 0, -17, 0, 0, 0, 0, -15, 0, 0, 0, 0,
	0, -16, 0, 0, 0, 0, 0, -17, 0, 0, -15, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0,
	0, 0, 0, -17;
constant %PIECE_MASKS = p => 1, n => 2, b => 4, r => 8, q => 16, k => 32;
constant SYMBOLS = "pnbrqkPNBRQK";
constant @PROMOTIONS = KNIGHT, BISHOP, ROOK, QUEEN;
constant RANK_1 = 7;
constant RANK_2 = 6;
constant RANK_7 = 1;
constant RANK_8 = 0;
constant %SIDES = 
  (KING)  => %BITS<KSIDE_CASTLE>,
  (QUEEN) => %BITS<QSIDE_CASTLE>
;
constant %ROOKS = 
  (WHITE) => [
    { square => %Ox88<a1>, flag => %BITS<QSIDE_CASTLE> },
    { square => %Ox88<h1>, flag => %BITS<KSIDE_CASTLE> }
  ],
  (BLACK) => [
    { square => %Ox88<a8>, flag => %BITS<QSIDE_CASTLE> },
    { square => %Ox88<h8>, flag => %BITS<KSIDE_CASTLE> }
  ]
;
constant %SECOND_RANK = (BLACK) => RANK_7, (WHITE) => RANK_2;
constant @TERMINATION_MARKERS = <1-0 0-1 1/2-1/2 *>;

sub rank($square) { $square +> 4 }
sub file($square) { $square +& 15 }

sub isDigit($c) { $c ~~ /<[0..9]>/ }
sub algebraic($square) { ('a'..'h')[file($square)] ~ [1..8].reverse[rank($square)] }
sub swapColor($color) { $color eq WHITE ?? BLACK !! WHITE }

our sub validateFen($fen) {
  my @tokens = $fen.words;
  fail "Invalid FEN: must contain six space-delimited fields" if @tokens.elems !== 6;
  try my $moveNumber = @tokens[5].parse-base(10);
  fail "Invalid FEN: could not parse move number" if $!;
  try my $halfMoves = @tokens[4].parse-base(10);
  fail "Invalid FEN: could not parse number of half moves" if $!;
  fail "Invalid FEN: en-passant square is invalid" unless @tokens[3] ~~ /^[\-|<[a..h]><[36]>]$/;
  fail "Invalid FEN: castling availability is invalid" if @tokens[2] ~~ /<-[KQkq-]>/;
  fail "Invalid FEN: side-to-move is invalid" unless @tokens[1] eq 'w'|'b';
  my @rows = @tokens[0].split('/');
  fail "Invalid FEN: piece data does not contain 8 '/'-delimited rows" unless @rows == 8;
  for @rows -> $row {
    state $rank = 0;
    my UInt $sumFields = 0;
    my Bool $previousWasNumber = False;
    for ^$row.chars -> $k {
      if isDigit($row.substr($k, 1)) {
	if $previousWasNumber {
	  fail "Invalid FEN : piece data is invalid (consecutive numbers)";
	}
	$sumFields += $row.substr($k, 1);
	$previousWasNumber = True;
      } else {
	unless $row.substr($k, 1) eq "prnbqkPRNBQK".comb.any {
	  fail "Invalid FEN : piece data is invalid (invalid piece)";
	}
	$sumFields++;
	$previousWasNumber = False;
      }
    }
    $rank++;
    fail "Invalid FEN: piece data is invalid (too many or too few squares in rank {[1..8][8-$rank]})" unless $sumFields == 8;
    if @tokens[3] ~~ /3/ && @tokens[1] eq WHITE || @tokens[3] ~~ /6/ && @tokens[1] eq BLACK {
      fail "Invalid FEN: illegal en-passant square"
    }
    constant @kings =
      { color => 'white', regex => rx/K/ },
      { color => 'black', regex => rx/k/ }
      ;
    for @kings {
      unless @tokens[0].contains(.<regex>) {
	fail "Invalid FEN: missing {.<color>} king";
      }
      if @tokens[0].match(.<regex>, :g) > 1 {
	fail "Invalid FEN: too many {.<color>} kings";
      }
    }
    if @rows[0,7].join.contains(/<[pP]>/) {
      fail "Invalid FEN: some pawns are on the edge rows";
    }
  }
}
sub getDisambiguator($move, @moves) {
  my ($from, $to, $piece) = $move<from>, $move<to>, $move<piece>;
  my $ambiguities = 0;
  my $sameRank    = 0;
  my $sameFile    = 0;
  loop (my ($i, $len) = 0, @moves.elems; $i < $len; $i++) {
    my $ambigFrom  = @moves[$i]<from>;
    my $ambigTo    = @moves[$i]<to>;
    my $ambigPiece = @moves[$i]<piece>;
    if $piece eq $ambigPiece && $from ne $ambigFrom && $to eq $ambigTo {
      $ambiguities++;
      if rank($from) == rank($ambigFrom) { $sameRank++ }
      if file($from) == file($ambigFrom) { $sameFile++ }
    }
  }
  if $ambiguities > 0 {
    if $sameRank > 0 && $sameFile > 0 {
      return algebraic($from);
    } elsif $sameFile > 0 {
      return algebraic($from).substr(1, 1);
    } else {
      return algebraic($from).substr(0, 1);
    }
  }
  return '';
}

sub addMove(@moves, $color, $from, $to, $piece, $captured = void0, $flags = %BITS<NORMAL>) {
  my $r = rank($to);
  if $piece eq PAWN && $r == RANK_1|RANK_8 {
    loop (my $i = 0; $i < @PROMOTIONS.elems; $i++) {
      my $promotion = @PROMOTIONS[$i];
      @moves.push: {
	:$color,
	:$from,
	:$to,
	:$piece,
	:$captured,
	:$promotion,
	:flags($flags +| %BITS<PROMOTION>)
      };
    }
  } else {
    @moves.push: {
      :$color,
      :$from,
      :$to,
      :$piece,
      :$captured,
      :$flags
    }
  }
}
sub inferPieceType($san) {
  my $pieceType = $san.substr(0, 1);
  if 'a' le $pieceType le 'h' {
    my @matches = $san.match(/<[a..h]>\d.*<[a..h]>\d/, :g);
    if @matches {
      return void0
    }
    return PAWN;
  }
  $pieceType .= lc;
  if $pieceType eq 'o' {
    return KING
  }
  return $pieceType;
}
sub strippedSan(Str $move) {
  $move
    .subst(/'='/, '')
    .subst(/<[+#]>?<[?!]>*$/, '')
}
sub trimFen($fen) {
  $fen.split(' ').slice(0, 4).join(' ')
}

class Move {
  has (
    $.color,
    $.from,
    $.to,
    $.piece,
    $.captured,
    $.promotion,
    #`{{
    /**
     * @deprecated This field is deprecated and will be removed in version 2.0.0.
     * Please use move descriptor functions instead: `isCapture`, `isPromotion`,
     * `isEnPassant`, `isKingsideCastle`, `isQueensideCastle`, `isCastle`, and
     * `isBigPawn`
     */
    }}
    $.flags,
    $.san,
    $.lan,
    $.before,
    $.after
  );

  method new($chess, %internal) {
    self.bless: :$chess, :%internal
  }
  submethod BUILD(:$chess, :%internal (:$!color, :$!piece, :$from, :$to, :$flags, :$!captured, :$!promotion)) {
    my $fromAlgebraic = algebraic($from);
    my $toAlgebraic   = algebraic($to);
    $!from = $fromAlgebraic;
    $!to   = $toAlgebraic;
    my @moves = $chess!Chess::JS::moves: { :legal };
    $!san = $chess!Chess::JS::moveToSan(%internal, @moves);
    $!lan  = $fromAlgebraic ~ $toAlgebraic;
    $!before = $chess.fen();
    $chess!Chess::JS::makeMove(%internal);
    $!after = $chess.fen();
    $chess!Chess::JS::undoMove;
    $!flags = '';
    for %BITS.keys -> $flag {
      if %BITS{$flag} +& $flags {
	$!flags ~= %FLAGS{$flag}
      }
    }
    if $!promotion {
      $!lan ~= $!promotion
    }
  }
  method isCapture         { $!flags.contains: %FLAGS<CAPTURE>      }
  method isPromotion       { $!flags.contains: %FLAGS<PROMOTION>    }
  method isEnPassant       { $!flags.contains: %FLAGS<EP_CAPTURE>   }
  method isKingsideCastle  { $!flags.contains: %FLAGS<KSIDE_CASTLE> }
  method isQueensideCastle { $!flags.contains: %FLAGS<QSIDE_CASTLE> }
  method isBigPawn         { $!flags.contains: %FLAGS<BIG_PAWN>     }
}

has @!board[128];
has $!turn = WHITE;
has %!header;
has Square %!kings{Color};
has EnPassantSquare $!epSquare;
has $!halfMoves = 0;
has $!moveNumber = 0;
has @!history;
has %!comments;
has %!castling = (WHITE) => 0, (BLACK) => 0;
# tracks number of times a position has been seen for repetition checking
has %!positionCount;

method new($fen = DEFAULT-POSITION, % (:$skipValidation = False) = {}) {
  self.bless(:$fen, :$skipValidation);
}
submethod TWEAK(:$fen, :$skipValidation) {
  self.load: $fen, { :$skipValidation };
}

method clear( % (:$preserveHeaders = False) = {}) {
  @!board = Array.new: :shape(128);
  %!kings{$_}:delete for %!kings.keys;
  $!turn = WHITE;
  %!castling = (WHITE) => 0, (BLACK) => 0;
  $!epSquare = EnPassantSquare;
  $!halfMoves = 0;
  $!moveNumber = 1;
  @!history = [];
  %!comments := {};
  %!header := {} unless $preserveHeaders;
  %!positionCount := {};
  %!header<SetUp>:delete;
  %!header<FEN>:delete;
}
method load($fen, % (:$skipValidation = False, :$preserveHeaders = True) = {}) {
  my @tokens = $fen.words;
  if 2 ≤ @tokens < 6 {
    return samewith (|@tokens, |<- - 0 1>.slice(-(6 - @tokens))).join(" "),
      { :$skipValidation, :$preserveHeaders }
      ;
  }
  @tokens = $fen.words;
  unless $skipValidation {
    try validateFen $fen;
    fail $! if $!;
  }
  my $position = @tokens[0];
  my $square = 0;
  self.clear({ :preserveHeaders });
  loop (my $i = 0; $i < $position.chars; $i++) {
    my \piece = $position.substr($i, 1);
    if piece eq '/' {
      $square += 8;
    } elsif isDigit(piece) {
      $square += piece;
    } else {
      my $color = piece lt 'a' ?? WHITE !! BLACK;
      self.put: { type => piece.lc, :$color }, algebraic($square);
      $square++;
    }
  }
  $!turn = %(w => WHITE, b => BLACK){@tokens[1]};
  given @tokens[2] {
    when /K/ { %!castling{WHITE} +|= %BITS<KSIDE_CASTLE>; proceed }
    when /Q/ { %!castling{WHITE} +|= %BITS<QSIDE_CASTLE>; proceed }
    when /k/ { %!castling{BLACK} +|= %BITS<KSIDE_CASTLE>; proceed }
    when /q/ { %!castling{BLACK} +|= %BITS<QSIDE_CASTLE>;         }
  }
  $!epSquare = @tokens[3] eq '-' ?? EnPassantSquare !! %Ox88{@tokens[3]};
  $!halfMoves = @tokens[4].Int;
  $!moveNumber = @tokens[5].Int;
  self!updateSetup($fen);
  self!incPositionCount($fen);
}
method fen {
  my $empty = 0;
  my $fen = '';
  loop (my $i = %Ox88<a8>; $i ≤ %Ox88<h1>; $i++) {
    if @!board[$i] {
      if $empty > 0 {
	$fen ~= $empty;
	$empty = 0;
      }
      my (\color, \piece) = @!board[$i]<color type>;
      $fen ~= (color eq WHITE ?? *.uc !! *.lc)(piece);
    } else {
      $empty++
    }
    if ($i + 1) +& 136 {
      if $empty > 0 {
	$fen ~= $empty;
      }
      if $i !== %Ox88<h1> {
	$fen ~= '/';
      }
      $empty = 0;
      $i += 8;
    }
  }
  my $turn = %( (WHITE) => 'w', (BLACK) => 'b' ){$!turn};
  my $castling = '';
  if %!castling{WHITE} +& %BITS<KSIDE_CASTLE> {
    $castling ~= 'K'
  }
  if %!castling{WHITE} +& %BITS<QSIDE_CASTLE> {
    $castling ~= 'Q'
  }
  if %!castling{BLACK} +& %BITS<KSIDE_CASTLE> {
    $castling ~= 'k'
  }
  if %!castling{BLACK} +& %BITS<QSIDE_CASTLE> {
    $castling ~= 'q'
  }
  $castling ||= '-';
  my $epSquare = '-';
  if $!epSquare.defined {
    my \bigPawnSquare = $!epSquare + ($!turn eq WHITE ?? +16 !! -16);
    my \squares = [bigPawnSquare + 1, bigPawnSquare - 1];
    for squares -> \square {
      if square +& 136 {
	next;
      }
      my $color = $!turn;
      if @!board[square] && @!board[square]<color> eq $color && @!board[square]<type> eq PAWN {
	self!makeMove: {
	  :$color,
	  :from(square),
	  :to($!epSquare),
	  :piece(PAWN),
	  :captured(PAWN),
	  :flags(%BITS<EP_CAPTURE>)
	};
	my \isLegal = !self!isKingAttacked($color);
	self!undoMove();
	if isLegal {
	  $epSquare = algebraic($!epSquare);
	  last;
	}
      }
    }
  }
  return [
    $fen,
    $turn,
    $castling,
    $epSquare,
    $!halfMoves,
    $!moveNumber
  ].join(' ');
}
#`[[[
/*
 * Called when the initial board setup is changed with put() or remove().
 * modifies the SetUp and FEN properties of the header object. If the FEN
 * is equal to the default position, the SetUp and FEN are deleted the setup
 * is only updated if history.length is zero, ie moves haven't been made.
 */
]]]
method !updateSetup($fen) {
  if @!history.elems > 0 { return }
  if $fen ne DEFAULT-POSITION {
    %!header<SetUp> = "1";
    %!header<FEN>   = $fen;
  } else {
    %!header<SetUp>:delete;
    %!header<FEN>  :delete;
  }
}
method reset { 
  self.load(DEFAULT-POSITION);
}
method get(\square) {
  @!board[%Ox88{square}];
}
method put(% (:$type, :$color), \square) {
  if self!put({ :$type, :$color }, square) {
    self!updateCastlingRights;
    self!updateEnPassantSquare;
    self!updateSetup(self.fen);
    return True;
  }
  return False;
}
method !put(% (:$type, :$color), \square) {
  unless SYMBOLS.match($type.lc) {
    return False;
  }
  unless %Ox88{square}:exists {
    return False;
  }
  my $sq = %Ox88{square};
  if $type eq KING && !(%!kings{$color}:!exists || %!kings{$color} == $sq) {
    return False;
  }
  my \currentPieceOnSquare = @!board[$sq];
  if currentPieceOnSquare && currentPieceOnSquare<type> eq KING {
    %!kings{currentPieceOnSquare<color>}:delete;
  }
  @!board[$sq] = { :$type, :$color };
  if $type eq KING {
    %!kings{$color} = $sq;
  }
  return True;
}
method remove(\square) {
  my $piece = self.get(square);
  @!board[square]:delete;
  if $piece && $piece<type> eq KING {
    %!kings{$piece<color>}:delete;
  }
  self!updateCastlingRights();
  self!updateEnPassantSquare();
  self!updateSetup(self.fen());
  return $piece
}
method !updateCastlingRights {
  my \whiteKingInPlace = .defined && .<type> eq KING && .<color> eq WHITE given @!board[%Ox88<e1>];
  my \blackKingInPlace = .defined && .<type> eq KING && .<color> eq BLACK given @!board[%Ox88<e8>];
  if !whiteKingInPlace || (@!board[%Ox88<a1>]<type> // '') ne ROOK || @!board[%Ox88<a1>]<color> ne WHITE {
    %!castling{WHITE} +&= +^%BITS<QSIDE_CASTLE>;
  }
  if !whiteKingInPlace || (@!board[%Ox88<h1>]<type> // '') ne ROOK || @!board[%Ox88<h1>]<color> ne WHITE {
    %!castling{WHITE} +&= +^%BITS<KSIDE_CASTLE>;
  }
  if !blackKingInPlace || (@!board[%Ox88<a8>]<type> // '') ne ROOK || @!board[%Ox88<a8>]<color> ne BLACK {
    %!castling{BLACK} +&= +^%BITS<QSIDE_CASTLE>;
  }
  if !blackKingInPlace || (@!board[%Ox88<h8>]<type> // '') ne ROOK || @!board[%Ox88<h8>]<color> ne BLACK {
    %!castling{BLACK} +&= +^%BITS<KSIDE_CASTLE>;
  }
}
method !updateEnPassantSquare {
  unless $!epSquare.defined {
    return
  }
  my \startSquare = $!epSquare + ($!turn eq WHITE ?? -16 !! 16);
  my \currentSquare = $!epSquare + ($!turn eq WHITE ?? -16 !! 16);
  my \attackers = [currentSquare + 1, currentSquare - 1];
  if @!board[startSquare] || @!board[$!epSquare] || @!board[currentSquare]<color> ne swapColor($!turn) || @!board[currentSquare]<type>  ne PAWN {
    $!epSquare = EnPassantSquare;
    return;
  }
  my \canCapture = -> \square { !(square +& 136) && @!board[square]<color> eq $!turn && @!board[square]<type> eq PAWN };
  if attackers.grep(canCapture) {
    $!epSquare = EnPassantSquare
  }
}
method !attacked(\color, \square, Bool :$verbose) {
  my @attackers;
  loop (my $i = %Ox88<a8>; $i ≤ %Ox88<h1> ; $i++) {
    if $i +& 136 {
      $i += 7;
      next;
    }
    if !@!board[$i] || (@!board[$i]<color> // '') ne color {
      next;
    }
    my $piece = @!board[$i];
    die "square `{square}` is not valid" unless square ~~ Square;
    my \difference = $i - square;
    if difference == 0 {
      next;
    }
    my \index = difference + 119;
    if $ATTACKS[index] +& %PIECE_MASKS{$piece<type>} {
      if $piece<type> eq PAWN {
	if difference > 0 && $piece<color> eq WHITE || difference ≤ 0 && $piece<color> eq BLACK {
	  if !$verbose {
	    return True;
	  } else {
	    @attackers.push: algebraic($i)
	  }
	}
	next;
      }
      if $piece<type> eq 'n'|'k' {
	if !$verbose {
	  return True;
	} else {
	  @attackers.push: algebraic($i);
	  next;
	}
      }
      my \offset = $RAYS[index];
      my $j = $i + offset;
      my $blocked = False;
      while $j !== square {
	if @!board[$j] {
	  $blocked = True;
	  last;
	}
	$j += offset;
      }
      if !$blocked {
	if !$verbose {
	  return True;
	} else {
	  @attackers.push: algebraic($i);
	  next;
	}
      }
    }
  }
  if $verbose {
    return @attackers;
  } else {
    return False;
  }
}
method attackers($square, $attackedBy?) {
  if !$attackedBy.defined {
    return self!attacked($!turn, %Ox88{$square}, :verbose);
  } else {
    return self!attacked($attackedBy, %Ox88{$square}, :verbose);
  }
}
method !isKingAttacked($color) returns Bool {
  %!kings{$color}:!exists ?? False !! self!attacked(swapColor($color), %!kings{$color});
}
method isAttacked($square, \attackedBy) returns Bool {
  self!attacked(attackedBy, %Ox88{$square})
}
method isCheck {
  self!isKingAttacked($!turn);
}
method inCheck {
  self.isCheck();
}
method isCheckmate {
  self.isCheck() && self!moves().elems == 0
}
method isStalemate {
  !self.isCheck && self!moves().elems == 0
}
method isInsufficientMaterial {
  my %pieces = 
    b => 0,
    n => 0,
    r => 0,
    q => 0,
    k => 0,
    p => 0
  ;
  my @bishops;
  my UInt $numPieces = 0;
  my $squareColor = 0;
  loop (my $i = %Ox88<a8>; $i ≤ %Ox88<h1>; $i++) {
    $squareColor = ($squareColor + 1) % 2;
    if $i +& 136 {
      $i += 7;
      next;
    }
    my $piece = @!board[$i];
    if $piece {
      %pieces{$piece<type>} = %pieces{$piece<type>}:exists ?? %pieces{$piece<type>} + 1 !! 1;
      if $piece<type> eq BISHOP {
	@bishops.push($squareColor);
      }
      $numPieces++;
    }
  }
  if $numPieces == 2 {
    return True;
  } elsif 
  # k vs. kn .... or .... k vs. kb
  $numPieces == 3 && (%pieces{BISH