diff --git a/.gitignore b/.gitignore index 9e8b7fa..8725a78 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ venv .idea/ .venv .DS_Store +*.priv +*.priv.* diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..3e4ecb8 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,50 @@ +## About +This document describes development-related topics of pgn-tactics-generator. +pgn-tactics-generator is a python application dedicated to creating chess puzzles/tactics from a pgn file. +See [readme](./README.md) file for more details. + + +# Dependencies + +This script requires the [Requests](https://docs.python-requests.org/) and [python-chess](https://python-chess.readthedocs.io/) +libraries to run, as well as a copy of *Stockfish*. +Is recommended that you use Python 3 and pip3. +It should work with Python 2.7 and pip (probably you will need to install futures `pip install futures` ) + +To install the requirements use something, like: +`pip3 install -r requirements.txt --user` + +It's recommended that the dependencies are isolated in a [virtual environment](https://docs.python.org/3/tutorial/venv.html) + +# Testing +The project comes with a simple set of basic tests. + +## Running the tests +The test can be run with: +```bash +python -m unittest +``` + +The recommended way to run the test is using `pytest`: +```bash +pytest +``` + +ATTOW, the tests cover: + * investigate() function (running investigate with know arguments and expected result) + * puzzle.is_complete + * .ambiguous() and .move_list() methods of position_list class + +The idea is that after introducing some changes to the app, you are able to check +if puzzle generation logic stayed untouched (produces the same results as the previous version). + +## Test data +The tests are using [example data](./test/data) obtained with [positions_for_investigation.py](./positions_for_investigation.py). + +The script works in similar fashion as `main.py`: It reads PGN file, goes through +the games, finds "interesting" positions and generates tactics puzzles. +Some intermediate data (for example the puzzle definitions) are recorded and +writen to json files. + +You don't need to re-run the script (as the project already includes required data), unless you want to +modify/extend the tests. \ No newline at end of file diff --git a/README.md b/README.md index 81456a5..d0e6441 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ It's based on the great [https://github.com/clarkerubber/Python-Puzzle-Creator] Things that I changed: - Use a local pgn file with games as a source. - Write results to a file called tactics.pgn -- Default engine depth to 8 so it's faster. Before it was nodes=3500000 this is a depth around 20. So it took several minutes to analyze a game. With depth 8 it takes seconds. +- Default engine depth to 8, so it's faster. Before it was nodes=3500000 this is a depth around 20. So it took several minutes to analyze a game. With depth 8 it takes seconds. - You can use the `depth` argument to change the depth if you want more precision. -- chess.pop_count to chess.popcount because it was failing +- chess.pop_count to chess.popcount, because it was failing ### This is too complex, give something easy. There is another option if you don't want to install and manage python scripts @@ -24,6 +24,8 @@ It uses a different approach to create tactics, so probably it will generate a d This script requires the *Requests* and *Python-Chess* libraries to run, as well as a copy of *Stockfish* Is recommended that you use Python 3 and pip3. But it could work with Python 2.7 and pip (probably you will need to install futures `pip install futures` ) +Please, take a look at [development doc](DEVELOPMENT.md) for details. + ### Install requirements `pip3 install -r requirements.txt --user` @@ -39,7 +41,7 @@ You can download games from a specific user using this command: `python3 download_games.py ` -By default it will download the last 60 games from blitz, rapid and classical. +By default, it will download the last 60 games from blitz, rapid and classical. **Arguments** @@ -73,14 +75,14 @@ To execute the generator execute this command. By default it will look for the ` **Arguments** - `--quiet` to reduce the screen output. -- `--depth=8` select the stockfish depth analysis. Default is `8` and will take some seconds to analyze a game, with `--depth=18` will take around 6 minutes. +- `--depth=8` select the Stockfish depth analysis. Default is `8` and will take some seconds to analyze a game, with `--depth=18` will take around 6 minutes. - `--games=ruy_lopez.pgn` to select a specific pgn file. Default is `games.pgn` - `--strict=False` Use `False` to generate more tactics but a little more ambiguous. Default is `True` - `--threads=4` Stockfish argument, number of engine threads, default `4` - `--memory=2048` Stockfish argument, memory in MB to use for engine hashtables, default `2048` - `--includeBlunder=False` If False then generated puzzles won't include initial blunder move, default is `True` - `--stockfish=./stockfish-x86_64-bmi2` Path to Stockfish binary. - Optional. If ommited, the program will try to locate Stockfish in current directory or download it from the net + Optional. If omitted, the program will try to locate Stockfish in current directory or download it from the net Example: `python3 main.py --quiet --depth=12 --games=ruy_lopez.pgn --strict=True --threads=2 --memory=1024` @@ -93,7 +95,7 @@ The `result header` is the tactic result and not the game result. It can be load ## Problems? #### Stockfish errors -- If you have problems building stockfish try downloading stockfish directly https://stockfishchess.org/download/ +- If you have problems building Stockfish try downloading Stockfish directly https://stockfishchess.org/download/ ## Want to see all my chess related projects? Check [My projects](http://vitomd.com/blog/projects/) for a full detailed list. diff --git a/main.py b/main.py index 930d059..012b363 100755 --- a/main.py +++ b/main.py @@ -4,10 +4,9 @@ import argparse import logging -import sys +import chess.engine import chess.pgn -import chess.uci from modules.api.api import post_puzzle from modules.bcolors.bcolors import bcolors @@ -48,11 +47,8 @@ def prepare_settings(): stockfish_command = get_stockfish_command(settings.stockfish) logging.debug(f'Using {stockfish_command} to run Stockfish.') -engine = chess.uci.popen_engine(stockfish_command) -engine.setoption({'Threads': settings.threads, 'Hash': settings.memory}) -engine.uci() -info_handler = chess.uci.InfoHandler() -engine.info_handlers.append(info_handler) +engine = chess.engine.SimpleEngine.popen_uci(stockfish_command) +engine.configure({'Threads': settings.threads, 'Hash': settings.memory}) all_games = open(settings.games, "r") tactics_file = open("tactics.pgn", "w") @@ -67,27 +63,25 @@ def prepare_settings(): logging.debug(bcolors.WARNING + "Game ID: " + str(game_id) + bcolors.ENDC) logging.debug(bcolors.WARNING + "Game headers: " + str(game) + bcolors.ENDC) - prev_score = chess.uci.Score(None, None) + prev_score = chess.engine.Cp(0) puzzles = [] logging.debug(bcolors.OKGREEN + "Game Length: " + str(game.end().board().fullmove_number)) logging.debug("Analysing Game..." + bcolors.ENDC) - engine.ucinewgame() - while not node.is_end(): next_node = node.variation(0) - engine.position(next_node.board()) - engine.go(depth=settings.depth) - cur_score = info_handler.info["score"][1] + info = engine.analyse(next_node.board(), chess.engine.Limit(depth=settings.depth)) + + cur_score = info["score"].relative logging.debug(bcolors.OKGREEN + node.board().san(next_node.move) + bcolors.ENDC) - logging.debug(bcolors.OKBLUE + " CP: " + str(cur_score.cp)) - logging.debug(" Mate: " + str(cur_score.mate) + bcolors.ENDC) + logging.debug(bcolors.OKBLUE + " CP: " + str(cur_score.score())) + logging.debug(" Mate: " + str(cur_score.mate()) + bcolors.ENDC) + if investigate(prev_score, cur_score, node.board()): logging.debug(bcolors.WARNING + " Investigate!" + bcolors.ENDC) - puzzles.append( - puzzle(node.board(), next_node.move, str(game_id), engine, info_handler, game, settings.strict)) + puzzles.append(puzzle(node.board(), next_node.move, str(game_id), engine, info, game, settings.strict)) prev_score = cur_score node = next_node @@ -101,3 +95,5 @@ def prepare_settings(): tactics_file.write("\n\n") tactics_file.close() + +engine.quit() diff --git a/modules/investigate/investigate.py b/modules/investigate/investigate.py index 078c74e..9b37765 100644 --- a/modules/investigate/investigate.py +++ b/modules/investigate/investigate.py @@ -1,4 +1,6 @@ import chess +from chess import Board +from chess.engine import Score def sign(a): @@ -14,20 +16,25 @@ def material_count(board): return chess.popcount(board.occupied) -def investigate(a, b, board): - # determine if the difference between position A and B - # is worth investigating for a puzzle. - if a.cp is not None and b.cp is not None: - if (((-110 < a.cp < 850 and 200 < b.cp < 850) - or (-850 < a.cp < 110 and -200 > b.cp > -850)) +def investigate(a: Score, b: Score, board: Board): + """ + determine if the difference between position A and B + is worth investigating for a puzzle. + """ + a_cp, a_mate = a.score(), a.mate() + b_cp, b_mate = b.score(), b.mate() + + if a_cp is not None and b_cp is not None: + if (((-110 < a_cp < 850 and 200 < b_cp < 850) + or (-850 < a_cp < 110 and -200 > b_cp > -850)) and material_value(board) > 3 and material_count(board) > 6): return True - elif a.cp is not None and b.mate is not None and material_value(board) > 3: - if (a.cp < 110 and sign(b.mate) == -1) or (a.cp > -110 and sign(b.mate) == 1): + elif a_cp is not None and b_mate is not None and material_value(board) > 3: + if (a_cp < 110 and sign(b_mate) == -1) or (a_cp > -110 and sign(b_mate) == 1): + # from an even position, walking int a checkmate return True - elif (a.mate is not None - and b.mate is not None): - if sign(a.mate) == sign(b.mate): # actually means that they're opposite + elif a_mate is not None and b_mate is not None: + if sign(a_mate) == sign(b_mate): # actually means that they're opposite return True return False diff --git a/modules/puzzle/analysed.py b/modules/puzzle/analysed.py index 4d615f4..a64f040 100644 --- a/modules/puzzle/analysed.py +++ b/modules/puzzle/analysed.py @@ -7,9 +7,9 @@ def sign(self, val): return -1 if val <= 0 else 1 def sort_val(self): - if self.evaluation.cp is not None: - return self.evaluation.cp - elif self.evaluation.mate is not None: - return self.sign(self.evaluation.mate) * (abs(100 + self.evaluation.mate)) * 10000 + if self.evaluation.score() is not None: + return self.evaluation.score() + elif self.evaluation.is_mate(): + return self.sign(self.evaluation.mate()) * (abs(100 + self.evaluation.mate())) * 10000 else: return 0 diff --git a/modules/puzzle/position_list.py b/modules/puzzle/position_list.py index ad3582b..df6074f 100644 --- a/modules/puzzle/position_list.py +++ b/modules/puzzle/position_list.py @@ -2,7 +2,7 @@ from operator import methodcaller import chess -import chess.uci +import chess.engine from modules.bcolors.bcolors import bcolors from modules.puzzle.analysed import analysed @@ -23,11 +23,11 @@ def __init__(self, position, engine, info_handler, player_turn=True, best_move=N def move_list(self): if self.next_position is None or self.next_position.ambiguous() or self.next_position.position.is_game_over(): if self.best_move is not None: - return [self.best_move.bestmove.uci()] + return [self.best_move.move.uci()] else: return [] else: - return [self.best_move.bestmove.uci()] + self.next_position.move_list() + return [self.best_move.move.uci()] + self.next_position.move_list() def category(self): if self.next_position is None: @@ -58,19 +58,20 @@ def generate(self, depth): def evaluate_best(self, depth): logging.debug(bcolors.OKGREEN + "Evaluating Best Move...") - self.engine.position(self.position) - self.best_move = self.engine.go(depth=depth) - if self.best_move.bestmove is not None: - self.evaluation = self.info_handler.info["score"][1] + + self.best_move = self.engine.play(self.position, chess.engine.Limit(depth=depth), info=chess.engine.INFO_ALL) + + if self.best_move.move is not None: + self.evaluation = self.best_move.info['score'].relative self.next_position = position_list(self.position.copy(), self.engine, self.info_handler, not self.player_turn, strict=self.strict) - self.next_position.position.push(self.best_move.bestmove) - logging.debug("Best Move: " + self.best_move.bestmove.uci() + bcolors.ENDC) - logging.debug(bcolors.OKBLUE + " CP: " + str(self.evaluation.cp)) - logging.debug(" Mate: " + str(self.evaluation.mate) + bcolors.ENDC) + self.next_position.position.push(self.best_move.move) + logging.debug("Best Move: " + self.best_move.move.uci() + bcolors.ENDC) + logging.debug(bcolors.OKBLUE + " CP: " + str(self.evaluation.score())) + logging.debug(" Mate: " + str(self.evaluation.mate()) + bcolors.ENDC) return True else: logging.debug(bcolors.FAIL + "No best move!" + bcolors.ENDC) @@ -81,14 +82,15 @@ def evaluate_legals(self, depth): for i in self.position.legal_moves: position_copy = self.position.copy() position_copy.push(i) - self.engine.position(position_copy) - self.engine.go(depth=depth) - self.analysed_legals.append(analysed(i, self.info_handler.info["score"][1])) + + info = self.engine.analyse(position_copy, chess.engine.Limit(depth=depth)) + self.analysed_legals.append(analysed(i, info["score"].relative)) + self.analysed_legals = sorted(self.analysed_legals, key=methodcaller('sort_val')) for i in self.analysed_legals[:3]: logging.debug(bcolors.OKGREEN + "Move: " + str(i.move.uci()) + bcolors.ENDC) - logging.debug(bcolors.OKBLUE + " CP: " + str(i.evaluation.cp)) - logging.debug(" Mate: " + str(i.evaluation.mate)) + logging.debug(bcolors.OKBLUE + " CP: " + str(i.evaluation.score())) + logging.debug(" Mate: " + str(i.evaluation.mate())) logging.debug("... and " + str(max(0, len(self.analysed_legals) - 3)) + " more moves" + bcolors.ENDC) def material_difference(self): @@ -109,7 +111,7 @@ def is_complete(self, category, color, first_node, first_val): if (self.material_difference() > 0.2 and abs(self.material_difference() - first_val) > 0.1 and first_val < 2 - and self.evaluation.mate is None + and self.evaluation.mate() is None and self.material_count() > 6): return True else: @@ -118,7 +120,7 @@ def is_complete(self, category, color, first_node, first_val): if (self.material_difference() < -0.2 and abs(self.material_difference() - first_val) > 0.1 and first_val > -2 - and self.evaluation.mate is None + and self.evaluation.mate() is None and self.material_count() > 6): return True else: @@ -133,19 +135,18 @@ def ambiguous(self): # If strict == False then it will generate more tactics but more ambiguous move_number = 1 if self.strict else 2 if len(self.analysed_legals) > 1: - if (self.analysed_legals[0].evaluation.cp is not None - and self.analysed_legals[1].evaluation.cp is not None): - if (self.analysed_legals[0].evaluation.cp > -210 - or self.analysed_legals[move_number].evaluation.cp < -90): + if (self.analysed_legals[0].evaluation.score() is not None + and self.analysed_legals[1].evaluation.score() is not None): + if (self.analysed_legals[0].evaluation.score() > -210 + or self.analysed_legals[move_number].evaluation.score() < -90): return True - if (self.analysed_legals[0].evaluation.mate is not None - and self.analysed_legals[1].evaluation.mate is not None): - if (self.analysed_legals[0].evaluation.mate < 1 - and self.analysed_legals[1].evaluation.mate < 1): + if (self.analysed_legals[0].evaluation.mate() is not None + and self.analysed_legals[1].evaluation.mate() is not None): + if (self.analysed_legals[0].evaluation.mate() < 1 + and self.analysed_legals[1].evaluation.mate() < 1): return True - if (self.analysed_legals[0].evaluation.mate is not None - and self.analysed_legals[1].evaluation.cp is not None): - if self.analysed_legals[1].evaluation.cp < -200: + if self.analysed_legals[0].evaluation.is_mate() and not self.analysed_legals[1].evaluation.is_mate(): + if self.analysed_legals[1].evaluation.score() < -200: return True return False diff --git a/modules/utils/decoding.py b/modules/utils/decoding.py new file mode 100644 index 0000000..a92b816 --- /dev/null +++ b/modules/utils/decoding.py @@ -0,0 +1,65 @@ +from typing import Optional + +import chess +from chess import Move, Board +from chess.engine import Cp, Mate, BestMove, Score + +from modules.puzzle.analysed import analysed +from modules.puzzle.position_list import position_list +from modules.puzzle.puzzle import puzzle + + +def score_from_dict(d) -> Optional[Score]: + if not d: + return None + if d[0] is not None: + return Cp(d[0]) + else: + return Mate(d[1]) + + +def board_from_dict(d: dict) -> Board: + board = chess.Board(d['fen']) + return board + + +def move_from_str(s: str) -> Move: + return Move.from_uci(s) if s else None + + +def bestmove_from_dict(d: dict) -> BestMove: + return BestMove(move_from_str(d['move']), move_from_str(d['ponder'])) if d else None + + +def analyzed_from_dict(d: dict) -> analysed: + return analysed(move_from_str(d['move']), score_from_dict(d['evaluation'])) + + +def positionlist_from_dict(d: dict) -> position_list: + result = position_list(position=board_from_dict(d['position']), + engine=None, + info_handler=None, + player_turn=d['player_turn'], + best_move=bestmove_from_dict(d['best_move']), + evaluation=score_from_dict(d['evaluation']), + strict=d['strict']) + + result.next_position = positionlist_from_dict(d['next_position']) if d['next_position'] else None + result.analysed_legals = [analyzed_from_dict(al) for al in d['analysed_legals']] + + return result + + +def puzzle_from_dict(d: dict) -> puzzle: + result = puzzle( + last_pos=board_from_dict(d['last_pos']), + last_move=move_from_str(d['last_move']), + game_id=d['last_move'], + engine=None, + info_handler=None, + game=None, + strict=True + ) + + result.positions = positionlist_from_dict(d['positions']) + return result diff --git a/modules/utils/encoding.py b/modules/utils/encoding.py new file mode 100644 index 0000000..1d6c5bd --- /dev/null +++ b/modules/utils/encoding.py @@ -0,0 +1,75 @@ +from typing import Optional + +from chess import Move, Board +from chess.engine import BestMove, Score + +from modules.puzzle import position_list, puzzle + + +def move_to_dict(m: Move) -> Optional[str]: + return m.uci() if m else None + + +def bestmove_to_dict(bm: BestMove) -> dict: + return { + 'move': move_to_dict(bm.move), + 'ponder': move_to_dict(bm.ponder) + } if bm else None + + +def board_to_dict(b: Board, position_only: bool = False) -> Optional[dict]: + if not b: + return None + else: + result = { + 'fen': b.fen() + } + if not position_only: + result.update({ + 'aliases': b.aliases, + 'fullmove_number': b.fullmove_number, + 'move_stack': [m.uci() for m in b.move_stack], + 'uci_variant': b.uci_variant, + 'piece_map': piecemap_to_dict(b.piece_map()) + }) + return result + + +def piecemap_to_dict(pm) -> dict: + return {key: val.symbol() for key, val in pm.items()} + + +def score_to_dict(score: Score): + return [score.score(), score.mate()] if score else None + + +def positionlist_to_dict(pl: position_list) -> dict: + return { + 'position': board_to_dict(pl.position), + 'player_turn': pl.player_turn, + 'best_move': bestmove_to_dict(pl.best_move), + 'evaluation': score_to_dict(pl.evaluation), + 'next_position': positionlist_to_dict(pl.next_position) if pl.next_position else None, + 'analysed_legals': [analyzed_to_dict(al) for al in pl.analysed_legals], + 'strict': pl.strict, + 'is_ambiguous': pl.ambiguous() + } if pl else None + + +def analyzed_to_dict(a): + return { + 'move': move_to_dict(a.move), + 'evaluation': score_to_dict(a.evaluation) + } + + +def puzzle_to_dict(p: puzzle): + return { + 'game_id': p.game_id, + 'category': p.positions.category(), + 'last_pos': board_to_dict(p.last_pos), + 'last_move': move_to_dict(p.last_move), + 'move_list': p.positions.move_list(), + 'positions': positionlist_to_dict(p.positions) + # , 'game': p.game + } if p else None diff --git a/modules/utils/helpers.py b/modules/utils/helpers.py index 2c38e04..a8f13c8 100644 --- a/modules/utils/helpers.py +++ b/modules/utils/helpers.py @@ -23,9 +23,7 @@ def get_stockfish_command(path: str): def configure_logging(loglevel): logging.basicConfig(format="%(message)s", level=loglevel, stream=sys.stdout) logging.getLogger("requests.packages.urllib3").setLevel(logging.WARNING) - logging.getLogger("chess.uci").setLevel(logging.WARNING) logging.getLogger("chess.engine").setLevel(logging.WARNING) - logging.getLogger("chess._engine").setLevel(logging.WARNING) def prepare_terminal(): diff --git a/positions_for_investigation.py b/positions_for_investigation.py new file mode 100644 index 0000000..fa8e3e2 --- /dev/null +++ b/positions_for_investigation.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python + +""" +This utility allows for generating some data that can be used in tests. +The data include: + * arguments and result for investigate() function + * puzzle definitions and result of .is_complete() and .positions.ambiguous() methods for that puzzles +""" +import argparse +import json +import logging + +import chess.engine +import chess.pgn + +from modules.bcolors.bcolors import bcolors +from modules.utils.encoding import puzzle_to_dict, board_to_dict, score_to_dict +from modules.investigate.investigate import investigate +from modules.puzzle.puzzle import puzzle +from modules.utils.helpers import str2bool, get_stockfish_command, configure_logging, prepare_terminal + + +def prepare_settings(): + parser = argparse.ArgumentParser(description=__doc__) + + parser.add_argument("--threads", metavar="THREADS", nargs="?", type=int, default=4, + help="number of engine threads") + parser.add_argument("--memory", metavar="MEMORY", nargs="?", type=int, default=2048, + help="memory in MB to use for engine hashtables") + parser.add_argument("--depth", metavar="DEPTH", nargs="?", type=int, default=8, + help="depth for stockfish analysis") + parser.add_argument("--quiet", dest="loglevel", + default=logging.DEBUG, action="store_const", const=logging.INFO, + help="substantially reduce the number of logged messages") + parser.add_argument("--games", metavar="GAMES", default="games.pgn", + help="A specific pgn with games") + parser.add_argument("--strict", metavar="STRICT", default=True, + help="If False then it will be generate more tactics but maybe a little ambiguous") + parser.add_argument("--includeBlunder", metavar="INCLUDE_BLUNDER", default=True, + type=str2bool, const=True, dest="include_blunder", nargs="?", + help="If False then generated puzzles won't include initial blunder move") + parser.add_argument("--stockfish", metavar="STOCKFISH", default=None, help="Path to Stockfish binary") + + return parser.parse_args() + + +settings = prepare_settings() + +prepare_terminal() + +configure_logging(settings.loglevel) + +stockfish_command = get_stockfish_command(settings.stockfish) +logging.debug(f'Using {stockfish_command} to run Stockfish.') +engine = chess.engine.SimpleEngine.popen_uci(stockfish_command) +engine.configure({'Threads': settings.threads, 'Hash': settings.memory}) + +all_games = open(settings.games, "r") + + +def write_test_data(filename: str, payload: str): + with open(filename, "w") as f: + f.write(payload) + + +# data collection for testing investigate function +investigate_test_data = [] +complete_test_data = [] + +game_id = 0 +while True: + game = chess.pgn.read_game(all_games) + if game is None: + break + node = game + + game_id = game_id + 1 + logging.debug(bcolors.WARNING + "Game ID: " + str(game_id) + bcolors.ENDC) + logging.debug(bcolors.WARNING + "Game headers: " + str(game) + bcolors.ENDC) + + prev_score = chess.engine.Cp(0) + puzzles = [] + + logging.debug("Analysing Game..." + bcolors.ENDC) + + # find candidates (positions) + while not node.is_end(): + next_node = node.variation(0) + info = engine.analyse(next_node.board(), chess.engine.Limit(depth=settings.depth)) + + cur_score = info["score"].relative + logging.debug(bcolors.OKGREEN + node.board().san(next_node.move) + bcolors.ENDC) + logging.debug(bcolors.OKBLUE + " CP: " + str(cur_score.score())) + logging.debug(" Mate: " + str(cur_score.mate()) + bcolors.ENDC) + + result = investigate(prev_score, cur_score, node.board()) + + if result: + logging.debug(bcolors.WARNING + " Investigate!" + bcolors.ENDC) + puzzles.append( + puzzle(node.board(), next_node.move, str(game_id), engine, None, game, settings.strict)) + + # save the data for investigate() testing + investigate_test_data.append( + {'score_a': score_to_dict(prev_score), 'score_b': score_to_dict(cur_score), + 'board': board_to_dict(node.board(), True), 'result': result}) + + prev_score = cur_score + node = next_node + + # check puzzle completeness + for i in puzzles: + logging.info(bcolors.WARNING + "Generating new puzzle..." + bcolors.ENDC) + i.generate(settings.depth) + + is_complete = i.is_complete() + is_ambiguous = i.positions.ambiguous() + logging.info(f'{i.last_pos.fen()} -- {is_complete}, {is_ambiguous}') + if is_complete: + complete_test_data.append({'puzzle': puzzle_to_dict(i), 'is_complete': is_complete}) + +# dump the test data to files +write_test_data('investigate.json', json.dumps(investigate_test_data, indent=2)) +write_test_data('is_complete.json', json.dumps(complete_test_data, indent=2)) + +engine.quit() diff --git a/requirements.txt b/requirements.txt index 9d38342..280c1c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -colorama==0.3.7 -python-chess==0.24.2 +colorama==0.4.4 +chess==1.5.0 requests==2.20.0 diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/data/investigate.json b/test/data/investigate.json new file mode 100644 index 0000000..f0230b4 --- /dev/null +++ b/test/data/investigate.json @@ -0,0 +1,170 @@ +[ + { + "score_a": [ + 0, + null + ], + "score_b": [ + 265, + null + ], + "board": { + "fen": "2kr2r1/2p2p1p/pp4q1/3P1p2/8/5QP1/P4P1P/1RR3K1 b - - 0 24" + }, + "result": true + }, + { + "score_a": [ + 180, + null + ], + "score_b": [ + 378, + null + ], + "board": { + "fen": "r3k2r/3p1ppp/pqb1pn2/1p6/1P1bP3/P1NB4/2PB1PPP/R2Q1RK1 b kq - 2 12" + }, + "result": true + }, + { + "score_a": [ + -60, + null + ], + "score_b": [ + 212, + null + ], + "board": { + "fen": "r3k2r/3p1ppp/pqb1p3/1p6/1P1bB3/P1N5/2PB1PPP/R2Q1RK1 b kq - 0 13" + }, + "result": true + }, + { + "score_a": [ + 91, + null + ], + "score_b": [ + 273, + null + ], + "board": { + "fen": "r3r1k1/p1pq1ppp/2pp4/4b3/N3P3/6P1/PPP2P1P/1R1QR1K1 b - - 4 15" + }, + "result": true + }, + { + "score_a": [ + -108, + null + ], + "score_b": [ + 308, + null + ], + "board": { + "fen": "r5k1/p1p2ppp/2pp4/4b3/4P3/1P3QPq/P1P2P1P/1R2R1K1 b - - 2 19" + }, + "result": true + }, + { + "score_a": [ + 180, + null + ], + "score_b": [ + 711, + null + ], + "board": { + "fen": "4r1k1/p1p2ppp/1b1p4/2p5/4P3/1P3QPq/2P1RP1P/R5K1 w - - 2 24" + }, + "result": true + }, + { + "score_a": [ + 711, + null + ], + "score_b": [ + 203, + null + ], + "board": { + "fen": "4r1k1/p1p2ppp/1b1p4/2p2Q2/4P3/1P4Pq/2P1RP1P/R5K1 b - - 3 24" + }, + "result": true + }, + { + "score_a": [ + 172, + null + ], + "score_b": [ + 642, + null + ], + "board": { + "fen": "4rk2/p1pQ1pp1/1b3q2/2p1pP1p/2P5/1P2R1P1/7P/3R2K1 w - - 0 32" + }, + "result": true + }, + { + "score_a": [ + -66, + null + ], + "score_b": [ + 723, + null + ], + "board": { + "fen": "2kr3r/1pp2p1p/p6q/3P1p2/8/5Q2/P4PPP/1RR3K1 b - - 1 21" + }, + "result": true + }, + { + "score_a": [ + -26, + null + ], + "score_b": [ + 203, + null + ], + "board": { + "fen": "2kr2r1/1pp2p1p/p6q/3P1p2/8/1Q6/P4PPP/1RR3K1 b - - 3 22" + }, + "result": true + }, + { + "score_a": [ + 64, + null + ], + "score_b": [ + 274, + null + ], + "board": { + "fen": "2kr2r1/2p2p1p/pp4q1/3P1p2/8/5QP1/P4P1P/1RR3K1 b - - 0 24" + }, + "result": true + }, + { + "score_a": [ + 68, + null + ], + "score_b": [ + 627, + null + ], + "board": { + "fen": "2kr2r1/2p2p2/pp4q1/3P1p1p/7P/5QP1/P4P2/1RR3K1 b - - 0 25" + }, + "result": true + } +] \ No newline at end of file diff --git a/test/data/is_complete.json b/test/data/is_complete.json new file mode 100644 index 0000000..39431b1 --- /dev/null +++ b/test/data/is_complete.json @@ -0,0 +1,2522 @@ +[ + { + "puzzle": { + "game_id": "2", + "category": "Material", + "last_pos": { + "fen": "r3r1k1/p1pq1ppp/2pp4/4b3/N3P3/6P1/PPP2P1P/1R1QR1K1 b - - 4 15", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 15, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "60": "r", + "56": "r", + "55": "p", + "54": "p", + "53": "p", + "51": "q", + "50": "p", + "48": "p", + "43": "p", + "42": "p", + "36": "b", + "28": "P", + "24": "N", + "22": "P", + "15": "P", + "13": "P", + "10": "P", + "9": "P", + "8": "P", + "6": "K", + "4": "R", + "3": "Q", + "1": "R" + } + }, + "last_move": "e8e6", + "move_list": [ + "a4c5", + "d7e7", + "c5e6", + "e7e6" + ], + "positions": { + "position": { + "fen": "r5k1/p1pq1ppp/2ppr3/4b3/N3P3/6P1/PPP2P1P/1R1QR1K1 w - - 5 16", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 16, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "56": "r", + "55": "p", + "54": "p", + "53": "p", + "51": "q", + "50": "p", + "48": "p", + "44": "r", + "43": "p", + "42": "p", + "36": "b", + "28": "P", + "24": "N", + "22": "P", + "15": "P", + "13": "P", + "10": "P", + "9": "P", + "8": "P", + "6": "K", + "4": "R", + "3": "Q", + "1": "R" + } + }, + "player_turn": true, + "best_move": { + "move": "a4c5", + "ponder": "d7e7" + }, + "evaluation": [ + 382, + null + ], + "next_position": { + "position": { + "fen": "r5k1/p1pq1ppp/2ppr3/2N1b3/4P3/6P1/PPP2P1P/1R1QR1K1 b - - 6 16", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 16, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6", + "a4c5" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "56": "r", + "55": "p", + "54": "p", + "53": "p", + "51": "q", + "50": "p", + "48": "p", + "44": "r", + "43": "p", + "42": "p", + "36": "b", + "34": "N", + "28": "P", + "22": "P", + "15": "P", + "13": "P", + "10": "P", + "9": "P", + "8": "P", + "6": "K", + "4": "R", + "3": "Q", + "1": "R" + } + }, + "player_turn": false, + "best_move": { + "move": "d7e7", + "ponder": "c5e6" + }, + "evaluation": [ + -375, + null + ], + "next_position": { + "position": { + "fen": "r5k1/p1p1qppp/2ppr3/2N1b3/4P3/6P1/PPP2P1P/1R1QR1K1 w - - 7 17", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 17, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6", + "a4c5", + "d7e7" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "56": "r", + "55": "p", + "54": "p", + "53": "p", + "52": "q", + "50": "p", + "48": "p", + "44": "r", + "43": "p", + "42": "p", + "36": "b", + "34": "N", + "28": "P", + "22": "P", + "15": "P", + "13": "P", + "10": "P", + "9": "P", + "8": "P", + "6": "K", + "4": "R", + "3": "Q", + "1": "R" + } + }, + "player_turn": true, + "best_move": { + "move": "c5e6", + "ponder": "e7e6" + }, + "evaluation": [ + 373, + null + ], + "next_position": { + "position": { + "fen": "r5k1/p1p1qppp/2ppN3/4b3/4P3/6P1/PPP2P1P/1R1QR1K1 b - - 0 17", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 17, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6", + "a4c5", + "d7e7", + "c5e6" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "56": "r", + "55": "p", + "54": "p", + "53": "p", + "52": "q", + "50": "p", + "48": "p", + "44": "N", + "43": "p", + "42": "p", + "36": "b", + "28": "P", + "22": "P", + "15": "P", + "13": "P", + "10": "P", + "9": "P", + "8": "P", + "6": "K", + "4": "R", + "3": "Q", + "1": "R" + } + }, + "player_turn": false, + "best_move": { + "move": "e7e6", + "ponder": "b2b3" + }, + "evaluation": [ + -379, + null + ], + "next_position": { + "position": { + "fen": "r5k1/p1p2ppp/2ppq3/4b3/4P3/6P1/PPP2P1P/1R1QR1K1 w - - 0 18", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 18, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6", + "a4c5", + "d7e7", + "c5e6", + "e7e6" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "56": "r", + "55": "p", + "54": "p", + "53": "p", + "50": "p", + "48": "p", + "44": "q", + "43": "p", + "42": "p", + "36": "b", + "28": "P", + "22": "P", + "15": "P", + "13": "P", + "10": "P", + "9": "P", + "8": "P", + "6": "K", + "4": "R", + "3": "Q", + "1": "R" + } + }, + "player_turn": true, + "best_move": { + "move": "b2b3", + "ponder": "a7a5" + }, + "evaluation": [ + 380, + null + ], + "next_position": { + "position": { + "fen": "r5k1/p1p2ppp/2ppq3/4b3/4P3/1P4P1/P1P2P1P/1R1QR1K1 b - - 0 18", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 18, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6", + "a4c5", + "d7e7", + "c5e6", + "e7e6", + "b2b3" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "56": "r", + "55": "p", + "54": "p", + "53": "p", + "50": "p", + "48": "p", + "44": "q", + "43": "p", + "42": "p", + "36": "b", + "28": "P", + "22": "P", + "17": "P", + "15": "P", + "13": "P", + "10": "P", + "8": "P", + "6": "K", + "4": "R", + "3": "Q", + "1": "R" + } + }, + "player_turn": false, + "best_move": null, + "evaluation": null, + "next_position": null, + "analysed_legals": [], + "strict": true, + "is_ambiguous": false + }, + "analysed_legals": [ + { + "move": "b2b3", + "evaluation": [ + -380, + null + ] + }, + { + "move": "f2f4", + "evaluation": [ + -373, + null + ] + }, + { + "move": "a2a3", + "evaluation": [ + -365, + null + ] + }, + { + "move": "a2a4", + "evaluation": [ + -357, + null + ] + }, + { + "move": "d1e2", + "evaluation": [ + -287, + null + ] + }, + { + "move": "c2c3", + "evaluation": [ + -265, + null + ] + }, + { + "move": "g1g2", + "evaluation": [ + -264, + null + ] + }, + { + "move": "e1e3", + "evaluation": [ + -243, + null + ] + }, + { + "move": "d1d3", + "evaluation": [ + -233, + null + ] + }, + { + "move": "d1d2", + "evaluation": [ + -228, + null + ] + }, + { + "move": "g1h1", + "evaluation": [ + -224, + null + ] + }, + { + "move": "g1f1", + "evaluation": [ + -219, + null + ] + }, + { + "move": "e1e2", + "evaluation": [ + -219, + null + ] + }, + { + "move": "c2c4", + "evaluation": [ + -212, + null + ] + }, + { + "move": "h2h4", + "evaluation": [ + -208, + null + ] + }, + { + "move": "b2b4", + "evaluation": [ + -202, + null + ] + }, + { + "move": "e1f1", + "evaluation": [ + -199, + null + ] + }, + { + "move": "d1c1", + "evaluation": [ + -194, + null + ] + }, + { + "move": "f2f3", + "evaluation": [ + -193, + null + ] + }, + { + "move": "d1h5", + "evaluation": [ + -162, + null + ] + }, + { + "move": "d1f3", + "evaluation": [ + -138, + null + ] + }, + { + "move": "b1a1", + "evaluation": [ + -133, + null + ] + }, + { + "move": "h2h3", + "evaluation": [ + -77, + null + ] + }, + { + "move": "b1c1", + "evaluation": [ + 15, + null + ] + }, + { + "move": "g3g4", + "evaluation": [ + 25, + null + ] + }, + { + "move": "d1d6", + "evaluation": [ + 746, + null + ] + }, + { + "move": "d1d5", + "evaluation": [ + 815, + null + ] + }, + { + "move": "d1d4", + "evaluation": [ + 823, + null + ] + }, + { + "move": "d1g4", + "evaluation": [ + 838, + null + ] + } + ], + "strict": true, + "is_ambiguous": true + }, + "analysed_legals": [], + "strict": true, + "is_ambiguous": false + }, + "analysed_legals": [ + { + "move": "c5e6", + "evaluation": [ + -372, + null + ] + }, + { + "move": "c5d3", + "evaluation": [ + 36, + null + ] + }, + { + "move": "c5a6", + "evaluation": [ + 49, + null + ] + }, + { + "move": "c5b7", + "evaluation": [ + 86, + null + ] + }, + { + "move": "c5b3", + "evaluation": [ + 101, + null + ] + }, + { + "move": "c5a4", + "evaluation": [ + 137, + null + ] + }, + { + "move": "f2f4", + "evaluation": [ + 212, + null + ] + }, + { + "move": "g1f1", + "evaluation": [ + 294, + null + ] + }, + { + "move": "d1e2", + "evaluation": [ + 430, + null + ] + }, + { + "move": "g1g2", + "evaluation": [ + 451, + null + ] + }, + { + "move": "c2c3", + "evaluation": [ + 488, + null + ] + }, + { + "move": "g1h1", + "evaluation": [ + 496, + null + ] + }, + { + "move": "d1d3", + "evaluation": [ + 530, + null + ] + }, + { + "move": "b1a1", + "evaluation": [ + 538, + null + ] + }, + { + "move": "f2f3", + "evaluation": [ + 563, + null + ] + }, + { + "move": "d1d2", + "evaluation": [ + 569, + null + ] + }, + { + "move": "e1f1", + "evaluation": [ + 571, + null + ] + }, + { + "move": "d1g4", + "evaluation": [ + 585, + null + ] + }, + { + "move": "h2h3", + "evaluation": [ + 604, + null + ] + }, + { + "move": "h2h4", + "evaluation": [ + 608, + null + ] + }, + { + "move": "d1c1", + "evaluation": [ + 623, + null + ] + }, + { + "move": "c5d7", + "evaluation": [ + 625, + null + ] + }, + { + "move": "d1f3", + "evaluation": [ + 631, + null + ] + }, + { + "move": "b2b3", + "evaluation": [ + 636, + null + ] + }, + { + "move": "a2a3", + "evaluation": [ + 639, + null + ] + }, + { + "move": "d1h5", + "evaluation": [ + 641, + null + ] + }, + { + "move": "e1e2", + "evaluation": [ + 646, + null + ] + }, + { + "move": "a2a4", + "evaluation": [ + 653, + null + ] + }, + { + "move": "e1e3", + "evaluation": [ + 659, + null + ] + }, + { + "move": "c2c4", + "evaluation": [ + 661, + null + ] + }, + { + "move": "b1c1", + "evaluation": [ + 676, + null + ] + }, + { + "move": "b2b4", + "evaluation": [ + 685, + null + ] + }, + { + "move": "d1d5", + "evaluation": [ + 823, + null + ] + }, + { + "move": "d1d4", + "evaluation": [ + 823, + null + ] + }, + { + "move": "g3g4", + "evaluation": [ + 874, + null + ] + }, + { + "move": "d1d6", + "evaluation": [ + 930, + null + ] + } + ], + "strict": true, + "is_ambiguous": false + }, + "analysed_legals": [], + "strict": true, + "is_ambiguous": false + }, + "analysed_legals": [ + { + "move": "a4c5", + "evaluation": [ + -375, + null + ] + }, + { + "move": "f2f4", + "evaluation": [ + 7, + null + ] + }, + { + "move": "d1d2", + "evaluation": [ + 50, + null + ] + }, + { + "move": "g1h1", + "evaluation": [ + 51, + null + ] + }, + { + "move": "a4c3", + "evaluation": [ + 52, + null + ] + }, + { + "move": "d1d3", + "evaluation": [ + 60, + null + ] + }, + { + "move": "d1e2", + "evaluation": [ + 66, + null + ] + }, + { + "move": "e1e2", + "evaluation": [ + 72, + null + ] + }, + { + "move": "a2a3", + "evaluation": [ + 78, + null + ] + }, + { + "move": "h2h3", + "evaluation": [ + 92, + null + ] + }, + { + "move": "c2c3", + "evaluation": [ + 95, + null + ] + }, + { + "move": "f2f3", + "evaluation": [ + 96, + null + ] + }, + { + "move": "h2h4", + "evaluation": [ + 100, + null + ] + }, + { + "move": "d1g4", + "evaluation": [ + 104, + null + ] + }, + { + "move": "c2c4", + "evaluation": [ + 113, + null + ] + }, + { + "move": "d1f3", + "evaluation": [ + 123, + null + ] + }, + { + "move": "e1f1", + "evaluation": [ + 127, + null + ] + }, + { + "move": "d1h5", + "evaluation": [ + 128, + null + ] + }, + { + "move": "e1e3", + "evaluation": [ + 138, + null + ] + }, + { + "move": "b2b4", + "evaluation": [ + 140, + null + ] + }, + { + "move": "g1g2", + "evaluation": [ + 149, + null + ] + }, + { + "move": "b2b3", + "evaluation": [ + 151, + null + ] + }, + { + "move": "b1c1", + "evaluation": [ + 252, + null + ] + }, + { + "move": "b1a1", + "evaluation": [ + 286, + null + ] + }, + { + "move": "d1c1", + "evaluation": [ + 386, + null + ] + }, + { + "move": "g1f1", + "evaluation": [ + 461, + null + ] + }, + { + "move": "g3g4", + "evaluation": [ + 590, + null + ] + }, + { + "move": "a4b6", + "evaluation": [ + 623, + null + ] + }, + { + "move": "d1d6", + "evaluation": [ + 984, + null + ] + }, + { + "move": "d1d5", + "evaluation": [ + 1015, + null + ] + }, + { + "move": "d1d4", + "evaluation": [ + 1035, + null + ] + } + ], + "strict": true, + "is_ambiguous": false + } + }, + "is_complete": true + }, + { + "puzzle": { + "game_id": "2", + "category": "Material", + "last_pos": { + "fen": "4r1k1/p1p2ppp/1b1p4/2p5/4P3/1P3QPq/2P1RP1P/R5K1 w - - 2 24", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 24, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6", + "a4c5", + "d7e7", + "c5e6", + "e7e6", + "b2b3", + "e6h3", + "d1f3", + "a8e8", + "a2a4", + "c6c5", + "a4a5", + "e5c3", + "e1e2", + "c3a5", + "b1a1", + "a5b6" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "60": "r", + "55": "p", + "54": "p", + "53": "p", + "50": "p", + "48": "p", + "43": "p", + "41": "b", + "34": "p", + "28": "P", + "23": "q", + "22": "P", + "21": "Q", + "17": "P", + "15": "P", + "13": "P", + "12": "R", + "10": "P", + "6": "K", + "0": "R" + } + }, + "last_move": "f3f5", + "move_list": [ + "h3f5", + "e4f5", + "e8e2", + "c2c4" + ], + "positions": { + "position": { + "fen": "4r1k1/p1p2ppp/1b1p4/2p2Q2/4P3/1P4Pq/2P1RP1P/R5K1 b - - 3 24", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 24, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6", + "a4c5", + "d7e7", + "c5e6", + "e7e6", + "b2b3", + "e6h3", + "d1f3", + "a8e8", + "a2a4", + "c6c5", + "a4a5", + "e5c3", + "e1e2", + "c3a5", + "b1a1", + "a5b6", + "f3f5" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "60": "r", + "55": "p", + "54": "p", + "53": "p", + "50": "p", + "48": "p", + "43": "p", + "41": "b", + "37": "Q", + "34": "p", + "28": "P", + "23": "q", + "22": "P", + "17": "P", + "15": "P", + "13": "P", + "12": "R", + "10": "P", + "6": "K", + "0": "R" + } + }, + "player_turn": true, + "best_move": { + "move": "h3f5", + "ponder": "e4f5" + }, + "evaluation": [ + 646, + null + ], + "next_position": { + "position": { + "fen": "4r1k1/p1p2ppp/1b1p4/2p2q2/4P3/1P4P1/2P1RP1P/R5K1 w - - 0 25", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 25, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6", + "a4c5", + "d7e7", + "c5e6", + "e7e6", + "b2b3", + "e6h3", + "d1f3", + "a8e8", + "a2a4", + "c6c5", + "a4a5", + "e5c3", + "e1e2", + "c3a5", + "b1a1", + "a5b6", + "f3f5", + "h3f5" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "60": "r", + "55": "p", + "54": "p", + "53": "p", + "50": "p", + "48": "p", + "43": "p", + "41": "b", + "37": "q", + "34": "p", + "28": "P", + "22": "P", + "17": "P", + "15": "P", + "13": "P", + "12": "R", + "10": "P", + "6": "K", + "0": "R" + } + }, + "player_turn": false, + "best_move": { + "move": "e4f5", + "ponder": "e8e2" + }, + "evaluation": [ + -668, + null + ], + "next_position": { + "position": { + "fen": "4r1k1/p1p2ppp/1b1p4/2p2P2/8/1P4P1/2P1RP1P/R5K1 b - - 0 25", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 25, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6", + "a4c5", + "d7e7", + "c5e6", + "e7e6", + "b2b3", + "e6h3", + "d1f3", + "a8e8", + "a2a4", + "c6c5", + "a4a5", + "e5c3", + "e1e2", + "c3a5", + "b1a1", + "a5b6", + "f3f5", + "h3f5", + "e4f5" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "60": "r", + "55": "p", + "54": "p", + "53": "p", + "50": "p", + "48": "p", + "43": "p", + "41": "b", + "37": "P", + "34": "p", + "22": "P", + "17": "P", + "15": "P", + "13": "P", + "12": "R", + "10": "P", + "6": "K", + "0": "R" + } + }, + "player_turn": true, + "best_move": { + "move": "e8e2", + "ponder": "c2c4" + }, + "evaluation": [ + 624, + null + ], + "next_position": { + "position": { + "fen": "6k1/p1p2ppp/1b1p4/2p2P2/8/1P4P1/2P1rP1P/R5K1 w - - 0 26", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 26, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6", + "a4c5", + "d7e7", + "c5e6", + "e7e6", + "b2b3", + "e6h3", + "d1f3", + "a8e8", + "a2a4", + "c6c5", + "a4a5", + "e5c3", + "e1e2", + "c3a5", + "b1a1", + "a5b6", + "f3f5", + "h3f5", + "e4f5", + "e8e2" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "55": "p", + "54": "p", + "53": "p", + "50": "p", + "48": "p", + "43": "p", + "41": "b", + "37": "P", + "34": "p", + "22": "P", + "17": "P", + "15": "P", + "13": "P", + "12": "r", + "10": "P", + "6": "K", + "0": "R" + } + }, + "player_turn": false, + "best_move": { + "move": "c2c4", + "ponder": "e2b2" + }, + "evaluation": [ + -653, + null + ], + "next_position": { + "position": { + "fen": "6k1/p1p2ppp/1b1p4/2p2P2/2P5/1P4P1/4rP1P/R5K1 b - - 0 26", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 26, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6", + "a4c5", + "d7e7", + "c5e6", + "e7e6", + "b2b3", + "e6h3", + "d1f3", + "a8e8", + "a2a4", + "c6c5", + "a4a5", + "e5c3", + "e1e2", + "c3a5", + "b1a1", + "a5b6", + "f3f5", + "h3f5", + "e4f5", + "e8e2", + "c2c4" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "55": "p", + "54": "p", + "53": "p", + "50": "p", + "48": "p", + "43": "p", + "41": "b", + "37": "P", + "34": "p", + "26": "P", + "22": "P", + "17": "P", + "15": "P", + "13": "P", + "12": "r", + "6": "K", + "0": "R" + } + }, + "player_turn": true, + "best_move": { + "move": "e2b2", + "ponder": "a1a3" + }, + "evaluation": [ + 683, + null + ], + "next_position": { + "position": { + "fen": "6k1/p1p2ppp/1b1p4/2p2P2/2P5/1P4P1/1r3P1P/R5K1 w - - 1 27", + "aliases": [ + "Standard", + "Chess", + "Classical", + "Normal", + "Illegal", + "From Position" + ], + "fullmove_number": 27, + "move_stack": [ + "e2e4", + "e7e5", + "g1f3", + "d7d6", + "d2d4", + "e5d4", + "f3d4", + "b8c6", + "f1b5", + "c8d7", + "b5c6", + "d7c6", + "b1c3", + "g8f6", + "c1g5", + "f8e7", + "g5f6", + "e7f6", + "d4c6", + "b7c6", + "e1g1", + "e8g8", + "f1e1", + "f6e5", + "g2g3", + "d8d7", + "a1b1", + "f8e8", + "c3a4", + "e8e6", + "a4c5", + "d7e7", + "c5e6", + "e7e6", + "b2b3", + "e6h3", + "d1f3", + "a8e8", + "a2a4", + "c6c5", + "a4a5", + "e5c3", + "e1e2", + "c3a5", + "b1a1", + "a5b6", + "f3f5", + "h3f5", + "e4f5", + "e8e2", + "c2c4", + "e2b2" + ], + "uci_variant": "chess", + "piece_map": { + "62": "k", + "55": "p", + "54": "p", + "53": "p", + "50": "p", + "48": "p", + "43": "p", + "41": "b", + "37": "P", + "34": "p", + "26": "P", + "22": "P", + "17": "P", + "15": "P", + "13": "P", + "9": "r", + "6": "K", + "0": "R" + } + }, + "player_turn": false, + "best_move": null, + "evaluation": null, + "next_position": null, + "analysed_legals": [], + "strict": true, + "is_ambiguous": false + }, + "analysed_legals": [ + { + "move": "e2b2", + "evaluation": [ + -653, + null + ] + }, + { + "move": "g7g5", + "evaluation": [ + -620, + null + ] + }, + { + "move": "h7h6", + "evaluation": [ + -619, + null + ] + }, + { + "move": "e2c2", + "evaluation": [ + -616, + null + ] + }, + { + "move": "e2d2", + "evaluation": [ + -615, + null + ] + }, + { + "move": "a7a5", + "evaluation": [ + -605, + null + ] + }, + { + "move": "d6d5", + "evaluation": [ + -600, + null + ] + }, + { + "move": "h7h5", + "evaluation": [ + -593, + null + ] + }, + { + "move": "f7f6", + "evaluation": [ + -592, + null + ] + }, + { + "move": "g8f8", + "evaluation": [ + -586, + null + ] + }, + { + "move": "e2e4", + "evaluation": [ + -569, + null + ] + }, + { + "move": "c7c6", + "evaluation": [ + -569, + null + ] + }, + { + "move": "g7g6", + "evaluation": [ + -566, + null + ] + }, + { + "move": "a7a6", + "evaluation": [ + -563, + null + ] + }, + { + "move": "e2e5", + "evaluation": [ + -557, + null + ] + }, + { + "move": "e2e8", + "evaluation": [ + -553, + null + ] + }, + { + "move": "e2e7", + "evaluation": [ + -546, + null + ] + }, + { + "move": "g8h8", + "evaluation": [ + -533, + null + ] + }, + { + "move": "b6a5", + "evaluation": [ + -12, + null + ] + }, + { + "move": "e2e6", + "evaluation": [ + 94, + null + ] + }, + { + "move": "e2f2", + "evaluation": [ + 185, + null + ] + }, + { + "move": "e2a2", + "evaluation": [ + 335, + null + ] + }, + { + "move": "e2e3", + "evaluation": [ + 346, + null + ] + }, + { + "move": "e2e1", + "evaluation": [ + 364, + null + ] + } + ], + "strict": true, + "is_ambiguous": true + }, + "analysed_legals": [], + "strict": true, + "is_ambiguous": false + }, + "analysed_legals": [ + { + "move": "e8e2", + "evaluation": [ + -650, + null + ] + }, + { + "move": "e8d8", + "evaluation": [ + 305, + null + ] + }, + { + "move": "e8b8", + "evaluation": [ + 314, + null + ] + }, + { + "move": "e8a8", + "evaluation": [ + 320, + null + ] + }, + { + "move": "g8f8", + "evaluation": [ + 321, + null + ] + }, + { + "move": "e8c8", + "evaluation": [ + 326, + null + ] + }, + { + "move": "e8f8", + "evaluation": [ + 336, + null + ] + }, + { + "move": "e8e5", + "evaluation": [ + 475, + null + ] + }, + { + "move": "e8e6", + "evaluation": [ + 846, + null + ] + }, + { + "move": "g7g6", + "evaluation": [ + 880, + null + ] + }, + { + "move": "g7g5", + "evaluation": [ + 896, + null + ] + }, + { + "move": "h7h5", + "evaluation": [ + 926, + null + ] + }, + { + "move": "e8e3", + "evaluation": [ + 928, + null + ] + }, + { + "move": "e8e7", + "evaluation": [ + 953, + null + ] + }, + { + "move": "e8e4", + "evaluation": [ + 961, + null + ] + }, + { + "move": "h7h6", + "evaluation": [ + 974, + null + ] + }, + { + "move": "g8h8", + "evaluation": [ + null, + 1 + ] + }, + { + "move": "b6a5", + "evaluation": [ + null, + 1 + ] + }, + { + "move": "c7c6", + "evaluation": [ + null, + 1 + ] + }, + { + "move": "a7a6", + "evaluation": [ + null, + 1 + ] + }, + { + "move": "d6d5", + "evaluation": [ + null, + 1 + ] + }, + { + "move": "c5c4", + "evaluation": [ + null, + 1 + ] + }, + { + "move": "a7a5", + "evaluation": [ + null, + 1 + ] + }, + { + "move": "f7f6", + "evaluation": [ + null, + 3 + ] + } + ], + "strict": true, + "is_ambiguous": false + }, + "analysed_legals": [], + "strict": true, + "is_ambiguous": false + }, + "analysed_legals": [ + { + "move": "h3f5", + "evaluation": [ + -638, + null + ] + }, + { + "move": "h3h6", + "evaluation": [ + 285, + null + ] + }, + { + "move": "d6d5", + "evaluation": [ + 930, + null + ] + }, + { + "move": "h3g3", + "evaluation": [ + 936, + null + ] + }, + { + "move": "h3h2", + "evaluation": [ + 944, + null + ] + }, + { + "move": "c5c4", + "evaluation": [ + 982, + null + ] + }, + { + "move": "h3h4", + "evaluation": [ + 984, + null + ] + }, + { + "move": "g7g6", + "evaluation": [ + 1015, + null + ] + }, + { + "move": "e8e7", + "evaluation": [ + 1020, + null + ] + }, + { + "move": "e8e6", + "evaluation": [ + 1020, + null + ] + }, + { + "move": "h3g4", + "evaluation": [ + 1020, + null + ] + }, + { + "move": "a7a5", + "evaluation": [ + 1020, + null + ] + }, + { + "move": "h3h5", + "evaluation": [ + 1023, + null + ] + }, + { + "move": "h3f1", + "evaluation": [ + 1023, + null + ] + }, + { + "move": "h7h6", + "evaluation": [ + 1023, + null + ] + }, + { + "move": "c7c6", + "evaluation": [ + 1023, + null + ] + }, + { + "move": "g7g5", + "evaluation": [ + 1023, + null + ] + }, + { + "move": "h3g2", + "evaluation": [ + 1027, + null + ] + }, + { + "move": "f7f6", + "evaluation": [ + 1027, + null + ] + }, + { + "move": "g8h8", + "evaluation": [ + 1030, + null + ] + }, + { + "move": "g8f8", + "evaluation": [ + 1030, + null + ] + }, + { + "move": "e8f8", + "evaluation": [ + 1030, + null + ] + }, + { + "move": "e8d8", + "evaluation": [ + 1030, + null + ] + }, + { + "move": "e8a8", + "evaluation": [ + 1030, + null + ] + }, + { + "move": "e8e5", + "evaluation": [ + 1030, + null + ] + }, + { + "move": "b6a5", + "evaluation": [ + 1030, + null + ] + }, + { + "move": "e8b8", + "evaluation": [ + 1038, + null + ] + }, + { + "move": "h7h5", + "evaluation": [ + 1058, + null + ] + }, + { + "move": "a7a6", + "evaluation": [ + 1126, + null + ] + }, + { + "move": "e8e4", + "evaluation": [ + 1323, + null + ] + }, + { + "move": "e8c8", + "evaluation": [ + 1438, + null + ] + } + ], + "strict": true, + "is_ambiguous": false + } + }, + "is_complete": true + } +] \ No newline at end of file diff --git a/test/unit/__init__.py b/test/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/unit/test_regression.py b/test/unit/test_regression.py new file mode 100644 index 0000000..19bcf06 --- /dev/null +++ b/test/unit/test_regression.py @@ -0,0 +1,102 @@ +import json +import logging +import os +import unittest + +from modules.utils.decoding import score_from_dict, board_from_dict, puzzle_from_dict +from modules.investigate.investigate import investigate +from modules.utils.helpers import configure_logging + +TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +INVESTIGATE_FILE = f'{TEST_DIR}/../data/investigate.json' +IS_COMPLETE_FILE = f'{TEST_DIR}/../data/is_complete.json' + +configure_logging(logging.DEBUG) + + +class TestRegression(unittest.TestCase): + engine = None + investigate_test_data = [] + complete_test_data = [] + + @classmethod + def setUpClass(cls): + with open(INVESTIGATE_FILE, 'r') as f: + content = f.read() + TestRegression.investigate_test_data = json.loads(content) + + with open(IS_COMPLETE_FILE, 'r') as f: + content = f.read() + TestRegression.complete_test_data = json.loads(content) + + def test_investigate(self): + """ + Go through known investigate arguments and results and see if the actual result is the same. + :return: + """ + for definition in TestRegression.investigate_test_data: + score_a, score_b = score_from_dict(definition["score_a"]), score_from_dict(definition["score_b"]) + expected_result = definition["result"] + board = board_from_dict(definition["board"]) + print(score_a) + print(score_b) + logging.debug(f"Testing position {board.fen()} with scores {score_a} and {score_b}") + + result = investigate(score_a, score_b, board) + + self.assertEqual(expected_result, result) + + def test_ambiguous(self): + """ + Go through known ambiguous() arguments (position_list class) + and results and see if the actual result is the same. + :return: + """ + for definition in TestRegression.complete_test_data: + logging.debug(f"Testing puzzle {definition}") + + pd = definition['puzzle'] + expected_result = pd['positions']['is_ambiguous'] + puzzle = puzzle_from_dict(pd) + result = puzzle.positions.ambiguous() + + logging.debug(f'{expected_result} vs {result}') + self.assertEqual(expected_result, result) + + def test_positions_move_list(self): + """ + Go through known move_list() results (position_list class) + if the actual result is the same. + :return: + """ + for definition in TestRegression.complete_test_data: + logging.debug(f"Testing puzzle {definition}") + + pd = definition['puzzle'] + puzzle = puzzle_from_dict(pd) + result = puzzle.positions.move_list() + + expected_result = pd['move_list'] + + logging.debug(f'{expected_result} vs {result}') + self.assertEqual(expected_result, result) + + def test_is_complete(self): + """ + :return: + """ + for definition in TestRegression.complete_test_data: + logging.debug(f"Testing puzzle {definition}") + + expected_result = definition['is_complete'] + + pd = definition['puzzle'] + puzzle = puzzle_from_dict(pd) + result = puzzle.is_complete() + + logging.debug(f'{expected_result} vs {result}') + self.assertEqual(expected_result, result) + + +if __name__ == '__main__': + unittest.main()