├── main.py ├── README.md ├── write_regex_json.py ├── test_pgn.py ├── compiler.py ├── chess_engine.py ├── instruction_set.py └── tests.py /main.py: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2025, Nicholas Carlini . 2 | ## 3 | ## This program is free software: you can redistribute it and/or modify 4 | ## it under the terms of the GNU General Public License as published by 5 | ## the Free Software Foundation, either version 3 of the License, or 6 | ## (at your option) any later version. 7 | ## 8 | ## This program is distributed in the hope that it will be useful, 9 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | ## GNU General Public License for more details. 12 | ## 13 | ## You should have received a copy of the GNU General Public License 14 | ## along with this program. If not, see . 15 | 16 | import readline 17 | import json 18 | import re 19 | 20 | state = '' 21 | regexs = json.load(open("regex-chess.json")) 22 | while 'Game over' not in state: 23 | for pattern, repl in regexs: 24 | state = re.sub(pattern, repl, state) 25 | print(state, end="") 26 | state += input() + "\n" 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RegexChess 2 | 3 | RegexChess is a complete chess engine that plays moves making regular expression transforms 4 | to a given board state. It implements a 2-ply minimax search algorithm and generates moves 5 | in about 5-10 seconds. 6 | 7 | ## I just want to play a game 8 | 9 | You probably don't. It's really not good. But [here](https://nicholas.carlini.com/writing/2025/regex-chess.html) is a link to a JavaScript frontend that 10 | plays the engine. If you want to run it on your own machine, all you have to do is clone this 11 | project and then run `python3 main.py`. It's actually a very simple file: 12 | 13 | ```python 14 | import readline 15 | import json 16 | import re 17 | 18 | state = '' 19 | regexs = json.load(open("regex-chess.json")) 20 | while 'Game over' not in state: 21 | for pattern, repl in regexs: 22 | state = re.sub(pattern, repl, state) 23 | print(state, end="") 24 | state += input() + "\n" 25 | ``` 26 | 27 | ## How does it work? 28 | 29 | It's complicated. See [this article on my website](https://nicholas.carlini.com/writing/2025/regex-chess.html) for a long writeup. 30 | 31 | 32 | ## License 33 | 34 | GPL v3 -------------------------------------------------------------------------------- /write_regex_json.py: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2025, Nicholas Carlini . 2 | ## 3 | ## This program is free software: you can redistribute it and/or modify 4 | ## it under the terms of the GNU General Public License as published by 5 | ## the Free Software Foundation, either version 3 of the License, or 6 | ## (at your option) any later version. 7 | ## 8 | ## This program is distributed in the hope that it will be useful, 9 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | ## GNU General Public License for more details. 12 | ## 13 | ## You should have received a copy of the GNU General Public License 14 | ## along with this program. If not, see . 15 | 16 | import sys 17 | import json 18 | import re 19 | import argparse 20 | from pathlib import Path 21 | from typing import List, Tuple, Any 22 | 23 | from compiler import re_compile 24 | from chess_engine import make_reply_move 25 | 26 | def escape_non_ascii(match): 27 | return '\\u{:04x}'.format(ord(match.group())) 28 | 29 | def write_js_output(args: List[Tuple[str, List[Tuple[str, str]]]], outfile: str): 30 | """Write JavaScript format output""" 31 | state = ''' ╔═════════════════╗ 32 | 8 ║ ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ ║ 33 | 7 ║ ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ ║ 34 | 6 ║ ║ 35 | 5 ║ ║ 36 | 4 ║ ║ 37 | 3 ║ ║ 38 | 2 ║ ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ ║ 39 | 1 ║ ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ ║ 40 | ╚═════════════════╝ 41 | a b c d e f g h 42 | 43 | Move notation: [src][dest] (e.g. e2e4) or 'q' to quit 44 | [Castling Rights: KQkq, En Passant: -] 45 | Enter Your Move: ''' 46 | 47 | with open(outfile, 'w', encoding='utf-8') as f: 48 | f.write(f'let initialState = {json.dumps(state)};\n') 49 | f.write('let regexOperation = [\n') 50 | 51 | for op, regexs in args: 52 | for pattern, repl in regexs: 53 | repl = re.sub(r'\\(\d+)', r'$\1', repl) 54 | 55 | repl = re.sub(r'\\g<(\d+)>', r'$\1', repl) 56 | 57 | # Handle pattern escaping 58 | pattern = pattern.replace("\n", "\\n").replace("/",r"\/") 59 | if pattern[0] == '^': 60 | flags = "/g" 61 | else: 62 | flags = "/gm" 63 | content = ("['" + str(op).replace("'",'"') + "', /"+pattern+flags + ", " + json.dumps(repl).replace(r"\\n", r"\n") + "],") 64 | converted = re.sub(r'[^\x00-\x7F]', escape_non_ascii, content) 65 | 66 | f.write(converted + '\n') 67 | 68 | f.write(']\n') 69 | 70 | def write_json_output(args: List[Tuple[str, List[Tuple[str, str]]]], outfile: str): 71 | """Write JSON format output""" 72 | operations = [] 73 | 74 | operations.append(['^$', '<']) 75 | 76 | for op, regexs in args: 77 | for pattern, repl in regexs: 78 | regex_op = [pattern, 79 | repl, 80 | ] 81 | operations.append(regex_op) 82 | 83 | operations.append(['^<$', ''' ╔═════════════════╗ 84 | 8 ║ ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜ ║ 85 | 7 ║ ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟ ║ 86 | 6 ║ ║ 87 | 5 ║ ║ 88 | 4 ║ ║ 89 | 3 ║ ║ 90 | 2 ║ ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙ ║ 91 | 1 ║ ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖ ║ 92 | ╚═════════════════╝ 93 | a b c d e f g h 94 | 95 | Move notation: [src][dest] (e.g. e2e4) or 'q' to quit 96 | [Castling Rights: KQkq, En Passant: -] 97 | Enter Your Move: ''']) 98 | 99 | with open(outfile, 'w', encoding='utf-8') as f: 100 | json.dump(operations, f, indent=2, ensure_ascii=False) 101 | 102 | def main(): 103 | # Parse command line arguments 104 | parser = argparse.ArgumentParser(description='Generate regex patterns file') 105 | parser.add_argument('outfile', help='Output file path (.js or .json)') 106 | args = parser.parse_args() 107 | 108 | # Generate the regex tree 109 | regex_args = re_compile(lambda x: make_reply_move(x)) 110 | 111 | # Determine output format based on file extension 112 | file_ext = Path(args.outfile).suffix.lower() 113 | 114 | if file_ext == '.js': 115 | write_js_output(regex_args, args.outfile) 116 | elif file_ext == '.json': 117 | write_json_output(regex_args, args.outfile) 118 | else: 119 | print(f"Error: Unsupported file extension '{file_ext}'. Use .js or .json", file=sys.stderr) 120 | sys.exit(1) 121 | 122 | if __name__ == "__main__": 123 | main() 124 | -------------------------------------------------------------------------------- /test_pgn.py: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2025, Nicholas Carlini . 2 | ## 3 | ## This program is free software: you can redistribute it and/or modify 4 | ## it under the terms of the GNU General Public License as published by 5 | ## the Free Software Foundation, either version 3 of the License, or 6 | ## (at your option) any later version. 7 | ## 8 | ## This program is distributed in the hope that it will be useful, 9 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | ## GNU General Public License for more details. 12 | ## 13 | ## You should have received a copy of the GNU General Public License 14 | ## along with this program. If not, see . 15 | 16 | import random 17 | import unittest 18 | import chess 19 | import chess.pgn 20 | import io 21 | from instruction_set import * 22 | from compiler import * 23 | from chess_engine import * 24 | 25 | def extract_legal_boards(state_str): 26 | """ 27 | Extract all board positions from the next_boards variable in the state string. 28 | 29 | Args: 30 | state_str: String containing the state with #next_boards: variable 31 | 32 | Returns: 33 | set: Set of board positions (FEN format without turn indicator) 34 | """ 35 | 36 | if '#next_boards:' in state_str: 37 | next_boards_lines = [line for line in state_str.split('\n') 38 | if '#next_boards:' in line] 39 | 40 | if not next_boards_lines: 41 | raise ValueError("No #next_boards: variable found in state string") 42 | 43 | if len(next_boards_lines) > 1: 44 | print("Warning: Multiple #next_boards: lines found, using first one") 45 | 46 | next_boards_line = next_boards_lines[0] 47 | 48 | try: 49 | _, boards_str = next_boards_line.split(':', 1) 50 | except ValueError: 51 | raise ValueError("Invalid #next_boards line format - missing colon") 52 | 53 | boards_str = boards_str.strip() 54 | if not boards_str: 55 | return set() 56 | 57 | board_positions = [] 58 | for board in boards_str.split(';'): 59 | board = board.strip() 60 | if board: 61 | board_positions.append(board) 62 | 63 | return set(board_positions) 64 | else: 65 | next_boards_lines = [line for line in state_str.split('\n') 66 | if '#initial_board:' in line] 67 | return set([x.split(": ")[1] for x in next_boards_lines]) 68 | 69 | 70 | class ChessGameTester: 71 | """Helper class to test move generation for any chess game.""" 72 | 73 | def __init__(self): 74 | self.test_case = unittest.TestCase() 75 | 76 | def verify_legal_moves(self, fen): 77 | """ 78 | Verify that compute_next_boards generates all and only pseudo-legal moves for a position. 79 | 80 | Args: 81 | fen: Starting position in FEN format 82 | """ 83 | state = { 84 | "stack": [], 85 | "initial_board": fen 86 | } 87 | 88 | turn = fen.split()[1] 89 | color_one = color_white if turn == 'w' else color_black 90 | color_two = color_black if turn == 'w' else color_white 91 | 92 | tree = trace(lambda x: compute_legal_boards(x, color_one, color_two)) 93 | linear = linearize_tree(tree) 94 | args = create(linear) 95 | 96 | state_str = "%%\n#stack:\n#initial_board: " + fen + "\n" 97 | 98 | for op, regexs in args: 99 | for pattern, repl in regexs: 100 | state_str = re.sub(pattern, repl, state_str) 101 | 102 | our_moves = extract_legal_boards(state_str) 103 | our_moves = set(x for x in our_moves if x.split()[0] != fen.split()[0]) 104 | our_moves = set(x.replace(" b ", " w ") for x in our_moves) 105 | 106 | 107 | board = chess.Board(fen) 108 | python_chess_moves = set() 109 | for move in board.legal_moves: 110 | 111 | if move.promotion and move.promotion != chess.QUEEN: 112 | continue 113 | board_copy = board.copy() 114 | board_copy.push(move) 115 | python_chess_moves.add(" ".join(board_copy.fen().split(" ")[:-2]).replace(" b "," w ")) 116 | 117 | 118 | 119 | print(f"Position: {fen}, Our moves: {len(our_moves)}, Python-chess moves: {len(python_chess_moves)}") 120 | 121 | for x in our_moves: 122 | self.test_case.assertTrue(x in python_chess_moves or x.rpartition(" ")[0]+" -" in python_chess_moves, f"Position: {fen}, Our move: {x} not found in Python-chess moves:" + str(python_chess_moves)) 123 | 124 | self.test_case.assertEqual(len(our_moves), len(python_chess_moves), f"Position: {fen}, Our moves: {len(our_moves)}, Python-chess moves: {len(python_chess_moves)}") 125 | 126 | 127 | def test_game(self, pgn_str, game_name="Unnamed Game"): 128 | """ 129 | Test move generation for all positions in a chess game. 130 | 131 | Args: 132 | pgn_str: String containing the game in PGN format 133 | game_name: Name of the game for reporting purposes 134 | """ 135 | print(f"\nTesting positions from: {game_name}") 136 | print("PGN string length:", len(pgn_str)) 137 | 138 | # Normalize line endings and remove any BOM 139 | pgn_str = pgn_str.strip().replace('\r\n', '\n') 140 | if pgn_str.startswith('\ufeff'): 141 | pgn_str = pgn_str[1:] 142 | 143 | game = chess.pgn.read_game(io.StringIO(pgn_str)) 144 | if game is None: 145 | print(f"ERROR: Failed to parse PGN for game: {game_name}") 146 | print("Raw PGN string:") 147 | print(repr(pgn_str)) 148 | raise ValueError(f"Failed to parse PGN for game: {game_name}") 149 | 150 | board = game.board() 151 | move_count = 0 152 | positions_tested = 0 153 | 154 | # Test starting position 155 | print("Testing initial position") 156 | self.verify_legal_moves(board.fen()) 157 | positions_tested += 1 158 | 159 | # Test each position in the game 160 | mainline_moves = list(game.mainline_moves()) 161 | print(f"Found {len(mainline_moves)} moves in the game") 162 | print("Headers:", game.headers) 163 | print("First move:", game.next() if game.next() else "No moves found") 164 | for move in mainline_moves: 165 | move_count += 1 166 | 167 | # Get FEN before making the move 168 | fen = board.fen() 169 | print(f"Testing position after move {move_count}: {move.uci()}") 170 | self.verify_legal_moves(fen) 171 | positions_tested += 1 172 | 173 | # Make the move and continue 174 | board.push(move) 175 | 176 | print(f"Successfully tested {positions_tested} positions from {game_name} ({move_count} moves)") 177 | 178 | class ChessGameTests(unittest.TestCase): 179 | def setUp(self): 180 | self.tester = ChessGameTester() 181 | 182 | def test_all_games(self): 183 | games = open("Carlsen.pgn").read() 184 | 185 | game_list = [game for game in games.split("\n\n") if '[' not in game] 186 | random.shuffle(game_list) 187 | 188 | for game in game_list: 189 | print('run on', repr(game)) 190 | self.tester.test_game(game, "Carlsen Game") 191 | 192 | 193 | def test_scholars_mate(self): 194 | pgn = """ 195 | [Event "Scholar's Mate Example"] 196 | [Result "1-0"] 197 | 1. e4 e5 2. Qh5 Nc6 3. Bc4 Nf6?? 4. Qxf7# 1-0 198 | """ 199 | self.tester.test_game(pgn, "Scholar's Mate") 200 | 201 | def test_morphy_opera_game(self): 202 | pgn = """[Event "Opera Game"] 203 | [Site "Paris FRA"] 204 | [Date "1858.??.??"] 205 | [Round "?"] 206 | [White "Morphy, Paul"] 207 | [Black "Duke of Brunswick & Count Isouard"] 208 | [Result "1-0"] 209 | 210 | 1. e4 e5 2. Nf3 d6 3. d4 Bg4 4. dxe5 Bxf3 5. Qxf3 dxe5 6. Bc4 Nf6 7. Qb3 Qe7 8. Nc3 c6 9. Bg5 b5 10. Nxb5 cxb5 11. Bxb5+ Nbd7 12. O-O-O Rd8 13. Rxd7 Rxd7 14. Rd1 Qe6 15. Bxd7+ Nxd7 16. Qb8+ Nxb8 17. Rd8# 1-0""" 211 | self.tester.test_game(pgn, "Morphy's Opera Game") 212 | 213 | def test_immortal_game(self): 214 | pgn = """[Event "Casual Game"] 215 | [Site "London ENG"] 216 | [Date "1851.06.21"] 217 | [Round "?"] 218 | [White "Anderssen, Adolf"] 219 | [Black "Kieseritzky, Lionel"] 220 | [Result "1-0"] 221 | 222 | 1. e4 e5 2. f4 exf4 3. Bc4 Qh4+ 4. Kf1 b5 5. Bxb5 Nf6 6. Nf3 Qh6 7. d3 Nh5 8. Nh4 Qg5 9. Nf5 c6 10. g4 Nf6 11. Rg1 cxb5 12. h4 Qg6 13. h5 Qg5 14. Qf3 Ng8 15. Bxf4 Qf6 16. Nc3 Bc5 17. Nd5 Qxb2 18. Bd6 Bxg1 19. e5 Qxa1+ 20. Ke2 Na6 21. Nxg7+ Kd8 22. Qf6+ Nxf6 23. Be7# 1-0""" 223 | self.tester.test_game(pgn, "Anderssen's Immortal Game") 224 | 225 | def test_game_of_century(self): 226 | pgn = """[Event "Third Rosenwald Trophy"] 227 | [Site "New York, NY USA"] 228 | [Date "1956.10.17"] 229 | [Round "8"] 230 | [White "Byrne, Donald"] 231 | [Black "Fischer, Robert James"] 232 | [Result "0-1"] 233 | 234 | 1. Nf3 Nf6 2. c4 g6 3. Nc3 Bg7 4. d4 O-O 5. Bf4 d5 6. Qb3 dxc4 7. Qxc4 c6 8. e4 Nbd7 9. Rd1 Nb6 10. Qc5 Bg4 11. Bg5 Na4 12. Qa3 Nxc3 13. bxc3 Nxe4 14. Bxe7 Qb6 15. Bc4 Nxc3 16. Bc5 Rfe8+ 17. Kf1 Be6 18. Bxb6 Bxc4+ 19. Kg1 Ne2+ 20. Kf1 Nxd4+ 21. Kg1 Ne2+ 22. Kf1 Nc3+ 23. Kg1 axb6 24. Qb4 Ra4 25. Qxb6 Nxd1 26. h3 Rxa2 27. Kh2 Nxf2 28. Re1 Rxe1 29. Qd8+ Bf8 30. Nxe1 Bd5 31. Nf3 Ne4 32. Qb8 b5 33. h4 h5 34. Ne5 Kg7 35. Kg1 Bc5+ 36. Kf1 Ng3+ 37. Ke1 Bb4+ 38. Kd1 Bb3+ 39. Kc1 Ne2+ 40. Kb1 Nc3+ 41. Kc1 Rc2# 0-1""" 235 | self.tester.test_game(pgn, "Fischer's Game of the Century") 236 | 237 | def run_tests(): 238 | suite = unittest.TestLoader().loadTestsFromTestCase(ChessGameTests) 239 | runner = unittest.TextTestRunner(failfast=True, verbosity=2) 240 | result = runner.run(suite) 241 | 242 | if not result.wasSuccessful(): 243 | failure = result.failures[0] if result.failures else result.errors[0] 244 | test_case, traceback = failure 245 | print("\n\nFirst failing test:", test_case) 246 | print("\nError details:") 247 | print(traceback) 248 | exit(1) 249 | 250 | if __name__ == '__main__': 251 | run_tests() 252 | -------------------------------------------------------------------------------- /compiler.py: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2025, Nicholas Carlini . 2 | ## 3 | ## This program is free software: you can redistribute it and/or modify 4 | ## it under the terms of the GNU General Public License as published by 5 | ## the Free Software Foundation, either version 3 of the License, or 6 | ## (at your option) any later version. 7 | ## 8 | ## This program is distributed in the hope that it will be useful, 9 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | ## GNU General Public License for more details. 12 | ## 13 | ## You should have received a copy of the GNU General Public License 14 | ## along with this program. If not, see . 15 | 16 | from instruction_set import * 17 | 18 | class CallTree: 19 | def __init__(self, tree=[], pointer=0): 20 | self.tree = tree 21 | self.active_path = self.tree 22 | self.pointer = 0 23 | self.pointer_hist = [] 24 | self.active_path_hist = [] 25 | 26 | def append(self, node): 27 | if self.pointer < len(self.active_path): 28 | assert node == self.active_path[self.pointer] 29 | else: 30 | self.active_path.append(node) 31 | self.pointer += 1 32 | 33 | def branch(self, value): 34 | ret_val = True 35 | #print("Branch") 36 | self.active_path_hist.append(self.active_path) 37 | self.pointer_hist.append(self.pointer) 38 | if self.pointer < len(self.active_path): 39 | # we've created the tree structure at least once before 40 | assert self.active_path[self.pointer][0] == 'branch' 41 | if not self.traverse(self.active_path[self.pointer][2][0]): 42 | if self.active_path[self.pointer][2][0] is None: 43 | self.active_path[self.pointer][2][0] = [] 44 | self.active_path = self.active_path[self.pointer][2][0] 45 | else: 46 | #print("Finished along", self.active_path[self.pointer][2][0]) 47 | if self.active_path[self.pointer][2][1] is None: 48 | self.active_path[self.pointer][2][1] = [] 49 | self.active_path = self.active_path[self.pointer][2][1] 50 | ret_val = False 51 | else: 52 | #print("Do append") 53 | self.active_path.append(('branch', value, [[], None])) 54 | self.active_path = self.active_path[-1][2][0] 55 | self.pointer = 0 56 | return ret_val 57 | 58 | 59 | def merge(self): 60 | [*self.pointer_hist, self.pointer] = self.pointer_hist 61 | [*self.active_path_hist, self.active_path] = self.active_path_hist 62 | self.pointer += 1 63 | 64 | def traverse(self, path): 65 | if path is None: return False 66 | for node in path: 67 | if isinstance(node, (list, tuple)) and node[0] == 'branch': 68 | children = node[2] 69 | if len(children) != 2: 70 | print(f"Invalid branch structure: {node}") 71 | return False 72 | left, right = children 73 | if left is None or right is None: 74 | #print(f"Branch with incomplete children found: {node}") 75 | return False 76 | # Recursively check both left and right subtrees 77 | if not self.traverse(left): 78 | return False 79 | if not self.traverse(right): 80 | return False 81 | return True 82 | 83 | def is_complete(self): 84 | """ 85 | Check if the CallTree is fully constructed, i.e., there are no branches with None leaves. 86 | 87 | Returns: 88 | bool: True if the tree is complete, False otherwise. 89 | """ 90 | 91 | return traverse(self.tree) and len(self.tree) > 0 92 | 93 | 94 | class Tracer: 95 | def __init__(self, history, value, kind): 96 | self.history = history 97 | self.value = value 98 | self.kind = kind 99 | 100 | def ite(self): 101 | return self.history.branch(self) 102 | 103 | def __eq__(self, other): 104 | return Tracer(self.history, ("==", self, other), "bool") 105 | 106 | def __ne__(self, other): 107 | return Tracer(self.history, ("!=", self, other), "bool") 108 | 109 | def fen(self): 110 | return Tracer(self.history, ("fen", self), "str") 111 | 112 | def __add__(self, other): 113 | if self.kind == 'str': 114 | return Tracer(self.history, ("strcat", self, other), self.kind) 115 | else: 116 | if isinstance(other, int): 117 | if other < 0: 118 | return Tracer(self.history, ("-", self, -other), self.kind) 119 | else: 120 | return Tracer(self.history, ("+", self, other), self.kind) 121 | else: 122 | return Tracer(self.history, ("+", self, other), self.kind) 123 | 124 | 125 | def __sub__(self, other): 126 | return Tracer(self.history, ("-", self, other), "int") 127 | 128 | def __gt__(self, other): 129 | return Tracer(self.history, (">", self, other), "bool") 130 | 131 | def __lt__(self, other): 132 | return Tracer(self.history, ("<", self, other), "bool") 133 | 134 | def __ge__(self, other): 135 | return Tracer(self.history, (">=", self, other), "bool") 136 | 137 | def __le__(self, other): 138 | return Tracer(self.history, ("<=", self, other), "bool") 139 | 140 | def __and__(self, other): 141 | return Tracer(self.history, ("and", self, other), "bool") 142 | 143 | def __or__(self, other): 144 | return Tracer(self.history, ("or", self, other), "bool") 145 | 146 | def __invert__(self): 147 | return Tracer(self.history, ("not", self), "bool") 148 | 149 | def __mod__(self, other): 150 | assert other == 2 151 | return Tracer(self.history, ("%2", self), "bool") 152 | 153 | def isany(self, other): 154 | return Tracer(self.history, ("isany", self, other), "bool") 155 | 156 | 157 | 158 | class VarTracer: 159 | def __init__(self): 160 | self.history = CallTree(tree=[]) 161 | self.types = {} 162 | 163 | def __getitem__(self, key): 164 | if isinstance(key, Tracer): 165 | return Tracer(self.history, ("indirect_lookup", key), 'str') 166 | 167 | return Tracer(self.history, ("lookup", key), self.types.get(key) or 'str') 168 | 169 | def settype(self, key, kind): 170 | self.types[key] = kind 171 | 172 | def __setitem__(self, key, value): 173 | if isinstance(value, Tracer): 174 | kind = value.kind 175 | elif isinstance(value, int): 176 | kind = 'int' 177 | elif isinstance(value, str): 178 | kind = 'str' 179 | else: 180 | print("UNKNOWN", value) 181 | raise 182 | self.types[key] = kind 183 | self.history.append(("assign", key, value)) 184 | return Tracer(self.history, None, None) 185 | 186 | def merge(self): 187 | self.history.merge() 188 | 189 | def cond(self, other, tag): 190 | self.history.append(("cond", other, tag)) 191 | 192 | def fork_bool(self, var): 193 | self.history.append(("fork_bool", var)) 194 | 195 | def __getattr__(self, key): 196 | if key in INSTRUCTIONS: 197 | def fn(*args): 198 | self.history.append((key, *args)) 199 | return Tracer(self.history, ('nop',), 'str') 200 | return fn 201 | else: 202 | raise 203 | 204 | 205 | def trace(function): 206 | tracer = VarTracer() 207 | #while not tracer.history.is_complete(): 208 | for _ in range(10): 209 | tracer.history.pointer = 0 210 | function(tracer) 211 | return (tracer.history.tree) 212 | 213 | 214 | 215 | def linearize_expr(value): 216 | if isinstance(value, int): 217 | return [("push", value)] 218 | if isinstance(value, str): 219 | return [("push", value)] 220 | if isinstance(value, Tracer): 221 | value = value.value 222 | 223 | op = value[0] 224 | 225 | remap = {"==": "eq", 226 | "!=": "neq", 227 | "strcat": "string_cat", 228 | 'and': 'boolean_and', 229 | 'or': 'boolean_or', 230 | } 231 | 232 | remap_cmp = { 233 | "<": "less_than", 234 | ">": "greater_than", 235 | "<=": "less_equal_than", 236 | ">=": "greater_equal_than" 237 | } 238 | 239 | unary_ops = {"+": "add_unary", 240 | "-": "sub_unary", 241 | } 242 | 243 | if op in remap: 244 | return [*linearize_expr(value[2]), 245 | *linearize_expr(value[1]), 246 | (remap[op],)] 247 | elif op in remap_cmp: 248 | return [*linearize_expr(value[2]), 249 | ('to_unary',), 250 | *linearize_expr(value[1]), 251 | ('to_unary',), 252 | (remap_cmp[op],)] 253 | elif op == "not": 254 | return [*linearize_expr(value[1]), ("boolean_not",)] 255 | elif op == '+': 256 | return [*linearize_expr(value[1]), 257 | *linearize_expr(value[2]), 258 | ('binary_add',) 259 | ] 260 | elif op == '-': 261 | return [*linearize_expr(value[2]), 262 | *linearize_expr(value[1]), 263 | ('binary_subtract',) 264 | ] 265 | elif op in unary_ops: 266 | return [*linearize_expr(value[1]), 267 | ('to_unary',), 268 | *linearize_expr(value[2]), 269 | ('to_unary',), 270 | (unary_ops[op],), 271 | ('from_unary',) 272 | ] 273 | elif op == '%2': 274 | return [*linearize_expr(value[1]), 275 | ('to_unary',), 276 | ("mod2_unary",), 277 | ] 278 | elif op == "lookup": 279 | return [("lookup", value[1])] 280 | elif op == "indirect_lookup": 281 | return [("lookup", value[1].value[1]), 282 | ("indirect_lookup",)] 283 | elif op == "isany": 284 | return [*linearize_expr(value[1]), 285 | ('isany', value[2])] 286 | elif op == "fen": 287 | return [*linearize_expr(value[1]), 288 | ('fen',)] 289 | elif op == 'nop': 290 | return [] 291 | else: 292 | raise ValueError(f"Unknown operation: {op}") 293 | 294 | 295 | def linearize_tree(call_tree): 296 | """ 297 | Given a call tree (a nested structure of tuples like ('assign', key, value), 298 | ('lookup', key), ('branch', [left, right]), and possibly other operations), 299 | produce a linear sequence of instructions as tuples. 300 | """ 301 | 302 | tag_counter = [0] 303 | def next_tag(): 304 | tag = f"UID{tag_counter[0]}" 305 | tag_counter[0] += 1 306 | return tag 307 | 308 | def linearize_subtree(subtree): 309 | instructions = [] 310 | for node in subtree: 311 | if isinstance(node, tuple): 312 | op = node[0] 313 | 314 | # Handle known operations 315 | if op == "assign": 316 | # node = ("assign", key, value) 317 | _, key, value = node 318 | if isinstance(value, Tracer): 319 | instructions.extend(linearize_expr(value.value)) 320 | else: 321 | instructions.append(("push", value)) 322 | instructions.append(('assign_pop', key)) 323 | 324 | elif op == "lookup": 325 | # node = ("lookup", key) 326 | _, key = node 327 | instructions.append(('lookup', key)) 328 | 329 | elif op == "branch": 330 | # node = ("branch", [left_subtree, right_subtree]) 331 | _, value, (left_subtree, right_subtree) = node 332 | tag1 = next_tag() 333 | tag2 = next_tag() 334 | instructions.extend(linearize_expr(value)) 335 | # cond(tag1) 336 | instructions.append(('cond', tag1)) 337 | # true branch 338 | instructions.extend(linearize_subtree(left_subtree)) 339 | 340 | else_case = linearize_subtree(right_subtree) 341 | 342 | if len(else_case) > 0: 343 | instructions.append(('pause', tag2)) 344 | 345 | instructions.append(('reactivate', tag1)) 346 | 347 | # false branch 348 | instructions.extend(else_case) 349 | # reactivate(tag2) 350 | instructions.append(('reactivate', tag2)) 351 | else: 352 | instructions.append(('reactivate', tag1)) 353 | 354 | elif op == "reactivate": 355 | # node = ("reactivate", tag) 356 | _, tag = node 357 | instructions.append(('reactivate', tag)) 358 | elif op == "fork_bool": 359 | _, tag = node 360 | instructions.append(('fork_bool', tag)) 361 | 362 | elif op == "cond": 363 | _, node, tag = node 364 | instructions.extend(linearize_expr(node)) 365 | instructions.append(('cond', tag)) 366 | 367 | elif op == "fork_with_new_var": 368 | _, tag, vars = node 369 | instructions.append(('fork_with_new_var', tag, vars)) 370 | 371 | elif op in ['assign_pop', "intxy_to_location", "push", "indirect_assign", "destroy_active_threads", "pause", "join_pop", "contract_chess", "expand_chess", "list_pop", "fork_inactive", "variable_uniq", "make_pretty", "unpretty", 'is_stack_empty', 'pop', 'peek', 'fork_list_pop', 'delete_var', 'check_king_alive', 'keep_only_first_thread', 'keep_only_max_thread', 'keep_only_last_thread', 'keep_only_min_thread', 'illegal_move', 'test_checkmate', 'square_to_xy', 'do_piece_assign', 'assign_stack_to', 'piece_value', "fix_double_list", 'binary_subtract', 'swap', 'is_same_kind', 'boolean_and', 'binary_add', 'sub_unary', 'promote_to_queen']: 372 | _, *args = node 373 | instructions.append((op, *args)) 374 | elif op == 'nop': 375 | pass 376 | else: 377 | # Unknown node type 378 | raise ValueError(f"Unknown node type: {op}") 379 | else: 380 | # If not a tuple, unexpected structure 381 | raise ValueError(f"Unexpected node structure: {node}") 382 | return instructions 383 | 384 | return linearize_subtree(call_tree) 385 | 386 | def create(sequence): 387 | out = [] 388 | for op,*args in sequence: 389 | out.append(((op, args), eval(op)(*args))) 390 | 391 | #out2 = [] 392 | #for (op, re_pairs) in out: 393 | # out2.append((op, [(re.compile(x), y) for x,y in re_pairs])) 394 | 395 | return out 396 | 397 | def re_compile(fn): 398 | tree = trace(fn) 399 | linear = linearize_tree(tree) 400 | args = create(linear) 401 | 402 | return args 403 | -------------------------------------------------------------------------------- /chess_engine.py: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2025, Nicholas Carlini . 2 | ## 3 | ## This program is free software: you can redistribute it and/or modify 4 | ## it under the terms of the GNU General Public License as published by 5 | ## the Free Software Foundation, either version 3 of the License, or 6 | ## (at your option) any later version. 7 | ## 8 | ## This program is distributed in the hope that it will be useful, 9 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | ## GNU General Public License for more details. 12 | ## 13 | ## You should have received a copy of the GNU General Public License 14 | ## along with this program. If not, see . 15 | 16 | from instruction_set import * 17 | 18 | def color_black(piece, inv=False): 19 | if isinstance(piece, str): 20 | return piece.upper() if inv else piece.lower() 21 | elif isinstance(piece, list): 22 | return [color_black(p, inv) for p in piece] 23 | 24 | def color_white(piece, inv=False): 25 | if isinstance(piece, str): 26 | return piece.lower() if inv else piece.upper() 27 | elif isinstance(piece, list): 28 | return [color_white(p, inv) for p in piece] 29 | 30 | def join_sub_to_main(variables, maxn, main="main", sub="sub"): 31 | variables.pause(sub) 32 | variables.reactivate(main) 33 | for _ in range(maxn): 34 | variables.join_pop(sub) 35 | 36 | 37 | def king_moves(variables, color): 38 | variables.lookup('initial_board') 39 | variables.expand_chess() 40 | variables['king'] = color('k') 41 | for ii,i in enumerate('abcdefgh'): 42 | for j in range(1,9): 43 | if (variables[str(i)+str(j)] == variables['king']).ite(): 44 | variables['kingx'] = ii 45 | variables['kingy'] = j-1 46 | variables['kingpos'] = str(i)+str(j) 47 | variables.merge() 48 | 49 | def make_move(dx, dy): 50 | if dy == -1: dy = 7 51 | if dx == -1: dx = 7 52 | variables.fork_with_new_var('inactive', 53 | {"dy": i2s(dy), 54 | "dx": i2s(dx)}) 55 | 56 | 57 | if (variables['kingx'] > 0).ite(): 58 | make_move(-1, 0) 59 | if (variables['kingy'] > 0).ite(): 60 | make_move(-1, -1) 61 | variables.merge() 62 | if (variables['kingy'] < 7).ite(): 63 | make_move(-1, 1) 64 | variables.merge() 65 | variables.merge() 66 | if (variables['kingx'] < 7).ite(): 67 | make_move(1, 0) 68 | if (variables['kingy'] > 0).ite(): 69 | make_move(1, -1) 70 | variables.merge() 71 | if (variables['kingy'] < 7).ite(): 72 | make_move(1, 1) 73 | variables.merge() 74 | variables.merge() 75 | 76 | if (variables['kingy'] > 0).ite(): 77 | make_move(0, -1) 78 | variables.merge() 79 | if (variables['kingy'] < 7).ite(): 80 | make_move(0, 1) 81 | variables.merge() 82 | 83 | variables.pause("main") 84 | variables.reactivate("inactive") 85 | 86 | variables['kingx'] = variables['kingx'] + variables['dx'] 87 | variables['kingy'] = variables['kingy'] + variables['dy'] 88 | 89 | if (variables['kingx'] >= 8).ite(): 90 | variables['kingx'] = variables['kingx'] - 8 91 | variables.merge() 92 | 93 | if (variables['kingy'] >= 8).ite(): 94 | variables['kingy'] = variables['kingy'] - 8 95 | variables.merge() 96 | 97 | variables.intxy_to_location('kingx', 'kingy') 98 | variables.assign_pop("newking") 99 | if (variables[variables['newking']].isany(color([' ', 'K', 'Q', 'R', 'B', 'N', 'P'], inv=True))).ite(): 100 | variables.lookup("newking") 101 | variables.push(color("k")) 102 | variables.indirect_assign() 103 | variables.lookup("kingpos") 104 | variables.push(" ") 105 | variables.indirect_assign() 106 | else: 107 | variables.destroy_active_threads() 108 | variables.merge() 109 | 110 | variables["ep"] = '-' 111 | 112 | variables.contract_chess() 113 | join_sub_to_main(variables, 8) 114 | 115 | variables.assign_stack_to('legal_king_moves', 8) 116 | 117 | variables.delete_var('kingx') 118 | variables.delete_var('kingy') 119 | variables.delete_var('kingpos') 120 | variables.delete_var('king') 121 | variables.contract_chess() 122 | variables.pop() 123 | 124 | return variables 125 | 126 | def rook_moves(variables, color, rook_piece='r'): 127 | return bishop_moves(variables, color, rook_piece, 'rook', dydx=[(7, 0), (1, 0), (0, 1), (0, 7)]) 128 | 129 | 130 | def bishop_moves(variables, color, bishop_piece='b', name='bishop', dydx=[(7, 7), (7, 1), (1, 7), (1, 1)]): 131 | # This is a brutal function, I'm very sorry if you're reading this. 132 | 133 | variables.lookup('initial_board') 134 | variables.expand_chess() 135 | 136 | 137 | # Initialize lists to hold bishop positions 138 | variables[name+'x_lst'] = '' 139 | variables[name+'y_lst'] = '' 140 | variables[name+'pos_lst'] = '' 141 | variables[name+'piece_lst'] = '' 142 | 143 | # Identify positions of all bishops of the given color 144 | for ii, i in enumerate('abcdefgh'): 145 | for j in range(1, 9): 146 | if (variables[str(i)+str(j)].isany(color(['q', 'r', 'b']))).ite(): 147 | variables[name+'x_lst'] += i2s(ii) + ";" 148 | variables[name+'y_lst'] += i2s(j-1) + ";" 149 | variables[name+'pos_lst'] += str(i)+str(j) + ";" 150 | variables[name+'piece_lst'] += variables[str(i)+str(j)] 151 | variables[name+'piece_lst'] += ';' 152 | variables.merge() 153 | 154 | MAX_BISHOPS = 6 # Maximum bishops to process 155 | for iteration in range(MAX_BISHOPS): 156 | if (variables[name+'x_lst'] != "").ite(): 157 | variables.settype(name+'x', 'int') 158 | variables.settype(name+'y', 'int') 159 | variables.list_pop(name+'x_lst', name+'x') 160 | variables.list_pop(name+'y_lst', name+'y') 161 | variables.list_pop(name+'pos_lst', name+'pos') 162 | variables.list_pop(name+'piece_lst', 'rook_piece') 163 | variables.fork_inactive("wait1") 164 | variables.merge() 165 | 166 | variables.pause("toplevel") 167 | variables.reactivate("wait1") 168 | 169 | variables['legal_'+name+'_moves'] = '' 170 | if True: 171 | if True: 172 | variables['next_lst'] = '' 173 | variables[name+'x_tmp'] = variables[name+'x'] 174 | variables[name+'y_tmp'] = variables[name+'y'] 175 | 176 | if (variables['rook_piece'] == color('B')).ite(): 177 | for dx, dy in [(7, 7), (7, 1), (1, 7), (1, 1)]: 178 | variables['dy'] = dy 179 | variables['dx'] = dx 180 | variables[name+'x'] = variables[name+'x_tmp'] 181 | variables[name+'y'] = variables[name+'y_tmp'] 182 | variables['ok'] = "True" 183 | variables.fork_inactive("waiting") 184 | variables.merge() 185 | if (variables['rook_piece'] == color('R')).ite(): 186 | for dx, dy in [(7, 0), (1, 0), (0, 7), (0, 1)]: 187 | variables['dy'] = dy 188 | variables['dx'] = dx 189 | variables[name+'x'] = variables[name+'x_tmp'] 190 | variables[name+'y'] = variables[name+'y_tmp'] 191 | variables['ok'] = "True" 192 | variables.fork_inactive("waiting") 193 | variables.merge() 194 | if (variables['rook_piece'] == color('Q')).ite(): 195 | for dx, dy in [(7, 0), (1, 0), (0, 7), (0, 1)] + [(7, 7), (7, 1), (1, 7), (1, 1)]: 196 | variables['dy'] = dy 197 | variables['dx'] = dx 198 | variables[name+'x'] = variables[name+'x_tmp'] 199 | variables[name+'y'] = variables[name+'y_tmp'] 200 | variables['ok'] = "True" 201 | variables.fork_inactive("waiting") 202 | variables.merge() 203 | 204 | if True: 205 | variables.pop() 206 | variables.pause("bishwait") 207 | variables.reactivate("waiting") 208 | for i in range(8): 209 | variables[name+'x'] += variables['dx'] 210 | if (variables[name+'x'] >= 8).ite(): 211 | variables[name+'x'] -= 8 212 | variables.merge() 213 | 214 | if (variables['dx'] == 1).ite(): 215 | variables['ok'] = variables['ok'] & (variables[name+'x'] != 0) 216 | variables.merge() 217 | if (variables['dx'] == 7).ite(): 218 | variables['ok'] = variables['ok'] & (variables[name+'x'] != 7) 219 | variables.merge() 220 | 221 | variables[name+'y'] += variables['dy'] 222 | if (variables[name+'y'] >= 8).ite(): 223 | variables[name+'y'] -= 8 224 | variables.merge() 225 | 226 | if (variables['dy'] == 1).ite(): 227 | variables['ok'] = variables['ok'] & (variables[name+'y'] != 0) 228 | variables.merge() 229 | if (variables['dy'] == 7).ite(): 230 | variables['ok'] = variables['ok'] & (variables[name+'y'] != 7) 231 | variables.merge() 232 | 233 | if variables['ok'].ite(): 234 | 235 | variables.intxy_to_location(name+'x', name+'y') 236 | variables.assign_pop("newbishop") 237 | 238 | if (variables[variables['newbishop']].isany(color([' ', 'K', 'Q', 'R', 'B', 'N', 'P'], inv=True))).ite(): 239 | variables['next_lst'] += variables['newbishop'] + ";" 240 | variables.merge() 241 | 242 | variables['ok'] &= variables[variables['newbishop']] == " " 243 | variables.merge() 244 | 245 | variables.lookup("next_lst") 246 | join_sub_to_main(variables, 32, main="bishwait", sub="3sub") 247 | variables.assign_stack_to('next_lst', 40) 248 | variables.fix_double_list() 249 | 250 | variables.variable_uniq("next_lst") 251 | for _ in range(32): 252 | if (variables['next_lst'] != "").ite(): 253 | variables.list_pop('next_lst', 'new_bishpos') 254 | variables.fork_inactive("inactive") 255 | variables.merge() 256 | 257 | variables.pause("main") 258 | variables.reactivate("inactive") 259 | 260 | variables.lookup("new_bishpos") 261 | variables.lookup('rook_piece') 262 | variables.indirect_assign() 263 | variables.lookup(name+"pos") 264 | variables.push(" ") 265 | variables.indirect_assign() 266 | variables["ep"] = '-' 267 | variables.contract_chess() 268 | join_sub_to_main(variables, 32, sub="2sub") 269 | variables.assign_stack_to('xtmp', 40) 270 | variables.lookup("xtmp") 271 | join_sub_to_main(variables, 32, main="toplevel", sub="4sub") 272 | variables.assign_stack_to('legal_'+name+'_moves', 64) 273 | 274 | variables.contract_chess() 275 | variables.pop() 276 | 277 | # Cleanup temporary variables 278 | variables.delete_var('new_bishpos') 279 | variables.delete_var('newbishop') 280 | variables.delete_var(name+'pos') 281 | variables.delete_var(name+'x') 282 | variables.delete_var(name+'y') 283 | variables.delete_var(name+'x_tmp') 284 | variables.delete_var(name+'y_tmp') 285 | variables.delete_var(name+'x_lst') 286 | variables.delete_var(name+'y_lst') 287 | variables.delete_var(name+'pos_lst') 288 | variables.delete_var('next_lst') 289 | variables.delete_var('xtmp') 290 | variables.delete_var('ok') 291 | 292 | return variables 293 | 294 | def queen_moves(variables, color): 295 | rook_moves(variables, color, 'q') 296 | variables['legal_queen_moves'] = variables['legal_rook_moves'] 297 | variables.variable_uniq("legal_queen_moves") 298 | 299 | def pawn_moves(variables, color): 300 | variables.lookup('initial_board') 301 | variables.expand_chess() 302 | 303 | if color == color_white: 304 | ep_rank = 4 # White pawns must be on rank 5 to capture EP 305 | else: 306 | ep_rank = 3 # Black pawns must be on rank 4 to capture EP 307 | 308 | find_pieces(variables, color, 'pawn', 'p') 309 | 310 | MAX_PAWNS = 8 311 | for iteration in range(MAX_PAWNS): 312 | if (variables['pawnx_lst'] != "").ite(): 313 | variables.settype('pawnx', 'int') 314 | variables.settype('pawny', 'int') 315 | variables.list_pop('pawnx_lst', 'pawnx') 316 | variables.list_pop('pawny_lst', 'pawny') 317 | variables.list_pop('pawnpos_lst', 'pawnpos') 318 | variables.fork_inactive("inactivep1") 319 | variables.merge() 320 | 321 | 322 | variables.pause("mainp1") 323 | variables.reactivate("inactivep1") 324 | variables.delete_var('pawnx_lst') 325 | variables.delete_var('pawny_lst') 326 | variables.delete_var('pawnpos_lst') 327 | 328 | if True: 329 | if True: 330 | variables['next_lst'] = '' 331 | 332 | if color == color_white: 333 | variables['forward'] = variables['pawny'] + 1 334 | else: 335 | variables['forward'] = variables['pawny'] - 1 336 | variables['left'] = variables['pawnx'] + 1 337 | variables['right'] = variables['pawnx'] - 1 338 | 339 | variables.intxy_to_location('pawnx', 'forward') 340 | variables.assign_pop("newpawn") 341 | 342 | if (variables[variables['newpawn']] == " ").ite(): 343 | variables['next_lst'] += variables['newpawn'] + ";" 344 | variables.merge() 345 | 346 | variables.intxy_to_location('left', 'forward') 347 | variables.assign_pop("newpawn") 348 | if (variables[variables['newpawn']].isany(color(['K', 'Q', 'R', 'B', 'N', 'P'], inv=True))).ite(): 349 | variables['next_lst'] += variables['newpawn'] + ";" 350 | variables.merge() 351 | 352 | if ((variables['pawny'] == ep_rank) & (variables['newpawn'] == variables['ep'])).ite(): 353 | variables['next_lst'] += variables['newpawn'] + ";" 354 | variables.merge() 355 | 356 | variables.intxy_to_location('right', 'forward') 357 | variables.assign_pop("newpawn") 358 | if ((variables['pawnx'] != 0) & variables[variables['newpawn']].isany(color(['K', 'Q', 'R', 'B', 'N', 'P'], inv=True))).ite(): 359 | variables['next_lst'] += variables['newpawn'] + ";" 360 | variables.merge() 361 | 362 | if ((variables['pawny'] == ep_rank) & (variables['newpawn'] == variables['ep'])).ite(): 363 | variables['next_lst'] += variables['newpawn'] + ";" 364 | variables.merge() 365 | 366 | # first rank 367 | if (variables['pawny'] == (1 if color == color_white else 6)).ite(): 368 | if color == color_white: 369 | variables['forward2'] = variables['pawny'] + 2 370 | else: 371 | variables['forward2'] = variables['pawny'] - 2 372 | pass 373 | else: 374 | variables['forward2'] = variables['pawny'] 375 | variables.merge() 376 | 377 | # En passant captures 378 | if (variables['pawny'] == ep_rank).ite(): 379 | # Check if pawn is in correct position for EP capture 380 | variables.intxy_to_location('pawnx', 'forward') 381 | variables.assign_pop("ep_dest") 382 | if (variables['ep_dest'] == variables['ep']).ite(): 383 | # EP capture is available 384 | variables['next_lst'] += variables['ep_dest'] + ";" 385 | variables.merge() 386 | variables.merge() 387 | 388 | variables.intxy_to_location('pawnx', 'forward') 389 | variables.assign_pop("newpawn1") 390 | variables.intxy_to_location('pawnx', 'forward2') 391 | variables.assign_pop("newpawn2") 392 | if ((variables[variables['newpawn1']] == " ") & (variables[variables['newpawn2']] == " ")).ite(): 393 | variables['next_lst'] += variables['newpawn2'] + ";" 394 | 395 | variables.merge() 396 | 397 | variables.variable_uniq("next_lst") 398 | for _ in range(4): 399 | if (variables['next_lst'] != "").ite(): 400 | variables.list_pop('next_lst', 'new_pawnpos') 401 | variables.fork_inactive("inactive") 402 | variables.merge() 403 | 404 | variables.contract_chess() 405 | variables.pause("main") 406 | variables.reactivate("inactive") 407 | 408 | variables.lookup("new_pawnpos") 409 | variables.push(color("p")) 410 | variables.indirect_assign() 411 | variables.lookup("pawnpos") 412 | variables.push(" ") 413 | variables.indirect_assign() 414 | 415 | if (variables['new_pawnpos'] == variables['ep']).ite(): 416 | variables.lookup('ep') 417 | variables.square_to_xy() 418 | variables.settype('ep_x', 'int') 419 | variables.settype('ep_y', 'int') 420 | variables.assign_pop('ep_x') 421 | variables.assign_pop('ep_y') 422 | variables['ep_y'] += -1 if color == color_white else 1 423 | variables.intxy_to_location('ep_x', 'ep_y') 424 | variables.push(" ") 425 | variables.indirect_assign() 426 | variables.merge() 427 | 428 | variables.settype('np_x', 'int') 429 | variables.settype('np_y', 'int') 430 | variables.settype('op_y', 'int') 431 | 432 | variables.lookup('new_pawnpos') 433 | variables.square_to_xy() 434 | variables.assign_pop("np_x") 435 | variables.assign_pop("np_y") 436 | 437 | variables.lookup('pawnpos') 438 | variables.square_to_xy() 439 | variables.pop() 440 | variables.assign_pop("op_y") 441 | 442 | if color == color_white: 443 | variables["top"] = variables["np_y"] - variables["op_y"] 444 | else: 445 | variables["top"] = variables["op_y"] - variables["np_y"] 446 | 447 | if (variables['top'] == 2).ite(): 448 | if color == color_white: 449 | variables["ep_y"] = 2 450 | else: 451 | variables["ep_y"] = 5 452 | variables.intxy_to_location('np_x', 'ep_y') 453 | variables.assign_pop("ep") 454 | else: 455 | variables["ep"] = '-' 456 | variables.merge() 457 | 458 | variables.promote_to_queen() 459 | 460 | variables.contract_chess() 461 | join_sub_to_main(variables, 4) 462 | 463 | variables.assign_stack_to('legal_pawn_moves', 4) 464 | variables.variable_uniq("legal_pawn_moves") 465 | variables.lookup("legal_pawn_moves") 466 | join_sub_to_main(variables, 8, main="mainp1") 467 | variables.assign_stack_to('legal_pawn_moves', 32) 468 | 469 | 470 | variables.delete_var('pawnx_lst') 471 | variables.delete_var('pawny_lst') 472 | variables.delete_var('pawnpos_lst') 473 | variables.delete_var('pawnx') 474 | variables.delete_var('pawny') 475 | variables.delete_var('pawnpos') 476 | variables.delete_var('forward') 477 | variables.delete_var('forward2') 478 | variables.delete_var('left') 479 | variables.delete_var('right') 480 | variables.delete_var('newpawn') 481 | variables.delete_var('newpawn1') 482 | variables.delete_var('newpawn2') 483 | variables.delete_var('next_lst') 484 | variables.delete_var('new_pawnpos') 485 | variables.delete_var('ep_dest') 486 | variables.delete_var('ep_x') 487 | variables.delete_var('ep_y') 488 | variables.delete_var('op_x') 489 | variables.delete_var('op_y') 490 | variables.delete_var('np_x') 491 | variables.delete_var('np_y') 492 | variables.contract_chess() 493 | variables.pop() 494 | 495 | 496 | def knight_moves(variables, color, knight_piece='n'): 497 | variables.lookup('initial_board') 498 | variables.expand_chess() 499 | 500 | variables['knightx_lst'] = '' 501 | variables['knighty_lst'] = '' 502 | variables['knightpos_lst'] = '' 503 | 504 | for ii, i in enumerate('abcdefgh'): 505 | for j in range(1, 9): 506 | if (variables[str(i)+str(j)] == color(knight_piece)).ite(): 507 | variables['knightx_lst'] += i2s(ii) + ";" 508 | variables['knighty_lst'] += i2s(j - 1) + ";" 509 | variables['knightpos_lst'] += str(i) + str(j) + ";" 510 | variables.merge() 511 | 512 | MAX_KNIGHTS = 2 513 | for iteration in range(MAX_KNIGHTS): 514 | if (variables['knightx_lst'] != "").ite(): 515 | variables.settype('knightx', 'int') 516 | variables.settype('knighty', 'int') 517 | variables.list_pop('knightx_lst', 'knightx') 518 | variables.list_pop('knighty_lst', 'knighty') 519 | variables.list_pop('knightpos_lst', 'knightpos') 520 | 521 | variables['next_lst'] = '' 522 | 523 | # Handle +2 moves first (no safety check needed for addition) 524 | if (variables['knightx'] < 6).ite(): # Has room to move +2 in x 525 | if (variables['knighty'] < 7).ite(): # Has room for +1 in y 526 | variables['tmpx'] = variables['knightx'] + 2 527 | variables['tmpy'] = variables['knighty'] + 1 528 | add_knight_move(variables, color) 529 | variables.merge() 530 | if (variables['knighty'] >= 1).ite(): # Has room for -1 in y 531 | variables['tmpx'] = variables['knightx'] + 2 532 | variables['tmpy'] = variables['knighty'] - 1 533 | add_knight_move(variables, color) 534 | variables.merge() 535 | variables.merge() 536 | 537 | # Handle -2 moves (need check for x >= 2) 538 | if (variables['knightx'] >= 2).ite(): 539 | if (variables['knighty'] < 7).ite(): # Has room for +1 in y 540 | variables['tmpx'] = variables['knightx'] - 2 541 | variables['tmpy'] = variables['knighty'] + 1 542 | add_knight_move(variables, color) 543 | variables.merge() 544 | if (variables['knighty'] >= 1).ite(): # Has room for -1 in y 545 | variables['tmpx'] = variables['knightx'] - 2 546 | variables['tmpy'] = variables['knighty'] - 1 547 | add_knight_move(variables, color) 548 | variables.merge() 549 | variables.merge() 550 | 551 | # Handle +1 moves in x 552 | if (variables['knightx'] < 7).ite(): # Has room for +1 in x 553 | if (variables['knighty'] < 6).ite(): # Has room for +2 in y 554 | variables['tmpx'] = variables['knightx'] + 1 555 | variables['tmpy'] = variables['knighty'] + 2 556 | add_knight_move(variables, color) 557 | variables.merge() 558 | if (variables['knighty'] >= 2).ite(): # Has room for -2 in y 559 | variables['tmpx'] = variables['knightx'] + 1 560 | variables['tmpy'] = variables['knighty'] - 2 561 | add_knight_move(variables, color) 562 | variables.merge() 563 | variables.merge() 564 | 565 | # Handle -1 moves in x 566 | if (variables['knightx'] >= 1).ite(): # Has room for -1 in x 567 | if (variables['knighty'] < 6).ite(): # Has room for +2 in y 568 | variables['tmpx'] = variables['knightx'] - 1 569 | variables['tmpy'] = variables['knighty'] + 2 570 | add_knight_move(variables, color) 571 | variables.merge() 572 | if (variables['knighty'] >= 2).ite(): # Has room for -2 in y 573 | variables['tmpx'] = variables['knightx'] - 1 574 | variables['tmpy'] = variables['knighty'] - 2 575 | add_knight_move(variables, color) 576 | variables.merge() 577 | variables.merge() 578 | 579 | variables.variable_uniq("next_lst") 580 | for _ in range(8): 581 | if (variables['next_lst'] != "").ite(): 582 | variables.list_pop('next_lst', 'new_knightpos') 583 | variables.fork_inactive("inactive") 584 | variables.merge() 585 | 586 | variables.contract_chess() 587 | variables.pause("main") 588 | variables.reactivate("inactive") 589 | 590 | variables.lookup("new_knightpos") 591 | variables.push(color(knight_piece)) 592 | variables.indirect_assign() 593 | 594 | variables.lookup("knightpos") 595 | variables.push(" ") 596 | variables.indirect_assign() 597 | 598 | variables["ep"] = '-' 599 | 600 | variables.contract_chess() 601 | join_sub_to_main(variables, 16) 602 | else: 603 | variables.contract_chess() 604 | 605 | variables.merge() 606 | if iteration != MAX_KNIGHTS - 1: 607 | variables.lookup('initial_board') 608 | variables.expand_chess() 609 | 610 | variables.assign_stack_to('legal_knight_moves', 40) 611 | 612 | # Cleanup 613 | variables.delete_var('knightx') 614 | variables.delete_var('knighty') 615 | variables.delete_var('knightpos') 616 | variables.delete_var('knightx_lst') 617 | variables.delete_var('knighty_lst') 618 | variables.delete_var('knightpos_lst') 619 | variables.delete_var('new_knightpos') 620 | variables.delete_var('newknight') 621 | variables.delete_var('next_lst') 622 | variables.delete_var('tmpx') 623 | variables.delete_var('tmpy') 624 | 625 | return variables 626 | 627 | # Helper method to add a valid 628 | def add_knight_move(variables, color): 629 | variables.intxy_to_location('tmpx', 'tmpy') 630 | variables.assign_pop("newknight") 631 | if (variables[variables['newknight']] 632 | .isany(color([' ', 'K', 'Q', 'R', 'B', 'N', 'P'], inv=True)) 633 | ).ite(): 634 | variables['next_lst'] += variables['newknight'] + ";" 635 | variables.merge() 636 | 637 | def is_square_under_attack_by_rook(variables, sq, color): 638 | variables.lookup('initial_board') 639 | variables.expand_chess() 640 | def square(x, y): 641 | return variables[chr(0x61+x)+str(y)] 642 | start_x = ord(sq[0])-0x61 643 | start_y = int(sq[1]) 644 | 645 | for dy, dx in [(0, 1), (0, -1), (1, 0), (-1, 0)]: 646 | for i in range(1, 8): 647 | end_x = start_x + i * dx 648 | end_y = start_y + i * dy 649 | if 0 <= end_x < 8 and 1 <= end_y < 9: 650 | if i > 1: 651 | empty = (square(start_x + dx, start_y + dy) == ' ') 652 | for j in range(2, i): 653 | empty = empty & (square(start_x + j * dx, start_y + j * dy) == ' ') 654 | variables['attacked'] |= empty & (square(end_x, end_y).isany(color(['R', 'Q']))) 655 | else: 656 | variables['attacked'] |= (square(end_x, end_y).isany(color(['R', 'Q']))) 657 | variables.contract_chess() 658 | return variables 659 | 660 | def is_square_under_attack_by_bishop(variables, sq, color): 661 | variables.lookup('initial_board') 662 | variables.expand_chess() 663 | def square(x, y): 664 | return variables[chr(0x61+x)+str(y)] 665 | start_x = ord(sq[0])-0x61 666 | start_y = int(sq[1]) 667 | 668 | # Change to diagonal directions 669 | for dy, dx in [(1, 1), (1, -1), (-1, 1), (-1, -1)]: 670 | for i in range(1, 8): 671 | end_x = start_x + i * dx 672 | end_y = start_y + i * dy 673 | if 0 <= end_x < 8 and 1 <= end_y < 9: 674 | if i > 1: 675 | empty = (square(start_x + dx, start_y + dy) == ' ') 676 | for j in range(2, i): 677 | empty = empty & (square(start_x + j * dx, start_y + j * dy) == ' ') 678 | variables['attacked'] |= empty & (square(end_x, end_y).isany([color('B'), color('Q')])) 679 | else: 680 | variables['attacked'] |= square(end_x, end_y).isany([color('B'), color('Q')]) 681 | variables.contract_chess() 682 | return variables 683 | 684 | def is_square_under_attack_by_knight(variables, sq, color): 685 | variables.lookup('initial_board') 686 | variables.expand_chess() 687 | def square(x, y): 688 | return variables[chr(0x61+x)+str(y)] 689 | start_x = ord(sq[0])-0x61 690 | start_y = int(sq[1]) 691 | 692 | # Check all 8 knight move squares: 693 | for dx, dy in [ 694 | (-2,-1), (-2,1), # Left 2, up/down 1 695 | (2,-1), (2,1), # Right 2, up/down 1 696 | (-1,-2), (-1,2), # Left 1, up/down 2 697 | (1,-2), (1,2) # Right 1, up/down 2 698 | ]: 699 | end_x = start_x + dx 700 | end_y = start_y + dy 701 | if 0 <= end_x < 8 and 1 <= end_y <= 8: 702 | variables['attacked'] |= (square(end_x, end_y) == color('N')) 703 | 704 | variables.contract_chess() 705 | return variables 706 | 707 | def is_square_under_attack_by_pawn(variables, sq, color): 708 | variables.lookup('initial_board') 709 | variables.expand_chess() 710 | def square(x, y): 711 | return variables[chr(0x61+x)+str(y)] 712 | start_x = ord(sq[0])-0x61 713 | start_y = int(sq[1]) 714 | 715 | # Direction depends on attacking color 716 | # White pawns attack upward (target must be above them) 717 | # Black pawns attack downward (target must be below them) 718 | dy = -1 if color == color_white else 1 719 | 720 | # Check the two possible attacking pawn positions 721 | for dx in [-1, 1]: 722 | end_x = start_x + dx 723 | end_y = start_y + dy 724 | if 0 <= end_x < 8 and 1 <= end_y <= 8: 725 | variables['attacked'] |= (square(end_x, end_y) == color('P')) 726 | 727 | variables.contract_chess() 728 | return variables 729 | 730 | def is_square_under_attack_by_king(variables, sq, color): 731 | variables.lookup('initial_board') 732 | variables.expand_chess() 733 | def square(x, y): 734 | return variables[chr(0x61+x)+str(y)] 735 | start_x = ord(sq[0])-0x61 736 | start_y = int(sq[1]) 737 | 738 | # Check all 8 adjacent squares: 739 | for dx, dy in [ 740 | (-1,-1), (0,-1), (1,-1), # Below 741 | (-1,0), (1,0), # Same rank 742 | (-1,1), (0,1), (1,1) # Above 743 | ]: 744 | end_x = start_x + dx 745 | end_y = start_y + dy 746 | if 0 <= end_x < 8 and 1 <= end_y <= 8: 747 | variables['attacked'] |= (square(end_x, end_y) == color('K')) 748 | 749 | variables.contract_chess() 750 | return variables 751 | 752 | def is_square_under_attack(variables, sq, color, push=True): 753 | if push: 754 | variables['attacked'] = 'False' 755 | is_square_under_attack_by_rook(variables, sq, color) 756 | variables.pop() 757 | is_square_under_attack_by_bishop(variables, sq, color) 758 | variables.pop() 759 | is_square_under_attack_by_knight(variables, sq, color) 760 | variables.pop() 761 | is_square_under_attack_by_pawn(variables, sq, color) 762 | variables.pop() 763 | is_square_under_attack_by_king(variables, sq, color) 764 | variables.pop() 765 | 766 | def prepare_human_move(variables, has_move): 767 | variables.expand_chess() 768 | variables.make_pretty(has_move) 769 | 770 | def make_human_move(variables, has_move): 771 | variables.unpretty(has_move) 772 | if not has_move: 773 | variables.contract_chess() 774 | variables.assign_pop('initial_board') 775 | return 776 | 777 | variables.contract_chess() 778 | variables.assign_pop('before_move_board') 779 | variables.lookup('before_move_board') 780 | variables.expand_chess() 781 | 782 | #variables.doprint() 783 | 784 | if ((variables['src'] == 'e1') & (variables['dst'] == 'g1') & (variables[variables['src']] == 'K')).ite(): 785 | variables['f1'] = 'R' 786 | variables['h1'] = ' ' 787 | variables.merge() 788 | if ((variables['src'] == 'e1') & (variables['dst'] == 'c1') & (variables[variables['src']] == 'K')).ite(): 789 | variables['d1'] = 'R' 790 | variables['a1'] = ' ' 791 | variables.merge() 792 | if ((variables['dst'] == variables['ep']) & (variables[variables['src']] == 'P')).ite(): 793 | for s in 'abcdefgh': 794 | if (variables['ep'] == s+'6').ite(): 795 | variables[s+'5'] = ' ' 796 | variables.merge() 797 | variables.merge() 798 | 799 | variables['tmp3'] = variables[variables['src']] 800 | variables['tmp2'] = variables[variables['dst']] 801 | 802 | if ((variables[variables['src']] == 'P') & ((variables['dst'] == 'a1') | (variables['dst'] == 'b1') | (variables['dst'] == 'c2') | (variables['dst'] == 'd1') | (variables['dst'] == 'e1') | (variables['dst'] == 'f1') | (variables['dst'] == 'g1') | (variables['dst'] == 'h1'))).ite(): 803 | variables['tmp3'] = 'Q' 804 | variables.merge() 805 | 806 | 807 | variables.lookup("dst") 808 | variables.lookup("tmp3") 809 | variables.indirect_assign() 810 | 811 | variables.lookup("src") 812 | variables.push(" ") 813 | variables.indirect_assign() 814 | 815 | 816 | 817 | variables.contract_chess() 818 | variables.assign_pop("after_move") 819 | 820 | variables.delete_var('tmp3') 821 | variables.delete_var('tmp2') 822 | variables.delete_var('dst') 823 | variables.delete_var('src') 824 | variables.delete_var('move') 825 | 826 | 827 | 828 | variables['initial_board'] = variables['before_move_board'] 829 | variables.delete_var('before_move_board') 830 | 831 | compute_legal_boards(variables, color_white, color_black) 832 | 833 | if (variables['initial_board'].fen() != variables['after_move'].fen()).ite(): 834 | variables.destroy_active_threads() 835 | variables.merge() 836 | variables.illegal_move() 837 | 838 | variables.delete_var('after_move') 839 | 840 | def castle_moves(variables, color): 841 | variables.lookup('initial_board') 842 | variables.expand_chess() 843 | 844 | variables['legal_castle_moves'] = '' 845 | 846 | # Check kingside castle 847 | if (variables['castle_' + ('white' if color == color_white else 'black') + '_king']).ite(): 848 | # Check if squares between king and rook are empty 849 | if color == color_white: 850 | king_empty = (variables['f1'] == ' ') & (variables['g1'] == ' ') 851 | path_squares = ['e1', 'f1'] 852 | else: 853 | king_empty = (variables['f8'] == ' ') & (variables['g8'] == ' ') 854 | path_squares = ['e8', 'f8'] 855 | 856 | variables.contract_chess() 857 | 858 | # Check if path is under attack 859 | variables['attacked'] = 'False' 860 | for sq in path_squares: 861 | is_square_under_attack(variables, sq, color_white if color == color_black else color_black, push=False) 862 | 863 | variables.expand_chess() 864 | 865 | if (king_empty & ~variables['attacked']).ite(): 866 | if color == color_white: 867 | # Move white king 868 | variables['e1'] = ' ' 869 | variables['g1'] = color('K') 870 | # Move white rook 871 | variables['h1'] = ' ' 872 | variables['f1'] = color('R') 873 | else: 874 | # Move black king 875 | variables['e8'] = ' ' 876 | variables['g8'] = color('K') 877 | # Move black rook 878 | variables['h8'] = ' ' 879 | variables['f8'] = color('R') 880 | variables.contract_chess() 881 | variables['legal_castle_moves'] += variables.peek() 882 | variables['legal_castle_moves'] += ";" 883 | variables.lookup('initial_board') 884 | variables.expand_chess() 885 | variables.merge() 886 | variables.merge() 887 | 888 | # Check queenside castle 889 | if (variables['castle_' + ('white' if color == color_white else 'black') + '_queen']).ite(): 890 | # Check if squares between king and rook are empty 891 | if color == color_white: 892 | queen_empty = (variables['b1'] == ' ') & (variables['c1'] == ' ') & (variables['d1'] == ' ') 893 | path_squares = ['d1', 'e1'] 894 | else: 895 | queen_empty = (variables['b8'] == ' ') & (variables['c8'] == ' ') & (variables['d8'] == ' ') 896 | path_squares = ['d8', 'e8'] 897 | 898 | variables.contract_chess() 899 | 900 | # Check if path is under attack 901 | variables['attacked'] = 'False' 902 | for sq in path_squares: 903 | is_square_under_attack(variables, sq, color_white if color == color_black else color_black, push=False) 904 | 905 | variables.expand_chess() 906 | 907 | if (queen_empty & ~variables['attacked']).ite(): 908 | if color == color_white: 909 | # Move white king 910 | variables['e1'] = ' ' 911 | variables['c1'] = color('K') 912 | # Move white rook 913 | variables['a1'] = ' ' 914 | variables['d1'] = color('R') 915 | else: 916 | # Move black king 917 | variables['e8'] = ' ' 918 | variables['c8'] = color('K') 919 | # Move black rook 920 | variables['a8'] = ' ' 921 | variables['d8'] = color('R') 922 | variables.contract_chess() 923 | variables['legal_castle_moves'] += variables.peek() 924 | variables['legal_castle_moves'] += ";" 925 | variables.lookup('initial_board') 926 | variables.expand_chess() 927 | variables.merge() 928 | variables.merge() 929 | 930 | variables.contract_chess() 931 | return variables 932 | 933 | def compute_next_boards(variables, color, castle=True): 934 | """ 935 | Returns the output boards in the next_boards variable 936 | """ 937 | 938 | variables.fork_inactive("tmp") 939 | variables.pause("cur_state") 940 | variables.reactivate("tmp") 941 | #variables.delete_var("before_move_board") 942 | #variables.delete_var("after_move") 943 | #variables.delete_var("saved_board") 944 | 945 | if castle: 946 | castle_moves(variables, color) 947 | variables['legal_moves'] = variables['legal_castle_moves'] 948 | variables.delete_var('legal_castle_moves') 949 | variables.fork_inactive("save") 950 | variables.delete_var('legal_moves') 951 | 952 | king_moves(variables, color) 953 | variables['legal_moves'] = variables['legal_king_moves'] 954 | variables.delete_var('legal_king_moves') 955 | variables.fork_inactive("save") 956 | variables.delete_var('legal_moves') 957 | 958 | bishop_moves(variables, color) 959 | variables['legal_moves'] = variables['legal_bishop_moves'] 960 | variables.delete_var('legal_bishop_moves') 961 | variables.fork_inactive("save") 962 | variables.delete_var('legal_moves') 963 | 964 | knight_moves(variables, color) 965 | variables['legal_moves'] = variables['legal_knight_moves'] 966 | variables.delete_var('legal_knight_moves') 967 | variables.fork_inactive("save") 968 | variables.delete_var('legal_moves') 969 | 970 | pawn_moves(variables, color) 971 | variables['legal_moves'] = variables['legal_pawn_moves'] 972 | variables.delete_var('legal_pawn_moves') 973 | variables.fork_inactive("save") 974 | variables.delete_var('legal_moves') 975 | 976 | variables.pause("main") 977 | variables.reactivate("save") 978 | variables.lookup("legal_moves") 979 | join_sub_to_main(variables, 8) 980 | variables.assign_stack_to('next_boards', 8) 981 | variables.lookup("next_boards") 982 | variables.pause("sub") 983 | variables.fix_double_list() 984 | 985 | variables.reactivate("cur_state") 986 | variables.join_pop("sub") 987 | variables.assign_pop("next_boards") 988 | 989 | return variables 990 | 991 | def compute_legal_boards(variables, color_one, color_two, do_score=False): 992 | variables['saved_board'] = variables['initial_board'] 993 | compute_next_boards(variables, color_one) 994 | 995 | variables.fix_double_list() 996 | for _ in range(100): 997 | if (variables['next_boards'] != '').ite(): 998 | variables.fork_list_pop('next_boards', 'initial_board', 'maybe') 999 | variables.merge() 1000 | variables.destroy_active_threads() 1001 | variables.reactivate("maybe") 1002 | 1003 | if (variables['initial_board'] == variables['saved_board']).ite(): 1004 | variables.destroy_active_threads() 1005 | variables.merge() 1006 | 1007 | # Castling won't invalidate any more boards than just rook moves 1008 | compute_next_boards(variables, color_two, castle=False) 1009 | variables.fix_double_list() 1010 | 1011 | variables.check_king_alive() 1012 | 1013 | if (variables['alive']).ite(): 1014 | variables.pause('legal') 1015 | variables.merge() 1016 | variables.destroy_active_threads() 1017 | variables.reactivate('legal') 1018 | 1019 | if (variables['saved_board'] == variables['initial_board']).ite(): 1020 | variables.destroy_active_threads() 1021 | variables.merge() 1022 | 1023 | if do_score: 1024 | for _ in range(100): 1025 | if (variables['next_boards'] != '').ite(): 1026 | variables.fork_list_pop('next_boards', 'toscore', 'nmaybe') 1027 | variables.merge() 1028 | variables.pause("waiting") 1029 | variables.reactivate("nmaybe") 1030 | variables.lookup("toscore") 1031 | 1032 | variables.piece_value() 1033 | variables.keep_only_max_thread() 1034 | variables.pause("scored") 1035 | variables.reactivate("waiting") 1036 | variables.destroy_active_threads() 1037 | variables.reactivate("scored") 1038 | variables.delete_var("next_boards") 1039 | variables.delete_var('saved_board') 1040 | variables.delete_var('alive') 1041 | return 1042 | 1043 | variables.delete_var("next_boards") 1044 | variables.delete_var('saved_board') 1045 | variables.delete_var('alive') 1046 | variables.lookup('initial_board') 1047 | 1048 | def flip_square(square): 1049 | """ 1050 | Convert a chess square to its mirror position. 1051 | e.g., 'a1' -> 'h8', 'b2' -> 'g7', etc. 1052 | 1053 | Args: 1054 | square (str): Chess square in format 'a1' through 'h8' 1055 | 1056 | Returns: 1057 | str: Mirrored square position 1058 | """ 1059 | file, rank = square[0], int(square[1]) 1060 | 1061 | # Flip rank (1->8, 2->7, etc.) 1062 | new_rank = 9 - rank 1063 | 1064 | return file + str(new_rank) 1065 | 1066 | 1067 | def is_flipped_board(variables): 1068 | variables.expand_chess() 1069 | variables.push("True") 1070 | for row in "1234": 1071 | for col in "abcdefgh": 1072 | square = col+row 1073 | variables.lookup(square) 1074 | variables.lookup(flip_square(square)) 1075 | variables.is_same_kind() 1076 | variables.boolean_and() 1077 | variables.assign_pop("foo") 1078 | if (variables['foo'] == 'True').ite(): 1079 | variables.push("AA") 1080 | else: 1081 | variables.push("A") 1082 | variables.merge() 1083 | 1084 | variables.contract_chess() 1085 | 1086 | def make_reply_move(variables, has_move=True): 1087 | make_human_move(variables, has_move) 1088 | variables.pop() 1089 | 1090 | compute_legal_boards(variables, color_black, color_white, do_score=True) 1091 | 1092 | variables.lookup("initial_board") 1093 | is_flipped_board(variables) 1094 | variables.pop() 1095 | variables.sub_unary() 1096 | variables.keep_only_min_thread() 1097 | variables.keep_only_last_thread() 1098 | variables.pop() 1099 | 1100 | variables.lookup('initial_board') 1101 | variables.test_checkmate() 1102 | 1103 | prepare_human_move(variables, has_move) 1104 | -------------------------------------------------------------------------------- /instruction_set.py: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2025, Nicholas Carlini . 2 | ## 3 | ## This program is free software: you can redistribute it and/or modify 4 | ## it under the terms of the GNU General Public License as published by 5 | ## the Free Software Foundation, either version 3 of the License, or 6 | ## (at your option) any later version. 7 | ## 8 | ## This program is distributed in the hope that it will be useful, 9 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | ## GNU General Public License for more details. 12 | ## 13 | ## You should have received a copy of the GNU General Public License 14 | ## along with this program. If not, see . 15 | 16 | import re 17 | import sys 18 | import json 19 | import time 20 | 21 | INSTRUCTIONS = {} 22 | 23 | def instruction(func): 24 | INSTRUCTIONS[func.__name__] = func 25 | return func 26 | 27 | 28 | @instruction 29 | def lookup(variable): 30 | # Find the variable's value and push it onto the stack 31 | return [(r"(%%\n#stack:)([^%]*\n#"+variable+": )([^#%]*)\n", 32 | r"\1\n\3\2\3\n")] 33 | # Groups: 34 | # 1: (%%\n#stack:) - Matches from %% to #stack: 35 | # 2: ([^%]*\n#"+variable+": ) - Matches from stack to the variable definition 36 | # 3: ([^#]*) - Captures the variable's value 37 | # Result: Keeps original variable and also pushes its value onto stack 38 | 39 | @instruction 40 | def indirect_lookup(): 41 | return [ 42 | # Look up the value of the variable whose name is on top of stack 43 | # and replace the name with the value 44 | (r"(%%\n#stack:\n)([^\n]+)\n([^%]*#\2: )([^#%\n]*)", 45 | r"\1\4\n\3\4") 46 | ] 47 | 48 | @instruction 49 | def indirect_assign(): 50 | # Pop top two stack values and use them for variable assignment 51 | # First pop is the value to assign 52 | # Second pop is the variable name to assign to 53 | return [ 54 | # Try to update existing variable and mark thread if successful 55 | (r"(%%)[^%]*#stack:\n([^\n]*)\n([^\n]*)\n([^%]*#\3: )[^\n]*", 56 | r"\1`\n#stack:\n\4\2"), 57 | 58 | # If no backtick (no existing variable), create new one at end of thread 59 | (r"(%%)([^`][^%]*#stack:\n)([^\n]*)\n([^\n]*)\n([^%]*$)", 60 | r"\1`\2\5#\4: \3\n"), 61 | 62 | # Clean up backtick 63 | (r"%%`", 64 | r"%%") 65 | ] 66 | # Groups for update case: 67 | # 1: %% - Thread start 68 | # 2: Value to assign 69 | # 3: Variable name 70 | # 4: Existing variable declaration 71 | 72 | # Groups for create case: 73 | # 1: %% - Thread start 74 | # 2: Everything through stack: 75 | # 3: Value to assign 76 | # 4: Variable name 77 | # 5: Rest of thread content 78 | 79 | @instruction 80 | def assign_pop(varname): 81 | return [ 82 | # Try to update existing variable and mark thread if successful 83 | (r"(%%)\n#stack:\n([^\n]*)\n([^%]*#" + varname + r": )[^\n]*", 84 | r"\1`\n#stack:\n\3\2"), 85 | 86 | # If no backtick (no existing variable), create new one at end of thread 87 | (r"(%%)([^`]\n?#stack:\n)([^\n%]*)\n([^%]*)", 88 | r"\1`\2\4#" + varname + r": \3\n"), 89 | 90 | # Clean up backtick 91 | (r"%%`", 92 | r"%%") 93 | ] 94 | 95 | @instruction 96 | def is_stack_empty(): 97 | """Check if the stack is empty, pushing True/False as result""" 98 | return [ 99 | # If not empty, mark with backtick and False 100 | (r"(%%\n#stack:\n)([^#%])", r"\1`False\n\2"), 101 | 102 | # If no backtick (so must be empty), push True 103 | (r"(%%\n#stack:\n)([^`])", r"\1True\n\2"), 104 | 105 | # If no backtick (so must be empty), push True 106 | (r"(%%\n#stack:\n)$", r"\1True\n"), 107 | 108 | # Clean up backtick 109 | (r"`", r"") 110 | ] 111 | @instruction 112 | def push(const): 113 | if type(const) == int: 114 | const = f"int{const:010b}" 115 | # Push a constant value onto the stack 116 | return [(r"(%%\n#stack:\n)", # Find the stack position 117 | r"\g<1>"+const+r"\n")] # Add the constant after #stack: 118 | # Groups: 119 | # 1: Everything from %% through #stack:\n 120 | # Result: Appends the constant value to the stack 121 | 122 | @instruction 123 | def pop(): 124 | # Remove top value from stack 125 | return [(r"(%%\n#stack:\n)([^\n]*)\n", # Match stack header and top value 126 | r"\1")] # Keep only the header 127 | # Groups: 128 | # 1: (%%\n#stack:\n) - Matches from %% through #stack:\n 129 | # 2: ([^\n]*)\n - Captures the top stack value 130 | # Result: Removes the top stack value 131 | 132 | @instruction 133 | def peek(): 134 | return [] 135 | 136 | @instruction 137 | def dup(): 138 | # Duplicate top value on stack 139 | return [(r"(%%\n#stack:\n)([^\n]*)\n", # Match stack header and top value 140 | r"\1\2\n\2\n")] # Duplicate the top value 141 | # Groups: 142 | # 1: (%%\n#stack:\n) - Matches from %% through #stack:\n 143 | # 2: ([^\n]*) - Captures the top stack value 144 | # Result: Duplicates the top stack value 145 | 146 | @instruction 147 | def swap(): 148 | # Swap top two values on stack 149 | return [(r"(%%\n#stack:\n)([^\n]*)\n([^\n]*)\n", # Match top two values 150 | r"\1\3\n\2\n")] # Reverse their order 151 | # Groups: 152 | # 1: (%%\n#stack:\n) - Matches from %% through #stack:\n 153 | # 2: ([^\n]*) - Captures the top stack value 154 | # 3: ([^\n]*) - Captures the second stack value 155 | # Result: Swaps the order of the top two values 156 | 157 | @instruction 158 | def eq(): 159 | return [ 160 | # Compare top two stack values for equality 161 | # If equal: Replace with True (marked with backtick) 162 | (r"(%%\n#stack:\n)([^\n]*)\n\2\n", # Match two identical values 163 | r"\1`True\n"), # Replace with marked True 164 | 165 | # If not equal (and not already marked): Replace with False 166 | (r"(%%\n#stack:\n)([^`][^\n]*)\n([^\n]*)\n", # Match any two values 167 | r"\1False\n"), # Replace with False 168 | 169 | # Remove the backtick marker from True results 170 | (r"`", 171 | r"") 172 | ] 173 | # Uses backtick to prevent False case from overwriting True case 174 | 175 | @instruction 176 | def isany(options): 177 | """ 178 | Check if the top value on the stack matches any of the given options. 179 | Returns True if there's a match, False otherwise. 180 | Uses backtick (`) to mark successful matches so they're not overwritten. 181 | 182 | Args: 183 | options: List of strings to check against the top of stack 184 | """ 185 | # Create a single pattern with all options joined by | 186 | options_pattern = "|".join(re.escape(opt) for opt in options) 187 | 188 | return [ 189 | # If top of stack matches any option, mark as True with ` 190 | (fr"(%%\n#stack:\n)({options_pattern})\n", 191 | r"\1`True\n"), 192 | 193 | # If no match (no backtick), replace with False 194 | (fr"(%%\n#stack:\n)([^`\n]*)\n", 195 | r"\1False\n"), 196 | 197 | # Clean up the backtick marker 198 | (r"`", 199 | r"") 200 | ] 201 | 202 | @instruction 203 | def neq(): 204 | return [*eq(), 205 | *boolean_not()] 206 | 207 | @instruction 208 | def lit_assign(varname, value): 209 | # Assign a literal value to a variable 210 | return [(r"(%%[^%]*)(#" + varname + r": )[^\n]*", # Find the variable 211 | r"\1\2" + value)] # Replace its value 212 | # Groups: 213 | # 1: Everything from %% to the variable name 214 | # 2: The variable declaration (#varname: ) 215 | # Result: Updates the variable's value 216 | 217 | @instruction 218 | def assign(src_var, dst_var): 219 | # Copy value from source variable to destination variable 220 | return [(r"(%%[^%]*#" + src_var + r": )([^\n]*)(.*#" + dst_var + r": )[^\n]*", 221 | r"\1\2\3\2")] 222 | # Groups: 223 | # 1: Everything from %% through source variable declaration 224 | # 2: Source variable's value 225 | # 3: Everything between source and destination, including destination declaration 226 | # Result: Copies source value to destination while preserving both variables 227 | 228 | @instruction 229 | def cond(tag): 230 | # Handle conditional execution based on stack value 231 | return [(r"%(%\n#stack:\nTrue)", # If True on stack 232 | r"%\1`"), # Mark section for processing 233 | (r"%(\n#stack:\nFalse)", # If False on stack 234 | tag+r"\1`"), # Mark section for processing 235 | (r"\n(True|False)`\n", # Clean up True/False and marker 236 | "\n")] 237 | # Uses backtick to mark processed conditions 238 | # Removes True/False from stack after condition is checked 239 | 240 | @instruction 241 | def reactivate(tag): 242 | return [(r"%"+tag+r"\n([^%]*)", 243 | r"%%\n\1")] 244 | 245 | 246 | @instruction 247 | def pause(tag): 248 | return [(r"%%\n([^%]*)", 249 | r"%"+tag+r"\n\1")] 250 | 251 | @instruction 252 | def fork_bool(variable): 253 | return [(r"%%\n([^%]*)", 254 | r"%%\n\1#"+variable+r": True\n%%\n\1#"+variable+r": False\n") 255 | ] 256 | 257 | @instruction 258 | def fork_inactive(tag): 259 | return [(r"%%\n([^%]*)", 260 | r"%%\n\1" + "%"+tag+r"\n\1") 261 | ] 262 | 263 | @instruction 264 | def fork_with_new_var(tag, vars): 265 | # Creates a copy of the active thread and adds a new variable to the inactive copy 266 | # The original thread stays active, the copy becomes inactive and tagged 267 | # tag: tag to mark the inactive thread 268 | # var: name of the new variable to create 269 | # name: value to assign to the new variable 270 | return [(r"%%\n([^%]*)", # Match active thread 271 | r"%%\n\1%" + tag + r"\n\1" + "\n".join("#"+var + r": " + val for var, val in vars.items()) + r"\n")] 272 | # Groups: 273 | # 1: ([^%]*) - Captures the entire thread content 274 | # Result: Creates two threads: 275 | # 1. Active thread (%%): Original content unchanged 276 | # 2. Inactive thread (%tag): Original content plus new variable 277 | 278 | @instruction 279 | def fork_list_pop(src_list_var, dst_var, tag): 280 | return [*list_pop(src_list_var, None), 281 | *fork_inactive('zztmp'), 282 | *pause('zz1tmp'), 283 | *reactivate('zztmp'), 284 | *assign_pop(dst_var), 285 | *delete_var(src_list_var), 286 | *pause(tag), 287 | *reactivate('zz1tmp'), 288 | *pop()] 289 | 290 | @instruction 291 | def fix_double_list(): 292 | return [(";;", ";")]*10 + [(": ;", ": ")] 293 | 294 | @instruction 295 | def destroy_active_threads(): 296 | return [(r"(%%\n[^%]*)", 297 | r"") 298 | ] 299 | 300 | @instruction 301 | def variable_uniq(variable, maxn=10): 302 | uniq = [ 303 | # Match duplicates anywhere in the list 304 | (r"(%%[^%]*#"+variable+r": [^\n]*)([^;\n]*;)\2+([^%\n]*)", 305 | r"\1\2\3") 306 | ] 307 | return uniq * maxn 308 | 309 | def expand_castling(): 310 | """Convert FEN castling rights (KQkq) to individual boolean variables.""" 311 | patterns = [] 312 | 313 | # Generate all possible combinations 314 | pieces = ['K', 'Q', 'k', 'q'] 315 | for i in range(2**4): # 16 possibilities 316 | # Create the FEN castling string 317 | fen_str = '' 318 | bools = [] 319 | for j, piece in enumerate(pieces): 320 | if i & (1 << j): 321 | fen_str += piece 322 | if piece == 'K': 323 | bools.append("white_king: True") 324 | elif piece == 'Q': 325 | bools.append("white_queen: True") 326 | elif piece == 'k': 327 | bools.append("black_king: True") 328 | elif piece == 'q': 329 | bools.append("black_queen: True") 330 | else: 331 | if piece == 'K': 332 | bools.append("white_king: False") 333 | elif piece == 'Q': 334 | bools.append("white_queen: False") 335 | elif piece == 'k': 336 | bools.append("black_king: False") 337 | elif piece == 'q': 338 | bools.append("black_queen: False") 339 | 340 | if not fen_str: 341 | fen_str = '-' 342 | 343 | patterns.append(( 344 | f"(%%[^%]*)(#castling: {fen_str}\\n)", 345 | "\\1" + "\n".join([f"#castle_{b}" for b in sorted(bools)]) + "\n" 346 | )) 347 | 348 | # Remove the original castling line 349 | patterns.append(( 350 | r"#castling: [KQkq-]+\n", 351 | r"" 352 | )) 353 | 354 | return patterns 355 | 356 | @instruction 357 | def fen(): 358 | return [("(%%\n#stack:\n[^ ]*) [^\n]*\n", r"\1\n")] 359 | 360 | @instruction 361 | def expand_chess(): 362 | return [ 363 | # 1) Move FEN from stack top to #fen: 364 | (r"(%%\n#stack:\n)([^\n]+)\n([^%]*)", 365 | r"\1\3#fen: \2\n"), 366 | 367 | # 2) Extract the turn from FEN. The FEN format is typically: 368 | # [piece_placement] [turn] [castling] [en_passant] [halfmove] [fullmove] 369 | # We only need turn (w or b). This assumes at least two fields in the FEN. 370 | # After this step, we have #fen: [piece_placement] on one line 371 | # and #turn: w or #turn: b on another line. 372 | 373 | (r"(#fen:\s+)([rnbqkpRNBQKP1-8/]+)\s+([wb])\s+([KQkq]+|-)\s+([a-h][1-8]|-).*", 374 | r"\1\2\n#turn: \3\n#castling: \4\n#ep: \5"), 375 | 376 | # 3) Split the piece_placement (in #fen:) into ranks #rank8: ... through #rank1: ... 377 | # piece_placement = something like "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" 378 | (r"(#fen:\s+)([^/]*)/([^/]*)/([^/]*)/([^/]*)/([^/]*)/([^/]*)/([^/]*)/([^ \n]*)", 379 | r"#fen:\n#rank8: \2\n#rank7: \3\n#rank6: \4\n#rank5: \5\n#rank4: \6\n#rank3: \7\n#rank2: \8\n#rank1: \9"), 380 | 381 | # 4) Expand digits into spaces for all ranks: 382 | # Replace '8' with 8 spaces 383 | (r"(#rank\d+:.*)8", r"\1 "), 384 | # Replace '7' with 7 spaces 385 | (r"(#rank\d+:.*)7", r"\1 "), 386 | # Replace '6' with 6 spaces 387 | (r"(#rank\d+:.*)6", r"\1 "), 388 | # Replace '5' with 5 spaces 389 | (r"(#rank\d+:.*)5", r"\1 "), 390 | # Replace '4' with 4 spaces 391 | (r"(#rank\d+:.*)4", r"\1 "), 392 | # Replace '3' with 3 spaces 393 | (r"(#rank\d+:.*)3", r"\1 "), 394 | # Replace '2' with 2 spaces 395 | (r"(#rank\d+:.*)2", r"\1 "), 396 | # Replace '1' with 1 space 397 | (r"(#rank\d+:.*)1", r"\1 "), 398 | (r"(#rank\d+:.*)3", r"\1 "), 399 | # Replace '2' with 2 spaces 400 | (r"(#rank\d+:.*)2", r"\1 "), 401 | # Replace '1' with 1 space 402 | (r"(#rank\d+:.*)1", r"\1 "), 403 | (r"(#rank\d+:.*)3", r"\1 "), 404 | # Replace '2' with 2 spaces 405 | (r"(#rank\d+:.*)2", r"\1 "), 406 | # Replace '1' with 1 space 407 | (r"(#rank\d+:.*)1", r"\1 "), 408 | # Replace '2' with 2 spaces 409 | (r"(#rank\d+:.*)2", r"\1 "), 410 | # Replace '1' with 1 space 411 | (r"(#rank\d+:.*)1", r"\1 "), 412 | (r"(#rank\d+:.*)1", r"\1 "), 413 | 414 | # Apply these digit-to-space replacements repeatedly until no more digits remain. 415 | # (The rewriting framework typically re-applies rules until stable.) 416 | 417 | # 5) Each rank now has exactly 8 chars representing pieces or spaces. 418 | # We break each #rankX: line into #aX:, #bX:, ..., #hX: 419 | # Capturing each character: 420 | (r"#rank(\d+): (.{1})(.{1})(.{1})(.{1})(.{1})(.{1})(.{1})(.{1})", 421 | r"#a\1: \2\n#b\1: \3\n#c\1: \4\n#d\1: \5\n#e\1: \6\n#f\1: \7\n#g\1: \8\n#h\1: \9"), 422 | 423 | *expand_castling(), 424 | 425 | # 6) Remove the #fen: line as it is no longer needed: 426 | (r"#fen:[^\n]*\n", r"") 427 | ] 428 | 429 | import re 430 | 431 | def zzassign_stack_to(variables, var, max_repeats=10): 432 | """ 433 | Repeatedly pops items from stack and appends them with semicolons to variable. 434 | Args: 435 | variables: Variables instance to call instructions on 436 | var: Variable name to append to 437 | max_repeats: Maximum number of items to process 438 | """ 439 | # Create the variable if needed with empty string 440 | variables[var] = "" 441 | 442 | # Process up to max_repeats items 443 | for _ in range(max_repeats): 444 | # Check if stack is empty 445 | if variables.is_stack_empty().ite(): 446 | pass 447 | else: 448 | variables[var] += variables.peek() 449 | variables[var] += ';' 450 | variables.merge() 451 | return variables 452 | 453 | @instruction 454 | def assign_stack_to(var, max_repeats=10): 455 | return [ 456 | *push(""), 457 | *assign_pop(var) 458 | ] + [ 459 | (f"(%%\n#stack:\n)([^%#\n]*)\n([^%]*#{var}: )([^\n]*)", 460 | r"\1\3\2;\4") 461 | ]*max_repeats 462 | 463 | @instruction 464 | def contract_spaces(): 465 | # Replace runs of spaces with a single digit, starting from the largest run (8) down to 1. 466 | # Apply repeatedly until no more spaces remain. 467 | x = [ 468 | (r"(#rank._fen: [^\n]*)( )", r"\g<1>8"), 469 | (r"(#rank._fen: [^\n]*)( )", r"\g<1>7"), 470 | (r"(#rank._fen: [^\n]*)( )", r"\g<1>6"), 471 | (r"(#rank._fen: [^\n]*)( )", r"\g<1>5"), 472 | (r"(#rank._fen: [^\n]*)( )", r"\g<1>4"), 473 | ] + [ 474 | (r"(#rank._fen: [^\n]*)( )", r"\g<1>3") 475 | ] * 2 + [ 476 | (r"(#rank._fen: [^\n]*)( )", r"\g<1>2"), 477 | ] * 3 + [ 478 | (r"(#rank._fen: [^\n]*)( )", r"\g<1>1") 479 | ] * 5 480 | return x 481 | 482 | @instruction 483 | def contract_chess(): 484 | return [ 485 | # 1) Combine each rank's squares into a single line: 486 | # For rank 1 as an example: 487 | # #a1: X\n#b1: Y\n...#h1: Z => #rank1_fen: XYZ....H 488 | # 489 | # Do this for all ranks (1 through 8): 490 | 491 | # Remove white kingside castling if king or rook not in place 492 | (r"(%%[^%]*)(#e1: [^K\n].*\n|#h1: [^R\n].*\n)([^%]*#castle_white_king: )True", 493 | r"\1\2\3False"), 494 | 495 | # Remove white queenside castling if king or rook not in place 496 | (r"(%%[^%]*)(#e1: [^K\n].*\n|#a1: [^R\n].*\n)([^%]*#castle_white_queen: )True", 497 | r"\1\2\3False"), 498 | 499 | # Remove black kingside castling if king or rook not in place 500 | (r"(%%[^%]*)(#e8: [^k\n].*\n|#h8: [^r\n].*\n)([^%]*#castle_black_king: )True", 501 | r"\1\2\3False"), 502 | 503 | # Remove black queenside castling if king or rook not in place 504 | (r"(%%[^%]*)(#e8: [^k\n].*\n|#a8: [^r\n].*\n)([^%]*#castle_black_queen: )True", 505 | r"\1\2\3False"), 506 | 507 | 508 | (r"(%%[^%]*)#a1: ([^\n])\n#b1: ([^\n])\n#c1: ([^\n])\n#d1: ([^\n])\n#e1: ([^\n])\n#f1: ([^\n])\n#g1: ([^\n])\n#h1: ([^\n])", 509 | r"\1#rank1_fen: \2\3\4\5\6\7\8\9"), 510 | (r"(%%[^%]*)#a2: ([^\n])\n#b2: ([^\n])\n#c2: ([^\n])\n#d2: ([^\n])\n#e2: ([^\n])\n#f2: ([^\n])\n#g2: ([^\n])\n#h2: ([^\n])", 511 | r"\1#rank2_fen: \2\3\4\5\6\7\8\9"), 512 | (r"(%%[^%]*)#a3: ([^\n])\n#b3: ([^\n])\n#c3: ([^\n])\n#d3: ([^\n])\n#e3: ([^\n])\n#f3: ([^\n])\n#g3: ([^\n])\n#h3: ([^\n])", 513 | r"\1#rank3_fen: \2\3\4\5\6\7\8\9"), 514 | (r"(%%[^%]*)#a4: ([^\n])\n#b4: ([^\n])\n#c4: ([^\n])\n#d4: ([^\n])\n#e4: ([^\n])\n#f4: ([^\n])\n#g4: ([^\n])\n#h4: ([^\n])", 515 | r"\1#rank4_fen: \2\3\4\5\6\7\8\9"), 516 | (r"(%%[^%]*)#a5: ([^\n])\n#b5: ([^\n])\n#c5: ([^\n])\n#d5: ([^\n])\n#e5: ([^\n])\n#f5: ([^\n])\n#g5: ([^\n])\n#h5: ([^\n])", 517 | r"\1#rank5_fen: \2\3\4\5\6\7\8\9"), 518 | (r"(%%[^%]*)#a6: ([^\n])\n#b6: ([^\n])\n#c6: ([^\n])\n#d6: ([^\n])\n#e6: ([^\n])\n#f6: ([^\n])\n#g6: ([^\n])\n#h6: ([^\n])", 519 | r"\1#rank6_fen: \2\3\4\5\6\7\8\9"), 520 | (r"(%%[^%]*)#a7: ([^\n])\n#b7: ([^\n])\n#c7: ([^\n])\n#d7: ([^\n])\n#e7: ([^\n])\n#f7: ([^\n])\n#g7: ([^\n])\n#h7: ([^\n])", 521 | r"\1#rank7_fen: \2\3\4\5\6\7\8\9"), 522 | (r"(%%[^%]*)#a8: ([^\n])\n#b8: ([^\n])\n#c8: ([^\n])\n#d8: ([^\n])\n#e8: ([^\n])\n#f8: ([^\n])\n#g8: ([^\n])\n#h8: ([^\n])", 523 | r"\1#rank8_fen: \2\3\4\5\6\7\8\9"), 524 | 525 | 526 | 527 | # 2) Contract castling rights into FEN format 528 | # Create empty temp castling variable 529 | (r"(%%[^%]*)#castle_black_king: ([^\n]*)\n#castle_black_queen: ([^\n]*)\n#castle_white_king: ([^\n]*)\n#castle_white_queen: ([^\n]*)\n", 530 | r"\1#castle_black_king: \2\n#castle_black_queen: \3\n#castle_white_king: \4\n#castle_white_queen: \5\n#castling_temp: \n"), 531 | 532 | 533 | (r"(#castle_white_king: True\n[^%]*#castling_temp: [^\n]*)", 534 | r"\1K"), 535 | (r"(#castle_white_queen: True\n[^%]*#castling_temp: [^\n]*)", 536 | r"\1Q"), 537 | (r"(#castle_black_king: True\n[^%]*#castling_temp: [^\n]*)", 538 | r"\1k"), 539 | (r"(#castle_black_queen: True\n[^%]*#castling_temp: [^\n]*)", 540 | r"\1q"), 541 | 542 | # If no castling rights, use "-" 543 | (r"(#castling_temp: )\n", 544 | r"\1-\n"), 545 | 546 | # 4) Convert runs of spaces in fen_line to digits. 547 | # We'll rely on a separate function to produce these rules, which we will then 548 | # apply repeatedly until no spaces remain. 549 | ] + contract_spaces() + [ 550 | 551 | # 3) Combine all ranks into a single fen_line (note fen order: rank8/rank7/.../rank1): 552 | (r"#rank8_fen: ([^\n]+)\n#rank7_fen: ([^\n]+)\n#rank6_fen: ([^\n]+)\n#rank5_fen: ([^\n]+)\n#rank4_fen: ([^\n]+)\n#rank3_fen: ([^\n]+)\n#rank2_fen: ([^\n]+)\n#rank1_fen: ([^\n]+)", 553 | r"#fen_line: \1/\2/\3/\4/\5/\6/\7/\8"), 554 | 555 | # 5) Add turn and castling info to fen_line: 556 | (r"#fen_line: ([^\n]+)\n#turn: ([wb])\n#castle_[^:]+:.*\n#castle_[^:]+:.*\n#castle_[^:]+:.*\n#castle_[^:]+:.*\n#castling_temp: ([^\n]+)\n#ep: ([^\n]+)", 557 | r"#fen_line: \1 \2 \3 \4"), 558 | 559 | # 6) Clean up intermediate variables 560 | (r"(%%[^%]*)(#[a-h]\d:[^\n]*\n)*", r"\1"), 561 | (r"(%%[^%]*)(#rank\d+_fen:[^\n]*\n)", r"\1"), 562 | (r"(%%[^%]*)(#castle_[^:]+:[^\n]*\n)", r"\1"), 563 | (r"(%%[^%]*)(#castling_temp:[^\n]*\n)", r"\1"), 564 | 565 | # 7) Move fen_line back onto the stack: 566 | (r"(%%\n#stack:\n)([^%]*)#fen_line: ([^\n]+)\n", 567 | r"\1\3\n\2"), 568 | 569 | # 8) Remove any remaining intermediate variables 570 | (r"(%%[^%]*)(#fen_line:[^\n]*\n)", r"\1"), 571 | (r"(%%[^%]*)(#turn:[^\n]*\n)", r"\1"), 572 | (r"(%%[^%]*)(#ep:[^\n]*\n)", r"\1") ] 573 | 574 | @instruction 575 | def binary_add(): 576 | patterns = [] 577 | 578 | patterns.append(( 579 | r"(%%\n#stack:\n)", 580 | r"\1bit:\n", 581 | )) 582 | 583 | for bit in range(10): 584 | patterns.append(( 585 | rf"(%%\n#stack:\nbit:)AA", 586 | rf"\1A" 587 | )) 588 | patterns.append(( 589 | rf"(%%\n#stack:\nbit:A*)\nint([01]{{{9-bit}}})1([01]{{{bit}}})", 590 | rf"\1A\nint\g<2>0\g<3>" 591 | )) 592 | patterns.append(( 593 | rf"(%%\n#stack:\nbit:A*)(\nint.*\nint[01]{{{9-bit}}}1[01]{{{bit}}})", 594 | rf"\1A\2" 595 | )) 596 | patterns.append(( 597 | rf"(%%\n#stack:\nbit:(AA|))A\nint([01]{{{9-bit}}})0([01]{{{bit}}})", 598 | rf"\1\nint\g<3>1\g<4>" 599 | )) 600 | patterns.extend(pop()) 601 | patterns.extend(swap()) 602 | patterns.extend(pop()) 603 | 604 | return patterns 605 | 606 | @instruction 607 | def binary_subtract(): 608 | patterns = [] 609 | 610 | patterns.append(( 611 | r"(%%\n#stack:\n)", 612 | r"\1bit:A\n", 613 | )) 614 | 615 | for bit in range(10): 616 | patterns.append(( 617 | rf"(%%\n#stack:\nbit:)AA", 618 | rf"\1A" 619 | )) 620 | patterns.append(( 621 | rf"(%%\n#stack:\nbit:A*)\nint([01]{{{9-bit}}})1([01]{{{bit}}})", 622 | rf"\1A\nint\g<2>0\g<3>" 623 | )) 624 | patterns.append(( 625 | rf"(%%\n#stack:\nbit:A*)(\nint.*\nint[01]{{{9-bit}}}0[01]{{{bit}}})", 626 | rf"\1A\2" 627 | )) 628 | patterns.append(( 629 | rf"(%%\n#stack:\nbit:(AA|))A\nint([01]{{{9-bit}}})0([01]{{{bit}}})", 630 | rf"\1\nint\g<3>1\g<4>" 631 | )) 632 | patterns.extend(pop()) 633 | patterns.extend(swap()) 634 | patterns.extend(pop()) 635 | patterns.append(( 636 | rf"(%%\n#stack:\n)int1[01]*", 637 | rf"\1int0000000000" 638 | )) 639 | 640 | return patterns 641 | 642 | 643 | @instruction 644 | def to_unary(): 645 | patterns = [] 646 | 647 | # Process each bit from left (bit=9) down to right (bit=0) 648 | for bit in reversed(range(10)): 649 | place_val = 2 ** bit 650 | patterns.append(( 651 | rf"(%%\n#stack:\n)int([01]{{{9-bit}}})1([01]{{{bit}}})", 652 | rf"\1int\g<2>0\g<3>{'A'*place_val}" 653 | )) 654 | 655 | # Finally, remove all 'int0*' if only zeros remain in the binary part 656 | # so that the result is purely unary 'A's (or nothing if it was zero). 657 | patterns.append(( 658 | r"(%%\n#stack:\n)(int0*)", 659 | r"\1" 660 | )) 661 | 662 | return patterns 663 | 664 | @instruction 665 | def from_unary(): 666 | patterns = [] 667 | 668 | # 1) Insert a temporary "int" prefix with no bits decided yet. 669 | # We'll rebuild bits into that. We match ANY unary As 670 | # after "#stack:\n" and turn it into "int + those As". 671 | # 672 | # So: "%%\n#stack:\nAAAA" -> "%%\n#stack:\nintAAAA" 673 | # 674 | patterns.append(( 675 | r"(%%\n#stack:\n)(A*)", 676 | r"\1int\g<2>" 677 | )) 678 | 679 | # 2) For each bit from 9 down to 0, test whether we have >= 2^bit A's left. 680 | for bit in reversed(range(10)): 681 | place_val = 2**bit 682 | 683 | # (a) If we have at least 'place_val' A's, set that bit to '1'. 684 | patterns.append(( 685 | rf"(%%\n#stack:\nint[01]*)(A{{{place_val}}})(A*)", 686 | rf"\g<1>1\g<3>" 687 | )) 688 | 689 | # (b) Otherwise, set that bit to '0'. 690 | patterns.append(( 691 | rf"(%%\n#stack:\n)int([01]{{{9-bit}}})([^01]A*)", 692 | rf"\1int\g<2>0\g<3>" 693 | )) 694 | 695 | return patterns 696 | 697 | @instruction 698 | def add_unary(): 699 | # Add top two unary numbers by concatenating their A's 700 | return [(r"(%%\n#stack:\n)(A*)\n(A*)\n", # Match top two unary numbers 701 | r"\1\2\3\n")] # Concatenate them together 702 | # Groups: 703 | # 1: (%%\n#stack:\n) - Matches from %% through #stack:\n 704 | # 2: (A*) - Captures first unary number (sequence of A's) 705 | # 3: (A*) - Captures second unary number (sequence of A's) 706 | # Result: Combines both sequences of A's into single sum 707 | 708 | 709 | 710 | 711 | @instruction 712 | def sub_unary(): 713 | """ 714 | Subtract the top unary number (B) from the next unary number (A) on the stack. 715 | Both A and B are represented as sequences of 'A's. 716 | The result (A - B) is pushed back onto the stack. 717 | If B is greater than A, the result is zero (no 'A's). 718 | """ 719 | return [ 720 | # Pattern 1: A >= B 721 | ( 722 | r"(%%\n#stack:\n)" # Group 1: Thread and #stack header 723 | r"(A*)\n" # Group 2: B (top of stack) 724 | r"\2(A*)\n", # Group 3: A starts with B's A's, Group 4: Remaining A's after subtraction 725 | r"\1`sub\3\n" # Replacement: Mark with `sub and push remaining A's 726 | ), 727 | # Pattern 2: A < B 728 | ( 729 | r"(%%\n#stack:\n)" # Group 1: Thread and #stack header 730 | r"(A*)\n" # Group 2: B (top of stack) 731 | r"(A*)\n", # Group 3: A 732 | r"\1`zero\n" # Replacement: Mark with `zero 733 | ), 734 | # Pattern 3: Finalize subtraction by removing `sub 735 | ( 736 | r"`sub(A*)\n", # Match the `sub marker followed by remaining A's 737 | r"\1\n" # Replace with the remaining A's only 738 | ), 739 | # Pattern 4: Finalize zero by removing `zero 740 | ( 741 | r"`zero\n", # Match the `zero` marker 742 | r"\n" # Replace with nothing (zero result) 743 | ), 744 | ] 745 | 746 | 747 | @instruction 748 | def mod2_unary(): 749 | return [(r"(%%\n#stack:\n)(A*)\2\n", 750 | r"\1`True\n"), 751 | (r"(%%\n#stack:\n)[^`\n][^\n]*\n", 752 | r"\1`False\n"), 753 | (r"(%%\n#stack:\n)\n\n", 754 | r"\1`False\n"), 755 | ("`", "") 756 | 757 | ] 758 | 759 | @instruction 760 | def string_cat(): 761 | # Add top two unary numbers by concatenating their A's 762 | return [(r"(%%\n#stack:\n)([^\n]*)\n([^\n]*)\n", # Match top two unary numbers 763 | r"\1\2\3\n")] # Concatenate them together 764 | # Groups: 765 | # 1: (%%\n#stack:\n) - Matches from %% through #stack:\n 766 | # 2: ([^\n]*) - Captures first string 767 | # 3: ([^\n]*) - Captures second string 768 | # Result: Combines both sequences into single string 769 | 770 | @instruction 771 | def boolean_not(): 772 | return [ 773 | # Convert True to False (marked with backtick) 774 | (r"(%%\n#stack:\n)True\n", 775 | r"\1`False\n"), 776 | 777 | # Convert False to True (marked with backtick) 778 | (r"(%%\n#stack:\n)False\n", 779 | r"\1`True\n"), 780 | 781 | # Remove the backtick marker 782 | (r"`", 783 | r"") 784 | ] 785 | 786 | @instruction 787 | def boolean_and(): 788 | return [ 789 | # True AND True = True (marked with backtick) 790 | (r"(%%\n#stack:\n)True\nTrue\n", 791 | r"\1`True\n"), 792 | 793 | # Any other combination = False (if not already marked) 794 | (r"(%%\n#stack:\n)([^`][^\n]*)\n([^\n]*)\n", 795 | r"\1False\n"), 796 | 797 | # Remove the backtick marker 798 | (r"`", 799 | r"") 800 | ] 801 | 802 | @instruction 803 | def boolean_or(): 804 | return [ 805 | # False OR False = False (marked with backtick) 806 | (r"(%%\n#stack:\n)False\nFalse\n", 807 | r"\1`False\n"), 808 | 809 | # Any other combination = True (if not already marked) 810 | (r"(%%\n#stack:\n)([^`][^\n]*)\n([^\n]*)\n", 811 | r"\1True\n"), 812 | 813 | # Remove the backtick marker 814 | (r"`", 815 | r"") 816 | ] 817 | 818 | @instruction 819 | def greater_than(): 820 | return [ 821 | # If first has more A's than second with some remainder, it's greater 822 | # Match: first sequence followed by second sequence plus at least one more A 823 | (r"(%%\n#stack:\n)(A*)(A+)\n\2\n", # Pattern matches when first > second 824 | r"\1`True\n"), 825 | 826 | # If not already marked True, then first isn't greater 827 | # Use * instead of + to allow empty strings 828 | (r"(%%\n#stack:\n)([^`\n]*)\n([^\n]*)\n", # Any two values including empty 829 | r"\1False\n"), 830 | 831 | # Remove the backtick marker from True results 832 | (r"`", 833 | r"") 834 | ] 835 | 836 | @instruction 837 | def less_than(): 838 | return [ 839 | *swap(), 840 | *greater_than(), 841 | ] 842 | 843 | @instruction 844 | def less_equal_than(): 845 | return [ 846 | *greater_than(), 847 | *boolean_not() 848 | ] 849 | 850 | @instruction 851 | def greater_equal_than(): 852 | return [ 853 | *less_than(), 854 | *boolean_not() 855 | ] 856 | 857 | @instruction 858 | def intxy_to_location(var1, var2): 859 | out = lookup(var1) + lookup(var2) 860 | 861 | for i in range(8): 862 | out.append((r"(%%\n#stack:\n)"+i2s(i), 863 | r"\g<1>"+str(i+1))) 864 | out += swap() 865 | 866 | for i in range(8): 867 | out.append((r"(%%\n#stack:\n)"+i2s(i), 868 | r"\g<1>"+chr(0x61+i))) 869 | 870 | out += string_cat() 871 | 872 | return out 873 | 874 | @instruction 875 | def square_to_xy(): 876 | # First convert the file (a-h) to number (0-7) 877 | file_patterns = [] 878 | for i, file in enumerate('abcdefgh'): 879 | file_patterns.append(( 880 | r"(%%\n#stack:\n)" + file + r"([1-8])\n", 881 | r"\1" + i2s(i) + r"\n\2\n" 882 | )) 883 | 884 | # Then convert the rank (1-8) to number (0-7) 885 | rank_patterns = [] 886 | for i in range(1, 9): 887 | rank_patterns.append(( 888 | r"(%%\n#stack:\n)([^\n]*)\n" + str(i) + r"\n", 889 | r"\1\2\n" + i2s(i-1) + r"\n" 890 | )) 891 | 892 | return file_patterns + rank_patterns 893 | 894 | 895 | @instruction 896 | def join_pop(sub): 897 | return [(r"(%%\n#stack:\n)(.*\n)([^%]*)%"+sub+r"\n#stack:\n(.*\n)[^%]*", 898 | r"\1\4\2\3")] 899 | 900 | @instruction 901 | def delete_var(var): 902 | return [ 903 | # Match and remove the entire variable line 904 | (r"(%%[^%]*)(#"+var+r": [^\n]*\n)", 905 | r"\1") 906 | ] 907 | 908 | @instruction 909 | def list_pop(src_list_var, dst_var): 910 | """ 911 | Pop first item from a semicolon-delimited list variable and assign to destination variable. 912 | 913 | 1. Takes everything up to first semicolon from source list variable 914 | 2. Puts that value on top of stack 915 | 3. Updates source list variable to remove the popped item 916 | 4. Assigns top of stack to destination variable using assign_pop 917 | 918 | Args: 919 | src_list_var: Source list variable name to pop from 920 | dst_var: Destination variable name to assign popped value to 921 | """ 922 | # First get everything before first semicolon onto stack, 923 | # and update source variable to remove it 924 | patterns = [ 925 | # Handle case with items after semicolon: 926 | # Take first item to stack, leave rest in variable 927 | 928 | (r"(%%[^%]*#stack:\n)([^%]*#" + src_list_var + r": )([^\n;]*);([^;\n]*)", 929 | r"\1\3\n\2\4"), 930 | ] 931 | 932 | if dst_var is not None: 933 | # Then use assign_pop to move stack top to destination 934 | patterns.extend(assign_pop(dst_var)) 935 | 936 | return patterns 937 | 938 | @instruction 939 | def make_pretty(has_move): 940 | # Build the capture pattern 941 | capture_pattern = '' 942 | files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] 943 | ranks = ['8', '7', '6', '5', '4', '3', '2', '1'] 944 | 945 | for rank in ranks: 946 | for file in files: 947 | capture_pattern += f"#{file}{rank}:\\s*([kqrbnpKQRBNP ])\\s*" 948 | 949 | # The board template with capture group references 950 | board_template = """ ╔═════════════════╗ 951 | 8 ║ \\1 \\2 \\3 \\4 \\5 \\6 \\7 \\8 ║ 952 | 7 ║ \\9 \\10 \\11 \\12 \\13 \\14 \\15 \\16 ║ 953 | 6 ║ \\17 \\18 \\19 \\20 \\21 \\22 \\23 \\24 ║ 954 | 5 ║ \\25 \\26 \\27 \\28 \\29 \\30 \\31 \\32 ║ 955 | 4 ║ \\33 \\34 \\35 \\36 \\37 \\38 \\39 \\40 ║ 956 | 3 ║ \\41 \\42 \\43 \\44 \\45 \\46 \\47 \\48 ║ 957 | 2 ║ \\49 \\50 \\51 \\52 \\53 \\54 \\55 \\56 ║ 958 | 1 ║ \\57 \\58 \\59 \\60 \\61 \\62 \\63 \\64 ║ 959 | ╚═════════════════╝ 960 | a b c d e f g h 961 | 962 | ~""" 963 | 964 | 965 | # Return list of (pattern, replacement) tuples 966 | return [ 967 | (r"%%\n", r""), 968 | 969 | # First pattern: Capture all positions and create board layout 970 | (capture_pattern, board_template), 971 | 972 | (r"#castle_black_king: ([^\n]*)\n#castle_black_queen: ([^\n]*)\n#castle_white_king: ([^\n]*)\n#castle_white_queen: ([^\n]*)\n", 973 | r"#castle_black_king: \1\n#castle_black_queen: \2\n#castle_white_king: \3\n#castle_white_queen: \4\n#castling_temp: \n"), 974 | 975 | 976 | (r"(.*#castle_white_king: True\n[^%]*#castling_temp: [^\n]*)", 977 | r"\1K"), 978 | (r"(.*#castle_white_queen: True\n[^%]*#castling_temp: [^\n]*)", 979 | r"\1Q"), 980 | (r"(.*#castle_black_king: True\n[^%]*#castling_temp: [^\n]*)", 981 | r"\1k"), 982 | (r"(.*#castle_black_queen: True\n[^%]*#castling_temp: [^\n]*)", 983 | r"\1q"), 984 | 985 | # If no castling rights, use "-" 986 | (r"(.*#castling_temp: )\n", 987 | r"\1-\n"), 988 | 989 | 990 | (r"#castling_temp: ([^\n]*)\n#ep: ([^\n]*)\n", 991 | r"[Castling Rights: \1, En Passant: \2]\n"), 992 | 993 | (r"#[^a-h].*\n", ""), 994 | (r"#.[^1-8].*\n", ""), 995 | ] + [ 996 | # Second pattern: Convert pieces to UTF-8 symbols 997 | ("║(.*)K", r"║\1♔"), ("║(.*)Q", r"║\1♕"), ("║(.*)R", r"║\1♖"), ("║(.*)B", r"║\1♗"), ("║(.*)N", r"║\1♘"), ("║(.*)P", r"║\1♙"), 998 | ("║(.*)k", r"║\1♚"), ("║(.*)q", r"║\1♛"), ("║(.*)r", r"║\1♜"), ("║(.*)b", r"║\1♝"), ("║(.*)n", r"║\1♞"), ("║(.*)p", r"║\1♟") 999 | ] * 8 + [ 1000 | 1001 | 1002 | ] + ([("~", "Move notation: [src][dest] (e.g. e2e4) or 'q' to quit\n"), (r"\]\n", r"]\nEnter Your Move: ")] if has_move else [("~","\n")]) 1003 | 1004 | 1005 | 1006 | @instruction 1007 | def unpretty(has_move): 1008 | # Create piece conversions 1009 | pieces = { 1010 | 'K': '♔', 'Q': '♕', 'R': '♖', 'B': '♗', 'N': '♘', 'P': '♙', 1011 | 'k': '♚', 'q': '♛', 'r': '♜', 'b': '♝', 'n': '♞', 'p': '♟' 1012 | } 1013 | 1014 | # Create board coordinates 1015 | files = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] 1016 | ranks = ['8', '7', '6', '5', '4', '3', '2', '1'] 1017 | 1018 | # Build the piece pattern 1019 | piece_chars = ''.join(pieces.values()) + ' ' 1020 | piece_pattern = f'[{piece_chars}]' 1021 | 1022 | # Generate board pattern programmatically 1023 | board_lines = [ 1024 | " ╔═════════════════╗" 1025 | ] 1026 | 1027 | for rank in ranks: 1028 | captures = ' '.join(f'({piece_pattern})' for _ in files) 1029 | board_lines.append(f"{rank} ║ {captures} ║") 1030 | 1031 | board_lines.extend([ 1032 | " ╚═════════════════╝", 1033 | " a b c d e f g h", 1034 | "", 1035 | ".*", 1036 | r"\[Castling Rights: (.*), En Passant: (.*)\]", 1037 | ]) 1038 | if has_move: 1039 | board_lines.append( 1040 | r"Enter Your Move: ([a-h][1-8][a-h][1-8]|q)(%|[^%])*" # Move input - capture group for the move 1041 | ) 1042 | else: 1043 | board_lines.append( 1044 | "[^%]*" 1045 | ) 1046 | 1047 | 1048 | board_pattern = '\n'.join(board_lines) 1049 | 1050 | # Generate replacement template 1051 | replacement_lines = ["%%\n#stack:"] 1052 | pos = 1 1053 | for rank in ranks: 1054 | for file in files: 1055 | replacement_lines.append(f"#{file}{rank}: \\{pos}") 1056 | pos += 1 1057 | 1058 | # Add src and dst variables for the move (using capture group \65 for the move) 1059 | replacement_lines.extend([ 1060 | "#turn: w", 1061 | "#castling: \\65", 1062 | "#ep: \\66", 1063 | ]) 1064 | if has_move: 1065 | replacement_lines.extend([ 1066 | "#move: \\67", 1067 | "#src: \\67", 1068 | "#dst: \\67\n", 1069 | ]) 1070 | else: 1071 | replacement_lines.append("") 1072 | 1073 | 1074 | replacement = '\n'.join(replacement_lines) 1075 | 1076 | # Build patterns list 1077 | patterns = [(board_pattern, replacement), *expand_castling()] 1078 | 1079 | if has_move: 1080 | # Add patterns to split move into src and dst 1081 | patterns.extend([ 1082 | 1083 | # Extract first two characters for src 1084 | (r"#src: ([a-h])([1-8])[a-h][1-8]", r"#src: \1\2"), 1085 | # Extract last two characters for dst 1086 | (r"#dst: [a-h][1-8]([a-h])([1-8])", r"#dst: \1\2"), 1087 | ]) 1088 | patterns.append((r"[ \n]*%", "%")) 1089 | 1090 | # Add piece conversion patterns 1091 | patterns.extend((unicode_piece, letter) for letter, unicode_piece in pieces.items()) 1092 | 1093 | return patterns 1094 | 1095 | @instruction 1096 | def piece_value(): 1097 | """ 1098 | Computes material value of a chess position in FEN notation. 1099 | Returns value as (white - black + 100) to keep positive 1100 | """ 1101 | # Remove all numbers (apply multiple times) 1102 | patterns = [(r"(%%\n#stack:\n[^ ]*) [^\n]*", r"\1")] 1103 | patterns += [(r"(%%\n#stack:\n[^\n]*)[ /1-8]([^\n]*\n)", r"\1\2")] * 32 1104 | 1105 | # Duplicate the stack for white and black calculations 1106 | patterns.extend(dup()) 1107 | 1108 | # For white value: Remove all lowercase (apply multiple times) 1109 | patterns.extend([ 1110 | (r"(%%\n#stack:\n[^\n]*)([a-z])([^\n]*\n)", r"\1\3") 1111 | ] * 32) 1112 | 1113 | # Convert white pieces to unary values (apply each multiple times) 1114 | for piece, value in [ 1115 | ("K", 2*"AAAAAAAAAA"), # King = 10 1116 | ("Q", 2*"AAAAAAAAA"), # Queen = 9 1117 | ("R", 2*"AAAAA"), # Rook = 5 1118 | ("B", 2*"AAA"), # Bishop = 3 1119 | ("N", 2*"AAA"), # Knight = 3 1120 | ("P", 2*"A"), # Pawn = 1 1121 | ]: 1122 | patterns.extend([ 1123 | (r"(%%\n#stack:\n[^\n]*)"+piece+r"([^\n]*\n)", r"\1"+value+r"\2") 1124 | ] * 16) 1125 | 1126 | # Swap to process second element 1127 | patterns.extend(swap()) 1128 | 1129 | # For black value: Remove all uppercase (apply multiple times) 1130 | patterns.extend([ 1131 | (r"(%%\n#stack:\n[^\n]*)([A-Z])([^\n]*\n)", r"\1\3") 1132 | ] * 32) 1133 | 1134 | # Convert black pieces to unary values (apply each multiple times) 1135 | for piece, value in [ 1136 | ("k", 2*"AAAAAAAAAA"), # King = 10 1137 | ("q", 2*"AAAAAAAAA"), # Queen = 9 1138 | ("r", 2*"AAAAA"), # Rook = 5 1139 | ("b", 2*"AAA"), # Bishop = 3 1140 | ("n", 2*"AAA"), # Knight = 3 1141 | ("p", 2*"A"), # Pawn = 1 1142 | ]: 1143 | patterns.extend([ 1144 | (r"(%%\n#stack:\n[^\n]*)"+piece+r"([^\n]*\n)", r"\1"+value+r"\2") 1145 | ] * 16) 1146 | 1147 | # Push 200 in unary (200 A's) 1148 | patterns.extend(push("A" * 200)) 1149 | 1150 | # Add white pieces value 1151 | patterns.extend(swap()) 1152 | patterns.extend(sub_unary()) 1153 | 1154 | # Subtract black pieces value 1155 | patterns.extend(add_unary()) 1156 | 1157 | return patterns 1158 | 1159 | @instruction 1160 | def check_king_alive(): 1161 | return [(r"%%([^%]*#next_boards: [^\n]*;([^k/;]*/){7}[^k/;\n]*[ ;][^\n]*\n)", 1162 | r"%%`\1#alive: False\n"), 1163 | (r"%%([^%]*#next_boards: [^\n]*;([^K/;]*/){7}[^K/;\n]*[ ;][^\n]*\n)", 1164 | r"%%`\1#alive: False\n"), 1165 | (r"%%([^`][^%]*)", 1166 | r"%%\1#alive: True\n"), 1167 | (r"`", "") 1168 | ] 1169 | 1170 | @instruction 1171 | def promote_to_queen(): 1172 | return [(fr"(%%[^%]*#{r}1: )p", r"\1q") for r in "abcdefgh"] + [(fr"(%%[^%]*#{r}8: )P", r"\1Q") for r in "abcdefgh"] 1173 | 1174 | @instruction 1175 | def keep_only_first_thread(): 1176 | return [("(%%[^%]*)([^%]|%)*", 1177 | r"\1")] 1178 | 1179 | @instruction 1180 | def keep_only_max_thread(): 1181 | return [(r"(%%\n#stack:\n(A+)\n[^%]*)(%%\n#stack:\n\2A*[^%]*)", 1182 | r"\1"), 1183 | (r"(%%[^%]*)(%%[^%]*)", r"\2\1"), 1184 | ]*50 1185 | 1186 | @instruction 1187 | def keep_only_last_thread(): 1188 | return [("([^%]|%)*(%%[^%]*)", 1189 | r"\2")] 1190 | 1191 | @instruction 1192 | def keep_only_min_thread(): 1193 | return [(r"(%%\n#stack:\n(A+)\n[^%]*)(%%\n#stack:\n\2A*[^%]*)", 1194 | r"\1"), 1195 | (r"(%%[^%]*)(%%[^%]*)", r"\2\1"), 1196 | ]*50 1197 | 1198 | 1199 | @instruction 1200 | def illegal_move(): 1201 | return [("^[^%<]*$", 1202 | "*Illegal Move*\nYou Lose.\nGame over.\n")] 1203 | 1204 | @instruction 1205 | def test_checkmate(): 1206 | return [("^[^*%<]*$", 1207 | "*Checkmate*\nYou win!\nGame over.\n")] 1208 | 1209 | @instruction 1210 | def do_piece_assign(piece_chr, piece, x, y, pos): 1211 | return [(f"%%([^%]*#{pos}: {piece_chr}[^%]*)#{piece}x_lst: ([^\n]*)\n#{piece}y_lst: ([^\n]*)\n#{piece}pos_lst: ([^\n]*)\n", 1212 | fr"%%\1#{piece}x_lst: {x};\2\n#{piece}y_lst: {y};\3\n#{piece}pos_lst: {pos};\4\n")] 1213 | 1214 | def find_all_pieces(variables, color): 1215 | variables.lookup('initial_board') 1216 | variables.expand_chess() 1217 | PIECES = ['king', 'queen', 'rook', 'bishop', 'knight', 'pawn'] 1218 | for piece in PIECES: 1219 | variables[piece+'x_lst'] = '' 1220 | variables[piece+'y_lst'] = '' 1221 | variables[piece+'pos_lst'] = '' 1222 | 1223 | for ii, i in enumerate('abcdefgh'): 1224 | for j in range(1, 9): 1225 | for piece_char, piece in zip('kqrbnp', PIECES): 1226 | variables.do_piece_assign(color(piece_char), piece, i2s(ii), i2s(j-1), str(i)+str(j)) 1227 | 1228 | def find_pieces(variables, color, piece, piece_chr): 1229 | variables[piece+'x_lst'] = '' 1230 | variables[piece+'y_lst'] = '' 1231 | variables[piece+'pos_lst'] = '' 1232 | 1233 | for ii, i in enumerate('abcdefgh'): 1234 | for j in range(1, 9): 1235 | variables.do_piece_assign(color(piece_chr), piece, i2s(ii), i2s(j-1), str(i)+str(j)) 1236 | 1237 | 1238 | @instruction 1239 | def is_same_kind(): 1240 | out = [] 1241 | for piece in 'kqrbnp ': 1242 | out.append((fr"(%%\n#stack:\n){piece.lower()}\n{piece.upper()}\n", 1243 | r"\1`True\n")) 1244 | 1245 | out.extend([ 1246 | # Any other combination = False (if not already marked) 1247 | (r"(%%\n#stack:\n)([^`][^\n]*)\n([^\n]*)\n", 1248 | r"\1False\n"), 1249 | 1250 | # Remove the backtick marker 1251 | (r"`", 1252 | r"") 1253 | ]) 1254 | return out 1255 | 1256 | 1257 | def i2s(num): 1258 | return f"int{num:010b}" 1259 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | ## Copyright (C) 2025, Nicholas Carlini . 2 | ## 3 | ## This program is free software: you can redistribute it and/or modify 4 | ## it under the terms of the GNU General Public License as published by 5 | ## the Free Software Foundation, either version 3 of the License, or 6 | ## (at your option) any later version. 7 | ## 8 | ## This program is distributed in the hope that it will be useful, 9 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | ## GNU General Public License for more details. 12 | ## 13 | ## You should have received a copy of the GNU General Public License 14 | ## along with this program. If not, see . 15 | 16 | import unittest 17 | import re 18 | from typing import Dict, List, Tuple, Union 19 | from dataclasses import dataclass 20 | 21 | from instruction_set import * 22 | from compiler import * 23 | from chess_engine import * 24 | 25 | @dataclass 26 | class CPUState: 27 | """Represents the state of the regex CPU""" 28 | variables: Dict[str, str] 29 | stack: List[str] 30 | 31 | def to_string(self) -> str: 32 | """Convert the state to the %% format string""" 33 | lines = ["%%", "#stack:", *self.stack] 34 | for var_name, value in sorted(self.variables.items()): 35 | lines.append(f"#{var_name}: {value}") 36 | lines = "\n".join(lines) 37 | return lines+"\n" 38 | 39 | @classmethod 40 | def from_string(cls, state_str: str) -> 'CPUState': 41 | """Parse a %% format string into a CPUState""" 42 | if not state_str.startswith("%%"): 43 | raise ValueError("State must start with %%") 44 | 45 | variables = {} 46 | stack = [] 47 | 48 | lines = state_str.split('\n')[:-1] 49 | in_stack = False 50 | 51 | for line in lines[1:]: # Skip %% line 52 | line = line.strip() 53 | if not line and not in_stack: 54 | continue 55 | 56 | if not line.startswith('#'): 57 | if in_stack: 58 | stack.append(line) 59 | continue 60 | 61 | if line == "#stack:": 62 | in_stack = True 63 | continue 64 | 65 | parts = line[1:].split(':', 1) 66 | if len(parts) != 2: 67 | continue 68 | 69 | var_name, value = parts 70 | variables[var_name.strip()] = value.strip() 71 | 72 | return cls(variables=variables, stack=stack) 73 | 74 | 75 | def execute_instruction(state: Dict[str, str], instruction: List[Tuple[str, str]]) -> Dict[str, str]: 76 | """Execute a single instruction on a state dictionary""" 77 | # Convert dict to string format 78 | cpu_state = CPUState( 79 | variables={k: v for k, v in state.items() if k != "stack"}, 80 | stack=state.get("stack", []) 81 | ) 82 | state_str = cpu_state.to_string() 83 | #print("Go", repr(state_str)) 84 | 85 | # Apply regex transformations 86 | for pattern, replacement in instruction: 87 | state_str = re.sub(pattern, replacement, state_str) 88 | #print(repr(state_str)) 89 | 90 | # Convert back to dictionary 91 | result_state = CPUState.from_string(state_str) 92 | return { 93 | **result_state.variables, 94 | "stack": result_state.stack 95 | } 96 | 97 | class RegexCPUTest(unittest.TestCase): 98 | def setUp(self): 99 | self.initial_state = { 100 | "stack": [], 101 | "a1": "R", 102 | "b1": "N", 103 | "c1": "B", 104 | "d1": "Q", 105 | "e1": "K", 106 | "f1": "B", 107 | "g1": "N", 108 | "h1": "R" 109 | } 110 | 111 | def assertStateEqual(self, actual: Dict[str, str], expected: Dict[str, str], msg=None): 112 | """Assert that two state dictionaries are equivalent""" 113 | # Ensure stack is always a list 114 | actual_copy = dict(actual) 115 | expected_copy = dict(expected) 116 | 117 | if "stack" not in actual_copy: 118 | actual_copy["stack"] = [] 119 | if "stack" not in expected_copy: 120 | expected_copy["stack"] = [] 121 | 122 | self.assertEqual(actual_copy, expected_copy, msg) 123 | 124 | def test_push(self): 125 | """Test pushing a value onto the stack""" 126 | 127 | initial = {"stack": []} 128 | expected = {"stack": ["test_value"]} 129 | 130 | result = execute_instruction(initial, push("test_value")) 131 | self.assertStateEqual(result, expected) 132 | 133 | def test_lookup(self): 134 | """Test looking up a variable's value""" 135 | 136 | initial = { 137 | "stack": [], 138 | "test_var": "test_value" 139 | } 140 | expected = { 141 | "stack": ["test_value"], 142 | "test_var": "test_value" 143 | } 144 | 145 | 146 | result = execute_instruction(initial, lookup("test_var")) 147 | self.assertStateEqual(result, expected) 148 | 149 | def test_eq_true(self): 150 | """Test equality comparison with equal values""" 151 | 152 | initial = { 153 | "stack": ["same", "same"] 154 | } 155 | expected = { 156 | "stack": ["True"] 157 | } 158 | 159 | result = execute_instruction(initial, eq()) 160 | self.assertStateEqual(result, expected) 161 | 162 | def test_eq_false(self): 163 | """Test equality comparison with different values""" 164 | 165 | initial = { 166 | "stack": ["value1", "value2"] 167 | } 168 | expected = { 169 | "stack": ["False"] 170 | } 171 | 172 | result = execute_instruction(initial, eq()) 173 | self.assertStateEqual(result, expected) 174 | 175 | def test_state_conversion(self): 176 | """Test state conversion between dict and string formats""" 177 | state_dict = { 178 | "stack": ["value1", "value2"], 179 | "var1": "test1", 180 | "var2": "test2" 181 | } 182 | 183 | # Convert to string and back to dict 184 | cpu_state = CPUState( 185 | variables={k: v for k, v in state_dict.items() if k != "stack"}, 186 | stack=state_dict["stack"] 187 | ) 188 | state_str = cpu_state.to_string() 189 | result_state = CPUState.from_string(state_str) 190 | 191 | result_dict = { 192 | **result_state.variables, 193 | "stack": result_state.stack 194 | } 195 | 196 | self.assertEqual(state_dict, result_dict) 197 | 198 | def test_assign_pop(self): 199 | """Test assigning a value from the stack to a variable""" 200 | 201 | initial = { 202 | "stack": ["test_value"], 203 | "test_var": "old_value" 204 | } 205 | expected = { 206 | "stack": [], 207 | "test_var": "test_value" 208 | } 209 | 210 | result = execute_instruction(initial, assign_pop("test_var")) 211 | self.assertStateEqual(result, expected) 212 | 213 | def test_assign_pop_noexist(self): 214 | """Test assigning a value from the stack to a variable""" 215 | 216 | initial = { 217 | "stack": ["test_value"], 218 | } 219 | expected = { 220 | "stack": [], 221 | "test_var": "test_value" 222 | } 223 | 224 | result = execute_instruction(initial, assign_pop("test_var")) 225 | self.assertStateEqual(result, expected) 226 | 227 | def test_dup(self): 228 | """Test duplicating the top value on the stack""" 229 | 230 | initial = { 231 | "stack": ["test_value"] 232 | } 233 | expected = { 234 | "stack": ["test_value", "test_value"] 235 | } 236 | 237 | result = execute_instruction(initial, dup()) 238 | self.assertStateEqual(result, expected) 239 | 240 | def test_pop(self): 241 | """Test removing the top value from the stack""" 242 | 243 | initial = { 244 | "stack": ["value_to_remove", "remaining_value"] 245 | } 246 | expected = { 247 | "stack": ["remaining_value"] 248 | } 249 | 250 | result = execute_instruction(initial, pop()) 251 | self.assertStateEqual(result, expected) 252 | 253 | def test_multiple_unary_additions(self): 254 | """Test multiple unary addition cases with different numbers""" 255 | 256 | def run_addition_test(num1: str, num2: str, expected: str): 257 | """Helper to run a single addition test case""" 258 | state = {"stack": []} 259 | 260 | # Push first number and convert to unary 261 | state = execute_instruction(state, push(f"int{num1:010b}")) 262 | state = execute_instruction(state, to_unary()) 263 | 264 | # Push second number and convert to unary 265 | state = execute_instruction(state, push(f"int{num2:010b}")) 266 | state = execute_instruction(state, to_unary()) 267 | 268 | # Add and convert back 269 | state = execute_instruction(state, add_unary()) 270 | final_state = execute_instruction(state, from_unary()) 271 | 272 | expected_state = {"stack": [f"int{expected:010b}"]} 273 | self.assertStateEqual(final_state, expected_state) 274 | 275 | # Test case 1: 12 + 34 = 46 276 | run_addition_test(12, 34, 46) 277 | 278 | # Test case 2: 99 + 1 = 100 279 | run_addition_test(99, 1, 100) 280 | 281 | # Test case 3: 220 + 80 = 300 282 | run_addition_test(220, 80, 300) 283 | 284 | def test_multiple_unary_subtractions(self): 285 | """Test multiple unary addition cases with different numbers""" 286 | 287 | def run_subtraction_test(num1: str, num2: str, expected: str): 288 | """Helper to run a single addition test case""" 289 | state = {"stack": []} 290 | 291 | # Push first number and convert to unary 292 | state = execute_instruction(state, push(f"int{num1:010b}")) 293 | state = execute_instruction(state, to_unary()) 294 | 295 | # Push second number and convert to unary 296 | state = execute_instruction(state, push(f"int{num2:010b}")) 297 | state = execute_instruction(state, to_unary()) 298 | 299 | # Add and convert back 300 | state = execute_instruction(state, sub_unary()) 301 | final_state = execute_instruction(state, from_unary()) 302 | 303 | expected_state = {"stack": [f"int{expected:010b}"]} 304 | self.assertStateEqual(final_state, expected_state) 305 | 306 | run_subtraction_test(5, 4, 1) 307 | 308 | run_subtraction_test(5, 0, 5) 309 | 310 | run_subtraction_test(5, 5, 0) 311 | 312 | run_subtraction_test(5, 6, 0) 313 | 314 | run_subtraction_test(100, 6, 94) 315 | 316 | run_subtraction_test(0, 0, 0) 317 | 318 | run_subtraction_test(0, 5, 0) 319 | 320 | 321 | def test_to_unary_conversion(self): 322 | """Test converting a decimal number to unary representation""" 323 | 324 | initial = { 325 | "stack": ["int0000000011"] 326 | } 327 | expected = { 328 | "stack": ["AAA"] # 3 in unary 329 | } 330 | 331 | result = execute_instruction(initial, to_unary()) 332 | self.assertStateEqual(result, expected) 333 | 334 | def test_from_unary_conversion(self): 335 | """Test converting from unary back to decimal""" 336 | 337 | initial = { 338 | "stack": ["AAAAA"] # 5 in unary 339 | } 340 | expected = { 341 | "stack": ["int0000000101"] 342 | } 343 | 344 | result = execute_instruction(initial, from_unary()) 345 | self.assertStateEqual(result, expected) 346 | 347 | 348 | def test_from_unary_conversion_zero(self): 349 | """Test converting from unary back to decimal""" 350 | 351 | initial = { 352 | "stack": [""] # 0 in unary 353 | } 354 | expected = { 355 | "stack": ["int0000000000"] 356 | } 357 | 358 | result = execute_instruction(initial, from_unary()) 359 | self.assertStateEqual(result, expected) 360 | 361 | def test_to_unary_conversion_zero(self): 362 | """Test converting from unary back to decimal""" 363 | 364 | initial = { 365 | "stack": ["int0000000000"] 366 | } 367 | expected = { 368 | "stack": [""] 369 | } 370 | 371 | result = execute_instruction(initial, to_unary()) 372 | self.assertStateEqual(result, expected) 373 | 374 | def test_add_unary(self): 375 | """Test adding two unary numbers""" 376 | 377 | initial = { 378 | "stack": ["AAA", "AA"] # 3 and 2 in unary 379 | } 380 | expected = { 381 | "stack": ["AAAAA"] # 5 in unary 382 | } 383 | 384 | result = execute_instruction(initial, add_unary()) 385 | self.assertStateEqual(result, expected) 386 | 387 | def test_mod2_unary(self): 388 | """Test adding two unary numbers""" 389 | 390 | for i in range(10): 391 | initial = { 392 | "stack": ["A"*i] 393 | } 394 | expected = { 395 | "stack": [str((i%2) == 0)] 396 | } 397 | 398 | result = execute_instruction(initial, mod2_unary()) 399 | self.assertStateEqual(result, expected) 400 | 401 | def run_comparison_test(self, operation, num1: str, num2: str, expected: str): 402 | """Helper to run a single comparison test case 403 | 404 | Args: 405 | operation: Function that returns the instruction list for the comparison 406 | num1: First number to compare (will be second on stack) 407 | num2: Second number to compare (will be first on stack) 408 | expected: Expected result ("True" or "False") 409 | """ 410 | state = {"stack": [], 411 | "foo": "bar"} 412 | 413 | # Push second number (will be popped first) and convert to unary 414 | state = execute_instruction(state, push(f"int{num2:010b}")) 415 | state = execute_instruction(state, to_unary()) 416 | 417 | # Push first number and convert to unary 418 | state = execute_instruction(state, push(f"int{num1:010b}")) 419 | state = execute_instruction(state, to_unary()) 420 | 421 | # Compare and check result 422 | final_state = execute_instruction(state, operation()) 423 | 424 | expected_state = {"stack": [expected], 425 | "foo": "bar"} 426 | self.assertStateEqual(final_state, expected_state) 427 | 428 | def test_greater_than(self): 429 | """Test comparing two unary numbers with greater than operation""" 430 | test_cases = [ 431 | (5, 3, "True"), # Basic greater than 432 | (3, 5, "False"), # Basic less than 433 | (5, 5, "False"), # Equal values 434 | (10, 1, "True"), # Large difference 435 | (0, 5, "False"), # Zero case 436 | (100, 99, "True"), # Large numbers, small difference 437 | ] 438 | 439 | for num1, num2, expected in test_cases: 440 | with self.subTest(f"{num1} > {num2}"): 441 | self.run_comparison_test(greater_than, num1, num2, expected) 442 | 443 | def test_less_than(self): 444 | """Test comparing two unary numbers with less than operation""" 445 | test_cases = [ 446 | (3, 5, "True"), # Basic less than 447 | (5, 3, "False"), # Basic greater than 448 | (5, 5, "False"), # Equal values 449 | (1, 10, "True"), # Large difference 450 | (5, 0, "False"), # Zero case 451 | (99, 100, "True"), # Large numbers, small difference 452 | ] 453 | 454 | for num1, num2, expected in test_cases: 455 | with self.subTest(f"{num1} < {num2}"): 456 | self.run_comparison_test(less_than, num1, num2, expected) 457 | 458 | def test_greater_equal_than(self): 459 | """Test comparing two unary numbers with greater than or equal operation""" 460 | test_cases = [ 461 | (5, 3, "True"), # Basic greater than 462 | (3, 5, "False"), # Basic less than 463 | (5, 5, "True"), # Equal values 464 | (10, 1, "True"), # Large difference 465 | (0, 5, "False"), # Zero case 466 | (5, 0, "True"), # Compare with zero 467 | (100, 99, "True"), # Large numbers, small difference 468 | (99, 99, "True"), # Large equal numbers 469 | ] 470 | 471 | for num1, num2, expected in test_cases: 472 | with self.subTest(f"{num1} >= {num2}"): 473 | self.run_comparison_test(greater_equal_than, num1, num2, expected) 474 | 475 | def test_less_equal_than(self): 476 | """Test comparing two unary numbers with less than or equal operation""" 477 | test_cases = [ 478 | (3, 5, "True"), # Basic less than 479 | (5, 3, "False"), # Basic greater than 480 | (5, 5, "True"), # Equal values 481 | (1, 10, "True"), # Large difference 482 | (5, 0, "False"), # Compare with zero 483 | (0, 0, "True"), # Both zero 484 | (99, 100, "True"), # Large numbers, small difference 485 | (99, 99, "True"), # Large equal numbers 486 | ] 487 | 488 | for num1, num2, expected in test_cases: 489 | with self.subTest(f"{num1} <= {num2}"): 490 | self.run_comparison_test(less_equal_than, num1, num2, expected) 491 | 492 | def test_boolean_not(self): 493 | """Test negating a boolean value""" 494 | 495 | initial = { 496 | "stack": ["True"] 497 | } 498 | expected = { 499 | "stack": ["False"] 500 | } 501 | 502 | result = execute_instruction(initial, boolean_not()) 503 | self.assertStateEqual(result, expected) 504 | 505 | def test_indirect_lookup(self): 506 | """Test indirect lookup which gets a variable name from the stack and looks up its value. 507 | 508 | Example: 509 | If state has: 510 | pointer = "target" 511 | And stack has: 512 | ["pointer"] 513 | 514 | Then indirect lookup should: 515 | 1. Pop "pointer" from stack 516 | 2. Push the value of pointer ("target") onto stack 517 | """ 518 | 519 | # Test basic indirect lookup 520 | initial_state = { 521 | "stack": ["pointer"], 522 | "pointer": "target" 523 | } 524 | expected_state = { 525 | "stack": ["target"], 526 | "pointer": "target" 527 | } 528 | result = execute_instruction(initial_state, indirect_lookup()) 529 | self.assertStateEqual(result, expected_state) 530 | 531 | def test_indirect_assign(self): 532 | """Test indirect assign which gets both variable name and value from the stack. 533 | 534 | Example: 535 | If stack has: 536 | ["new_value", "target"] 537 | 538 | Then indirect assign should: 539 | 1. Pop "target" (variable name) from stack 540 | 2. Pop "new_value" (value to assign) from stack 541 | 3. Set variable "target" to have value "new_value" 542 | """ 543 | 544 | # Test basic indirect assign 545 | initial_state = { 546 | "stack": ["new_value", "target"], 547 | "target": "old_value" 548 | } 549 | expected_state = { 550 | "stack": [], 551 | "target": "new_value" 552 | } 553 | result = execute_instruction(initial_state, indirect_assign()) 554 | self.assertStateEqual(result, expected_state) 555 | 556 | # Test creating new variable 557 | initial_state = { 558 | "stack": ["first_value", "new_var"] 559 | } 560 | expected_state = { 561 | "stack": [], 562 | "new_var": "first_value" 563 | } 564 | result = execute_instruction(initial_state, indirect_assign()) 565 | self.assertStateEqual(result, expected_state) 566 | 567 | def test_intxy_to_location(self): 568 | """Test conversion of integer coordinates to chess square notation. 569 | Tests all 64 squares from a1 to h8 programmatically. 570 | 571 | Files (1-8) map to a-h 572 | Ranks stay as numbers 1-8 573 | """ 574 | # Generate and test all squares 575 | for file in range(1, 9): # 1-8 for a-h 576 | for rank in range(1, 9): # 1-8 for ranks 577 | expected_square = chr(ord('a') + file - 1) + str(rank) 578 | 579 | initial_state = { 580 | "stack": [], 581 | "x": f"int{file-1:010b}", 582 | "y": f"int{rank-1:010b}" 583 | } 584 | 585 | expected_state = { 586 | "stack": [expected_square], 587 | "x": f"int{file-1:010b}", 588 | "y": f"int{rank-1:010b}" 589 | } 590 | 591 | result = execute_instruction(initial_state, intxy_to_location("x", "y")) 592 | self.assertStateEqual(result, expected_state) 593 | 594 | def test_list_pop(self): 595 | """Test popping items from a semicolon-delimited list""" 596 | 597 | # Test case 1: Multiple items in list 598 | initial = { 599 | "stack": [], 600 | "source_list": "first;second;third", 601 | "dest_var": "old_value" 602 | } 603 | expected = { 604 | "stack": [], 605 | "source_list": "second;third", 606 | "dest_var": "first" 607 | } 608 | 609 | result = execute_instruction(initial, list_pop("source_list", "dest_var")) 610 | self.assertStateEqual(result, expected) 611 | 612 | # Test case 2: Single item in list (no semicolons) 613 | initial = { 614 | "stack": [], 615 | "source_list": "only_item;", 616 | "dest_var": "old_value" 617 | } 618 | expected = { 619 | "stack": [], 620 | "source_list": "", 621 | "dest_var": "only_item" 622 | } 623 | 624 | result = execute_instruction(initial, list_pop("source_list", "dest_var")) 625 | self.assertStateEqual(result, expected) 626 | 627 | # Test case 3: Destination variable doesn't exist yet 628 | initial = { 629 | "stack": ["baz"], 630 | "source_list": "first;second;" 631 | } 632 | expected = { 633 | "stack": ["baz"], 634 | "source_list": "second;", 635 | "dest_var": "first" 636 | } 637 | 638 | result = execute_instruction(initial, list_pop("source_list", "dest_var")) 639 | self.assertStateEqual(result, expected) 640 | 641 | def test_boolean_and(self): 642 | """Test AND operation for all combinations, preserving stack and variables""" 643 | test_cases = [ 644 | (True, True, True), # a AND b = result 645 | (True, False, False), 646 | (False, True, False), 647 | (False, False, False) 648 | ] 649 | 650 | for a, b, expected_result in test_cases: 651 | with self.subTest(f"{a} AND {b} = {expected_result}"): 652 | # Setup initial state with extra stack items and variables 653 | initial = { 654 | "stack": [str(b), str(a), "preserve_me"], 655 | "test_var": "original_value", 656 | "other_var": "unchanged" 657 | } 658 | 659 | # Expected state should preserve other stack items and variables 660 | expected = { 661 | "stack": [str(expected_result), "preserve_me"], 662 | "test_var": "original_value", 663 | "other_var": "unchanged" 664 | } 665 | 666 | result = execute_instruction(initial, boolean_and()) 667 | self.assertStateEqual(result, expected) 668 | 669 | def test_boolean_or(self): 670 | """Test OR operation for all combinations, preserving stack and variables""" 671 | test_cases = [ 672 | (True, True, True), # a OR b = result 673 | (True, False, True), 674 | (False, True, True), 675 | (False, False, False) 676 | ] 677 | 678 | for a, b, expected_result in test_cases: 679 | with self.subTest(f"{a} OR {b} = {expected_result}"): 680 | # Setup initial state with extra stack items and variables 681 | initial = { 682 | "stack": [str(b), str(a), "keep_this_value"], 683 | "some_var": "test_value", 684 | "another_var": "should_not_change" 685 | } 686 | 687 | # Expected state should preserve other stack items and variables 688 | expected = { 689 | "stack": [str(expected_result), "keep_this_value", ], 690 | "some_var": "test_value", 691 | "another_var": "should_not_change" 692 | } 693 | 694 | result = execute_instruction(initial, boolean_or()) 695 | self.assertStateEqual(result, expected) 696 | 697 | def test_variable_uniq(self): 698 | """Test removing duplicates from a semicolon-delimited list while preserving order. 699 | Lists always end with a semicolon followed by newline.""" 700 | 701 | test_cases = [ 702 | # Basic case with duplicates 703 | { 704 | "initial": { 705 | "stack": [], 706 | "test_list": "a;b;b;b;c;c;d;" 707 | }, 708 | "expected": { 709 | "stack": [], 710 | "test_list": "a;b;c;d;" 711 | } 712 | }, 713 | # No duplicates case 714 | { 715 | "initial": { 716 | "stack": [], 717 | "test_list": "a;b;c;d;" 718 | }, 719 | "expected": { 720 | "stack": [], 721 | "test_list": "a;b;c;d;" 722 | } 723 | }, 724 | # Multiple consecutive duplicates 725 | { 726 | "initial": { 727 | "stack": [], 728 | "test_list": "x;x;x;x;y;y;y;z;" 729 | }, 730 | "expected": { 731 | "stack": [], 732 | "test_list": "x;y;z;" 733 | } 734 | } 735 | ] 736 | 737 | for i, test_case in enumerate(test_cases): 738 | with self.subTest(f"Case {i}"): 739 | result = execute_instruction( 740 | test_case["initial"], 741 | variable_uniq("test_list") 742 | ) 743 | self.assertStateEqual(result, test_case["expected"]) 744 | 745 | def test_delete_var(self): 746 | """Test deleting a variable from the state""" 747 | 748 | test_cases = [ 749 | # Basic case - delete middle variable 750 | { 751 | "initial": { 752 | "stack": ["value1"], 753 | "keep1": "test1", 754 | "delete_me": "remove", 755 | "keep2": "test2" 756 | }, 757 | "expected": { 758 | "stack": ["value1"], 759 | "keep1": "test1", 760 | "keep2": "test2" 761 | }, 762 | "var_to_delete": "delete_me" 763 | }, 764 | # Delete last variable 765 | { 766 | "initial": { 767 | "stack": [], 768 | "a": "keep", 769 | "b": "also_keep", 770 | "last": "remove" 771 | }, 772 | "expected": { 773 | "stack": [], 774 | "a": "keep", 775 | "b": "also_keep" 776 | }, 777 | "var_to_delete": "last" 778 | }, 779 | # Delete first variable (not stack) 780 | { 781 | "initial": { 782 | "stack": ["preserve"], 783 | "first": "remove", 784 | "middle": "keep", 785 | "end": "keep" 786 | }, 787 | "expected": { 788 | "stack": ["preserve"], 789 | "middle": "keep", 790 | "end": "keep" 791 | }, 792 | "var_to_delete": "first" 793 | }, 794 | # Try to delete non-existent variable 795 | { 796 | "initial": { 797 | "stack": [], 798 | "existing": "keep" 799 | }, 800 | "expected": { 801 | "stack": [], 802 | "existing": "keep" 803 | }, 804 | "var_to_delete": "not_here" 805 | } 806 | ] 807 | 808 | for i, test_case in enumerate(test_cases): 809 | with self.subTest(f"Case {i}"): 810 | result = execute_instruction( 811 | test_case["initial"], 812 | delete_var(test_case["var_to_delete"]) 813 | ) 814 | self.assertStateEqual(result, test_case["expected"]) 815 | 816 | def test_isany(self): 817 | """Test matching stack top against multiple options""" 818 | 819 | # Test case 1: Match first option 820 | initial = { 821 | "stack": ["a"], 822 | "other_var": "preserve" 823 | } 824 | expected = { 825 | "stack": ["True"], 826 | "other_var": "preserve" 827 | } 828 | result = execute_instruction(initial, isany(["a", "b", "c"])) 829 | self.assertStateEqual(result, expected) 830 | 831 | # Test case 2: Match middle option 832 | initial = { 833 | "stack": ["b"], 834 | "other_var": "preserve" 835 | } 836 | expected = { 837 | "stack": ["True"], 838 | "other_var": "preserve" 839 | } 840 | result = execute_instruction(initial, isany(["a", "b", "c"])) 841 | self.assertStateEqual(result, expected) 842 | 843 | # Test case 3: Match last option 844 | initial = { 845 | "stack": ["c"], 846 | "other_var": "preserve" 847 | } 848 | expected = { 849 | "stack": ["True"], 850 | "other_var": "preserve" 851 | } 852 | result = execute_instruction(initial, isany(["a", "b", "c"])) 853 | self.assertStateEqual(result, expected) 854 | 855 | # Test case 4: No match 856 | initial = { 857 | "stack": ["d"], 858 | "other_var": "preserve" 859 | } 860 | expected = { 861 | "stack": ["False"], 862 | "other_var": "preserve" 863 | } 864 | result = execute_instruction(initial, isany(["a", "b", "c"])) 865 | self.assertStateEqual(result, expected) 866 | 867 | # Test case 6: Single option 868 | initial = { 869 | "stack": ["x"], 870 | "other_var": "preserve" 871 | } 872 | expected = { 873 | "stack": ["True"], 874 | "other_var": "preserve" 875 | } 876 | result = execute_instruction(initial, isany(["x"])) 877 | self.assertStateEqual(result, expected) 878 | 879 | # Test case 7: Match with special characters 880 | initial = { 881 | "stack": ["test.123"], 882 | "other_var": "preserve" 883 | } 884 | expected = { 885 | "stack": ["True"], 886 | "other_var": "preserve" 887 | } 888 | result = execute_instruction(initial, isany(["test.123", "other"])) 889 | self.assertStateEqual(result, expected) 890 | 891 | # Test case 8: Preserve rest of stack 892 | initial = { 893 | "stack": ["a", "keep1", "keep2"], 894 | "other_var": "preserve" 895 | } 896 | expected = { 897 | "stack": ["True", "keep1", "keep2"], 898 | "other_var": "preserve" 899 | } 900 | result = execute_instruction(initial, isany(["a", "b", "c"])) 901 | self.assertStateEqual(result, expected) 902 | 903 | def test_piece_value(self): 904 | """Test piece value calculations for various board positions with +100 offsets.""" 905 | 906 | def run_test(fen: str, expected_value: int): 907 | """Helper to run a single piece value test.""" 908 | state = {"stack": [fen]} 909 | 910 | # Execute piece_value operation 911 | state = execute_instruction(state, piece_value()) 912 | 913 | # Convert result back from unary 914 | final_state = execute_instruction(state, from_unary()) 915 | 916 | # Expected value should be formatted as int with 3 digits 917 | expected_state = {"stack": [f"int{expected_value:010b}"]} 918 | self.assertStateEqual(final_state, expected_state) 919 | 920 | test_cases = [ 921 | # 1) Empty board 922 | ("8/8/8/8/8/8/8/8", 100), 923 | 924 | # 2) Starting position 925 | ("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR", 100), 926 | 927 | # 3) Just kings 928 | ("4k3/8/8/8/8/8/8/4K3", 100), 929 | 930 | # 4) Single white pawn 931 | ("4k3/8/8/8/8/8/P7/4K3", 101), 932 | 933 | # 5) Single black pawn 934 | ("4k3/p7/8/8/8/8/8/4K3", 99), 935 | 936 | # 6) White queen vs black rook (fixed!) 937 | ("4kr2/8/8/8/3Q4/8/8/4K3", 104), 938 | 939 | # 7) Complex position with multiple pieces (fixed!) 940 | ("4k3/8/2p1p3/3Q4/4n3/2P5/5P2/4K3", 106), 941 | 942 | # 8) All white major pieces, no black (fixed!) 943 | ("k7/8/8/8/8/8/8/RNBQKBR1", 128), 944 | 945 | # 9) All black major pieces, no white (fixed!) 946 | ("rbqkbnr1/8/8/8/8/8/8/7K", 72), 947 | 948 | # 10) Equal material (Q vs Q+R) (fixed example) 949 | ("2kqr3/8/8/3Q4/8/8/8/K3R3", 100), 950 | 951 | # 11) Max white material without pawns (example fix) 952 | ("8/8/8/8/8/8/R7/RQQQQQQQ", 173), 953 | 954 | # 12) Max black material without pawns (example fix) 955 | ("rqqqqqqq/r7/8/8/8/8/8/8", 27), 956 | 957 | # 13) All white pawns 958 | ("7k/8/8/8/8/8/PPPPPPPP/K7", 108), 959 | 960 | # 14) All black pawns 961 | ("7k/pppppppp/8/8/8/8/8/7K", 92), 962 | 963 | # 15) Mixed pawn ending 964 | ("8/ppp5/8/PPP4k/8/8/8/7K", 100), 965 | 966 | # 16) Queen vs 3 minor pieces 967 | ("4k3/8/2nbn3/8/3Q4/8/8/4K3", 100), 968 | 969 | # 17) Rooks and knights only 970 | ("rn3nr/8/8/8/8/8/8/RN3NR", 100), 971 | 972 | # 18) Complex middlegame 973 | ("r1bqk2r/ppp2ppp/2n5/3np3/2B5/5N2/PPP2PPP/RNBQ1RK1", 102), 974 | 975 | # 19) Knight vs bishop 976 | ("4k3/8/8/3n4/4B3/8/8/4K3", 100), 977 | 978 | # 20) Multiple queens 979 | ("4k3/8/8/3QQ3/8/8/8/4K3", 118) 980 | ] 981 | 982 | # Run all test cases 983 | for i, (fen, expected) in enumerate(test_cases): 984 | with self.subTest(f"Case {i+1}: {fen}"): 985 | run_test(fen, (expected-100)*2+200) 986 | 987 | 988 | def test_is_stack_empty(self): 989 | """Test checking if stack is empty""" 990 | 991 | # Case 1: Truly empty stack with variables after 992 | initial = { 993 | "stack": [], 994 | "var1": "test", 995 | "var2": "value" 996 | } 997 | expected = { 998 | "stack": ["True"], 999 | "var1": "test", 1000 | "var2": "value" 1001 | } 1002 | result = execute_instruction(initial, is_stack_empty()) 1003 | self.assertStateEqual(result, expected) 1004 | 1005 | # Case 2: Empty stack with no variables 1006 | initial = { 1007 | "stack": [] 1008 | } 1009 | expected = { 1010 | "stack": ["True"] 1011 | } 1012 | result = execute_instruction(initial, is_stack_empty()) 1013 | self.assertStateEqual(result, expected) 1014 | 1015 | # Case 3: Single item on stack 1016 | initial = { 1017 | "stack": ["item1"], 1018 | "var": "test" 1019 | } 1020 | expected = { 1021 | "stack": ["False", "item1"], 1022 | "var": "test" 1023 | } 1024 | result = execute_instruction(initial, is_stack_empty()) 1025 | self.assertStateEqual(result, expected) 1026 | 1027 | # Case 4: Multiple items on stack 1028 | initial = { 1029 | "stack": ["top", "middle", "bottom"], 1030 | "var": "test" 1031 | } 1032 | expected = { 1033 | "stack": ["False", "top", "middle", "bottom"], 1034 | "var": "test" 1035 | } 1036 | result = execute_instruction(initial, is_stack_empty()) 1037 | self.assertStateEqual(result, expected) 1038 | 1039 | def test_castle_rights(self): 1040 | """Test castling rights are removed if pieces aren't on correct squares""" 1041 | 1042 | # Test white kingside castling - only allowed if K on e1 and R on h1 1043 | initial = { 1044 | "stack": ["8/8/8/8/8/8/8/4K2R w KQkq -"], 1045 | } 1046 | expected = { 1047 | "stack": ["8/8/8/8/8/8/8/4K2R w K -"], 1048 | } 1049 | result = execute_instruction(initial, expand_chess() + contract_chess()) 1050 | self.assertStateEqual(result, expected) 1051 | 1052 | # Test both white castling rights - King on e1 with both rooks 1053 | initial = { 1054 | "stack": ["8/8/8/8/8/8/8/R3K2R w KQkq -"], 1055 | } 1056 | expected = { 1057 | "stack": ["8/8/8/8/8/8/8/R3K2R w KQ -"], 1058 | } 1059 | result = execute_instruction(initial, expand_chess() + contract_chess()) 1060 | self.assertStateEqual(result, expected) 1061 | 1062 | # Test both black castling rights - King on e8 with both rooks 1063 | initial = { 1064 | "stack": ["r3k2r/8/8/8/8/8/8/8 w KQkq -"], 1065 | } 1066 | expected = { 1067 | "stack": ["r3k2r/8/8/8/8/8/8/8 w kq -"], 1068 | } 1069 | result = execute_instruction(initial, expand_chess() + contract_chess()) 1070 | self.assertStateEqual(result, expected) 1071 | 1072 | # Test all castling rights - Both kings with their rooks 1073 | initial = { 1074 | "stack": ["r3k2r/8/8/8/8/8/8/R3K2R w KQkq -"], 1075 | } 1076 | expected = { 1077 | "stack": ["r3k2r/8/8/8/8/8/8/R3K2R w KQkq -"], 1078 | } 1079 | result = execute_instruction(initial, expand_chess() + contract_chess()) 1080 | self.assertStateEqual(result, expected) 1081 | 1082 | # Test white queenside castling - only allowed if K on e1 and R on a1 1083 | initial = { 1084 | "stack": ["8/8/8/8/8/8/8/R3K3 w KQkq -"], 1085 | } 1086 | expected = { 1087 | "stack": ["8/8/8/8/8/8/8/R3K3 w Q -"], 1088 | } 1089 | result = execute_instruction(initial, expand_chess() + contract_chess()) 1090 | self.assertStateEqual(result, expected) 1091 | 1092 | # Test black kingside castling - only allowed if k on e8 and r on h8 1093 | initial = { 1094 | "stack": ["4k2r/8/8/8/8/8/8/8 w KQkq -"], 1095 | } 1096 | expected = { 1097 | "stack": ["4k2r/8/8/8/8/8/8/8 w k -"], 1098 | } 1099 | result = execute_instruction(initial, expand_chess() + contract_chess()) 1100 | self.assertStateEqual(result, expected) 1101 | 1102 | # Test black queenside castling - only allowed if k on e8 and r on a8 1103 | initial = { 1104 | "stack": ["r3k3/8/8/8/8/8/8/8 w KQkq -"], 1105 | } 1106 | expected = { 1107 | "stack": ["r3k3/8/8/8/8/8/8/8 w q -"], 1108 | } 1109 | result = execute_instruction(initial, expand_chess() + contract_chess()) 1110 | self.assertStateEqual(result, expected) 1111 | 1112 | def test_rook_attacks(self): 1113 | """Test rook attack detection for all squares with rook on h1""" 1114 | 1115 | # Initial position: Rook on h1 1116 | initial_state = """%% 1117 | #stack: 1118 | #attacked: False 1119 | #initial_board: 8/8/8/8/8/8/8/1P5R 1120 | """ 1121 | 1122 | # Expected attacked squares are all h-file and first rank squares 1123 | attacked_squares = set() 1124 | # Add all h-file squares 1125 | for rank in '12345678': 1126 | attacked_squares.add('h' + rank) 1127 | # Add all first rank squares 1128 | for file in 'abcdefgh': 1129 | attacked_squares.add(file + '1') 1130 | attacked_squares.remove('h1') 1131 | attacked_squares.remove('a1') 1132 | 1133 | # Test each square 1134 | for file in 'abcdefgh': 1135 | for rank in '12345678': 1136 | square = file + rank 1137 | 1138 | # Generate and apply the regex transformations 1139 | tree = trace(lambda x: is_square_under_attack_by_rook(x, square, color_white)) 1140 | linear = linearize_tree(tree) 1141 | args = create(linear) 1142 | 1143 | # Apply transformations 1144 | state = initial_state 1145 | for op, regexs in args: 1146 | for pattern, repl in regexs: 1147 | state = re.sub(pattern, repl, state) 1148 | 1149 | # Extract final state 1150 | cpu_state = CPUState.from_string(state) 1151 | 1152 | # Check if square is correctly identified as attacked or not 1153 | expected = str(square in attacked_squares) 1154 | actual = cpu_state.variables["attacked"] 1155 | 1156 | assert actual == expected, \ 1157 | f'Failed for square {square}: expected {expected}, got {actual}' 1158 | 1159 | 1160 | def test_bishop_attacks(self): 1161 | """Test bishop attack detection for all squares with bishop on e4""" 1162 | 1163 | # Initial position: Bishop on e4 1164 | initial_state = """%% 1165 | #stack: 1166 | #attacked: False 1167 | #initial_board: 8/8/8/8/4B3/8/6P1/8 1168 | """ 1169 | 1170 | 1171 | attacked_squares = set([ 1172 | # Northeast diagonal 1173 | 'f5', 'g6', 'h7', 1174 | # Southeast diagonal 1175 | 'f3', 'g2', 1176 | # Southwest diagonal 1177 | 'd3', 'c2', 'b1', 1178 | # Northwest diagonal 1179 | 'd5', 'c6', 'b7', 'a8' 1180 | ]) 1181 | 1182 | # Test each square 1183 | for file in 'abcdefgh': 1184 | for rank in '12345678': 1185 | square = file + rank 1186 | 1187 | # Generate and apply the regex transformations 1188 | tree = trace(lambda x: is_square_under_attack_by_bishop(x, square, color_white)) 1189 | linear = linearize_tree(tree) 1190 | args = create(linear) 1191 | 1192 | # Apply transformations 1193 | state = initial_state 1194 | for op, regexs in args: 1195 | for pattern, repl in regexs: 1196 | state = re.sub(pattern, repl, state) 1197 | 1198 | # Extract final state 1199 | cpu_state = CPUState.from_string(state) 1200 | 1201 | # Check if square is correctly identified as attacked or not 1202 | expected = str(square in attacked_squares) 1203 | actual = cpu_state.variables["attacked"] 1204 | 1205 | assert actual == expected, \ 1206 | f'Failed for square {square}: expected {expected}, got {actual}' 1207 | 1208 | 1209 | def test_pawn_attacks(self): 1210 | """Test pawn attack detection for all squares with pawn on e4""" 1211 | 1212 | # Initial position: White pawn on e4 1213 | initial_state = """%% 1214 | #stack: 1215 | #attacked: False 1216 | #initial_board: 8/8/8/8/4P3/8/8/8 1217 | """ 1218 | 1219 | # Expected attacked squares - just the two diagonal squares in front 1220 | attacked_squares = set([ 1221 | 'd5', # Up-left diagonal 1222 | 'f5' # Up-right diagonal 1223 | ]) 1224 | 1225 | # Test each square 1226 | for file in 'abcdefgh': 1227 | for rank in '12345678': 1228 | square = file + rank 1229 | 1230 | # Generate and apply the regex transformations 1231 | tree = trace(lambda x: is_square_under_attack_by_pawn(x, square, color_white)) 1232 | linear = linearize_tree(tree) 1233 | args = create(linear) 1234 | 1235 | # Apply transformations 1236 | state = initial_state 1237 | for op, regexs in args: 1238 | for pattern, repl in regexs: 1239 | state = re.sub(pattern, repl, state) 1240 | 1241 | # Extract final state 1242 | cpu_state = CPUState.from_string(state) 1243 | 1244 | # Check if square is correctly identified as attacked or not 1245 | expected = str(square in attacked_squares) 1246 | actual = cpu_state.variables["attacked"] 1247 | 1248 | assert actual == expected, \ 1249 | f'Failed for square {square}: expected {expected}, got {actual}' 1250 | 1251 | 1252 | # Let's also test black pawn attacks 1253 | black_initial_state = """%% 1254 | #stack: 1255 | #attacked: False 1256 | #initial_board: 8/8/8/8/4p3/8/8/8 1257 | """ 1258 | 1259 | # Black pawn on e4 attacks d3 and f3 1260 | black_attacked_squares = set([ 1261 | 'd3', # Down-left diagonal 1262 | 'f3' # Down-right diagonal 1263 | ]) 1264 | 1265 | # Test each square for black pawn 1266 | for file in 'abcdefgh': 1267 | for rank in '12345678': 1268 | square = file + rank 1269 | 1270 | # Generate and apply the regex transformations 1271 | tree = trace(lambda x: is_square_under_attack_by_pawn(x, square, color_black)) 1272 | linear = linearize_tree(tree) 1273 | args = create(linear) 1274 | 1275 | # Apply transformations 1276 | state = black_initial_state 1277 | for op, regexs in args: 1278 | for pattern, repl in regexs: 1279 | state = re.sub(pattern, repl, state) 1280 | 1281 | # Extract final state 1282 | cpu_state = CPUState.from_string(state) 1283 | 1284 | # Check if square is correctly identified as attacked or not 1285 | expected = str(square in black_attacked_squares) 1286 | actual = cpu_state.variables["attacked"] 1287 | 1288 | assert actual == expected, \ 1289 | f'Failed for square {square}: expected {expected}, got {actual}' 1290 | 1291 | print("All black pawn squares tested successfully!") 1292 | 1293 | 1294 | 1295 | def test_keep_only_min_thread(self): 1296 | """Test keeping only the thread with the minimum number of 'A's.""" 1297 | 1298 | 1299 | import random 1300 | random.seed(42) # For reproducibility 1301 | 1302 | def generate_test_case(): 1303 | # Generate between 2 and 5 threads 1304 | num_threads = random.randint(2, 5) 1305 | 1306 | # Generate lengths for each thread (0-20 As) 1307 | lengths = [random.randint(1, 30) for _ in range(num_threads)] 1308 | min_length = min(lengths) 1309 | 1310 | # Create threads 1311 | threads = [] 1312 | for length in lengths: 1313 | # Sometimes add other characters 1314 | thread = 'A' * length 1315 | threads.append('#stack:\n' + thread) 1316 | 1317 | # Create input string 1318 | input_str = '%%\n' + '\n%%\n'.join(threads) + '\n' 1319 | 1320 | # Find the first thread with minimum number of As 1321 | min_thread = min(threads, key=lambda x: x.count('A')) 1322 | expected = f'%%\n{min_thread}\n' 1323 | 1324 | return input_str, expected 1325 | 1326 | test_cases = [generate_test_case() for _ in range(100)] 1327 | 1328 | for input_str, expected in test_cases: 1329 | # Generate and apply the regex transformations 1330 | args = keep_only_max_thread() + keep_only_first_thread() 1331 | 1332 | # Apply transformations 1333 | result = input_str 1334 | for pattern, repl in args: 1335 | result = re.sub(pattern, repl, result) 1336 | 1337 | self.assertEqual(result, expected, 1338 | f"Failed for input:\n{input_str}\nExpected:\n{expected}\nGot:\n{result}") 1339 | 1340 | 1341 | import chess 1342 | import re 1343 | 1344 | 1345 | def calculate_moves(board_state, piece_function, color_function): 1346 | """ 1347 | Calculate legal moves for a given piece on a board state. 1348 | 1349 | Args: 1350 | board_state (str): FEN-like string representing the board state 1351 | piece_function: Function that calculates moves for a specific piece 1352 | color_function: Color function (color_white or color_black) 1353 | 1354 | Returns: 1355 | str: Resulting board state after calculating all legal moves 1356 | """ 1357 | # Initialize the state with the board 1358 | state = f'%%\n#stack:\n#initial_board: {board_state}\n' 1359 | 1360 | # Generate and linearize the move tree 1361 | tree = trace(lambda x: piece_function(x, color_function)) 1362 | linear = linearize_tree(tree) 1363 | args = create(linear) 1364 | 1365 | # Apply each operation to transform the state 1366 | for op, regexs in args: 1367 | for pattern, repl in regexs: 1368 | state = re.sub(pattern, repl, state) 1369 | # Safety check for malformed states 1370 | if '#' in state.replace("\n#", ""): 1371 | raise ValueError("Invalid state generated during move calculation") 1372 | 1373 | return state 1374 | 1375 | def extract_legal_moves(state_str): 1376 | """Extract the legal moves from the state string, ignoring color.""" 1377 | for line in state_str.split('\n'): 1378 | if line.startswith('#legal'): 1379 | # Split on semicolons and filter out empty strings, removing color 1380 | moves = [move.strip().split()[0] for move in line.split(':')[1].split(';') 1381 | if move.strip()] 1382 | return moves 1383 | return [] 1384 | 1385 | def fen_to_board_state(fen): 1386 | """Convert full FEN to just the position part.""" 1387 | return fen.split()[0] 1388 | 1389 | 1390 | class ChessBoardTests(unittest.TestCase): 1391 | def _verify_moves(self, pos, piece_function, color_function, piece_type): 1392 | """Helper to verify moves match between our implementation and python-chess.""" 1393 | result = calculate_moves(pos, piece_function, color_function) 1394 | our_moves = set(extract_legal_moves(result)) 1395 | if color_function == color_black: 1396 | pos = pos.replace('w', 'b') 1397 | 1398 | board = chess.Board(pos + " 0 1") 1399 | python_chess_moves = set() 1400 | for move in board.generate_pseudo_legal_moves(): 1401 | if board.piece_type_at(move.from_square) == getattr(chess, piece_type): 1402 | board_copy = board.copy() 1403 | board_copy.push(move) 1404 | python_chess_moves.add(board_copy.board_fen()) 1405 | 1406 | # Remove original position from our moves 1407 | original_pos = pos.split()[0] 1408 | our_moves_without_original = our_moves - {original_pos} 1409 | 1410 | self.assertEqual( 1411 | our_moves_without_original, 1412 | python_chess_moves, 1413 | f"\nPosition: {pos}\nOur moves: {our_moves_without_original}\nPython-chess moves: {python_chess_moves}" 1414 | ) 1415 | 1416 | def test_knight_moves(self): 1417 | test_positions = [ 1418 | # Knight in corner 1419 | "8/8/8/8/8/8/8/7N w - -", 1420 | # Knight in center 1421 | "8/8/8/3N4/8/8/8/8 w - -", 1422 | # Knight with friendly pieces blocking 1423 | "8/8/8/3N4/2P1P3/3P4/8/8 w - -", 1424 | # Knight with enemy pieces to capture 1425 | "8/8/8/3N4/2p1p3/3p4/8/8 w - -", 1426 | # Multiple knights 1427 | "8/8/2N5/8/3N4/8/8/8 w - -" 1428 | ] 1429 | for pos in test_positions: 1430 | with self.subTest(position=pos): 1431 | self._verify_moves(pos, knight_moves, color_white, 'KNIGHT') 1432 | 1433 | def test_king_moves(self): 1434 | test_positions = [ 1435 | # King in corner 1436 | "8/8/8/8/8/8/8/7K w - -", 1437 | # King in center 1438 | "8/8/8/3K4/8/8/8/8 w - -", 1439 | # King with friendly pieces blocking 1440 | "8/8/8/2PPP3/2PKP3/2PPP3/8/8 w - -", 1441 | # King with enemy pieces to capture 1442 | "8/8/8/2ppp3/2pKp3/2ppp3/8/8 w - -", 1443 | # King near enemy king (showing blocked squares) 1444 | "8/8/8/3k4/3K4/8/8/8 w - -" 1445 | ] 1446 | for pos in test_positions: 1447 | with self.subTest(position=pos): 1448 | self._verify_moves(pos, king_moves, color_white, 'KING') 1449 | 1450 | def test_queen_moves(self): 1451 | test_positions = [ 1452 | # Queen in corner 1453 | "8/8/8/8/8/8/8/7Q w - -", 1454 | # Queen in center 1455 | "8/8/8/3Q4/8/8/8/8 w - -", 1456 | # Queen with friendly pieces blocking 1457 | "8/8/8/2PPP3/2PQP3/2PPP3/8/8 w - -", 1458 | # Queen with enemy pieces to capture 1459 | "8/8/8/2ppp3/2pQp3/2ppp3/8/8 w - -", 1460 | # Queen with mixed blocking and captures 1461 | "8/8/1p6/3Q4/5P2/8/8/8 w - -" 1462 | ] 1463 | for pos in test_positions: 1464 | with self.subTest(position=pos): 1465 | self._verify_moves(pos, queen_moves, color_white, 'QUEEN') 1466 | 1467 | def test_rook_moves(self): 1468 | test_positions = [ 1469 | # Rook in corner 1470 | "8/8/8/8/8/8/8/7R w - -", 1471 | # Rook in center 1472 | "8/8/8/3R4/8/8/8/8 w - -", 1473 | # Rook with friendly pieces blocking 1474 | "8/8/8/2P1P3/3R4/2P1P3/8/8 w - -", 1475 | # Rook with enemy pieces to capture 1476 | "8/8/8/2p1p3/3R4/2p1p3/8/8 w - -", 1477 | # Multiple rooks 1478 | "8/8/2R5/8/3R4/8/8/8 w - -" 1479 | ] 1480 | for pos in test_positions: 1481 | with self.subTest(position=pos): 1482 | self._verify_moves(pos, rook_moves, color_white, 'ROOK') 1483 | 1484 | def test_bishop_moves(self): 1485 | test_positions = [ 1486 | # Bishop in corner 1487 | "8/8/8/8/8/8/8/7B w - -", 1488 | # Bishop in center 1489 | "8/8/8/3B4/8/8/8/8 w - -", 1490 | # Bishop with friendly pieces blocking 1491 | "8/8/2P3P1/8/3B4/8/2P3P1/8 w - -", 1492 | # Bishop with enemy pieces to capture 1493 | "8/8/2p3p1/8/3B4/8/2p3p1/8 w - -", 1494 | # Multiple bishops 1495 | "8/8/2B5/8/3B4/8/8/8 w - -" 1496 | ] 1497 | for pos in test_positions: 1498 | with self.subTest(position=pos): 1499 | self._verify_moves(pos, bishop_moves, color_white, 'BISHOP') 1500 | 1501 | def test_pawn_moves(self): 1502 | test_positions = [ 1503 | # Pawn on starting square 1504 | "8/8/8/8/8/8/4P3/8 w - -", 1505 | # Pawn in middle of board 1506 | "8/8/8/8/4P3/8/8/8 w - -", 1507 | # Pawn with captures available 1508 | "8/8/8/3p1p2/4P3/8/8/8 w - -", 1509 | # Pawn blocked by friendly piece 1510 | "8/8/8/4P3/4P3/8/8/8 w - -", 1511 | # Pawn blocked by opponent piece 1512 | "8/8/8/4p3/4P3/8/8/8 w - -", 1513 | # Multiple pawns 1514 | "8/8/8/8/8/8/2P1P3/8 w - -", 1515 | "rnbqkbnr/1ppppppp/8/p7/P7/8/1PPPPPPP/RNBQKBNR w KQkq -" 1516 | ] 1517 | for pos in test_positions: 1518 | with self.subTest(position=pos): 1519 | self._verify_moves(pos, pawn_moves, color_white, 'PAWN') 1520 | self._verify_moves(pos, pawn_moves, color_black, 'PAWN') 1521 | 1522 | 1523 | def test_pawn_moves_en_passant(self): 1524 | """ 1525 | Tests specifically for en passant captures. Each FEN sets up an en passant 1526 | target square so that White can capture en passant immediately. 1527 | """ 1528 | ep_positions = [ 1529 | # 1) Simple EP on adjacent files (d5 vs. e5) 1530 | "8/8/8/3Pp3/8/8/8/8 w - e6", 1531 | 1532 | # 2) EP capture from the g-file (g5 vs. f5) 1533 | "8/8/8/5pP1/8/8/8/8 w - f6", 1534 | 1535 | # 3) EP capture from the c-file (c5 vs. b5) 1536 | "8/8/8/1pP5/8/8/8/8 w - b6", 1537 | 1538 | # 4) EP with multiple White pawns on the board 1539 | "8/2p5/8/3Pp3/8/8/2P1P3/8 w - e6", 1540 | 1541 | "8/8/8/PPp5/8/8/7P/8 w K c6" 1542 | ] 1543 | 1544 | for pos in ep_positions: 1545 | with self.subTest(position=pos): 1546 | self._verify_moves(pos, pawn_moves, color_white, 'PAWN') 1547 | 1548 | def test_combined_attacks(self): 1549 | """Test attack detection for complex positions against python-chess""" 1550 | 1551 | test_positions = [ 1552 | # Original test position 1553 | "7Q/8/3N3R/6N1/K3Pn2/2q1B1k1/3R4/8", 1554 | 1555 | # Simple positions 1556 | "8/8/8/8/4Q3/8/8/8", # Single queen in center 1557 | "8/8/8/3B4/4N3/8/8/8", # Bishop and knight 1558 | 1559 | # Complex positions 1560 | "r1bqkb1r/pppp1ppp/2n2n2/4p3/4P3/2N2N2/PPPP1PPP/R1BQK2R", # Development position 1561 | "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R", # Complex middlegame 1562 | "8/3K4/2p5/p2b2r1/5k2/8/8/1q6", # Endgame position 1563 | "2r3k1/p4p2/3Rp2p/1p2P1pK/8/1P4P1/P4P2/8", # Rook endgame 1564 | "6k1/5ppp/8/8/8/8/1B6/K7", # Simple bishop vs king 1565 | "4k3/8/3Q4/8/4P3/8/8/4K3", # Queen and pawn 1566 | "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR" # Starting position 1567 | ] 1568 | 1569 | for fen in test_positions: 1570 | # Create initial state with the FEN position 1571 | initial_state = f"""%% 1572 | #stack: 1573 | #initial_board: {fen} 1574 | """ 1575 | 1576 | # Get attacked squares from python-chess 1577 | board = chess.Board(f"{fen} w - - 0 1") 1578 | expected_squares = set() 1579 | for square in chess.SQUARES: 1580 | if board.is_attacked_by(chess.WHITE, square): 1581 | square_name = chess.square_name(square) 1582 | expected_squares.add(square_name) 1583 | 1584 | # Test each square 1585 | for file in 'abcdefgh': 1586 | for rank in '12345678': 1587 | square = file + rank 1588 | 1589 | # Generate and apply the regex transformations 1590 | tree = trace(lambda x: is_square_under_attack(x, square, color_white)) 1591 | linear = linearize_tree(tree) 1592 | args = create(linear) 1593 | 1594 | # Apply transformations 1595 | state = initial_state 1596 | for op, regexs in args: 1597 | for pattern, repl in regexs: 1598 | state = re.sub(pattern, repl, state) 1599 | 1600 | # Extract final state 1601 | cpu_state = CPUState.from_string(state) 1602 | 1603 | # Check if square is correctly identified as attacked or not 1604 | expected = str(square in expected_squares) 1605 | actual = cpu_state.variables["attacked"] 1606 | 1607 | assert actual == expected, \ 1608 | f'Failed for FEN {fen}, square {square}: expected {expected}, got {actual}' 1609 | 1610 | 1611 | def run_tests(): 1612 | unittest.main(argv=[''], verbosity=2, exit=False) 1613 | 1614 | if __name__ == '__main__': 1615 | run_tests() 1616 | 1617 | --------------------------------------------------------------------------------