From b6342cc117886747830218fbec54a310e17795c3 Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Sat, 19 Sep 2026 18:05:08 +0200 Subject: [PATCH] Drop the pearls currency Pearls were earned every run and spent on nothing: the aquarium fills up by finishing worlds, not by paying for it. A counter that only ever goes up is one more stat competing for attention on the result sheet and the home screen, and one more field to carry through the run payload, the progress file and both test suites. Stars and the animal ladder already say how a run went, so nothing is lost. Co-Authored-By: Claude Opus 5 --- python-backend/musicmouse/services/web/schemas.py | 2 -- python-backend/musicmouse/services/web/tippen_api.py | 2 -- python-backend/musicmouse/tippen/progress.py | 5 +---- python-backend/tests/test_tippen_api.py | 2 -- python-backend/tests/test_tippen_progress.py | 11 ++++------- web/src/api/types.ts | 2 -- web/src/components/TippenApp.tsx | 1 - web/src/components/tippen/AppHeader.tsx | 2 +- web/src/components/tippen/Aquarium.tsx | 1 - web/src/components/tippen/ResultSheet.tsx | 1 - web/src/lib/tippen/__tests__/grading.test.ts | 3 --- web/src/lib/tippen/__tests__/progress.test.ts | 1 - web/src/lib/tippen/engine.ts | 2 +- web/src/lib/tippen/grading.ts | 9 --------- web/src/lib/tippen/progress.ts | 3 --- 15 files changed, 7 insertions(+), 40 deletions(-) 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, }; }