mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-11-03 22:03:54 +01:00
- 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.
79 lines
3.2 KiB
TypeScript
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);
|
|
});
|
|
});
|