-
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
Add skeleton classes Chess and Move #160
Merged
Merged
Changes from 7 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
bb7df6e
initial chess and move
bdf8746
update test
madcpf 68be945
add test
madcpf e358ef5
Merge branch 'quantumlib:main' into board
madcpf 89b6c6e
add more checks
madcpf 1b8db7b
Merge branch 'board' of https://github.com/madcpf/unitary into board
madcpf d5d363d
add doc str
madcpf a88d525
update
madcpf abf1025
update chess
madcpf d8ffd09
update move
madcpf 830c819
address comments
madcpf 8ac36fc
update test without using mock
madcpf 66737ef
reformat
madcpf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,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. | ||
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() |
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,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__ |
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
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,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() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
nit: Usually you put the first line with the triple quotes. This usually looks better in the IDE.
"""Checks if the game is over.
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.
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?