import 'dart:collection'; import 'dart:io'; import 'package:omnichess/constants/piece.dart'; class Position { static List get startingBoard => [ Piece.blackRook, Piece.blackKnight, Piece.blackBishop, Piece.blackQueen, Piece.blackKing, Piece.blackBishop, Piece.blackKnight, Piece.blackRook, Piece.blackPawn, Piece.blackPawn, Piece.blackPawn, Piece.blackPawn, Piece.blackPawn, Piece.blackPawn, Piece.blackPawn, Piece.blackPawn, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.emptySquare, Piece.whitePawn, Piece.whitePawn, Piece.whitePawn, Piece.whitePawn, Piece.whitePawn, Piece.whitePawn, Piece.whitePawn, Piece.whitePawn, Piece.whiteRook, Piece.whiteKnight, Piece.whiteBishop, Piece.whiteQueen, Piece.whiteKing, Piece.whiteBishop, Piece.whiteKnight, Piece.whiteRook, ]; List board; bool isWhiteTurn; bool canWhiteCastleKingside; bool canWhiteCastleQueenside; bool canBlackCastleKingside; bool canBlackCastleQueenside; int? enPassantTargetSquare; int halfmoveClock; int fullmoveNumber; String? get enPassantTargetSquareAlgebraic => null == enPassantTargetSquare ? null : String.fromCharCodes([ 97 + (enPassantTargetSquare! % 8), 56 - (enPassantTargetSquare! ~/ 8) ]); String get fen { int i = 0; final StringBuffer stringBuffer = StringBuffer(); while (i < 64) { if (board[i] == 0) { int emptyCount = 1; i++; while (i % 8 != 0 && board[i] == 0) { emptyCount++; i++; } stringBuffer.write(emptyCount.toString()); } else { stringBuffer.write( Piece.fenCharacterByPiece[board[i]], ); i++; } if (i < 64 && i % 8 == 0) { stringBuffer.write("/"); } } return "$stringBuffer" " " "${isWhiteTurn ? "w" : "b"}" " " "${canWhiteCastleKingside ? "K" : ""}" "${canWhiteCastleQueenside ? "Q" : ""}" "${canBlackCastleKingside ? "k" : ""}" "${canBlackCastleQueenside ? "q" : ""}" "${!(canWhiteCastleKingside || canWhiteCastleQueenside || canBlackCastleKingside || canBlackCastleQueenside) ? "-" : ""}" " " "${enPassantTargetSquareAlgebraic ?? "-"}" " " "$halfmoveClock" " " "$fullmoveNumber"; } Position({ required this.board, required this.isWhiteTurn, required this.canWhiteCastleKingside, required this.canWhiteCastleQueenside, required this.canBlackCastleKingside, required this.canBlackCastleQueenside, required this.enPassantTargetSquare, required this.halfmoveClock, required this.fullmoveNumber, }); factory Position.from(Position position) => Position( board : List.from(position.board, growable: false), isWhiteTurn : position.isWhiteTurn, canWhiteCastleKingside : position.canWhiteCastleKingside, canWhiteCastleQueenside: position.canWhiteCastleQueenside, canBlackCastleKingside : position.canBlackCastleKingside, canBlackCastleQueenside: position.canBlackCastleQueenside, enPassantTargetSquare : position.enPassantTargetSquare, halfmoveClock : position.halfmoveClock, fullmoveNumber : position.fullmoveNumber, ); factory Position.fromStartingPosition() => Position( board : startingBoard, isWhiteTurn : true, canWhiteCastleKingside : true, canWhiteCastleQueenside: true, canBlackCastleKingside : true, canBlackCastleQueenside: true, enPassantTargetSquare : null, halfmoveClock : 0, fullmoveNumber : 1, ); factory Position.fromFen( String fenPieces, String fenTurnPlayer, String fenCastlingRights, String fenEnPassantTargetSquare, String fenHalfmoveClock, String fenFullmoveNumber, ) { const String legalPieces = "pnbrqk"; int boardIndex = 0; final List newBoard = List.filled(64, Piece.emptySquare); for (int fenPiecesIndex = 0; fenPiecesIndex < fenPieces.length; fenPiecesIndex++) { final String fenPiece = fenPieces[fenPiecesIndex]; final int? fenPieceNumber = int.tryParse(fenPiece); if (fenPiece == "/") { // Row separator, no action needed. } else if (null != fenPieceNumber) { boardIndex += fenPieceNumber; } else if (legalPieces.contains(fenPiece.toLowerCase())) { newBoard[boardIndex] = Piece.pieceByFenCharacter[fenPiece]!; boardIndex++; } else { throw FormatException("Invalid character $fenPiece", fenPieces, fenPiecesIndex); } } if (boardIndex != 64) { throw FormatException("Invalid board size: Expected 64, Found $boardIndex", fenPieces); } final List board = newBoard; final bool isWhiteTurn = fenTurnPlayer == "w"; final bool canWhiteCastleKingside = fenCastlingRights.contains("K"); final bool canWhiteCastleQueenside = fenCastlingRights.contains("Q"); final bool canBlackCastleKingside = fenCastlingRights.contains("k"); final bool canBlackCastleQueenside = fenCastlingRights.contains("q"); final int? enPassantTargetSquare = fenEnPassantTargetSquare == "-" ? null : squareStringToIndex(fenEnPassantTargetSquare); final int halfmoveClock = int.parse(fenHalfmoveClock); final int fullmoveNumber = int.parse(fenFullmoveNumber); return Position( board : board, isWhiteTurn : isWhiteTurn, canWhiteCastleKingside : canWhiteCastleKingside, canWhiteCastleQueenside: canWhiteCastleQueenside, canBlackCastleKingside : canBlackCastleKingside, canBlackCastleQueenside: canBlackCastleQueenside, enPassantTargetSquare : enPassantTargetSquare, halfmoveClock : halfmoveClock, fullmoveNumber : fullmoveNumber, ); } factory Position.fromPositionCommand(String command) { final Queue commandComponents = Queue.from(command.split(" ")); final String type = commandComponents.removeFirst(); Position position = switch (type) { "fen" => (() { final String fenPieces = commandComponents.removeFirst(); final String fenTurnPlayer = commandComponents.removeFirst(); final String fenCastlingRights = commandComponents.removeFirst(); final String fenEnPassantTargetSquare = commandComponents.removeFirst(); final String fenHalfmoveClock = commandComponents.removeFirst(); final String fenFullmoveNumber = commandComponents.removeFirst(); return Position.fromFen( fenPieces, fenTurnPlayer, fenCastlingRights, fenEnPassantTargetSquare, fenHalfmoveClock, fenFullmoveNumber, ); })(), "startpos" => Position.fromStartingPosition(), _ => throw FormatException("Invalid position type $type"), }; if (commandComponents.isEmpty) { return position; } commandComponents.removeFirst(); // "moves" while (commandComponents.isNotEmpty) { final String move = commandComponents.removeFirst(); position.playMove(move); } return position; } void playMoveIndices(int startSquare, int endSquare, [String? promotingPiece]) => isWhiteTurn ? _playMoveWhite(startSquare, endSquare, promotingPiece) : _playMoveBlack(startSquare, endSquare, promotingPiece); void playMove(String move) { final (int startSquare, int endSquare, String? promotingPiece) = moveStringToIndices(move); playMoveIndices(startSquare, endSquare, promotingPiece); } void _playMoveWhite(int startSquare, int endSquare, String? promotingPiece) { board[endSquare] = board[startSquare]; board[startSquare] = Piece.emptySquare; isWhiteTurn = false; if (0 == endSquare) { // sth moved to a8 canBlackCastleQueenside = false; } else if (7 == endSquare) { // sth moved to h8 canBlackCastleKingside = false; } if (Piece.whitePawn == board[endSquare]) { halfmoveClock = 0; if (enPassantTargetSquare == endSquare) { board[endSquare + 8] = Piece.emptySquare; } enPassantTargetSquare = (startSquare - endSquare == 16) ? startSquare - 8 : null; if (endSquare < 8) { board[endSquare] = Piece.pieceByFenCharacter[promotingPiece]!; } return; } halfmoveClock++; enPassantTargetSquare = null; if (Piece.whiteKing == board[endSquare]) { canWhiteCastleQueenside = false; canWhiteCastleKingside = false; if (startSquare - endSquare == 2) { // queenside castle board[56] = Piece.emptySquare; board[59] = Piece.whiteRook; return; } if (endSquare - startSquare == 2) { // kingside castle board[63] = Piece.emptySquare; board[61] = Piece.whiteRook; return; } return; } if (56 == startSquare) { // sth from a1 moved canWhiteCastleQueenside = false; } else if (63 == startSquare) { // sth from h1 moved canWhiteCastleKingside = false; } return; } void _playMoveBlack(int startSquare, int endSquare, String? promotingPiece) { board[endSquare] = board[startSquare]; board[startSquare] = Piece.emptySquare; isWhiteTurn = true; fullmoveNumber = fullmoveNumber + 1; if (56 == endSquare) { // sth moved to a8 canWhiteCastleQueenside = false; } else if (63 == endSquare) { // sth moved to h8 canWhiteCastleKingside = false; } if (Piece.blackPawn == board[endSquare]) { halfmoveClock = 0; if (enPassantTargetSquare == endSquare) { board[endSquare - 8] = Piece.emptySquare; } enPassantTargetSquare = (endSquare - startSquare == 16) ? startSquare + 8 : null; if (endSquare > 55) { board[endSquare] = Piece.pieceByFenCharacter[promotingPiece]!; } return; } halfmoveClock++; enPassantTargetSquare = null; if (Piece.blackKing == board[endSquare]) { canBlackCastleQueenside = false; canBlackCastleKingside = false; if (startSquare - endSquare == 2) { // queenside castle board[0] = Piece.emptySquare; board[3] = Piece.blackRook; return; } if (endSquare - startSquare == 2) { // kingside castle board[7] = Piece.emptySquare; board[5] = Piece.blackRook; return; } } if (0 == startSquare) { // sth from a8 moved canBlackCastleQueenside = false; } else if (7 == startSquare) { // sth from h8 moved canBlackCastleKingside = false; } } void setStartingPosition() { board = startingBoard; isWhiteTurn = true; canWhiteCastleKingside = true; canWhiteCastleQueenside = true; canBlackCastleKingside = true; canBlackCastleQueenside = true; enPassantTargetSquare = null; halfmoveClock = 0; fullmoveNumber = 1; } bool setFenPosition( String fenPieces, String fenTurnPlayer, String fenCastlingRights, String fenEnPassantTargetSquare, String fenHalfmoveClock, String fenFullmoveNumber, ) { try { const String legalPieces = "pnbrqk"; int boardIndex = 0; final List newBoard = List.filled(64, Piece.emptySquare); for (int fenPiecesIndex = 0; fenPiecesIndex < fenPieces.length; fenPiecesIndex++) { final String fenPiece = fenPieces[fenPiecesIndex]; final int? fenPieceNumber = int.tryParse(fenPiece); if (fenPiece == "/") { // Row separator, no action needed. } else if (null != fenPieceNumber) { boardIndex += fenPieceNumber; } else if (legalPieces.contains(fenPiece.toLowerCase())) { newBoard[boardIndex] = Piece.pieceByFenCharacter[fenPiece]!; boardIndex++; } else { // If character is not recognized return false. return false; } } if (boardIndex != 64) { return false; } board = newBoard; isWhiteTurn = fenTurnPlayer == "w"; canWhiteCastleKingside = fenCastlingRights.contains("K"); canWhiteCastleQueenside = fenCastlingRights.contains("Q"); canBlackCastleKingside = fenCastlingRights.contains("k"); canBlackCastleQueenside = fenCastlingRights.contains("q"); enPassantTargetSquare = fenEnPassantTargetSquare == "-" ? null : squareStringToIndex(fenEnPassantTargetSquare); halfmoveClock = int.parse(fenHalfmoveClock); fullmoveNumber = int.parse(fenFullmoveNumber); return true; } catch (e) { stderr.writeln("error \"$e\""); return false; } } static String squareIndexToString(int index) => String.fromCharCodes([ 97 + (index % 8), 56 - (index ~/ 8) ]); static int squareStringToIndex(String square) => (56 - square.codeUnitAt(1)) * 8 + square.codeUnitAt(0) - 97; static (int, int, String?) moveStringToIndices(String move) => ( (56 - move.codeUnitAt(1)) * 8 + move.codeUnitAt(0) - 97, (56 - move.codeUnitAt(3)) * 8 + move.codeUnitAt(2) - 97, move.length > 4 ? move.substring(4).toLowerCase() : null, ); }