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

Add skeleton classes Chess and Move #160

Merged
merged 13 commits into from
Oct 9, 2023
137 changes: 137 additions & 0 deletions unitary/examples/quantum_chinese_chess/chess.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Copyright 2023 The Unitary Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from unitary.examples.quantum_chinese_chess.board import Board
from unitary.examples.quantum_chinese_chess.enums import Language
from unitary.examples.quantum_chinese_chess.move import Move, get_move_from_string

# List of accepable commands.
_HELP_TEXT = """
Each location on the board is represented by two characters [abcdefghi][0-9], i.e. from a0 to i9. You may input (s=source, t=target)
- s1t1 to do a slide move, e.g. "a1a4";
- s1^t1t2 to do a split move, e.g. "a1^b1a2";
- s1s2^t1 to do a merge move, e.g. "b1a2^a1";
Other commands:
- "exit" to quit
- "help": to see this message again
"""


class QuantumChineseChess:
"""A class that implements Quantum Chinese Chess using the unitary API."""

def __init__(self):
self.players_name = []
self.print_welcome()
self.board = Board.from_fen()
self.board.set_language(self.lang)
print(self.board)
self.player_quit = -1
self.current_player = self.board.current_player
self.debug_level = 3

def game_over(self) -> int:
"""
Checks if the game is over.
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Usually you put the first line with the triple quotes. This usually looks better in the IDE.

"""Checks if the game is over.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks Doug. It's fixed.

Currently the pytest fails with error:
E ModuleNotFoundError: No module named 'mock'

, while locally I installed package mock and all tests pass. How may I install this module in the online repository?

Output:
madcpf marked this conversation as resolved.
Show resolved Hide resolved
-1: game continues
0: player 0 wins
1: player 1 wins
2: draw
madcpf marked this conversation as resolved.
Show resolved Hide resolved
"""
# The other player wins if the current player quits.
if self.player_quit > -1:
return 1 - self.player_quit
return -1
# TODO(): add the following checks
# - The current player wins if general is captured in the current move.
# - The other player wins if the flying general rule is satisfied, i.e. there is no piece
# (after measurement) between two generals.
# - If player 0 made N repeatd back-and_forth moves in a row.

def get_move(self) -> Move:
"""Check if the player wants to exit or needs help message. Otherwise parse and return the move."""
input_str = input(
f"\nIt is {self.players_name[self.current_player]}'s turn to move: "
)
if input_str.lower() == "help":
print(_HELP_TEXT)
raise ValueError("")
if input_str.lower() == "exit":
self.player_quit = self.current_player
raise ValueError("Exiting.")
try:
move = get_move_from_string(input_str.lower(), self.board)
return move
except ValueError as e:
madcpf marked this conversation as resolved.
Show resolved Hide resolved
raise e

def play(self) -> None:
"""The loop where each player takes turn to play."""
while True:
try:
move = self.get_move()
print(move.to_str(self.debug_level))
# TODO(): apply the move.
print(self.board)
except ValueError as e:
print(e)
# Continue if the player does not quit.
if self.player_quit == -1:
print("\nPlease re-enter your move.")
continue
# Check if the game is over.
game_over = self.game_over()
# If the game continues, switch the player.
if game_over == -1:
self.current_player = 1 - self.current_player
self.board.current_player = self.current_player
continue
elif game_over == 0:
print(f"{self.players_name[0]} wins! Game is over.")
elif game_over == 1:
print(f"{self.players_name[1]} wins! Game is over.")
elif game_over == 2:
print("Draw! Game is over.")
break

def print_welcome(self) -> None:
"""
Prints the welcome message. Gets board language and players' name.
"""
welcome_message = """
madcpf marked this conversation as resolved.
Show resolved Hide resolved
Welcome to Quantum Chinese Chess!
"""
print(welcome_message)
print(_HELP_TEXT)
# TODO(): add whole set of Chinese interface support.
lang = input(
"Switch to Chinese board characters? (y/n) (default to be English) "
)
if lang.lower() == "y":
self.lang = Language.ZH
else:
self.lang = Language.EN
name_0 = input("Player 0's name (default to be Player_0): ")
self.players_name.append("Player_0" if len(name_0) == 0 else name_0)
name_1 = input("Player 1's name (default to be Player_1): ")
self.players_name.append("Player_1" if len(name_1) == 0 else name_1)


def main():
game = QuantumChineseChess()
game.play()


if __name__ == "__main__":
main()
43 changes: 43 additions & 0 deletions unitary/examples/quantum_chinese_chess/chess_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2023 The Unitary Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
import io
import sys
from unitary.examples.quantum_chinese_chess.chess import QuantumChineseChess
from unitary.examples.quantum_chinese_chess.enums import Language


def test_game_init():
output = io.StringIO()
sys.stdout = output
with mock.patch("builtins.input", side_effect=["y", "Bob", "Ben"]):
game = QuantumChineseChess()
assert game.lang == Language.ZH
assert game.players_name == ["Bob", "Ben"]
assert game.current_player == 0
assert "Welcome" in output.getvalue()
sys.stdout = sys.__stdout__


def test_game_invalid_move():
output = io.StringIO()
sys.stdout = output
with mock.patch("builtins.input", side_effect=["y", "Bob", "Ben", "a1n1", "exit"]):
game = QuantumChineseChess()
game.play()
assert (
"Invalid location string. Make sure they are from a0 to i9."
in output.getvalue()
)
sys.stdout = sys.__stdout__
6 changes: 3 additions & 3 deletions unitary/examples/quantum_chinese_chess/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ class MoveVariant(enum.Enum):


class Color(enum.Enum):
NA = 0
RED = 1
BLACK = 2
NA = -1
madcpf marked this conversation as resolved.
Show resolved Hide resolved
RED = 0
BLACK = 1


class Type(enum.Enum):
Expand Down
177 changes: 177 additions & 0 deletions unitary/examples/quantum_chinese_chess/move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Copyright 2023 The Unitary Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional, List, Tuple
from unitary.alpha.quantum_effect import QuantumEffect
from unitary.examples.quantum_chinese_chess.board import Board
from unitary.examples.quantum_chinese_chess.enums import MoveType, MoveVariant, Type


def parse_input_string(str_to_parse: str) -> Tuple[List[str], List[str]]:
"""Check if the input string could be turned into a valid move.
Returns the sources and targets if it is valid.
The input needs to be:
- s1t1 for slide/jump move; or
- s1^t1t2 for split moves; or
- s1s2^t1 for merge moves.
Examples:
'a1a2'
'b1^a3c3'
'a3b1^c3'
"""
sources = None
targets = None

if "^" in str_to_parse:
sources_str, targets_str = str_to_parse.split("^", maxsplit=1)
# The only two allowed cases here are s1^t1t2 and s1s2^t1.
if (
str_to_parse.count("^") > 1
or len(str_to_parse) != 7
or len(sources_str) not in [2, 4]
):
raise ValueError(f"Invalid sources/targets string {str_to_parse}.")
sources = [sources_str[i : i + 2] for i in range(0, len(sources_str), 2)]
targets = [targets_str[i : i + 2] for i in range(0, len(targets_str), 2)]
if len(sources) == 2:
if sources[0] == sources[1]:
raise ValueError("Two sources should not be the same.")
elif targets[0] == targets[1]:
raise ValueError("Two targets should not be the same.")
else:
# The only allowed case here is s1t1.
if len(str_to_parse) != 4:
raise ValueError(f"Invalid sources/targets string {str_to_parse}.")
sources = [str_to_parse[0:2]]
targets = [str_to_parse[2:4]]
if sources[0] == targets[0]:
raise ValueError("Source and target should not be the same.")

# Make sure all the locations are valid.
for location in sources + targets:
if location[0].lower() not in "abcdefghi" or not location[1].isdigit():
raise ValueError(
f"Invalid location string. Make sure they are from a0 to i9."
)
return sources, targets


def get_move_from_string(str_to_parse: str, board: Board) -> "Move":
"""Check if the input string is valid. If it is, determine the move type and variant and return the move."""
try:
sources, targets = parse_input_string(str_to_parse)
except ValueError as e:
raise e
# Additional checks based on the current board.
for source in sources:
if board.board[source].type_ == Type.EMPTY:
raise ValueError("Could not move empty piece.")
if board.board[source].color.value != board.current_player:
raise ValueError("Could not move the other player's piece.")
# TODO(): add analysis to determine move type and variant.
move_type = MoveType.UNSPECIFIED_STANDARD
move_variant = MoveVariant.UNSPECIFIED
return Move(
sources[0],
targets[0],
board=board,
move_type=move_type,
move_variant=move_variant,
)


class Move(QuantumEffect):
"""The base class of all chess moves."""

def __init__(
self,
source: str,
target: str,
board: Board,
source2: Optional[str] = None,
target2: Optional[str] = None,
move_type: Optional[MoveType] = None,
move_variant: Optional[MoveVariant] = None,
):
self.source = source
self.source2 = source2
self.target = target
self.target2 = target2
self.move_type = move_type
self.move_variant = move_variant
self.board = board

def __eq__(self, other):
if isinstance(other, Move):
return (
self.source == other.source
and self.source2 == other.source2
and self.target == other.target
and self.target2 == other.target2
and self.move_type == other.move_type
and self.move_variant == other.move_variant
)
return False

def _verify_objects(self, *objects):
# TODO(): add checks that apply to all move types
return

def effect(self, *objects):
# TODO(): add effects according to move_type and move_variant
return

def is_split_move(self) -> bool:
return self.target2 is not None

def is_merge_move(self) -> bool:
return self.source2 is not None

def to_str(self, verbose_level: int = 1) -> str:
"""
Constructs the string representation of the move.
According to the value of verbose_level:
- 1: only returns the move source(s) and target(s);
- 2: additionally returns the move type and variant;
- 3: additionally returns the source(s) and target(s) piece type and color.
"""
if verbose_level < 1:
return ""

if self.is_split_move():
move_str = [self.source + "^" + self.target + str(self.target2)]
elif self.is_merge_move():
move_str = [self.source + str(self.source2) + "^" + self.target]
else:
move_str = [self.source + self.target]

if verbose_level > 1:
move_str.append(self.move_type.name)
move_str.append(self.move_variant.name)

if verbose_level > 2:
source = self.board.board[self.source]
target = self.board.board[self.target]
move_str.append(
source.color.name
+ "_"
+ source.type_.name
+ "->"
+ target.color.name
+ "_"
+ target.type_.name
)
return ":".join(move_str)

def __str__(self):
return self.to_str()
Loading
Loading