From a9d4085a4b19dc4660d1e28d1d43ca6352966096 Mon Sep 17 00:00:00 2001 From: relikd Date: Fri, 12 Feb 2021 00:36:01 +0100 Subject: [PATCH] refactoring II --- LP/Alphabet.py | 21 ++ LP/FailedAttempts.py | 9 +- LP/IOReader.py | 51 ++++ LP/IOWriter.py | 148 ++++++++++ LP/InterruptDB.py | 234 ++------------- LP/InterruptIndices.py | 64 +++++ LP/{HeuristicSearch.py => InterruptSearch.py} | 149 +++------- LP/InterruptToWeb.py | 116 ++++++++ LP/KeySearch.py | 99 +++++++ LP/LPath.py | 1 + LP/NGrams.py | 36 +-- LP/{HeuristicLib.py => Probability.py} | 3 +- LP/Rune.py | 83 ++++++ LP/RuneRunner.py | 245 ---------------- LP/RuneSolver.py | 192 ++++++------- LP/RuneText.py | 269 +++++++----------- LP/__init__.py | 25 +- LP/{lib.py => utils.py} | 11 +- README.md | 82 ++++-- img/4gq25.jpg | Bin 0 -> 26342 bytes img/gematria-primus.jpg | Bin 0 -> 53549 bytes img/zN4h51m.jpg | Bin 0 -> 41731 bytes playground.py | 109 ++++--- probability.py | 89 +++--- solver.py | 61 ++-- 25 files changed, 1080 insertions(+), 1017 deletions(-) create mode 100755 LP/Alphabet.py create mode 100755 LP/IOReader.py create mode 100755 LP/IOWriter.py create mode 100755 LP/InterruptIndices.py rename LP/{HeuristicSearch.py => InterruptSearch.py} (54%) create mode 100755 LP/InterruptToWeb.py create mode 100755 LP/KeySearch.py rename LP/{HeuristicLib.py => Probability.py} (96%) create mode 100755 LP/Rune.py delete mode 100755 LP/RuneRunner.py rename LP/{lib.py => utils.py} (90%) create mode 100644 img/4gq25.jpg create mode 100755 img/gematria-primus.jpg create mode 100644 img/zN4h51m.jpg diff --git a/LP/Alphabet.py b/LP/Alphabet.py new file mode 100755 index 0000000..e5f78e6 --- /dev/null +++ b/LP/Alphabet.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- + +white_rune = {'•': ' ', '⁘': '.', '⁚': ',', '⁖': ';', '⁜': '#'} +white_text = {v: k for k, v in white_rune.items()} +alphabet = [ # Using last value for display. Custom added: V + (2, 'ᚠ', ['F']), (3, 'ᚢ', ['V', 'U']), (5, 'ᚦ', ['TH']), (7, 'ᚩ', ['O']), + (11, 'ᚱ', ['R']), (13, 'ᚳ', ['K', 'C']), (17, 'ᚷ', ['G']), + (19, 'ᚹ', ['W']), (23, 'ᚻ', ['H']), (29, 'ᚾ', ['N']), (31, 'ᛁ', ['I']), + (37, 'ᛄ', ['J']), (41, 'ᛇ', ['EO']), (43, 'ᛈ', ['P']), (47, 'ᛉ', ['X']), + (53, 'ᛋ', ['Z', 'S']), (59, 'ᛏ', ['T']), (61, 'ᛒ', ['B']), + (67, 'ᛖ', ['E']), (71, 'ᛗ', ['M']), (73, 'ᛚ', ['L']), + (79, 'ᛝ', ['ING', 'NG']), (83, 'ᛟ', ['OE']), (89, 'ᛞ', ['D']), + (97, 'ᚪ', ['A']), (101, 'ᚫ', ['AE']), (103, 'ᚣ', ['Y']), + (107, 'ᛡ', ['IO', 'IA']), (109, 'ᛠ', ['EA']) +] +text_map = {t: r for _, r, ta in alphabet for t in ta} +rune_map = {r: t for _, r, ta in alphabet for t in ta} +primes_map = {r: p for p, r, _ in alphabet} +RUNES = [r for _, r, _ in alphabet] # array already sorted +# del alphabet # used in playground for GP display diff --git a/LP/FailedAttempts.py b/LP/FailedAttempts.py index de12c02..385c5e2 100755 --- a/LP/FailedAttempts.py +++ b/LP/FailedAttempts.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 -from RuneText import rune_map, RuneText +# -*- coding: UTF-8 -*- +from Alphabet import rune_map +from RuneText import RuneText from NGrams import NGrams @@ -57,5 +59,6 @@ class NGramShifter(object): print() -# NGramShifter().guess('ᛈᚢᛟᚫᛈᚠᛖᚱᛋᛈᛈᚦᛗᚾᚪᚱᛚᚹᛈᛖᚩᛈᚢᛠᛁᛁᚻᛞᛚᛟᛠ', 'ᛟ') -# NGramShifter().guess([1, 2, 4, 5, 7, 9, 0, 12], 'ᛟ') +if __name__ == '__main__': + NGramShifter().guess('ᛈᚢᛟᚫᛈᚠᛖᚱᛋᛈᛈᚦᛗᚾᚪᚱᛚᚹᛈᛖᚩᛈᚢᛠᛁᛁᚻᛞᛚᛟᛠ', 'ᛟ') + NGramShifter().guess([1, 2, 4, 5, 7, 9, 0, 12], 'ᛟ') diff --git a/LP/IOReader.py b/LP/IOReader.py new file mode 100755 index 0000000..9ed68d3 --- /dev/null +++ b/LP/IOReader.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +import re # load_indices +from Alphabet import RUNES +from RuneText import RuneText + +re_norune = re.compile('[^' + ''.join(RUNES) + ']') + + +######################################### +# load page and convert to indices for faster access +######################################### + +def load_indices(fname, interrupt, maxinterrupt=None, minlen=None, limit=None): + with open(fname, 'r') as f: + data = RuneText(re_norune.sub('', f.read())).index_no_white[:limit] + if maxinterrupt is not None: + # incl. everything up to but not including next interrupt + # e.g., maxinterrupt = 0 will return text until first interrupt + for i, x in enumerate(data): + if x != interrupt: + continue + if maxinterrupt == 0: + if minlen and i < minlen: + continue + return data[:i] + maxinterrupt -= 1 + return data + + +######################################### +# find the longest chunk in a list of indices, which does not include an irp +######################################### + +def longest_no_interrupt(data, interrupt, irpmax=0): + def add(i): + nonlocal ret, prev + idx = prev.pop(0) + if idx == 0: + ret = [] + ret.append((i - idx, idx)) + + prev = [0] * (irpmax + 1) + ret = [] + for i, x in enumerate(data): + if x == interrupt: + prev.append(i + 1) + add(i) + add(i + 1) + length, pos = max(ret) + return pos, length diff --git a/LP/IOWriter.py b/LP/IOWriter.py new file mode 100755 index 0000000..99a3b25 --- /dev/null +++ b/LP/IOWriter.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +import sys +from RuneText import RuneText +import utils + + +######################################### +# IOWriter : handle std output with highlight etc. +######################################### + +class IOWriter(object): + def __init__(self): + self.BREAK_MODE = None + self.VERBOSE = '-v' in sys.argv + self.QUIET = '-q' in sys.argv + self.COLORS = True # sys.stdout.isatty() doesnt matter if no highlight + self.cur_color = None + self.file_output = None + + def clear(self): + self.linesum = 0 + self.out_r = '' + self.out_t = '' + self.out_p = '' + self.out_ps = '' + + def mark(self, color=None): + if self.COLORS: + m = f'\x1b[{color}' if color else '\x1b[0m' + self.cur_color = color + self.out_r += m + self.out_t += m + self.out_p += m + # self.out_ps += m # No. Because a word may be split-up + + def run(self, data, highlight=None): # make sure sorted, non-overlapping + break_on = self.BREAK_MODE # set by user + if break_on is None: # check None specifically, to allow '' as value + break_on = 'l' if self.VERBOSE else 's' # dynamically adapt mode + + wsum = 0 + self.clear() + if not highlight: + highlight = [] + highlight.append((len(data), len(data))) + + for i in range(len(data)): + # Handle color highlight + if i == highlight[0][0]: + try: + color = highlight[0][2] # e.g. 1;30m for bold black + except IndexError: + color = '1;31m' # fallback to bold red + self.mark(color) + elif i >= highlight[0][1]: + self.mark() + highlight.pop(0) + + cur = data[i] + eow = i + 1 == len(data) or data[i + 1].kind not in 'rl' + + # Output current rune + if cur.kind == 'l': + if cur.kind == break_on: + self.write() + continue # ignore all \n,\r if not forced explicitly + self.out_r += cur.rune + self.out_t += cur.text + if cur.kind != 'r': + if self.VERBOSE: + self.out_p += ' ' + if cur.kind == break_on: + self.write() + continue + + # Special case when printing numbers. + # Keep both lines (text + numbers) in sync. + if self.VERBOSE: + b = f'{cur.prime}' # TODO: option for indices instead + fillup = len(b) - len(cur.text) + self.out_t += ' ' * fillup + self.out_p += b + if not eow: + if fillup >= 0: + self.out_t += ' ' + self.out_p += '+' + + # Mark prime words + wsum += cur.prime + if eow and wsum > 0: + self.linesum += wsum + if self.VERBOSE: + if self.out_ps: + self.out_ps += ' + ' + self.out_ps += str(wsum) + if utils.is_prime(wsum): + if self.VERBOSE: + self.out_ps += '*' + elif not self.QUIET: # and wsum > 109 + self.out_t += '__' + wsum = 0 + self.write() + + def write(self): + def print_f(x=''): + if self.file_output: + with open(self.file_output, 'a') as f: + f.write(x + '\n') + else: + print(x) + + if not self.out_t: + return + + prev_color = self.cur_color + if prev_color: + self.mark() + + sffx = ' = {}'.format(self.linesum) + if utils.is_prime(self.linesum): + sffx += '*' + if utils.is_emirp(self.linesum): + sffx += '√' + + if not self.QUIET or self.VERBOSE: + print_f() + if not self.QUIET: + print_f(self.out_r) + if not (self.QUIET or self.VERBOSE): + self.out_t += sffx + print_f(self.out_t) + if self.VERBOSE: + self.out_ps += sffx + print_f(self.out_p) + print_f(self.out_ps) + self.clear() + if prev_color: + self.mark(prev_color) + + +if __name__ == '__main__': + txt = RuneText('Hi there. And welc\nome, to my world; "manatee"') + io = IOWriter() + io.BREAK_MODE = 's' # 'l' + # io.VERBOSE = True + # io.QUIET = True + io.run(txt, [(4, 12), (13, 27)]) diff --git a/LP/InterruptDB.py b/LP/InterruptDB.py index 49fafce..24bcf5e 100755 --- a/LP/InterruptDB.py +++ b/LP/InterruptDB.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 +# -*- coding: UTF-8 -*- import os -from HeuristicSearch import SearchInterrupt -from HeuristicLib import Probability -from RuneText import RUNES, load_indices +from InterruptSearch import InterruptSearch +from Probability import Probability +from IOReader import load_indices from LPath import FILES_ALL, FILES_UNSOLVED, LPath @@ -13,20 +14,14 @@ from LPath import FILES_ALL, FILES_UNSOLVED, LPath class InterruptDB(object): def __init__(self, data, interrupt): self.irp = interrupt - self.iguess = SearchInterrupt(data, interrupt) + self.iguess = InterruptSearch(data, irp=interrupt) self.irp_count = len(self.iguess.stops) - def make(self, dbname, name, keylen): - def fn(x): - return Probability.target_diff(x, keylen) # used in db_norm - # return Probability.IC_w_keylen(x, keylen) # used in db_high - - if keylen == 0: - keylen = 1 - score, skips = fn(self.iguess.join()), [[]] # without interrupts + def make(self, dbname, name, keylen, fn_score): + if keylen == 0: # without interrupts + score, skips = fn_score(self.iguess.join(), 1), [[]] else: - score, skips = self.iguess.sequential(fn, startAt=0, maxdepth=99) - # score, skips = self.iguess.genetic(fn, topDown=False, maxdepth=4) + score, skips = self.iguess.all(keylen, fn_score) for i, interrupts in enumerate(skips): skips[i] = self.iguess.to_occurrence_index(interrupts) @@ -35,18 +30,17 @@ class InterruptDB(object): name, score, self.irp, self.irp_count, keylen, nums, dbname) return score, skips - def make_secondary(self, dbname, name, keylen, threshold): + def make_secondary(self, dbname, name, keylen, fn_score, threshold): scores = [] - def fn(x): - score = Probability.target_diff(x, keylen) # used in db_norm - # score = Probability.IC_w_keylen(x, keylen) # used in db_high + def fn(x, kl): + score = fn_score(x, kl) if score >= threshold: scores.append(score) return 1 return -1 - _, skips = self.iguess.sequential(fn, startAt=0, maxdepth=99) + _, skips = self.iguess.all(keylen, fn) for i, interrupts in enumerate(skips): skips[i] = self.iguess.to_occurrence_index(interrupts) ret = list(zip(scores, skips)) @@ -58,25 +52,6 @@ class InterruptDB(object): name, score, self.irp, self.irp_count, keylen, nums, dbname) return len(filtered) - @staticmethod - def longest_no_interrupt(data, interrupt, irpmax=0): - def add(i): - nonlocal ret, prev - idx = prev.pop(0) - if idx == 0: - ret = [] - ret.append((i - idx, idx)) - - prev = [0] * (irpmax + 1) - ret = [] - for i, x in enumerate(data): - if x == interrupt: - prev.append(i + 1) - add(i) - add(i + 1) - length, pos = max(ret) - return pos, length - @staticmethod def load(dbname): if not os.path.isfile(LPath.InterruptDB(dbname)): @@ -103,171 +78,12 @@ class InterruptDB(object): f.write(f'{name}|{irpmax}|{score:.5f}|{irp}|{keylen}|{nums}\n') -######################################### -# InterruptIndices : Read chapters and extract indices (cluster by runes) -######################################### - -class InterruptIndices(object): - def __init__(self): - self.pos = InterruptIndices.read() - - def consider(self, name, irp, limit): - nums = self.pos[name]['pos'][irp] - if len(nums) <= limit: - return self.pos[name]['total'] - return nums[limit] # number of runes, which is not last index - - def total(self, name): - return self.pos[name]['total'] - - def longest_no_interrupt(self, name, irp, irpmax=0): - irpmax += 1 - nums = self.pos[name]['pos'][irp] + [self.pos[name]['total']] * irpmax - ret = [(y - x, x) for x, y in zip(nums, nums[irpmax:])] - return sorted(ret, reverse=True) - - @staticmethod - def write(dbname='db_indices'): - with open(LPath.InterruptDB(dbname), 'w') as f: - f.write('# file | total runes in file | interrupt | indices\n') - for name in FILES_ALL: - fname = f'pages/{name}.txt' - data = load_indices(fname, 0) - total = len(data) - nums = [[] for x in range(29)] - for idx, rune in enumerate(data): - nums[rune].append(idx) - for irp, pos in enumerate(nums): - f.write('{}|{}|{}|{}\n'.format( - name, total, irp, ','.join(map(str, pos)))) - - @staticmethod - def read(dbname='db_indices'): - with open(LPath.InterruptDB(dbname), 'r') as f: - ret = {} - for line in f.readlines(): - if line.startswith('#'): - continue - line = line.strip() - name, total, irp, nums = line.split('|') - if name not in ret: - ret[name] = {'total': int(total), - 'pos': [[] for _ in range(29)]} - pos = ret[name]['pos'] - pos[int(irp)] = list(map(int, nums.split(','))) if nums else [] - return ret - - -######################################### -# InterruptToWeb : Read interrupt DB and create html graphic / matrix -######################################### - -class InterruptToWeb(object): - def __init__(self, dbname, template='template.html'): - with open(LPath.results(template), 'r') as f: - self.template = f.read() - self.indices = InterruptIndices() - self.scores = {} - db = InterruptDB.load(dbname) - for k, v in db.items(): - for irpc, score, irp, kl, nums in v: - if k not in self.scores: - self.scores[k] = [[] for _ in range(29)] - part = self.scores[k][irp] - while kl >= len(part): - part.append((0, 0)) # (score, irpc) - oldc = part[kl][1] - if irpc > oldc or (irpc == oldc and score > part[kl][0]): - part[kl] = (score, irpc) - - def cls(self, x, low=0, high=1): - if x <= low: - return ' class="m0"' - return f' class="m{int((min(high, x) - low) / (high - low) * 14) + 1}"' - - def table_reliable(self): - trh = '' - trtotal = 'Total' - trd = [f'{x}' for x in RUNES] - del_row = [True] * 29 - for name in FILES_ALL: - if name not in self.scores: - continue - total = self.indices.total(name) - trh += f'
{name}
' - trtotal += f'{total}' - for i in range(29): - scrs = self.scores[name][i][1:] - if not scrs: - trd[i] += '–' - continue - del_row[i] = False - worst_irpc = min([x[1] for x in scrs]) - if worst_irpc == 0: - if max([x[1] for x in scrs]) != 0: - trd[i] += '?' - continue - num = self.indices.consider(name, i, worst_irpc) - trd[i] += f'{num}' - - trh += '\n' - trtotal += '\n' - for i in range(29): - trd[i] += '\n' - if del_row[i]: - trd[i] = '' - return f'{trh}{"".join(trd)}{trtotal}
' - - def table_interrupt(self, irp, pmin=1.25, pmax=1.65): - maxkl = max(len(x[irp]) for x in self.scores.values()) - trh = '' - trbest = 'best' - trd = [f'{x}' for x in range(maxkl)] - for name in FILES_ALL: - maxscore = 0 - bestkl = -1 - try: - klarr = self.scores[name][irp] - except KeyError: - continue - trh += f'
{name}
' - for kl, (score, _) in enumerate(klarr): - if score < 0: - trd[kl] += f'–' - else: - trd[kl] += f'{score:.2f}' - if score > maxscore: - maxscore = score - bestkl = kl - trbest += f'{bestkl}' - trh += '\n' - trbest += '\n' - for i in range(29): - trd[i] += '\n' - return f'{trh}{"".join(trd[1:])}{trbest}
' - - def make(self, outfile, pmin=1.25, pmax=1.65): - nav = '' - txt = '' - for i in range(29): - has_entries = any(True for x in self.scores.values() if x[i]) - if not has_entries: - continue - nav += f'{RUNES[i]}\n' - txt += f'

Interrupt {i}: {RUNES[i]}

' - txt += self.table_interrupt(i, pmin, pmax) - html = self.template.replace('__NAVIGATION__', nav) - html = html.replace('__TAB_RELIABLE__', self.table_reliable()) - html = html.replace('__INTERRUPT_TABLES__', txt) - with open(LPath.results(outfile), 'w') as f: - f.write(html) - - ######################################### # helper functions ######################################### -def create_initial_db(dbname, minkl=1, maxkl=32, max_irp=20, irpset=range(29)): +def create_initial_db(dbname, fn_score, klset=range(1, 33), + max_irp=20, irpset=range(29)): oldDB = InterruptDB.load(dbname) oldValues = {k: set((a, b, c) for a, _, b, c, _ in v) for k, v in oldDB.items()} @@ -276,15 +92,16 @@ def create_initial_db(dbname, minkl=1, maxkl=32, max_irp=20, irpset=range(29)): data = load_indices(LPath.page(name), irp, maxinterrupt=max_irp) db = InterruptDB(data, irp) print('load:', name, 'interrupt:', irp, 'count:', db.irp_count) - for keylen in range(minkl, maxkl + 1): # key length + for keylen in klset: # key length if (db.irp_count, irp, keylen) in oldValues.get(name, []): print(f'{keylen}: skipped.') continue - score, interrupts = db.make(dbname, name, keylen) + score, interrupts = db.make(dbname, name, keylen, fn_score) print(f'{keylen}: {score:.4f}, solutions: {len(interrupts)}') -def find_secondary_solutions(db_in, db_out, threshold=0.75, max_irp=20): +def find_secondary_solutions(db_in, db_out, fn_score, + threshold=0.75, max_irp=20): oldDB = InterruptDB.load(db_in) search_set = set() for name, arr in oldDB.items(): @@ -299,13 +116,14 @@ def find_secondary_solutions(db_in, db_out, threshold=0.75, max_irp=20): print('load:', name, 'interrupt:', irp, 'keylen:', kl) data = load_indices(LPath.page(name), irp, maxinterrupt=max_irp) db = InterruptDB(data, irp) - c = db.make_secondary(db_out, name, kl, threshold) + c = db.make_secondary(db_out, name, kl, fn_score, threshold) print('found', c, 'additional solutions') if __name__ == '__main__': - # find_secondary_solutions('db_high', 'db_high_secondary', threshold=1.4) - # find_secondary_solutions('db_norm', 'db_norm_secondary', threshold=0.55) - # create_initial_db('db_norm', minkl=1, maxkl=32, max_irp=20) - # InterruptToWeb('db_high').make('index_high.html') - InterruptToWeb('db_norm').make('index_norm.html', pmin=0.40, pmax=0.98) + create_initial_db('db_high', Probability.IC_w_keylen, max_irp=20) + create_initial_db('db_norm', Probability.target_diff, max_irp=20) + # find_secondary_solutions('db_high', 'db_high_secondary', + # Probability.IC_w_keylen, threshold=1.4) + # find_secondary_solutions('db_norm', 'db_norm_secondary', + # Probability.target_diff, threshold=0.55) diff --git a/LP/InterruptIndices.py b/LP/InterruptIndices.py new file mode 100755 index 0000000..f2f2f70 --- /dev/null +++ b/LP/InterruptIndices.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +from IOReader import load_indices +from LPath import FILES_ALL, LPath + + +######################################### +# InterruptIndices : Read chapters and extract indices (cluster by runes) +######################################### + +class InterruptIndices(object): + def __init__(self): + self.pos = InterruptIndices.load() + + def consider(self, name, irp, limit): + nums = self.pos[name]['pos'][irp] + total = self.pos[name]['total'] if len(nums) <= limit else nums[limit] + return nums[:limit], total + + def total(self, name): + return self.pos[name]['total'] + + def longest_no_interrupt(self, name, irp, irpmax=0): + irpmax += 1 + nums = self.pos[name]['pos'][irp] + [self.pos[name]['total']] * irpmax + ret = [(y - x, x) for x, y in zip(nums, nums[irpmax:])] + return sorted(ret, reverse=True) + + @staticmethod + def write(dbname='db_indices'): + with open(LPath.InterruptDB(dbname), 'w') as f: + f.write('# file | total runes in file | interrupt | indices\n') + for name in FILES_ALL: + data = load_indices(LPath.page(name), 0) + total = len(data) + nums = [[] for x in range(29)] + for idx, rune in enumerate(data): + nums[rune].append(idx) + for irp, pos in enumerate(nums): + f.write('{}|{}|{}|{}\n'.format( + name, total, irp, ','.join(map(str, pos)))) + + @staticmethod + def load(dbname='db_indices'): + with open(LPath.InterruptDB(dbname), 'r') as f: + ret = {} + for line in f.readlines(): + if line.startswith('#'): + continue + line = line.strip() + name, total, irp, nums = line.split('|') + if name not in ret: + ret[name] = {'total': int(total), + 'pos': [[] for _ in range(29)]} + pos = ret[name]['pos'] + pos[int(irp)] = list(map(int, nums.split(','))) if nums else [] + return ret + + +if __name__ == '__main__': + # InterruptIndices.write() + for name, val in InterruptIndices.load().items(): + print(name, 'total:', val['total']) + print(' ', [len(x) for x in val['pos']]) diff --git a/LP/HeuristicSearch.py b/LP/InterruptSearch.py similarity index 54% rename from LP/HeuristicSearch.py rename to LP/InterruptSearch.py index 2eec534..f94837c 100755 --- a/LP/HeuristicSearch.py +++ b/LP/InterruptSearch.py @@ -1,114 +1,18 @@ #!/usr/bin/env python3 +# -*- coding: UTF-8 -*- import itertools # product, compress, combinations import bisect # bisect_left, insort -from lib import affine_decrypt ######################################### -# GuessVigenere : Shift values around with a given keylength +# InterruptSearch : Hill climbing algorithm for interrupt detection ######################################### -class GuessVigenere(object): - def __init__(self, nums): - self.nums = nums - - def guess(self, keylength, score_fn): # minimize score_fn - found = [] - avg_score = 0 - for offset in range(keylength): - bi = -1 - bs = 9999999 - for i in range(29): - shifted = [(x - i) % 29 for x in self.nums[offset::keylength]] - score = score_fn(shifted) - if score < bs: - bs = score - bi = i - avg_score += bs - found.append(bi) - return avg_score / keylength, found - - -######################################### -# GuessAffine : Find greatest common affine key -######################################### - -class GuessAffine(object): - def __init__(self, nums): - self.nums = nums - - def guess(self, keylength, score_fn): # minimize score_fn - found = [] - avg_score = 0 - for offset in range(keylength): - candidate = (None, None) - best = 9999999 - for s in range(29): - for t in range(29): - shifted = [affine_decrypt(x, (s, t)) - for x in self.nums[offset::keylength]] - score = score_fn(shifted) - if score < best: - best = score - candidate = (s, t) - avg_score += best - found.append(candidate) - return avg_score / keylength, found - - -######################################### -# GuessPattern : Find a key that is rotated ABC BCA CAB, or ABC CAB BCA -######################################### - -class GuessPattern(object): - def __init__(self, nums): - self.nums = nums - - @staticmethod - def pattern(keylen, fn_pattern): - mask = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'[:keylen] - return fn_pattern(mask, keylen) - - def split(self, keylen, mask, offset=0): - ret = {} - for _ in range(offset): - next(mask) - ret = {k: [] for k in '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'[:keylen]} - for n, k in zip(self.nums, mask): - ret[k].append(n) - return ret.values() - - def zip(self, key_mask, offset=0): - for _ in range(offset): - next(key_mask) - return [(n - k) % 29 for n, k in zip(self.nums, key_mask)] - - @staticmethod - def guess(parts, score_fn): # minimize score_fn - found = [] - avg_score = 0 - for nums in parts: - best = 9999999 - candidate = 0 - for i in range(29): - score = score_fn([(x - i) % 29 for x in nums]) - if score < best: - best = score - candidate = i - avg_score += best - found.append(candidate) - return avg_score / len(parts), found - - -######################################### -# SearchInterrupt : Hill climbing algorithm for interrupt detection -######################################### - -class SearchInterrupt(object): - def __init__(self, arr, interrupt_chr): # remove all whitespace in arr +class InterruptSearch(object): + def __init__(self, arr, irp): # remove all whitespace in arr self.single_result = False # if False, return list of equal likelihood self.full = arr - self.stops = [i for i, n in enumerate(arr) if n == interrupt_chr] + self.stops = [i for i, n in enumerate(arr) if n == irp] def to_occurrence_index(self, interrupts): return [self.stops.index(x) + 1 for x in interrupts] @@ -124,11 +28,27 @@ class SearchInterrupt(object): i = x return ret + self.full[i + 1:] + # Just enumerate all possibilities. + # If you need to limit the options, trim the data before computation + def all(self, keylen, score_fn): + best_s = -8 + found = [] # [match, match, ...] + for x in itertools.product([False, True], repeat=len(self.stops)): + part = list(itertools.compress(self.stops, x)) + score = score_fn(self.join(part), keylen) + if score >= best_s: + if score > best_s or self.single_result: + best_s = score + found = [part] + else: + found.append(part) + return best_s, found + # Go over the full string but only look at the first {maxdepth} interrupts. # Enumerate all possibilities and choose the one with the highest score. # If first interrupt is set, add it to the resulting set. If not, ignore it # Every iteration will add a single interrupt only, not the full set. - def sequential(self, score_fn, startAt=0, maxdepth=9): + def sequential(self, keylen, score_fn, startAt=0, maxdepth=9): found = [[]] def best_in_one(i, depth, prefix=[]): @@ -137,7 +57,7 @@ class SearchInterrupt(object): irp = self.stops[i:i + depth] for x in itertools.product([False, True], repeat=depth): part = list(itertools.compress(irp, x)) - score = score_fn(self.join(prefix + part)) + score = score_fn(self.join(prefix + part), keylen) if score >= best_s: if score > best_s or self.single_result: best_s = score @@ -162,7 +82,7 @@ class SearchInterrupt(object): # first step: move maxdepth-sized window over data i = startAt - 1 # in case loop isnt called for i in range(startAt, len(self.stops) - maxdepth): - # print('.', end='') + print('.', end='') parts, _ = best_in_all(i, maxdepth) found = [] search = self.stops[i] @@ -180,7 +100,7 @@ class SearchInterrupt(object): found.append(prfx + [search]) if bitNotSet: found.append(prfx) - # print('.') + print('.') # last step: all permutations for the remaining (< maxdepth) bits i += 1 remaining, score = best_in_all(i, min(maxdepth, len(self.stops) - i)) @@ -191,10 +111,12 @@ class SearchInterrupt(object): # Choose the bitset with the highest score and repeat. # If no better score found, increment number of testing bits and repeat. # Either start with all interrupts set (topDown) or none set. - def genetic(self, score_fn, topDown=False, maxdepth=3): + def genetic(self, keylen, score_fn, topDown=False, maxdepth=3): current = self.stops if topDown else [] def evolve(lvl): + if lvl > 0: + yield from evolve(lvl - 1) for x in itertools.combinations(self.stops, lvl + 1): tmp = current[:] for y in x: @@ -202,9 +124,9 @@ class SearchInterrupt(object): tmp.pop(bisect.bisect_left(tmp, y)) else: bisect.insort(tmp, y) - yield tmp, score_fn(self.join(tmp)) + yield tmp, score_fn(self.join(tmp), keylen) - best = score_fn(self.join()) + best = score_fn(self.join(), keylen) level = 0 # or start directly with maxdepth - 1 while level < maxdepth: print('.', end='') @@ -227,7 +149,10 @@ class SearchInterrupt(object): return best, all_of_them -# a = GuessInterrupt([2, 0, 1, 0, 14, 15, 0, 13, 24, 25, 25, 25], 0) -# print(a.sequential(lambda x: (1.2 if len(x) == 11 else 0.1))) -# print(a.sequential(lambda x: (1.1 if len(x) == 10 else 0.1))) -# print(a.sequential(lambda x: (1.3 if len(x) == 9 else 0.1))) +if __name__ == '__main__': + a = InterruptSearch([2, 0, 1, 0, 14, 15, 0, 13, 24, 25, 25, 25], irp=0) + print(a.sequential(1, lambda x, k: (1.2 if len(x) == 11 else 0.1))) + print(a.sequential(1, lambda x, k: (1.1 if len(x) == 10 else 0.1))) + print(a.sequential(1, lambda x, k: (1.3 if len(x) == 9 else 0.1))) + print(a.genetic(1, lambda x, k: (1.5 if len(x) == 10 else 0.1))) + print(a.all(1, lambda x, k: (1.4 if len(x) == 11 else 0.1))) diff --git a/LP/InterruptToWeb.py b/LP/InterruptToWeb.py new file mode 100755 index 0000000..a6e6426 --- /dev/null +++ b/LP/InterruptToWeb.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +from Alphabet import RUNES +from LPath import FILES_ALL, LPath +from InterruptDB import InterruptDB +from InterruptIndices import InterruptIndices + + +######################################### +# InterruptToWeb : Read interrupt DB and create html graphic / matrix +######################################### + +class InterruptToWeb(object): + def __init__(self, dbname, template='template.html'): + with open(LPath.results(template), 'r') as f: + self.template = f.read() + self.indices = InterruptIndices() + self.scores = {} + db = InterruptDB.load(dbname) + for k, v in db.items(): + for irpc, score, irp, kl, nums in v: + if k not in self.scores: + self.scores[k] = [[] for _ in range(29)] + part = self.scores[k][irp] + while kl >= len(part): + part.append((0, 0)) # (score, irpc) + oldc = part[kl][1] + if irpc > oldc or (irpc == oldc and score > part[kl][0]): + part[kl] = (score, irpc) + + def cls(self, x, low=0, high=1): + if x <= low: + return ' class="m0"' + return f' class="m{int((min(high, x) - low) / (high - low) * 14) + 1}"' + + def table_reliable(self): + trh = '' + trtotal = 'Total' + trd = [f'{x}' for x in RUNES] + del_row = [True] * 29 + for name in FILES_ALL: + if name not in self.scores: + continue + total = self.indices.total(name) + trh += f'
{name}
' + trtotal += f'{total}' + for i in range(29): + scrs = self.scores[name][i][1:] + if not scrs: + trd[i] += '–' + continue + del_row[i] = False + worst_irpc = min([x[1] for x in scrs]) + if worst_irpc == 0: + if max([x[1] for x in scrs]) != 0: + trd[i] += '?' + continue + _, num = self.indices.consider(name, i, worst_irpc) + trd[i] += f'{num}' + + trh += '\n' + trtotal += '\n' + for i in range(29): + trd[i] += '\n' + if del_row[i]: + trd[i] = '' + return f'{trh}{"".join(trd)}{trtotal}
' + + def table_interrupt(self, irp, pmin=1.25, pmax=1.65): + maxkl = max(len(x[irp]) for x in self.scores.values()) + trh = '' + trbest = 'best' + trd = [f'{x}' for x in range(maxkl)] + for name in FILES_ALL: + maxscore = 0 + bestkl = -1 + try: + klarr = self.scores[name][irp] + except KeyError: + continue + trh += f'
{name}
' + for kl, (score, _) in enumerate(klarr): + if score < 0: + trd[kl] += f'–' + else: + trd[kl] += f'{score:.2f}' + if score > maxscore: + maxscore = score + bestkl = kl + trbest += f'{bestkl}' + trh += '\n' + trbest += '\n' + for i in range(29): + trd[i] += '\n' + return f'{trh}{"".join(trd[1:])}{trbest}
' + + def make(self, outfile, pmin=1.25, pmax=1.65): + nav = '' + txt = '' + for i in range(29): + has_entries = any(True for x in self.scores.values() if x[i]) + if not has_entries: + continue + nav += f'{RUNES[i]}\n' + txt += f'

Interrupt {i}: {RUNES[i]}

' + txt += self.table_interrupt(i, pmin, pmax) + html = self.template.replace('__NAVIGATION__', nav) + html = html.replace('__TAB_RELIABLE__', self.table_reliable()) + html = html.replace('__INTERRUPT_TABLES__', txt) + with open(LPath.results(outfile), 'w') as f: + f.write(html) + + +if __name__ == '__main__': + InterruptToWeb('db_high').make('index_high.html') + InterruptToWeb('db_norm').make('index_norm.html', pmin=0.40, pmax=0.98) diff --git a/LP/KeySearch.py b/LP/KeySearch.py new file mode 100755 index 0000000..8269b0a --- /dev/null +++ b/LP/KeySearch.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +from utils import affine_decrypt + + +######################################### +# GuessVigenere : Shift values around with a given keylength +######################################### + +class GuessVigenere(object): + def __init__(self, nums): + self.nums = nums + + def guess(self, keylength, score_fn): # minimize score_fn + found = [] + avg_score = 0 + for offset in range(keylength): + bi = -1 + bs = 9999999 + for i in range(29): + shifted = [(x - i) % 29 for x in self.nums[offset::keylength]] + score = score_fn(shifted) + if score < bs: + bs = score + bi = i + avg_score += bs + found.append(bi) + return avg_score / keylength, found + + +######################################### +# GuessAffine : Find greatest common affine key +######################################### + +class GuessAffine(object): + def __init__(self, nums): + self.nums = nums + + def guess(self, keylength, score_fn): # minimize score_fn + found = [] + avg_score = 0 + for offset in range(keylength): + candidate = (None, None) + best = 9999999 + for s in range(29): + for t in range(29): + shifted = [affine_decrypt(x, (s, t)) + for x in self.nums[offset::keylength]] + score = score_fn(shifted) + if score < best: + best = score + candidate = (s, t) + avg_score += best + found.append(candidate) + return avg_score / keylength, found + + +######################################### +# GuessPattern : Find a key that is rotated ABC BCA CAB, or ABC CAB BCA +######################################### + +class GuessPattern(object): + def __init__(self, nums): + self.nums = nums + + @staticmethod + def pattern(keylen, fn_pattern): + mask = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'[:keylen] + return fn_pattern(mask, keylen) + + def split(self, keylen, mask, offset=0): + ret = {} + for _ in range(offset): + next(mask) + ret = {k: [] for k in '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'[:keylen]} + for n, k in zip(self.nums, mask): + ret[k].append(n) + return ret.values() + + def zip(self, key_mask, offset=0): + for _ in range(offset): + next(key_mask) + return [(n - k) % 29 for n, k in zip(self.nums, key_mask)] + + @staticmethod + def guess(parts, score_fn): # minimize score_fn + found = [] + avg_score = 0 + for nums in parts: + best = 9999999 + candidate = 0 + for i in range(29): + score = score_fn([(x - i) % 29 for x in nums]) + if score < best: + best = score + candidate = i + avg_score += best + found.append(candidate) + return avg_score / len(parts), found diff --git a/LP/LPath.py b/LP/LPath.py index 20f66ad..f635cc7 100755 --- a/LP/LPath.py +++ b/LP/LPath.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- coding: UTF-8 -*- import os.path FILES_SOLVED = ['0_warning', '0_welcome', '0_wisdom', '0_koan_1', diff --git a/LP/NGrams.py b/LP/NGrams.py index 0a2dd81..5303ecf 100755 --- a/LP/NGrams.py +++ b/LP/NGrams.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 +# -*- coding: UTF-8 -*- import re -from RuneText import RUNES, re_norune, RuneText +from Alphabet import RUNES +from IOReader import re_norune +from RuneText import RuneText from LPath import LPath @@ -54,21 +57,20 @@ class NGrams(object): return ret -def make_translation(stream=False): # if true, ignore spaces / word bounds - NGrams.translate(LPath.data('baseline-text'), - LPath.data('baseline-rune'), stream) +if __name__ == '__main__': + def make_translation(stream=False): # if true, ignore spaces / word bounds + NGrams.translate(LPath.data('baseline-text'), + LPath.data('baseline-rune'), stream) + def make_ngrams(max_ngram=1): + for i in range(1, max_ngram + 1): + print(f'generate {i}-gram file') + NGrams.make(i, infile=LPath.data('baseline-rune-words'), + outfile=LPath.data(f'p-{i}gram')) + NGrams.make(i, infile=LPath.root('_solved.txt'), + outfile=LPath.data(f'p-solved-{i}gram')) + NGrams.make(i, infile=LPath.data('baseline-rune-no-e'), + outfile=LPath.data(f'p-no-e-{i}gram')) -def make_ngrams(max_ngram=1): - for i in range(1, max_ngram + 1): - print(f'generate {i}-gram file') - NGrams.make(i, infile=LPath.data('baseline-rune-words'), - outfile=LPath.data(f'p-{i}gram')) - NGrams.make(i, infile=LPath.root('_solved.txt'), - outfile=LPath.data(f'p-solved-{i}gram')) - NGrams.make(i, infile=LPath.data('baseline-rune-no-e'), - outfile=LPath.data(f'p-no-e-{i}gram')) - - -# make_translation(stream=False) -# make_ngrams(5) + # make_translation(stream=False) + # make_ngrams(5) diff --git a/LP/HeuristicLib.py b/LP/Probability.py similarity index 96% rename from LP/HeuristicLib.py rename to LP/Probability.py index 2b37440..4d70c98 100755 --- a/LP/HeuristicLib.py +++ b/LP/Probability.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 +# -*- coding: UTF-8 -*- from NGrams import NGrams -from RuneText import RUNES +from Alphabet import RUNES def normalized_probability(int_prob): diff --git a/LP/Rune.py b/LP/Rune.py new file mode 100755 index 0000000..d849b38 --- /dev/null +++ b/LP/Rune.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# -*- coding: UTF-8 -*- +from Alphabet import RUNES, white_rune, rune_map, primes_map + + +######################################### +# Rune : Stores a single rune. Incl. text, prime, index, and kind +######################################### + +class Rune(object): + def __init__(self, r=None, t=None, i=None, p=None): + self._rune = r + self._text = t + self._index = i + self._prime = p + self._kind = None # one of: r n s l w + + def __repr__(self): + return f'<{self._rune}, {self._text}, {self._index}, {self._prime}>' + + @property + def rune(self): + if self._rune is None: + self._rune = RUNES[self._index] if self._index < 29 else '•' + return self._rune + + @property + def text(self): + if self._text is None: + r = self.rune + try: + self._text = rune_map[self.rune] + except KeyError: + self._text = white_rune.get(r, r) + return self._text + + @property + def index(self): + if self._index is None: + r = self._rune + self._index = RUNES.index(r) if r in RUNES else 29 + return self._index + + @property + def prime(self): + if self._prime is None: + self._prime = primes_map.get(self.rune, 0) + return self._prime + + @property + def kind(self): + if self._kind is None: + x = self.rune + if x in rune_map: + self._kind = 'r' # rune + elif x == '⁜': + self._kind = 's' # paragraph, but treat as sentence + elif x == '⁘': + self._kind = 's' # sentence + elif x == '\n' or x == '\r': + self._kind = 'l' # line end + elif x in '1234567890': + self._kind = 'n' # number + else: + self._kind = 'w' # whitespace (explicitly not n or s) + return self._kind + + def __add__(self, o): + if isinstance(o, Rune): + o = o.index + if self.index == 29 or o == 29: + return self + return Rune(i=(self.index + o) % 29) + + def __sub__(self, o): + if isinstance(o, Rune): + o = o.index + if self.index == 29 or o == 29: + return self + return Rune(i=(self.index - o) % 29) + + def __invert__(self): + return self if self.index == 29 else Rune(i=28 - self.index) diff --git a/LP/RuneRunner.py b/LP/RuneRunner.py deleted file mode 100755 index 8a08e89..0000000 --- a/LP/RuneRunner.py +++ /dev/null @@ -1,245 +0,0 @@ -#!/usr/bin/env python3 -import sys -from RuneText import RuneText -import lib as utils - - -######################################### -# RuneWriter : handle std output with highlight etc. -######################################### - -class RuneWriter(object): - MARKS = {'_': '\x1b[0m', 'r': '\x1b[31;01m', 'b': '\x1b[30;01m'} - - def __init__(self): - self.COLORS = True - self.VERBOSE = '-v' in sys.argv - self.QUIET = '-q' in sys.argv - self.BREAK_MODE = None - self.file_output = None - self.clear() - - def clear(self): - self.mark = False - self.alternate = False - self._marked = ['_'] * 4 - self.txt = [''] * 4 - - def is_empty(self): - return not self.txt[0] - - def line_break_mode(self): - if self.BREAK_MODE is not None: # if set by user - return self.BREAK_MODE - return 'l' if self.VERBOSE else 's' # dynamically adapt to mode - - def write(self, r=None, t=None, n1=None, n2=None): - m = ('b' if self.alternate else 'r') if self.mark else '_' - for i, v in enumerate([r, t, n1, n2]): - if v is None: - continue - if self.COLORS and self._marked[i] != m and i != 3: - self._marked[i] = m - prfx = self.MARKS[m] - else: - prfx = '' - self.txt[i] += prfx + v - - # def rm(self, r=0, t=0, n1=0, n2=0): - # for i, v in enumerate([r, t, n1, n2]): - # if v > 0: - # self.txt[i] = self.txt[i][:-v] - - def stdout(self): - def print_f(x=''): - if self.file_output: - with open(self.file_output, 'a') as f: - f.write(x + '\n') - else: - print(x) - - if self.is_empty(): - return - m = self.mark - self.mark = False # flush closing color - self.write(r='', t='', n1='', n2='') - self.mark = m - - if not self.QUIET or self.VERBOSE: - print_f() - if not self.QUIET: - print_f(self.txt[0]) - print_f(self.txt[1]) - if self.VERBOSE: - print_f(self.txt[2]) - print_f(self.txt[3]) - self.clear() - - -######################################### -# RuneReader : handles parsing of the file and line breaks etc. -######################################### - -class RuneReader(object): - def __init__(self): - self.data = None - self.loaded_file = None - self.words = {x: [] for x in range(20)} # increase for longer words - - def load(self, data=None, file=None, limit=None): - self.loaded_file = None - if not data: - with open(file, 'r') as f: - data = f.read()[:limit] - self.loaded_file = file - self.data = data if isinstance(data, RuneText) else RuneText(data) - self.generate_word_list() - - def has_data(self): - if len(self.data) > 0: - return True - return False - - def runes_no_whitespace(self): - return [x for x in self.data if x.kind == 'r'] - - def generate_word_list(self): - for x in self.words.values(): - x.clear() - res = [] - ai = 0 - ari = 0 - zri = 0 - for zi, x in enumerate(self.data): - if x.kind == 'l': - continue - elif x.kind == 'r': - res.append(x) - zri += 1 - else: - if len(res) > 0: - xt = RuneText(res) - self.words[len(xt)].append((ai, zi, ari, zri, xt)) - res = [] - ai = zi - ari = zri - - # count_callback('c|w|l', count, is-first-flag) - def parse(self, rune_fn, count_fn, whitespace_fn, break_line_on='l'): - word_sum = 0 - line_sum = 0 - for i, x in enumerate(self.data): - if x.kind == 'r': - r = rune_fn(self.data, i, word_sum == 0) - count_fn('c', r.prime, word_sum == 0) - word_sum += r.prime - elif x.kind == 'l' and x.kind != break_line_on: - continue # ignore all \n,\r if not forced explicitly - else: - if word_sum > 0: - count_fn('w', word_sum, line_sum == 0) - line_sum += word_sum - word_sum = 0 - if x.kind != 'l': # still ignore \n,\r - whitespace_fn(x) - if x.kind == break_line_on: - count_fn('l', line_sum, line_sum == 0) - line_sum = 0 - if word_sum > 0: - count_fn('w', word_sum, line_sum == 0) - line_sum += word_sum - if line_sum > 0: - count_fn('l', line_sum, True) - - -######################################### -# RuneRunner : Merge RuneWriter and RuneReader and stay in sync -######################################### - -class RuneRunner(object): - def __init__(self): - self.input = RuneReader() - self.output = RuneWriter() - self.marked_chars = [] - self.mark_alternate = [] - self.next_mark = False - self.fn_cipher = None - - def highlight_words_with_len(self, search_length): - found = [x for x in self.input.words[search_length]] - self.marked_chars = set(x for fp in found for x in range(fp[0], fp[1])) - return found - - def highlight_rune(self, rune, mark_occurrences=[]): - ip = 0 - tp = 0 - ret = [] - for i, x in enumerate(self.input.data): - if x.kind == 'r': - if x.rune == rune: - ip += 1 - ret.append((ip, tp, i, ip in mark_occurrences)) - tp += 1 - self.marked_chars = set(i for _, _, i, _ in ret) - self.mark_alternate = set(i for _, _, i, f in ret if not f) - return ret - - def reset_highlight(self): - self.marked_chars = [] - self.mark_alternate = [] - - def start(self, fn_cipher): - self.fn_cipher = fn_cipher - self.next_mark = False - self.input.parse( - self.rune_callback, self.count_callback, self.whitespace_callback, - self.output.line_break_mode()) - - def rune_callback(self, encrypted_data, index, is_first): - if self.output.VERBOSE: - fillup = len(self.output.txt[2]) - len(self.output.txt[1]) - if not is_first: - fillup += 1 # +1 cause n1 will add a '+' - if fillup > 0: - self.output.write(t=' ' * fillup) - if self.marked_chars: - x = encrypted_data[index] # always search on original data - mt = index in self.marked_chars - mn = index + 1 in self.marked_chars - self.output.alternate = index in self.mark_alternate - else: - x, mt, mn = self.fn_cipher(encrypted_data, index) - self.output.mark = mt - self.output.write(r=x.rune, t=x.text) - self.next_mark = mn - return x - - def count_callback(self, typ, num, is_first): - if typ == 'c': # char - if self.output.VERBOSE: - self.output.write(n1=('' if is_first else '+') + str(num)) - return - prm = utils.is_prime(num) - if typ == 'w': # word - tt = ('' if is_first else ' + ') + str(num) + ('*' if prm else '') - self.output.write(n2=tt) - if prm and num > 109 and not (self.output.VERBOSE or self.output.QUIET): - self.output.write(t='__') - elif typ == 'l': # line end (ignoring \n if mode is set to 's') - self.output.mark = False - # if not is_first: - sffx = ' = {}'.format(num) + ('*' if prm else '') - if utils.is_emirp(num): - sffx += '√' - if self.output.VERBOSE: - self.output.write(n2=sffx) - elif not self.output.QUIET: - self.output.write(t=sffx) - self.output.stdout() - - def whitespace_callback(self, rune): - if not self.next_mark: # dont mark whitespace after selection - self.output.mark = False - self.output.write(r=rune.rune, t=rune.text) - if self.output.VERBOSE: - self.output.write(n1=' ') diff --git a/LP/RuneSolver.py b/LP/RuneSolver.py index e0602d4..9492499 100755 --- a/LP/RuneSolver.py +++ b/LP/RuneSolver.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 -from RuneRunner import RuneRunner +# -*- coding: UTF-8 -*- from RuneText import Rune, RuneText -from lib import affine_decrypt +from utils import affine_decrypt ######################################### # RuneSolver : Generic parent class handles interrupts and text highlight ######################################### -class RuneSolver(RuneRunner): +class RuneSolver(object): def __init__(self): - super().__init__() self.reset() def reset(self): @@ -20,43 +19,31 @@ class RuneSolver(RuneRunner): def highlight_interrupt(self): return self.highlight_rune(self.INTERRUPT, self.INTERRUPT_POS) - def substitute_get(self, pos, keylen, search_term, found_term): - return found_term.zip_sub(search_term).description(count=True) - def substitute_supports_keylen(self): return False - def run(self, data=None): - if data: - self.input.load(data=data) - self.interrupt_counter = 0 - self.start(self.cipher_callback) + def substitute_get(self, pos, keylen, search_term, found_term, all_data): + return found_term.zip_sub(search_term).description(count=True) - def cipher_callback(self, encrypted_data, index): - obj = encrypted_data[index] - is_interrupt = obj.rune == self.INTERRUPT - if is_interrupt: - self.interrupt_counter += 1 - skip = is_interrupt and self.interrupt_counter in self.INTERRUPT_POS - mark_this = self.mark_char_at(index) - if not skip: - obj = self.cipher(obj, (index, encrypted_data)) - mark_next = self.mark_char_at(index) - return obj, mark_this, mark_next + def enum_data(self, data): + irp_i = 0 + r_pos = -1 + for i, obj in enumerate(data): + skip = obj.index == 29 + if not skip: + r_pos += 1 + is_interrupt = obj.rune == self.INTERRUPT + if is_interrupt: + irp_i += 1 + skip = is_interrupt and irp_i in self.INTERRUPT_POS + yield obj, i, r_pos, skip - def cipher(self, rune, context): - raise NotImplementedError # must subclass - - def mark_char_at(self, position): - return False + def run(self, data): + raise NotImplementedError('must subclass') + # return RuneText(), [(start-highlight, end-highlight), ...] def __str__(self): - txt = f'DATA: {len(self.input.data) if self.input.data else 0} bytes' - if self.input.loaded_file: - txt += f' (file: {self.input.loaded_file})' - else: - txt += f' (manual input)' - return txt + f'\ninterrupt jumps: {self.INTERRUPT_POS}' + return f'interrupt: {self.INTERRUPT}, jumps: {self.INTERRUPT_POS}' ######################################### @@ -66,22 +53,22 @@ class RuneSolver(RuneRunner): class SequenceSolver(RuneSolver): def __init__(self): super().__init__() - self.seq_index = 0 self.reset() def reset(self): super().reset() self.FN = None - def run(self, data=None): - self.seq_index = 0 + def run(self, data): assert(self.FN) - super().run(data=data) - - def cipher(self, rune, context): - x = self.FN(self.seq_index, rune) - self.seq_index += 1 - return x + seq_i = 0 + ret = [] + for rune, i, ri, skip in self.enum_data(data): + if not skip: + rune = self.FN(seq_i, rune) + seq_i += 1 + ret.append(rune) + return RuneText(ret), [] def __str__(self): return super().__str__() + f'\nf(x): {self.FN}' @@ -99,59 +86,52 @@ class RunningKeySolver(RuneSolver): def reset(self): super().reset() self.KEY_DATA = [] # the key material - self.KEY_INVERT = False # ABCD -> ZYXW self.KEY_SHIFT = 0 # ABCD -> DABC self.KEY_ROTATE = 0 # ABCD -> ZABC self.KEY_OFFSET = 0 # ABCD -> __ABCD self.KEY_POST_PAD = 0 # ABCD -> ABCD__ - def run(self, data=None): - self.k_current_pos = 0 - self.k_len = len(self.KEY_DATA) - self.k_full_len = self.KEY_OFFSET + self.k_len + self.KEY_POST_PAD - super().run(data=data) + def run(self, data): + k_len = len(self.KEY_DATA) + if k_len <= 0: + return data, [] + k_full_len = self.KEY_OFFSET + k_len + self.KEY_POST_PAD + k_current_pos = 0 + ret = [] + highlight = [[0, 0]] + for rune, i, ri, skip in self.enum_data(data): + if not skip: + u = k_current_pos - self.KEY_OFFSET + if u < 0 or u >= k_len or self.KEY_DATA[u] == 29: + self.unmodified_callback(rune) + else: + key_i = (u + self.KEY_SHIFT) % k_len + decrypted = self.decrypt(rune.index, key_i) + rune = Rune(i=(decrypted - self.KEY_ROTATE) % 29) + if i == highlight[-1][1]: + highlight[-1][1] = i + 1 + else: + highlight.append([i, i + 1]) + # rotate_key + if k_full_len > 0: # e.g., for key invert without a key + k_current_pos = (k_current_pos + 1) % k_full_len + ret.append(rune) + if highlight[0][1] == 0: + highlight = highlight[1:] + return RuneText(ret), highlight - def mark_char_at(self, position): - return self.active_key_pos() != -1 + def decrypt(self, rune_index, key_index): + raise NotImplementedError('must subclass') - def active_key_pos(self): - i = self.k_current_pos - self.KEY_OFFSET - if i >= 0 and i < self.k_len: - if self.KEY_DATA[i] != 29: # placeholder for unknown - return i - return -1 + def unmodified_callback(self, rune_index): + pass # subclass if needed - def cipher(self, rune, context): - r_idx = rune.index - if self.KEY_INVERT: - r_idx = 28 - r_idx - pos = self.active_key_pos() - if pos == -1: - self.copy_unmodified(r_idx) - else: - i = (pos + self.KEY_SHIFT) % self.k_len - r_idx = (self.decrypt(r_idx, i) - self.KEY_ROTATE) % 29 - # rotate_key - if self.k_full_len > 0: # e.g., for key invert without a key - self.k_current_pos = (self.k_current_pos + 1) % self.k_full_len - return Rune(i=r_idx) - - def decrypt(self, rune_index, key_index): # must subclass - raise NotImplementedError - - def copy_unmodified(self, rune_index): # subclass if needed - pass - - def key__str__(self): - return self.KEY_DATA # you should override this - - def key__str__basic_runes(self): + def key__str__(self): # you should override this return RuneText(self.KEY_DATA).description(indexWhitespace=True) def __str__(self): txt = super().__str__() txt += f'\nkey: {self.key__str__()}' - txt += f'\nkey invert: {self.KEY_INVERT}' txt += f'\nkey offset: {self.KEY_OFFSET} runes' txt += f'\nkey post pad: {self.KEY_POST_PAD} runes' txt += f'\nkey shift: {self.KEY_SHIFT} indices' @@ -170,15 +150,12 @@ class VigenereSolver(RunningKeySolver): def substitute_supports_keylen(self): return True - def substitute_get(self, pos, keylen, search_term, found_term): + def substitute_get(self, pos, keylen, search_term, found_term, all_data): ret = [Rune(r='⁚')] * keylen for i, r in enumerate(found_term.zip_sub(search_term)): ret[(pos + i) % keylen] = r return RuneText(ret).description(count=True, index=False) - def key__str__(self): - return self.key__str__basic_runes() - ######################################### # AffineSolver : Decrypt runes with an array of (s, t) affine keys @@ -188,42 +165,55 @@ class AffineSolver(RunningKeySolver): def decrypt(self, rune_index, key_index): return affine_decrypt(rune_index, self.KEY_DATA[key_index]) + def key__str__(self): + return self.KEY_DATA + ######################################### # AutokeySolver : Decrypts runes by using previously decrypted ones as input ######################################### class AutokeySolver(RunningKeySolver): - def run(self, data=None): + def run(self, data): key = self.KEY_DATA[self.KEY_SHIFT:] + self.KEY_DATA[:self.KEY_SHIFT] key = [29] * self.KEY_OFFSET + key + [29] * self.KEY_POST_PAD self.running_key = key - super().run(data=data) + return super().run(data) - def decrypt(self, rune_index, _): + def decrypt(self, rune_index, key_index): rune_index = (rune_index - self.running_key.pop(0)) % 29 self.running_key.append(rune_index) return rune_index - def copy_unmodified(self, rune_index): - if self.k_len > 0: - self.running_key.pop(0) - self.running_key.append(rune_index) + def unmodified_callback(self, rune_index): + self.running_key.pop(0) + self.running_key.append(rune_index) def substitute_supports_keylen(self): return True - def substitute_get(self, pos, keylen, search_term, found_term): - data = self.input.runes_no_whitespace() + def substitute_get(self, pos, keylen, search_term, found_term, all_data): + data = all_data.index_no_white ret = [Rune(r='⁚')] * keylen for o in range(len(search_term)): - plain = search_term[o] + plain = search_term[o].index i = pos + o while i >= 0: - plain = data[i] - plain + plain = (data[i] - plain) % 29 i -= keylen - ret[i + keylen] = plain + ret[i + keylen] = Rune(i=plain) return RuneText(ret).description(count=True, index=False) - def key__str__(self): - return self.key__str__basic_runes() + +if __name__ == '__main__': + slvr = VigenereSolver() + slvr.KEY_DATA = [1] + print(slvr) + txt = RuneText('hi there') + sol = slvr.run(txt) + print(sol[0].text) + sol, mark = slvr.run(txt) + print(sol.text) + slvr.KEY_DATA = [-1] + print(slvr.run(sol)[0].text) + print(mark) diff --git a/LP/RuneText.py b/LP/RuneText.py index 98b8fbf..7a52e21 100755 --- a/LP/RuneText.py +++ b/LP/RuneText.py @@ -1,112 +1,7 @@ #!/usr/bin/env python3 # -*- coding: UTF-8 -*- -import re # load_indices - -white_rune = {'•': ' ', '⁘': '.', '⁚': ',', '⁖': ';', '⁜': '#'} -white_text = {v: k for k, v in white_rune.items()} -alphabet = [ # Using last value for display. Custom added: V - (2, 'ᚠ', ['F']), (3, 'ᚢ', ['V', 'U']), (5, 'ᚦ', ['TH']), (7, 'ᚩ', ['O']), - (11, 'ᚱ', ['R']), (13, 'ᚳ', ['K', 'C']), (17, 'ᚷ', ['G']), - (19, 'ᚹ', ['W']), (23, 'ᚻ', ['H']), (29, 'ᚾ', ['N']), (31, 'ᛁ', ['I']), - (37, 'ᛄ', ['J']), (41, 'ᛇ', ['EO']), (43, 'ᛈ', ['P']), (47, 'ᛉ', ['X']), - (53, 'ᛋ', ['Z', 'S']), (59, 'ᛏ', ['T']), (61, 'ᛒ', ['B']), - (67, 'ᛖ', ['E']), (71, 'ᛗ', ['M']), (73, 'ᛚ', ['L']), - (79, 'ᛝ', ['ING', 'NG']), (83, 'ᛟ', ['OE']), (89, 'ᛞ', ['D']), - (97, 'ᚪ', ['A']), (101, 'ᚫ', ['AE']), (103, 'ᚣ', ['Y']), - (107, 'ᛡ', ['IO', 'IA']), (109, 'ᛠ', ['EA']) -] -text_map = {t: r for _, r, ta in alphabet for t in ta} -rune_map = {r: t for _, r, ta in alphabet for t in ta} -primes_map = {r: p for p, r, _ in alphabet} -RUNES = [r for _, r, _ in alphabet] # array already sorted -re_norune = re.compile('[^' + ''.join(RUNES) + ']') -# del alphabet # used in playground for GP display - - -######################################### -# Rune : Stores a single rune. Incl. text, prime, index, and kind -######################################### - -class Rune(object): - def __init__(self, r=None, t=None, i=None, p=None): - self._rune = r - self._text = t - self._index = i - self._prime = p - self._kind = None # one of: r n s l w - - def __repr__(self): - return f'<{self._rune}, {self._text}, {self._index}, {self._prime}>' - - @property - def rune(self): - if self._rune is None: - self._rune = RUNES[self._index] if self._index < 29 else '•' - return self._rune - - @property - def text(self, sameWhitespace=False): - if self._text is None: - if sameWhitespace: - self._text = rune_map.get(self.rune, ' ') - else: - r = self.rune - self._text = rune_map.get(r, white_rune.get(r, r)) - return self._text - - @property - def index(self): - if self._index is None: - r = self._rune - self._index = RUNES.index(r) if r in RUNES else 29 - return self._index - - @property - def prime(self): - if self._prime is None: - self._prime = primes_map.get(self.rune, 0) - return self._prime - - @property - def kind(self): - if self._kind is None: - x = self.rune - if x in rune_map: - self._kind = 'r' # rune - elif x == '⁜': - self._kind = 's' # paragraph, but treat as sentence - elif x == '⁘': - self._kind = 's' # sentence - elif x == '\n' or x == '\r': - self._kind = 'l' # line end - elif x in '1234567890': - self._kind = 'n' # number - else: - self._kind = 'w' # whitespace (explicitly not n or s) - return self._kind - - def __add__(self, o): - if isinstance(o, Rune): - o = o.index - if self.index == 29 or o == 29: - return self - return Rune(i=(self.index + o) % 29) - - def __sub__(self, o): - if isinstance(o, Rune): - o = o.index - if self.index == 29 or o == 29: - return self - return Rune(i=(self.index - o) % 29) - - def __radd__(self, o): - return self if self.index == 29 else Rune(i=(o + self.index) % 29) - - def __rsub__(self, o): - return self if self.index == 29 else Rune(i=(o - self.index) % 29) - - def __invert__(self): - return self if self.index == 29 else Rune(i=28 - self.index) +from Alphabet import white_rune, white_text, rune_map, text_map +from Rune import Rune ######################################### @@ -139,16 +34,6 @@ class RuneText(object): self._data_len = len(self._data) - def __len__(self): - return self._data_len - - def trim(self, maxlen): - if self._data_len > maxlen: - if self._rune_sum and self._rune_sum > 0: - self._rune_sum -= sum(x.prime for x in self._data[maxlen:]) - self._data = self._data[:maxlen] - self._data_len = maxlen - @classmethod def from_text(self, text): res = [] @@ -186,17 +71,29 @@ class RuneText(object): res.append(Rune(r=rune, t=char)) return res - def description(self, count=False, index=True, indexWhitespace=False): - return None if len(self) == 0 else \ - self.rune + (f' ({len(self)})' if count else '') + ' - ' + \ - self.text + (f' ({len(self.text)})' if count else '') + \ - (f' - {self.index if indexWhitespace else self.index_rune_only}' - if index else '') + def __len__(self): + return self._data_len - def zip_sub(self, other): - if len(self) != len(other): - raise IndexError('RuneText length mismatch') - return RuneText([x - y for x, y in zip(self._data, other._data)]) + def __getitem__(self, key): + if isinstance(key, str): + return [getattr(x, key) for x in self._data] + else: + return self._data[key] + + # def __setitem__(self, key, value): + # self._data[key] = value + + def __add__(self, other): + return RuneText([x + other for x in self._data]) + + def __sub__(self, other): + return RuneText([x - other for x in self._data]) + + def __invert__(self): + return RuneText([~x for x in self._data]) + + def __str__(self): + return f'RuneText<{len(self)}>' @property def text(self): @@ -207,11 +104,11 @@ class RuneText(object): return ''.join(x.rune for x in self._data) @property - def index(self): + def index_no_newline(self): return [x.index for x in self._data if x.kind != 'l'] @property - def index_rune_only(self): + def index_no_white(self): return [x.index for x in self._data if x.index != 29] @property @@ -224,50 +121,82 @@ class RuneText(object): self._rune_sum = sum(self.prime) return self._rune_sum - def __getitem__(self, key): - if isinstance(key, str): - return [getattr(x, key) for x in self._data] - else: - return self._data[key] + @property + def data_clean(self): + return [x if x.kind == 'r' else Rune(i=29) + for x in self._data if x.kind != 'l'] - def __setitem__(self, key, value): - self._data[key] = value + def description(self, count=False, index=True, indexWhitespace=False): + return None if len(self) == 0 else \ + self.rune + (f' ({len(self)})' if count else '') + ' - ' + \ + self.text + (f' ({len(self.text)})' if count else '') + \ + (' - {}'.format(self.index_no_newline if indexWhitespace else + self.index_no_white) + if index else '') - def __add__(self, other): - return RuneText([x + other for x in self._data]) + def trim(self, maxlen): + if self._data_len > maxlen: + if self._rune_sum and self._rune_sum > 0: + self._rune_sum -= sum(x.prime for x in self._data[maxlen:]) + self._data = self._data[:maxlen] + self._data_len = maxlen - def __sub__(self, other): - return RuneText([x - other for x in self._data]) + def zip_sub(self, other): + if len(self) != len(other): + raise IndexError('RuneText length mismatch') + return RuneText([x - y for x, y in zip(self._data, other._data)]) - def __radd__(self, other): - return RuneText([other + x for x in self._data]) + # def equal(self, other): + # if len(self) != len(other): + # return False + # return all(x.index == y.index for x, y in zip(self, other)) - def __rsub__(self, other): - return RuneText([other - x for x in self._data]) - - def __invert__(self): - return RuneText([~x for x in self._data]) - - def __repr__(self): - return f'RuneText<{len(self._data)}>' - - -######################################### -# load page and convert to indices for faster access -######################################### - -def load_indices(fname, interrupt, maxinterrupt=None, minlen=None, limit=None): - with open(fname, 'r') as f: - data = RuneText(re_norune.sub('', f.read())).index_rune_only[:limit] - if maxinterrupt is not None: - # incl. everything up to but not including next interrupt - # e.g., maxinterrupt = 0 will return text until first interrupt - for i, x in enumerate(data): - if x != interrupt: + def enum_words(self): # [(start, end, len), ...] may include \n \r + start = 0 + r_pos = 0 + word = [] + for i, x in enumerate(self._data): + if x.kind == 'r': + r_pos += 1 + word.append(x) + elif x.kind == 'l': continue - if maxinterrupt == 0: - if minlen and i < minlen: - continue - return data[:i] - maxinterrupt -= 1 - return data + else: + if len(word) > 0: + yield start, i, r_pos - len(word), RuneText(word) + word = [] + start = i + 1 + + +class RuneTextFile(RuneText): + def __init__(self, file, limit=None): + with open(file, 'r') as f: + super().__init__(f.read()[:limit]) + self.inverted = False + self.loaded_file = file + + def reopen(self, limit=None): + ret = RuneTextFile(self.loaded_file, limit) + if self.inverted: + ret.invert() + return ret + + def invert(self): + self.inverted = not self.inverted + self._rune_sum = None + self._data = [~x for x in self._data] + + def __str__(self): + return '@file: {} ({} bytes), inverted: {}'.format( + self.loaded_file, len(self._data), self.inverted) + + +if __name__ == '__main__': + x = RuneText('Hi there. And welc\nome, to my "world";') + for a, z, r_pos, word in x.enum_words(): + print((a, z), r_pos, word.text) + + y = RuneTextFile(file='../_input.txt') + print(y.loaded_file) + print(y.prime_sum) + print(y) diff --git a/LP/__init__.py b/LP/__init__.py index e1c85bc..60cef66 100644 --- a/LP/__init__.py +++ b/LP/__init__.py @@ -1,17 +1,24 @@ import sys -if True: - sys.path.append(__path__[0]) +if __name__ != '__main__': + sys.path.insert(0, __path__[0]) -import lib as utils +import utils from LPath import FILES_ALL, FILES_UNSOLVED, FILES_SOLVED from LPath import LPath as path -from RuneSolver import VigenereSolver, AffineSolver, AutokeySolver, SequenceSolver -from RuneText import Rune, RuneText -from RuneText import RUNES, alphabet, load_indices -from HeuristicSearch import GuessVigenere, GuessAffine, GuessPattern -from HeuristicSearch import SearchInterrupt -from HeuristicLib import Probability +from Alphabet import RUNES, alphabet +from Rune import Rune +from RuneText import RuneText, RuneTextFile + +from IOReader import load_indices, longest_no_interrupt +from IOWriter import IOWriter + +from RuneSolver import SequenceSolver, VigenereSolver, AffineSolver, AutokeySolver +from KeySearch import GuessVigenere, GuessAffine, GuessPattern +from Probability import Probability + from InterruptDB import InterruptDB +from InterruptIndices import InterruptIndices +from InterruptSearch import InterruptSearch from FailedAttempts import NGramShifter diff --git a/LP/lib.py b/LP/utils.py similarity index 90% rename from LP/lib.py rename to LP/utils.py index 3522074..82bb932 100755 --- a/LP/lib.py +++ b/LP/utils.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- coding: UTF-8 -*- import math @@ -113,7 +114,9 @@ def autokey_reverse(data, keylen, pos, search_term): ret[i + keylen] = plain return ret -# alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' -# cipher = 'YDIDWYASDDJVAPJMMBIASDTJVAMD' -# indices = [affine_decrypt(alphabet.index(x), (5, 9), 26) for x in cipher] -# print(''.join(alphabet[x] for x in indices)) + +if __name__ == '__main__': + alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + cipher = 'YDIDWYASDDJVAPJMMBIASDTJVAMD' + indices = [affine_decrypt(alphabet.index(x), (5, 9), 26) for x in cipher] + print(''.join(alphabet[x] for x in indices)) diff --git a/README.md b/README.md index 200fc09..ac30a70 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ### Main components: -- `playground.py` this is where you want to start. Simply run it and it will great you with all the posibilities. Use this if you want to experiment, translate runes, check for primes, etc. See [Playground](#playground) for more info. +- `playground.py` this is where you want to start. Simply run it and it will greet you with all the posibilities. Use this if you want to experiment, translate runes, check for primes, etc. See [Playground](#playground) for more info. - `solver.py` you can run `solver.py -s` to output all already solved pages. Other than that, this is the playground to test new ideas against the unsolved pages. Here you can automate stuff and test it on all the remaining pages; e.g., there is a section to try out totient functions. See [Solving](#solving) for more info. @@ -22,18 +22,20 @@ The `pages` folder contains all LP pages in text and graphic. Note, I have doubl Rune values are taken from Gematria, with these unicode characters representing: space (`•`), period (`⁘`), comma (`⁚`), semicolon (`⁖`), and chapter mark (`⁜`). -### The library +### The LP library These files you probably wont need to touch unless you want to modify some output behavior or rune handling. E.g. if you want to add a rune multiply method. These are the building blocks for the main components. -- `lib.py`, a small collection of reusable functions like `is_prime` and `rev` for emirp (reverse prime) checking. +- `utils.py`, a small collection of reusable functions like `rev`, `is_prime`, and `is_emirp` (reverse prime) checking. - `RuneText.py` is the representation layer. The class `RuneText` holds an array of `Rune` objects, which represent the individual runes. Each `Rune` has the attributes `rune`, `text`, `prime`, `index`, and `kind` (see [Solving](#solving)). -- `RuneRunner.py` is a collection of classes that handles data input as well as ouput to stdout. It does all the word sum calculations, prime word detection, line sums, and output formatting (including colors). Everything you don't want to worry about when processing the actual runes. - - `RuneSolver.py` contains a specific implementation for each cipher type. Two implementations in particular, `VigenereSolver` which has methods for setting and modifying key material as well as automatic key rotation and interrupt skipping. `SequenceSolver` interprets the cipher on a continuous or discrete function (i.e., Euler's totient). +- `IOWriter.py` handles data ouput to stdout. It does all the word sum calculations, prime word detection, line sums, and output formatting (including colors). Everything you don't want to worry about when processing the actual runes. + +- and many more … + Refer to `solver.py` or section [Solving](#solving) for examples on usage. @@ -51,13 +53,13 @@ Available commands are: d : Get decryption key (substitution) for a single phrase f : Find words with a given length (f 4, or f word) g : Print Gematria Primus (gp) or reversed Gematria (gpr) - h : Highlight occurrences of interrupt jumps (hj) or reset (h) + h : Highlight occurrences of interrupt jumps (hj or hj 28) k : Re/set decryption key (k), invert key (ki), ': change key shift (ks), rotation (kr), offset (ko), or after padding (kp) ': set key jumps (kj) e.g., [1,2] (first appearence of ᚠ is index 1) l : Toggle log level: normal (ln), quiet (lq) verbose (lv) p : Prime number and emirp check - t : Translate between runes, text, and indices (0-28) + t : Translate between runes, text, indices (0-28), and primes x : Execute decryption. Also: load data into memory ': set manually (x DATA) or load from file (xf p0-2) (default: _input.txt) ': limit/trim loaded data up to nth character (xl 300) @@ -148,7 +150,8 @@ Gematria Primus (reversed) ### h) Hightlight occurrences -Highlighting is currently very limited. The only supported option is `hj` which will hightlight all interrupts. That is, it will hightlight all occurrences of `ᚠ` in the text and mark those that are actively skipped or jumped over. +Highlighting is currently very limited. The only supported option is `hj` which will hightlight all interrupts. That is, it will hightlight all occurrences of `ᚠ` in the text and mark those that are actively skipped or jumped over. +Or use `hj x` to highlight a different rune (where x is either a number or text). ![highlight interrupts](img/hj.png) @@ -249,56 +252,87 @@ If the output is too long, you can limit (the already loaded data) with `xl 180` `Rune.kind` can be one of `r n s l w` – meaning (r)une, (n)umber, (s)entence, (l)ine, or (w)hitespace. A line is what you see in the source file (which is equivalent to a line in the original jpg page). A sentence is one that ends with a period (`⁘`). -`Rune` as well as `RuneText` both support simple arithmetic operations: `Rune(i=2) - 2` will yield a `ᚠ` rune. For example, you can invert a text with `28 - RuneText('test me')` or simply `~RuneText('inverted')`. +`Rune` as well as `RuneText` both support simple arithmetic operations: `Rune(i=2) - 2` will yield a `ᚠ` rune. You can invert text with `~RuneText('inverted')`. __Note:__ Always initialize a rune with its rune character or its index, never ASCII or its prime value. -### RuneRunner and I/O +### RuneTextFile, IOWriter, and I/O -`RuneRunner` has two noteworthy attributes `input` and `output`; `RuneReader` and `RuneWriter` respectively. Use the former to load data into memory: +Here is a fully working example to load rune data from file, solve it with a vigenere solver, and output it with color highlighting. -``` -solver.load(file='p33.txt') -solver.load(data='will be parsed') -solver.load(RuneText('will be copied')) +```python +solver = LP.VigenereSolver() +solver.KEY_DATA = LP.RuneText('divinity').index_no_white +solver.INTERRUPT_POS = [4, 5, 6, 7, 10, 11, 14, 18, 20, 21, 25] +d_in = LP.RuneTextFile(LP.path.page('0_welcome')) +d_out, _ = solver.run(d_in) +LP.IOWriter().run(d_out, [(0, 8), (510, 517), (630, 667)]) ``` The output writer has the options `COLORS`, `VERBOSE`, `QUIET`, and `BREAK_MODE` to control the appearance. `BREAK_MODE` can be one of the `Rune.kind` values. -### RuneSolver, VigenereSolver, SequenceSolver, AffineSolver +### RuneSolver, VigenereSolver, SequenceSolver, AffineSolver, AutokeySolver -All `RuneSolver` subclasses inherit the attributes of `RuneRunner` and will include additional data fields that can be set. In its most basic form it has the two fields `INTERRUPT` (must be rune) and `INTERRUPT_POS` (list of indices). +All solver subclasses inherit the attributes of `RuneSolver ` and will include additional data fields that can be set. The most basic form has the two fields `INTERRUPT` (must be rune) and `INTERRUPT_POS` (list of indices). -In the case of `VigenereSolver` the additional fields are `KEY_DATA` (list of indices), `KEY_INVERT` (bool), `KEY_SHIFT` (int), `KEY_ROTATE` (int), `KEY_OFFSET` (int), and `KEY_POST_PAD` (int). +In the case of `VigenereSolver` the additional fields are `KEY_DATA` (list of indices), `KEY_SHIFT` (int), `KEY_ROTATE` (int), `KEY_OFFSET` (int), and `KEY_POST_PAD` (int). The class `SequenceSolver` has only one additional parameter which is `FN` (function pointer or lambda expression). -`AffineSolver` is very similar to `VigenereSolver` but does not support key manipulation (yet). `KEY_DATA` and `KEY_INVERT` are the only two attributes. +`AffineSolver` is very similar to `VigenereSolver` but uses `(a, b)` tuples as key data. + +`AutokeySolver` is also based on `VigenereSolver` but reuses key data that was previously decrypted. + + +### OEIS checker + +`solver.py` has also a fully automated OEIS sequence checker. The script tests all 294k sequences that contain at least 14 numbers but limits each sequence to the first 40 numbers. For each sequence the script will try to shift the runes and see if a useful word is generated. Useful in this case means it appears in a [dictionary of 350k words](https://github.com/dwyl/english-words/). + +If all, or all but one, words appear in said dictionary, the seqeuence is printed out. Additionally, the script will also try to shift the generated sequence by all rune indices mod 29. Further, each sequence is tested not only starting at position 0, but also with an offset of -1 to +3 (e.g., 00123 to 345...). Each input is tested with all interrupt combinations (assuming ᚠ is interrupt). + +Assumptions: + +- the sequence starts at the beginning +- the beginning is at the top-left and text goes left-to-right +- whitespace is actually correct +- ᚠ is interrupt (or none at all) + ## Heuristics -This is where the magic happens. `HeuristicLib.py` contains the basic frequency analysis metrics like Index of Coincidence (IoC) and similarity matching. The latter is used to automatically detect key shifts – like in Vigenere or Affine. These metrics are based on english sample texts, in this case “Peace and War” or “Gadsby” (text without the letter ‘e’ [well almost, because there are still 6 e's in there ... liar!]). +This is where the magic happens. `Probability.py` contains the basic frequency analysis metrics like Index of Coincidence (IoC) and similarity matching. The latter is used to automatically detect key shifts – like in Vigenere or Affine. These metrics are based on english sample texts, in this case “Peace and War” or “Gadsby” (text without the letter ‘e’ [well almost, because there are still 6 e's in there ... liar!]). `NGrams.py` is respobsible for taking english text (or any other language) and translating it to runes. Also, counts runes in a text and creates the frequency distribution. The translation is the slowest part, but still very efficient. Creating all 1-gram to 5-grams of a 7 Mb text file takes approx. 20 sec. `FailedAttempts.py` is a collection of what the title is saying – failed attempts. Currently only holds a n-gram shifter. Which will shift every n runes in contrast to the normal decrypting of a single rune at a time. -#### GuessVigenere, GuessAffine +#### GuessVigenere, GuessAffine, GuessPattern -Two classes that enumerate all possible shifts for a key. For Vigenere that is key length * 29, for Affine key length * 29^2. To determine whether one shift is more likely than another, a similarity metric is used. In this case, the least square distance to a normal english distribution. The value will be lowest if it closely matches the frequencies of each rune. +These classes enumerate all possible shifts for a key. For Vigenere that is key length * 29, for Affine key length * 29^2. To determine whether one shift is more likely than another, a similarity metric is used. In this case, the least square distance to a normal english distribution. The value will be lowest if it closely matches the frequencies of each rune. + +`GuessPattern` uses an input template, e.g., `1234` and outputs different key permutations such as: + +`1234234134124123`, +`1234412334122341`, +`1234341212343412`, +`12344321`, and `123432`. -### HeuristicSearch.py +### InterruptSearch.py This is the heart of the interrupt detector. Searching the full set of possible constellations is not feasable (2 ^ {number of possible interrupts}). Thus, the class has two methods to avoid the full search. Both come with a maximum look ahead parameter that can be tweaked. Lets look at an example with 66 interrupts (p8–14). Testing all would require 2^66 or __7.4*10^19__ calculations. +#### SearchInterrupt.all + +Just tries all combinations without leaving anything out. + #### SearchInterrupt.sequential This will go through the text sequentially. Looking at the first N interrupts and try all combinations in this subset. The best combination will determine whether the current interrupt (1. interrupt index) should be added to the final result. If the current index was used to generate the best value then it is included otherwise not. __Note:__ it will only add the first interrupt, not all of them. The next iteration will look at the interrupts at index 1 to N+1. Adding the next index if it was in the set, and repeating with the remaining text. @@ -318,7 +352,7 @@ The complexity is not linear and depends on whether “there was just another be Calculating the best interrupt position takes quite long, so we can optimize our program by pre-calculating the IoC's. That is what `InterruptDB.py` is for. The class will search for the best interrupts and store the IoC score as well as the set of interrupts in a file. Later queries just need to process this file instead. -The current configuration will look at the first 20 interrupts, for all runes, on all pages, and up to a key length of 32 – thats 1.36*10^10 operations! The full execution time is somewhere around 38 hours. Luckily, it is a one-time job. The resulting database is used directly as is, plus a html file is generated by `InterruptToWeb` for a graphical representation. Meanwhile, `InterruptIndices` keeps count how reliable the results are, e.g., how many runes were considered when looking for the first 20 interrupts, and adds that information to the html. Here is the [html overview](./InterruptDB/). +The current configuration will look at the first 20 interrupts, for all runes, on all pages, and up to a key length of 32 – thats 1.36*10^10 operations! The full execution time is somewhere around 38 hours. Luckily, it is a one-time job. The resulting database is used directly as is, plus a html file is generated by `InterruptToWeb.py` for a graphical representation. Meanwhile, `InterruptIndices.py` keeps count how reliable the results are, e.g., how many runes were considered when looking for the first 20 interrupts, and adds that information to the html. Here is the [html overview](./InterruptDB/). ### probability.py diff --git a/img/4gq25.jpg b/img/4gq25.jpg new file mode 100644 index 0000000000000000000000000000000000000000..22027296dcd7df53c5127e6aadbabeaf3223f72d GIT binary patch literal 26342 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 J!odIkCIFAMzu*7> literal 0 HcmV?d00001 diff --git a/img/zN4h51m.jpg b/img/zN4h51m.jpg new file mode 100644 index 0000000000000000000000000000000000000000..47a4d27493afffedac5ed719fa7507358ff5a360 GIT binary patch literal 41731 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 1 else '' - if cmd[0] != 'l': # only log mode allowed after find operation - SOLVER.reset_highlight() - if cmd == 'help': print(help_str) elif cmd == 'q' or cmd == 'exit' or cmd == 'quit': exit() elif cmd == '?': + print('DATA:', INPUT) print(SOLVER) else: cmdX = {'a': command_a, 'd': command_d, 'f': command_f, @@ -83,7 +87,7 @@ def command_a(cmd, args): # [a]ll variations if 'i' in cmd: root = ~root for i in range(29): - print('{:02d}: {}'.format(i, (root + i).description(index=inclIndex))) + print('{:02d}: {}'.format(i, (root - i).description(index=inclIndex))) ######################################### @@ -125,12 +129,16 @@ def command_f(cmd, args): # (f)ind word search_term = LP.RuneText(args) s_len = len(search_term) - cur_words = SOLVER.highlight_words_with_len(s_len) - SOLVER.run() + cur_words = [x for x in INPUT.enum_words() if len(x[-1]) == s_len] + if len(cur_words) == 0: + print('No matching word found.') + return + + OUTPUT.run(INPUT, [(a, b) for a, b, _, _ in cur_words]) print() print('Found:') - for _, _, pos, _, w in cur_words: - print(f'{pos:04}: {w.description(count=True)}') + for _, _, pos, word in cur_words: + print(f'{pos:04}: {word.description(count=True)}') if search_term: print() keylen = [len(search_term)] @@ -146,9 +154,9 @@ def command_f(cmd, args): # (f)ind word raise ValueError('not a number.') print() print('Available substition:') - for _, _, pos, _, w in cur_words: + for _, _, pos, word in cur_words: for kl in keylen: - res = SOLVER.substitute_get(pos, kl, search_term, w) + res = SOLVER.substitute_get(pos, kl, search_term, word, INPUT) print(f'{pos:04}: {res}') @@ -179,19 +187,31 @@ def command_g(cmd, args): # (g)ematria primus ######################################### def command_h(cmd, args): # (h)ighlight - if cmd == 'h': - SOLVER.reset_highlight() - SOLVER.run() - elif cmd in 'hj hi': - res = SOLVER.highlight_interrupt() - SOLVER.run() + if len(cmd) > 1 and cmd[1] in 'ji': + try: + irp = get_cmd_int(cmd, args) + except ValueError: + irp = LP.RuneText(args)[0].index + res = [] + r_pos = -1 + for i, x in enumerate(INPUT): + if x.index != 29: + r_pos += 1 + if x.index == irp: + res.append(([i, i + 1], r_pos)) + + irp_set = [x for x in SOLVER.INTERRUPT_POS if x <= len(res)] + for i in irp_set: + res[i - 1][0].append('1;37m\x1b[45m') + # run without decryption + OUTPUT.run(INPUT, [x for x, _ in res]) txt = '' bits = '' - # first appearance of ᚠ is l_pos == 1; r_pos is the index on runes only - for l_pos, r_pos, _, is_set in res: - txt += '{}.{}.{} '.format(l_pos, 'T' if is_set else 'F', r_pos) - bits += '1' if is_set else '0' - print(f'\nInterrupts: {bits}\n{txt}') + for i, (_, r_pos) in enumerate(res): + i += 1 # first occurrence of interrupt is index 1 + txt += f"{i}.{'T' if i in irp_set else 'F'}.{r_pos} " + bits += '1' if i in irp_set else '0' + print(f'\nInterrupt({LP.RUNES[irp]}): {bits}\n{txt}') else: return False @@ -202,7 +222,7 @@ def command_h(cmd, args): # (h)ighlight def command_k(cmd, args): # (k)ey manipulation if cmd == 'k' or cmd == 'key': - SOLVER.KEY_DATA = LP.RuneText(args).index + SOLVER.KEY_DATA = LP.RuneText(args).index_no_newline print(f'set key: {SOLVER.KEY_DATA}') elif cmd[1] == 's': SOLVER.KEY_SHIFT = get_cmd_int(cmd, args, 'shift') @@ -213,8 +233,12 @@ def command_k(cmd, args): # (k)ey manipulation elif cmd[1] == 'p': SOLVER.KEY_POST_PAD = get_cmd_int(cmd, args, 'post padding') elif cmd[1] == 'i': - SOLVER.KEY_INVERT = not SOLVER.KEY_INVERT - print(f'set key invert: {SOLVER.KEY_INVERT}') + global INPUT + if isinstance(INPUT, LP.RuneTextFile): + INPUT.invert() + print(f'set key invert: {INPUT.inverted}') + else: + INPUT = ~INPUT elif cmd == 'kj': args = args.strip('[]') pos = [int(x) for x in args.split(',')] if args else [] @@ -222,7 +246,7 @@ def command_k(cmd, args): # (k)ey manipulation print(f'set interrupt jumps: {SOLVER.INTERRUPT_POS}') else: return False # command not found - SOLVER.run() + solve() ######################################### @@ -231,15 +255,15 @@ def command_k(cmd, args): # (k)ey manipulation def command_l(cmd, args): # (l)og level if cmd == 'lv' or args == 'v' or args == 'verbose': - SOLVER.output.VERBOSE = not SOLVER.output.VERBOSE + OUTPUT.VERBOSE = not OUTPUT.VERBOSE elif cmd == 'lq' or args == 'q' or args == 'quiet': - SOLVER.output.QUIET = not SOLVER.output.QUIET + OUTPUT.QUIET = not OUTPUT.QUIET elif cmd == 'ln' or args == 'n' or args == 'normal': - SOLVER.output.VERBOSE = False - SOLVER.output.QUIET = False + OUTPUT.VERBOSE = False + OUTPUT.QUIET = False else: return False - SOLVER.run() + solve() ######################################### @@ -267,7 +291,7 @@ def command_t(cmd, args): # (t)ranslate print('runes({}): {}'.format(len(word), word.rune)) print('plain({}): {}'.format(len(word.text), word.text)) print('reversed: {}'.format((~word).rune)) - print('indices: {}'.format(word.index)) + print('indices: {}'.format(word.index_no_newline)) print('prime({}{}): {}'.format(word.prime_sum, sffx, word.prime)) @@ -276,24 +300,23 @@ def command_t(cmd, args): # (t)ranslate ######################################### def command_x(cmd, args): # e(x)ecute decryption + global INPUT if cmd == 'x': - pass # just run the solver + if args.strip(): + INPUT = LP.RuneText(args) elif cmd == 'xf': # reload from file file = LP.path.page(args) if args else LP.path.root('_input.txt') print('loading file:', file) - SOLVER.input.load(file=file) - args = None # so run() won't override data + INPUT = LP.RuneTextFile(file) elif len(cmd) > 0 and cmd[1] == 'l': # limit content limit = get_cmd_int(cmd, args, 'read limit') - last_file = SOLVER.input.loaded_file - if last_file: - SOLVER.input.load(file=last_file) + if isinstance(INPUT, LP.RuneTextFile): + INPUT = INPUT.reopen() if limit > 0: - SOLVER.input.data.trim(limit) - args = None + INPUT.trim(limit) else: return False - SOLVER.run(args if args else None) + solve() if __name__ == '__main__': diff --git a/probability.py b/probability.py index b765d79..1085b8e 100755 --- a/probability.py +++ b/probability.py @@ -5,44 +5,51 @@ import LP INVERT = False KEY_MAX_SCORE = 0.05 AFF_MAX_SCORE = 0.04 -IRP_F_ONLY = True session_files = [] +db_i = LP.InterruptIndices() +if True: + db = LP.InterruptDB.load('db_norm') + IOC_MIN_SCORE = 0.55 +else: + db = LP.InterruptDB.load('db_high') + IOC_MIN_SCORE = 1.35 + ######################################### # Perform heuristic search on the keylength, interrupts, and key ######################################### def break_cipher(fname, candidates, solver, key_fn): + slvr = solver() + io = LP.IOWriter() + io.QUIET = True + inpt = LP.RuneTextFile(LP.path.page(fname)) + if INVERT: + inpt.invert() + data = inpt.index_no_white + + if key_fn.__name__ == 'GuessAffine': + key_max_score = AFF_MAX_SCORE + else: + key_max_score = KEY_MAX_SCORE + def fn_similarity(x): return LP.Probability(x).similarity() - filename = LP.path.page(fname) - slvr = solver() - slvr.input.load(file=filename) - slvr.output.QUIET = True - slvr.output.COLORS = False - slvr.KEY_INVERT = INVERT - key_max_score = KEY_MAX_SCORE - if key_fn.__name__ == 'GuessAffine': - key_max_score = AFF_MAX_SCORE + outfmt = 'IoC: {}, interrupt: {}, count: {}, solver: {}' for irp_count, score, irp, kl, skips in candidates: - if IRP_F_ONLY and irp != 0: - continue - data = LP.load_indices(filename, irp, maxinterrupt=irp_count) - if INVERT: - data = [28 - x for x in data] - iguess = LP.SearchInterrupt(data, (28 - irp) if INVERT else irp) - print('IoC: {}, interrupt: {}, count: {}, solver: {}'.format( - score, LP.RUNES[irp], len(iguess.stops), key_fn.__name__)) - testcase = iguess.join(iguess.from_occurrence_index(skips)) - + stops, upto = db_i.consider(fname, irp, irp_count) + print(outfmt.format(score, LP.RUNES[irp], len(stops), key_fn.__name__)) + testcase = data[:upto] + for x in reversed(skips): + testcase.pop(stops[x - 1]) key_score, key = key_fn(testcase).guess(kl, fn_similarity) if key_score > key_max_score: continue prio = (1 - key_score) * max(0, score) print(f' key_score: {prio:.4f}, {key}') - print(' skip:', skips) + print(f' skip: {skips}') txtname = f'{fname}_{prio:.4f}.{key_fn.__name__}.{irp}_{kl}' if INVERT: txtname += '.inv' @@ -53,26 +60,25 @@ def break_cipher(fname, candidates, solver, key_fn): with open(outfile, 'w') as f: f.write( f'{irp}, {kl}, {score:.4f}, {key_score:.4f}, {key}, {skips}\n') - slvr.output.file_output = outfile + io.file_output = outfile slvr.INTERRUPT = LP.RUNES[irp] slvr.INTERRUPT_POS = skips slvr.KEY_DATA = key - slvr.run() + io.run(slvr.run(inpt)[0]) def pattern_solver(fname, irp=0): - with open(LP.path.page(fname), 'r') as f: - orig = LP.RuneText(f.read()) + orig = LP.RuneTextFile(LP.path.page(fname)) # orig = LP.RuneText('ᛄᚹᚻᛗᛋᚪ-ᛋᛁᚫᛇ-ᛋᛠᚾᛞ-ᛇᛞ-ᛞᚾᚣᚹᛗ.ᛞᛈ-ᛝᛚᚳᚾᛗᚾᚣ-ᛖᛝᛖᚦᚣᚢ-ᚱᚻᛁᚠ-ᛟᛝ-ᛚᛖᚫᛋᛚᚳᛋᛇ.ᚣᚾᚻᛄᚾᚳᛡ-ᚷᚳᛝ-ᛈᛝ-ᛡᚷᚦᚷᛖ.ᚻᛠᚣ-ᛄᛞᚹᛒ-ᛇᛄᛝᚩᛟ.ᛗᛠᚣᛋᛖᛚᚠ-ᚾᚫᛁ-ᛄᚹᚻᚻᛚᛈᚹᚠ-ᚫᚩᛡᚫᛟ-ᚷᛠ-ᚪᛡᚠᛄᚱᛏᚢᛈ.ᛏᛈ-ᛇᛞ-ᛟᛗᛇᛒᛄᚳᛈ.ᛉᛟ-ᛒᚻᚱᛄᚣ-ᚾᚱ-ᚾᛡᛈᛈ-ᛚᛉᛗᛞ-ᛟᛝ-ᚷᛁᚱᚩᚹᛗ-ᚠᛇᚣ-ᚣᛝᛒ-ᛁ-ᚠᚾᚹᚢ-ᛠᚾᛈᚠᚻ.ᚫᛋᛄᚪᚻ-ᛒᛖᛋᚻᛠ-ᛄᛗ-ᛟᛡᚹᚪᛡ-ᛄᛋᛖᚢᛗ-ᛏᛖᛉᚪ-ᛞᛟᛉᚾᚠ-ᚱᛡᛒᛚᚩᛈᛝ-ᛋᛄᛚᛗ-ᛞᚱᛗᛗ-ᛒᛈ-ᛁᛉᚱᛄᛝ.ᛋᛇᚪ-ᛗᚠᚻᚣᚹᛉᛞ-ᛡᛁᚷᚪ-ᚩᚱ-ᚪᚾᚹᛇᛋᛞᛄᚷ-ᛡ-ᛖᚫᛄ-ᛞᛟᛁᚻᚹᛝ-ᛠᛈᛏ-ᚪᛗᛗᛚᛚᚪᛞ.ᛁᛠᛈᚷᛞ-ᛗᚣᛄᚳᚹᛚ-ᚻᛋᛟᛗ-ᚣᚫᛝᛚ-ᛠᛁᛝᛝᚪ-ᚳᛗ-ᚢᚫᛋ-ᛉᛠᚱ-ᛇᛡᛄᚻᛗᚾ-ᚻᛗᛝᛚ-ᛇᛞ-ᛟᚢᚣᚪᚷᚱ-ᛡᚷ-ᚷᛠ-ᛚᚻᛒ.ᛡᛒ-ᚩᛁᛄ-ᛗᛟᛉᚩᚣ-ᛞᚩ-ᚳᛗ-ᚾᛗᚩ-ᚷᛠ-ᛚᚱᚠᚷ-ᛁᚫᛗᛉ-ᛁᛠᚹᛚ-ᛖᛝᚾᛟᛗᚾ-ᛄᚾ-ᚾᚳᛚᛝ-ᛡ-ᚷᛞᛗᚱᚻᚩ-ᛗᛞᛠᚫᛞ-ᛞᚱᛗᛗ-ᚣᚪ-ᛗᛉᚢᛞᛇᚹ-ᛟᚱᛏᚱᛟᚢᛉᛗᛚᛈᛉᛝ.ᛏᛖ-ᛗᛋᚣ-ᚹᛁ-ᚹᛝ-ᛋᛇᛄᚳᛁᛋᛝ.ᛄᛚᚹ-ᚷᚠᛝ-ᚫᚷᛚᛡᛁᛡ.ᛖᚠᚣ-ᛉᛝᚻᛄᚾᛈᚠ-ᛉᚣ-ᛚᛄᛞᛝᛞᚪ-ᚩᛈ-ᚻᛟ-ᛖᚻᚱᚹ-ᛚᚷᚳ-ᛒᛈᛏᚻ-ᚠᛋᛠᚣᛋᚠ-ᛏᚷᛈᚪᛒ.') # orig = LP.RuneText('ᛇᚦ-ᛒᛏᚣᚳ-ᛇᛚᛉ-ᛄᛚᚦᚪ-ᛋᚱᛉᚦ-ᚦᛄᚻ-ᛉᛗ-ᛏᛞᛋ-ᚣᚾ-ᚣᛟᛇᛈᛟᛚ-ᛈᚹᛚᚪᛗ-ᚪᛉᛁᛇᛝᚢᚱᛉ.ᛞᛄ-ᚻᛠᚪᛚ.ᚠᛚ-ᚩᛋᚾ-ᚫᛞᛋᛁᛞᚹᚾᚪᚪ-ᚱᛟᚻ-ᛚᚠᛚᚳᛟᚱ-ᚣᛏ-ᚹᛏᛝᚣ-ᚳᚩ-ᛄᚷᛟ-ᛖ-ᚫᚻᚦᛠ-ᛒᛠᛁ-ᛁᚩᛡ-ᛗᛉᚠᚷᛁ-ᚣᚣᛋᛇᛗᛠᚹ.ᛇᚪ-ᛇᛉᛡ-ᛄᚾᛇᛁᛇᚫ-ᛋᚱᚹ-ᛝᚣᚦ-ᛠᛁᛄᛚᚢᛄ-ᚻᛇᛚᛟ-ᛒᛠᛒᛚ-ᚩᛈᛈ-ᚢᚻᛚ-ᛡᚾᛚ-ᛒᚦᚱᚠᚦᚫ-ᛞᚳ-ᛄᚳᚷ-ᚹᚫ-ᚱᛉᚣᛖᚱ.ᛒᛝᚹ-ᛟᚳᚫᚹᛈᚢ-ᚱᛋᛒ-ᚷᚦᚳᛏᛏᛠᚹ-ᚱᚣᛞ-ᚣᛠᛄ-ᛋ-qᚪᛚᚾᛖᛄᚪ-ᛇᚻᛖ-ᛏᛠᛈ-ᛝᛉᚾᚳ-ᛋᚾᚹᚦᚾ-ᚣᛞᛝᚣ-ᛠᛠᛡ-ᛉᛁᛚᚢᚩ.ᛗᛉᚦ-ᛒᛝᛇᛠᛟ-ᛁᛟᛏ-ᛠᛏᛄ-ᚫᚳᛉᛝᛖᚠ-ᛇᚠ.ᛄᛄᛝᛟᛡᛟ-ᛠᛖᚫ-ᚦᛏᛠᛗ-ᛁᛏᚩᛒᛡ-ᛝᛟ-ᛉᚠᛇᚷᛗᛠ-ᚠᛖ-ᚳᛖᛖᚾᛠᛁᚪᛟ-ᛉᚣ-ᚢᛁ.ᛒᛏ.ᛒᛠ-ᛠᛁᚢᛗ-ᛞᛟᛋᛠᚷᚠᛇᚫ-ᛏᚪ-ᛇᚦ-ᛒᚪᛟᚩᛗ.ᛟᚳᛇ-ᛞᛞ-ᛋᚱᛁᛋᚦ-ᛇᛒ-ᚳᛒᛟ-ᚳᛟᚳᚷᛇ.ᛗᛉᚦ-ᛞᚦᛉᛈᛚᛈᛚᛁᚢ-ᚳᛞᛡᛝᚻᚷ-ᛞᚪ-ᚳᛟᚳᛁᛟᛞ-') - data = orig.index + data = orig.index_no_newline if False: # longest uninterrupted text - pos, lg = LP.InterruptDB.longest_no_interrupt(data, interrupt=0, irpmax=0) + pos, lg = LP.longest_no_interrupt(data, interrupt=0, irpmax=0) data = data[pos:pos + lg] else: # from the beginning - data = data[:170] + data = data[:970] - data_i = [i for i, x in enumerate(data) if x == 29] + whitespace_i = [i for i, x in enumerate(data) if x == 29] data = [x for x in data if x != 29] def fn_similarity(x): @@ -84,22 +90,20 @@ def pattern_solver(fname, irp=0): # yield from x[::-1] yield from x[::-1][1:-1] + prnt_fmt = 'kl: {}, pattern-n: {}, IoC: {:.3f}, dist: {:.4f}, offset: {}, key: {}' print(fname) gr = LP.GuessPattern(data) for kl in range(3, 19): - # for pattern_shift in range(1): - # fn_pattern = fn_pattern_mirror for pattern_shift in range(1, kl): def fn_pattern_shift(x, kl): # shift by (more than) one, 012201120 for i in range(10000): yield from x[(i * pattern_shift) % kl:] yield from x[:(i * pattern_shift) % kl] - fn_pattern = fn_pattern_shift # Find proper pattern res = [] for offset in range(kl): # up to keylen offset - mask = LP.GuessPattern.pattern(kl, fn_pattern) + mask = LP.GuessPattern.pattern(kl, fn_pattern_shift) parts = gr.split(kl, mask, offset) score = sum(LP.Probability(x).IC() for x in parts) / kl if score > 1.6 and score < 2.1: @@ -107,13 +111,12 @@ def pattern_solver(fname, irp=0): # Find best matching key for pattern for score, parts, off in res: - sc, solution = LP.GuessPattern.guess(parts, fn_similarity) + sc, key = LP.GuessPattern.guess(parts, fn_similarity) if sc < 0.1: - fmt = 'kl: {}, pattern-n: {}, IoC: {:.3f}, dist: {:.4f}, offset: {}, key: {}' - print(fmt.format(kl, pattern_shift, score, sc, off, - LP.RuneText(solution).text)) - solved = gr.zip(fn_pattern(solution, kl), off) - for i in data_i: + print(prnt_fmt.format(kl, pattern_shift, score, sc, off, + LP.RuneText(key).text)) + solved = gr.zip(fn_pattern_shift(key, kl), off) + for i in whitespace_i: solved.insert(i, 29) print(' ', LP.RuneText(solved).text) @@ -121,10 +124,6 @@ def pattern_solver(fname, irp=0): ######################################### # main ######################################### -db = LP.InterruptDB.load('db_norm') -# IOC_MIN_SCORE = 1.4 # for db_high -IOC_MIN_SCORE = 0.55 # for db_norm - for fname in [ 'p0-2', # ??? 'p3-7', # ??? @@ -153,8 +152,8 @@ for fname in [ print(fname, 'not in db.') continue print() - print(f'loading file: pages/{fname}.txt') - candidates = [x for x in db[fname] if x[1] >= IOC_MIN_SCORE] + print(f'loading: {fname}') + candidates = [x for x in db[fname] if x[1] >= IOC_MIN_SCORE and x[2] == 0] if not candidates: maxscore = max(x[1] for x in db[fname]) print('No candidates. Highest score is only', maxscore) diff --git a/solver.py b/solver.py index d5a3953..dfc1039 100755 --- a/solver.py +++ b/solver.py @@ -18,41 +18,40 @@ MOEBIUS = load_sequence_file('seq_moebius') def print_all_solved(): - def plain(slvr): - slvr.KEY_DATA = [] + def plain(slvr, inpt): + pass - def invert(slvr): - slvr.KEY_DATA = [] - slvr.KEY_INVERT = True + def invert(slvr, inpt): + inpt.invert() - def solution_welcome(slvr): + def solution_welcome(slvr, inpt): slvr.KEY_DATA = [23, 10, 1, 10, 9, 10, 16, 26] # DIVINITY slvr.INTERRUPT = 'ᚠ' slvr.INTERRUPT_POS = [4, 5, 6, 7, 10, 11, 14, 18, 20, 21, 25] - def solution_koan_1(slvr): + def solution_koan_1(slvr, inpt): slvr.KEY_DATA = [26] # Y - slvr.KEY_INVERT = True + inpt.invert() - def solution_jpg107_167(slvr): # FIRFUMFERENFE + def solution_jpg107_167(slvr, inpt): # FIRFUMFERENFE slvr.KEY_DATA = [0, 10, 4, 0, 1, 19, 0, 18, 4, 18, 9, 0, 18] slvr.INTERRUPT = 'ᚠ' slvr.INTERRUPT_POS = [2, 3] - def solution_p56_end(slvr): + def solution_p56_end(slvr, inpt): slvr.FN = lambda i, r: r - (PRIMES[i] - 1) slvr.INTERRUPT = 'ᚠ' slvr.INTERRUPT_POS = [4] def solve(fname, fn_solution, solver=LP.VigenereSolver): slvr = solver() - slvr.output.COLORS = False - slvr.output.QUIET = True # or use -v/-q while calling - slvr.input.load(file=LP.path.page(fname)) - fn_solution(slvr) + inpt = LP.RuneTextFile(LP.path.page(fname)) + fn_solution(slvr, inpt) print(f'pages/{fname}.txt') print() - slvr.run() + io = LP.IOWriter() + # io.QUIET = True # or use -v/-q while calling + io.run(slvr.run(inpt)[0]) print() solve('0_warning', invert) @@ -67,36 +66,29 @@ def print_all_solved(): def play_around(): - slvr = LP.VigenereSolver() - slvr.output.COLORS = False - slvr.output.QUIET = True - slvr.KEY_DATA = [] - vowels = 'ᚢᚩᛁᛇᛖᛟᚪᚫᛡᛠ' + vowels = [LP.RUNES.index(x) for x in 'ᚢᚩᛁᛇᛖᛟᚪᚫᛡᛠ'] for uuu in LP.FILES_UNSOLVED: - slvr.input.load(file=LP.path.page(uuu)) + inpt = LP.RuneTextFile(LP.path.page(uuu)) print(uuu) - print('word count:', sum(len(x) for x in slvr.input.words.values())) - a = [1 if x.rune in vowels else 0 for x in slvr.input.runes_no_whitespace()] + print('word count:', sum(1 for _ in inpt.enum_words())) + a = [1 if x in vowels else 0 for x in inpt.index_no_white] b = [a[i:i + 5] for i in range(0, len(a), 5)] c = [int(''.join(str(y) for y in x), 2) for x in b] # print('-'.join(str(x) for x in c)) # print(LP.RuneText(c).text) # print(''.join('ABCDEFGHIJKLMNOPQRSTUVWXYZ___...'[x] for x in c)) - # slvr.run() def try_totient_on_unsolved(): slvr = LP.SequenceSolver() - slvr.output.QUIET = True - slvr.output.BREAK_MODE = '' # disable line breaks # slvr.INTERRUPT = 'ᛝ' # slvr.INTERRUPT_POS = [1] # for uuu in ['15-22']: for uuu in LP.FILES_UNSOLVED: print() print(uuu) - slvr.input.load(file=LP.path.page(uuu), limit=25) - # alldata = slvr.input.runes_no_whitespace() + [LP.Rune(i=29)] + inpt = LP.RuneTextFile(LP.path.page(uuu), limit=25).data_clean + # alldata = [x for x in inpt if x.index != 29] + [LP.Rune(i=29)] * 1 def ec(r, i): p1, p2 = LP.utils.elliptic_curve(i, 149, 263, 3299) @@ -110,7 +102,7 @@ def try_totient_on_unsolved(): # slvr.FN = lambda i, r: LP.Rune(i=(r.prime - PRIMES[FIBONACCI[i]] + z) % 29) # slvr.FN = lambda i, r: LP.Rune(i=(r.prime ** i + z) % 29) slvr.FN = lambda i, r: LP.Rune(i=(ec(r, i) + z) % 29) - slvr.run() + print(slvr.run(inpt)[0].text) def find_oeis(irp=0, invert=False, offset=0, allow_fails=1, min_match=2): @@ -158,12 +150,11 @@ def find_oeis(irp=0, invert=False, offset=0, allow_fails=1, min_match=2): splits = splits[1:] print() print(uuu) - with open(LP.path.page(uuu), 'r') as f: - data = LP.RuneText(f.read()[:120]).index_rune_only - irps = [i for i, x in enumerate(data[:splits[-1][1]]) if x == irp] - irps.reverse() # insert -1 starting with the last - if invert: - data = [28 - x for x in data] + data = LP.RuneTextFile(LP.path.page(uuu), limit=120).index_no_white + if invert: + data = [28 - x for x in data] + irps = [i for i, x in enumerate(data[:splits[-1][1]]) if x == irp] + irps.reverse() # insert -1 starting with the last min_len = sum(wlen[:2]) # must match at least n words data_len = len(data)