1
0
mirror of synced 2024-11-14 18:38:03 +01:00

HOLY FUCK #memo PASSED UNIT TESTS

This commit is contained in:
Stepland 2020-07-18 17:31:01 +02:00
parent 0fc63e08aa
commit 0ec5acc086
9 changed files with 314 additions and 257 deletions

View File

@ -5,12 +5,20 @@ from dataclasses import dataclass, field
from decimal import Decimal from decimal import Decimal
from fractions import Fraction from fractions import Fraction
from itertools import chain from itertools import chain
from typing import Dict, Optional, List, Union, Iterator, Callable, Mapping from typing import Callable, Dict, Iterator, List, Mapping, Optional, Union
from more_itertools import collapse, intersperse, mark_ends, windowed from more_itertools import collapse, intersperse, mark_ends, windowed
from sortedcontainers import SortedDict, SortedKeyList from sortedcontainers import SortedDict, SortedKeyList
from jubeatools.song import BeatsTime, TapNote, LongNote, NotePosition, Chart, Timing, Metadata from jubeatools.song import (
BeatsTime,
Chart,
LongNote,
Metadata,
NotePosition,
TapNote,
Timing,
)
from .command import dump_command from .command import dump_command
from .symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS from .symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
@ -28,7 +36,7 @@ COMMAND_ORDER = [
"jacket", "jacket",
"prevpos", "prevpos",
"holdbyarrow", "holdbyarrow",
"circlefree" "circlefree",
] ]
BEATS_TIME_TO_SYMBOL = { BEATS_TIME_TO_SYMBOL = {
@ -48,6 +56,7 @@ DIRECTION_TO_ARROW = {
NotePosition(0, 1): "", # U+2227 : LOGICAL AND NotePosition(0, 1): "", # U+2227 : LOGICAL AND
} }
# do NOT use the regular vertical bar, it will clash with the timing portion
DIRECTION_TO_LINE = { DIRECTION_TO_LINE = {
NotePosition(-1, 0): "", # U+2015 : HORIZONTAL BAR NotePosition(-1, 0): "", # U+2015 : HORIZONTAL BAR
NotePosition(1, 0): "", NotePosition(1, 0): "",
@ -71,6 +80,7 @@ def fraction_to_decimal(frac: Fraction):
"Thanks stackoverflow ! https://stackoverflow.com/a/40468867/10768117" "Thanks stackoverflow ! https://stackoverflow.com/a/40468867/10768117"
return frac.numerator / Decimal(frac.denominator) return frac.numerator / Decimal(frac.denominator)
@dataclass(frozen=True) @dataclass(frozen=True)
class LongNoteEnd: class LongNoteEnd:
time: BeatsTime time: BeatsTime
@ -106,19 +116,6 @@ class JubeatAnalyserDumpedSection(ABC):
symbols: Dict[BeatsTime, str] = field(default_factory=dict) symbols: Dict[BeatsTime, str] = field(default_factory=dict)
notes: List[Union[TapNote, LongNote, LongNoteEnd]] = field(default_factory=list) notes: List[Union[TapNote, LongNote, LongNoteEnd]] = field(default_factory=list)
def render(self, circle_free: bool = False) -> str:
blocs = []
commands = list(self._dump_commands())
if commands:
blocs.append(commands)
symbols = list(self._dump_symbol_definitions())
if symbols:
blocs.append(symbols)
notes = list(self._dump_notes(circle_free))
if notes:
blocs.append(notes)
return "\n".join(collapse([intersperse("", blocs), "--"]))
def _dump_commands(self) -> Iterator[str]: def _dump_commands(self) -> Iterator[str]:
keys = chain(COMMAND_ORDER, self.commands.keys() - set(COMMAND_ORDER)) keys = chain(COMMAND_ORDER, self.commands.keys() - set(COMMAND_ORDER))
for key in keys: for key in keys:
@ -144,7 +141,7 @@ def create_sections_from_chart(
difficulty: str, difficulty: str,
timing: Timing, timing: Timing,
metadata: Metadata, metadata: Metadata,
circle_free: bool circle_free: bool,
) -> Mapping[BeatsTime, JubeatAnalyserDumpedSection]: ) -> Mapping[BeatsTime, JubeatAnalyserDumpedSection]:
sections = SortedDefaultDict(section_factory) sections = SortedDefaultDict(section_factory)

View File

@ -1,10 +1,14 @@
"""Collection of parsing tools that are common to all the jubeat analyser formats""" """Collection of parsing tools that are common to all the jubeat analyser formats"""
import re import re
import warnings
from collections import Counter from collections import Counter
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass from dataclasses import dataclass
from decimal import Decimal from decimal import Decimal
from typing import Dict, List, Tuple from itertools import product
from typing import Dict, Iterator, List, Set, Tuple
import constraint
from jubeatools.song import BeatsTime, BPMEvent, LongNote, NotePosition from jubeatools.song import BeatsTime, BPMEvent, LongNote, NotePosition
@ -65,24 +69,6 @@ def decimal_to_beats(decimal_time: Decimal) -> BeatsTime:
return BeatsTime(decimal_time).limit_denominator(240) return BeatsTime(decimal_time).limit_denominator(240)
def note_distance(a: NotePosition, b: NotePosition) -> float:
return abs(complex(*a.as_tuple()) - complex(*b.as_tuple()))
def long_note_solution_heuristic(
solution: Dict[NotePosition, NotePosition]
) -> Tuple[int, int, int]:
c = Counter(int(note_distance(k, v)) for k, v in solution.items())
return (c[3], c[2], c[1])
def is_simple_solution(solution, domains) -> bool:
return all(
solution[v] == min(domains[v], key=lambda e: note_distance(e, v))
for v in solution.keys()
)
@dataclass(frozen=True) @dataclass(frozen=True)
class UnfinishedLongNote: class UnfinishedLongNote:
time: BeatsTime time: BeatsTime
@ -102,6 +88,94 @@ class UnfinishedLongNote:
) )
def find_long_note_candidates(
bloc: List[List[str]], note_symbols: Set[str], should_skip: Set[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[Tuple[int, int]] = set()
𝛿pos = LONG_DIRECTION[symbol]
candidate = NotePosition(x, y) + 𝛿pos
while 0 <= candidate.x < 4 and 0 <= candidate.y < 4:
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 pick_correct_long_note_candidates(
arrow_to_note_candidates: Dict[NotePosition, Set[NotePosition]],
bloc: List[List[str]],
should_skip: Set[NotePosition],
currently_defined_symbols: Dict[str, Decimal],
section_starting_beat: Decimal,
) -> Iterator[UnfinishedLongNote]:
"""Believe it or not, assigning each arrow to a valid note candidate
involves whipping out a CSP solver"""
problem = constraint.Problem()
for arrow_pos, note_candidates in arrow_to_note_candidates.items():
problem.addVariable(arrow_pos, list(note_candidates))
problem.addConstraint(constraint.AllDifferentConstraint())
solutions = problem.getSolutions()
if not solutions:
raise SyntaxError(
"Invalid long note arrow pattern in bloc :\n"
+ "\n".join("".join(line) for line in bloc)
)
solution = min(solutions, key=long_note_solution_heuristic)
if len(solutions) > 1 and not is_simple_solution(
solution, arrow_to_note_candidates
):
warnings.warn(
"Ambiguous arrow pattern in bloc :\n"
+ "\n".join("".join(line) for line in bloc)
+ "\n"
"The resulting long notes might not be what you expect"
)
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 = currently_defined_symbols[symbol]
note_time = decimal_to_beats(section_starting_beat + symbol_time)
yield UnfinishedLongNote(time=note_time, position=note_pos, tail_tip=arrow_pos)
def note_distance(a: NotePosition, b: NotePosition) -> float:
return abs(complex(*a.as_tuple()) - complex(*b.as_tuple()))
def long_note_solution_heuristic(
solution: Dict[NotePosition, NotePosition]
) -> Tuple[int, int, int]:
c = Counter(int(note_distance(k, v)) for k, v in solution.items())
return (c[3], c[2], c[1])
def is_simple_solution(solution, domains) -> bool:
return all(
solution[v] == min(domains[v], key=lambda e: note_distance(e, v))
for v in solution.keys()
)
class JubeatAnalyserParser: class JubeatAnalyserParser:
def __init__(self): def __init__(self):
self.music = None self.music = None

View File

@ -7,7 +7,7 @@ from functools import partial
from io import StringIO from io import StringIO
from itertools import chain, zip_longest from itertools import chain, zip_longest
from math import ceil from math import ceil
from typing import Dict, Iterator, List, Optional, Tuple, Union, Set from typing import Dict, Iterator, List, Optional, Set, Tuple, Union
from more_itertools import chunked, collapse, intersperse, mark_ends, windowed from more_itertools import chunked, collapse, intersperse, mark_ends, windowed
from path import Path from path import Path
@ -36,19 +36,19 @@ from ..dump_tools import (
DIRECTION_TO_LINE, DIRECTION_TO_LINE,
NOTE_TO_CIRCLE_FREE_SYMBOL, NOTE_TO_CIRCLE_FREE_SYMBOL,
JubeatAnalyserDumpedSection, JubeatAnalyserDumpedSection,
create_sections_from_chart,
LongNoteEnd, LongNoteEnd,
SortedDefaultDict, SortedDefaultDict,
create_sections_from_chart,
fraction_to_decimal, fraction_to_decimal,
) )
from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
AnyNote = Union[TapNote, LongNote, LongNoteEnd] AnyNote = Union[TapNote, LongNote, LongNoteEnd]
EMPTY_BEAT_SYMBOL = "" # U+0FF0D : FULLWIDTH HYPHEN-MINUS EMPTY_BEAT_SYMBOL = "" # U+0FF0D : FULLWIDTH HYPHEN-MINUS
EMPTY_POSITION_SYMBOL = "" # U+025A1 : WHITE SQUARE EMPTY_POSITION_SYMBOL = "" # U+025A1 : WHITE SQUARE
@dataclass @dataclass
class Frame: class Frame:
positions: Dict[NotePosition, str] = field(default_factory=dict) positions: Dict[NotePosition, str] = field(default_factory=dict)
@ -57,10 +57,10 @@ class Frame:
def dump(self, length: Decimal) -> Iterator[str]: def dump(self, length: Decimal) -> Iterator[str]:
# Check that bars are contiguous # Check that bars are contiguous
for a, b in windowed(sorted(self.bars), 2): for a, b in windowed(sorted(self.bars), 2):
if b is not None and b-a != 1: if b is not None and b - a != 1:
raise ValueError("Frame has discontinuous bars") raise ValueError("Frame has discontinuous bars")
# Check all bars are in the same 4-bar group # Check all bars are in the same 4-bar group
if self.bars.keys() != set(bar%4 for bar in self.bars): if self.bars.keys() != set(bar % 4 for bar in self.bars):
raise ValueError("Frame contains bars from different 4-bar groups") raise ValueError("Frame contains bars from different 4-bar groups")
for pos, bar in zip_longest(self.dump_positions(), self.dump_bars(length)): for pos, bar in zip_longest(self.dump_positions(), self.dump_bars(length)):
@ -68,10 +68,12 @@ class Frame:
bar = "" bar = ""
yield f"{pos} {bar}" yield f"{pos} {bar}"
def dump_positions(self) -> Iterator[str]: def dump_positions(self) -> Iterator[str]:
for y in range(4): for y in range(4):
yield "".join(self.positions.get(NotePosition(x, y), EMPTY_POSITION_SYMBOL) for x in range(4)) yield "".join(
self.positions.get(NotePosition(x, y), EMPTY_POSITION_SYMBOL)
for x in range(4)
)
def dump_bars(self, length: Decimal) -> Iterator[str]: def dump_bars(self, length: Decimal) -> Iterator[str]:
all_bars = [] all_bars = []
@ -89,6 +91,19 @@ class Frame:
class MemoDumpedSection(JubeatAnalyserDumpedSection): class MemoDumpedSection(JubeatAnalyserDumpedSection):
def render(self, circle_free: bool = False) -> str:
blocs = []
commands = list(self._dump_commands())
if commands:
blocs.append(commands)
symbols = list(self._dump_symbol_definitions())
if symbols:
blocs.append(symbols)
notes = list(self._dump_notes(circle_free))
if notes:
blocs.append(notes)
return "\n".join(collapse(intersperse("", blocs)))
def _dump_notes(self, circle_free: bool = False) -> Iterator[str]: def _dump_notes(self, circle_free: bool = False) -> Iterator[str]:
notes_by_bar: Dict[int, List[AnyNote]] = defaultdict(list) notes_by_bar: Dict[int, List[AnyNote]] = defaultdict(list)
bars: Dict[int, Dict[int, str]] = defaultdict(dict) bars: Dict[int, Dict[int, str]] = defaultdict(dict)
@ -99,13 +114,16 @@ class MemoDumpedSection(JubeatAnalyserDumpedSection):
bar_index = int(time_in_section) bar_index = int(time_in_section)
notes_by_bar[bar_index].append(note) notes_by_bar[bar_index].append(note)
if time_in_section % Fraction(1, 4) == 0: if time_in_section % Fraction(1, 4) == 0:
time_index = int(time_in_section * 4) time_in_bar = time_in_section % Fraction(1)
time_index = int(time_in_bar * 4)
if time_index not in bars[bar_index]: if time_index not in bars[bar_index]:
symbol = next(symbols_iterator) symbol = next(symbols_iterator)
chosen_symbols[time_in_section] = symbol chosen_symbols[time_in_section] = symbol
bars[bar_index][time_index] = symbol bars[bar_index][time_index] = symbol
elif time_in_section not in self.symbols: elif time_in_section not in self.symbols:
raise ValueError(f"No symbol defined for time in section : {time_in_section}") raise ValueError(
f"No symbol defined for time in section : {time_in_section}"
)
# Create frame by bar # Create frame by bar
section_symbols = ChainMap(chosen_symbols, self.symbols) section_symbols = ChainMap(chosen_symbols, self.symbols)
@ -157,11 +175,14 @@ class MemoDumpedSection(JubeatAnalyserDumpedSection):
# - The previous and current bars are all in the same 4-bar group # - The previous and current bars are all in the same 4-bar group
# - The note positions in the previous frame do not clash with the current frame # - The note positions in the previous frame do not clash with the current frame
if ( if (
len(frames) == 1 and len(frames) == 1
final_frames and and final_frames
final_frames[-1].bars and and final_frames[-1].bars
max(final_frames[-1].bars.keys()) // 4 == min(frames[0].bars.keys()) // 4 and and max(final_frames[-1].bars.keys()) // 4
(not (final_frames[-1].positions.keys() & frames[0].positions.keys())) == min(frames[0].bars.keys()) // 4
and (
not (final_frames[-1].positions.keys() & frames[0].positions.keys())
)
): ):
final_frames[-1].bars.update(frames[0].bars) final_frames[-1].bars.update(frames[0].bars)
final_frames[-1].positions.update(frames[0].positions) final_frames[-1].positions.update(frames[0].positions)
@ -172,7 +193,6 @@ class MemoDumpedSection(JubeatAnalyserDumpedSection):
yield from collapse(intersperse("", dumped_frames)) yield from collapse(intersperse("", dumped_frames))
def _raise_if_unfit_for_memo(chart: Chart, timing: Timing, circle_free: bool = False): def _raise_if_unfit_for_memo(chart: Chart, timing: Timing, circle_free: bool = False):
if len(timing.events) < 1: if len(timing.events) < 1:
raise ValueError("No BPM found in file") from None raise ValueError("No BPM found in file") from None
@ -202,7 +222,9 @@ def _dump_memo_chart(
_raise_if_unfit_for_memo(chart, timing, circle_free) _raise_if_unfit_for_memo(chart, timing, circle_free)
sections = create_sections_from_chart(MemoDumpedSection, chart, difficulty, timing, metadata, circle_free) sections = create_sections_from_chart(
MemoDumpedSection, chart, difficulty, timing, metadata, circle_free
)
# Jubeat Analyser format command # Jubeat Analyser format command
sections[0].commands["memo"] = None sections[0].commands["memo"] = None
@ -216,7 +238,10 @@ def _dump_memo_chart(
section.symbols = existing_symbols section.symbols = existing_symbols
for note in section.notes: for note in section.notes:
time_in_section = note.time - section_start time_in_section = note.time - section_start
if time_in_section % Fraction(1, 4) != 0 and time_in_section not in existing_symbols: if (
time_in_section % Fraction(1, 4) != 0
and time_in_section not in existing_symbols
):
new_symbol = next(extra_symbols) new_symbol = next(extra_symbols)
section.symbol_definitions[time_in_section] = new_symbol section.symbol_definitions[time_in_section] = new_symbol
existing_symbols[time_in_section] = new_symbol existing_symbols[time_in_section] = new_symbol
@ -232,7 +257,7 @@ def _dump_memo_chart(
def _dump_memo_internal(song: Song, circle_free: bool = False) -> List[JubeatFile]: def _dump_memo_internal(song: Song, circle_free: bool = False) -> List[JubeatFile]:
files = [] files: List[JubeatFile] = []
for difficulty, chart in song.charts.items(): for difficulty, chart in song.charts.items():
contents = _dump_memo_chart( contents = _dump_memo_chart(
difficulty, difficulty,

View File

@ -8,7 +8,7 @@ from itertools import chain, product, zip_longest
from typing import Dict, Iterator, List, Mapping, Optional, Set, Tuple, Union from typing import Dict, Iterator, List, Mapping, Optional, Set, Tuple, Union
import constraint import constraint
from more_itertools import mark_ends, collapse from more_itertools import collapse, mark_ends
from parsimonious import Grammar, NodeVisitor, ParseError from parsimonious import Grammar, NodeVisitor, ParseError
from path import Path from path import Path
@ -33,9 +33,11 @@ from ..load_tools import (
JubeatAnalyserParser, JubeatAnalyserParser,
UnfinishedLongNote, UnfinishedLongNote,
decimal_to_beats, decimal_to_beats,
find_long_note_candidates,
is_empty_line, is_empty_line,
is_simple_solution, is_simple_solution,
long_note_solution_heuristic, long_note_solution_heuristic,
pick_correct_long_note_candidates,
split_double_byte_line, split_double_byte_line,
) )
from ..symbol_definition import is_symbol_definition, parse_symbol_definition from ..symbol_definition import is_symbol_definition, parse_symbol_definition
@ -44,9 +46,8 @@ from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
memo_chart_line_grammar = Grammar( memo_chart_line_grammar = Grammar(
r""" r"""
line = ws position_part ws (timing_part ws)? comment? line = ws position_part ws (timing_part ws)? comment?
position_part = ~r"[^*#:|/\s]{4,8}" position_part = ~r"[^*#:|/\s]{4,8}"
timing_part = bar ~r"[^*#:|/\s]+" bar timing_part = "|" ~r"[^*#:|/\s]+" "|"
bar = "|" / ""
ws = ~r"[\t ]*" ws = ~r"[\t ]*"
comment = ~r"//.*" comment = ~r"//.*"
""" """
@ -265,32 +266,29 @@ class MemoParser(JubeatAnalyserParser):
def _iter_frames( def _iter_frames(
self, self,
) -> Iterator[ ) -> Iterator[Tuple[Mapping[str, Decimal], MemoFrame, Decimal, MemoLoadedSection]]:
Tuple[Mapping[str, Decimal], Decimal, MemoFrame, Decimal, MemoLoadedSection]
]:
"""iterate over tuples of """iterate over tuples of
currently_defined_symbols, frame_starting_beat, frame, section_starting_beat, section""" currently_defined_symbols, frame_starting_beat, frame, section_starting_beat, section"""
local_symbols: Dict[str, Decimal] = {} local_symbols: Dict[str, Decimal] = {}
section_starting_beat = Decimal(0) section_starting_beat = Decimal(0)
for section in self.sections: for section in self.sections:
frame_starting_beat = Decimal(0) frame_starting_beat = Decimal(0)
for frame in section.frames: for i, frame in enumerate(section.frames):
if frame.timing_part: if frame.timing_part:
frame_starting_beat = sum(f.duration for f in section.frames[:i])
local_symbols = { local_symbols = {
symbol: Decimal("0.25") * i symbol: Decimal("0.25") * i + frame_starting_beat
for i, symbol in enumerate(collapse(frame.timing_part)) for i, symbol in enumerate(collapse(frame.timing_part))
if symbol not in EMPTY_BEAT_SYMBOLS if symbol not in EMPTY_BEAT_SYMBOLS
} }
currently_defined_symbols = ChainMap(local_symbols, section.symbols) currently_defined_symbols = ChainMap(local_symbols, section.symbols)
yield currently_defined_symbols, frame_starting_beat, frame, section_starting_beat, section yield currently_defined_symbols, frame, section_starting_beat, section
frame_starting_beat += frame.duration
section_starting_beat += section.length section_starting_beat += section.length
def _iter_notes(self) -> Iterator[Union[TapNote, LongNote]]: def _iter_notes(self) -> Iterator[Union[TapNote, LongNote]]:
unfinished_longs: Dict[NotePosition, UnfinishedLongNote] = {} unfinished_longs: Dict[NotePosition, UnfinishedLongNote] = {}
for ( for (
currently_defined_symbols, currently_defined_symbols,
frame_starting_beat,
frame, frame,
section_starting_beat, section_starting_beat,
section, section,
@ -319,9 +317,7 @@ class MemoParser(JubeatAnalyserParser):
continue continue
should_skip.add(pos) should_skip.add(pos)
note_time = decimal_to_beats( note_time = decimal_to_beats(section_starting_beat + symbol_time)
section_starting_beat + frame_starting_beat + symbol_time
)
yield unfinished_long.ends_at(note_time) yield unfinished_long.ends_at(note_time)
unfinished_longs = { unfinished_longs = {
@ -329,58 +325,21 @@ class MemoParser(JubeatAnalyserParser):
} }
# 2/3 : look for new long notes starting on this bloc # 2/3 : look for new long notes starting on this bloc
arrow_to_note_candidates: Dict[NotePosition, Set[NotePosition]] = {} arrow_to_note_candidates = find_long_note_candidates(
for y, x in product(range(4), range(4)): frame.position_part, currently_defined_symbols.keys(), should_skip
pos = NotePosition(x, y) )
if pos in should_skip:
continue
symbol = frame.position_part[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[Tuple[int, int]] = set()
𝛿pos = LONG_DIRECTION[symbol]
candidate = NotePosition(x, y) + 𝛿pos
while 0 <= candidate.x < 4 and 0 <= candidate.y < 4:
if candidate in should_skip:
continue
new_symbol = frame.position_part[candidate.y][candidate.x]
if new_symbol in currently_defined_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
# Believe it or not, assigning each arrow to a valid note candidate
# involves whipping out a CSP solver
if arrow_to_note_candidates: if arrow_to_note_candidates:
problem = constraint.Problem() unfinished_longs.update(
for arrow_pos, note_candidates in arrow_to_note_candidates.items(): {
problem.addVariable(arrow_pos, list(note_candidates)) note.position: note
problem.addConstraint(constraint.AllDifferentConstraint()) for note in pick_correct_long_note_candidates(
solutions = problem.getSolutions() arrow_to_note_candidates,
if not solutions: frame.position_part,
raise SyntaxError( should_skip,
"Invalid long note arrow pattern in section :\n" + str(section) currently_defined_symbols,
section_starting_beat,
) )
solution = min(solutions, key=long_note_solution_heuristic) }
if len(solutions) > 1 and not is_simple_solution(
solution, arrow_to_note_candidates
):
warnings.warn(
"Ambiguous arrow pattern in section :\n" + str(section) + "\n"
"The chosen long notes might not be what you expect"
)
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 = section.symbols[symbol]
note_time = decimal_to_beats(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 # 3/3 : find regular notes
@ -393,15 +352,12 @@ class MemoParser(JubeatAnalyserParser):
symbol_time = currently_defined_symbols[symbol] symbol_time = currently_defined_symbols[symbol]
except KeyError: except KeyError:
continue continue
note_time = decimal_to_beats( note_time = decimal_to_beats(section_starting_beat + symbol_time)
section_starting_beat + frame_starting_beat + symbol_time
)
yield TapNote(note_time, position) yield TapNote(note_time, position)
def _iter_notes_without_longs(self) -> Iterator[TapNote]: def _iter_notes_without_longs(self) -> Iterator[TapNote]:
for ( for (
currently_defined_symbols, currently_defined_symbols,
frame_starting_beat,
frame, frame,
section_starting_beat, section_starting_beat,
_, _,
@ -413,9 +369,7 @@ class MemoParser(JubeatAnalyserParser):
symbol_time = currently_defined_symbols[symbol] symbol_time = currently_defined_symbols[symbol]
except KeyError: except KeyError:
continue continue
note_time = decimal_to_beats( note_time = decimal_to_beats(section_starting_beat + symbol_time)
section_starting_beat + frame_starting_beat + symbol_time
)
position = NotePosition(x, y) position = NotePosition(x, y)
yield TapNote(note_time, position) yield TapNote(note_time, position)

View File

@ -33,15 +33,28 @@ from ..dump_tools import (
DIRECTION_TO_ARROW, DIRECTION_TO_ARROW,
DIRECTION_TO_LINE, DIRECTION_TO_LINE,
JubeatAnalyserDumpedSection, JubeatAnalyserDumpedSection,
create_sections_from_chart,
LongNoteEnd, LongNoteEnd,
SortedDefaultDict, SortedDefaultDict,
create_sections_from_chart,
fraction_to_decimal, fraction_to_decimal,
) )
from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS from ..symbols import CIRCLE_FREE_SYMBOLS, NOTE_SYMBOLS
class MonoColumnDumpedSection(JubeatAnalyserDumpedSection): class MonoColumnDumpedSection(JubeatAnalyserDumpedSection):
def render(self, circle_free: bool = False) -> str:
blocs = []
commands = list(self._dump_commands())
if commands:
blocs.append(commands)
symbols = list(self._dump_symbol_definitions())
if symbols:
blocs.append(symbols)
notes = list(self._dump_notes(circle_free))
if notes:
blocs.append(notes)
return "\n".join(collapse([intersperse("", blocs), "--"]))
def _dump_notes(self, circle_free: bool = False,) -> Iterator[str]: def _dump_notes(self, circle_free: bool = False,) -> Iterator[str]:
frames: List[Dict[NotePosition, str]] = [] frames: List[Dict[NotePosition, str]] = []
frame: Dict[NotePosition, str] = {} frame: Dict[NotePosition, str] = {}
@ -122,6 +135,7 @@ def _raise_if_unfit_for_mono_column(
" representable in #circlefree mode" " representable in #circlefree mode"
) )
def _dump_mono_column_chart( def _dump_mono_column_chart(
difficulty: str, difficulty: str,
chart: Chart, chart: Chart,
@ -132,7 +146,9 @@ def _dump_mono_column_chart(
_raise_if_unfit_for_mono_column(chart, timing, circle_free) _raise_if_unfit_for_mono_column(chart, timing, circle_free)
sections = create_sections_from_chart(MonoColumnDumpedSection, chart, difficulty, timing, metadata, circle_free) sections = create_sections_from_chart(
MonoColumnDumpedSection, chart, difficulty, timing, metadata, circle_free
)
# Define extra symbols # Define extra symbols
existing_symbols = deepcopy(BEATS_TIME_TO_SYMBOL) existing_symbols = deepcopy(BEATS_TIME_TO_SYMBOL)

View File

@ -36,9 +36,11 @@ from ..load_tools import (
JubeatAnalyserParser, JubeatAnalyserParser,
UnfinishedLongNote, UnfinishedLongNote,
decimal_to_beats, decimal_to_beats,
find_long_note_candidates,
is_empty_line, is_empty_line,
is_simple_solution, is_simple_solution,
long_note_solution_heuristic, long_note_solution_heuristic,
pick_correct_long_note_candidates,
split_double_byte_line, split_double_byte_line,
) )
from ..symbol_definition import is_symbol_definition, parse_symbol_definition from ..symbol_definition import is_symbol_definition, parse_symbol_definition
@ -227,61 +229,21 @@ class MonoColumnParser(JubeatAnalyserParser):
} }
# 2/3 : look for new long notes starting on this bloc # 2/3 : look for new long notes starting on this bloc
arrow_to_note_candidates: Dict[NotePosition, Set[NotePosition]] = {} arrow_to_note_candidates = find_long_note_candidates(
for y, x in product(range(4), range(4)): bloc, section.symbols.keys(), should_skip
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[Tuple[int, int]] = set()
𝛿pos = LONG_DIRECTION[symbol]
candidate = NotePosition(x, y) + 𝛿pos
while 0 <= candidate.x < 4 and 0 <= candidate.y < 4:
if candidate in should_skip:
continue
new_symbol = bloc[candidate.y][candidate.x]
if new_symbol in section.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
# Believe it or not, assigning each arrow to a valid note candidate
# involves whipping out a CSP solver
if arrow_to_note_candidates: if arrow_to_note_candidates:
problem = constraint.Problem() unfinished_longs.update(
for arrow_pos, note_candidates in arrow_to_note_candidates.items(): {
problem.addVariable(arrow_pos, list(note_candidates)) note.position: note
problem.addConstraint(constraint.AllDifferentConstraint()) for note in pick_correct_long_note_candidates(
solutions = problem.getSolutions() arrow_to_note_candidates,
if not solutions: bloc,
raise SyntaxError( should_skip,
"Invalid long note arrow pattern in bloc :\n" section.symbols,
+ "\n".join("".join(line) for line in bloc) section_starting_beat,
) )
solution = min(solutions, key=long_note_solution_heuristic) }
if len(solutions) > 1 and not is_simple_solution(
solution, arrow_to_note_candidates
):
warnings.warn(
"Ambiguous arrow pattern in bloc :\n"
+ "\n".join("".join(line) for line in bloc)
+ "\n"
"The resulting long notes might not be what you expect"
)
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 = decimal_to_beats(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 # 3/3 : find regular notes

View File

@ -1,8 +1,19 @@
from decimal import Decimal from decimal import Decimal
from fractions import Fraction
from hypothesis import given from hypothesis import given
from jubeatools.song import BeatsTime, BPMEvent, Chart, Metadata, SecondsTime, Timing from jubeatools.song import (
BeatsTime,
BPMEvent,
Chart,
LongNote,
Metadata,
NotePosition,
SecondsTime,
TapNote,
Timing,
)
from jubeatools.testutils.strategies import NoteOption from jubeatools.testutils.strategies import NoteOption
from jubeatools.testutils.strategies import notes as notes_strat from jubeatools.testutils.strategies import notes as notes_strat

View File

@ -3,6 +3,7 @@ Hypothesis strategies to generate notes and charts
""" """
from enum import Enum, Flag, auto from enum import Enum, Flag, auto
from itertools import product from itertools import product
from typing import Set, Union
import hypothesis.strategies as st import hypothesis.strategies as st
from multidict import MultiDict from multidict import MultiDict
@ -111,7 +112,7 @@ def notes(draw, options: NoteOption):
return raw_notes return raw_notes
else: else:
last_notes = {NotePosition(x, y): None for y, x in product(range(4), range(4))} last_notes = {NotePosition(x, y): None for y, x in product(range(4), range(4))}
notes = set() notes: Set[Union[TapNote, LongNote]] = set()
for note in sorted(raw_notes, key=lambda n: (n.time, n.position)): for note in sorted(raw_notes, key=lambda n: (n.time, n.position)):
last_note_time = last_notes[note.position] last_note_time = last_notes[note.position]
if last_note_time is None: if last_note_time is None:

137
poetry.lock generated
View File

@ -72,7 +72,7 @@ description = "A library for property-based testing"
name = "hypothesis" name = "hypothesis"
optional = false optional = false
python-versions = ">=3.5.2" python-versions = ">=3.5.2"
version = "5.19.0" version = "5.20.1"
[package.dependencies] [package.dependencies]
attrs = ">=19.2.0" attrs = ">=19.2.0"
@ -109,12 +109,12 @@ description = "A lightweight library for converting complex datatypes to and fro
name = "marshmallow" name = "marshmallow"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "3.6.1" version = "3.7.0"
[package.extras] [package.extras]
dev = ["pytest", "pytz", "simplejson", "mypy (0.770)", "flake8 (3.8.2)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"] dev = ["pytest", "pytz", "simplejson", "mypy (0.782)", "flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"]
docs = ["sphinx (3.0.4)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)", "autodocsumm (0.1.13)"] docs = ["sphinx (3.1.2)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)", "autodocsumm (0.1.13)"]
lint = ["mypy (0.770)", "flake8 (3.8.2)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)"] lint = ["mypy (0.782)", "flake8 (3.8.3)", "flake8-bugbear (20.1.4)", "pre-commit (>=2.4,<3.0)"]
tests = ["pytest", "pytz", "simplejson"] tests = ["pytest", "pytz", "simplejson"]
[[package]] [[package]]
@ -267,7 +267,7 @@ description = "Alternative regular expression module, to replace re."
name = "regex" name = "regex"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "2020.6.8" version = "2020.7.14"
[[package]] [[package]]
category = "dev" category = "dev"
@ -286,7 +286,7 @@ description = "Simple, fast, extensible JSON encoder/decoder for Python"
name = "simplejson" name = "simplejson"
optional = false optional = false
python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*"
version = "3.17.0" version = "3.17.2"
[[package]] [[package]]
category = "main" category = "main"
@ -366,16 +366,16 @@ colorama = [
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
] ]
hypothesis = [ hypothesis = [
{file = "hypothesis-5.19.0-py3-none-any.whl", hash = "sha256:dd21b1be951fefc9022047824c262f4e88d95dd24141b837b92e235c63baabb7"}, {file = "hypothesis-5.20.1-py3-none-any.whl", hash = "sha256:22a3d4388046a02e132fa6889be2a25db70567a218cb73091689a4788c7c9acf"},
{file = "hypothesis-5.19.0.tar.gz", hash = "sha256:ba7c92006716aaee4684f7876c116adedcfb88b19fcb55d21c47b28f03f933bf"}, {file = "hypothesis-5.20.1.tar.gz", hash = "sha256:ee9eac5dd988cb438aa1aeb03b62ee5374160ee4e2b24a7d4a141cc188361979"},
] ]
isort = [ isort = [
{file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"}, {file = "isort-4.3.21-py2.py3-none-any.whl", hash = "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"},
{file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"}, {file = "isort-4.3.21.tar.gz", hash = "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1"},
] ]
marshmallow = [ marshmallow = [
{file = "marshmallow-3.6.1-py2.py3-none-any.whl", hash = "sha256:9aa20f9b71c992b4782dad07c51d92884fd0f7c5cb9d3c737bea17ec1bad765f"}, {file = "marshmallow-3.7.0-py2.py3-none-any.whl", hash = "sha256:0f3a630f6a2fd124929f1bdcb5df65bd14cc8f49f52a18d0bdcfa0c42414e4a7"},
{file = "marshmallow-3.6.1.tar.gz", hash = "sha256:35ee2fb188f0bd9fc1cf9ac35e45fd394bd1c153cee430745a465ea435514bd5"}, {file = "marshmallow-3.7.0.tar.gz", hash = "sha256:ba949379cb6ef73655f72075e82b31cf57012a5557ede642fc8614ab0354f869"},
] ]
more-itertools = [ more-itertools = [
{file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
@ -455,60 +455,77 @@ python-constraint = [
{file = "python-constraint-1.4.0.tar.bz2", hash = "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e"}, {file = "python-constraint-1.4.0.tar.bz2", hash = "sha256:501d6f17afe0032dfc6ea6c0f8acc12e44f992733f00e8538961031ef27ccb8e"},
] ]
regex = [ regex = [
{file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"}, {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"},
{file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"}, {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"},
{file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"}, {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"},
{file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"}, {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"},
{file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"}, {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"},
{file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"}, {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"},
{file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"}, {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"},
{file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"}, {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"},
{file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"}, {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"},
{file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"}, {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"},
{file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"}, {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"},
{file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"}, {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"},
{file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"}, {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"},
{file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"}, {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"},
{file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"}, {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"},
{file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"}, {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"},
{file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"}, {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"},
{file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"}, {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"},
{file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"}, {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"},
{file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"}, {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"},
{file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"}, {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"},
] ]
rope = [ rope = [
{file = "rope-0.17.0.tar.gz", hash = "sha256:658ad6705f43dcf3d6df379da9486529cf30e02d9ea14c5682aa80eb33b649e1"}, {file = "rope-0.17.0.tar.gz", hash = "sha256:658ad6705f43dcf3d6df379da9486529cf30e02d9ea14c5682aa80eb33b649e1"},
] ]
simplejson = [ simplejson = [
{file = "simplejson-3.17.0-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:87d349517b572964350cc1adc5a31b493bbcee284505e81637d0174b2758ba17"}, {file = "simplejson-3.17.2-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:2d3eab2c3fe52007d703a26f71cf649a8c771fcdd949a3ae73041ba6797cfcf8"},
{file = "simplejson-3.17.0-cp27-cp27m-win32.whl", hash = "sha256:1d1e929cdd15151f3c0b2efe953b3281b2fd5ad5f234f77aca725f28486466f6"}, {file = "simplejson-3.17.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:813846738277729d7db71b82176204abc7fdae2f566e2d9fcf874f9b6472e3e6"},
{file = "simplejson-3.17.0-cp27-cp27m-win_amd64.whl", hash = "sha256:1ea59f570b9d4916ae5540a9181f9c978e16863383738b69a70363bc5e63c4cb"}, {file = "simplejson-3.17.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:292c2e3f53be314cc59853bd20a35bf1f965f3bc121e007ab6fd526ed412a85d"},
{file = "simplejson-3.17.0-cp33-cp33m-win32.whl", hash = "sha256:8027bd5f1e633eb61b8239994e6fc3aba0346e76294beac22a892eb8faa92ba1"}, {file = "simplejson-3.17.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0dd9d9c738cb008bfc0862c9b8fa6743495c03a0ed543884bf92fb7d30f8d043"},
{file = "simplejson-3.17.0-cp33-cp33m-win_amd64.whl", hash = "sha256:22a7acb81968a7c64eba7526af2cf566e7e2ded1cb5c83f0906b17ff1540f866"}, {file = "simplejson-3.17.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:42b8b8dd0799f78e067e2aaae97e60d58a8f63582939af60abce4c48631a0aa4"},
{file = "simplejson-3.17.0-cp34-cp34m-win32.whl", hash = "sha256:17163e643dbf125bb552de17c826b0161c68c970335d270e174363d19e7ea882"}, {file = "simplejson-3.17.2-cp27-cp27m-win32.whl", hash = "sha256:8042040af86a494a23c189b5aa0ea9433769cc029707833f261a79c98e3375f9"},
{file = "simplejson-3.17.0-cp34-cp34m-win_amd64.whl", hash = "sha256:0fe3994207485efb63d8f10a833ff31236ed27e3b23dadd0bf51c9900313f8f2"}, {file = "simplejson-3.17.2-cp27-cp27m-win_amd64.whl", hash = "sha256:034550078a11664d77bc1a8364c90bb7eef0e44c2dbb1fd0a4d92e3997088667"},
{file = "simplejson-3.17.0-cp35-cp35m-win32.whl", hash = "sha256:4cf91aab51b02b3327c9d51897960c554f00891f9b31abd8a2f50fd4a0071ce8"}, {file = "simplejson-3.17.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:fed0f22bf1313ff79c7fc318f7199d6c2f96d4de3234b2f12a1eab350e597c06"},
{file = "simplejson-3.17.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fc9051d249dd5512e541f20330a74592f7a65b2d62e18122ca89bf71f94db748"}, {file = "simplejson-3.17.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:2e7b57c2c146f8e4dadf84977a83f7ee50da17c8861fd7faf694d55e3274784f"},
{file = "simplejson-3.17.0-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86afc5b5cbd42d706efd33f280fec7bd7e2772ef54e3f34cf6b30777cd19a614"}, {file = "simplejson-3.17.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:da3c55cdc66cfc3fffb607db49a42448785ea2732f055ac1549b69dcb392663b"},
{file = "simplejson-3.17.0-cp36-cp36m-win32.whl", hash = "sha256:926bcbef9eb60e798eabda9cd0bbcb0fca70d2779aa0aa56845749d973eb7ad5"}, {file = "simplejson-3.17.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:c1cb29b1fced01f97e6d5631c3edc2dadb424d1f4421dad079cb13fc97acb42f"},
{file = "simplejson-3.17.0-cp36-cp36m-win_amd64.whl", hash = "sha256:daaf4d11db982791be74b23ff4729af2c7da79316de0bebf880fa2d60bcc8c5a"}, {file = "simplejson-3.17.2-cp33-cp33m-win32.whl", hash = "sha256:8f713ea65958ef40049b6c45c40c206ab363db9591ff5a49d89b448933fa5746"},
{file = "simplejson-3.17.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:9a126c3a91df5b1403e965ba63b304a50b53d8efc908a8c71545ed72535374a3"}, {file = "simplejson-3.17.2-cp33-cp33m-win_amd64.whl", hash = "sha256:344e2d920a7f27b4023c087ab539877a1e39ce8e3e90b867e0bfa97829824748"},
{file = "simplejson-3.17.0-cp37-cp37m-win32.whl", hash = "sha256:fc046afda0ed8f5295212068266c92991ab1f4a50c6a7144b69364bdee4a0159"}, {file = "simplejson-3.17.2-cp34-cp34m-win32.whl", hash = "sha256:05b43d568300c1cd43f95ff4bfcff984bc658aa001be91efb3bb21df9d6288d3"},
{file = "simplejson-3.17.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7cce4bac7e0d66f3a080b80212c2238e063211fe327f98d764c6acbc214497fc"}, {file = "simplejson-3.17.2-cp34-cp34m-win_amd64.whl", hash = "sha256:cff6453e25204d3369c47b97dd34783ca820611bd334779d22192da23784194b"},
{file = "simplejson-3.17.0.tar.gz", hash = "sha256:2b4b2b738b3b99819a17feaf118265d0753d5536049ea570b3c43b51c4701e81"}, {file = "simplejson-3.17.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8acf76443cfb5c949b6e781c154278c059b09ac717d2757a830c869ba000cf8d"},
{file = "simplejson-3.17.0.win-amd64-py2.7.exe", hash = "sha256:1d346c2c1d7dd79c118f0cc7ec5a1c4127e0c8ffc83e7b13fc5709ff78c9bb84"}, {file = "simplejson-3.17.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:869a183c8e44bc03be1b2bbcc9ec4338e37fa8557fc506bf6115887c1d3bb956"},
{file = "simplejson-3.17.0.win-amd64-py3.3.exe", hash = "sha256:5cfd495527f8b85ce21db806567de52d98f5078a8e9427b18e251c68bd573a26"}, {file = "simplejson-3.17.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:5c659a0efc80aaaba57fcd878855c8534ecb655a28ac8508885c50648e6e659d"},
{file = "simplejson-3.17.0.win-amd64-py3.4.exe", hash = "sha256:8de378d589eccbc75941e480b4d5b4db66f22e4232f87543b136b1f093fff342"}, {file = "simplejson-3.17.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:72d8a3ffca19a901002d6b068cf746be85747571c6a7ba12cbcf427bfb4ed971"},
{file = "simplejson-3.17.0.win-amd64-py3.5.exe", hash = "sha256:f4b64a1031acf33e281fd9052336d6dad4d35eee3404c95431c8c6bc7a9c0588"}, {file = "simplejson-3.17.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:4b3442249d5e3893b90cb9f72c7d6ce4d2ea144d2c0d9f75b9ae1e5460f3121a"},
{file = "simplejson-3.17.0.win-amd64-py3.6.exe", hash = "sha256:ad8dd3454d0c65c0f92945ac86f7b9efb67fa2040ba1b0189540e984df904378"}, {file = "simplejson-3.17.2-cp35-cp35m-win32.whl", hash = "sha256:e058c7656c44fb494a11443191e381355388443d543f6fc1a245d5d238544396"},
{file = "simplejson-3.17.0.win-amd64-py3.7.exe", hash = "sha256:229edb079d5dd81bf12da952d4d825bd68d1241381b37d3acf961b384c9934de"}, {file = "simplejson-3.17.2-cp35-cp35m-win_amd64.whl", hash = "sha256:934115642c8ba9659b402c8bdbdedb48651fb94b576e3b3efd1ccb079609b04a"},
{file = "simplejson-3.17.0.win32-py2.7.exe", hash = "sha256:4fd5f79590694ebff8dc980708e1c182d41ce1fda599a12189f0ca96bf41ad70"}, {file = "simplejson-3.17.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:ffd4e4877a78c84d693e491b223385e0271278f5f4e1476a4962dca6824ecfeb"},
{file = "simplejson-3.17.0.win32-py3.3.exe", hash = "sha256:d140e9376e7f73c1f9e0a8e3836caf5eec57bbafd99259d56979da05a6356388"}, {file = "simplejson-3.17.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:10fc250c3edea4abc15d930d77274ddb8df4803453dde7ad50c2f5565a18a4bb"},
{file = "simplejson-3.17.0.win32-py3.4.exe", hash = "sha256:da00675e5e483ead345429d4f1374ab8b949fba4429d60e71ee9d030ced64037"}, {file = "simplejson-3.17.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:76ac9605bf2f6d9b56abf6f9da9047a8782574ad3531c82eae774947ae99cc3f"},
{file = "simplejson-3.17.0.win32-py3.5.exe", hash = "sha256:7739940d68b200877a15a5ff5149e1599737d6dd55e302625650629350466418"}, {file = "simplejson-3.17.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:7f10f8ba9c1b1430addc7dd385fc322e221559d3ae49b812aebf57470ce8de45"},
{file = "simplejson-3.17.0.win32-py3.6.exe", hash = "sha256:60aad424e47c5803276e332b2a861ed7a0d46560e8af53790c4c4fb3420c26c2"}, {file = "simplejson-3.17.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:bc00d1210567a4cdd215ac6e17dc00cb9893ee521cee701adfd0fa43f7c73139"},
{file = "simplejson-3.17.0.win32-py3.7.exe", hash = "sha256:1fbba86098bbfc1f85c5b69dc9a6d009055104354e0d9880bb00b692e30e0078"}, {file = "simplejson-3.17.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:af4868da7dd53296cd7630687161d53a7ebe2e63814234631445697bd7c29f46"},
{file = "simplejson-3.17.2-cp36-cp36m-win32.whl", hash = "sha256:7d276f69bfc8c7ba6c717ba8deaf28f9d3c8450ff0aa8713f5a3280e232be16b"},
{file = "simplejson-3.17.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a55c76254d7cf8d4494bc508e7abb993a82a192d0db4552421e5139235604625"},
{file = "simplejson-3.17.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:9a2b7543559f8a1c9ed72724b549d8cc3515da7daf3e79813a15bdc4a769de25"},
{file = "simplejson-3.17.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:311f5dc2af07361725033b13cc3d0351de3da8bede3397d45650784c3f21fbcf"},
{file = "simplejson-3.17.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2862beabfb9097a745a961426fe7daf66e1714151da8bb9a0c430dde3d59c7c0"},
{file = "simplejson-3.17.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:afebfc3dd3520d37056f641969ce320b071bc7a0800639c71877b90d053e087f"},
{file = "simplejson-3.17.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d4813b30cb62d3b63ccc60dd12f2121780c7a3068db692daeb90f989877aaf04"},
{file = "simplejson-3.17.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3fabde09af43e0cbdee407555383063f8b45bfb52c361bc5da83fcffdb4fd278"},
{file = "simplejson-3.17.2-cp37-cp37m-win32.whl", hash = "sha256:ceaa28a5bce8a46a130cd223e895080e258a88d51bf6e8de2fc54a6ef7e38c34"},
{file = "simplejson-3.17.2-cp37-cp37m-win_amd64.whl", hash = "sha256:9551f23e09300a9a528f7af20e35c9f79686d46d646152a0c8fc41d2d074d9b0"},
{file = "simplejson-3.17.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c94dc64b1a389a416fc4218cd4799aa3756f25940cae33530a4f7f2f54f166da"},
{file = "simplejson-3.17.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b59aa298137ca74a744c1e6e22cfc0bf9dca3a2f41f51bc92eb05695155d905a"},
{file = "simplejson-3.17.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ad8f41c2357b73bc9e8606d2fa226233bf4d55d85a8982ecdfd55823a6959995"},
{file = "simplejson-3.17.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:845a14f6deb124a3bcb98a62def067a67462a000e0508f256f9c18eff5847efc"},
{file = "simplejson-3.17.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d0b64409df09edb4c365d95004775c988259efe9be39697d7315c42b7a5e7e94"},
{file = "simplejson-3.17.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:55d65f9cc1b733d85ef95ab11f559cce55c7649a2160da2ac7a078534da676c8"},
{file = "simplejson-3.17.2.tar.gz", hash = "sha256:75ecc79f26d99222a084fbdd1ce5aad3ac3a8bd535cd9059528452da38b68841"},
] ]
six = [ six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},