omnichess/lib/classes/position.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]!;
}
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<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));
}
}