diff --git a/unitary/examples/quantum_chinese_chess/board.py b/unitary/examples/quantum_chinese_chess/board.py index 4c824c55..109801ea 100644 --- a/unitary/examples/quantum_chinese_chess/board.py +++ b/unitary/examples/quantum_chinese_chess/board.py @@ -19,8 +19,10 @@ Color, Type, Language, + MoveVariant, ) from unitary.examples.quantum_chinese_chess.piece import Piece +from unitary.examples.quantum_chinese_chess.move import Jump # The default initial state of the game. @@ -183,7 +185,31 @@ def flying_general_check(self) -> bool: if len(quantum_pieces) == 0: # If there are no pieces between two KINGs, the check successes. Game ends. return True - # TODO(): add check when there are quantum pieces in between. + # When there are only quantum pieces in between, the check successes (and game ends) + # if all of those pieces turn out to be empty. + capture_ancilla = self.board._add_ancilla("flying_general_check") + control_objects = [self.board[path] for path in quantum_pieces] + conditions = [0] * len(control_objects) + alpha.quantum_if(*control_objects).equals(*conditions).apply(alpha.Flip())( + capture_ancilla + ) + could_capture = self.board.pop([capture_ancilla])[0] + if could_capture: + # Force measure all path pieces to be empty. + for path_piece in control_objects: + self.board.force_measurement(path_piece, 0) + path_piece.reset() + + # Let the general/king fly, i.e. the opposite king will capture the current king. + current_king = self.board[self.king_locations[self.current_player]] + oppsite_king = self.board[self.king_locations[1 - self.current_player]] + Jump(MoveVariant.CLASSICAL)(oppsite_king, current_king) + print("==== FLYING GENERAL ! ====") + return True + else: + # Note: we are leaving the path pieces unchanged in entangled state. + print("==== General not flies yet ! ====") + return False def sample(self, repetitions: int) -> List[int]: """Sample the current board by the given `repetitions`. diff --git a/unitary/examples/quantum_chinese_chess/board_test.py b/unitary/examples/quantum_chinese_chess/board_test.py index 220c5439..a8812228 100644 --- a/unitary/examples/quantum_chinese_chess/board_test.py +++ b/unitary/examples/quantum_chinese_chess/board_test.py @@ -20,6 +20,14 @@ ) from unitary.examples.quantum_chinese_chess.board import Board from unitary.examples.quantum_chinese_chess.piece import Piece +from unitary.examples.quantum_chinese_chess.test_utils import ( + locations_to_bitboard, + assert_samples_in, + assert_sample_distribution, + get_board_probability_distribution, + set_board, +) +from unitary import alpha def test_init_with_default_fen(): @@ -141,7 +149,7 @@ def test_path_pieces(): assert board.path_pieces("c8", "d6") == ([], ["c7"]) -def test_flying_general_check(): +def test_flying_general_check_classical_cases(): board = Board.from_fen() # If they are in different columns, the check fails. board.king_locations = ["d0", "e9"] @@ -155,3 +163,53 @@ def test_flying_general_check(): board.board["e3"].reset() board.board["e6"].reset() assert board.flying_general_check() == True + + +def test_flying_general_check_quantum_cases(): + # When there are quantum pieces in between. + board = set_board(["a3", "a4", "e0", "e9"]) + board.king_locations = ["e0", "e9"] + board.current_player = 0 # i.e. RED + world = board.board + alpha.PhasedSplit()(world["a3"], world["c3"], world["e3"]) + world["e3"].is_entangled = True + alpha.PhasedSplit()(world["a4"], world["c4"], world["e4"]) + world["e4"].is_entangled = True + + result = board.flying_general_check() + # We check the ancilla to learn whether the general/king flies or not. + captured = world.post_selection[world["ancilla_ancilla_flying_general_check_0_0"]] + if captured: + assert result + assert_samples_in(board, {locations_to_bitboard(["c3", "c4", "e0"]): 1.0}) + else: + assert not result + assert_sample_distribution( + board, + { + locations_to_bitboard(["e0", "e9", "e3", "e4"]): 1.0 / 3, + locations_to_bitboard(["e0", "e9", "e3", "c4"]): 1.0 / 3, + locations_to_bitboard(["e0", "e9", "c3", "e4"]): 1.0 / 3, + }, + ) + + +def test_flying_general_check_quantum_cases_not_captured(): + # When there are quantum pieces in between, but the capture cannot happen. + board = set_board(["a3", "e0", "e9"]) + board.king_locations = ["e0", "e9"] + board.current_player = 0 # i.e. RED + world = board.board + alpha.PhasedSplit()(world["a3"], world["e3"], world["e4"]) + world["e3"].is_entangled = True + world["e4"].is_entangled = True + + result = board.flying_general_check() + assert not result + assert_sample_distribution( + board, + { + locations_to_bitboard(["e0", "e9", "e3"]): 1.0 / 2, + locations_to_bitboard(["e0", "e9", "e4"]): 1.0 / 2, + }, + ) diff --git a/unitary/examples/quantum_chinese_chess/move.py b/unitary/examples/quantum_chinese_chess/move.py index 1b2cdd59..e82533fc 100644 --- a/unitary/examples/quantum_chinese_chess/move.py +++ b/unitary/examples/quantum_chinese_chess/move.py @@ -15,23 +15,21 @@ import cirq from unitary import alpha from unitary.alpha.quantum_effect import QuantumEffect -from unitary.examples.quantum_chinese_chess.board import Board from unitary.examples.quantum_chinese_chess.piece import Piece from unitary.examples.quantum_chinese_chess.enums import MoveType, MoveVariant, Type # TODO(): now the class is no longer the base class of all chess moves. Maybe convert this class # to a helper class to save each move (with its pop results) in a string form into move history. -class Move(QuantumEffect): +class Move: """The base class of all chess moves.""" def __init__( self, - source: str, - target: str, - board: Board, - source2: Optional[str] = None, - target2: Optional[str] = None, + source: Piece, + target: Piece, + source2: Optional[Piece] = None, + target2: Optional[Piece] = None, move_type: Optional[MoveType] = None, move_variant: Optional[MoveVariant] = None, ): @@ -41,28 +39,12 @@ def __init__( 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 self.to_str(3) == other.to_str(3) 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 @@ -80,27 +62,25 @@ def to_str(self, verbose_level: int = 1) -> str: return "" if self.is_split_move(): - move_str = [self.source + "^" + self.target + str(self.target2)] + move_str = [self.source.name + "^" + self.target.name + self.target2.name] elif self.is_merge_move(): - move_str = [self.source + str(self.source2) + "^" + self.target] + move_str = [self.source.name + self.source2.name + "^" + self.target.name] else: - move_str = [self.source + self.target] + move_str = [self.source.name + self.target.name] 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 + self.source.color.name + "_" - + source.type_.name + + self.source.type_.name + "->" - + target.color.name + + self.target.color.name + "_" - + target.type_.name + + self.target.type_.name ) return ":".join(move_str) diff --git a/unitary/examples/quantum_chinese_chess/move_test.py b/unitary/examples/quantum_chinese_chess/move_test.py index d6ae9a6d..8830f829 100644 --- a/unitary/examples/quantum_chinese_chess/move_test.py +++ b/unitary/examples/quantum_chinese_chess/move_test.py @@ -39,30 +39,31 @@ def test_move_eq(): board = Board.from_fen() + world = board.board move1 = Move( - "a1", - "b2", - board, - "c1", + world["a1"], + world["b2"], + world["c1"], move_type=MoveType.MERGE_JUMP, move_variant=MoveVariant.CAPTURE, ) move2 = Move( - "a1", - "b2", - board, - "c1", + world["a1"], + world["b2"], + world["c1"], move_type=MoveType.MERGE_JUMP, move_variant=MoveVariant.CAPTURE, ) move3 = Move( - "a1", "b2", board, move_type=MoveType.JUMP, move_variant=MoveVariant.CAPTURE + world["a1"], + world["b2"], + move_type=MoveType.JUMP, + move_variant=MoveVariant.CAPTURE, ) move4 = Move( - "a1", - "b2", - board, - "c1", + world["a1"], + world["b2"], + world["c1"], move_type=MoveType.MERGE_SLIDE, move_variant=MoveVariant.CAPTURE, ) @@ -75,11 +76,11 @@ def test_move_eq(): def test_move_type(): # TODO(): change to real senarios board = Board.from_fen() + world = board.board move1 = Move( - "a1", - "b2", - board, - "c1", + world["a1"], + world["b2"], + world["c1"], move_type=MoveType.MERGE_JUMP, move_variant=MoveVariant.CAPTURE, ) @@ -87,10 +88,9 @@ def test_move_type(): assert move1.is_merge_move() move2 = Move( - "a1", - "b2", - board, - target2="c1", + world["a1"], + world["b2"], + target2=world["c1"], move_type=MoveType.SPLIT_JUMP, move_variant=MoveVariant.BASIC, ) @@ -98,7 +98,10 @@ def test_move_type(): assert move2.is_merge_move() == False move3 = Move( - "a1", "b2", board, move_type=MoveType.SLIDE, move_variant=MoveVariant.CAPTURE + world["a1"], + world["b2"], + move_type=MoveType.SLIDE, + move_variant=MoveVariant.CAPTURE, ) assert move3.is_split_move() == False assert move3.is_merge_move() == False @@ -107,11 +110,11 @@ def test_move_type(): def test_to_str(): # TODO(): change to real scenarios board = Board.from_fen() + world = board.board move1 = Move( - "a0", - "a6", - board, - "c1", + world["a0"], + world["a6"], + world["c1"], move_type=MoveType.MERGE_JUMP, move_variant=MoveVariant.CAPTURE, ) @@ -121,10 +124,9 @@ def test_to_str(): assert move1.to_str(3) == "a0c1^a6:MERGE_JUMP:CAPTURE:RED_ROOK->BLACK_PAWN" move2 = Move( - "a0", - "b3", - board, - target2="c1", + world["a0"], + world["b3"], + target2=world["c1"], move_type=MoveType.SPLIT_JUMP, move_variant=MoveVariant.BASIC, ) @@ -134,7 +136,10 @@ def test_to_str(): assert move2.to_str(3) == "a0^b3c1:SPLIT_JUMP:BASIC:RED_ROOK->NA_EMPTY" move3 = Move( - "a0", "a6", board, move_type=MoveType.SLIDE, move_variant=MoveVariant.CAPTURE + world["a0"], + world["a6"], + move_type=MoveType.SLIDE, + move_variant=MoveVariant.CAPTURE, ) assert move3.to_str(0) == "" assert move3.to_str(1) == "a0a6"