diff --git a/dev_display.py b/dev_display.py new file mode 100644 index 0000000..a1bc96b --- /dev/null +++ b/dev_display.py @@ -0,0 +1,65 @@ +"""Display board from input seeds.""" + +import logging +from pathlib import Path +from typing import Optional + +import click + +from royal_game._exceptions import BoardError +from royal_game.modules.board import Board + +logging.basicConfig(format="%(levelname)s: %(message)s") +logger = logging.getLogger(__name__) + + +def process_input(seed: str, decimal: bool) -> Board | None: + """Produce board from input seed string.""" + try: + seed = int(seed, 10 if decimal else 2) + except ValueError as e: + logger.warning(e) + return None + + try: + board = Board(seed) + except BoardError as e: + logger.warning(e) + board = Board(seed, no_verify=True) + finally: + return board + + +@click.command() +@click.option("--decimal/--binary", "-d/-b", default=True) +@click.argument("in_file", required=False, type=click.Path(exists=True, path_type=Path)) +def main(decimal: bool, in_file: Optional[Path]): + """ + Accept seed input from user or file and display the corresponding boards. + + If an input file is provided, decimal encoding is assumed. The file should + contain one seed per line. + + Warnings are shown for invalid boards but the program will not terminate. + """ + if in_file is not None: + decimal = True + with open(in_file, "r") as fin: + seeds = [line.strip() for line in fin.readlines()] + + for seed in seeds: + board = process_input(seed, decimal) + if board is not None: + print(f"{seed}:\n{board}") + + else: + user_input = "" + while user_input != "quit": + user_input = input("Enter a board seed in the chosen encoding: ") + board = process_input(user_input, decimal) + if board is not None: + print(board) + + +if __name__ == "__main__": + main() diff --git a/rust_translate.py b/rust_translate.py deleted file mode 100644 index 5b11086..0000000 --- a/rust_translate.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Translate Rust board seed to Python board seed.""" - -from royal_game._exceptions import BoardError -from royal_game.modules.board import Board - - -def translate(rust_seed: int, verify: bool = True) -> int: - """ - Rust board encoding schema copied below. - - Two bits each for board grids to achieve consistency in the order of - W1...W4, 5...12, W13, W14, B1...B4, 5...12, B13, B14 (from least to most significant). - Note the more significant bit of private grids are unused (56 total) - - Three bits each for WS and BS from the least to the most significant (6 total) - - Bits 62-63 indicate the endgame state. - This is useful for stopping search and assigning rewards. - 00: not set/calculated - 01: white win - 10: black win - 11: in progress - """ - py_seed = 0 - white_total, black_total = 0, 0 - - for py_offset, rust_offset in enumerate((0, 2, 4, 6, 24, 26)): - if ((rust_seed & (0b11 << rust_offset)) >> rust_offset) == 0b01: - white_total += 1 - py_seed += 1 << py_offset - - for py_offset, rust_offset in enumerate((28, 30, 32, 34, 52, 54)): - if ((rust_seed & (0b11 << rust_offset)) >> rust_offset) == 0b10: - black_total += 1 - py_seed += 1 << (py_offset + 6) - - for py_offset, rust_offset in zip(range(12, 28, 2), range(8, 24, 2)): - grid_status = (rust_seed & (0b11 << rust_offset)) >> rust_offset - py_seed += grid_status << py_offset - - if grid_status == 0b01: - white_total += 1 - elif grid_status == 0b10: - black_total += 1 - - num_ws = (rust_seed & (0b111 << 56)) >> 56 - py_seed += num_ws << 28 - num_bs = (rust_seed & (0b111 << 59)) >> 59 - py_seed += num_bs << 34 - - # WE - py_seed += (7 - white_total - num_ws) << 31 - # BE - py_seed += (7 - black_total - num_bs) << 37 - - if verify: - try: - _ = Board(py_seed) - except BoardError as e: - print(f"Provided rust seed {rust_seed} is invalid.") - raise e - - return py_seed diff --git a/test_translate.py b/test_translate.py index aad0f84..98b956c 100644 --- a/test_translate.py +++ b/test_translate.py @@ -1,20 +1,40 @@ import pytest from royal_game._exceptions import BoardError -from rust_translate import translate +from translate import rust_to_python, python_to_rust -def test_translate(): +def test_rust_to_python(): assert ( - translate(0b0010111000000000000010000100000000100000000000001000010000000000) + rust_to_python(0b0010111000000000000010000100000000100000000000001000010000000000) == 87510499392 ) assert ( - translate(0b0110000000000000000000000010100000100000000000000000001000000000) + rust_to_python(0b0110000000000000000000000010100000100000000000000000001000000000) == 83751871040 ) with pytest.raises(BoardError): # too many black pieces - translate(0b0000000000001010101010101010000000000000101010101010101000000000) + rust_to_python(0b0000000000001010101010101010000000000000101010101010101000000000) + + +def test_python_to_rust(): + assert ( + python_to_rust(87510499392) + == 0b1110111000000000000010000100000000100000000000001000010000000000 + ) + + assert ( + python_to_rust(83751871040) + == 0b0110000000000000000000000010100000100000000000000000001000000000 + ) + + assert ( + python_to_rust(966988398624) + == 0b1000001000000101000000000000000000000100010100000000000000000000 + ) + + with pytest.raises(BoardError): + python_to_rust(155055118456) diff --git a/tournament.py b/tournament.py index 714ccfb..9faeae9 100644 --- a/tournament.py +++ b/tournament.py @@ -81,7 +81,7 @@ def output_results(num_wins: defaultdict, num_games: int, self_play: bool) -> No help="Enable saving full debug output to games.log in addition to summary statistics", ) def main( - players: Iterable[click.Path], + players: Iterable[Path], num_games: int, board_seed: int, binary_seed: bool, diff --git a/translate.py b/translate.py new file mode 100644 index 0000000..322d8b0 --- /dev/null +++ b/translate.py @@ -0,0 +1,129 @@ +"""Translate Rust board seed to Python board seed.""" + +import logging +import click +from pathlib import Path + +from royal_game._exceptions import BoardError +from royal_game._constants import white_order_iter, black_order_iter +from royal_game.modules.board import Board + +logging.basicConfig(format="%(levelname)s: %(message)s") +logger = logging.getLogger(__name__) + + +def rust_to_python(rust_seed: int, verify: bool = True) -> int: + """ + Rust board encoding schema copied below. + + Two bits each for board grids to achieve consistency in the order of + W1...W4, 5...12, W13, W14, B1...B4, 5...12, B13, B14 (from least to most significant). + Note the more significant bit of private grids are unused (56 total) + + Three bits each for WS and BS from the least to the most significant (6 total) + + Bits 62-63 indicate the endgame state. + This is useful for stopping search and assigning rewards. + 00: not set/calculated + 01: white win + 10: black win + 11: in progress + """ + py_seed = 0 + white_total, black_total = 0, 0 + + for py_offset, rust_offset in enumerate((0, 2, 4, 6, 24, 26)): + if ((rust_seed & (0b11 << rust_offset)) >> rust_offset) == 0b01: + white_total += 1 + py_seed += 1 << py_offset + + for py_offset, rust_offset in enumerate((28, 30, 32, 34, 52, 54)): + if ((rust_seed & (0b11 << rust_offset)) >> rust_offset) == 0b10: + black_total += 1 + py_seed += 1 << (py_offset + 6) + + for py_offset, rust_offset in zip(range(12, 28, 2), range(8, 24, 2)): + grid_status = (rust_seed & (0b11 << rust_offset)) >> rust_offset + py_seed += grid_status << py_offset + + if grid_status == 0b01: + white_total += 1 + elif grid_status == 0b10: + black_total += 1 + + num_ws = (rust_seed & (0b111 << 56)) >> 56 + py_seed += num_ws << 28 + num_bs = (rust_seed & (0b111 << 59)) >> 59 + py_seed += num_bs << 34 + + # WE + py_seed += (7 - white_total - num_ws) << 31 + # BE + py_seed += (7 - black_total - num_bs) << 37 + + if verify: + try: + _ = Board(py_seed) + except BoardError as e: + logger.warning("Provided rust seed %d is invalid.", rust_seed) + raise e + + return py_seed + + +def python_to_rust(py_seed: int, verify: bool = True) -> int: + """ + Python board encoding schema copied below. + + bit 0-5: one bit each for W1...W14 indicating + whether the grid is occupied + bit 6-11: one bit each for B1...B14 indicating + whether the grid is occupied + bit 12-27: two bit each for 5...12 indicating + whether the grid is empty, white, or black + bit 28-39: three bit each for WS, WE, BS, BE + indicating number of pieces at start/end + + The endgame state bits will always be set. Thus the following assumption + is not valid: + + python_to_rust(rust_to_python(rust_seed)) == rust_seed + + Only compare the lower 62 bits if needed. + """ + if verify: + try: + _ = Board(py_seed) + except BoardError as e: + logger.warning("Provided python seed %d is invalid.", py_seed) + raise e + + board = Board(py_seed, no_verify=True) + rust_seed = 0 + + if board.board["WE"].num_pieces == 7: + rust_seed += 0b01 << 62 + elif board.board["BE"].num_pieces == 7: + rust_seed += 0b10 << 62 + else: + rust_seed += 0b11 << 62 + + rust_seed += int(board.board["WS"]) << 56 + rust_seed += int(board.board["BS"]) << 59 + + for grid, offset in zip(list(white_order_iter()), range(0, 28, 2)): + # we can take this shortcut because GridStatus(1) indicates white + # which match the status bit for an occupied white private grid + rust_seed += int(board.board[grid]) << offset + + for grid, offset in zip(list(black_order_iter()), range(28, 56, 2)): + rust_seed += int(board.board[grid]) << offset + + return rust_seed + +def main(): + pass + + +if __name__ == "__main__": + main()