-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Pengfei Chen
committed
Oct 5, 2023
1 parent
fb6db0b
commit bb7df6e
Showing
3 changed files
with
319 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
# 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. | ||
Output: | ||
-1: game continues | ||
0: player 0 wins | ||
1: player 1 wins | ||
2: draw | ||
""" | ||
# 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: | ||
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("Existing.") | ||
try: | ||
move = get_move_from_string(input_str.lower(), self.board) | ||
return move | ||
except ValueError as e: | ||
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. | ||
match self.game_over(): | ||
# If the game continues, switch the player. | ||
case -1: | ||
self.current_player = 1 - self.current_player | ||
continue | ||
case 0: | ||
print(f'{self.players_name[0]} wins! Game is over.') | ||
break; | ||
case 1: | ||
print(f'{self.players_name[1]} wins! Game is over.') | ||
break; | ||
case 2: | ||
print('Draw! Game is over.') | ||
|
||
def print_welcome(self) -> None: | ||
""" | ||
Prints the welcome message. Gets board language and players' name. | ||
""" | ||
welcome_message = """ | ||
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# 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 pytest | ||
from unitary.examples.quantum_chinese_chess.chess import QuantumChineseChess | ||
from unitary.examples.quantum_chinese_chess.enums import Language | ||
|
||
def test_game_init(): | ||
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 | ||
|
||
def test_game_false_input(): | ||
with mock.patch('builtins.input', side_effect=["y", "Bob", "Ben", "a1n1", "exit"]): | ||
with pytest.raises(ValueError, match = "Invalid location string. Make sure they are from a0 to i9."): | ||
game = QuantumChineseChess() | ||
game.play() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
# 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 | ||
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 | ||
) | ||
|
||
|
||
|
||
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 | ||
# 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): | ||
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 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() |