From 8bcd3acbc19c480943f73404d5757475ee142335 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:16:47 +0200 Subject: [PATCH] Add promoted piece validation to FEN validation (#895) Fixes #416 --- .gitignore | 1 + src/apiutil.h | 46 +++++++++++++++++++++++++++++++++++++++++++++ src/pyffish.cpp | 1 + test.py | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 0 deletions(-) diff --git a/.gitignore b/.gitignore index b045cda..2b789a7 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ ffish.js venv *.so pyffish.egg-info +__pycache__/ # IDEs .vscode diff --git a/src/apiutil.h b/src/apiutil.h index 1886c54..f3003a9 100644 --- a/src/apiutil.h +++ b/src/apiutil.h @@ -422,6 +422,7 @@ namespace FEN { enum FenValidation : int { FEN_INVALID_COUNTING_RULE = -14, FEN_INVALID_CHECK_COUNT = -13, + FEN_INVALID_PROMOTED_PIECE = -12, FEN_INVALID_NB_PARTS = -11, FEN_INVALID_CHAR = -10, FEN_TOUCHING_KINGS = -9, @@ -545,6 +546,47 @@ inline Validation check_for_valid_characters(const std::string& firstFenPart, co return OK; } +inline Validation check_promoted_pieces(const std::string& firstFenPart, const Variant* v) { + // Only check promoted pieces if the variant supports shogi-style promotions + if (!v || !v->shogiStylePromotions) + return OK; + + for (size_t i = 0; i < firstFenPart.length() - 1; ++i) { + // Look for promoted pieces ('+' followed by piece character) + if (firstFenPart[i] == '+') { + char pieceChar = firstFenPart[i + 1]; + + // Skip if next character is not a piece character or is a special character + if (isdigit(pieceChar) || pieceChar == '/' || pieceChar == ' ' || pieceChar == '[') + continue; + + // Find the piece type corresponding to this character + size_t idx = v->pieceToChar.find(pieceChar); + if (idx == std::string::npos) { + // Try synonyms + idx = v->pieceToCharSynonyms.find(pieceChar); + if (idx == std::string::npos) + continue; // Character validation will catch this + } + + // Ensure idx is within valid range for piece types + if (idx >= PIECE_TYPE_NB) + continue; + + // Get the piece type directly from the index + PieceType pt = PieceType(idx); + + // Check if this piece type has a promoted form + if (pt != NO_PIECE_TYPE && pt < PIECE_TYPE_NB && v->promotedPieceType[pt] == NO_PIECE_TYPE) { + std::cerr << "Invalid promoted piece: '+' followed by '" << pieceChar + << "'. This piece cannot be promoted in variant." << std::endl; + return NOK; + } + } + } + return OK; +} + inline std::vector get_fen_parts(const std::string& fullFen, char delim) { std::vector fenParts; std::string curPart; @@ -953,6 +995,10 @@ inline FenValidation validate_fen(const std::string& fen, const Variant* v, bool if (check_for_valid_characters(fenParts[0], validSpecialCharactersFirstField, v) == NOK) return FEN_INVALID_CHAR; + // check for valid promoted pieces + if (check_promoted_pieces(fenParts[0], v) == NOK) + return FEN_INVALID_PROMOTED_PIECE; + // check for number of ranks const int nbRanks = v->maxRank + 1; // check for number of files diff --git a/src/pyffish.cpp b/src/pyffish.cpp index 67a177b..02da330 100644 --- a/src/pyffish.cpp +++ b/src/pyffish.cpp @@ -462,6 +462,7 @@ PyMODINIT_FUNC PyInit_pyffish() { // validation PyModule_AddObject(module, "FEN_OK", PyLong_FromLong(FEN::FEN_OK)); + PyModule_AddObject(module, "FEN_INVALID_PROMOTED_PIECE", PyLong_FromLong(FEN::FEN_INVALID_PROMOTED_PIECE)); // initialize stockfish pieceMap.init(); diff --git a/test.py b/test.py index 83f41c9..bef588e 100644 --- a/test.py +++ b/test.py @@ -1184,6 +1184,62 @@ class TestPyffish(unittest.TestCase): fen = sf.start_fen(variant) self.assertEqual(sf.validate_fen(fen, variant), sf.FEN_OK) + def test_validate_fen_promoted_pieces(self): + # Test promoted piece validation specifically + + # Valid promoted pieces should pass + valid_promoted_fens = { + "shogi": [ + "lnsgkgsnl/1r5b1/pppppp+ppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL[-] w - - 0 1", # promoted pawn + "lnsgkgsnl/1r5+b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL[-] w - - 0 1", # promoted bishop + "lnsgkgsnl/1+r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL[-] w - - 0 1", # promoted rook + "ln+sgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL[-] w - - 0 1", # promoted silver + "l+nsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL[-] w - - 0 1", # promoted knight + "+lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL[-] w - - 0 1", # promoted lance + ] + } + + # Invalid promoted pieces should fail with FEN_INVALID_PROMOTED_PIECE (-12) + invalid_promoted_fens = { + "kyotoshogi": [ + "p+nks+l/5/5/5/+LS+K+NP[-] w 0 1", # promoted king (+K) - kings cannot be promoted + ], + "shogi": [ + "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSG++KGSNL[-] w - - 0 1", # double promotion (++K) + ] + } + + # Non-shogi variants should ignore promoted piece syntax ('+' should be invalid character) + non_shogi_promoted_fens = { + "chess": [ + "rnb+qkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", # '+' not valid in chess + ] + } + + # Test valid promoted pieces + for variant, fens in valid_promoted_fens.items(): + for fen in fens: + with self.subTest(variant=variant, fen=fen, test_type="valid_promoted"): + result = sf.validate_fen(fen, variant) + self.assertEqual(result, sf.FEN_OK, f"Expected valid promoted piece FEN to pass: {fen}") + + # Test invalid promoted pieces (should return FEN_INVALID_PROMOTED_PIECE = -12) + for variant, fens in invalid_promoted_fens.items(): + for fen in fens: + with self.subTest(variant=variant, fen=fen, test_type="invalid_promoted"): + result = sf.validate_fen(fen, variant) + self.assertEqual(result, sf.FEN_INVALID_PROMOTED_PIECE, + f"Expected invalid promoted piece FEN to return -12: {fen}, got {result}") + + # Test non-shogi variants (should fail with character validation, not promoted piece validation) + for variant, fens in non_shogi_promoted_fens.items(): + for fen in fens: + with self.subTest(variant=variant, fen=fen, test_type="non_shogi"): + result = sf.validate_fen(fen, variant) + # Should fail with character validation (FEN_INVALID_CHAR = -10), not promoted piece validation + self.assertEqual(result, -10, + f"Expected non-shogi variant to fail with character error (-10): {fen}, got {result}") + def test_get_fog_fen(self): fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" # startpos result = sf.get_fog_fen(fen, "fogofwar") -- 1.7.0.4