### ----------------------------------------------------
### -- Chess
### -- Licenses: Artistic-2.0
### -- Authors: Lucien Grondin
### -- File: lib/Chess/JS.rakumod
### ----------------------------------------------------
### -- Chunk 2 of 2
OP} == 1 || %pieces{KNIGHT} == 1)
  {
    return True;
  } elsif $numPieces == %pieces{BISHOP} + 2 {
    my $sum = 0;
    my \len = @bishops.elems;
    loop (my $i = 0; $i < len; $i++) {
      $sum += @bishops[$i]
    }
    if $sum == 0|len {
      return True
    }
  }
  return False;
}
method isThreefoldRepetition {
  self!getPositionCount(self.fen()) ≥ 3
}
method isDrawByFiftyMoves {
  $!halfMoves ≥ 100
}
method isDraw {
  self.isDrawByFiftyMoves || self.isStalemate || self.isInsufficientMaterial || self.isThreefoldRepetition
}
method isGameOver {
  self.isCheckmate || self.isStalemate || self.isDraw
}
method moves(% (Bool :$verbose, :$square, :$piece) = {}) {
  my @moves = self!moves({ :$square, :$piece });
  if $verbose {
    return @moves.map( -> \move { Move.new(self, move) } );
  } else {
    return @moves.map( -> \move { self!moveToSan(move, @moves) } )
  }
}
method !moves(% (Bool :$legal = True, :$piece, :$square) = {}) {
  my \forSquare = $square ?? $square.toLowerCase !! void0;
  my \forPiece = $piece ?? $piece.lc !! $piece<>;
  my @moves;
  my $us = $!turn;
  my $them = swapColor($us);
  my $firstSquare = %Ox88<a8>;
  my $lastSquare = %Ox88<h1>;
  my $singleSquare = False;
  if forSquare {
    if %Ox88{forSquare}:!exists {
      return []
    } else {
      $firstSquare = $lastSquare = %Ox88{forSquare};
      $singleSquare = True;
    }
  }
  loop (my $from = $firstSquare; $from ≤ $lastSquare; $from++) {
    if $from +& 136 {
      $from += 7;
      next;
    }
    if !@!board[$from] || @!board[$from]<color> eq $them {
      next;
    }
    my \type = @!board[$from]<type><>;
    my $to;
    if type eq PAWN {
      next if forPiece && forPiece ne type;
	
      $to = $from + %PAWN_OFFSETS{$us}[0];
      if !@!board[$to] {
	addMove(@moves, $us, $from, $to, PAWN);
	$to = $from + %PAWN_OFFSETS{$us}[1];
	if %SECOND_RANK{$us} == rank($from) && !@!board[$to] {
	  addMove(@moves, $us, $from, $to, PAWN, void0, %BITS<BIG_PAWN>);
	}
      }
      loop (my $j = 2; $j < 4; $j++) {
	$to = $from + %PAWN_OFFSETS{$us}[$j];
	next if $to +& 136;

	if (@!board[$to]<color> // '') eq $them {
	  addMove(@moves, $us, $from, $to, PAWN, @!board[$to]<type>, %BITS<CAPTURE>);
	} elsif $!epSquare.defined && $to == $!epSquare {
	  addMove(@moves, $us, $from, $to, PAWN, PAWN, %BITS<EP_CAPTURE>);
	}
      }
    } else {
      if forPiece && forPiece ne type {
	next; }
      loop (my ($j, \len) = 0, %PIECE_OFFSETS{type}.elems; $j < len; $j++) {
	my \offset = %PIECE_OFFSETS{type}[$j];
	$to = $from;
	loop {
	  $to += offset;
	  last if $to +& 136;

	  if !@!board[$to] {
	    addMove(@moves, $us, $from, $to, type);
	  } else {
	    last if @!board[$to]<color> eq $us;

	    addMove(@moves, $us, $from, $to, type, @!board[$to]<type>, %BITS<CAPTURE>);
	    last;
	  }
	  if type eq KNIGHT|KING {
	    last;
	  }
	}
      }
    }
  }
  if !forPiece.defined  || forPiece eq KING {
    if !$singleSquare || $lastSquare == %!kings{$us} {
      if %!castling{$us} +& %BITS<KSIDE_CASTLE> {
	my \castlingFrom = %!kings{$us}<>;
	my \castlingTo = castlingFrom + 2;
	if !@!board[castlingFrom + 1] &&
	  !@!board[castlingTo] &&
	  !self!attacked($them, %!kings{$us}) &&
	  !self!attacked($them, castlingFrom + 1)
	  && !self!attacked($them, castlingTo) {
	  addMove(@moves, $us, %!kings{$us}, castlingTo, KING, void0, %BITS<KSIDE_CASTLE>);
	}
      }
      if %!castling{$us} +& %BITS<QSIDE_CASTLE> {
	my \castlingFrom = %!kings{$us};
	my \castlingTo = castlingFrom - 2;
	if !@!board[castlingFrom - 1] && !@!board[castlingFrom - 2] && !@!board[castlingFrom - 3] && !self!attacked($them, %!kings{$us}) && !self!attacked($them, castlingFrom - 1) && !self!attacked($them, castlingTo) {
	  addMove(@moves, $us, %!kings{$us}, castlingTo, KING, void0, %BITS<QSIDE_CASTLE>);
	}
      }
    }
  }
  if !$legal || (%!kings{$us}:!exists) {
    return @moves;
  } else {
    return gather for @moves -> $move {
      self!makeMove($move);
      take $move unless self!isKingAttacked($us);
      self!undoMove;
    }
  }
}
method move($move, % (:$strict = False) = {}) {
  my %moveObj;
  if $move ~~ Str {
    %moveObj := self!moveFromSan($move, $strict);
  } elsif $move ~~ Hash {
    my @moves = self!moves();
    loop (my ($i, \len) = 0, @moves.elems; $i < len; $i++) {
      if $move<from> eq algebraic(@moves[$i]<from>) && $move<to> eq algebraic(@moves[$i]<to>) && (!@moves[$i]<promotion>:exists || $move<promotion> eq @moves[$i]<promotion>) {
	%moveObj := @moves[$i];
	last;
      }
    }
  }
  if !%moveObj {
    if $move ~~ Str {
      die "Invalid move: $move";
    } else {
      die "Invalid move: {$move.raku}";
    }
  }
  my $prettyMove = Move.new(self, %moveObj);
  self!makeMove(%moveObj);
  self!incPositionCount($prettyMove.after);
  return $prettyMove;
}
method !push($move) {
  @!history.push: {
    :$move,
    :kings(%!kings.clone),
    :$!turn,
    :castling(%!castling.clone),
    :$!epSquare,
    :$!halfMoves,
    :$!moveNumber
  }
}
method !makeMove($move) {
  my $us = $!turn;
  my $them = swapColor($us);
  self!push($move);
  @!board[$move<to>] = @!board[$move<from>];
  @!board[$move<from>]:delete;
  if $move<flags> +& %BITS<EP_CAPTURE> {
    if $!turn eq BLACK {
      @!board[$move<to> - 16]:delete;
    } else {
      @!board[$move<to> + 16]:delete;
    }
  }
  if $move<promotion> {
    @!board[$move<to>] = { type => $move<promotion>, color => $us }
  }
  if (@!board[$move<to>]<type> // '') eq KING {
    %!kings{$us} = $move<to>;
    if $move<flags> +& %BITS<KSIDE_CASTLE> {
      my \castlingTo = $move<to> - 1;
      my \castlingFrom = $move<to> + 1;
      @!board[castlingTo] = @!board[castlingFrom];
      @!board[castlingFrom]:delete;
    } elsif $move<flags> +& %BITS<QSIDE_CASTLE> {
      my \castlingTo = $move<to> + 1;
      my \castlingFrom = $move<to> - 2;
      @!board[castlingTo] = @!board[castlingFrom];
      @!board[castlingFrom]:delete;
    }
    %!castling{$us} = 0;
  }
  if %!castling{$us} {
    loop (my ($i, \len) = 0, %ROOKS{$us}.elems; $i < len; $i++) {
      if $move<from> == %ROOKS{$us}[$i]<square> && %!castling{$us} +& %ROOKS{$us}[$i]<flag> {
	%!castling{$us} +^= %ROOKS{$us}[$i]<flag>;
	last;
      }
    }
  }
  if %!castling{$them} {
    loop (my ($i, \len) = 0, %ROOKS{$them}.elems; $i < len; $i++) {
      if $move<to> == %ROOKS{$them}[$i]<square> && %!castling{$them} +& %ROOKS{$them}[$i]<flag> {
	%!castling{$them} +^= %ROOKS{$them}[$i]<flag>;
	last;
      }
    }
  }
  if $move<flags> +& %BITS<BIG_PAWN> {
    if $us eq BLACK {
      $!epSquare = $move<to> - 16;
    } else {
      $!epSquare = $move<to> + 16;
    }
  } else {
    $!epSquare = EnPassantSquare
  }
  if $move<piece> eq PAWN {
    $!halfMoves = 0;
  } elsif $move<flags> +& (%BITS<CAPTURE> +| %BITS<EP_CAPTURE>) {
    $!halfMoves = 0;
  } else {
    $!halfMoves++;
  }
  if $us eq BLACK {
    $!moveNumber++;
  }
  $!turn = $them;
}
method undo {
  my \move = self!undoMove;
  if \move {
    my \prettyMove = Move.new(self, move);
    self!decPositionCount(prettyMove.after);
    return prettyMove;
  }
  return Nil
}
method !undoMove {
  my \old = @!history.pop;
  if !old.defined {
    return Nil
  }
  my \move = old<move>;
  %!kings{$_}:delete for %!kings.keys;
  %!kings = old<kings><>;
  $!turn  = old<turn>;
  %!castling := old<castling>;
  $!epSquare = old<epSquare>;
  $!halfMoves = old<halfMoves>;
  $!moveNumber = old<moveNumber>;
  my \us = $!turn;
  my \them = swapColor(us);
  @!board[move<from>] = @!board[move<to>];
  @!board[move<from>]<type> = move<piece>;
  @!board[move<to>]:delete;
  if move<captured> {
    if move<flags> +& %BITS<EP_CAPTURE> {
      my $index;
      if us eq BLACK {
	$index = move<to> - 16;
      } else {
	$index = move<to> + 16;
      }
      @!board[$index] = { type => PAWN, color => them };
    } else {
      @!board[move<to>] = { type => move<captured>, color => them };
    }
  }
  if move<flags> +& (%BITS<KSIDE_CASTLE> +| %BITS<QSIDE_CASTLE>) {
    my ($castlingTo, $castlingFrom);
    if move<flags> +& %BITS<KSIDE_CASTLE> {
      $castlingTo = move<to> + 1;
      $castlingFrom = move<to> - 1;
    } else {
      $castlingTo = move<to> - 2;
      $castlingFrom = move<to> + 1;
    }
    @!board[$castlingTo] = @!board[$castlingFrom]:delete;

  }
  return move;
}
method pgn(% (:$newline = "\n", :$maxWidth = 0) = {}) {
  my \result = [];
  my $headerExists = False;
  for %!header.keys -> $key {
    result.push: qq{[$key "%!header{$key}"]$newline};
    FIRST $headerExists = True;
  }
  if $headerExists && @!history.elems {
    result.push: $newline;
  }
  my &appendComment = sub ($moveString2 is rw) {
    my $comment = %!comments{self.fen};
    if $comment.defined {
      my $delimiter = $moveString2.chars > 0 ?? ' ' !! '';
      $moveString2 = "$moveString2$delimiter\{$comment\}";
    }
    return $moveString2;
  }
  my \reversedHistory = [];
  while @!history.elems > 0 {
    reversedHistory.push: self!undoMove;
  }
  my \moves = [];
  my $moveString = '';
  if reversedHistory.elems == 0 {
    moves.push: &appendComment('');
  }
  while reversedHistory.elems > 0 {
    $moveString = appendComment($moveString);
    my $move = reversedHistory.pop;
    if !$move {
      last;
    }
    if !@!history.elems && $move<color> eq BLACK {
      my $prefix = "$!moveNumber...";
      $moveString = $moveString eq '' ?? "$moveString $prefix" !! $prefix;
    } elsif $move<color> eq WHITE {
      if $moveString.chars {
	moves.push: $moveString;
      }
      $moveString = $!moveNumber ~ '.';
    }
    $moveString ~= ' ' ~ self!moveToSan($move, self!moves({ :legal }));
    self!makeMove($move);
  }
  if $moveString.chars {
    moves.push(appendComment($moveString));
  }
  if %!header<Result>:exists {
    moves.push: %!header<Result>;
  }
  if $maxWidth == 0 {
    return result.join('') ~ moves.join(' ');
  }
  my &strip = sub {
    if result.elems > 0 && result.tail eq ' ' {
      result.pop();
      return True;
    }
    return False;
  }
  my &wrapComment = sub ($width, $move) {
    for $move.words -> $token {
      if !$token {
	next;
      }
      if $width + $token.chars > $maxWidth {
	while &strip() {
	  $width--;
	}
	result.push($newline);
	$width = 0;
      }
      result.push($token);
      $width += $token.chars;
      result.push(' ');
      $width++;
    }
    return $width;
  }
  my $currentWidth = 0;
  loop (my $i = 0; $i < moves.elems; $i++) {
    if $currentWidth + moves[$i].chars > $maxWidth {
      if moves[$i] ~~ /'{'/ {
	$currentWidth = wrapComment($currentWidth, moves[$i]);
	next;
      }
    }
    if $currentWidth + moves[$i].chars > $maxWidth && $i !== 0 {
      if result.tail eq ' ' {
	result.pop;
      }
      result.push($newline);
      $currentWidth = 0;
    } elsif $i !== 0 {
      result.push(' ');
      $currentWidth++;
    }
    result.push(moves[$i]);
    $currentWidth += moves[$i].chars;
  }
  return result.join('');
}
# deprecated Use `setHeader` and `getHeaders` instead
method header(*@args) {
  loop (my $i = 0; $i < @args.elems; $i += 2) {
    if @args[$i] ~~ Str && @args[$i+1] ~~ Str {
      %!header{@args[$i]} = @args[$i + 1];
    }
  }
  return %!header;
}
method setHeader($key, $value) {
  %!header{$key} = $value;
  return %!header;
}
method removeHeader($key) {
  if %!header{$key}:exists {
    %!header{$key}:delete;
    return True;
  }
  return False;
}
method getHeaders {
  %!header
}

method loadPgn($pgn, % (:$strict = False) = {}) {
    use URI::Encode;

    sub parse_pgn_header($header) {
        my %headerObj;
        for $header.lines -> $line { # Split headers with Raku's logical newlines
            next unless $line ~~ /^\s* '[' \s* (<[A..Za..z]>+) \s* '"' (<-["]>*) '"' \s* ']' \s* $/;
            my ($key, $value) = ~$0, ~$1;
            %headerObj{$key} = $value if $key.trim.chars > 0;
        }
        %headerObj;
    }

    return self.loadPgn($pgn.trim, { :$strict }) if $pgn ~~ /\s+$/;
    my $headerRegex = rx/^ (\[ (\n | . )* \]) ( \s* \n ** 2 | \s* \n* $ ) /; # Match headers with logical \n
    my $headerMatch = $pgn ~~ $headerRegex;
    my $headerString = $headerMatch ?? ($headerMatch[0] // '') !! '';

    self.reset;
    my %headers = parse_pgn_header($headerString);
    my $fen = '';
    for %headers {
        $fen = .value if .key eq 'fen'|'FEN';
        self.setHeader(.key, .value);
    }

    if !$strict {
        self.load($fen, :preserveHeaders) if $fen;
    } else {
        if %headers<SetUp>:exists && %headers<SetUp> eq '1' {
            fail "Invalid PGN: FEN tag must be supplied with SetUp tag" unless %headers<FEN>:exists;
            self.load(%headers<FEN>, :preserveHeaders);
        }
    }

    # Comment encoding/decoding helper functions
    sub toHex($s) {
        $s.comb.map({ .ord < 128 ?? .ord.fmt('%02x') !! .encode('utf-8').map(*.fmt('%02x')).join }).join
    }

    sub fromHex($s) {
        return '' unless $s.chars;
        my $encoded = '%' ~ $s.comb(/.. | . /).join('%');
        uri_decode($encoded); # Use URI::Encode inside the method
    }

    sub encodeComment($s) {
        my $cleaned = $s.subst(/\n/, ' ', :g); # Replace logical \n with spaces
        '{' ~ toHex($cleaned.substr(1, *-1)) ~ '}';
    }

    sub decodeComment($s) {
        return fromHex($s.substr(1, *-1)) if $s.starts-with('{') && $s.ends-with('}');
    }

    # Process moves, mimicking chess.js
    #
    my $ms = $pgn.substr($headerString.chars);

    $ms .= subst(
        rx/ (\{ <-[}]>* \}) | (\; <-[\n]>* ) /, # Capture comments, exclude logical \n
        -> $m { $m[0] ?? encodeComment($m[0]) !! ' ' ~ encodeComment('{' ~ $m[1].substr(1) ~ '}') },
        :g
    );
    $ms .= subst(/\n/, ' ', :g); # Normalize logical \n to spaces
    

    my $ravRegex = rx/ '(' ~ ')' <-[()]>+ /;
    while $ms ~~ $ravRegex {
        $ms .= subst($ravRegex, '');
    }

    $ms .= subst(/\d+ '.' '..'?/, '', :g);
    $ms .= subst(/'... '/, '', :g);
    $ms .= subst(/'\$' \d+/, '', :g);
    my @moves = $ms.trim.words;

    my $result = '';
    for @moves -> $move {
        my $comment = decodeComment($move);
        if $comment.defined {
            %!comments{self.fen} = $comment;
            next;
        }
        try my %move-obj = self!moveFromSan($move, $strict);
        if $! {
            if $move eq any(@TERMINATION_MARKERS) {
                $result = $move;
            } else {
                fail "Invalid move in PGN: $move" if $strict;
                last;
            }
        } else {
            $result = '';
            self!makeMove(%move-obj);
	    note %move-obj.raku unless %move-obj<color>;
            self!incPositionCount(self.fen);
        }
    }

    self.header('Result', $result) if $result && %!header && !%!header<Result>;
    True;
}

#`[[[
/*
 * Convert a move from 0x88 coordinates to Standard Algebraic Notation
 * (SAN)
 *
 * @param {boolean} strict Use the strict SAN parser. It will throw errors
 * on overly disambiguated moves (see below):
 *
 * r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4
 * 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned
 * 4. ... Ne7 is technically the valid SAN
 */
]]]
method !moveToSan($move, @moves) {
  my $output = '';
  if $move<flags> +& %BITS<KSIDE_CASTLE> {
    $output = 'O-O';
  } elsif $move<flags> +& %BITS<QSIDE_CASTLE> {
    $output = 'O-O-O';
  } else {
    if $move<piece> ne PAWN {
      my \disambiguator = getDisambiguator($move, @moves);
      $output ~= $move<piece>.uc ~ disambiguator;
    }
    if $move<flags> +& (%BITS<CAPTURE> +| %BITS<EP_CAPTURE>) {
      if $move<piece> eq PAWN {
	$output ~= algebraic($move<from>).substr(0,1);
      }
      $output ~= 'x';
    }
    $output ~= algebraic($move<to>);
    if $move<promotion> {
      $output ~= '=' ~ $move<promotion>.uc
    }
  }
  self!makeMove($move);
  if self.isCheck {
    if self.isCheckmate {
      $output ~= '#';
    } else {
      $output ~= '+';
    }
  }
  self!undoMove;
  return $output;
}
# convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates
method !moveFromSan($move, $strict = False) {
  my \cleanMove = strippedSan($move);
  my $pieceType = inferPieceType(cleanMove);
  my @moves = self!moves({ :legal, piece => $pieceType });
  loop (my ($i, \len) = 0, @moves.elems; $i < len; $i++) {
    if cleanMove eq strippedSan(self!moveToSan(@moves[$i], @moves)) {
      return @moves[$i];
    }
  }
  if $strict {
    fail "can't parse $move in strict mode";
  }
  my Match $matches;
  my Str (
    $piece,
    $from,
    $to,
    $promotion
  );
  my $overlyDisambiguated = False;
  $matches = cleanMove.match(/(<[pnbrqkPNBRQK]>)?(<[a..h]><[1..8]>)x?\-?(<[a..h]><[1..8]>)(<[qrbnQRBN]>)?/);
  if $matches {
    $piece = ~$matches[0] if $matches[0];
    $from  = ~$matches[1] if $matches[1];
    $to    = ~$matches[2] if $matches[2];
    $promotion = ~$matches[3] if $matches[3];
    if $from.chars == 1 {
      $overlyDisambiguated = True;
    }
  } else {
    $matches = cleanMove.match(/(<[pnbrqkPNBRQK]>)?(<[a..h]>?<[1..8]>?)x?\-?(<[a..h]><[1..8]>)(<[qrbnQRBN]>)?/);
    if $matches {
      $piece = ~$matches[0] if $matches[0];
      $from  = ~$matches[1] if $matches[1];
      $to    = ~$matches[2] if $matches[2];
      $promotion = ~$matches[3] if $matches[3];
      if $from.chars == 1 {
	$overlyDisambiguated = True;
      }
    }
  }
  $pieceType = inferPieceType(cleanMove);
  @moves = self!moves: {
    :legal,
    :piece( $piece ?? $piece !! $pieceType )
  };
  if !$to {
    fail "could not parse $move (no destination square)";
  }
  {
    loop (my ($i, \len) = 0, @moves.elems; $i < len; $i++) {
      if !$from {
	if cleanMove eq strippedSan(self!moveToSan(@moves[$i], @moves)).subst(/x/, '') {
	  return @moves[$i];
	}
      } elsif (!$piece || $piece.lc eq @moves[$i]<piece>) && %Ox88{$from} == @moves[$i]<from> && %Ox88{$to} == @moves[$i]<to> && (!$promotion || $promotion.lc eq @moves[$i]<promotion>) {
	return @moves[$i];
      } elsif $overlyDisambiguated {
	my \square = algebraic(@moves[$i]<from>);
	if (!$piece || $piece.lc == @moves[$i]<piece>) && %Ox88{$to} == @moves[$i]<to> && ($from == square[0] || $from == square[1]) && (!$promotion || $promotion.lc == @moves[$i]<promotion>) {
	  return @moves[$i];
	}
      }
    }
  }
  fail "could not parse $move";
}
method ascii {
  my $s = "   +------------------------+\n";
  loop (my $i = %Ox88<a8>; $i ≤ %Ox88<h1>; $i++) {
    if file($i) == 0 {
      $s ~= " " ~ [1..8].reverse[rank($i)] ~ " |";
    }
    if @!board[$i] {
      my $piece = @!board[$i]<type>;
      my $color = @!board[$i]<color>;
      my $symbol = $color eq WHITE ?? $piece.uc !! $piece.lc;
      $s ~= " $symbol ";
    } else {
      $s ~= " . ";
    }
    if ($i + 1) +& 136 {
      $s ~= "|\n";
      $i += 8;
    }
  }
  $s ~= "   +------------------------+\n";
  $s ~= "     a  b  c  d  e  f  g  h";
  return $s;
}
method perft($depth) {
  my @moves = self!moves({ :!legal });
  my $nodes = 0;
  my $color = $!turn;
  loop (my ($i, \len) = 0, @moves.elems; $i < len; $i++) {
    self!makeMove(@moves[$i]);
    if !self!isKingAttacked($color) {
      if $depth - 1 > 0 {
	$nodes += self.perft: $depth - 1;
      } else {
	$nodes++;
      }
    }
    self!undoMove();
  }
  return $nodes;
}

method board {
  gather {
    my @row;
    loop (my $i = %Ox88<a8>; $i ≤ %Ox88<h1>; $i++) {
      if !@!board[$i].defined {
	@row.push(Nil)
      } else {
	@row.push: %(
	  :square(algebraic($i)),
	  :type(@!board[$i]<type>),
	  :color(@!board[$i]<color>)
	)
      }
      if ($i + 1) +& 136 {
	take @row;
	@row := [];
	$i += 8
      }
    }
  }
}
method squareColor($square) {
  if %Ox88{$square}:exists {
    my $sq = %Ox88{$square};
    return (rank($sq) + file($sq)) % 2 == 0 ?? 'light' !! 'dark';
  }
}
method history(% (:$verbose = False) = {}) {
  my \reversedHistory = [];
  my \moveHistory = [];
  while @!history.elems > 0 {
    reversedHistory.push(self!undoMove);
  }
  loop {
    my \move = reversedHistory.pop;
    if !move {
      last;
    }
    if $verbose {
      moveHistory.push: Move.new(self, move);
    } else {
      moveHistory.push: self!moveToSan(move, self!moves)
    }
    self!makeMove(move);
  }
  return moveHistory;
}
#`[[[
/*
 * Keeps track of position occurrence counts for the purpose of repetition
 * checking. All three methods (`_inc`, `_dec`, and `_get`) trim the
 * irrelevent information from the fen, initialising new positions, and
 * removing old positions from the record if their counts are reduced to 0.
 */
]]]
method !getPositionCount($fen) {
  my \trimmedFen = trimFen($fen);
  %!positionCount{trimmedFen} // 0;
}
method !incPositionCount($fen) {
  my \trimmedFen = trimFen($fen);
  if %!positionCount{trimmedFen} === void0 {
    %!positionCount{trimmedFen} = 0
  }
  %!positionCount{trimmedFen} += 1;
}
method !decPositionCount($fen) {
  my \trimmedFen = trimFen($fen);
  if %!positionCount{trimmedFen} == 1 {
    %!positionCount{trimmedFen}:delete
  } else {
    %!positionCount{trimmedFen} -= 1;
  }
}
method !pruneComments {
  my @reversedHistory;
  my %currentComments;
  sub copyComments($fen) {
    if %!comments{$fen}:exists {
      %currentComments{$fen} = %!comments{$fen};
    }
  }
  while @!history {
    @reversedHistory.push: self!undoMove;
  }
  copyComments(self.fen);
  loop {
    last unless my $move = @reversedHistory.pop();
    self!makeMove($move);
    copyComments(self.fen);
  }
  %!comments := %currentComments
}
method getComment { %!comments{self.fen} }
method setComment($comment) {
  %!comments{self.fen} = $comment.trans: '{}' => '[]'
}
method removeComment { %!comments{self.fen}:delete }
method getComments {
  self!pruneComments;
  %!comments.map: { %( fen => .key, comment => .value ) }
}
method setCastlingRights($color, %rights) {
  for KING, QUEEN -> $side {
    if %rights{$side}:exists {
      if %rights{$side} {
	%!castling{$color} +|= %SIDES{$side};
      } else {
	%!castling{$color} +&= +^%SIDES{$side};
      }
    }
  }
  self!updateCastlingRights;
  my %result = self.getCastlingRights($color);
  (!%rights{KING}.defined || %rights{KING} == %result{KING}) &&
  (!%rights{QUEEN}.defined || %rights{QUEEN} == %result{QUEEN})
}
method getCastlingRights($color) {
  %(
    (KING) => %!castling{$color} +& %SIDES{KING} !== 0,
    (QUEEN) => %!castling{$color} +& %SIDES{QUEEN} !== 0,
  )
}



# vi: shiftwidth=2 nu nowrap


