Spaces:
Running
Running
| """状態遷移マシンのテスト""" | |
| import pytest | |
| import time | |
| from unittest.mock import patch | |
| from rock_paper_scissors.game.state_machine import StateMachine | |
| from rock_paper_scissors.game.states import GameState, Hand, GameResult | |
| class TestStateMachine: | |
| """状態遷移のテスト""" | |
| def test_initial_state_is_idle(self): | |
| """初期状態はIDLE""" | |
| sm = StateMachine() | |
| assert sm.state == GameState.IDLE | |
| def test_wave_detected_from_idle_transitions_to_countdown(self): | |
| """手振り検出でIDLE→COUNTDOWN(直接カウントダウン開始)""" | |
| sm = StateMachine() | |
| assert sm.state == GameState.IDLE | |
| sm.on_wave_detected() | |
| assert sm.state == GameState.COUNTDOWN | |
| assert sm.countdown_step == 3 | |
| def test_countdown_tick(self): | |
| """カウントダウンが進む""" | |
| sm = StateMachine() | |
| sm._state = GameState.COUNTDOWN | |
| sm._countdown_step = 3 | |
| assert sm.on_countdown_tick() is True # 3→2 | |
| assert sm.countdown_step == 2 | |
| assert sm.on_countdown_tick() is True # 2→1 | |
| assert sm.countdown_step == 1 | |
| assert sm.on_countdown_tick() is False # 1→0(PLAY遷移) | |
| assert sm.state == GameState.PLAY | |
| def test_play_sets_reachy_hand(self): | |
| """PLAY状態でReachyの手が設定される""" | |
| sm = StateMachine() | |
| sm._state = GameState.COUNTDOWN | |
| sm._countdown_step = 1 | |
| sm.on_countdown_tick() | |
| assert sm.state == GameState.PLAY | |
| assert sm.reachy_hand in [Hand.ROCK, Hand.PAPER, Hand.SCISSORS] | |
| def test_user_hand_detected_transitions_to_react(self): | |
| """ユーザーの手検出でPLAY→REACT(最小表示時間経過後)""" | |
| sm = StateMachine() | |
| sm._state = GameState.PLAY | |
| sm._state_start_time = time.time() - 10 # 十分な時間経過 | |
| sm._reachy_hand = Hand.ROCK | |
| sm.on_user_hand_detected(Hand.SCISSORS) | |
| assert sm.state == GameState.REACT | |
| assert sm.user_hand == Hand.SCISSORS | |
| assert sm.game_result == GameResult.WIN | |
| def test_react_complete_transitions_to_idle(self): | |
| """REACT完了でREACT→IDLE""" | |
| sm = StateMachine() | |
| sm._state = GameState.REACT | |
| sm._reachy_hand = Hand.ROCK | |
| sm._user_hand = Hand.SCISSORS | |
| sm._game_result = GameResult.WIN | |
| sm.on_react_complete() | |
| assert sm.state == GameState.IDLE | |
| assert sm.reachy_hand is None | |
| assert sm.user_hand is None | |
| assert sm.game_result is None | |
| def test_force_idle(self): | |
| """異常時にIDLEに強制遷移""" | |
| sm = StateMachine() | |
| sm._state = GameState.PLAY | |
| sm._reachy_hand = Hand.ROCK | |
| sm.force_idle() | |
| assert sm.state == GameState.IDLE | |
| assert sm.reachy_hand is None | |
| def test_state_change_callback(self): | |
| """状態変更時のコールバックが呼ばれる""" | |
| sm = StateMachine() | |
| callback_args = [] | |
| def callback(old_state, new_state): | |
| callback_args.append((old_state, new_state)) | |
| sm.set_on_state_change(callback) | |
| sm.on_wave_detected() | |
| assert len(callback_args) == 1 | |
| assert callback_args[0] == (GameState.IDLE, GameState.COUNTDOWN) | |
| def test_full_game_flow(self): | |
| """フルゲームフロー: IDLE → COUNTDOWN → PLAY → REACT → IDLE""" | |
| sm = StateMachine() | |
| # IDLE → COUNTDOWN | |
| assert sm.state == GameState.IDLE | |
| sm.on_wave_detected() | |
| assert sm.state == GameState.COUNTDOWN | |
| assert sm.countdown_step == 3 | |
| # COUNTDOWN → PLAY | |
| sm.on_countdown_tick() # 3→2 | |
| sm.on_countdown_tick() # 2→1 | |
| sm.on_countdown_tick() # 1→0 → PLAY | |
| assert sm.state == GameState.PLAY | |
| assert sm.reachy_hand is not None | |
| # PLAY: ユーザーの手を検出(最小表示時間前) | |
| sm.on_user_hand_detected(Hand.PAPER) | |
| assert sm.state == GameState.PLAY # まだ遷移しない | |
| assert sm.user_hand == Hand.PAPER | |
| # 最小表示時間経過をシミュレート | |
| sm._state_start_time = time.time() - 10 | |
| # PLAY → REACT(update経由で遷移) | |
| sm.update() | |
| assert sm.state == GameState.REACT | |
| assert sm.game_result is not None | |
| # REACT → IDLE | |
| sm.on_react_complete() | |
| assert sm.state == GameState.IDLE | |
| assert sm.reachy_hand is None | |
| class TestStateMachineAutoTransitions: | |
| """自動状態遷移のテスト""" | |
| def test_react_auto_transitions_to_idle_after_duration(self): | |
| """REACT状態は一定時間後にIDLEへ自動遷移""" | |
| sm = StateMachine() | |
| sm._state = GameState.REACT | |
| sm._state_start_time = time.time() - 10 # 十分な時間経過 | |
| changed = sm.update() | |
| assert changed is True | |
| assert sm.state == GameState.IDLE | |
| class TestPlayMinDuration: | |
| """PLAY状態の最小表示時間(手を確認する間)のテスト""" | |
| def test_user_hand_not_processed_before_min_duration(self): | |
| """最小表示時間前はユーザーの手を検出してもREACTに遷移しない""" | |
| sm = StateMachine() | |
| sm._state = GameState.PLAY | |
| sm._state_start_time = time.time() # 今入ったばかり | |
| sm._reachy_hand = Hand.ROCK | |
| # ユーザーの手を検出 | |
| sm.on_user_hand_detected(Hand.SCISSORS) | |
| # まだREACTに遷移していない | |
| assert sm.state == GameState.PLAY | |
| # ユーザーの手は保存されている | |
| assert sm.user_hand == Hand.SCISSORS | |
| def test_user_hand_processed_after_min_duration(self): | |
| """最小表示時間後はユーザーの手検出でREACTに遷移する""" | |
| sm = StateMachine() | |
| sm._state = GameState.PLAY | |
| sm._state_start_time = time.time() - 10 # 十分な時間経過 | |
| sm._reachy_hand = Hand.ROCK | |
| # ユーザーの手を検出 | |
| sm.on_user_hand_detected(Hand.SCISSORS) | |
| # REACTに遷移 | |
| assert sm.state == GameState.REACT | |
| assert sm.game_result == GameResult.WIN | |
| def test_pending_hand_processed_on_update_after_min_duration(self): | |
| """保留中の手は最小表示時間経過後のupdateでREACTに遷移する""" | |
| sm = StateMachine() | |
| sm._state = GameState.PLAY | |
| sm._state_start_time = time.time() # 今入ったばかり | |
| sm._reachy_hand = Hand.ROCK | |
| # ユーザーの手を検出(まだ遷移しない) | |
| sm.on_user_hand_detected(Hand.SCISSORS) | |
| assert sm.state == GameState.PLAY | |
| assert sm.user_hand == Hand.SCISSORS | |
| # 時間経過をシミュレート | |
| sm._state_start_time = time.time() - 10 | |
| # updateで遷移 | |
| changed = sm.update() | |
| assert changed is True | |
| assert sm.state == GameState.REACT | |
| assert sm.game_result == GameResult.WIN | |
| def test_update_does_not_transition_without_user_hand(self): | |
| """ユーザーの手がない状態ではupdateしてもREACTに遷移しない""" | |
| sm = StateMachine() | |
| sm._state = GameState.PLAY | |
| sm._state_start_time = time.time() - 10 # 十分な時間経過 | |
| sm._reachy_hand = Hand.ROCK | |
| sm._user_hand = None # ユーザーの手は未検出 | |
| changed = sm.update() | |
| assert changed is False | |
| assert sm.state == GameState.PLAY | |
| def test_multiple_hand_detections_uses_latest(self): | |
| """最小表示時間中に複数回検出された場合、最新の手を使う""" | |
| sm = StateMachine() | |
| sm._state = GameState.PLAY | |
| sm._state_start_time = time.time() # 今入ったばかり | |
| sm._reachy_hand = Hand.ROCK | |
| # 最初の検出: パー | |
| sm.on_user_hand_detected(Hand.PAPER) | |
| assert sm.user_hand == Hand.PAPER | |
| # 2回目の検出: チョキ | |
| sm.on_user_hand_detected(Hand.SCISSORS) | |
| assert sm.user_hand == Hand.SCISSORS | |
| # 時間経過 | |
| sm._state_start_time = time.time() - 10 | |
| # updateで遷移(最新のチョキで判定) | |
| sm.update() | |
| assert sm.state == GameState.REACT | |
| # Rock vs Scissors = Reachy WIN | |
| assert sm.game_result == GameResult.WIN | |