Welcome to Mankunku

Jazz ear training — call and response. Pick your instrument to get started.

API Reference: Difficulty

Adaptive difficulty algorithm and difficulty level profiles.

Source: src/lib/difficulty/


adaptive.ts

Adaptive difficulty algorithm that adjusts musical complexity based on performance.

Constants

ConstantValueDescription
WINDOW_SIZE25Number of recent scores per dimension
ADVANCE_THRESHOLD0.85Average score to advance
RETREAT_THRESHOLD0.50Average score to retreat
MIN_ATTEMPTS_BETWEEN_CHANGES10Cooldown between difficulty adjustments (per dimension)
MAX_LEVEL100Maximum player level

createInitialAdaptiveState(): AdaptiveState

Returns a fresh state with all values at their defaults (level 1, no scores).

interface AdaptiveState {
  currentLevel: number;                // Average of pitch + rhythm complexity (1-100)
  pitchComplexity: number;             // Pitch difficulty (1-100)
  rhythmComplexity: number;            // Rhythm difficulty (1-100)
  recentScores: number[];              // Circular buffer of last 25 overall scores
  recentPitchScores: number[];         // Circular buffer of last 25 pitch accuracy scores
  recentRhythmScores: number[];        // Circular buffer of last 25 rhythm accuracy scores
  attemptsAtLevel: number;             // Total attempts at current level
  attemptsSinceChange: number;         // Min of pitch/rhythm cooldowns
  pitchAttemptsSinceChange: number;    // Attempts since last pitch complexity change
  rhythmAttemptsSinceChange: number;   // Attempts since last rhythm complexity change
}

processAttempt(state, overall, pitchAccuracy, rhythmAccuracy): AdaptiveState

Process a new attempt and return updated state.

Pitch and rhythm are adjusted independently — each dimension has its own score window and cooldown (minimum 10 attempts between changes per dimension):

  1. Pitch: If pitch accuracy window average ≥ 85% → pitchComplexity++; if < 50% → pitchComplexity--
  2. Rhythm: If rhythm accuracy window average ≥ 85% → rhythmComplexity++; if < 50% → rhythmComplexity--
  3. Hold (50–85%): No change for that dimension
  4. currentLevel = Math.round((pitchComplexity + rhythmComplexity) / 2)

getAdaptiveSummary(state): string

Human-readable summary using the current difficulty band name. E.g. "Beginner 3 (Pitch: 3, Rhythm: 2) — Avg: 78%".

Per-scale / per-key proficiency

Shared single-dimension advancement is also exposed for scale- and key-specific proficiency tracking (see src/lib/types/progress.ts).

FunctionSignatureDescription
createInitialScaleProficiency() → ScaleProficiencyFresh scale proficiency state (level 1, empty window)
createInitialKeyProficiency() → KeyProficiencyFresh key proficiency state (level 1, empty window)
processScaleAttempt(state, overall) → ScaleProficiencySame window + cooldown algorithm as processAttempt, single dimension
processKeyAttempt(state, overall) → KeyProficiencySame as processScaleAttempt, for per-key tracking

params.ts

Difficulty level profiles defining what musical elements are available at each level.

DifficultyProfile interface

interface DifficultyProfile {
  level: number;
  name: string;
  scaleTypes: ScaleFamily[];
  maxInterval: number;
  rhythmTypes: ('whole' | 'half' | 'quarter' | 'eighth' | 'triplet' | 'sixteenth')[];
  swing: boolean;
  syncopation: boolean;
  barsRange: [number, number];
  tempoRange: [number, number];
  keys: PitchClass[];
}

DIFFICULTY_PROFILES: DifficultyProfile[]

10 profiles (levels 1–10).

LevelNameScale FamiliesRhythmTempoKeys
1Roots & 5thsmajorquarter60–80C, F, G
2Full Pentatonicmajor, pentatonicquarter60–90C, D, F, G, Bb
3Swing 8thsmajor, pentatonicquarter, eighth70–1007 keys
4Diatonic Lines+bluesquarter, eighth80–120all 12
5Approach Notes+bebop+triplet90–140all 12
6Enclosures+melodic-minor+triplet100–160all 12
7Bebop Lines+harmonic-minor+sixteenth120–180all 12
8Altered Harmony+symmetric+sixteenth140–200all 12
9Complex Rhythmsame as 8all160–240all 12
10No Limitssame as 8all180–300all 12

levelToContentTier(playerLevel): number

Maps player levels 1-100 to content tiers 1-10. E.g., levels 1-5 → tier 1, levels 91-100 → tier 10.

getProfile(level): DifficultyProfile

Returns the profile for a level. Accepts both content tiers (1-10) and player levels (1-100, auto-mapped via levelToContentTier). Throws if the level is invalid.


calculate.ts

Static difficulty calculator for a finished lick. Used when persisting curated and user-entered licks, and by the combinatorial lick generator.

calculateDifficulty(phrase): DifficultyMetadata

Compute a { level, pitchComplexity, rhythmComplexity, lengthBars } summary (all values clamped to 1–100 except lengthBars). Scores four dimensions and combines them:

Pitch complexity (raw 0–~65):

  • Note count (≤ 25 pts) — 2 notes ≈ trivial, ≥ 14 demanding
  • Intervals (≤ 30 pts) — average + max interval + share of leaps > P5
  • Chromaticism (≤ 25 pts) — share of non-diatonic pitch classes + length of chromatic runs
  • Range (≤ 10 pts) — pitch spread in semitones

Rhythm complexity (raw 0–~65):

  • Density (≤ 25 pts) — notes per bar
  • Fastest subdivision (≤ 30 pts) — sixteenths 30 / triplet-8ths 21 / 8ths 10 / 4ths 3
  • Off-beat notes (≤ 25 pts) — fraction of notes not on a quarter-note grid
  • Variety (≤ 15 pts) — distinct duration values
  • Rests (≤ 5 pts)

Raw sub-scores are multiplied by a 1.5× scaling factor to stretch into the usable 1–70 range so the adaptive system has room to progress. Overall level is weighted 55% pitch / 45% rhythm.


display.ts

Difficulty display utilities — maps 1-100 values to 10 color-coded bands (1–10, 11–20, …, 91–100).

DifficultyDisplay interface

interface DifficultyDisplay {
  band: number;   // 1–10
  label: string;  // e.g. "21-30"
  color: string;  // Hex from green → red
  name: string;   // Band name
}

difficultyBand(difficulty): number

Returns the 1–10 band index for a difficulty value (1–100). Clamped to the valid range.

difficultyColor(difficulty): string

Returns the hex color for a difficulty value. Colors progress from green (easy) through lime / yellow / amber / orange to deep red (hardest).

difficultyDisplay(difficulty): DifficultyDisplay

Returns { band, label, color, name } for a difficulty value.

BandRangeName
11–10Beginner
211–20Elementary
321–30Easy
431–40Moderate
541–50Intermediate
651–60Challenging
761–70Advanced
871–80Expert
981–90Master
1091–100Virtuoso