From a155c783b8fffdf34d33d5830acf0892bed33e01 Mon Sep 17 00:00:00 2001 From: Fabian Fichter Date: Tue, 19 Jan 2021 20:09:15 +0100 Subject: [PATCH] Support atomic chess without checks (#81) Add variant nocheckatomic which implements a modified version of atomic chess where check and check-/stalemate do not apply, which is very similar to the rules used on ICC. The variant atomic is an incomplete implementation of lichess atomic chess rules, added for easier usage in GUIs. It will occasionally play illegal moves in this variant. --- README.md | 1 + src/evaluate.cpp | 27 +++++++++-- src/parser.cpp | 13 +++++ src/position.cpp | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++- src/position.h | 10 ++++ src/psqt.cpp | 7 +++ src/ucioption.cpp | 2 +- src/variant.cpp | 22 +++++++++ src/variant.h | 2 + src/variants.ini | 1 + 10 files changed, 215 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7e5c8b9..a5e84e4 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ The games currently supported besides chess are listed below. Fairy-Stockfish ca - [King of the Hill](https://en.wikipedia.org/wiki/King_of_the_Hill_(chess)), [Racing Kings](https://en.wikipedia.org/wiki/V._R._Parton#Racing_Kings) - [Three-check](https://en.wikipedia.org/wiki/Three-check_chess), Five-check - [Los Alamos](https://en.wikipedia.org/wiki/Los_Alamos_chess) +- [Atomic](https://en.wikipedia.org/wiki/Atomic_chess) (non-standard rules) - [Horde](https://en.wikipedia.org/wiki/Dunsany%27s_Chess#Horde_Chess) - [Knightmate](https://www.chessvariants.com/diffobjective.dir/knightmate.html) diff --git a/src/evaluate.cpp b/src/evaluate.cpp index 239cf2e..ce1495a 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -857,12 +857,31 @@ namespace { if (pos.extinction_value() == -VALUE_MATE) { Bitboard bExt = attackedBy[Us][ALL_PIECES] & pos.pieces(Them); - while (bExt) + for (PieceType pt : pos.extinction_piece_types()) { - PieceType pt = type_of(pos.piece_on(pop_lsb(&bExt))); + if (pt == ALL_PIECES) + continue; int denom = std::max(pos.count_with_hand(Them, pt) - pos.extinction_piece_count(), 1); - if (pos.extinction_piece_types().find(pt) != pos.extinction_piece_types().end()) - score += make_score(1000, 1000) / (denom * denom); + // Explosion threats + if (pos.blast_on_capture()) + { + int evasions = popcount(((attackedBy[Them][pt] & ~pos.pieces(Them)) | pos.pieces(Them, pt)) & ~attackedBy[Us][ALL_PIECES]) * denom; + int attacks = popcount((attackedBy[Them][pt] | pos.pieces(Them, pt)) & attackedBy[Us][ALL_PIECES]); + int explosions = 0; + + Bitboard bExtBlast = bExt & (attackedBy2[Us] | ~attackedBy[Us][pt]); + while (bExtBlast) + { + Square s = pop_lsb(&bExtBlast); + if (((attacks_bb(s) | s) & pos.pieces(Them, pt)) && !(attacks_bb(s) & pos.pieces(Us, pt))) + explosions++; + } + int danger = 20 * attacks / (evasions + 1) + 40 * explosions; + score += make_score(danger * (100 + danger), 0); + } + else + // Direct extinction threats + score += make_score(1000, 1000) / (denom * denom) * popcount(bExt & pos.pieces(Them, pt)); } } diff --git a/src/parser.cpp b/src/parser.cpp index 9da29fd..d45c763 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -237,6 +237,7 @@ Variant* VariantParser::parse(Variant* v) { parse_attribute("mandatoryPawnPromotion", v->mandatoryPawnPromotion); parse_attribute("mandatoryPiecePromotion", v->mandatoryPiecePromotion); parse_attribute("pieceDemotion", v->pieceDemotion); + parse_attribute("blastOnCapture", v->blastOnCapture); parse_attribute("endgameEval", v->endgameEval); parse_attribute("doubleStep", v->doubleStep); parse_attribute("doubleStepRank", v->doubleStepRank); @@ -365,6 +366,18 @@ Variant* VariantParser::parse(Variant* v) { std::cerr << "pieceToCharTable - Missing piece type: " << ptu << std::endl; } } + + // Check for limitations + + // Options incompatible with royal kings + if (v->pieceTypes.find(KING) != v->pieceTypes.end()) + { + if (v->blastOnCapture) + std::cerr << "Can not use kings with blastOnCapture" << std::endl; + if (v->flipEnclosedPieces) + std::cerr << "Can not use kings with flipEnclosedPieces" << std::endl; + } + } return v; } diff --git a/src/position.cpp b/src/position.cpp index 38b2cc6..72df161 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -948,7 +948,7 @@ bool Position::legal(Move m) const { if (type_of(m) == CASTLING) { // Non-royal pieces can not be impeded from castling - if (type_of(piece_on(from)) != KING) + if (type_of(piece_on(from)) != KING && !var->extinctionPseudoRoyal) return true; // After castling, the rook and king final positions are the same in @@ -960,6 +960,10 @@ bool Position::legal(Move m) const { if (attackers_to(s, ~us)) return false; + // TODO: need to consider touching kings + if (var->extinctionPseudoRoyal && attackers_to(from, ~us)) + return false; + // Will the gate be blocked by king or rook? Square rto = to + (to_sq(m) > from_sq(m) ? WEST : EAST); if (is_gating(m) && (gating_square(m) == to || gating_square(m) == rto)) @@ -1590,6 +1594,59 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { st->gatesBB[us] = 0; } + // Remove the blast pieces + if (captured && blast_on_capture()) + { + std::memset(st->unpromotedBycatch, 0, sizeof(st->unpromotedBycatch)); + st->demotedBycatch = st->promotedBycatch = 0; + Bitboard blast = (attacks_bb(to) & (pieces() ^ pieces(PAWN))) | to; + while (blast) + { + Square bsq = pop_lsb(&blast); + Piece bpc = piece_on(bsq); + Color bc = color_of(bpc); + if (type_of(bpc) != PAWN) + st->nonPawnMaterial[bc] -= PieceValue[MG][bpc]; + + // Update board and piece lists + // In order to not have to store the values of both board and unpromotedBoard, + // demote promoted pieces, but keep promoted pawns as promoted, + // and store demotion/promotion bitboards to disambiguate the piece state + bool capturedPromoted = is_promoted(bsq); + Piece unpromotedCaptured = unpromoted_piece_on(bsq); + st->unpromotedBycatch[bsq] = unpromotedCaptured ? unpromotedCaptured : bpc; + if (unpromotedCaptured) + st->demotedBycatch |= bsq; + else if (capturedPromoted) + st->promotedBycatch |= bsq; + remove_piece(bsq); + board[bsq] = NO_PIECE; + if (captures_to_hand()) + { + Piece pieceToHand = !capturedPromoted || drop_loop() ? ~bpc + : unpromotedCaptured ? ~unpromotedCaptured + : make_piece(~color_of(bpc), PAWN); + add_to_hand(pieceToHand); + k ^= Zobrist::inHand[pieceToHand][pieceCountInHand[color_of(pieceToHand)][type_of(pieceToHand)] - 1] + ^ Zobrist::inHand[pieceToHand][pieceCountInHand[color_of(pieceToHand)][type_of(pieceToHand)]]; + } + + // Update material hash key + k ^= Zobrist::psq[bpc][bsq]; + st->materialKey ^= Zobrist::psq[bpc][pieceCount[bpc]]; + if (type_of(bpc) == PAWN) + st->pawnKey ^= Zobrist::psq[bpc][bsq]; + + // Update castling rights if needed + if (st->castlingRights && castlingRightsMask[bsq]) + { + int cr = castlingRightsMask[bsq]; + k ^= Zobrist::castling[st->castlingRights & cr]; + st->castlingRights &= ~cr; + } + } + } + // Update the key with the final value st->key = k; // Calculate checkers bitboard (if move gives check) @@ -1650,6 +1707,29 @@ void Position::undo_move(Move m) { || (is_pass(m) && pass())); assert(type_of(st->capturedPiece) != KING); + // Add the blast pieces + if (st->capturedPiece && blast_on_capture()) + { + Bitboard blast = attacks_bb(to) | to; + while (blast) + { + Square bsq = pop_lsb(&blast); + Piece unpromotedBpc = st->unpromotedBycatch[bsq]; + Piece bpc = st->demotedBycatch & bsq ? make_piece(color_of(unpromotedBpc), promoted_piece_type(type_of(unpromotedBpc))) + : unpromotedBpc; + bool isPromoted = (st->promotedBycatch | st->demotedBycatch) & bsq; + + // Update board and piece lists + if (bpc) + { + put_piece(bpc, bsq, isPromoted, st->demotedBycatch & bsq ? unpromotedBpc : NO_PIECE); + if (captures_to_hand()) + remove_from_hand(!drop_loop() && (st->promotedBycatch & bsq) ? make_piece(~color_of(unpromotedBpc), PAWN) + : ~unpromotedBpc); + } + } + } + // Remove gated piece if (is_gating(m)) { @@ -1856,6 +1936,53 @@ Key Position::key_after(Move m) const { } +Value Position::blast_see(Move m) const { + assert(is_ok(m)); + + Square from = from_sq(m); + Square to = to_sq(m); + Color us = color_of(moved_piece(m)); + Bitboard fromto = type_of(m) == DROP ? square_bb(to) : from | to; + Bitboard blast = ((attacks_bb(to) & ~pieces(PAWN)) | fromto) & pieces(); + + Value result = VALUE_ZERO; + + // Add the least valuable attacker for quiet moves + if (!capture(m)) + { + Bitboard attackers = attackers_to(to, pieces() ^ fromto, ~us); + Value minAttacker = VALUE_INFINITE; + + while (attackers) + { + Square s = pop_lsb(&attackers); + if (extinction_piece_types().find(type_of(piece_on(s))) == extinction_piece_types().end()) + minAttacker = std::min(minAttacker, blast & s ? VALUE_ZERO : PieceValue[MG][piece_on(s)]); + } + + if (minAttacker == VALUE_INFINITE) + return VALUE_ZERO; + + result += minAttacker; + if (type_of(m) == DROP) + result -= PieceValue[MG][dropped_piece_type(m)]; + } + + // Sum up blast piece values + while (blast) + { + Piece bpc = piece_on(pop_lsb(&blast)); + if (extinction_piece_types().find(type_of(bpc)) != extinction_piece_types().end()) + return color_of(bpc) == us ? extinction_value() + : capture(m) ? -extinction_value() + : VALUE_ZERO; + result += color_of(bpc) == us ? -PieceValue[MG][bpc] : PieceValue[MG][bpc]; + } + + return capture(m) || must_capture() ? result - 1 : std::min(result, VALUE_ZERO); +} + + /// Position::see_ge (Static Exchange Evaluation Greater or Equal) tests if the /// SEE value of move is greater or equal to the given threshold. We'll use an /// algorithm similar to alpha-beta pruning with a null window. @@ -1869,10 +1996,15 @@ bool Position::see_ge(Move m, Value threshold) const { return VALUE_ZERO >= threshold; Square from = from_sq(m), to = to_sq(m); + // nCheck if (check_counting() && color_of(moved_piece(m)) == sideToMove && gives_check(m)) return true; + // Atomic explosion SEE + if (blast_on_capture()) + return blast_see(m) >= threshold; + // Extinction if ( extinction_value() != VALUE_NONE && piece_on(to) @@ -1882,6 +2014,7 @@ bool Position::see_ge(Move m, Value threshold) const { && count(~sideToMove) == extinction_piece_count() + 1))) return extinction_value() < VALUE_ZERO; + // Do not evaluate SEE if value would be unreliable if (must_capture() || !checking_permitted() || is_gating(m) || count() == count()) return VALUE_ZERO >= threshold; @@ -2117,7 +2250,7 @@ bool Position::is_immediate_game_end(Value& result, int ply) const { // extinction if (extinction_value() != VALUE_NONE) { - for (Color c : { WHITE, BLACK }) + for (Color c : { ~sideToMove, sideToMove }) for (PieceType pt : extinction_piece_types()) if ( count_with_hand( c, pt) <= var->extinctionPieceCount && count_with_hand(~c, pt) >= var->extinctionOpponentPieceCount + (extinction_claim() && c == sideToMove)) diff --git a/src/position.h b/src/position.h index 688b6de..3f65599 100644 --- a/src/position.h +++ b/src/position.h @@ -58,6 +58,9 @@ struct StateInfo { Bitboard checkersBB; Piece capturedPiece; Piece unpromotedCapturedPiece; + Piece unpromotedBycatch[SQUARE_NB]; + Bitboard promotedBycatch; + Bitboard demotedBycatch; StateInfo* previous; Bitboard blockersForKing[COLOR_NB]; Bitboard pinners[COLOR_NB]; @@ -122,6 +125,7 @@ public: bool mandatory_pawn_promotion() const; bool mandatory_piece_promotion() const; bool piece_demotion() const; + bool blast_on_capture() const; bool endgame_eval() const; bool double_step_enabled() const; Rank double_step_rank_max() const; @@ -251,6 +255,7 @@ public: void undo_null_move(); // Static Exchange Evaluation + Value blast_see(Move m) const; bool see_ge(Move m, Value threshold = VALUE_ZERO) const; // Accessing hash keys @@ -422,6 +427,11 @@ inline bool Position::piece_demotion() const { return var->pieceDemotion; } +inline bool Position::blast_on_capture() const { + assert(var != nullptr); + return var->blastOnCapture; +} + inline bool Position::endgame_eval() const { assert(var != nullptr); return var->endgameEval && !count_in_hand(WHITE, ALL_PIECES) && !count_in_hand(BLACK, ALL_PIECES); diff --git a/src/psqt.cpp b/src/psqt.cpp index aa05b32..929f175 100644 --- a/src/psqt.cpp +++ b/src/psqt.cpp @@ -129,7 +129,11 @@ void init(const Variant* v) { // Consider promotion types in pawn score if (pt == PAWN) + { score -= make_score(0, (QueenValueEg - maxPromotion) / 100); + if (v->blastOnCapture) + score += score; + } // Scale slider piece values with board size const PieceInfo* pi = pieceMap.find(pt)->second; @@ -168,6 +172,9 @@ void init(const Variant* v) { else if (v->twoBoards) score = make_score(mg_value(score) * 7000 / (7000 + mg_value(score)), eg_value(score) * 7000 / (7000 + eg_value(score))); + else if (v->blastOnCapture) + score = make_score(mg_value(score) * 7000 / (7000 + mg_value(score)), + eg_value(score) * 7000 / (7000 + eg_value(score))); else if (v->checkCounting) score = make_score(mg_value(score) * (40000 + mg_value(score)) / 41000, eg_value(score) * (30000 + eg_value(score)) / 31000); diff --git a/src/ucioption.cpp b/src/ucioption.cpp index cf923b0..9314daa 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -45,7 +45,7 @@ namespace UCI { // standard variants of XBoard/WinBoard std::set standard_variants = { "normal", "nocastle", "fischerandom", "knightmate", "3check", "makruk", "shatranj", - "asean", "seirawan", "crazyhouse", "bughouse", "suicide", "giveaway", "losers", + "asean", "seirawan", "crazyhouse", "bughouse", "suicide", "giveaway", "losers", "atomic", "capablanca", "gothic", "janus", "caparandom", "grand", "shogi", "xiangqi" }; diff --git a/src/variant.cpp b/src/variant.cpp index d0eb095..aef593d 100644 --- a/src/variant.cpp +++ b/src/variant.cpp @@ -289,6 +289,26 @@ namespace { v->extinctionPieceTypes = {ALL_PIECES}; return v; } + // Atomic chess without checks (ICC rules) + // https://www.chessclub.com/help/atomic + Variant* nocheckatomic_variant() { + Variant* v = fairy_variant_base(); + v->variantTemplate = "atomic"; + v->remove_piece(KING); + v->add_piece(COMMONER, 'k'); + v->extinctionValue = -VALUE_MATE; + v->extinctionPieceTypes = {COMMONER}; + v->blastOnCapture = true; + return v; + } + // Atomic chess + // https://en.wikipedia.org/wiki/Atomic_chess + Variant* atomic_variant() { + Variant* v = nocheckatomic_variant(); + // TODO: castling, check(-mate), stalemate are not yet properly implemented + v->extinctionPseudoRoyal = true; + return v; + } Variant* threecheck_variant() { Variant* v = fairy_variant_base(); v->startFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 3+3 0 1"; @@ -998,6 +1018,8 @@ void VariantMap::init() { add("kinglet", kinglet_variant()->conclude()); add("threekings", threekings_variant()->conclude()); add("horde", horde_variant()->conclude()); + add("nocheckatomic", nocheckatomic_variant()->conclude()); + add("atomic", atomic_variant()->conclude()); add("3check", threecheck_variant()->conclude()); add("5check", fivecheck_variant()->conclude()); add("crazyhouse", crazyhouse_variant()->conclude()); diff --git a/src/variant.h b/src/variant.h index ce4ee80..f6da799 100644 --- a/src/variant.h +++ b/src/variant.h @@ -54,6 +54,7 @@ struct Variant { bool mandatoryPawnPromotion = true; bool mandatoryPiecePromotion = false; bool pieceDemotion = false; + bool blastOnCapture = false; bool endgameEval = false; bool doubleStep = true; Rank doubleStepRank = RANK_2; @@ -112,6 +113,7 @@ struct Variant { bool bikjangRule = false; Value extinctionValue = VALUE_NONE; bool extinctionClaim = false; + bool extinctionPseudoRoyal = false; // TODO: implementation incomplete std::set extinctionPieceTypes = {}; int extinctionPieceCount = 0; int extinctionOpponentPieceCount = 0; diff --git a/src/variants.ini b/src/variants.ini index b77c932..7375492 100644 --- a/src/variants.ini +++ b/src/variants.ini @@ -119,6 +119,7 @@ # mandatoryPawnPromotion: pawn promotion is mandatory [bool] (default: true) # mandatoryPiecePromotion: piece promotion (and demotion if enabled) is mandatory [bool] (default: false) # pieceDemotion: enable demotion of pieces (e.g., Kyoto shogi) [bool] (default: false) +# blastOnCapture: captures explode all adjacent non-pawn pieces (e.g., atomic chess) [bool] (default: false) # endgameEval: enable special endgame evaluation (for very chess-like variants only) [bool] (default: false) # doubleStep: enable pawn double step [bool] (default: true) # doubleStepRank: relative rank from where pawn double steps are allowed [Rank] (default: 2) -- 1.7.0.4