373 lines
14 KiB
Dart
373 lines
14 KiB
Dart
import 'dart:collection';
|
|
import 'dart:io';
|
|
|
|
import 'package:omnichess/constants/piece.dart';
|
|
|
|
class Position {
|
|
|
|
static List<int> get startingBoard => <int>[
|
|
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<int> 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<int>.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<int> newBoard = List<int>.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<int> 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<String> commandComponents = Queue<String>.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?.toUpperCase()]!;
|
|
}
|
|
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?.toLowerCase()]!;
|
|
}
|
|
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<int> newBoard = List<int>.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,
|
|
);
|
|
|
|
static bool isTake(Position position, String move) {
|
|
final (int from, int to, String? promotingPiece) = moveStringToIndices(move);
|
|
return
|
|
(position.board[to] != Piece.emptySquare) ||
|
|
(position.enPassantTargetSquare == to && (position.board[from] == Piece.whitePawn || position.board[from] == Piece.blackPawn));
|
|
}
|
|
|
|
} |