Skip to content
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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions unitary/alpha/sparse_vector_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def _perform_measurement(self, qubits):
# abstract method of OperationTarget
def sample(self, qubits, repetitions, prng):
probs = abs(self._amplitudes) ** 2
probs = probs / np.sum(probs)
samples = prng.choice(self._states, size=repetitions, p=probs)
out = np.empty((repetitions, len(qubits)), dtype=np.uint8)
for j, q in enumerate(qubits):
Expand Down
17 changes: 17 additions & 0 deletions unitary/examples/quantum_checkers/README.md
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
Copy link
Collaborator

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.

In this game, multiple rules are implemented.
1. Classical: Normal checkers
2. Superpositions: Checkers where pieces can exist in superposition
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
1 change: 1 addition & 0 deletions unitary/examples/quantum_checkers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Added for unit testing
311 changes: 311 additions & 0 deletions unitary/examples/quantum_checkers/checkers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
from enums import CheckersResult, CheckersRules, Colors
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.
Consider also putting this as part of the Checkers class.



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:
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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:
i.e.
num_white_moves = len(self.board.calculate_all_possible_moves(Colors.WHITE))

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider splitting the printing out into a different function.



class Square:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add docstring

def __init__(self) -> None:
self.occupant = None


class Board:
Copy link
Collaborator

Choose a reason for hiding this comment

The 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:
Copy link
Collaborator

Choose a reason for hiding this comment

The 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()
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove commented code.

self.board_matrix[to_row][to_col].occupant = Piece(
Copy link
Collaborator

Choose a reason for hiding this comment

The 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:
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Consider to_row + from_row) // 2 instead.

)

# 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unneeded return statement.


def print_board(self):
output = "\n"
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The 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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can simplify this:

return row < self.num_rows and row>=0 and col < self.num_cols and col>=0

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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use is not None rather than '!= None' when comparing against the singleton None.

if self.board_matrix[row][col].occupant.king == False:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if not self.board_matrix[row][col].occupant.king

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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove commented code.

game.play()


if __name__ == "__main__":
main()
Binary file added unitary/examples/quantum_checkers/crown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading