From 99602ebe4af50bfb8a245cbbd7c1e37ee52270cc Mon Sep 17 00:00:00 2001 From: yusitnikov Date: Thu, 26 Sep 2024 21:04:15 +0300 Subject: [PATCH] Add sudoku chess variant --- src/parser.cpp | 23 +++++++++++++++ src/position.cpp | 77 +++++++++++++++++++++++++++++++++++++++++++++++- src/position.h | 49 ++++++++++++++++++++++++++++++ src/types.h | 7 +++++ src/variant.h | 5 ++++ src/variants.ini | 19 ++++++++++++ 6 files changed, 179 insertions(+), 1 deletion(-) diff --git a/src/parser.cpp b/src/parser.cpp index 3ebf1b16f..fd1933618 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -488,6 +488,11 @@ Variant* VariantParser::parse(Variant* v) { parse_attribute("flyingGeneral", v->flyingGeneral); parse_attribute("soldierPromotionRank", v->soldierPromotionRank); parse_attribute("flipEnclosedPieces", v->flipEnclosedPieces); + parse_attribute("sudoku", v->sudoku); + parse_attribute("sudokuBoxWidth", v->sudokuBoxWidth); + parse_attribute("sudokuBoxHeight", v->sudokuBoxHeight); + parse_attribute("sudokuAllowedPawns", v->sudokuAllowedPawns); + parse_attribute("sudokuRoyalConflict", v->sudokuRoyalConflict); // game end parse_attribute("nMoveRuleTypes", v->nMoveRuleTypes[WHITE], v->pieceToChar); parse_attribute("nMoveRuleTypes", v->nMoveRuleTypes[BLACK], v->pieceToChar); @@ -640,6 +645,24 @@ Variant* VariantParser::parse(Variant* v) { if (v->flagPieceSafe && v->blastOnCapture) std::cerr << "Can not use flagPieceSafe with blastOnCapture (flagPieceSafe uses simple assessment that does not see blast)." << std::endl; } + // Check invalid sudoku box sizes + if (v->sudoku && v->sudokuBoxWidth && v->sudokuBoxHeight) + { + int boxesCount = (v->maxFile / v->sudokuBoxWidth + 1) * (v->maxRank / v->sudokuBoxHeight + 1); + if (DoCheck) + { + int width = v->maxFile + 1, height = v->maxRank + 1; + if (width % v->sudokuBoxWidth || height % v->sudokuBoxHeight) + std::cerr << "Sudoku boxes don't fit the board size" << std::endl; + if (boxesCount > width && boxesCount > height) + std::cerr << "Too many sudoku boxes" << std::endl; + } + // Ensure that boxes' count doesn't exceed the allocated array size for tracking the conflicts + // (see StateInfo::pieceCountInSudokuHouse). + // Do this safety measure even when DoCheck is false to avoid memory access issues. + if (boxesCount > FILE_NB) + v->sudokuBoxWidth = v->sudokuBoxHeight = 0; + } return v; } diff --git a/src/position.cpp b/src/position.cpp index 5278c0157..cf57b4993 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -600,6 +600,75 @@ void Position::set_check_info(StateInfo* si) const { } +/// Position::set_sudoku_conflicts_info() sets piece count for each sudoku house +/// and sudoku conflicts count for each player + +void Position::set_sudoku_conflicts_info(StateInfo* si) const { + if (!var->sudoku) return; + + si->sudokuConflictsCount[WHITE] = si->sudokuConflictsCount[BLACK] = 0; + memset(si->pieceCountInSudokuHouse, 0, sizeof si->pieceCountInSudokuHouse); + + for (Bitboard b = pieces(); b; ) + { + Square s = pop_lsb(b); + Piece pc = piece_on(s); + + Color c = color_of(pc); + PieceType pt = piece_type_for_sudoku(pc); + + if (is_initial_pawn(pc, s)) continue; + int allowed = allowed_sudoku_conflicts(pt); + + auto count = si->pieceCountInSudokuHouse[c][pt]; + int newFileCount = ++count[SH_FILE][file_of(s)]; + int newRankCount = ++count[SH_RANK][rank_of(s)]; + int newBoxCount = sudoku_boxes() ? ++count[SH_BOX][sudoku_box_of(s)] : 0; + if (newFileCount > allowed || newRankCount > allowed || newBoxCount > allowed) { + ++si->sudokuConflictsCount[c]; + } + } +} + + +/// Position::move_adds_sudoku_conflicts() checks if performing a move leads to a new sudoku conflict + +bool Position::move_adds_sudoku_conflicts(Move m) const { + // Note: currently the function is called only for capture moves. + // That's why it doesn't check for special move types like castling, dropping, etc. + assert(capture(m)); + + if (!var->sudoku) return false; + + Square from = from_sq(m); + Square to = to_sq(m); + Piece pc = moved_piece(m); + Color c = color_of(pc); + assert(c == sideToMove); + PieceType pt = piece_type_for_sudoku(pc); + int allowed = allowed_sudoku_conflicts(pt); + + auto count = st->pieceCountInSudokuHouse[c][pt]; + + File fromFile = file_of(from); + File toFile = file_of(to); + if (fromFile != toFile && count[SH_FILE][toFile] >= allowed) return true; + + Rank fromRank = rank_of(from); + Rank toRank = rank_of(to); + if (fromRank != toRank && count[SH_RANK][toRank] >= allowed) return true; + + if (sudoku_boxes()) + { + int fromBox = sudoku_box_of(from); + int toBox = sudoku_box_of(to); + if (fromBox != toBox && count[SH_BOX][toBox] >= allowed) return true; + } + + return false; +} + + /// Position::set_state() computes the hash keys of the position, and other /// data that once computed is updated incrementally as moves are made. /// The function is only used when a new position is set up, and to verify @@ -614,6 +683,7 @@ void Position::set_state(StateInfo* si) const { si->move = MOVE_NONE; set_check_info(si); + set_sudoku_conflicts_info(si); for (Bitboard b = pieces(); b; ) { @@ -1053,6 +1123,10 @@ bool Position::legal(Move m) const { if (must_capture() && !capture(m) && has_capture()) return false; + // Illegal captures + if (capture(m) && type_of(captured_piece(m)) != KING && (sudoku_conflicts(us) || move_adds_sudoku_conflicts(m))) + return false; + // Illegal non-drop moves if (must_drop() && count_in_hand(us, var->mustDropType) > 0) { @@ -1560,7 +1634,7 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { Square from = from_sq(m); Square to = to_sq(m); Piece pc = moved_piece(m); - Piece captured = piece_on(type_of(m) == EN_PASSANT ? capture_square(to) : to); + Piece captured = captured_piece(m); if (to == from) { assert((type_of(m) == PROMOTION && sittuyin_promotion()) || (is_pass(m) && (pass(us) || var->wallOrMove ))); @@ -2093,6 +2167,7 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { // Update king attacks used for fast check detection set_check_info(st); + set_sudoku_conflicts_info(st); // Calculate the repetition info. It is the ply distance from the previous // occurrence of the same position, negative in the 3-fold case, or zero diff --git a/src/position.h b/src/position.h index 8868d7646..5be91fb20 100644 --- a/src/position.h +++ b/src/position.h @@ -59,6 +59,8 @@ struct StateInfo { // Not copied when making a move (will be recomputed anyhow) Key key; + int sudokuConflictsCount[COLOR_NB]; + int pieceCountInSudokuHouse[COLOR_NB][PIECE_TYPE_NB][SH_NB][FILE_NB]; Bitboard checkersBB; Piece unpromotedCapturedPiece; Piece unpromotedBycatch[SQUARE_NB]; @@ -223,6 +225,13 @@ class Position { int count_with_hand(Color c, PieceType pt) const; bool bikjang() const; bool allow_virtual_drop(Color c, PieceType pt) const; + bool sudoku_boxes() const; + int sudoku_box_of(Square s) const; + PieceType piece_type_for_sudoku(Piece pc) const; + int allowed_sudoku_conflicts(PieceType pt) const; + int sudoku_conflicts(Color c) const; + void set_sudoku_conflicts_info(StateInfo* si) const; + bool move_adds_sudoku_conflicts(Move m) const; // Position representation Bitboard pieces(PieceType pt = ALL_PIECES) const; @@ -279,6 +288,7 @@ class Position { bool gives_check(Move m) const; Piece moved_piece(Move m) const; Piece captured_piece() const; + Piece captured_piece(Move m) const; const std::string piece_to_partner() const; // Piece specific @@ -344,6 +354,7 @@ class Position { void set_castling_right(Color c, Square rfrom); void set_state(StateInfo* si) const; void set_check_info(StateInfo* si) const; + bool is_initial_pawn(Piece pc, Square s) const; // Other helpers void move_piece(Square from, Square to); @@ -1407,6 +1418,11 @@ inline Piece Position::captured_piece() const { return st->capturedPiece; } +inline Piece Position::captured_piece(Move m) const { + Square to = to_sq(m); + return piece_on(type_of(m) == EN_PASSANT ? capture_square(to) : to); +} + inline const std::string Position::piece_to_partner() const { if (!st->capturedPiece) return std::string(); Color color = color_of(st->capturedPiece); @@ -1447,6 +1463,10 @@ inline void Position::remove_piece(Square s) { unpromotedBoard[s] = NO_PIECE; } +inline bool Position::is_initial_pawn(Piece pc, Square s) const { + return type_of(pc) == PAWN && rank_of(s) == relative_rank(color_of(pc), RANK_2, max_rank()); +} + inline void Position::move_piece(Square from, Square to) { Piece pc = board[from]; @@ -1500,6 +1520,35 @@ inline bool Position::allow_virtual_drop(Color c, PieceType pt) const { && count_in_hand(c, QUEEN) >= 0); } +inline bool Position::sudoku_boxes() const { + assert(var != nullptr); + return var->sudoku && var->sudokuBoxWidth && var->sudokuBoxHeight; +} + +inline int Position::sudoku_box_of(Square s) const { + assert(var != nullptr); + assert(sudoku_boxes()); + return rank_of(s) / var->sudokuBoxHeight * (files() / var->sudokuBoxWidth) + file_of(s) / var->sudokuBoxWidth; +} + +inline PieceType Position::piece_type_for_sudoku(Piece pc) const { + assert(var != nullptr); + PieceType pt = type_of(pc); + if (var->sudokuRoyalConflict && (pt == KING || pt == COMMONER)) return QUEEN; + return pt; +} + +inline int Position::allowed_sudoku_conflicts(PieceType pt) const { + assert(var != nullptr); + return pt == PAWN ? var->sudokuAllowedPawns : 1; +} + +inline int Position::sudoku_conflicts(Color c) const { + assert(var != nullptr); + assert(st != nullptr); + return var->sudoku ? st->sudokuConflictsCount[c] : 0; +} + inline Value Position::material_counting_result() const { auto weight_count = [this](PieceType pt, int v){ return v * (count(WHITE, pt) - count(BLACK, pt)); }; int materialCount; diff --git a/src/types.h b/src/types.h index b5812abf0..80ee4e844 100644 --- a/src/types.h +++ b/src/types.h @@ -587,6 +587,13 @@ struct DirtyPiece { /// avoid left-shifting a signed int to avoid undefined behavior. enum Score : int { SCORE_ZERO }; +enum SudokuHouse : int { + SH_FILE, + SH_RANK, + SH_BOX, + SH_NB, +}; + constexpr Score make_score(int mg, int eg) { return Score((int)((unsigned int)eg << 16) + mg); } diff --git a/src/variant.h b/src/variant.h index 98fe314bc..49774fce5 100644 --- a/src/variant.h +++ b/src/variant.h @@ -119,6 +119,11 @@ struct Variant { Rank soldierPromotionRank = RANK_1; EnclosingRule flipEnclosedPieces = NO_ENCLOSING; bool freeDrops = false; + bool sudoku = false; + int sudokuBoxWidth = 4; + int sudokuBoxHeight = 2; + int sudokuAllowedPawns = FILE_NB; + bool sudokuRoyalConflict = false; // game end PieceSet nMoveRuleTypes[COLOR_NB] = {piece_set(PAWN), piece_set(PAWN)}; diff --git a/src/variants.ini b/src/variants.ini index d905dcb03..56c1beedb 100644 --- a/src/variants.ini +++ b/src/variants.ini @@ -296,6 +296,11 @@ # adjudicateFullBoard: apply material counting immediately when board is full [bool] (default: false) # countingRule: enable counting rules [CountingRule] (default: none) # castlingWins: Specified castling moves are win conditions. Losing these rights is losing. [CastlingRights] (default: -) +# sudoku: enable sudoku variant - can't capture opponent's pieces when having sudoku conflicts on your pieces [bool] (default: false) +# sudokuBoxWidth: the width of sudoku box (0 to disable boxes) [int] (default: 4) +# sudokuBoxHeight: the height of sudoku box (0 to disable boxes) [int] (default: 2) +# sudokuAllowedPawns: how many pawns could be in a sudoku house without having a sudoku conflict [int] (default: unlimited) +# sudokuRoyalConflict: treat queen and king (or commoner) as one piece type for sudoku conflicts [bool] (default: false) ################################################ ### Example for minishogi configuration that would be equivalent to the built-in variant: @@ -2010,3 +2015,17 @@ passOnStalemate = true [andersanti:antichess] passOnStalemate = true + +[sudoku:chess] +king = - +commoner = k +castlingKingPiece = k +extinctionValue = loss +extinctionPieceTypes = k +sudoku = true + +[sudokupawns:sudoku] +sudokuAllowedPawns = 2 + +[sudokuroyal:sudokupawns] +sudokuRoyalConflict = true