--- /dev/null
+from enum import Enum
+
+class Zoombini:
+ class Property(Enum):
+ pass # marker class
+
+ class Hair(Property):
+ PINK = 'pink'; GREEN = 'green'; TUFT = 'tuft'; SPIKY = 'spiky'; FLAT = 'flat'
+ class Eyes(Property):
+ ONE = 'one'; WIDE = 'wide'; SLEEPY = 'sleepy'; SHADES = 'shades'; GLASSES = 'glasses'
+ class Nose(Property):
+ RED = 'red'; ORANGE = 'orange'; GREEN = 'green'; BLUE = 'blue'; PURPLE = 'purple'
+ class Feet(Property):
+ SHOES = 'shoes'; PROPELLER = 'propeller'; SPRING = 'spring'; WHEELS = 'wheels'; SKATES = 'skates'
+
+ def __init__(self, attrs):
+ assert (
+ all(isinstance(a, Zoombini.Property) for a in attrs)
+ and len({type(a) for a in attrs}) == 4
+ )
+ self.attrs = {type(attr): attr for attr in attrs}
+
+ def has(self, attr):
+ return self.attrs[type(attr)] == attr
+
+ def __str__(self):
+ return str({type(a).__name__: a.value for a in self.attrs.values()})
+
+def load(path):
+ import pickle
+ with path.open() as f:
+ return pickle.load(f)
--- /dev/null
+import sys
+if '../../..' not in sys.path:
+ sys.path.append('../../..')
+
+import zoombinis.common as common
+from zoombinis.puzzles.allergic_cliffs.common import *
+
+import pathlib
+import random
+
+try:
+ [file_name] = sys.argv[1:]
+ troupe = common.load(pathlib.Path(file_name))
+except:
+ print("args: <troupe-file>", file = sys.stderr)
+ sys.exit(1)
+
+cand_rules = set(ALL_RULES)
+waiting = troupe
+across = {Cliff.UPPER: set(), Cliff.LOWER: set()}
+
+def matching_rules(zoombini, rules):
+ return sum(zoombini.has(r.attr) for r in rules)
+
+def send(z, cliff):
+ print("Send {} across the {} cliff.".format(z, cliff.name.lower()))
+ ans = input("Did it work? (y/n) ")
+ if ans == 'n':
+ cliff = cliff.other()
+ print("Send {} across the {} cliff.".format(z, cliff.name.lower()))
+ input("Ready? (Enter) ")
+ waiting.remove(z)
+ across[cliff].add(z)
+
+def by_cliff(cliff):
+ return frozenset(c for c in cand_rules)
+
+while waiting and len(upper_cands) >= 3 and len(lower_cands) >= 3:
+ upper_rules = {r for r in rules if r.cliff == Cliff.UPPER}
+ lower_rules = {r for r in rules if r.cliff == Cliff.LOWER}
+ up = max(waiting, key = lambda z: matching_rules(z, upper_rules) * matching_rules(z, lower_rules))
+ if not across[Cliff.UPPER]:
+ cliff = Cliff.UPPER
+ elif not across[Cliff.LOWER]:
+ cliff = Cliff.LOWER
+ else:
+ score = lambda cliff: sum(len(up.attrs & z.attrs) for z in across[cliff]) / len(across[cliff])
+ cliff = max(Cliff, key = score)
+ send(up, cliff)
+
+if len(upper_cands) < 3:
+ pos_attrs = {attr for (cliff, attr) in lower_cands}
+ pos_cliff = Cliff.LOWER
+else:
+ pos_attrs = {attr for (cliff, attr) in upper_cands}
+ pos_cliff = Cliff.UPPER
+
+while waiting:
+ up = waiting.pop()
+ cliff = pos_cliff if any(up.has(attr) for attr in pos_attrs) else pos_cliff.other()
+ send(up, cliff)
--- /dev/null
+import sys
+sys.path.append('../../..')
+import zoombinis.common as comm
+
+from enum import Enum
+
+Cliff = Enum('Cliff', ['UPPER', 'LOWER'])
+Cliff.other = lambda self: {Cliff.UPPER: Cliff.LOWER, Cliff.LOWER: Cliff.UPPER}[self]
+
+ALL_RULES = frozenset(
+ (cliff, attr)
+ for cliff in Cliff
+ for prop in comm.Zoombini.Property
+ for attr in prop
+)
--- /dev/null
+import pickle
+import sys
+
+from common import Zoombini
+Z = Zoombini
+
+def get_options(type_):
+ return '/'.join(variant.value for variant in type_)
+
+zs = []
+try:
+ while True:
+ template = "\tchoose {} ({}): "
+ print(template.format('hair', get_options(Z.Hair)), file = sys.stderr, end = '')
+ hair = input()
+ print(template.format('eyes', get_options(Z.Eyes)), file = sys.stderr, end = '')
+ eyes = input()
+ print(template.format('nose', get_options(Z.Nose)), file = sys.stderr, end = '')
+ nose = input()
+ print(template.format('feet', get_options(Z.Feet)), file = sys.stderr, end = '')
+ feet = input()
+
+ zs.append(
+ Zoombini({
+ Z.Hair[hair.upper()],
+ Z.Eyes[eyes.upper()],
+ Z.Nose[nose.upper()],
+ Z.Feet[feet.upper()],
+ })
+ )
+ print("added", file = sys.stderr)
+except EOFError:
+ pass
+
+pickle.dump(zs, sys.stdout.buffer)
+print()