generatePieceMoves method

List<Move> generatePieceMoves(
  1. int square, [
  2. MoveGenParams options = MoveGenParams.normal
])

Generates all moves for the piece on square in accordance with options.

Implementation

List<Move> generatePieceMoves(
  int square, [
  MoveGenParams options = MoveGenParams.normal,
]) {
  Square piece = board[square];
  if (piece.isEmpty) return [];
  Colour colour = piece.colour;
  int dirMult = Bishop.playerDirection[piece.colour];

  PieceType pieceType = variant.pieceType(piece, square);
  if (options.onlySquare != null &&
      pieceType.optimisationData != null &&
      pieceType.optimisationData!
          .excludePiece(square, options.onlySquare!, size)) {
    return const [];
  }

  List<Move> moves = [];
  int from = square;
  bool exit = false;

  void generateTeleportMoves(TeleportMoveDefinition md) {
    for (int to = 0; to < size.numIndices; to++) {
      if (!size.onBoard(square)) {
        to += size.h;
        if (!size.onBoard(square)) break;
      }
      if (to == square) continue;
      // todo: md.firstMove, when better first move logic is built
      if (options.ignorePieces) {
        moves.add(StandardMove(from: from, to: to));
        continue;
      }
      Square target = board[to];
      if (target.isEmpty && md.quiet) {
        moves.add(StandardMove(from: from, to: to));
        continue;
      }
      if (target.isNotEmpty && target.colour != colour && md.capture) {
        final targetPieceType = variant.pieceType(target, to);
        if (targetPieceType.royal) {
          // doesn't make sense for teleports to be able to capture royals
          // unless we later implement region restricted teleport moves
          continue;
        }
        moves.add(StandardMove(from: from, to: to, capturedPiece: target));
      }
    }
  }

  void addMove(StandardMove m) {
    final mm = variant.generatePromotionMoves(
      base: m,
      state: state,
      pieceType: pieceType,
    );
    if (mm != null) moves.addAll(mm);
    if (mm == null) moves.add(m);
    if (variant.gating) {
      int gRank = size.rank(m.from);
      if ((gRank == Bishop.rank1 && colour == Bishop.white) ||
          (gRank == size.maxRank && colour == Bishop.black)) {
        final gatingMoves = generateGatingMoves(m);
        moves.addAll(gatingMoves);
        if (gatingMoves.isNotEmpty &&
            variant.gatingMode == GatingMode.fixed) {
          moves.remove(m);
        }
      }
    }
    if (options.onlySquare != null && m.to == options.onlySquare) {
      exit = true;
    }
  }

  // Generate normal moves
  for (MoveDefinition md in pieceType.moves) {
    if (exit) break;
    if (!md.capture && !options.quiet) continue;
    if (!md.quiet && !options.captures) continue;
    if (options.onlySquare != null &&
        md.excludeMove(square, options.onlySquare!, dirMult, size)) {
      continue;
    }
    if (md is TeleportMoveDefinition) {
      generateTeleportMoves(md);
      continue;
    }

    if (md.firstOnly &&
        !variant.canFirstMove(
          state: state,
          from: from,
          colour: colour,
          moveDefinition: md,
        )) {
      continue;
    }

    if (md is! StandardMoveDefinition) continue;
    int range = md.range == 0 ? variant.boardSize.maxDim : md.range;
    int squaresSinceHop = -1;

    for (int i = 0; i < range; i++) {
      if (exit) break;
      int to = square + md.normalised * (i + 1) * dirMult;
      if (!size.onBoard(to)) break;
      if (variant.hasRegions) {
        if (!variant.allowMovement(piece, to)) break;
      }
      if (md.lame) {
        int fromLame = from + md.normalised * i * dirMult;
        int blockSq = fromLame + md.lameNormalised! * dirMult;
        if (board[blockSq].isNotEmpty && !options.ignorePieces) break;
      }

      Square target = board[to];
      bool setEnPassant =
          variant.enPassant && md.firstOnly && pieceType.enPassantable;

      if (md.hopper) {
        if (target.isEmpty) {
          if (squaresSinceHop == -1) continue;
          squaresSinceHop++;
          if (md.limitedHopper) {
            if (squaresSinceHop > md.hopDistance) {
              break;
            }
            if (squaresSinceHop != md.hopDistance) {
              continue;
            }
          }
        } else {
          squaresSinceHop++;
          if (squaresSinceHop == 0) continue;
          if (md.limitedHopper && squaresSinceHop != md.hopDistance) {
            break;
          }
        }
      }

      if (target.isEmpty) {
        // TODO: prioritise ep? for moves that could be both ep and quiet
        if (md.quiet) {
          if (!options.quiet && options.onlySquare == null) continue;
          StandardMove m =
              StandardMove(to: to, from: from, setEnPassant: setEnPassant);
          addMove(m);
        } else if (variant.enPassant &&
            md.enPassant &&
            (state.epSquare == to || options.ignorePieces) &&
            options.captures) {
          // en passant
          StandardMove m = StandardMove(
            to: to,
            from: from,
            capturedPiece: makePiece(variant.epPiece, colour.opponent),
            enPassant: true,
            setEnPassant: setEnPassant,
          );
          addMove(m);
        } else if (options.onlySquare != null && to == options.onlySquare) {
          StandardMove m = StandardMove(
            to: to,
            from: from,
          );
          addMove(m);
        } else {
          if (!options.ignorePieces) {
            if (md.slider) {
              continue;
            } else {
              break;
            }
          }
          StandardMove m = StandardMove(from: from, to: to);
          addMove(m);
        }
      } else if (target.colour == colour) {
        if (!options.ignorePieces) break;
        StandardMove m = StandardMove(from: from, to: to);
        addMove(m);
      } else {
        if (md.capture) {
          if (!options.captures) break;
          if (options.onlyPiece && target.type != options.pieceType) break;
          StandardMove m = StandardMove(
            to: to,
            from: from,
            capturedPiece: target,
            setEnPassant: setEnPassant,
          );
          addMove(m);
        } else if (options.ignorePieces) {
          StandardMove m = StandardMove(
            to: to,
            from: from,
            setEnPassant: setEnPassant,
          );
          addMove(m);
        }
        if (!options.ignorePieces) break;
      }
    }
  }

  // Generate castling
  if (variant.castling && options.castling && pieceType.royal && !inCheck) {
    bool kingside = colour == Bishop.white
        ? state.castlingRights.wk
        : state.castlingRights.bk;
    bool queenside = colour == Bishop.white
        ? state.castlingRights.wq
        : state.castlingRights.bq;
    int royalRank = size.rank(from);

    for (int i = 0; i < 2; i++) {
      bool sideCondition = i == 0
          ? (kingside && variant.castlingOptions.kingside)
          : (queenside && variant.castlingOptions.queenside);
      if (!sideCondition) continue;
      // Conditions for castling:
      // * All squares between the king's start and end (inclusive) must be
      // free and not attacked
      // * All squares between the rook and the king's target must be free.
      // * Obviously the king's start is occupied by the king, but it can't
      // be in check
      // * The square the rook lands on must be free (but can be attacked)
      int targetFile = i == 0
          ? variant.castlingOptions.kTarget!
          : variant.castlingOptions.qTarget!;
      int targetSq =
          size.square(targetFile, royalRank); // where the king lands
      int rookFile = i == 0 ? castlingTargetK! : castlingTargetQ!;
      int rookSq = size.square(rookFile, royalRank); // where the rook starts
      int rookTargetFile = i == 0 ? targetFile - 1 : targetFile + 1;
      int rookTargetSq =
          size.square(rookTargetFile, royalRank); // where the rook lands
      // Check rook target square is empty (or occupied by the rook/king already)
      if (board[rookTargetSq].isNotEmpty &&
          rookTargetSq != rookSq &&
          rookTargetSq != from) {
        continue;
      }
      // Check king target square is empty (or occupied by the castling rook)
      if (board[targetSq].isNotEmpty &&
          targetSq != rookSq &&
          targetSq != square) continue;
      int numMidSqs = (targetFile - royalFile!).abs();
      bool valid = true;
      if (!options.ignorePieces) {
        int numRookMidSquares = (targetFile - rookFile).abs();
        if (numRookMidSquares > 1) {
          for (int j = 1; j <= numRookMidSquares; j++) {
            int midFile = rookFile + (i == 0 ? -j : j);
            int midSq = size.square(midFile, royalRank);
            if (board[midSq].isNotEmpty && midFile != royalFile) {
              valid = false;
              break;
            }
          }
        }
        for (int j = 1; j <= numMidSqs; j++) {
          int midFile = royalFile! + (i == 0 ? j : -j);

          // For some Chess960 positions.
          // See also https://github.com/alexobviously/bishop/issues/11
          // as to why this is first.
          if (midFile == rookFile) continue;

          int midSq = size.square(midFile, royalRank);

          // None of these squares can be attacked
          if (isAttacked(midSq, colour.opponent)) {
            // squares between & dest square must not be attacked
            valid = false;
            break;
          }

          if (midFile == targetFile && targetFile == royalFile) {
            continue;
          } // king starting on target

          if (j != numMidSqs && board[midSq].isNotEmpty) {
            // squares between to and from must be empty
            valid = false;
            break;
          }
        }
      }
      if (valid) {
        int castlingDir = i == 0 ? Castling.k : Castling.q;
        StandardMove m = StandardMove(
          from: from,
          to: targetSq,
          castlingDir: castlingDir,
          castlingPieceSquare: rookSq,
        );
        if (variant.gatingMode != GatingMode.fixed) moves.add(m);
        if (variant.gating) {
          int gRank = size.rank(m.from);
          if ((gRank == Bishop.rank1 && colour == Bishop.white) ||
              (gRank == size.maxRank && colour == Bishop.black)) {
            moves.addAll(generateGatingMoves(m));
          }
        }
      }
    }
  }

  if (options.onlySquare != null) {
    List<Move> remove = [];
    for (Move m in moves) {
      if (m.to != options.onlySquare) {
        remove.add(m);
      }
    }
    for (Move m in remove) {
      moves.remove(m);
    }
  }

  if (options.legal) {
    List<Move> remove = [];
    for (Move m in moves) {
      bool valid = makeMove(m, false);
      if (!valid ||
          lostBy(colour, ignoreSoftResults: true) ||
          kingAttacked(colour)) {
        remove.add(m);
      }
      undo();
    }
    for (Move m in remove) {
      moves.remove(m);
    }
  }
  return moves;
}