Add backend support for the typing game: curriculum, progress, and reward unlocks
Moves the typing app's lesson plan and progress from client-side YAML/localStorage into the backend, and adds a reward system that ties passing a lesson to unlocking part of the music library. Lock state is always recomputed live from curriculum x progress x the live library, never persisted separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,8 @@ from musicmouse.services.lirc import LircService
|
||||
from musicmouse.services.mqtt import MqttService, build_entities
|
||||
from musicmouse.services.podcasts import PodcastFeedService
|
||||
from musicmouse.services.web import WebService
|
||||
from musicmouse.tippen.curriculum import CurriculumError
|
||||
from musicmouse.tippen.runtime import TippenRuntime, build_tippen_runtime
|
||||
|
||||
_log = logging.getLogger("musicmouse")
|
||||
|
||||
@@ -100,10 +102,20 @@ def main(argv: list[str] | None = None) -> int:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
tippen: TippenRuntime | None = None
|
||||
if config.general.tippen is not None:
|
||||
try:
|
||||
tippen = build_tippen_runtime(config.general.tippen)
|
||||
except CurriculumError as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
runner = (
|
||||
run_simulated(config, args.config, args.script)
|
||||
run_simulated(config, args.config, args.script, tippen=tippen)
|
||||
if args.simulate
|
||||
else run_real(config, args.config, hardware=wants_hardware(config, args.no_hardware))
|
||||
else run_real(
|
||||
config, args.config, hardware=wants_hardware(config, args.no_hardware), tippen=tippen
|
||||
)
|
||||
)
|
||||
try:
|
||||
asyncio.run(runner)
|
||||
@@ -115,7 +127,13 @@ def main(argv: list[str] | None = None) -> int:
|
||||
# ------------------------------------------------------------------------- real
|
||||
|
||||
|
||||
async def run_real(config: Config, config_path: Path, *, hardware: bool = True) -> None:
|
||||
async def run_real(
|
||||
config: Config,
|
||||
config_path: Path,
|
||||
*,
|
||||
hardware: bool = True,
|
||||
tippen: TippenRuntime | None = None,
|
||||
) -> None:
|
||||
bus = EventBus()
|
||||
await bus.start()
|
||||
clock = RealClock()
|
||||
@@ -137,7 +155,7 @@ async def run_real(config: Config, config_path: Path, *, hardware: bool = True)
|
||||
player = _build_player(bus, general, clock=clock)
|
||||
|
||||
library = await build_library(config)
|
||||
app = _build_app(config, bus, mouse, player, library, clock=clock)
|
||||
app = _build_app(config, bus, mouse, player, library, clock=clock, tippen=tippen)
|
||||
services = _build_services(app, mouse, player, clock=clock, config_path=config_path)
|
||||
|
||||
_log.info(
|
||||
@@ -176,7 +194,9 @@ async def run_real(config: Config, config_path: Path, *, hardware: bool = True)
|
||||
# -------------------------------------------------------------------- simulated
|
||||
|
||||
|
||||
async def run_simulated(config: Config, config_path: Path, script: Path | None) -> None:
|
||||
async def run_simulated(
|
||||
config: Config, config_path: Path, script: Path | None, *, tippen: TippenRuntime | None = None
|
||||
) -> None:
|
||||
# Imported here so the production path never touches the simulator.
|
||||
from musicmouse.simulator.harness import build_simulation
|
||||
from musicmouse.simulator.repl import run_repl
|
||||
@@ -185,7 +205,7 @@ async def run_simulated(config: Config, config_path: Path, script: Path | None)
|
||||
# A script runs on virtual time, so `wait 1s` is instant. The prompt runs on the
|
||||
# real clock, so playback ticks along while you watch it.
|
||||
sim = await build_simulation(
|
||||
config, clock=RealClock() if script is None else None, track_duration=5.0
|
||||
config, clock=RealClock() if script is None else None, track_duration=5.0, tippen=tippen
|
||||
)
|
||||
|
||||
services = _build_services(
|
||||
@@ -280,6 +300,7 @@ def _build_app(
|
||||
library: MusicLibrary,
|
||||
*,
|
||||
clock: RealClock,
|
||||
tippen: TippenRuntime | None = None,
|
||||
) -> App:
|
||||
app = App(
|
||||
config=config,
|
||||
@@ -292,6 +313,7 @@ def _build_app(
|
||||
# identity check.
|
||||
playlists=library.figure_playlists(),
|
||||
clock=clock,
|
||||
tippen=tippen,
|
||||
)
|
||||
register_all(bus, app)
|
||||
return app
|
||||
|
||||
Reference in New Issue
Block a user