Skip to content

Commit

Permalink
Non-royal castling pieces (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexobviously authored May 30, 2024
1 parent bda6358 commit 2091e40
Show file tree
Hide file tree
Showing 14 changed files with 141 additions and 45 deletions.
5 changes: 4 additions & 1 deletion lib/src/actions/actions/checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ class ActionCheckPieceCount extends Action {
}
return [
EffectSetGameResult(
WonGameElimination(winner: white ? Bishop.white : Bishop.black),
WonGameElimination(
winner: white ? Bishop.white : Bishop.black,
pieceType: pieceType,
),
),
];
},
Expand Down
1 change: 1 addition & 0 deletions lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ enum Variants {
dobutsu(Dobutsu.dobutsu, alt: 'Dobutsu Shogi'),
spawn(MiscVariants.spawn, alt: 'Spawn Chess'),
kinglet(MiscVariants.kinglet, alt: 'Kinglet Chess'),
extinction(MiscVariants.extinction),
threeKings(MiscVariants.threeKings, alt: 'Three Kings Chess'),
domination(MiscVariants.domination),
dart(MiscVariants.dart),
Expand Down
25 changes: 15 additions & 10 deletions lib/src/fen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ ParseFenResult parseFen({
int sq = 0;
int emptySquares = 0;
List<int> royalSquares = List.filled(Bishop.numPlayers, Bishop.invalid);
List<int> castlingSquares = List.filled(Bishop.numPlayers, Bishop.invalid);

for (String c in boardSymbols) {
if (c == '~') {
Expand Down Expand Up @@ -154,6 +155,9 @@ ParseFenResult parseFen({
if (variant.pieces[pieceIndex].type.royal) {
royalSquares[colour] = sq;
}
if (variant.pieces[pieceIndex].type.castling) {
castlingSquares[colour] = sq;
}
sq++;
}
}
Expand Down Expand Up @@ -191,7 +195,7 @@ ParseFenResult parseFen({
final castling = variant.castling
? setupCastling(
castlingString: castlingStr,
royalSquares: royalSquares,
castlingSquares: castlingSquares,
board: board,
variant: variant,
)
Expand All @@ -204,6 +208,7 @@ ParseFenResult parseFen({
epSquare: ep,
castlingRights: castling.castlingRights,
royalSquares: royalSquares,
castlingSquares: castlingSquares,
virginFiles: virginFiles,
hands: hands,
gates: gates,
Expand All @@ -222,14 +227,14 @@ class ParseFenResult {

class CastlingSetup {
final int castlingRights;
final int? royalFile;
final int? castlingFile;
final int? castlingTargetK;
final int? castlingTargetQ;
final List<String>? castlingFileSymbols;

const CastlingSetup({
required this.castlingRights,
this.royalFile,
this.castlingFile,
this.castlingTargetK,
this.castlingTargetQ,
this.castlingFileSymbols,
Expand All @@ -240,7 +245,7 @@ class CastlingSetup {

CastlingSetup setupCastling({
required String castlingString,
required List<int> royalSquares,
required List<int> castlingSquares,
required List<int> board,
required BuiltVariant variant,
}) {
Expand All @@ -252,20 +257,20 @@ CastlingSetup setupCastling({
throw ('Invalid castling string');
}
List<String>? castlingFileSymbols;
int? royalFile;
int? castlingFile;
int? castlingTargetK;
int? castlingTargetQ;
final size = variant.boardSize;
CastlingRights cr = 0;
for (String c in castlingString.split('')) {
// there is probably a better way to do all of this
bool white = c == c.toUpperCase();
royalFile = size.file(royalSquares[white ? 0 : 1]);
castlingFile = size.file(castlingSquares[white ? 0 : 1]);
if (Castling.symbols.containsKey(c)) {
cr += Castling.symbols[c]!;
} else {
int cFile = fileFromSymbol(c);
bool kingside = cFile > size.file(royalSquares[white ? 0 : 1]);
bool kingside = cFile > size.file(castlingSquares[white ? 0 : 1]);
if (kingside) {
castlingTargetK = cFile;
cr += white ? Castling.k : Castling.bk;
Expand All @@ -285,9 +290,9 @@ CastlingSetup setupCastling({
bool kingside = false;
for (int j = 0; j < size.h; j++) {
int piece = board[r + j].type;
if (piece == variant.royalPiece) {
if (piece == variant.castlingPiece) {
kingside = true;
} else if (piece == variant.castlingPiece) {
} else if (piece == variant.rookPiece) {
if (kingside) {
castlingTargetK = j;
} else {
Expand All @@ -306,7 +311,7 @@ CastlingSetup setupCastling({
}
return CastlingSetup(
castlingRights: cr,
royalFile: royalFile,
castlingFile: castlingFile,
castlingTargetK: castlingTargetK,
castlingTargetQ: castlingTargetQ,
castlingFileSymbols: castlingFileSymbols,
Expand Down
17 changes: 10 additions & 7 deletions lib/src/game/game.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class Game {

int? castlingTargetK;
int? castlingTargetQ;
int? royalFile;
int? castlingFile;
List<String> castlingFileSymbols = ['K', 'Q', 'k', 'q'];
late MoveGenParams royalCaptureOptions;

Expand Down Expand Up @@ -81,7 +81,7 @@ class Game {
final newState = result.state.copyWith(hash: zobrist.compute(result.state));
zobrist.incrementHash(newState.hash);
history.add(newState);
royalFile = result.castling.royalFile;
castlingFile = result.castling.castlingFile;
castlingTargetK = result.castling.castlingTargetK;
castlingTargetQ = result.castling.castlingTargetQ;
castlingFileSymbols =
Expand Down Expand Up @@ -384,7 +384,10 @@ class Game {
}

// Generate castling
if (variant.castling && options.castling && pieceType.royal && !inCheck) {
if (variant.castling &&
options.castling &&
pieceType.castling &&
!inCheck) {
bool kingside = colour == Bishop.white
? state.castlingRights.wk
: state.castlingRights.bk;
Expand Down Expand Up @@ -425,22 +428,22 @@ class Game {
if (board[targetSq].isNotEmpty &&
targetSq != rookSq &&
targetSq != square) continue;
int numMidSqs = (targetFile - royalFile!).abs();
int numMidSqs = (targetFile - castlingFile!).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) {
if (board[midSq].isNotEmpty && midFile != castlingFile) {
valid = false;
break;
}
}
}
for (int j = 1; j <= numMidSqs; j++) {
int midFile = royalFile! + (i == 0 ? j : -j);
int midFile = castlingFile! + (i == 0 ? j : -j);

// For some Chess960 positions.
// See also https://github.com/alexobviously/bishop/issues/11
Expand All @@ -456,7 +459,7 @@ class Game {
break;
}

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

Expand Down
40 changes: 27 additions & 13 deletions lib/src/game/game_movement.dart
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ extension GameMovement on Game {

int castlingRights = state.castlingRights;
List<int> royalSquares = List.from(state.royalSquares);
List<int> castlingSquares = List.from(state.castlingSquares);

if (move.enPassant) {
// Remove the captured ep piece
Expand Down Expand Up @@ -265,29 +266,37 @@ extension GameMovement on Game {
: variant.castlingOptions.qTarget!;
int rookFile = kingside ? castlingFile - 1 : castlingFile + 1;
int rookSq = size.square(rookFile, fromRank);
int kingSq = size.square(castlingFile, fromRank);
int castlingSq = size.square(castlingFile, fromRank);
int rook = board[move.castlingPieceSquare!];
hash ^= zobrist.table[move.castlingPieceSquare!][rook.piece];
if (board[kingSq].isNotEmpty) {
hash ^= zobrist.table[kingSq][board[kingSq].piece];
if (board[castlingSq].isNotEmpty) {
hash ^= zobrist.table[castlingSq][board[castlingSq].piece];
}
hash ^= zobrist.table[kingSq][fromSq.piece];
hash ^= zobrist.table[castlingSq][fromSq.piece];
if (board[rookSq].isNotEmpty) {
hash ^= zobrist.table[rookSq][board[rookSq].piece];
}
hash ^= zobrist.table[rookSq][rook.piece];
board[move.castlingPieceSquare!] = Bishop.empty;
board[kingSq] = fromSq.setInitialState(false);
board[castlingSq] = fromSq.setInitialState(false);
board[rookSq] = rook;
castlingRights = castlingRights.remove(colour);
royalSquares[colour] = kingSq;
} else if (fromPiece.royal) {
// king moved
castlingRights = castlingRights.remove(colour);
royalSquares[colour] = move.to;
castlingSquares[colour] = castlingSq;
if (fromPiece.royal) {
royalSquares[colour] = castlingSq;
}
} else if (fromPiece.royal || fromPiece.castling) {
// Royal or castling piece moved, but it wasn't castling.
if (fromPiece.castling) {
castlingRights = castlingRights.remove(colour);
castlingSquares[colour] = move.to;
}
if (fromPiece.royal) {
royalSquares[colour] = move.to;
}
} else {
// If the player's rook moved, remove relevant castling rights
if (fromSq.type == variant.castlingPiece) {
if (fromSq.type == variant.rookPiece) {
int fromFile = size.file(move.from);
bool onFirstRank = size.rank(move.from) == size.firstRank(colour);
int ks = colour == Bishop.white ? Castling.k : Castling.bk;
Expand All @@ -302,8 +311,8 @@ extension GameMovement on Game {
castlingRights = castlingRights.flip(qs);
}
}
// If the opponent's rook was captured, remove relevant castling rights
if (move.capture && move.capturedPiece!.type == variant.castlingPiece) {
// If the opponent's rook was captured, remove relevant castling rights.
if (move.capture && move.capturedPiece!.type == variant.rookPiece) {
// rook captured
int toFile = size.file(move.to);
int opponent = colour.opponent;
Expand All @@ -321,6 +330,10 @@ extension GameMovement on Game {
}
}
}
// If the opponent's castling piece was captured.
if (move.capture && move.capturedPiece!.type == variant.castlingPiece) {
castlingRights = castlingRights.remove(colour.opponent);
}
if (castlingRights != state.castlingRights) {
hash ^= zobrist.table[zobrist.castling][state.castlingRights];
hash ^= zobrist.table[zobrist.castling][castlingRights];
Expand All @@ -341,6 +354,7 @@ extension GameMovement on Game {
state.turn == Bishop.black ? state.fullMoves + 1 : state.fullMoves,
castlingRights: castlingRights,
royalSquares: royalSquares,
castlingSquares: castlingSquares,
virginFiles: virginFiles,
epSquare: epSquare,
hash: hash,
Expand Down
5 changes: 3 additions & 2 deletions lib/src/game/game_outputs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,11 @@ extension GameOutputs on Game {
int firstTurn = history.first.turn;
int turn = firstTurn;
String pgn = '';
for (int i = 0; i < moves.length; i++) {
if (i == 0 || turn == Bishop.white)
for (int i = 0; i < moves.length; i++) {
if (i == 0 || turn == Bishop.white) {
pgn =
'$pgn${firstMove + i ~/ 2}${(i == 0 && turn == Bishop.black) ? "" : ". "}';
}
if (i == 0 && turn == Bishop.black) {
pgn = '$pgn... ';
firstMove++;
Expand Down
9 changes: 7 additions & 2 deletions lib/src/game/game_result.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,18 @@ class WonGameRoyalDead extends WonGame {
}

class WonGameElimination extends WonGame {
const WonGameElimination({required super.winner});
final String? pieceType;
const WonGameElimination({required super.winner, this.pieceType});

@override
String toString() => 'WonGameElimination($winner)';

@override
String get readable => '${super.readable} by elimination';
String get readable => [
super.readable,
'by elimination',
if (pieceType != null) '($pieceType)',
].join(' ');
}

class WonGameStalemate extends WonGame {
Expand Down
16 changes: 14 additions & 2 deletions lib/src/piece_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ class PieceType {
/// All of the different move groups this piece can make.
final List<MoveDefinition> moves;

/// Royal pieces can be checkmated, and can castle.
/// Royal pieces can be checkmated, and can castle,
/// unless [castling] is false.
final bool royal;

/// Whether this is a castling piece.
/// If not specified, it will be the same as [royal].
final bool castling;

/// Defines the promotion behaviour of this piece type.
final PiecePromoOptions promoOptions;

Expand Down Expand Up @@ -51,14 +56,15 @@ class PieceType {
this.betza,
required this.moves,
this.royal = false,
bool? castling,
this.promoOptions = PiecePromoOptions.promoPiece,
this.enPassantable = false,
this.noSanSymbol = false,
this.value = Bishop.defaultPieceValue,
this.regionEffects = const [],
this.actions = const [],
this.optimisationData,
});
}) : castling = castling ?? royal;

factory PieceType.fromJson(
Map<String, dynamic> json, {
Expand All @@ -68,6 +74,7 @@ class PieceType {
return PieceType.fromBetza(
json['betza'],
royal: json['royal'] ?? false,
castling: json['castling'],
promoOptions: json.containsKey('promoOptions')
? PiecePromoOptions.fromJson(json['promoOptions'])
: PiecePromoOptions.promoPiece,
Expand Down Expand Up @@ -97,6 +104,7 @@ class PieceType {
return {
'betza': betza,
if (verbose || royal) 'royal': royal,
if (verbose || castling != royal) 'castling': castling,
if (verbose || promoOptions != PiecePromoOptions.promoPiece)
'promoOptions': promoOptions.toJson(),
if (verbose || enPassantable) 'enPassantable': enPassantable,
Expand All @@ -119,6 +127,7 @@ class PieceType {
String? betza,
List<MoveDefinition>? moves,
bool? royal,
bool? castling,
PiecePromoOptions? promoOptions,
bool? enPassantable,
bool? noSanSymbol,
Expand All @@ -131,6 +140,7 @@ class PieceType {
betza: betza ?? this.betza,
moves: moves ?? this.moves,
royal: royal ?? this.royal,
castling: castling ?? this.castling,
promoOptions: promoOptions ?? this.promoOptions,
enPassantable: enPassantable ?? this.enPassantable,
noSanSymbol: noSanSymbol ?? this.noSanSymbol,
Expand Down Expand Up @@ -194,6 +204,7 @@ class PieceType {
factory PieceType.fromBetza(
String betza, {
bool royal = false,
bool? castling,
PiecePromoOptions promoOptions = PiecePromoOptions.promoPiece,
bool enPassantable = false,
bool noSanSymbol = false,
Expand All @@ -209,6 +220,7 @@ class PieceType {
betza: betza,
moves: moves,
royal: royal,
castling: castling,
promoOptions: promoOptions,
enPassantable: enPassantable,
noSanSymbol: noSanSymbol,
Expand Down
Loading

0 comments on commit 2091e40

Please sign in to comment.