-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding quantum checkers #189
base: main
Are you sure you want to change the base?
Changes from all commits
95a7388
af99e75
7839fa2
23546f7
223ca87
a12c25a
617ab58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# Quantum cheqqers | ||
This game is created for the master thesis by Luuk van den Nouweland, university of Leiden. | ||
Supervisor: Evert van Nieuwenburg | ||
|
||
# Rules | ||
In this game, multiple rules are implemented. | ||
1. Classical: Normal checkers | ||
2. Superpositions: Checkers where pieces can exist in superposition | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be good to explain the rules a bit more. I don't understand what rules 3 and 4 mean in the context of checkers. Does rule 2 mean that you can move to two possible squares? |
||
3. Simple entanglement: Checkers where pieces can be entangled with each other, but entanglement with pieces that are entangled is not possible. IS CURRENTLY BUGGED. MAY CRASH | ||
4. Entanglement: Checkers with unlimited entanglement | ||
|
||
# How to run | ||
Copy this repository and run the main file: `python3 .\main.py` | ||
Flags: If you want to play with GUI (experimental) add `--GUI=True`: `python3 .\main.py --GUI=True` | ||
|
||
# Full implementation | ||
For the full implementation with a Monte Carlo Tree search agent, see https://github.com/LuukvandenNouweland/quantum_checkers |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Added for unit testing |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,311 @@ | ||
from enums import CheckersResult, CheckersRules, Colors | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add copyright notice to the top of the file. See any other file in the repository for an example. |
||
from typing import List | ||
from copy import deepcopy | ||
|
||
# GLOBAL GAME SETTINGS | ||
forced_take = True | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Global constants should be all caps `FORCED_TAKE'. Also, I would explain what this option does. |
||
|
||
|
||
class Move: | ||
def __init__(self, start_row, start_col, end_row, end_col) -> None: | ||
self.start_row = start_row | ||
self.start_col = start_col | ||
self.end_row = end_row | ||
self.end_col = end_col | ||
|
||
|
||
class Checkers: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a docstring. |
||
def __init__(self, num_rows=5, num_cols=5, num_rows_pieces=1) -> None: | ||
self.board = Board(num_rows, num_cols, num_rows_pieces) | ||
pass | ||
|
||
def result(self): | ||
""" | ||
returns: | ||
UNFINISHED = 0 | ||
White wins = 1 | ||
Black wins = 2 | ||
DRAW = 3 | ||
BOTH_WIN = 4 | ||
""" | ||
if ( | ||
len(self.board.calculate_all_possible_moves(Colors.WHITE)) == 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider setting these as variables so that you don't recalculate all the moves twice: |
||
and len(self.board.calculate_all_possible_moves(Colors.BLACK)) == 0 | ||
): | ||
return CheckersResult.DRAW | ||
elif len(self.board.calculate_all_possible_moves(Colors.WHITE)) == 0: | ||
return CheckersResult.WHITE_WINS | ||
elif len(self.board.calculate_all_possible_moves(Colors.BLACK)) == 0: | ||
return CheckersResult.BLACK_WINS | ||
else: | ||
return CheckersResult.UNFINISHED | ||
|
||
def do_move(self, move: Move): | ||
self.board.move_piece( | ||
move.start_row, move.start_col, move.end_row, move.end_col | ||
) | ||
print(move.start_row, move.start_col, move.end_row, move.end_col) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider splitting the printing out into a different function. |
||
|
||
|
||
class Square: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add docstring |
||
def __init__(self) -> None: | ||
self.occupant = None | ||
|
||
|
||
class Board: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add docstring for the class and the functions. |
||
def __init__(self, num_rows, num_cols, num_rows_pieces) -> None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should these arguments have defaults for a standard checker board? |
||
self.num_rows = num_rows | ||
self.num_cols = num_cols | ||
|
||
# Initalize empty board | ||
# test = Square() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove commented code. |
||
self.board_matrix = [ | ||
[Square() for x in range(self.num_cols)] for x in range(self.num_rows) | ||
] | ||
if num_rows_pieces * 2 >= num_rows: | ||
print( | ||
f"Too many rows ({num_rows_pieces}) filled with pieces. Decrease this number for this size of board. [{num_rows}]x[{num_cols}]" | ||
) | ||
exit() | ||
|
||
# Initialize pieces on correct squares | ||
for y in range(num_rows_pieces): | ||
for x in range(self.num_cols): | ||
if y % 2 == 0 and x % 2 == 0: | ||
self.board_matrix[y][x].occupant = Piece(Colors.BLACK) | ||
self.board_matrix[self.num_rows - 1 - y][x].occupant = Piece( | ||
Colors.WHITE | ||
) | ||
|
||
elif y % 2 != 0 and x % 2 != 0: | ||
self.board_matrix[y][x].occupant = Piece(Colors.BLACK) | ||
self.board_matrix[self.num_rows - 1 - y][x].occupant = Piece( | ||
Colors.WHITE | ||
) | ||
|
||
# self.board_matrix[1][3].occupant = Piece(Colors.WHITE) | ||
# Test to see if king works | ||
# self.board_matrix[4][4].occupant = Piece(Colors.BLACK, king=True) | ||
# self.board_matrix[4][5].occupant = Piece(Colors.WHITE) | ||
# self.board_matrix[3][3].occupant = Piece(Colors.WHITE) | ||
# self.board_matrix[3][5].occupant = Piece(Colors.WHITE) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove commented code. |
||
|
||
def move_piece(self, from_row, from_col, to_row, to_col): | ||
""" | ||
Moves a piece from given row and column to given row and column. Changes it into a king if it reaches the end | ||
""" | ||
# self.board_matrix[to_row][to_col].occupant = deepcopy(self.board_matrix[from_row][from_col].occupant) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove commented code. |
||
self.board_matrix[to_row][to_col].occupant = Piece( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason you are recreating the object each time? |
||
self.board_matrix[from_row][from_col].occupant.color, | ||
self.board_matrix[from_row][from_col].occupant.king, | ||
) | ||
self.remove_piece(from_row, from_col) | ||
|
||
# If jumping over a piece, we need to remove it aswell | ||
# Jump from column 2 over 3 to 4 we add 3+(3-2) | ||
# Jump from column 3 over 2 to 1 we add 2+(2-3) | ||
# jump_row = i.end_row+(i.end_row-i.start_row) | ||
if abs(to_row - from_row) > 1: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume this does not handle "double jumps"? Should we verify that abs(to_row - from_row) == 2 and abs(to_col - from_col)==2? |
||
self.remove_piece( | ||
int((to_row + from_row) / 2), int((to_col + from_col) / 2) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Consider |
||
) | ||
|
||
# If it is not a king and reaches the end it needs to be kinged | ||
if self.board_matrix[to_row][to_col].occupant.king == False and ( | ||
to_row == 0 or to_row == self.num_rows - 1 | ||
): | ||
self.board_matrix[to_row][to_col].occupant.king = True | ||
return | ||
|
||
def remove_piece(self, row, col): | ||
self.board_matrix[row][col].occupant = None | ||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: unneeded return statement. |
||
|
||
def print_board(self): | ||
output = "\n" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional suggestion: consider using a list of strings that you append to, then do ''.join(output_list) at the end, in order to avoid multiple string concatenations. |
||
output += " |" | ||
for i in range(self.num_cols): | ||
output += f" {i} |" | ||
output += "\n" + "---|" * (self.num_cols + 1) | ||
output += "\n" | ||
for i in range(self.num_rows): | ||
output += f" {i} |" | ||
for j in range(self.num_cols): | ||
# print(f"{self.board_matrix[i][j].occupant.color}") | ||
if self.board_matrix[i][j].occupant != None: | ||
# print(self.board_matrix[i][j].occupant.color) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove commented code. |
||
if self.board_matrix[i][j].occupant.color == Colors.WHITE: | ||
if self.board_matrix[i][j].occupant.king == True: | ||
output += " W " | ||
else: | ||
output += " w " | ||
else: | ||
if self.board_matrix[i][j].occupant.king: | ||
output += " B " | ||
else: | ||
output += " b " | ||
# print(type(self.board_matrix[i][j].occupant)) | ||
# output += f" {self.board_matrix[i][j].occupant.color} " | ||
else: | ||
output += " " | ||
output += "|" | ||
output += "\n" + "---|" * (self.num_cols + 1) | ||
output += "\n" | ||
return output | ||
|
||
def calculate_all_possible_moves(self, color: Colors): | ||
legal_moves = [] | ||
legal_take_moves = [] | ||
# Loop over all squares, if there is a piece there check what moves are possible. | ||
# Moves will be | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move will be.... |
||
for i in range(self.num_rows): | ||
for j in range(self.num_cols): | ||
if ( | ||
self.board_matrix[i][j].occupant != None | ||
and self.board_matrix[i][j].occupant.color == color | ||
): | ||
# temp1, temp2 = self.calculate_legal_move(i, j, color) | ||
# legal_moves.extend(temp1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove commented code. |
||
# legal_take_moves.extend(temp2) | ||
temp_legal_moves, temp_take_moves = self.possible_moves(i, j) | ||
legal_moves.extend(temp_legal_moves) | ||
legal_take_moves.extend(temp_take_moves) | ||
if len(legal_take_moves) != 0 and forced_take: | ||
return legal_take_moves | ||
return legal_moves | ||
|
||
def on_board(self, row, col): | ||
""" | ||
Checks if given location is on the board on not. | ||
Returns true if [row][col] is on the board | ||
""" | ||
if row < 0 or row > self.num_rows - 1 or col < 0 or col > self.num_cols - 1: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can simplify this:
|
||
return False | ||
return True | ||
|
||
def possible_blind_moves(self, row, col): | ||
""" | ||
Returns for one piece the possible moves that pieces can move in without checking if that moves is on the board or if there is a piece in the way | ||
Returns empty list if there is no piece | ||
""" | ||
legal_moves = [] | ||
if self.board_matrix[row][col].occupant != None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should use |
||
if self.board_matrix[row][col].occupant.king == False: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
if ( | ||
self.board_matrix[row][col].occupant.color == Colors.WHITE | ||
): # White non king piece | ||
legal_moves.append(Move(row, col, row - 1, col - 1)) | ||
legal_moves.append(Move(row, col, row - 1, col + 1)) | ||
else: # Black non king piece | ||
legal_moves.append(Move(row, col, row + 1, col - 1)) | ||
legal_moves.append(Move(row, col, row + 1, col + 1)) | ||
else: # King piece can move in all directions | ||
legal_moves.append(Move(row, col, row - 1, col - 1)) | ||
legal_moves.append(Move(row, col, row - 1, col + 1)) | ||
legal_moves.append(Move(row, col, row + 1, col - 1)) | ||
legal_moves.append(Move(row, col, row + 1, col + 1)) | ||
return legal_moves | ||
|
||
def possible_moves(self, row, col): # For one | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "For one". What does this mean? I would remove this comment. |
||
""" | ||
For one piece, calculate all legal moves | ||
""" | ||
legal_moves = self.possible_blind_moves(row, col) | ||
new_legal_moves = [] # all possible moves | ||
take_moves = [] # Moves that take another piece | ||
for i in legal_moves: | ||
# For each move, check if the coordinates are on the board | ||
# If so, check if it is empty. If so, it is a legal move | ||
# If there is another piece, check if it is a different color than your own color | ||
# If so, check if one square further is empty | ||
# If so you can take a piece | ||
if self.on_board(i.end_row, i.end_col): # Coordinate is on board | ||
if self.board_matrix[i.end_row][i.end_col].occupant == None: # Empty | ||
# print(f"{i.end_row},{i.end_col}") | ||
new_legal_moves.append(i) | ||
elif ( | ||
self.board_matrix[i.end_row][i.end_col].occupant.color | ||
!= self.board_matrix[i.start_row][i.start_col].occupant.color | ||
): | ||
# Different color so we have to check if we can jump over | ||
# Jump from column 2 over 3 to 4 we add 3+(3-2) | ||
# Jump from column 3 over 2 to 1 we add 2+(2-3) | ||
jump_row = i.end_row + (i.end_row - i.start_row) | ||
jump_col = i.end_col + (i.end_col - i.start_col) | ||
if ( | ||
self.on_board(jump_row, jump_col) | ||
and self.board_matrix[jump_row][jump_col].occupant == None | ||
): # we can jump over. For readibility not in previous if statement | ||
new_legal_moves.append( | ||
Move(i.start_row, i.start_col, jump_row, jump_col) | ||
) | ||
take_moves.append( | ||
Move(i.start_row, i.start_col, jump_row, jump_col) | ||
) | ||
return new_legal_moves, take_moves | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add tests for this class and others in this file. |
||
|
||
|
||
class Piece: | ||
def __init__(self, color=None, king=False) -> None: | ||
self.color = color | ||
self.king = king | ||
|
||
|
||
class GameInterface: | ||
def __init__(self, game: Checkers) -> None: | ||
self.game = game | ||
self.player = Colors.WHITE | ||
|
||
def get_move(self): | ||
return input(f"Player {self.player.name} to move: ") | ||
|
||
def play(self): | ||
while self.game.result() == CheckersResult.UNFINISHED: | ||
self.print_board() | ||
legal_moves = self.print_legal_moves() | ||
move = self.get_move() | ||
try: | ||
move = int(move) | ||
except: | ||
print("Input has to be an integer!") | ||
continue | ||
if move > len(legal_moves) or move < 1: | ||
print(f"Input has to be an integer between 1 and {len(legal_moves)}!") | ||
continue | ||
self.game.do_move(legal_moves[move - 1]) | ||
|
||
self.player = Colors.BLACK if self.player == Colors.WHITE else Colors.WHITE | ||
|
||
def print_board(self): | ||
print(self.game.board.print_board()) | ||
|
||
def get_legal_moves(self): | ||
""" | ||
Returns list of legal moves | ||
""" | ||
return self.game.board.calculate_all_possible_moves(self.player) | ||
|
||
def print_legal_moves(self): | ||
""" | ||
Prints all legal moves and returns list of legal moves | ||
""" | ||
index = 1 | ||
print(f"Legal moves for player {self.player.name}:") | ||
legal_moves = self.game.board.calculate_all_possible_moves(self.player) | ||
for i in legal_moves: | ||
print( | ||
f"{index}. [{i.start_col}][{i.start_row}] to [{i.end_col}][{i.end_row}]" | ||
) | ||
index += 1 | ||
return legal_moves | ||
|
||
|
||
def main(): | ||
game = GameInterface(Checkers()) | ||
# game.print_board() | ||
# game.print_legal_moves() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove commented code. |
||
game.play() | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be a sub-heading "## Rules". Same for below. "# Rules" would be for the title.