Add promoted piece validation to FEN validation (#895)
authorCopilot <198982749+Copilot@users.noreply.github.com>
Mon, 11 Aug 2025 20:16:47 +0000 (22:16 +0200)
committerGitHub <noreply@github.com>
Mon, 11 Aug 2025 20:16:47 +0000 (22:16 +0200)
Fixes #416

.gitignore
src/apiutil.h
src/pyffish.cpp
test.py

index b045cda..2b789a7 100644 (file)
@@ -24,6 +24,7 @@ ffish.js
 venv
 *.so
 pyffish.egg-info
+__pycache__/
 
 # IDEs
 .vscode
index 1886c54..f3003a9 100644 (file)
@@ -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<std::string> get_fen_parts(const std::string& fullFen, char delim) {
     std::vector<std::string> 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
index 67a177b..02da330 100644 (file)
@@ -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 (file)
--- 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")