def test_irregular_beats_per_frame_2() -> None: chart = """ b=1 ①□□□ □□□□ □□□□ □□□□ -- □□□□ □①□□ □□□□ □□□□ -- b=2.75 □□□□ □□□□ □□①□ □□□□ -- □□□□ □□□□ □□□□ □□□① -- """ expected = [ TapNote(BeatsTime("0.00"), NotePosition(0, 0)), TapNote(BeatsTime("1.00"), NotePosition(1, 1)), TapNote(BeatsTime("2.00"), NotePosition(2, 2)), TapNote(BeatsTime("4.75"), NotePosition(3, 3)), ] compare_chart_notes(chart, expected)
def test_circle_free() -> None: chart = """ #holdbyarrow=1 #circlefree=1 □□□□ □□□□ □□□□ >□□① -- □□□□ □□□□ □□□□ □□□13 -- """ expected = [ LongNote(BeatsTime(0), NotePosition(3, 3), BeatsTime(7), NotePosition(0, 3)) ] compare_chart_notes(chart, expected)
def _iter_notes_without_longs(self) -> Iterator[TapNote]: for currently_defined_symbols, frame in self._iter_frames(): # cross compare symbols with the position information for y, x in product(range(4), range(4)): symbol = frame.position_part[y][x] try: symbol_time = currently_defined_symbols[symbol] except KeyError: continue position = NotePosition(x, y) yield TapNote(symbol_time, position)
def _iter_notes_without_longs(self) -> Iterator[TapNote]: section_starting_beat = BeatsTime(0) for section in self.sections: for bloc, y, x in product(section.blocs(), range(4), range(4)): symbol = bloc[y][x] if symbol in section.symbols: symbol_time = section.symbols[symbol] note_time = section_starting_beat + symbol_time position = NotePosition(x, y) yield TapNote(note_time, position) section_starting_beat += section.length
def test_simple_section() -> None: chart = """ ①□□□ □⑤□□ □□⑨□ □□□⑬ ------- """ expected = [ TapNote(time=BeatsTime(i), position=NotePosition(i, i)) for i in range(4) ] compare_chart_notes(chart, expected)
def test_long_notes_simple_solution_no_warning() -> None: chart = """ #holdbyarrow=1 □□□□ >①①< □□□□ □□□□ -- □□□□ □①①□ □□□□ □□□□ -- """ expected = [ LongNote(BeatsTime(0), NotePosition(x, y), BeatsTime(4), NotePosition(tx, ty)) for (x, y), (tx, ty) in [ ((1, 1), (0, 1)), ((2, 1), (3, 1)), ] ] compare_chart_notes(chart, expected)
def _iter_notes(self) -> Iterator[Union[TapNote, LongNote]]: unfinished_longs: Dict[NotePosition, UnfinishedLongNote] = {} for section_starting_beat, section, bloc in self._iter_blocs(): should_skip: Set[NotePosition] = set() # 1/3 : look for ends to unfinished long notes for pos, unfinished_long in unfinished_longs.items(): x, y = astuple(pos) symbol = bloc[y][x] if self.circle_free and symbol in CIRCLE_FREE_SYMBOLS: should_skip.add(pos) symbol_time = CIRCLE_FREE_TO_BEATS_TIME[symbol] note_time = section_starting_beat + symbol_time yield unfinished_long.ends_at(note_time) elif symbol in section.symbols: should_skip.add(pos) symbol_time = section.symbols[symbol] note_time = section_starting_beat + symbol_time yield unfinished_long.ends_at(note_time) unfinished_longs = { k: unfinished_longs[k] for k in unfinished_longs.keys() - should_skip } # 2/3 : look for new long notes starting on this bloc arrow_to_note_candidates = find_long_note_candidates( bloc, section.symbols.keys(), should_skip) if arrow_to_note_candidates: solution = pick_correct_long_note_candidates( arrow_to_note_candidates, bloc, ) for arrow_pos, note_pos in solution.items(): should_skip.add(arrow_pos) should_skip.add(note_pos) symbol = bloc[note_pos.y][note_pos.x] symbol_time = section.symbols[symbol] note_time = section_starting_beat + symbol_time unfinished_longs[note_pos] = UnfinishedLongNote( time=note_time, position=note_pos, tail_tip=arrow_pos) # 3/3 : find regular notes for y, x in product(range(4), range(4)): position = NotePosition(x, y) if position in should_skip: continue symbol = bloc[y][x] if symbol in section.symbols: symbol_time = section.symbols[symbol] note_time = section_starting_beat + symbol_time yield TapNote(note_time, position)
def test_long_notes_ambiguous_case() -> None: chart = """ #holdbyarrow=1 ①①<< □□□□ □□□□ □□□□ -- ①①□□ □□□□ □□□□ □□□□ -- """ expected = [ LongNote(BeatsTime(0), NotePosition(x, y), BeatsTime(4), NotePosition(tx, ty)) for (x, y), (tx, ty) in [ ((0, 0), (2, 0)), ((1, 0), (3, 0)), ] ] with pytest.warns(UserWarning): compare_chart_notes(chart, expected)
def test_long_notes() -> None: chart = """ #holdbyarrow=1 ①□□< □□□□ □□□□ □□□□ -- ①□□□ □□□□ □□□□ □□□□ -- """ expected = [ LongNote( time=BeatsTime(0), position=NotePosition(0, 0), duration=BeatsTime(4), tail_tip=NotePosition(3, 0), ) ] compare_chart_notes(chart, expected)
def test_long_notes_complex_case() -> None: chart = """ #holdbyarrow=1 □□□□ □□∨□ □∨□□ >①①① -- □□□□ □□□□ □□□□ □①①① -- """ expected = [ LongNote(BeatsTime(0), NotePosition(x, y), BeatsTime(4), NotePosition(tx, ty)) for (x, y), (tx, ty) in [ ((1, 3), (1, 2)), ((2, 3), (2, 1)), ((3, 3), (0, 3)), ] ] compare_chart_notes(chart, expected)
def find_long_note_candidates( bloc: List[List[str]], note_symbols: AbstractSet[str], should_skip: AbstractSet[NotePosition], ) -> Dict[NotePosition, Set[NotePosition]]: "Return a dict of arrow position to landing note candidates" arrow_to_note_candidates: Dict[NotePosition, Set[NotePosition]] = {} for y, x in product(range(4), range(4)): pos = NotePosition(x, y) if pos in should_skip: continue symbol = bloc[y][x] if symbol not in LONG_ARROWS: continue # at this point we are sure we have a long arrow # we need to check in its direction for note candidates note_candidates = set() 𝛿pos = LONG_DIRECTION[symbol] candidate = NotePosition(x, y) + 𝛿pos while True: try: candidate = NotePosition.from_raw_position(candidate) except ValueError: break if candidate not in should_skip: new_symbol = bloc[candidate.y][candidate.x] if new_symbol in note_symbols: note_candidates.add(candidate) candidate += 𝛿pos # if no notes have been crossed, we just ignore the arrow if note_candidates: arrow_to_note_candidates[pos] = note_candidates return arrow_to_note_candidates
def long_note( draw: DrawFunc, time_strat: st.SearchStrategy[BeatsTime] = beat_time(max_section=10), duration_strat: st.SearchStrategy[BeatsTime] = beat_time(min_numerator=1, max_section=3), ) -> LongNote: time = draw(time_strat) position = draw(note_position()) duration = draw(duration_strat) tail_is_vertical = draw(st.booleans()) tail_offset = draw(st.integers(min_value=1, max_value=3)) if tail_is_vertical: x = position.x y = (position.y + tail_offset) % 4 else: x = (position.x + tail_offset) % 4 y = position.y tail_tip = NotePosition(x, y) return LongNote(time, position, duration, tail_tip)
def test_half_width_symbols() -> None: chart = """ b=7 *⑲:4.5 *21:5 *25:6 口⑪①口 ①⑮⑤⑪ ⑤口口⑮ ⑨口口⑨ 21口口⑲ 25口口25 口⑲21口 25口口25 ---------- """ expected = [ TapNote(BeatsTime(t), NotePosition(x, y)) for t, x, y in [ ("0.0", 2, 0), ("0.0", 0, 1), ("1.0", 2, 1), ("1.0", 0, 2), ("2.0", 0, 3), ("2.0", 3, 3), ("2.5", 1, 0), ("2.5", 3, 1), ("3.5", 1, 1), ("3.5", 3, 2), ("4.5", 1, 2), ("4.5", 3, 0), ("5.0", 0, 0), ("5.0", 2, 2), ("6.0", 0, 1), ("6.0", 3, 1), ("6.0", 0, 3), ("6.0", 3, 3), ] ] compare_chart_notes(chart, expected)
def notes( draw: DrawFunc, collisions: bool = False, note_strat: st.SearchStrategy[Union[TapNote, LongNote]] = st.one_of( tap_note(), long_note()), beat_time_strat: st.SearchStrategy[BeatsTime] = beat_time(max_section=3), ) -> Set[Union[TapNote, LongNote]]: raw_notes: Set[Union[TapNote, LongNote]] = draw(st.sets(note_strat, max_size=32)) if collisions: return raw_notes else: last_notes: Dict[NotePosition, Optional[BeatsTime]] = { NotePosition(x, y): None for y, x in product(range(4), range(4)) } notes: Set[Union[TapNote, LongNote]] = set() for note in sorted(raw_notes, key=lambda n: (n.time, n.position)): last_note_time = last_notes[note.position] if last_note_time is None: new_time = draw(beat_time_strat) else: numerator = draw( st.integers(min_value=1, max_value=last_note_time.denominator * 4)) distance = BeatsTime(numerator, last_note_time.denominator) new_time = last_note_time + distance if isinstance(note, LongNote): notes.add( LongNote( time=new_time, position=note.position, duration=note.duration, tail_tip=note.tail_tip, )) last_notes[note.position] = new_time + note.duration else: notes.add(TapNote(time=new_time, position=note.position)) last_notes[note.position] = new_time return notes
def test_symbol_definition() -> None: chart = """ *A:2 //⑨と同タイミング *B:2.125 *C:2.25 //⑩と同じ *D:2.375 *E:2.5 //⑪と同じ *F:2.625 *G:2.75 //⑫と同じ *H:2.875 *I:3 //⑬と同じ *J:3.125 *K:3.25 //⑭と同じ *L:3.375 ABCD L③□E K⑦□F JIHG -- """ expected = [ TapNote(BeatsTime(t), NotePosition(x, y)) for t, x, y in [ ("4/8", 1, 1), ("12/8", 1, 2), ("16/8", 0, 0), ("17/8", 1, 0), ("18/8", 2, 0), ("19/8", 3, 0), ("20/8", 3, 1), ("21/8", 3, 2), ("22/8", 3, 3), ("23/8", 2, 3), ("24/8", 1, 3), ("25/8", 0, 3), ("26/8", 0, 2), ("27/8", 0, 1), ] ] compare_chart_notes(chart, expected)
def test_compound_section() -> None: chart = """ □①①□ □⑩⑪□ ④⑧⑨⑤ ③⑥⑦③ ⑯⑫⑬⑯ □□□□ □□□□ ⑭□□⑭ ------------- 2 """ expected = [ TapNote(time=BeatsTime("1/4") * (t - 1), position=NotePosition(x, y)) for t, x, y in [ (1, 1, 0), (1, 2, 0), (3, 0, 3), (3, 3, 3), (4, 0, 2), (5, 3, 2), (6, 1, 3), (7, 2, 3), (8, 1, 2), (9, 2, 2), (10, 1, 1), (11, 2, 1), (12, 1, 0), (13, 2, 0), (14, 0, 3), (14, 3, 3), (16, 0, 0), (16, 3, 0), ] ] compare_chart_notes(chart, expected)
def dump_positions(self) -> Iterator[str]: for y in range(4): yield "".join( self.positions.get(NotePosition(x, y), EMPTY_POSITION_SYMBOL) for x in range(4))
def _iter_notes(self) -> Iterator[Union[TapNote, LongNote]]: unfinished_longs: Dict[NotePosition, UnfinishedLongNote] = {} for ( currently_defined_symbols, frame, section_starting_beat, section, ) in self._iter_frames(): should_skip: Set[NotePosition] = set() # 1/3 : look for ends to unfinished long notes for pos, unfinished_long in unfinished_longs.items(): x, y = astuple(pos) symbol = frame.position_part[y][x] if self.circle_free and symbol in CIRCLE_FREE_SYMBOLS: circled_symbol = CIRCLE_FREE_TO_NOTE_SYMBOL[symbol] try: symbol_time = currently_defined_symbols[circled_symbol] except KeyError: raise SyntaxError( "Chart section positional part constains the circle free " f"symbol '{symbol}' but the associated circled symbol " f"'{circled_symbol}' could not be found in the timing part:\n" f"{section}") else: try: symbol_time = currently_defined_symbols[symbol] except KeyError: continue should_skip.add(pos) note_time = section_starting_beat + symbol_time yield unfinished_long.ends_at(note_time) unfinished_longs = { k: unfinished_longs[k] for k in unfinished_longs.keys() - should_skip } # 2/3 : look for new long notes starting on this bloc arrow_to_note_candidates = find_long_note_candidates( frame.position_part, currently_defined_symbols.keys(), should_skip) if arrow_to_note_candidates: solution = pick_correct_long_note_candidates( arrow_to_note_candidates, frame.position_part, ) for arrow_pos, note_pos in solution.items(): should_skip.add(arrow_pos) should_skip.add(note_pos) symbol = frame.position_part[note_pos.y][note_pos.x] symbol_time = currently_defined_symbols[symbol] note_time = section_starting_beat + symbol_time unfinished_longs[note_pos] = UnfinishedLongNote( time=note_time, position=note_pos, tail_tip=arrow_pos) # 3/3 : find regular notes for y, x in product(range(4), range(4)): position = NotePosition(x, y) if position in should_skip: continue symbol = frame.position_part[y][x] try: symbol_time = currently_defined_symbols[symbol] except KeyError: continue note_time = section_starting_beat + symbol_time yield TapNote(note_time, position)
def _dump_frame(frame: Dict[NotePosition, str]) -> Iterator[str]: for y in range(4): yield "".join(frame.get(NotePosition(x, y), "□") for x in range(4))
from fractions import Fraction from jubeatools.song import LongNote, NotePosition, TapNote notes = { TapNote(time=Fraction(0, 1), position=NotePosition(x=0, y=0)), TapNote(time=Fraction(0, 1), position=NotePosition(x=0, y=1)), TapNote(time=Fraction(0, 1), position=NotePosition(x=0, y=3)), LongNote( time=Fraction(0, 1), position=NotePosition(x=1, y=0), duration=Fraction(1, 16), tail_tip=NotePosition(x=2, y=0), ), TapNote(time=Fraction(0, 1), position=NotePosition(x=1, y=1)), LongNote( time=Fraction(0, 1), position=NotePosition(x=1, y=2), duration=Fraction(1, 3), tail_tip=NotePosition(x=2, y=2), ), LongNote( time=Fraction(1, 8), position=NotePosition(x=0, y=3), duration=Fraction(1, 4), tail_tip=NotePosition(x=2, y=3), ), TapNote(time=Fraction(1, 4), position=NotePosition(x=0, y=0)), TapNote(time=Fraction(5, 16), position=NotePosition(x=1, y=0)), LongNote( time=Fraction(1, 2),
def note_position(draw: DrawFunc) -> NotePosition: x = draw(st.integers(min_value=0, max_value=3)) y = draw(st.integers(min_value=0, max_value=3)) return NotePosition(x, y)