"""状態遷移マシンのテスト""" 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