rock_paper_scissors / tests /test_state_machine.py
trtd56's picture
Initial commit
24836e5
"""状態遷移マシンのテスト"""
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