diff --git a/python-backend/musicmouse/services/web/schemas.py b/python-backend/musicmouse/services/web/schemas.py index 6ec2a84..9107554 100644 --- a/python-backend/musicmouse/services/web/schemas.py +++ b/python-backend/musicmouse/services/web/schemas.py @@ -372,7 +372,6 @@ class TippenSettingsIn(BaseModel): class TippenProgressOut(BaseModel): lessons: dict[str, TippenLessonProgressOut] key_stats: dict[str, TippenKeyStatOut] - pearls: int aquarium: list[str] streak: TippenStreakOut settings: TippenSettingsOut @@ -395,7 +394,6 @@ class TippenRunIn(BaseModel): animal: AnimalId points: float passed: bool - pearls: int = Field(ge=0) strokes: list[TippenStrokeIn] = Field(default_factory=list) diff --git a/python-backend/musicmouse/services/web/tippen_api.py b/python-backend/musicmouse/services/web/tippen_api.py index 2178e70..6d377a2 100644 --- a/python-backend/musicmouse/services/web/tippen_api.py +++ b/python-backend/musicmouse/services/web/tippen_api.py @@ -102,7 +102,6 @@ def progress_out(progress: TypingProgress) -> TippenProgressOut: key: TippenKeyStatOut(ema=stat.ema, attempts=stat.attempts, errors=stat.errors) for key, stat in progress.key_stats.items() }, - pearls=progress.pearls, aquarium=list(progress.aquarium), streak=TippenStreakOut(days=progress.streak.days, last_played=progress.streak.last_played), settings=TippenSettingsOut( @@ -159,7 +158,6 @@ def record_tippen_run(app: App, body: TippenRunIn) -> TippenRunOut: animal=body.animal, points=body.points, passed=body.passed, - pearls=body.pearls, strokes=tuple( Stroke(key=s.key, expected=s.expected, correct=s.correct, at=s.at) for s in body.strokes ), diff --git a/python-backend/musicmouse/tippen/progress.py b/python-backend/musicmouse/tippen/progress.py index 798e19f..45b9050 100644 --- a/python-backend/musicmouse/tippen/progress.py +++ b/python-backend/musicmouse/tippen/progress.py @@ -90,7 +90,6 @@ class RunResult: animal: AnimalId points: float passed: bool - pearls: int strokes: tuple[Stroke, ...] @@ -143,7 +142,6 @@ class TypingProgress(BaseModel): version: Literal[1] = 1 lessons: dict[str, LessonProgress] = Field(default_factory=dict) key_stats: dict[str, KeyStat] = Field(default_factory=dict) - pearls: int = 0 #: Pets that have moved into the aquarium, in the order they arrived. aquarium: list[CreatureId] = Field(default_factory=list) streak: Streak = Field(default_factory=Streak) @@ -265,7 +263,7 @@ def record_run( curriculum: Curriculum, day: str | None = None, ) -> RecordOutcome: - """Record a finished run: stars, animal, pearls, key stats, streak, and the unlock. + """Record a finished run: stars, animal, key stats, streak, and the unlock. The unlock rule, in one place: two stars unlocks the next lesson, and so does the fifth attempt whatever the score. Speed is nowhere in it. @@ -317,7 +315,6 @@ def record_run( "lessons": lessons, "aquarium": aquarium, "key_stats": _fold_key_stats(progress.key_stats, result.strokes), - "pearls": progress.pearls + result.pearls, "streak": _bump_streak(progress.streak, day), } ) diff --git a/python-backend/tests/test_tippen_api.py b/python-backend/tests/test_tippen_api.py index c4301f9..dff0f65 100644 --- a/python-backend/tests/test_tippen_api.py +++ b/python-backend/tests/test_tippen_api.py @@ -56,7 +56,6 @@ def _run_body(lesson_id: str, *, stars: int = 3, passed: bool = True) -> dict: "animal": "fish", "points": 42.0, "passed": passed, - "pearls": 5, "strokes": [{"key": "a", "expected": "a", "correct": True, "at": 0.0}], } @@ -171,7 +170,6 @@ async def test_get_progress_is_fresh_with_only_the_first_lesson_unlocked( body = (await client.get("/api/tippen/progress")).json() assert body["lessons"]["l01"]["unlocked"] is True assert body["lessons"]["l02"]["unlocked"] is False - assert body["pearls"] == 0 async def test_put_settings_round_trips(client: httpx2.AsyncClient) -> None: diff --git a/python-backend/tests/test_tippen_progress.py b/python-backend/tests/test_tippen_progress.py index 8a3a180..b31a813 100644 --- a/python-backend/tests/test_tippen_progress.py +++ b/python-backend/tests/test_tippen_progress.py @@ -53,13 +53,12 @@ def _curriculum() -> Curriculum: return Curriculum(worlds=worlds, lessons=lessons) -def _result(*, stars: int, passed: bool, points: float = 10.0, pearls: int = 3) -> RunResult: +def _result(*, stars: int, passed: bool, points: float = 10.0) -> RunResult: return RunResult( stars=stars, # type: ignore[arg-type] animal="fish", points=points, passed=passed, - pearls=pearls, strokes=(Stroke(key="a", expected="a", correct=True, at=0.0),), ) @@ -99,7 +98,6 @@ def test_save_then_load_round_trips(tmp_path: Path) -> None: assert reloaded.lessons["l01"].best_stars == 3 assert reloaded.lessons["l02"].unlocked is True - assert reloaded.pearls == 3 # Atomic write leaves no temp file behind. assert list(tmp_path.glob("*.tmp*")) == [] @@ -209,16 +207,15 @@ def test_finishing_a_world_awards_its_creature_exactly_once() -> None: assert step3.progress.aquarium == ["clownfish"] -def test_pearls_and_streak_accumulate() -> None: +def test_streak_accumulates() -> None: curriculum = _curriculum() progress = fresh_progress(curriculum) day1 = record_run( - progress, "l01", _result(stars=1, passed=False, pearls=3), curriculum, day="2026-01-01" + progress, "l01", _result(stars=1, passed=False), curriculum, day="2026-01-01" ) day2 = record_run( - day1.progress, "l01", _result(stars=1, passed=False, pearls=4), curriculum, day="2026-01-02" + day1.progress, "l01", _result(stars=1, passed=False), curriculum, day="2026-01-02" ) - assert day2.progress.pearls == 7 assert day2.progress.streak.days == 2 assert day2.progress.streak.last_played == "2026-01-02" diff --git a/web/src/api/types.ts b/web/src/api/types.ts index a0d605d..e200d54 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -236,7 +236,6 @@ export interface TippenSettings { export interface TippenProgress { lessons: Record; key_stats: Record; - pearls: number; aquarium: string[]; streak: TippenStreak; settings: TippenSettings; @@ -257,7 +256,6 @@ export interface TippenRunInput { animal: string; points: number; passed: boolean; - pearls: number; strokes: TippenStroke[]; } diff --git a/web/src/components/TippenApp.tsx b/web/src/components/TippenApp.tsx index 24b06c1..7c3fada 100644 --- a/web/src/components/TippenApp.tsx +++ b/web/src/components/TippenApp.tsx @@ -291,7 +291,6 @@ export function TippenApp({ onExit }: Props) { compact={screen === "run"} status={
- 🦪 {progress.pearls} {!progress.settings.sound && 🔇}
} diff --git a/web/src/components/tippen/AppHeader.tsx b/web/src/components/tippen/AppHeader.tsx index a74672e..72312db 100644 --- a/web/src/components/tippen/AppHeader.tsx +++ b/web/src/components/tippen/AppHeader.tsx @@ -5,7 +5,7 @@ import type { ReactNode } from "react"; interface Props { title?: string; - /** Shown on the right - pearls, streak, a back hint. */ + /** Shown on the right - streak, best animal, a back hint. */ status?: ReactNode; /** Smaller header while a lesson is running, so the target line gets the room. */ compact?: boolean; diff --git a/web/src/components/tippen/Aquarium.tsx b/web/src/components/tippen/Aquarium.tsx index 02ab6d1..1c40f7c 100644 --- a/web/src/components/tippen/Aquarium.tsx +++ b/web/src/components/tippen/Aquarium.tsx @@ -90,7 +90,6 @@ export function Aquarium({ worlds, progress, nextLesson, onContinue, onOpenMap } )}
- -
{/* How close the next animal is. A near miss is the strongest reason to press diff --git a/web/src/lib/tippen/__tests__/grading.test.ts b/web/src/lib/tippen/__tests__/grading.test.ts index 985ebce..b217d14 100644 --- a/web/src/lib/tippen/__tests__/grading.test.ts +++ b/web/src/lib/tippen/__tests__/grading.test.ts @@ -67,9 +67,6 @@ describe("grade", () => { expect(grade(state).errors).toBe(1); }); - it("awards pearls even for a bad run", () => { - expect(grade(run("a".repeat(20), [0, 1, 2, 3, 4, 5, 6, 7])).pearls).toBeGreaterThan(0); - }); }); describe("stars", () => { diff --git a/web/src/lib/tippen/__tests__/progress.test.ts b/web/src/lib/tippen/__tests__/progress.test.ts index 7d382df..9b7eb5d 100644 --- a/web/src/lib/tippen/__tests__/progress.test.ts +++ b/web/src/lib/tippen/__tests__/progress.test.ts @@ -13,7 +13,6 @@ function basicProgress(over: Partial = {}): Progress { return { lessons: {}, keyStats: {}, - pearls: 0, aquarium: [], streak: { days: 0, lastPlayed: null }, settings: { sound: true, keyboardHint: "auto" }, diff --git a/web/src/lib/tippen/engine.ts b/web/src/lib/tippen/engine.ts index a0457d7..7ef6ae5 100644 --- a/web/src/lib/tippen/engine.ts +++ b/web/src/lib/tippen/engine.ts @@ -117,7 +117,7 @@ export function press(state: RunState, key: string, now: number): [RunState, Run } /** Give up on the rest of the line - what Escape does. The run is still graded on what - * was typed, so a half-finished bubbles round still earns its pearls. */ + * was typed, so a half-finished bubbles round still earns its stars. */ export function abandonRun(state: RunState, now: number): RunState { if (isFinished(state) || state.startedAt === null) return state; return { ...state, finishedAt: now }; diff --git a/web/src/lib/tippen/grading.ts b/web/src/lib/tippen/grading.ts index 92b47ae..6d83a9b 100644 --- a/web/src/lib/tippen/grading.ts +++ b/web/src/lib/tippen/grading.ts @@ -35,8 +35,6 @@ export interface RunResult { animal: AnimalId; /** Whether this run unlocks the next lesson on its own. */ passed: boolean; - /** Pearls earned - the aquarium currency. */ - pearls: number; /** Kept for the race-mode ghost and the per-key stats. */ strokes: readonly Stroke[]; } @@ -162,12 +160,6 @@ export function isPassed(accuracy: number): boolean { return accuracy >= STAR_THRESHOLDS.two; } -/** One pearl per five correct keys, plus a bonus per star. Small numbers that go up - * every single run, including a bad one - the aquarium should never stall. */ -function pearlsFor(characters: number, stars: number): number { - return Math.floor(characters / 5) + stars * 2; -} - export function grade(state: RunState): RunResult { const characters = state.strokes.filter((stroke) => stroke.correct).length; const errors = state.missed.size; @@ -196,7 +188,6 @@ export function grade(state: RunState): RunResult { stars, animal: animalFor(points).id, passed: isPassed(accuracy), - pearls: pearlsFor(characters, stars), strokes: state.strokes, }; } diff --git a/web/src/lib/tippen/progress.ts b/web/src/lib/tippen/progress.ts index 785c32d..19c6f43 100644 --- a/web/src/lib/tippen/progress.ts +++ b/web/src/lib/tippen/progress.ts @@ -37,7 +37,6 @@ export interface Settings { export interface Progress { lessons: Record; keyStats: Record; - pearls: number; /** Pets that have moved into the aquarium, in the order they arrived. */ aquarium: CreatureId[]; streak: { days: number; lastPlayed: string | null }; @@ -63,7 +62,6 @@ export function progressFromApi(progress: ApiProgress): Progress { return { lessons, keyStats, - pearls: progress.pearls, aquarium: progress.aquarium as CreatureId[], streak: { days: progress.streak.days, lastPlayed: progress.streak.last_played }, settings: { sound: progress.settings.sound, keyboardHint: progress.settings.keyboard_hint }, @@ -83,7 +81,6 @@ export function toRunInput(lessonId: string, result: RunResult): TippenRunInput animal: result.animal, points: result.points, passed: result.passed, - pearls: result.pearls, strokes, }; }