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 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 18:05:08 +02:00
parent fd6283718c
commit b6342cc117
15 changed files with 7 additions and 40 deletions

View File

@@ -236,7 +236,6 @@ export interface TippenSettings {
export interface TippenProgress {
lessons: Record<string, TippenLessonProgress>;
key_stats: Record<string, TippenKeyStat>;
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[];
}

View File

@@ -291,7 +291,6 @@ export function TippenApp({ onExit }: Props) {
compact={screen === "run"}
status={
<div style={{ display: "flex", gap: 12, alignItems: "center", color: "var(--paper)", fontWeight: 800 }}>
<span>🦪 {progress.pearls}</span>
{!progress.settings.sound && <span title="Ton aus">🔇</span>}
</div>
}

View File

@@ -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;

View File

@@ -90,7 +90,6 @@ export function Aquarium({ worlds, progress, nextLesson, onContinue, onOpenMap }
)}
<div style={{ display: "flex", justifyContent: "center", gap: 30, marginTop: 14 }}>
<Stat label="Perlen" value={`🦪 ${progress.pearls}`} />
<Stat label="Tage am Stück" value={`🔥 ${progress.streak.days}`} />
<Stat
label="Schnellstes Tier"

View File

@@ -93,7 +93,6 @@ export function ResultSheet({
<div style={{ display: "flex", justifyContent: "center", gap: 26, marginTop: 12 }}>
<Stat label="Richtig" value={`${Math.round(result.accuracy * 100)}%`} />
<Stat label="Zeichen/Min" value={String(Math.round(result.speed))} />
<Stat label="Perlen" value={`+${result.pearls}`} />
</div>
{/* How close the next animal is. A near miss is the strongest reason to press

View File

@@ -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", () => {

View File

@@ -13,7 +13,6 @@ function basicProgress(over: Partial<Progress> = {}): Progress {
return {
lessons: {},
keyStats: {},
pearls: 0,
aquarium: [],
streak: { days: 0, lastPlayed: null },
settings: { sound: true, keyboardHint: "auto" },

View File

@@ -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 };

View File

@@ -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,
};
}

View File

@@ -37,7 +37,6 @@ export interface Settings {
export interface Progress {
lessons: Record<string, LessonProgress>;
keyStats: Record<string, KeyStat>;
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,
};
}