diff --git a/copycat/copycat.py b/copycat/copycat.py index cc1218b..aa96d47 100644 --- a/copycat/copycat.py +++ b/copycat/copycat.py @@ -22,6 +22,7 @@ class Reporter(object): def report_workspace(self, workspace): pass + class Copycat(object): def __init__(self, rng_seed=None, reporter=None): self.coderack = Coderack(self) diff --git a/copycat/curses_main.py b/copycat/curses_main.py index 19bb441..d1efb07 100644 --- a/copycat/curses_main.py +++ b/copycat/curses_main.py @@ -11,6 +11,7 @@ if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--seed', type=int, default=None, help='Provide a deterministic seed for the RNG.') + parser.add_argument('--focus-on-slipnet', action='store_true', help='Show the slipnet and coderack, instead of the workspace.') parser.add_argument('initial', type=str, help='A...') parser.add_argument('modified', type=str, help='...is to B...') parser.add_argument('target', type=str, help='...as C is to... what?') @@ -18,7 +19,13 @@ if __name__ == '__main__': try: window = curses.initscr() - copycat = Copycat(reporter=CursesReporter(window), rng_seed=options.seed) + copycat = Copycat( + reporter=CursesReporter( + window, + focus_on_slipnet=options.focus_on_slipnet, + ), + rng_seed=options.seed, + ) copycat.run_forever(options.initial, options.modified, options.target) except KeyboardInterrupt: pass diff --git a/copycat/curses_reporter.py b/copycat/curses_reporter.py index cfed1cd..3b2f998 100644 --- a/copycat/curses_reporter.py +++ b/copycat/curses_reporter.py @@ -1,24 +1,90 @@ import curses +import time + from copycat import Reporter +from bond import Bond +from correspondence import Correspondence +from description import Description +from group import Group +from letter import Letter +from rule import Rule + + +class SafeSubwindow(object): + def __init__(self, window, h, w, y, x): + self.w = window.derwin(h, w, y, x) + + def addnstr(self, y, x, s, n): + self.w.addnstr(y, x, s, n) + + def addstr(self, y, x, s, attr=curses.A_NORMAL): + try: + self.w.addstr(y, x, s, attr) + except Exception as e: + if str(e) != 'addstr() returned ERR': + raise + + def border(self): + self.w.border() + + def erase(self): + self.w.erase() + + def getch(self): + self.w.nodelay(True) # make getch() non-blocking + return self.w.getch() + + def getmaxyx(self): + return self.w.getmaxyx() + + def is_vacant(self, y, x): + ch_plus_attr = self.w.inch(y, x) + if ch_plus_attr == -1: + return True # it's out of bounds + return (ch_plus_attr & 0xFF) == 0x20 + + def refresh(self): + self.w.refresh() class CursesReporter(Reporter): - def __init__(self, window): + def __init__(self, window, focus_on_slipnet=False): curses.curs_set(0) # hide the cursor + curses.noecho() # hide keypresses height, width = window.getmaxyx() - slipnetHeight = 10 - coderackHeight = height - 15 + if focus_on_slipnet: + upperHeight = 10 + else: + upperHeight = 25 answersHeight = 5 - self.temperatureWindow = window.derwin(height, 5, 0, 0) - self.slipnetWindow = curses.newwin(slipnetHeight, width-5, 0, 5) - self.coderackWindow = curses.newwin(coderackHeight, width-5, slipnetHeight, 5) - self.answersWindow = curses.newwin(answersHeight, width-5, slipnetHeight + coderackHeight, 5) - for w in [self.temperatureWindow, self.slipnetWindow, self.answersWindow]: - w.clear() + coderackHeight = height - upperHeight - answersHeight + self.focusOnSlipnet = focus_on_slipnet + self.temperatureWindow = SafeSubwindow(window, height, 5, 0, 0) + self.upperWindow = SafeSubwindow(window, upperHeight, width-5, 0, 5) + self.coderackWindow = SafeSubwindow(window, coderackHeight, width-5, upperHeight, 5) + self.answersWindow = SafeSubwindow(window, answersHeight, width-5, upperHeight + coderackHeight, 5) + for w in [self.temperatureWindow, self.upperWindow, self.answersWindow]: + w.erase() w.border() w.refresh() self.answers = {} + def do_keyboard_shortcuts(self): + w = self.temperatureWindow # just a random window + ordch = w.getch() + if ordch in [ord('P'), ord('p')]: + w.addstr(0, 0, 'PAUSE', curses.A_STANDOUT) + w.refresh() + ordch = None + while ordch not in [ord('P'), ord('p'), 27, ord('Q'), ord('q')]: + time.sleep(0.1) + ordch = w.getch() + w.erase() + w.border() + w.refresh() + if ordch in [27, ord('Q'), ord('q')]: + raise KeyboardInterrupt() + def report_answer(self, answer): d = self.answers.setdefault(answer['answer'], { 'answer': answer['answer'], @@ -84,22 +150,29 @@ class CursesReporter(Reporter): pageHeight, pageWidth = w.getmaxyx() columnWidth = (pageWidth - len('important-object-correspondence-scout (n)')) / (NUMBER_OF_BINS - 1) - def is_vacant(y, x): - return (w.inch(y, x) & 0xFF) == 0x20 - w.erase() for u, string in printable_entries: # Find the highest point on the page where we could place this entry. start_column = (u - 1) * columnWidth end_column = start_column + len(string) for r in range(pageHeight): - if all(is_vacant(r, c) for c in xrange(start_column, end_column+20)): + if all(w.is_vacant(r, c) for c in xrange(start_column, end_column+20)): w.addstr(r, start_column, string) break w.refresh() + def slipnode_name_and_attr(self, slipnode): + if slipnode.activation == 100: + return (slipnode.name.upper(), curses.A_STANDOUT) + if slipnode.activation > 50: + return (slipnode.name.upper(), curses.A_BOLD) + else: + return (slipnode.name.lower(), curses.A_NORMAL) + def report_slipnet(self, slipnet): - w = self.slipnetWindow + if not self.focusOnSlipnet: + return + w = self.upperWindow pageHeight, pageWidth = w.getmaxyx() w.erase() w.addstr(1, 2, 'Total: %d slipnodes and %d sliplinks' % ( @@ -107,25 +180,17 @@ class CursesReporter(Reporter): len(slipnet.sliplinks), )) - def name_and_attr(node): - if node.activation == 100: - return (node.name.upper(), curses.A_STANDOUT) - if node.activation > 50: - return (node.name.upper(), curses.A_BOLD) - else: - return (node.name.lower(), curses.A_NORMAL) - for c, node in enumerate(slipnet.letters): - s, attr = name_and_attr(node) + s, attr = self.slipnode_name_and_attr(node) w.addstr(2, 2 * c + 2, s, attr) for c, node in enumerate(slipnet.numbers): - s, attr = name_and_attr(node) + s, attr = self.slipnode_name_and_attr(node) w.addstr(3, 2 * c + 2, s, attr) row = 4 column = 2 for node in slipnet.slipnodes: if node not in slipnet.letters + slipnet.numbers: - s, attr = name_and_attr(node) + s, attr = self.slipnode_name_and_attr(node) if column + len(s) > pageWidth - 1: row += 1 column = 2 @@ -135,11 +200,195 @@ class CursesReporter(Reporter): w.refresh() def report_temperature(self, temperature): - height = self.temperatureWindow.getmaxyx()[0] + self.do_keyboard_shortcuts() + w = self.temperatureWindow + height = w.getmaxyx()[0] max_mercury = height - 4 mercury = max_mercury * temperature.value() / 100.0 for i in range(max_mercury): ch = ' ,o%8'[int(4 * min(max(0, mercury - i), 1))] - self.temperatureWindow.addstr(max_mercury - i, 1, 3*ch) - self.temperatureWindow.addnstr(height - 2, 1, '%3d' % temperature.actual_value, 3) - self.temperatureWindow.refresh() + w.addstr(max_mercury - i, 1, 3*ch) + w.addnstr(height - 2, 1, '%3d' % temperature.actual_value, 3) + w.refresh() + + def length_of_workspace_object_depiction(self, o, description_structures): + result = len(str(o)) + if o.descriptions: + result += 2 + result += 2 * (len(o.descriptions) - 1) + for d in o.descriptions: + s, _ = self.slipnode_name_and_attr(d.descriptor) + result += len(s) + if d not in description_structures: + result += 2 + result += 1 + return result + + def depict_workspace_object(self, w, row, column, o, maxImportance, description_structures): + if maxImportance != 0.0 and o.relativeImportance == maxImportance: + attr = curses.A_BOLD + else: + attr = curses.A_NORMAL + w.addstr(row, column, str(o), attr) + column += len(str(o)) + if o.descriptions: + w.addstr(row, column, ' (', curses.A_NORMAL) + column += 2 + for i, d in enumerate(o.descriptions): + if i != 0: + w.addstr(row, column, ', ', curses.A_NORMAL) + column += 2 + s, attr = self.slipnode_name_and_attr(d.descriptor) + if d not in description_structures: + s = '[%s]' % s + w.addstr(row, column, s, attr) + column += len(s) + w.addstr(row, column, ')', curses.A_NORMAL) + column += 1 + return column + + def depict_bond(self, w, row, column, bond): + slipnet = bond.ctx.slipnet + if bond.directionCategory == slipnet.right: + s = '-- %s -->' % bond.category.name + elif bond.directionCategory == slipnet.left: + s = '<-- %s --' % bond.category.name + elif bond.directionCategory is None: + s = '<-- %s -->' % bond.category.name + if isinstance(bond.leftObject, Group): + s = 'G' + s + if isinstance(bond.rightObject, Group): + s = s + 'G' + w.addstr(row, column, s, curses.A_NORMAL) + return column + len(s) + + def depict_grouping_brace(self, w, firstrow, lastrow, column): + if firstrow == lastrow: + w.addstr(firstrow, column, '}', curses.A_NORMAL) + else: + w.addstr(firstrow, column, '\\', curses.A_NORMAL) + w.addstr(lastrow, column, '/', curses.A_NORMAL) + for r in xrange(firstrow + 1, lastrow): + w.addstr(r, column, '|', curses.A_NORMAL) + + def report_workspace(self, workspace): + if self.focusOnSlipnet: + return + slipnet = workspace.ctx.slipnet + w = self.upperWindow + pageHeight, pageWidth = w.getmaxyx() + w.erase() + w.addstr(1, 2, '%d objects (%d letters in %d groups), %d other structures (%d bonds, %d correspondences, %d descriptions, %d rules)' % ( + len(workspace.objects), + len([o for o in workspace.objects if isinstance(o, Letter)]), + len([o for o in workspace.objects if isinstance(o, Group)]), + len(workspace.structures) - len([o for o in workspace.objects if isinstance(o, Group)]), + len([o for o in workspace.structures if isinstance(o, Bond)]), + len([o for o in workspace.structures if isinstance(o, Correspondence)]), + len([o for o in workspace.structures if isinstance(o, Description)]), + len([o for o in workspace.structures if isinstance(o, Rule)]), + )) + + group_objects = {o for o in workspace.objects if isinstance(o, Group)} + letter_objects = {o for o in workspace.objects if isinstance(o, Letter)} + group_and_letter_objects = group_objects | letter_objects + assert set(workspace.objects) == group_and_letter_objects + assert group_objects <= set(workspace.structures) + + latent_groups = {o.group for o in workspace.objects if o.group is not None} + assert latent_groups <= group_objects + assert group_objects <= latent_groups + member_groups = {o for g in group_objects for o in g.objectList if isinstance(o, Group)} + assert member_groups <= group_objects + + bond_structures = {o for o in workspace.structures if isinstance(o, Bond)} + known_bonds = {o.leftBond for o in group_and_letter_objects if o.leftBond is not None} + known_bonds |= {o.rightBond for o in group_and_letter_objects if o.rightBond is not None} + assert known_bonds == bond_structures + + description_structures = {o for o in workspace.structures if isinstance(o, Description)} + latent_descriptions = {d for o in group_and_letter_objects for d in o.descriptions} + assert description_structures <= latent_descriptions + + current_rules = set([workspace.rule]) if workspace.rule is not None else set() + + correspondences_between_initial_and_target = {o for o in workspace.structures if isinstance(o, Correspondence)} + + assert set(workspace.structures) == set.union( + group_objects, + bond_structures, + description_structures, + current_rules, + correspondences_between_initial_and_target, + ) + for g in group_objects: + assert g.string in [workspace.initial, workspace.modified, workspace.target] + + row = 2 + for o in current_rules: + w.addstr(row, 2, str(o), curses.A_BOLD) + + for string in [workspace.initial, workspace.modified, workspace.target]: + row += 1 + maxImportance = max(o.relativeImportance for o in group_and_letter_objects if o.string == string) + letters_in_string = sorted( + (o for o in letter_objects if o.string == string), + key=lambda o: o.leftIndex, + ) + groups_in_string = sorted( + (o for o in group_objects if o.string == string), + key=lambda o: o.leftIndex, + ) + bonds_in_string = sorted( + (b for b in bond_structures if b.string == string), + key=lambda b: b.leftObject.rightIndex, + ) + assert bonds_in_string == sorted(string.bonds, key=lambda b: b.leftObject.rightIndex) + startrow_for_group = {} + endrow_for_group = {} + + max_column = 0 + for letter in letters_in_string: + for g in groups_in_string: + if g.leftIndex == letter.leftIndex: + startrow_for_group[g] = row + if g.rightIndex == letter.rightIndex: + endrow_for_group[g] = row + column = self.depict_workspace_object(w, row, 2, letter, maxImportance, description_structures) + row += 1 + max_column = max(max_column, column) + for b in bonds_in_string: + if b.leftObject.rightIndex == letter.rightIndex: + assert b.rightObject.leftIndex == letter.rightIndex + 1 + column = self.depict_bond(w, row, 4, b) + row += 1 + max_column = max(max_column, column) + + for group in groups_in_string: + start = startrow_for_group[group] + end = endrow_for_group[group] + # Place this group's graphical depiction. + depiction_width = 3 + self.length_of_workspace_object_depiction(group, description_structures) + for firstcolumn in xrange(max_column, 1000): + lastcolumn = firstcolumn + depiction_width + okay = all( + w.is_vacant(r, c) + for c in xrange(firstcolumn, lastcolumn + 1) + for r in xrange(start, end + 1) + ) + if okay: + self.depict_grouping_brace(w, start, end, firstcolumn + 1) + self.depict_workspace_object(w, (start + end) / 2, firstcolumn + 3, group, maxImportance, description_structures) + break + + row += 1 + column = 2 + + for o in correspondences_between_initial_and_target: + slipnet = workspace.ctx.slipnet + w.addstr(row, column, '%s (%s)' % (str(o), str([m for m in o.conceptMappings if m.label != slipnet.identity])), curses.A_NORMAL) + row += 1 + column = 2 + + w.border() + w.refresh()