Skip to content

Commit

Permalink
initial chess and move
Browse files Browse the repository at this point in the history
  • Loading branch information
Pengfei Chen committed Oct 5, 2023
1 parent fb6db0b commit bb7df6e
Show file tree
Hide file tree
Showing 3 changed files with 319 additions and 0 deletions.
132 changes: 132 additions & 0 deletions unitary/examples/quantum_chinese_chess/chess.py
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()
31 changes: 31 additions & 0 deletions unitary/examples/quantum_chinese_chess/chess_test.py
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()

156 changes: 156 additions & 0 deletions unitary/examples/quantum_chinese_chess/move.py
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()

0 comments on commit bb7df6e

Please sign in to comment.