Files
sheetmusictrainer/src/music/targetPool.test.ts
OpenClaw 15b8705907 Unified target-note controls: key filter + fret window across all modes
- New shared PracticeFilter settings (keyTonic/keyMode/fretWindow), persisted
  via AppSettings.practice, defaults off (behavior unchanged).
- music/targetPool.ts: single source for the effective target pool —
  base playable range ∩ fret window ∩ key/mode pitch classes (9 modes incl.
  pentatonic, same scale machinery as Phrase Mode).
- useGameLogic now builds validNotes via computeTargetPool.
- TargetNoteControls component (key on/off + tonic/mode, fret window on/off +
  min/max) rendered in Controls for all game modes; key is independent of the
  displayed key signature.
- Phrase Mode binds its key/fret-window setup to the same shared filter
  (read/write mirror) and drops the duplicate controls in its setup panel.
- Tests: targetPool.test.ts (7 cases); suite 156/156 green, build ok.
2026-09-14 07:16:29 +00:00

79 lines
3.2 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { computeTargetPool } from './targetPool';
const base = {
instrumentId: 'guitar',
difficulty: 'custom',
tuningId: 'standard',
customMinFret: 0,
customMaxFret: 12,
keyEnabled: false,
keyTonic: 'C',
keyMode: 'major',
fretWindowEnabled: false,
fretMin: 0,
fretMax: 4,
};
describe('computeTargetPool', () => {
it('returns the base playable range when no filters are active', () => {
const pool = computeTargetPool(base);
expect(pool).toContain(40); // low E
expect(pool).toContain(64); // high e (12th fret)
expect(pool.length).toBeGreaterThan(30);
});
it('applies the fret window when enabled', () => {
const pool = computeTargetPool({ ...base, fretWindowEnabled: true, fretMin: 0, fretMax: 4 });
// guitar standard: open strings 40,45,50,55,59,64; window adds up to +4
const lo = Math.min(...pool);
const hi = Math.max(...pool);
expect(lo).toBe(40);
expect(hi).toBe(68); // 64 + 4
expect(pool).toContain(59); // open g
expect(pool).not.toContain(69);
});
it('restricts to pitch classes of the key when key filter is on', () => {
const pool = computeTargetPool({ ...base, keyEnabled: true, keyTonic: 'C', keyMode: 'major' });
const pcs = new Set([0, 2, 4, 5, 7, 9, 11]); // C major
for (const midi of pool) {
expect(pcs.has(((midi % 12) + 12) % 12)).toBe(true);
}
// chromatic notes excluded
expect(pool).not.toContain(61); // C# within range
});
it('combines fret window and key filter', () => {
const pool = computeTargetPool({ ...base, fretWindowEnabled: true, fretMin: 0, fretMax: 5, keyEnabled: true, keyTonic: 'E', keyMode: 'minor' });
const pcs = new Set([4, 6, 7, 9, 11, 0, 2]); // E minor natural: E F# G A B C D
expect(pool.length).toBeGreaterThan(0);
for (const midi of pool) {
expect(pcs.has(((midi % 12) + 12) % 12)).toBe(true);
}
// F (pc 5) and F# (pc 6 is in E minor; F natural is not) — F natural excluded
expect(pool.some(m => ((m % 12) + 12) % 12 === 5)).toBe(false);
});
it('supports modes beyond the single-note key signatures (dorian)', () => {
const pool = computeTargetPool({ ...base, keyEnabled: true, keyTonic: 'D', keyMode: 'dorian' });
const pcs = new Set([2, 4, 5, 7, 9, 11, 0]); // D dorian: D E F G A B C
expect(pool.length).toBeGreaterThan(0);
for (const midi of pool) {
expect(pcs.has(((midi % 12) + 12) % 12)).toBe(true);
}
});
it('falls back to major for an invalid mode id', () => {
const a = computeTargetPool({ ...base, keyEnabled: true, keyTonic: 'C', keyMode: 'major' });
const b = computeTargetPool({ ...base, keyEnabled: true, keyTonic: 'C', keyMode: 'bogus' as string });
expect(b).toEqual(a);
});
it('returns an empty pool rather than crashing for a non-fretted window request', () => {
const pool = computeTargetPool({ ...base, instrumentId: 'piano', fretWindowEnabled: true });
// piano has no fret window: window is ignored, base pool returned
expect(pool.length).toBeGreaterThan(0);
});
});