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
193 changes: 193 additions & 0 deletions unitary/examples/quantum_chinese_chess/chess.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# 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 Tuple, List
from unitary.examples.quantum_chinese_chess.board import Board
from unitary.examples.quantum_chinese_chess.enums import Language, GameState, Type
from unitary.examples.quantum_chinese_chess.move import Move

# 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
"""

_WELCOME_MESSAGE = """
Welcome to Quantum Chinese Chess!
"""


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.game_state = GameState.CONTINUES
self.current_player = self.board.current_player
self.debug_level = 3

def game_over(self) -> None:
"""Checks if the game is over, and update self.game_state accordingly."""
if self.game_state != GameState.CONTINUES:
return
return
# 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.

@staticmethod
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 apply_move(self, str_to_parse: str) -> None:
"""Check if the input string is valid. If it is, determine the move type and variant and return the move."""
try:
sources, targets = self.parse_input_string(str_to_parse)
except ValueError as e:
raise e
# Additional checks based on the current board.
for source in sources:
if self.board.board[source].type_ == Type.EMPTY:
raise ValueError("Could not move empty piece.")
if self.board.board[source].color.value != self.board.current_player:
raise ValueError("Could not move the other player's piece.")
# TODO(): add analysis to determine move type and variant.

def next_move(self) -> bool:
"""Check if the player wants to exit or needs help message. Otherwise parse and apply the move.
Returns True if the move was made, otherwise returns False.
"""
input_str = input(
f"\nIt is {self.players_name[self.current_player]}'s turn to move: "
)
if input_str.lower() == "help":
print(_HELP_TEXT)
elif input_str.lower() == "exit":
# The other player wins if the current player quits.
self.game_state = GameState(1 - self.current_player)
print("Exiting.")
else:
try:
# The move is success if no ValueError is raised.
self.apply_move(input_str.lower())
return True
except ValueError as e:
print(e)
return False

def play(self) -> None:
"""The loop where each player takes turn to play."""
while True:
move_success = self.next_move()
print(self.board)
if not move_success:
# Continue if the player does not quit.
if self.game_state == GameState.CONTINUES:
print("\nPlease re-enter your move.")
continue
# Check if the game is over.
self.game_over()
# If the game continues, switch the player.
if self.game_state == GameState.CONTINUES:
self.current_player = 1 - self.current_player
self.board.current_player = self.current_player
continue
elif self.game_state == GameState.RED_WINS:
print(f"{self.players_name[0]} wins! Game is over.")
elif self.game_state == GameState.BLACK_WINS:
print(f"{self.players_name[1]} wins! Game is over.")
elif self.game_state == GameState.DRAW:
print("Draw! Game is over.")
break

def print_welcome(self) -> None:
"""Prints the welcome message. Gets board language and players' name."""
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()
84 changes: 84 additions & 0 deletions unitary/examples/quantum_chinese_chess/chess_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 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 pytest
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(monkeypatch):
inputs = iter(["y", "Bob", "Ben"])
monkeypatch.setattr("builtins.input", lambda _: next(inputs))
output = io.StringIO()
sys.stdout = output
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_parse_input_string_success(monkeypatch):
inputs = iter(["y", "Bob", "Ben"])
monkeypatch.setattr("builtins.input", lambda _: next(inputs))
game = QuantumChineseChess()
assert game.parse_input_string("a1b1") == (["a1"], ["b1"])
assert game.parse_input_string("a1b1^c2") == (["a1", "b1"], ["c2"])
assert game.parse_input_string("a1^b1c2") == (["a1"], ["b1", "c2"])


def test_parse_input_string_fail(monkeypatch):
inputs = iter(["y", "Bob", "Ben"])
monkeypatch.setattr("builtins.input", lambda _: next(inputs))
game = QuantumChineseChess()
with pytest.raises(ValueError, match="Invalid sources/targets string "):
game.parse_input_string("a1^b1")
with pytest.raises(ValueError, match="Invalid sources/targets string "):
game.parse_input_string("a^1b1c2")
with pytest.raises(ValueError, match="Two sources should not be the same."):
game.parse_input_string("a1a1^c2")
with pytest.raises(ValueError, match="Two targets should not be the same."):
game.parse_input_string("a1^c2c2")
with pytest.raises(ValueError, match="Invalid sources/targets string "):
game.parse_input_string("a1b")
with pytest.raises(ValueError, match="Source and target should not be the same."):
game.parse_input_string("a1a1")
with pytest.raises(ValueError, match="Invalid location string."):
game.parse_input_string("a1n1")


def test_apply_move_fail(monkeypatch):
inputs = iter(["y", "Bob", "Ben"])
monkeypatch.setattr("builtins.input", lambda _: next(inputs))
game = QuantumChineseChess()
with pytest.raises(ValueError, match="Could not move empty piece."):
game.apply_move("a1b1")
with pytest.raises(ValueError, match="Could not move the other player's piece."):
game.apply_move("a0b1")


def test_game_invalid_move(monkeypatch):
output = io.StringIO()
sys.stdout = output
inputs = iter(["y", "Bob", "Ben", "a1n1", "exit"])
monkeypatch.setattr("builtins.input", lambda _: next(inputs))
game = QuantumChineseChess()
game.play()
assert (
"Invalid location string. Make sure they are from a0 to i9."
in output.getvalue()
)
sys.stdout = sys.__stdout__
33 changes: 28 additions & 5 deletions unitary/examples/quantum_chinese_chess/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,33 @@


class Language(enum.Enum):
"""Currently we support the following two ways to print the board.
TODO(): support Chinese langurage instructions.
"""

EN = 0 # English
ZH = 1 # Chinese


class SquareState(enum.Enum):
"""Defines two possible initial state of each piece of the board."""

EMPTY = 0
OCCUPIED = 1


class GameState(enum.Enum):
madcpf marked this conversation as resolved.
Show resolved Hide resolved
"""Defines all possible outcomes of the game, plus the continue state."""

CONTINUES = -1
RED_WINS = 0
BLACK_WINS = 1
DRAW = 2


class MoveType(enum.Enum):
"""Each valid move will be classfied into one of the following MoveTypes."""

NULL_TYPE = 0
UNSPECIFIED_STANDARD = 1
JUMP = 2
Expand All @@ -41,21 +58,26 @@ class MoveType(enum.Enum):


class MoveVariant(enum.Enum):
"""Each valid move will be classfied into one of the following MoveVariat, in addition to
the MoveType above.
"""

UNSPECIFIED = 0
BASIC = 1
EXCLUDED = 2
CAPTURE = 3


class Color(enum.Enum):
NA = 0
RED = 1
BLACK = 2
"""Empty pieces are associated with Color=NA. Other pieces should be either RED or BLACK."""

NA = -1
madcpf marked this conversation as resolved.
Show resolved Hide resolved
RED = 0
BLACK = 1


class Type(enum.Enum):
"""
The names are from FEN for Xiangqi.
"""The names are from FEN for Xiangqi.
The four values are symbols corresponding to
- English red
- English black
Expand Down Expand Up @@ -87,6 +109,7 @@ def type_of(c: str) -> Optional["Type"]:

@staticmethod
def symbol(type_: "Type", color: Color, lang: Language = Language.EN) -> str:
"""Returns symbol of the given piece according to its color and desired language."""
if type_ == Type.EMPTY:
return "."
if lang == Language.EN: # Return English symbols
Expand Down
Loading
Loading