"""Four-fret hand windows shared by chord and single-note placement. This describes playable regions, not an anatomical fingering solver. Chord and single tones may occupy any frets within the window. Only shifting the window is movement. Actual sounded-note limits remain in fretboard.py. """ import functools import typing import generator.fretboard VERSION = 'hand-window-2' class Motion(typing.NamedTuple): shift: int = 0 fingers: int = 0 strings: int = 0 @property def ergonomics(self): return self.fingers + self.strings def base(shape): return min((p.fret for p in shape), default=0) @functools.lru_cache(maxsize=4096) def regions(shape): if not shape: return tuple([0]) return tuple(range(max(1, max(p.fret for p in shape) - 3), min(base(shape), generator.fretboard.MAX_ANCHOR_FRET) + 1)) @functools.lru_cache(maxsize=32768) def motion(previous, current, old_hand, hand, acoustic=False): if not previous or not current: return Motion() if len(previous) == len(current) == 1: pairs = ((previous[0], current[0]),) unmatched = 0 else: def voices(shape): return {('bass' if acoustic and p.string_number in (5, 6) else p.string_number): p for p in shape} a, b = voices(previous), voices(current) pairs = tuple((a[s], b[s]) for s in a.keys() & b.keys()) unmatched = len(a.keys() ^ b.keys()) return Motion(abs(hand - old_hand), unmatched + sum(abs((a.fret - old_hand) - (b.fret - hand)) for a, b in pairs), sum(abs(a.string_number - b.string_number) for a, b in pairs)) def summary(shifts): shifts = tuple(shifts) return {'total_travel': sum(shifts), 'shift_count': sum(s > 0 for s in shifts), 'largest_shift': max(shifts, default=0)}