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)
Beispiel #3
0
 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)
Beispiel #4
0
 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)
Beispiel #7
0
    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)
Beispiel #10
0
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)
Beispiel #11
0
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
Beispiel #12
0
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)
Beispiel #13
0
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)
Beispiel #14
0
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
Beispiel #15
0
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)
Beispiel #16
0
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)
Beispiel #17
0
 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))
Beispiel #18
0
    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)
Beispiel #19
0
 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))
Beispiel #20
0
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),
Beispiel #21
0
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)