import logging import random from styles.base import AlarmStyle # State Machine states for the puzzle flow. STATE_RINGING = 0 STATE_WAIT_BEFORE_BLINK = 1 STATE_BLINKING = 2 STATE_WAIT_FOR_INPUT = 3 STATE_EVALUATING = 4 STATE_WAIT_BEFORE_RETRY = 5 BLINK_INTERVAL = 0.3 class BlinkStyle(AlarmStyle): """The count-the-blinks puzzle. Press once to start, watch the LED blink 1-7 times, then press the button exactly that many times. A wrong count starts a fresh sequence. """ def __init__(self, set_led): super().__init__(set_led) self.state = STATE_RINGING self.target_blinks = 0 self.user_presses = 0 self.last_interaction_time = 0 self.button_was_pressed = False self._blink_phase = 0 self._blink_phases = 0 self._blink_next_toggle: float | None = None def update(self, now, is_pressed): button_just_pressed = False if is_pressed and not self.button_was_pressed: self.button_was_pressed = True button_just_pressed = True elif not is_pressed and self.button_was_pressed: self.button_was_pressed = False if self.state == STATE_RINGING: if button_just_pressed: logging.info( "Alarm button pressed! Puzzle started. Waiting 3 seconds..." ) self.state = STATE_WAIT_BEFORE_BLINK self.last_interaction_time = now elif self.state == STATE_WAIT_BEFORE_BLINK: if now - self.last_interaction_time >= 3.0: self.target_blinks = random.randint(1, 7) logging.info(f"Blinking {self.target_blinks} times...") self.state = STATE_BLINKING self._blink_phase = 0 self._blink_phases = self.target_blinks * 2 self._blink_next_toggle = now + BLINK_INTERVAL self.set_led(True) elif self.state == STATE_BLINKING: # Non-blocking blink: toggle the LED at fixed intervals so the # runner keeps sampling the button and can be interrupted. if self._blink_next_toggle is None: # Safety for direct state assignment (e.g. tests). self._blink_phase = 0 self._blink_phases = self.target_blinks * 2 self._blink_next_toggle = now + BLINK_INTERVAL self.set_led(True) while now >= self._blink_next_toggle: self._blink_phase += 1 self._blink_next_toggle += BLINK_INTERVAL if self._blink_phase >= self._blink_phases: self.set_led(False) self._blink_phase = 0 self._blink_phases = 0 self._blink_next_toggle = None logging.info("Blinking finished. Waiting for input...") self.state = STATE_WAIT_FOR_INPUT self.user_presses = 0 self.last_interaction_time = now self.button_was_pressed = is_pressed break else: self.set_led(self._blink_phase % 2 == 0) elif self.state == STATE_WAIT_FOR_INPUT: self.set_led(is_pressed) if button_just_pressed: self.user_presses += 1 logging.info(f"Button pressed: {self.user_presses} times") if is_pressed: self.last_interaction_time = now if not is_pressed and (now - self.last_interaction_time >= 3.0): self.state = STATE_EVALUATING elif self.state == STATE_EVALUATING: logging.info( f"Evaluation: Target={self.target_blinks}, " f"Entered={self.user_presses}" ) if self.user_presses == self.target_blinks: logging.info("Puzzle solved correctly! Alarm clock is stopping.") self.set_led(False) return False # Signal the runner to stop. else: logging.info("Incorrect input! Waiting before retrying...") self.state = STATE_WAIT_BEFORE_RETRY self.last_interaction_time = now self.set_led(False) elif self.state == STATE_WAIT_BEFORE_RETRY: if now - self.last_interaction_time >= 5.0: self.target_blinks = random.randint(1, 7) logging.info(f"New attempt! Blinking {self.target_blinks} times...") self.state = STATE_BLINKING return True # Keep running if __name__ == "__main__": # Self-check: a correct blink sequence stops the alarm. style = BlinkStyle(lambda on: None) assert style.update(100.0, True) is True assert style.state == STATE_WAIT_BEFORE_BLINK import random as _random _random.randint = lambda a, b: 3 # force 3 blinks style.update(103.1, False) assert style.state == STATE_BLINKING and style.target_blinks == 3 style.update(105.0, False) assert style.state == STATE_WAIT_FOR_INPUT for t in (105.5, 106.0, 106.5): style.update(t, True) style.update(t + 0.05, False) assert style.user_presses == 3 style.update(109.7, False) assert style.update(109.8, False) is False # solved -> stop print("blink self-check ok")