From ee077081608c335d39e518bfc38ae2432f0d2bfd Mon Sep 17 00:00:00 2001 From: madcpf Date: Tue, 10 Oct 2023 22:35:55 -0700 Subject: [PATCH] update test --- .../examples/quantum_chinese_chess/chess.py | 88 +++++---- .../quantum_chinese_chess/chess_test.py | 184 ++++++++++++++++-- .../examples/quantum_chinese_chess/enums.py | 8 +- 3 files changed, 223 insertions(+), 57 deletions(-) diff --git a/unitary/examples/quantum_chinese_chess/chess.py b/unitary/examples/quantum_chinese_chess/chess.py index da26a2da..cc67d101 100644 --- a/unitary/examples/quantum_chinese_chess/chess.py +++ b/unitary/examples/quantum_chinese_chess/chess.py @@ -125,18 +125,9 @@ def check_classical_rule( source_piece = self.board.board[source] target_piece = self.board.board[target] # Check if the move is blocked by classical path piece. - if len(classical_path_pieces) > 0: - if source_piece.type_ != Type.CANNON: - # The path is blocked by classical pieces. - raise ValueError("The path is blocked.") - elif len(classical_path_pieces) > 1: - # Invalid cannon move, since there could only be at most one classical piece between - # the source (i.e. the cannon) and the target. - raise ValueError("Cannon cannot fire like this.") - elif source_piece.color == target_piece.color: - raise ValueError("Cannon cannot fire to a piece with same color.") - elif target_piece.color == Color.NA: - raise ValueError("Cannon cannot fire to an empty piece.") + if len(classical_path_pieces) > 0 and source_piece.type_ != Type.CANNON: + # The path is blocked by classical pieces. + raise ValueError("The path is blocked.") # Check if the target has classical piece of the same color. if not target_piece.is_entangled and source_piece.color == target_piece.color: @@ -190,6 +181,15 @@ def check_classical_rule( elif source_piece.type_ == Type.CANNON: if dx != 0 and dy != 0: raise ValueError("CANNON cannot move like this.") + if len(classical_path_pieces) > 0: + if len(classical_path_pieces) > 1: + # Invalid cannon move, since there could only be at most one classical piece between + # the source (i.e. the cannon) and the target. + raise ValueError("CANNON cannot fire like this.") + elif source_piece.color == target_piece.color: + raise ValueError("CANNON cannot fire to a piece with same color.") + elif target_piece.color == Color.NA: + raise ValueError("CANNON cannot fire to an empty piece.") elif source_piece.type_ == Type.PAWN: if abs(dx) + abs(dy) != 1: raise ValueError("PAWN cannot move like this.") @@ -226,51 +226,71 @@ def classify_move( if len(sources) == 1 and len(targets) == 1: if len(quantum_path_pieces_0) == 0: - if not source.is_entangled() and not target.is_entangled(): - if target.color == source.color: - raise ValueError( - "The target piece is classical with the same color as the source piece." - ) - move_type = MoveType.CLASSICAL + if ( + len(classical_path_pieces_0) == 0 + and source.type_ == Type.CANNON + and target.color.value == 1 - source.color.value + ): + raise ValueError( + "CANNON could not fire/capture without a cannon platform." + ) + if not source.is_entangled and not target.is_entangled: + return MoveType.CLASSICAL, MoveVariant.UNSPECIFIED else: move_type = MoveType.JUMP else: move_type = MoveType.SLIDE - if move_type != MoveType.CLASSICAL and len(classical_path_pieces_0) == 1: - # Special checks when source is cannon. - if target.color == source.color: - raise ValueError( - "Cannon cannot capture a piece with the same color." - ) - elif target.color == Color.NA: - raise ValueError("Cannon cannot capture an empty piece.") - else: - return MoveType.CANNON_NORMAL_FIRE, MoveVariant.CAPTURE + if ( + move_type != MoveType.CLASSICAL + and source.type_ == Type.CANNON + and ( + len(classical_path_pieces_0) == 1 or len(quantum_path_pieces_0) > 0 + ) + ): + # By this time the classical cannon fire has been identified as CLASSICAL JUMP. + return MoveType.CANNON_FIRE, MoveVariant.CAPTURE # Determine MoveVariant. if target.color == Color.NA: move_variant = MoveVariant.BASIC + # TODO(): such move could be a merge. Take care of such cases later. elif target.color == source.color: move_variant = MoveVariant.EXCLUDED else: move_variant = MoveVariant.CAPTURE elif len(sources) == 2: + source_1 = self.board.board[sources[1]] + if not source.is_entangled or not source_1.is_entangled: + raise ValueError( + "Both sources need to be in quantum state in order to merge." + ) if target.type_ != Type.EMPTY: + # TODO(): Currently we don't support merge + excluded/capture, or cannon_merge_fire + capture. Maybe add support later. + if len(classical_path_pieces_0) > 0 or len(classical_path_pieces_1) > 0: + raise ValueError("Currently CANNON could not merge while fire.") raise ValueError("Currently we could only merge into an empty piece.") - if len(classical_path_pieces_0) > 0 or len(classical_path_pieces_1) > 0: - raise ValueError("Currently Cannon could not merge while fire.") if len(quantum_path_pieces_0) == 0 and len(quantum_path_pieces_1) == 0: - move_type = Type.MERGE_JUMP + move_type = MoveType.MERGE_JUMP else: - move_type = Type.MERGE_SLIDE + move_type = MoveType.MERGE_SLIDE + move_variant = MoveVariant.BASIC + elif len(targets) == 2: + target_1 = self.board.board[targets[1]] + if target.type_ != Type.EMPTY or target_1.type_ != Type.EMPTY: + # TODO(): Currently we don't support split + excluded/capture, or cannon_split_fire + capture. Maybee add support later. + if len(classical_path_pieces_0) > 0 or len(classical_path_pieces_1) > 0: + raise ValueError("Currently CANNON could not split while fire.") + raise ValueError("Currently we could only split into empty pieces.") if source.type_ == Type.KING: + # TODO(): Currently we don't support KING split. Maybe add support later. raise ValueError("King split is not supported currently.") if len(quantum_path_pieces_0) == 0 and len(quantum_path_pieces_1) == 0: - move_type = Type.SPLIT_JUMP + move_type = MoveType.SPLIT_JUMP else: - move_type = Type.SPLIT_SLIDE + move_type = MoveType.SPLIT_SLIDE + move_variant = MoveVariant.BASIC return move_type, move_variant def apply_move(self, str_to_parse: str) -> None: diff --git a/unitary/examples/quantum_chinese_chess/chess_test.py b/unitary/examples/quantum_chinese_chess/chess_test.py index 09669455..67b56c61 100644 --- a/unitary/examples/quantum_chinese_chess/chess_test.py +++ b/unitary/examples/quantum_chinese_chess/chess_test.py @@ -21,6 +21,8 @@ Color, Type, SquareState, + MoveType, + MoveVariant, ) @@ -100,20 +102,6 @@ def test_check_classical_rule(monkeypatch): with pytest.raises(ValueError, match="The path is blocked."): game.check_classical_rule("a0", "a4", ["a3"]) - # Cannon could jump across exactly one piece. - game.check_classical_rule("b2", "b9", ["b7"]) - with pytest.raises(ValueError, match="Cannon cannot fire like this."): - game.check_classical_rule("b2", "b9", ["b5", "b7"]) - # Cannon cannot fire to a piece with same color. - game.board.board["b3"].reset(game.board.board["b2"]) - game.board.board["b2"].reset() - with pytest.raises( - ValueError, match="Cannon cannot fire to a piece with same color." - ): - game.check_classical_rule("b3", "e3", ["c3"]) - with pytest.raises(ValueError, match="Cannon cannot fire to an empty piece."): - game.check_classical_rule("b3", "d3", ["c3"]) - # Target should not be a classical piece of the same color. with pytest.raises( ValueError, match="The target place has classical piece with the same color." @@ -168,6 +156,20 @@ def test_check_classical_rule(monkeypatch): game.check_classical_rule("b7", "b4", []) with pytest.raises(ValueError, match="CANNON cannot move like this."): game.check_classical_rule("b7", "a8", []) + # Cannon could jump across exactly one piece. + game.check_classical_rule("b2", "b9", ["b7"]) + with pytest.raises(ValueError, match="CANNON cannot fire like this."): + game.check_classical_rule("b2", "b9", ["b5", "b7"]) + # Cannon cannot fire to a piece with same color. + game.board.board["b3"].reset(game.board.board["b2"]) + game.board.board["b2"].reset() + game.board.board["e3"].is_entangled = True + with pytest.raises( + ValueError, match="CANNON cannot fire to a piece with same color." + ): + game.check_classical_rule("b3", "e3", ["c3"]) + with pytest.raises(ValueError, match="CANNON cannot fire to an empty piece."): + game.check_classical_rule("b3", "d3", ["c3"]) # PAWN game.check_classical_rule("a6", "a5", []) @@ -192,19 +194,163 @@ def test_check_classical_rule(monkeypatch): game.check_classical_rule("c4", "d4", []) -def test_classify_move(): - # classical basic - # classical excluded - # classical capture +def test_classify_move_fail(monkeypatch): + output = io.StringIO() + sys.stdout = output + inputs = iter(["y", "Bob", "Ben"]) + monkeypatch.setattr("builtins.input", lambda _: next(inputs)) + game = QuantumChineseChess() + with pytest.raises( + ValueError, match="CANNON could not fire/capture without a cannon platform." + ): + game.classify_move(["b7"], ["b2"], [], [], [], []) + + with pytest.raises( + ValueError, match="Both sources need to be in quantum state in order to merge." + ): + game.classify_move(["b2", "h2"], ["e2"], [], [], [], []) + + game.board.board["c0"].reset(game.board.board["b7"]) + game.board.board["c0"].is_entangled = True + game.board.board["b7"].reset() + game.board.board["g0"].reset(game.board.board["h7"]) + game.board.board["g0"].is_entangled = True + game.board.board["h7"].reset() + with pytest.raises( + ValueError, match="Currently CANNON could not merge while fire." + ): + game.classify_move(["c0", "g0"], ["e0"], ["d0"], [], ["f0"], []) + + game.board.board["b3"].reset(game.board.board["b2"]) + game.board.board["b3"].is_entangled = True + game.board.board["b2"].reset() + game.board.board["d3"].reset(game.board.board["h2"]) + game.board.board["d3"].is_entangled = True + game.board.board["h2"].reset() + with pytest.raises( + ValueError, match="Currently we could only merge into an empty piece." + ): + game.classify_move(["b3", "d3"], ["c3"], [], [], [], []) + + with pytest.raises( + ValueError, match="Currently CANNON could not split while fire." + ): + game.classify_move(["g0"], ["e0", "i0"], ["f0"], [], ["h0"], []) + + game.board.board["d0"].is_entangled = True + with pytest.raises( + ValueError, match="Currently we could only split into empty pieces." + ): + game.classify_move(["d3"], ["d0", "d4"], [], [], [], []) + + game.board.board["d0"].reset() + game.board.board["f0"].reset() + with pytest.raises(ValueError, match="King split is not supported currently."): + game.classify_move(["e0"], ["d0", "f0"], [], [], [], []) + + +def test_classify_move_success(monkeypatch): + output = io.StringIO() + sys.stdout = output + inputs = iter(["y", "Bob", "Ben"]) + monkeypatch.setattr("builtins.input", lambda _: next(inputs)) + game = QuantumChineseChess() + # classical + assert game.classify_move(["h9"], ["g7"], [], [], [], []) == ( + MoveType.CLASSICAL, + MoveVariant.UNSPECIFIED, + ) + assert game.classify_move(["b2"], ["b9"], ["b7"], [], [], []) == ( + MoveType.CLASSICAL, + MoveVariant.UNSPECIFIED, + ) + # jump basic + game.board.board["c9"].is_entangled = True + assert game.classify_move(["c9"], ["e7"], [], [], [], []) == ( + MoveType.JUMP, + MoveVariant.BASIC, + ) + game.board.board["b2"].is_entangled = True + assert game.classify_move(["b2"], ["e2"], [], [], [], []) == ( + MoveType.JUMP, + MoveVariant.BASIC, + ) + # jump excluded + game.board.board["a3"].is_entangled = True + assert game.classify_move(["a0"], ["a3"], [], [], [], []) == ( + MoveType.JUMP, + MoveVariant.EXCLUDED, + ) + # jump capture + game.board.board["g4"].reset(game.board.board["g6"]) + game.board.board["g4"].is_entangled = True + game.board.board["g6"].reset() + assert game.classify_move(["g4"], ["g3"], [], [], [], []) == ( + MoveType.JUMP, + MoveVariant.CAPTURE, + ) + # slide basic + assert game.classify_move(["a0"], ["a4"], [], ["a3"], [], []) == ( + MoveType.SLIDE, + MoveVariant.BASIC, + ) + # slide excluded + game.board.board["i7"].reset(game.board.board["h7"]) + game.board.board["i7"].is_entangled = True + game.board.board["h7"].reset() + game.board.board["i6"].is_entangled = True + assert game.classify_move(["i9"], ["i6"], [], ["i7"], [], []) == ( + MoveType.SLIDE, + MoveVariant.EXCLUDED, + ) + # slide capture + assert game.classify_move(["a0"], ["a6"], [], ["a3"], [], []) == ( + MoveType.SLIDE, + MoveVariant.CAPTURE, + ) + # split_jump basic + assert game.classify_move(["g4"], ["f4", "h4"], [], [], [], []) == ( + MoveType.SPLIT_JUMP, + MoveVariant.BASIC, + ) + # split_slide basic + game.board.board["d3"].reset(game.board.board["h2"]) + game.board.board["h2"].reset() + game.board.board["c3"].is_entangled = True + game.board.board["e3"].is_entangled = True + assert game.classify_move(["d3"], ["b3", "f3"], [], ["c3"], [], ["e3"]) == ( + MoveType.SPLIT_SLIDE, + MoveVariant.BASIC, + ) + # merge_jump basic + game.board.board["b7"].is_entangled = True + assert game.classify_move(["b7", "i7"], ["e7"], [], [], [], []) == ( + MoveType.MERGE_JUMP, + MoveVariant.BASIC, + ) + # merge_slide basic + assert game.classify_move(["b7", "i7"], ["a7"], [], [], [], ["b7"]) == ( + MoveType.MERGE_SLIDE, + MoveVariant.BASIC, + ) + # cannon_fire capture - pass + assert game.classify_move(["i7"], ["i3"], [], ["i6"], [], []) == ( + MoveType.CANNON_FIRE, + MoveVariant.CAPTURE, + ) + game.board.board["i6"].is_entangled = False + assert game.classify_move(["i7"], ["i3"], ["i6"], [], [], []) == ( + MoveType.CANNON_FIRE, + MoveVariant.CAPTURE, + ) diff --git a/unitary/examples/quantum_chinese_chess/enums.py b/unitary/examples/quantum_chinese_chess/enums.py index 71e39911..485f2b91 100644 --- a/unitary/examples/quantum_chinese_chess/enums.py +++ b/unitary/examples/quantum_chinese_chess/enums.py @@ -61,10 +61,10 @@ class MoveVariant(enum.Enum): the MoveType above. """ - UNSPECIFIED = 0 - BASIC = 1 - EXCLUDED = 2 - CAPTURE = 3 + UNSPECIFIED = 0 # Used together with MoveType = CLASSICAL. + BASIC = 1 # The target piece is empty. + EXCLUDED = 2 # The target piece has the same color. + CAPTURE = 3 # The target piece has the opposite color. class Color(enum.Enum):