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>
97 lines
2.6 KiB
Python
97 lines
2.6 KiB
Python
"""Assemble the whole app against fake hardware.
|
|
|
|
This is the real bus, the real device, the real reactions - only the serial link and
|
|
VLC are substituted. That is what makes a scenario meaningful: everything between the
|
|
tag being read and the LED bytes being written is production code.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from musicmouse.app import App
|
|
from musicmouse.bus import EventBus
|
|
from musicmouse.clock import Clock, FakeClock
|
|
from musicmouse.config import Config
|
|
from musicmouse.devices.mouse import MusicMouseDevice
|
|
from musicmouse.library import MusicLibrary
|
|
from musicmouse.reactions import register_all
|
|
from musicmouse.simulator.driver import SimulatorDriver
|
|
from musicmouse.simulator.fake_player import DEFAULT_TRACK_DURATION, FakePlayer
|
|
from musicmouse.simulator.fake_transport import FakeTransport
|
|
from musicmouse.tippen.runtime import TippenRuntime
|
|
|
|
__all__ = ["Simulation", "build_simulation"]
|
|
|
|
|
|
@dataclass
|
|
class Simulation:
|
|
app: App
|
|
driver: SimulatorDriver
|
|
transport: FakeTransport
|
|
player: FakePlayer
|
|
clock: Clock
|
|
bus: EventBus
|
|
|
|
async def aclose(self) -> None:
|
|
self.player.close()
|
|
await self.bus.stop()
|
|
|
|
|
|
async def build_simulation(
|
|
config: Config,
|
|
*,
|
|
clock: Clock | None = None,
|
|
track_duration: float = DEFAULT_TRACK_DURATION,
|
|
library: MusicLibrary | None = None,
|
|
tippen: TippenRuntime | None = None,
|
|
) -> Simulation:
|
|
bus = EventBus()
|
|
await bus.start()
|
|
|
|
if clock is None:
|
|
clock = FakeClock(idle=bus.drain)
|
|
|
|
transport = FakeTransport()
|
|
mouse = MusicMouseDevice(bus, transport, config.tag_map, port="simulated")
|
|
transport.attach(mouse.feed)
|
|
|
|
player = FakePlayer(
|
|
bus,
|
|
clock=clock,
|
|
track_duration=track_duration,
|
|
**FakePlayer.volume_kwargs(config.general),
|
|
)
|
|
|
|
if library is None:
|
|
library_config = config.general.library
|
|
library = await MusicLibrary.build(
|
|
library_config.root,
|
|
library_config.cache,
|
|
frozenset(config.general.audio_extensions),
|
|
figure_kinds=config.figure_kinds,
|
|
)
|
|
|
|
app = App(
|
|
config=config,
|
|
bus=bus,
|
|
mouse=mouse,
|
|
player=player,
|
|
library=library,
|
|
playlists=library.figure_playlists(),
|
|
clock=clock,
|
|
tippen=tippen,
|
|
)
|
|
register_all(bus, app)
|
|
mouse.on_connected()
|
|
await bus.drain()
|
|
|
|
return Simulation(
|
|
app=app,
|
|
driver=SimulatorDriver(app, transport, clock),
|
|
transport=transport,
|
|
player=player,
|
|
clock=clock,
|
|
bus=bus,
|
|
)
|