Source code for mini_arcade_core.scenes.systems.builtins.score_chain
"""
Reusable helpers for short-lived score-chain windows.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, Generic, TypeVar
from mini_arcade_core.scenes.systems.phases import SystemPhase
# pylint: disable=invalid-name
TCtx = TypeVar("TCtx")
# pylint: enable=invalid-name
def _default_enabled_when(_ctx: object) -> bool:
return True
[docs]
@dataclass
class ScoreChainState:
"""
Mutable chain state for consecutive score events.
"""
step_index: int = 0
remaining_seconds: float = 0.0
@property
def active(self) -> bool:
"""
Whether the score chain is currently active and can be extended.
:return: True if the chain is active, False otherwise.
:rtype: bool
"""
return self.step_index > 0 and self.remaining_seconds > 0.0
[docs]
def reset_score_chain(state: ScoreChainState) -> None:
"""
Clear the current score chain.
"""
state.step_index = 0
state.remaining_seconds = 0.0
[docs]
def claim_score_chain_points(
state: ScoreChainState,
*,
steps: tuple[int, ...],
window_seconds: float,
) -> int:
"""
Claim the next score value in a chain and refresh its timer.
"""
if not steps:
return 0
points = steps[min(state.step_index, len(steps) - 1)]
state.step_index += 1
state.remaining_seconds = max(0.0, float(window_seconds))
return int(points)
[docs]
@dataclass(frozen=True)
class ScoreChainBinding(Generic[TCtx]):
"""
Declarative score-chain expiry rule.
"""
state_getter: Callable[[TCtx], ScoreChainState]
on_expired: Callable[[TCtx, ScoreChainState], None] | None = None
[docs]
@dataclass
class ScoreChainSystem(Generic[TCtx]):
"""
Expire score chains after their timer runs out.
"""
name: str = "common_score_chain"
phase: int = SystemPhase.SIMULATION
order: int = 17
enabled_when: Callable[[TCtx], bool] = _default_enabled_when
bindings: tuple[ScoreChainBinding[TCtx], ...] = ()
[docs]
def step(self, ctx: TCtx) -> None:
"""Advance active score-chain timers by frame dt."""
if not self.enabled_when(ctx):
return
dt = max(0.0, float(getattr(ctx, "dt", 0.0)))
if dt <= 0.0:
return
for binding in self.bindings:
state = binding.state_getter(ctx)
if state.remaining_seconds <= 0.0:
continue
state.remaining_seconds = max(
0.0,
float(state.remaining_seconds) - dt,
)
if state.remaining_seconds > 0.0:
continue
if binding.on_expired is not None:
binding.on_expired(ctx, state)
reset_score_chain(state)
__all__ = [
"ScoreChainBinding",
"ScoreChainState",
"ScoreChainSystem",
"claim_score_chain_points",
"reset_score_chain",
]