icfp25/harald/explore.py

284 lines
7.9 KiB
Python

import api
import functools
from collections import defaultdict
class ExploreError(Exception):
pass
@functools.total_ordering
class Path:
def __init__(self, path=""):
self.path = path
def __repr__(self):
return "." +self.path if self.path else "."
def __bool__(self):
return bool(self.path)
def __eq__(self, other):
return self.path == other.path
def __lt__(self, other):
return (len(self.path), self.path) < (len(other.path), other.path)
def __hash__(self):
return self.path.__hash__()
def __format__(self, spec):
return str(self).__format__(spec)
def extend(self, door):
return Path(self.path + str(door))
def last(self):
return Path(self.path[:-1]), int(self.path[-1])
def shorten(self, path, pmerge):
if self.path.startswith(pmerge.path):
return Path(path.path + self.path[len(pmerge.path):])
else:
return self
class Explore:
def __init__(self, problem):
self.problem = problem
self.rooms = {}
self.room_ids = defaultdict(lambda: set())
self.neighbors = {}
self.unification_id = {}
self.unifications = defaultdict(lambda: set())
def _path(self, path):
return self.unification_id.get(path, path)
def save(self):
return {
"problem": self.problem,
"room": dict(self.rooms),
"room_ids": dict(self.room_ids),
"neighbours": dict(self.neighbors),
"unification_id": dict(self.unification_id),
"unifications": dict(self.unifications),
}
@classmethod
def load(cls, obj):
new = cls(obj["problem"])
new.problem = obj["problem"]
new.rooms.update(obj["room"])
new.room_ids.update(obj["room_ids"])
new.neighbors.update(obj["neighbours"])
new.unification_id.update(obj["unification_id"])
new.unifications.update(obj["unifications"])
return new
def explore(self, path=Path()):
path = self._path(path)
if path in self.rooms:
return self.rooms[path]
paths = [path.extend(i) for i in range(6)]
self.neighbors[path] = [(p, None) for p in paths]
print("explore", paths)
results = api.explore([p.path for p in paths])["results"]
print("id", path, results)
label = results[0][-2]
neighbors = [r[-1] for r in results]
room_id = str(label) + "".join(str(n) for n in neighbors)
self.rooms[path] = room_id
self.room_ids[room_id].add(path)
# update pen-ultimate room
if path:
pl, dl = path.last()
p, _ = self.neighbors[pl][dl]
self.neighbors[pl][dl] = (p, room_id)
return room_id
def dump(self):
print(self.problem + " rooms:")
for r, rid in self.rooms.items():
print(f" {r:<10}: {rid} {self.neighbors[r]}")
print()
for rid, paths in self.room_ids.items():
print(f" {rid}: {paths}")
print()
def unify(self, path1, path2):
"""
try to unify rooms at paths p1, p2
return unified rooms
"""
path1 = self._path(path1)
path2 = self._path(path2)
if path1 == path2:
return
if path1 not in self.rooms:
raise ExploreError(f"room '{path1}' not explored")
if path2 not in self.rooms:
raise ExploreError(f"room '{path2}' not explored")
if self.rooms[path1] != self.rooms[path2]:
raise ExploreError(
f"ids of '{path1}'({self.rooms[path1]}) and '{path2}'({self.rooms[path2]}) do not match"
)
path = min(path1, path2)
pmerge = max(path1, path2)
room_id = self.rooms[path]
rooms = self.rooms.copy()
room_ids = self.room_ids.copy()
neighbors = self.neighbors.copy()
unifications = self.unifications.copy()
unification_id = self.unification_id.copy()
merged_neighbors = []
for n, ((p, rid), (pm, rmid)) in enumerate(
zip(neighbors[path], neighbors[pmerge])
):
if rid and rmid and rid != rmid:
raise ExploreError(
f"neighbor {n} of '{path}'({rid}) and '{pmerge}'({rmid}) do not match"
)
merged_neighbors.append((p.shorten(path, pmerge), rid or rmid))
# unify paths
unification_id[pmerge] = path
for p in unifications[pmerge]:
unification_id[p] = path
unifications[path] = unifications[path1] | unifications[path2] | {pmerge}
if pmerge in unifications:
del unifications[pmerge]
# fix rooms
del rooms[pmerge]
room_ids[room_id] = {p for p in room_ids[room_id] if p != pmerge}
neighbors[path] = merged_neighbors
del neighbors[pmerge]
for p, ns in neighbors.items():
new = []
for np, rid in ns:
np = np.shorten(path, pmerge)
new.append((np, rid))
if rid:
assert np in rooms, f"path {np} of {(p,ns)} not in rooms"
neighbors[p] = new
unified = self.__class__(self.problem)
unified.rooms = rooms
unified.room_ids = room_ids
unified.neighbors = neighbors
unified.unifications = unifications
unified.unification_id = unification_id
return unified
def unify_all(self):
unified = self
while True:
for rid, paths in unified.room_ids.items():
if len(paths) > 1:
paths = list(paths)
print("unify", paths[0], paths[1])
unified = unified.unify(paths[0], paths[1])
break
break
return unified
def unexplored(self):
for path, ns in self.neighbors.items():
for p, rid in ns:
if not rid:
yield p
def is_explored(self):
return next(self.unexplored(), None) is None and all(
len(p) == 1 for p in self.room_ids.values()
)
def guess(self):
ids = {}
for i, rid in enumerate(self.room_ids.keys()):
ids[rid] = i
connected = set()
connections = []
for path, ns in self.neighbors.items():
src_id = self.rooms[path]
for src_door, (trg_path, trg_id) in enumerate(ns):
src = (src_id, src_door)
trg_door = next(
j
for j, (p, rid) in enumerate(self.neighbors[trg_path])
if rid == src_id
)
trg = (trg_id, trg_door)
if (src, trg) in connected or (trg, src) in connected:
continue
print(src, trg)
connected.add((src, trg))
connections.append(
{
"from": {"room": ids[src[0]], "door": src[1]},
"to": {"room": ids[trg[0]], "door": trg[1]},
}
)
layout = {
"rooms": [int(rid[0]) for rid in ids.keys()],
"startingRoom": ids[self.rooms[Path()]],
"connections": connections,
}
# print(layout)
return api.guess(layout)
def solve(problem):
ex = Explore(problem)
api.select(ex.problem)
ex.explore()
ex.dump()
while True:
unexplored = next(ex.unexplored(), None)
if not unexplored:
break
print("explore", unexplored)
ex.explore(unexplored)
ex.dump()
print("unify")
ex = ex.unify_all()
ex.dump()
print("explored", ex.is_explored())
if ex.is_explored():
print("guess", ex.guess())
if __name__ == "__main__":
# with open("test.rooms") as h:
# obj = ast.literal_eval(h.read())
solve("primus")