mirror of
https://github.com/kuhyx/WUT_Computer_Science.git
synced 2026-11-11 00:07:09 +01:00
PORR was the last course whose `run` did not work. Four measurements, not
guesses, explain why -- and none of them is the matrix size everyone assumes:
ProcessingType.PROCESSES 20.9s at n=2, 32.8s at n=5, 78.2s at n=10. That
is process-pool setup PER TEST, and it grows.
matrix_type poli3 densifies a sparse .mat into a 2.3 GB npz cache
(nemeth12: 723 MB) -- over the 2G ceiling before
any solving happens.
n=10000 800 MB matrix, and np.linalg.solve wants a copy.
n=5000 spd_5000.npz was a Git LFS pointer whose object
404s on GitHub, so it regenerates from scratch.
Remove those and what is left still exercises the whole Richardson
implementation at every size that fits: 18 tests, 28 seconds through the
harness, exit 0. The submitted parameter set is one documented environment
variable away. `main.py` now propagates pytest's status (verified: exit 5 when
nothing is collected) -- it discarded it before, so `run` could not tell a
passing suite from a failing one.
The rest of this commit is scripts/check_refactor_safety.py, which exists
because three regressions got past both ruff and a smoke run during this
campaign, and I would rather they could not get past the next one:
--signatures diffs two trees function by function, parameter by
parameter, default by default. This is what found the 22
EARIN functions whose defaults an annotation pass silently
dropped.
--logging a module with a __main__ guard that logs at INFO must call
basicConfig, or it prints nothing at all. Found two more
in PORR just now.
--print-keywords refuses to let an automated rewrite near print(x, end="")
or print(x, file=sys.stderr); losing those turns a
rendered board into one glyph per line, or silently moves
error output onto stdout.
All three pass on the whole archive. Also in here: 12 latent TypeErrors mypy
found in EARIN that ruff could not -- call sites still passing positionally to
parameters this campaign had made keyword-only, plus one caller of a function
whose argument I had removed. Every one would have raised at runtime.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
648 lines
25 KiB
Python
Executable File
648 lines
25 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Play draughts against a min-max bot with alpha-beta pruning, on 8x8."""
|
|
|
|
import copy
|
|
import logging
|
|
import math
|
|
import re
|
|
import sys
|
|
|
|
# A capture jumps exactly two squares, and the board is drawn four text rows
|
|
# per board row, with the rank label on the third of them.
|
|
CAPTURE_DISTANCE = 2
|
|
ROWS_PER_SQUARE = 4
|
|
LABEL_ROW = 2
|
|
ARGV_WITH_COLOUR = 2
|
|
ARGV_WITH_DEPTH = 3
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Game:
|
|
"""Game."""
|
|
|
|
def __init__(self, size: int) -> None:
|
|
"""Set up an empty board of the given size and place both armies."""
|
|
self.board_size = size
|
|
self.white_positions = self.initialize_white()
|
|
self.black_positions = self.initialize_black()
|
|
|
|
def initialize_white(self) -> set[tuple[int, int, bool]]:
|
|
"""Initialize white pieces."""
|
|
white_positions = []
|
|
for y_coordinate in range(math.floor((self.board_size - 2) / 2)):
|
|
for x_coordinate in range(self.board_size):
|
|
if y_coordinate % 2 == 0:
|
|
if x_coordinate % 2 == 1:
|
|
white_positions.append((x_coordinate, y_coordinate, False))
|
|
elif x_coordinate % 2 == 0:
|
|
white_positions.append((x_coordinate, y_coordinate, False))
|
|
return white_positions
|
|
|
|
def initialize_black(self) -> set[tuple[int, int, bool]]:
|
|
"""Initialize black pieces."""
|
|
black_positions = []
|
|
for y_coordinate in range(
|
|
self.board_size - math.floor((self.board_size - 2) / 2), self.board_size
|
|
):
|
|
for x_coordinate in range(self.board_size):
|
|
if y_coordinate % 2 == 0:
|
|
if x_coordinate % 2 == 1:
|
|
black_positions.append((x_coordinate, y_coordinate, False))
|
|
elif x_coordinate % 2 == 0:
|
|
black_positions.append((x_coordinate, y_coordinate, False))
|
|
return black_positions
|
|
|
|
def check_move_out_of_bounds(self, to_: tuple[int, int]) -> bool:
|
|
"""Check if the move destination is out of the bounds of the board."""
|
|
if to_[0] < 0 or to_[0] > self.board_size - 1:
|
|
return True
|
|
return bool(to_[1] < 0 or to_[1] > self.board_size - 1)
|
|
|
|
def check_piece_exists(self, coords: tuple[int, int], color: str) -> bool:
|
|
"""Check if a piece of given color exists at a given spot."""
|
|
if color == "white":
|
|
if any(
|
|
piece in self.white_positions
|
|
for piece in ((*coords, False), (*coords, True))
|
|
):
|
|
return True
|
|
elif any(
|
|
piece in self.black_positions
|
|
for piece in ((*coords, False), (*coords, True))
|
|
):
|
|
return True
|
|
return False
|
|
|
|
def check_piece_king(self, coords: tuple[int, int], color: str) -> bool:
|
|
"""Check if a piece of in a given spot and of a given color is a king.
|
|
|
|
Return false if no piece is found.
|
|
"""
|
|
if color == "white":
|
|
return (*coords, True) in self.white_positions
|
|
if color == "black":
|
|
return (*coords, True) in self.black_positions
|
|
return False
|
|
|
|
# https://stackoverflow.com/a/2191707
|
|
def check_move_piece_capable(
|
|
self, from_: tuple[int, int], to_: tuple[int, int], color: str
|
|
) -> bool:
|
|
"""Check if the move is exactly one square diagonally."""
|
|
if abs(to_[0] - from_[0]) == 1:
|
|
if self.check_piece_king(from_, color):
|
|
return True
|
|
if color == "white":
|
|
return to_[1] == from_[1] + 1
|
|
if color == "black":
|
|
return to_[1] == from_[1] - 1
|
|
return False
|
|
|
|
def check_capture(
|
|
self, from_: tuple[int, int], to_: tuple[int, int], color: str
|
|
) -> tuple[int, int] | None:
|
|
"""Check if a piece was captured for a given move.
|
|
|
|
Return captured piece coordinates or None.
|
|
"""
|
|
# captures can only happen if the player moved twice-diagonally
|
|
|
|
if (
|
|
abs(to_[0] - from_[0]) != CAPTURE_DISTANCE
|
|
or abs(to_[1] - from_[1]) != CAPTURE_DISTANCE
|
|
):
|
|
return None
|
|
|
|
middle = (abs(to_[0] + from_[0]) // 2, abs(to_[1] + from_[1]) // 2)
|
|
|
|
if color == "white" and self.check_piece_exists(middle, "black"):
|
|
return middle
|
|
if color == "black" and self.check_piece_exists(middle, "white"):
|
|
return middle
|
|
return None
|
|
|
|
def check_move_legal(
|
|
self,
|
|
from_: tuple[int, int],
|
|
to_: tuple[int, int],
|
|
color: str,
|
|
*,
|
|
give_feedback: bool = False,
|
|
) -> bool | tuple[int, int]:
|
|
"""Check whether a move is legal.
|
|
|
|
Returns False when it is not, True when it is a plain move, and the
|
|
coordinates of the captured piece when it is a capture.
|
|
"""
|
|
if self.check_move_out_of_bounds(to_):
|
|
if give_feedback:
|
|
logger.info(
|
|
"Illegal move! Final position is out of the bounds of the board"
|
|
)
|
|
return False
|
|
if not self.check_piece_exists(from_, color):
|
|
if give_feedback:
|
|
logger.info(
|
|
"Illegal move! There is no piece on the starting position "
|
|
"that belongs to the player"
|
|
)
|
|
return False
|
|
if self.check_piece_exists(to_, "white") or self.check_piece_exists(
|
|
to_, "black"
|
|
):
|
|
if give_feedback:
|
|
logger.info(
|
|
"Illegal move! Cannot move to position taken by another piece"
|
|
)
|
|
return False
|
|
capture = self.check_capture(from_, to_, color)
|
|
if capture is None:
|
|
if self.check_move_piece_capable(from_, to_, color):
|
|
return True
|
|
if give_feedback:
|
|
logger.info("Illegal move! You can only move diagonally")
|
|
return False
|
|
return capture
|
|
|
|
def make_move(
|
|
self, from_: tuple[int, int], to_: tuple[int, int], color: str
|
|
) -> bool:
|
|
"""Move a piece on the board and remove any captured pieces."""
|
|
move_legal = self.check_move_legal(from_, to_, color)
|
|
if move_legal is False:
|
|
return False
|
|
capture = move_legal if isinstance(move_legal, tuple) else None
|
|
king = self.check_piece_king(from_, color)
|
|
|
|
if color == "white":
|
|
self.white_positions.remove((*from_, king))
|
|
if capture:
|
|
captured_king = self.check_piece_king(capture, "black")
|
|
self.black_positions.remove((*move_legal, captured_king))
|
|
if to_[1] == self.board_size - 1:
|
|
self.white_positions.append((*to_, True))
|
|
else:
|
|
self.white_positions.append((*to_, king))
|
|
|
|
else:
|
|
self.black_positions.remove((*from_, king))
|
|
if capture:
|
|
captured_king = self.check_piece_king(capture, "white")
|
|
self.white_positions.remove((*move_legal, captured_king))
|
|
if to_[1] == 0:
|
|
self.black_positions.append((*to_, True))
|
|
else:
|
|
self.black_positions.append((*to_, king))
|
|
|
|
return True
|
|
|
|
def _print_letters(self, *, rotate: bool) -> None:
|
|
"""Write the file letters that run above and below the board."""
|
|
sys.stdout.write(" ")
|
|
for col in range(self.board_size):
|
|
letter = self.board_size - 1 - col if rotate else col
|
|
sys.stdout.write(f" {chr(ord('a') + letter)} ")
|
|
|
|
def _square_code(self, pos: tuple[int, int], background: str) -> str:
|
|
"""Return the one-character code of a square: piece, king, or empty."""
|
|
if (*pos, False) in self.white_positions:
|
|
return "w"
|
|
if (*pos, True) in self.white_positions:
|
|
return "W"
|
|
if (*pos, False) in self.black_positions:
|
|
return "b"
|
|
if (*pos, True) in self.black_positions:
|
|
return "B"
|
|
return background
|
|
|
|
def _print_cell(self, row: int, col: int, line: int, *, rotate: bool) -> None:
|
|
"""Write the one text row of one square."""
|
|
background = "#" if (col % 2 == (row // 4) % 2) != rotate else " "
|
|
pos = (self.board_size - 1 - col, row // 4) if rotate else (col, row // 4)
|
|
checker = self._square_code(pos, background)
|
|
|
|
if col == 0:
|
|
sys.stdout.write(
|
|
f"{row // 4:3d}" if line % ROWS_PER_SQUARE == LABEL_ROW else " "
|
|
)
|
|
|
|
match line % ROWS_PER_SQUARE:
|
|
case 0:
|
|
sys.stdout.write("+---")
|
|
case 1 | 3:
|
|
sys.stdout.write(f"|{3 * background}")
|
|
case 2:
|
|
sys.stdout.write(f"|{background}{checker}{background}")
|
|
|
|
def print_board(self, *, rotate: bool = False) -> None:
|
|
"""Print the board in the console.
|
|
|
|
The three helpers below used to be nested functions here, which is
|
|
what put this method at C901 17.
|
|
"""
|
|
self._print_letters(rotate=rotate)
|
|
sys.stdout.write(" \n")
|
|
|
|
row_range = (
|
|
range(self.board_size * 4)
|
|
if not rotate
|
|
else reversed(range(self.board_size * 4))
|
|
)
|
|
|
|
for line, row in enumerate(row_range):
|
|
for col in range(self.board_size):
|
|
self._print_cell(row, col, line, rotate=rotate)
|
|
if line % ROWS_PER_SQUARE == 0:
|
|
sys.stdout.write("+\n")
|
|
else:
|
|
sys.stdout.write(
|
|
f"|{row // ROWS_PER_SQUARE}\n"
|
|
if line % ROWS_PER_SQUARE == LABEL_ROW
|
|
else "|\n"
|
|
)
|
|
sys.stdout.write(" ")
|
|
for _col in range(self.board_size):
|
|
sys.stdout.write("+---")
|
|
sys.stdout.write("+\n")
|
|
self._print_letters(rotate=rotate)
|
|
sys.stdout.write("\n")
|
|
|
|
# Ran first in the code
|
|
def get_possible_moves_capture(
|
|
self, from_: tuple[int, int], color: str
|
|
) -> list[tuple[tuple[int, int], tuple[int, int]]]:
|
|
"""Return all possible captures for a piece."""
|
|
# all capturing moves:
|
|
legal_moves = []
|
|
move_down_left_two = (from_[0] + 2, from_[1] - 2)
|
|
move_down_right_two = (from_[0] + 2, from_[1] + 2)
|
|
move_up_left_two = (from_[0] - 2, from_[1] - 2)
|
|
move_up_right_two = (from_[0] - 2, from_[1] + 2)
|
|
if self.check_move_legal(from_, move_down_left_two, color) is not False:
|
|
legal_moves.append((from_, move_down_left_two))
|
|
if self.check_move_legal(from_, move_down_right_two, color) is not False:
|
|
legal_moves.append((from_, move_down_right_two))
|
|
if self.check_move_legal(from_, move_up_left_two, color) is not False:
|
|
legal_moves.append((from_, move_up_left_two))
|
|
if self.check_move_legal(from_, move_up_right_two, color) is not False:
|
|
legal_moves.append((from_, move_up_right_two))
|
|
return legal_moves
|
|
|
|
def get_possible_moves_non_capture(
|
|
self, from_: tuple[int, int], color: str
|
|
) -> list[tuple[tuple[int, int], tuple[int, int]]]:
|
|
"""Return all possible moves that are not captures for a piece."""
|
|
# all non-capturing moves
|
|
legal_moves = []
|
|
move_down_left_one = (from_[0] + 1, from_[1] - 1)
|
|
move_down_right_one = (from_[0] + 1, from_[1] + 1)
|
|
move_up_left_one = (from_[0] - 1, from_[1] - 1)
|
|
move_up_right_one = (from_[0] - 1, from_[1] + 1)
|
|
if self.check_move_legal(from_, move_down_left_one, color) is not False:
|
|
legal_moves.append((from_, move_down_left_one))
|
|
if self.check_move_legal(from_, move_down_right_one, color) is not False:
|
|
legal_moves.append((from_, move_down_right_one))
|
|
if self.check_move_legal(from_, move_up_left_one, color) is not False:
|
|
legal_moves.append((from_, move_up_left_one))
|
|
if self.check_move_legal(from_, move_up_right_one, color) is not False:
|
|
legal_moves.append((from_, move_up_right_one))
|
|
return legal_moves
|
|
|
|
def get_possible_moves(
|
|
self, color: str
|
|
) -> tuple[list[tuple[tuple[int, int], tuple[int, int]]], bool]:
|
|
"""Return all possible moves for a given piece color."""
|
|
legal_moves = []
|
|
captures = []
|
|
if color == "white":
|
|
for white_position in self.white_positions:
|
|
legal_moves += self.get_possible_moves_non_capture(
|
|
(white_position[0], white_position[1]), color
|
|
)
|
|
captures += self.get_possible_moves_capture(
|
|
(white_position[0], white_position[1]), color
|
|
)
|
|
elif color == "black":
|
|
for black_position in self.black_positions:
|
|
legal_moves += self.get_possible_moves_non_capture(
|
|
(black_position[0], black_position[1]), color
|
|
)
|
|
captures += self.get_possible_moves_capture(
|
|
(black_position[0], black_position[1]), color
|
|
)
|
|
|
|
if len(captures) > 0:
|
|
return (captures, True)
|
|
return (legal_moves + captures, False)
|
|
|
|
def _search_branch(
|
|
self,
|
|
depth: int,
|
|
alpha_beta: tuple[float, float],
|
|
color: str,
|
|
current_color: str,
|
|
*,
|
|
maximising: bool,
|
|
) -> tuple[float, tuple[tuple[int, int], tuple[int, int]] | None]:
|
|
"""One half of the alpha-beta search.
|
|
|
|
The maximising and minimising halves were byte-for-byte mirror images
|
|
of each other, which is what made alpha_beta too complex to pass C901.
|
|
"""
|
|
alpha, beta = alpha_beta
|
|
opposite_color = "white" if current_color == "black" else "black"
|
|
best_eval = float("-inf") if maximising else float("inf")
|
|
best_move = None
|
|
|
|
for move in self.get_possible_moves(current_color)[0]:
|
|
new_state = copy.deepcopy(self)
|
|
new_state.make_move(*move, current_color)
|
|
eval_, _ = new_state.alpha_beta(
|
|
depth - 1, alpha_beta, color, opposite_color
|
|
)
|
|
|
|
if (eval_ > best_eval) if maximising else (eval_ < best_eval):
|
|
best_eval = eval_
|
|
best_move = move
|
|
|
|
if maximising:
|
|
alpha = max(alpha, eval_)
|
|
else:
|
|
beta = min(beta, eval_)
|
|
|
|
if alpha >= beta:
|
|
break
|
|
|
|
return best_eval, best_move
|
|
|
|
def alpha_beta(
|
|
self,
|
|
depth: int,
|
|
alpha_beta: tuple[float, float],
|
|
color: str,
|
|
current_color: str | None = None,
|
|
) -> tuple[float, tuple[tuple[int, int], tuple[int, int]] | None]:
|
|
"""Search with alpha-beta pruning; return the best move and its score."""
|
|
if current_color is None:
|
|
current_color = color
|
|
if depth == 0:
|
|
return self.evaluate(color), None
|
|
return self._search_branch(
|
|
depth,
|
|
alpha_beta,
|
|
color,
|
|
current_color,
|
|
maximising=current_color == color,
|
|
)
|
|
|
|
def evaluate(self, color: str) -> float:
|
|
"""Score the board from the given colour's point of view."""
|
|
white_score = 0
|
|
black_score = 0
|
|
|
|
for white_position in self.white_positions:
|
|
if white_position[2]:
|
|
white_score += 10
|
|
else:
|
|
white_score += 5
|
|
|
|
for black_position in self.black_positions:
|
|
if black_position[2]:
|
|
black_score += 10
|
|
else:
|
|
black_score += 5
|
|
|
|
return (
|
|
white_score - black_score if color == "white" else black_score - white_score
|
|
)
|
|
|
|
def input_to_coordinates(self, user_input: str) -> tuple[int, int]:
|
|
"""Change input from a1 form to tuple form."""
|
|
pos_x = ord(user_input[0]) - ord("a")
|
|
pos_y = int(user_input[1::])
|
|
return pos_x, pos_y
|
|
|
|
def handle_player_move(self, color: str) -> None:
|
|
"""Prompt player to move, validate their input and make move."""
|
|
has_moved = False
|
|
possible_moves = self.get_possible_moves(color)[0]
|
|
while not has_moved:
|
|
user_input = input(
|
|
f"You are {color}. How do you want to move? (format: d6 e5)\n"
|
|
)
|
|
regex = r"^[a-z]\d+\s[a-z]\d+$"
|
|
match = re.search(regex, user_input)
|
|
if not match:
|
|
logger.info("Invalid input, try again")
|
|
continue
|
|
[move_from, move_to] = user_input.split(" ")
|
|
from_coordinates = self.input_to_coordinates(move_from)
|
|
to_coordinates = self.input_to_coordinates(move_to)
|
|
|
|
if (from_coordinates, to_coordinates) not in possible_moves:
|
|
legal_no_captures = self.check_move_legal(
|
|
from_coordinates, to_coordinates, color, give_feedback=True
|
|
)
|
|
if legal_no_captures:
|
|
logger.info("Invalid move! You can capture a piece")
|
|
continue
|
|
|
|
has_moved = self.make_move(from_coordinates, to_coordinates, color)
|
|
self.print_board(rotate=color == "white")
|
|
|
|
def start_game(self, player_color: str = "black", algorithm_depth: int = 5) -> None:
|
|
"""Start the main loop of the game."""
|
|
if player_color not in ("black", "white"):
|
|
logger.info("Invalid color! Color can be black or white")
|
|
return
|
|
ai_color = "black" if player_color == "white" else "white"
|
|
|
|
game.print_board(rotate=player_color == "white")
|
|
if player_color == "white":
|
|
game.handle_player_move("white")
|
|
while True:
|
|
ai_turn = True
|
|
while ai_turn:
|
|
possible_moves_ai = game.get_possible_moves(ai_color)
|
|
if len(possible_moves_ai[0]) == 0:
|
|
logger.info("Game over, %s wins", player_color)
|
|
return
|
|
_, ai_move = game.alpha_beta(algorithm_depth, (5, 10), ai_color)
|
|
if ai_move is None:
|
|
logger.info("Game over, %s wins", player_color)
|
|
return
|
|
game.make_move(ai_move[0], ai_move[1], ai_color)
|
|
logger.info(
|
|
"AI's move: %s%s %s%s",
|
|
chr(ord("a") + ai_move[0][0]),
|
|
ai_move[0][1],
|
|
chr(ord("a") + ai_move[1][0]),
|
|
ai_move[1][1],
|
|
)
|
|
game.print_board(rotate=player_color == "white")
|
|
ai_turn = game.get_possible_moves(ai_color)[1] and possible_moves_ai[1]
|
|
player_turn = True
|
|
while player_turn:
|
|
possible_moves_player = game.get_possible_moves(player_color)
|
|
if len(possible_moves_player[0]) == 0:
|
|
logger.info("Game over, %s wins", ai_color)
|
|
return
|
|
game.handle_player_move(player_color)
|
|
player_turn = (
|
|
game.get_possible_moves(player_color)[1]
|
|
and possible_moves_player[1]
|
|
)
|
|
|
|
def ai_turn(
|
|
self,
|
|
ai_color: str,
|
|
algorithm_depth: int,
|
|
possible_moves_ai: list[object],
|
|
*,
|
|
print_info: bool = True,
|
|
) -> bool:
|
|
"""Calculate ai move and makes it."""
|
|
if len(possible_moves_ai) == 0:
|
|
if print_info:
|
|
logger.info("Game over, %s loses", ai_color)
|
|
return True
|
|
_, ai_move = game.alpha_beta(algorithm_depth, (5, 10), ai_color)
|
|
if ai_move is None:
|
|
ai_move = possible_moves_ai[0]
|
|
game.make_move(ai_move[0], ai_move[1], ai_color)
|
|
if print_info:
|
|
logger.info(
|
|
"AI's move: %s%s %s%s",
|
|
chr(ord("a") + ai_move[0][0]),
|
|
ai_move[0][1],
|
|
chr(ord("a") + ai_move[1][0]),
|
|
ai_move[1][1],
|
|
)
|
|
game.print_board(rotate=True)
|
|
return False
|
|
|
|
def auto_game(self, white_depth: int, black_depth: int) -> str:
|
|
"""Auto game mode between two bots."""
|
|
game_turn = 0
|
|
max_turns = 250
|
|
while game_turn < max_turns:
|
|
bot_white_turn = True
|
|
while bot_white_turn:
|
|
possible_moves_ai = game.get_possible_moves("white")
|
|
if self.ai_turn(
|
|
"white", white_depth, possible_moves_ai[0], print_info=False
|
|
):
|
|
return "white"
|
|
bot_white_turn = (
|
|
game.get_possible_moves("white")[1] and possible_moves_ai[1]
|
|
)
|
|
bot_black_turn = True
|
|
while bot_black_turn:
|
|
possible_moves_ai = game.get_possible_moves("black")
|
|
if self.ai_turn(
|
|
"black", black_depth, possible_moves_ai[0], print_info=False
|
|
):
|
|
return "black"
|
|
bot_black_turn = (
|
|
game.get_possible_moves("black")[1] and possible_moves_ai[1]
|
|
)
|
|
game_turn += 1
|
|
if game_turn >= max_turns:
|
|
logger.info("Game ended after %s turns!", max_turns)
|
|
return ""
|
|
return ""
|
|
|
|
|
|
def auto_simulation(white_depth: int, black_depth: int, iterations: int) -> None:
|
|
"""Run iterations amount of simulations."""
|
|
logger.info(
|
|
"Running %s simulations with\n white depth = %s,black depth = %s",
|
|
iterations,
|
|
white_depth,
|
|
black_depth,
|
|
)
|
|
white_wins = 0
|
|
black_wins = 0
|
|
white_pieces_captured = 0
|
|
black_pieces_captured = 0
|
|
current_iteration = 0
|
|
while current_iteration < iterations:
|
|
result = game.auto_game(white_depth, black_depth)
|
|
if result == "white":
|
|
black_wins += 1
|
|
if result == "black":
|
|
white_wins += 1
|
|
if result == "":
|
|
break
|
|
white_pieces_captured += 16 - len(game.white_positions)
|
|
black_pieces_captured += 16 - len(game.black_positions)
|
|
current_iteration += 1
|
|
logger.info(
|
|
"White wins = %s, Black wins = %s,\n white pieces captured in "
|
|
"total = %s,\n black pieces captured in total = %s",
|
|
white_wins,
|
|
black_wins,
|
|
white_pieces_captured,
|
|
black_pieces_captured,
|
|
)
|
|
logger.info("")
|
|
|
|
|
|
def print_help() -> None:
|
|
"""Print help."""
|
|
logger.info(
|
|
"python main.py [algorithm_depth] - play the game against the bot as "
|
|
"black,\n if no algorithm depth is specified the default (5) will "
|
|
"be set\n\npython main.py -h --help print this prompt\npython main.py -t "
|
|
"--test [max_white_depth] [max_black_depth] non interactive\n(does not "
|
|
"print moves) for testing how different bot depth play against eachother,"
|
|
"\nif depths are not provided default value of 5 is set\ncompares "
|
|
"heuristic speed and path length\npython main.py -w --white "
|
|
"[algorithm_depth] play as white pieces,\nif no algorithm depth is "
|
|
"specified the default (5) will be set\npython main.py -b --black "
|
|
"[algorithm_depth] play as black pieces,\nif no algorithm depth is "
|
|
"specified the default (5) will be set\n"
|
|
)
|
|
|
|
|
|
def default(color: str = "black", algorithm_depth: int = 5) -> None:
|
|
"""Play one game against the bot, as black by default."""
|
|
game.start_game(color, algorithm_depth)
|
|
|
|
|
|
# Ran first in the code
|
|
if __name__ == "__main__":
|
|
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
|
game = Game(8)
|
|
if len(sys.argv) > 1:
|
|
if sys.argv[1] == "-h" or sys.argv[1] == "--help":
|
|
print_help()
|
|
sys.exit()
|
|
if sys.argv[1] == "-t" or sys.argv[1] == "--test":
|
|
MAX_WHITE_DEPTH = 4
|
|
MAX_BLACK_DEPTH = 4
|
|
if len(sys.argv) > ARGV_WITH_COLOUR:
|
|
MAX_WHITE_DEPTH = int(sys.argv[2])
|
|
if len(sys.argv) > ARGV_WITH_DEPTH:
|
|
MAX_BLACK_DEPTH = int(sys.argv[3])
|
|
for i in range(MAX_WHITE_DEPTH + 1):
|
|
for j in range(MAX_BLACK_DEPTH + 1):
|
|
game = Game(8)
|
|
auto_simulation(i, j, 10)
|
|
sys.exit()
|
|
if sys.argv[1] == "-w" or sys.argv[1] == "--white":
|
|
ALGORITHM_DEPTH = 5
|
|
if len(sys.argv) > ARGV_WITH_COLOUR:
|
|
ALGORITHM_DEPTH = int(sys.argv[2])
|
|
default("white", ALGORITHM_DEPTH)
|
|
sys.exit()
|
|
if sys.argv[1] == "-b" or sys.argv[1] == "--black":
|
|
ALGORITHM_DEPTH = 5
|
|
if len(sys.argv) > ARGV_WITH_COLOUR:
|
|
ALGORITHM_DEPTH = int(sys.argv[2])
|
|
default("black", ALGORITHM_DEPTH)
|
|
sys.exit()
|
|
if len(sys.argv) > 1:
|
|
default("black", int(sys.argv[1]))
|
|
default()
|