Massively improve CursesReporter.

The Slipnet itself turns out to be boring to look at.
More interest is found in the Workspace structures, such as bonds,
groups, and correspondences.

The old behavior of `curses_main.py` is still accessible via

    python curses_main.py abc abd xyz --focus-on-slipnet
This commit is contained in:
Arthur O'Dwyer
2017-05-02 17:56:14 -07:00
parent ef4a9c56c5
commit 0eec6a5259
3 changed files with 287 additions and 30 deletions

View File

@ -22,6 +22,7 @@ class Reporter(object):
def report_workspace(self, workspace): def report_workspace(self, workspace):
pass pass
class Copycat(object): class Copycat(object):
def __init__(self, rng_seed=None, reporter=None): def __init__(self, rng_seed=None, reporter=None):
self.coderack = Coderack(self) self.coderack = Coderack(self)

View File

@ -11,6 +11,7 @@ if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--seed', type=int, default=None, help='Provide a deterministic seed for the RNG.') 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('initial', type=str, help='A...')
parser.add_argument('modified', type=str, help='...is to B...') parser.add_argument('modified', type=str, help='...is to B...')
parser.add_argument('target', type=str, help='...as C is to... what?') parser.add_argument('target', type=str, help='...as C is to... what?')
@ -18,7 +19,13 @@ if __name__ == '__main__':
try: try:
window = curses.initscr() 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) copycat.run_forever(options.initial, options.modified, options.target)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass

View File

@ -1,24 +1,90 @@
import curses import curses
import time
from copycat import Reporter 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): class CursesReporter(Reporter):
def __init__(self, window): def __init__(self, window, focus_on_slipnet=False):
curses.curs_set(0) # hide the cursor curses.curs_set(0) # hide the cursor
curses.noecho() # hide keypresses
height, width = window.getmaxyx() height, width = window.getmaxyx()
slipnetHeight = 10 if focus_on_slipnet:
coderackHeight = height - 15 upperHeight = 10
else:
upperHeight = 25
answersHeight = 5 answersHeight = 5
self.temperatureWindow = window.derwin(height, 5, 0, 0) coderackHeight = height - upperHeight - answersHeight
self.slipnetWindow = curses.newwin(slipnetHeight, width-5, 0, 5) self.focusOnSlipnet = focus_on_slipnet
self.coderackWindow = curses.newwin(coderackHeight, width-5, slipnetHeight, 5) self.temperatureWindow = SafeSubwindow(window, height, 5, 0, 0)
self.answersWindow = curses.newwin(answersHeight, width-5, slipnetHeight + coderackHeight, 5) self.upperWindow = SafeSubwindow(window, upperHeight, width-5, 0, 5)
for w in [self.temperatureWindow, self.slipnetWindow, self.answersWindow]: self.coderackWindow = SafeSubwindow(window, coderackHeight, width-5, upperHeight, 5)
w.clear() self.answersWindow = SafeSubwindow(window, answersHeight, width-5, upperHeight + coderackHeight, 5)
for w in [self.temperatureWindow, self.upperWindow, self.answersWindow]:
w.erase()
w.border() w.border()
w.refresh() w.refresh()
self.answers = {} 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): def report_answer(self, answer):
d = self.answers.setdefault(answer['answer'], { d = self.answers.setdefault(answer['answer'], {
'answer': answer['answer'], 'answer': answer['answer'],
@ -84,22 +150,29 @@ class CursesReporter(Reporter):
pageHeight, pageWidth = w.getmaxyx() pageHeight, pageWidth = w.getmaxyx()
columnWidth = (pageWidth - len('important-object-correspondence-scout (n)')) / (NUMBER_OF_BINS - 1) 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() w.erase()
for u, string in printable_entries: for u, string in printable_entries:
# Find the highest point on the page where we could place this entry. # Find the highest point on the page where we could place this entry.
start_column = (u - 1) * columnWidth start_column = (u - 1) * columnWidth
end_column = start_column + len(string) end_column = start_column + len(string)
for r in range(pageHeight): 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) w.addstr(r, start_column, string)
break break
w.refresh() 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): def report_slipnet(self, slipnet):
w = self.slipnetWindow if not self.focusOnSlipnet:
return
w = self.upperWindow
pageHeight, pageWidth = w.getmaxyx() pageHeight, pageWidth = w.getmaxyx()
w.erase() w.erase()
w.addstr(1, 2, 'Total: %d slipnodes and %d sliplinks' % ( w.addstr(1, 2, 'Total: %d slipnodes and %d sliplinks' % (
@ -107,25 +180,17 @@ class CursesReporter(Reporter):
len(slipnet.sliplinks), 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): 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) w.addstr(2, 2 * c + 2, s, attr)
for c, node in enumerate(slipnet.numbers): 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) w.addstr(3, 2 * c + 2, s, attr)
row = 4 row = 4
column = 2 column = 2
for node in slipnet.slipnodes: for node in slipnet.slipnodes:
if node not in slipnet.letters + slipnet.numbers: 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: if column + len(s) > pageWidth - 1:
row += 1 row += 1
column = 2 column = 2
@ -135,11 +200,195 @@ class CursesReporter(Reporter):
w.refresh() w.refresh()
def report_temperature(self, temperature): 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 max_mercury = height - 4
mercury = max_mercury * temperature.value() / 100.0 mercury = max_mercury * temperature.value() / 100.0
for i in range(max_mercury): for i in range(max_mercury):
ch = ' ,o%8'[int(4 * min(max(0, mercury - i), 1))] ch = ' ,o%8'[int(4 * min(max(0, mercury - i), 1))]
self.temperatureWindow.addstr(max_mercury - i, 1, 3*ch) w.addstr(max_mercury - i, 1, 3*ch)
self.temperatureWindow.addnstr(height - 2, 1, '%3d' % temperature.actual_value, 3) w.addnstr(height - 2, 1, '%3d' % temperature.actual_value, 3)
self.temperatureWindow.refresh() 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()