Various
authorJakob Cornell <jakob@jcornell.net>
Sat, 22 Jun 2019 02:32:04 +0000 (21:32 -0500)
committerJakob Cornell <jakob@jcornell.net>
Sat, 22 Jun 2019 02:32:04 +0000 (21:32 -0500)
common.py
puzzles/allergic_cliffs/4.py
ui.py [new file with mode: 0644]

index 13f8afbba8fe2165a8a1cc79c7e5ebbad3ab65a2..ce0cf44a91fcdb4515abe3ebabaee4e50b55d2dd 100644 (file)
--- a/common.py
+++ b/common.py
@@ -13,20 +13,31 @@ class Zoombini:
        class Feet(Property):
                SHOES = 'shoes'; PROPELLER = 'propeller'; SPRING = 'spring'; WHEELS = 'wheels'; SKATES = 'skates'
 
+       PROPERTIES = frozenset([Hair, Eyes, Nose, Feet])
+
        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
+               self.by_prop = {type(attr): attr for attr in attrs}
+               self.attrs = frozenset(attrs)
 
        def __str__(self):
                return str({type(a).__name__: a.value for a in self.attrs.values()})
 
+ALL_ATTRS = frozenset(attr for prop in Zoombini.PROPERTIES for attr in prop)
+
 def load(path):
        import pickle
-       with path.open() as f:
+       with path.open('rb') as f:
                return pickle.load(f)
+
+def save(troupe, path):
+       import pickle
+       with path.open('wb') as f:
+               pickle.dump(troupe, f)
+
+def random_zoombini():
+       import random
+       return Zoombini({random.choice(list(t)) for t in ALL_ATTRS})
index 2650c16a853f2f0e1b82d6b4936ddd1ac4a7b119..80de256b62b0ca1fa998f1658739770b2f25bc46 100644 (file)
@@ -1,8 +1,7 @@
 import sys
-if '../../..' not in sys.path:
-       sys.path.append('../../..')
 
 import zoombinis.common as common
+import zoombinis.ui as ui
 from zoombinis.puzzles.allergic_cliffs.common import *
 
 import pathlib
@@ -15,47 +14,73 @@ 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)
+class Instance:
+       def __init__(self, zoombinis, io_agent):
+               self.io_agent = io_agent
+               self.waiting = set(zoombinis)
+               self.across = {cliff: set() for cliff in Cliff}
+               self.cand_rules = {cliff: set(common.ALL_ATTRS) for cliff in Cliff}
+
+               self.pos_cliff = None
+               self.pos_cand_rules = None
+               self.pos_rules = None
+
+       def choose_zoombini(self):
+               if self.pos_rules is not None:
+                       return next(iter(self.waiting))
+               elif self.pos_cliff is None:
+                       key = lambda z: (
+                               len(z.attrs & self.cand_rules[Cliff.UPPER])
+                               * len(z.attrs & self.cand_rules[Cliff.LOWER])
+                       )
+                       return max(self.waiting, key = key)
+               else:
+                       return next(iter(self.waiting))
+
+       def choose_cliff(self, zoombini):
+               self.check_solve()
+               if self.pos_rules is None:
+                       def key(cliff):
+                               pool = self.across[cliff] if self.across[cliff] else self.waiting - {zoombini}
+                               # assume that if there's a cliff with no Zoombinis across there are still others waiting to cross
+                               return sum(len(zoombini.attrs & z.attrs) for z in pool) / len(pool)
+                       return max(Cliff, key = key)
+               else:
+                       return self.pos_cliff if zoombini.attrs & self.pos_rules else self.pos_cliff.other()
+
+       def check_solve(self):
+               if self.pos_cliff is None:
+                       drained = [cliff for cliff in Cliff if len(self.cand_rules[cliff]) < 3]
+                       if drained:
+                               [neg_cliff] = drained
+                               self.pos_cliff = neg_cliff.other()
+                               self.pos_cand_rules = self.cand_rules[self.pos_cliff]
+                               self.check_solve()
+               elif len(self.pos_cand_rules) == 3:
+                       self.pos_rules = frozenset(self.pos_cand_rules)
+
+       def send(zoombini, cliff):
+               send_fmt = "Send {} across the {} cliff."
+               self.io_agent.print(send_fmt.format(zoombini, cliff.name.lower()))
+               ans = self.io_agent.choose("Did it work?", ["y", "n"])
+               if ans == 'n':
+                       cliff = cliff.other()
+                       self.io_agent.print(send_fmt.format(zoombini, cliff.name.lower()))
+                       self.io_agent.wait()
+               self.waiting.remove(zoombini)
+               self.across[cliff].add(zoombini)
+               if self.pos_cliff is None:
+                       for attr in zoombini.attrs:
+                               self.cand_rules[cliff.other()].discard(attr)
+               elif self.pos_rules is None and cliff == self.pos_cliff.other():
+                       for attr in zoombini.attrs:
+                               self.pos_cand_rules.discard(attr)
+
+       def run(self):
+               while self.waiting:
+                       z = self.choose_zoombini()
+                       c = self.choose_cliff()
+                       self.send(z, c)
+                       self.waiting.remove(z)
+
+Instance(troupe, ui.Agent(sys.stdin, sys.stderr)).run()
diff --git a/ui.py b/ui.py
new file mode 100644 (file)
index 0000000..99a54f3
--- /dev/null
+++ b/ui.py
@@ -0,0 +1,20 @@
+class Agent:
+       def __init__(self, in_stream, out_stream):
+               self.in_stream = in_stream
+               self.out_stream = out_stream
+
+       def print(self, message):
+               print(message, file = self.out_stream)
+
+       def choose(self, message, choices):
+               def prompt():
+                       self.out_stream.write("{} ({}) ".format(message, '/'.join(choices)))
+                       return self.in_stream.readline().strip()
+               resp = prompt()
+               while resp not in choices:
+                       resp = prompt()
+               return resp
+
+       def wait(self):
+               self.out_stream.write("Ready? (Enter) ")
+               self.in_stream.readline()