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})
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
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()