Skip to content

Instantly share code, notes, and snippets.

@00001H
Last active January 7, 2024 05:39
Show Gist options
  • Save 00001H/1f3c165a386aa842cb8af413f277f97e to your computer and use it in GitHub Desktop.
Save 00001H/1f3c165a386aa842cb8af413f277f97e to your computer and use it in GitHub Desktop.
Automatic IWL Solver
import operator,copy
from abc import abstractmethod,ABCMeta
colors = dict(
RED = "red",
BLUE = "blue",
GREEN = "green",
ORANGE = "orange",
WHITE = "white",
CYAN = "cyan",
BROWN = "brown",
PURPLE = "purple",
SIGNFLIP = "signflip",
SILVER = "silver",
PURE = "silver",
GOLD = "gold"#approved by dev to call Master Keys "Gold Keys"
)
globals().update(colors)
FROZEN = 1
PAINTED = 2
ERODED = 4
def master_immune(c):
return c in (GOLD,SILVER)
class KeySet:
def __init__(self,k=None):
self.k = {} if k is None else k
def __eq__(self,other):
return self.k == other.k
def __getitem__(self,c):
return self.k.get(c,0)
def __setitem__(self,c,n):
self.k[c] = n
def __hash__(self):
return hash(frozenset(self.k.items()))
def dup(self):
return KeySet(self.k.copy())
def __repr__(self):
s = ""
for k,v in self.k.items():
s += f"{k}: {v}\n"
return s
class GameData:
def __init__(self,k=None):
self.keys = KeySet() if k is None else k
def __eq__(self,other):
return self.keys == other.keys
def __hash__(self):
return hash(self.keys)
def dup(self):
return GameData(self.keys.dup())
def __repr__(self):
return f"--KEYS--\n{self.keys}"
class Action(metaclass=ABCMeta):
def __init__(self):
self.desc = None
def describe_as(self,st):
self.desc = st
return self
def __repr__(self):
return super().__repr__() if self.desc is None else self.desc
@abstractmethod
def use(self,game):
pass
def then(self,action2):
return MultistepAction(self,action2)
class MultistepAction(Action):
def __init__(self,*a):
super().__init__()
self.actions = a
def add(self,a):
self.actions.append(a)
def use(self,game):
for i in self.actions:
i.use(game)
class AddKeyAction(Action):
def __init__(self,c,a):
super().__init__()
self.color = c
self.amount = a
def use(self,game):
game.keys[self.color] += self.amount
class Actionable(metaclass=ABCMeta):
@abstractmethod
def get_possible_actions(self,game):
return []
from collections.abc import MutableSequence
class DuplessList(MutableSequence):
def __init__(self,*a,**k):
self.v = list(*a,**k)
def __getitem__(self,i):
return self.v[i]
def __setitem__(self,i,v):
self.v[i] = v
def __delitem__(self,i):
del self.v[i]
def __eq__(self,o):
return self.v == o.v
def __len__(self):
return len(self.v)
def append(self,thing):#overriding anyway because C-impl might be faster
self.v.append(thing)
def pop(self):
return self.v.pop()
def extend(self,other):
self.v.extend(other)
def __iadd__(self,other):
return self.v.__iadd__(other)
def remove(self,thing):
self.v.remove(thing)
def reverse(self,thing):
self.v.reverse(thing)
def insert(self,where,thing):
self.v.insert(where,thing)
def __deepcopy__(self,memo):
return DuplessList()
def __repr__(self):
return repr(self.v)+"(not copyable)"
class GameObject(Actionable):
def __init__(self):
super().__init__()
self.accessible = True
self.id = "?"
self.grants_access_to = DuplessList()
self.exists = True
@abstractmethod
def _hsh(self):
return 0
@abstractmethod
def _cmpeq(self,other):
return True
def __hash__(self):
return (hash(self.accessible)
^ hash(self.id) << 5
^ hash(self.exists) << 12
^ self._hsh()
)
def __eq__(self,other):
return (type(self)==type(other)
and self.accessible == other.accessible
and self.exists == other.exists
and self.grants_access_to == other.grants_access_to
and self._cmpeq(other)
)
@abstractmethod
def _repr(self):
return ""
def __repr__(self):
return f"#{self.id}={self._repr()}"
def dup(self):
return copy.deepcopy(self)
@abstractmethod
def _get_possible_actions(self,game):
return []
def lb(s,t):
return s.locked_behind(t)
def locked_behind(self,thing):
self.accessible = False
thing.grants_access_to.append(self)
return self
def restore(self):
self.exists = True
def remove(self):
self.exists = False
for i in self.grants_access_to:
i.accessible = True
def get_possible_actions(self,game):
return self._get_possible_actions(game) if self.accessible else []
class DeleteObjectAction(Action):
def __init__(self,dst):
super().__init__()
self.dst = dst
def use(self,game):
self.dst.remove()
class Key(GameObject):
def __init__(self,c,a):
super().__init__()
self.color = c
self.amount = a
def _hsh(self):
return hash(self.color) ^ hash(self.amount)
def _cmpeq(self,other):
return self.color == other.color and self.amount == other.amount
def _repr(self):
return f"Key({self.amount} {self.color})"
def _get_possible_actions(self,game):
if self.amount == SIGNFLIP:
return [AddKeyAction(self.color,-2*game.keys[self.color])
.then(DeleteObjectAction(self))
.describe_as(f"Get {self}")]
else:
return [AddKeyAction(self.color,self.amount)
.then(DeleteObjectAction(self))
.describe_as(f"Get {self}")]
def sign(x):
if x < 0:
return -1
if x > 0:
return 1
return 0
class SetDoorCurseAction(Action):
def __init__(self,d,c):
super().__init__()
self.door = d
self.curse = c
def use(self,game):
if self.curse:
self.door.curse()
else:
self.door.uncurse()
class DoorEffectMaskAction(Action):
def __init__(self,d,m):
super().__init__()
self.door = d
self.mask = m
def use(self,game):
self.door.eff &= self.mask
class Lock:
def __init__(self,c,a=0):
self.color = c
self.amount = a
self.old_color = None
def curse(self):
self.old_color = self.color
self.color = BROWN
def uncurse(self):
self.color = self.old_color
self.old_color = None
@property
def cursed(self):
return self.old_color is not None
def __repr__(self):
return f"{self.amount or 'blank'} {self.color}"
def __eq__(self,other):
return self.color == other.color and self.amount == other.amount and self.old_color == other.old_color
def __hash__(self):
return hash(self.color) ^ hash(self.amount) ^ hash(self.old_color)
@staticmethod
def _nmatch_keys(pkeys,amnt):
if amnt == 0:
return (not pkeys)
if sign(pkeys.real) != sign(amnt.real):
return False
if sign(pkeys.imag) != sign(amnt.imag):
return False
if abs(pkeys.real) < abs(amnt.real):
return False
return abs(pkeys.imag) >= abs(amnt.imag)
def meets_req(self,game):
pkz = game.keys[self.color]
return self._nmatch_keys(pkz,self.amount)
class Door(GameObject):
def __init__(self,c,*l,e=0):
super().__init__()
self.color = c
self.eff = e
self.old_color = None
self.locks = tuple(l)
self.copies = 0
def curse(self):
self.old_color = self.color
self.color = BROWN
for l in self.locks:
l.curse()
def uncurse(self):
self.color = self.old_color
self.old_color = None
for l in self.locks:
l.uncurse()
@property
def cursed(self):
return self.old_color is not None
def _hsh(self):
return hash(self.color) ^ hash(self.locks) ^ hash(self.eff) ^ hash(self.old_color)
def _cmpeq(self,other):
return (self.color == other.color
and self.eff == other.eff
and self.old_color == other.old_color
and self.locks == other.locks
)
def _repr_effects(self):
if self.eff:
return bool(self.eff&FROZEN)*"F"+bool(self.eff&PAINTED)*"P"+bool(self.eff&ERODED)*"E"+" "
return ""
@property
def simple(self):
return len(self.locks)==1 and self.locks[0].color == self.color
def _repr(self):
if len(self.locks) == 0:
return "<ERROR: Lockless door>"
cstr = (self.old_color+"(cursed)" if self.cursed else self.color)
s = "Door("+self._repr_effects()
if self.simple:
s += str(self.locks[0].amount)+" "+cstr
elif len(self.locks) == 1:
s += cstr+", lock is "+str(self.locks[0])
else:
s += cstr+", combo"+str(self.locks)
return s+")"
def _get_possible_actions(self,game):
close_action = None
if self.eff:
dem = 0
if game.keys[RED].real >= 1:
dem |= FROZEN
if game.keys[BLUE].real >= 3:
dem |= PAINTED
if game.keys[GREEN].real >= 5:
dem |= ERODED
if dem:
dema = DoorEffectMaskAction(self,~dem)
if close_action is None:
close_action = dema
else:
close_action = close_action.then(dema)
if self.cursed and game.keys[BROWN].real < 0:
uca = SetDoorCurseAction(self,False)
if close_action is None:
close_action = uca
else:
close_action = close_action.then(uca)
if not self.cursed and game.keys[BROWN].real > 0:
ca = SetDoorCurseAction(self,True)
if close_action is None:
close_action = ca
else:
close_action = close_action.then(ca)
if close_action is None:
l = []
smz = 0
for lk in self.locks:
if lk.meets_req(game):
smz -= lk.amount
else:
break
else:
l.append(AddKeyAction(self.color,smz)
.then(DeleteObjectAction(self))
.describe_as(f"Open {self}"))
if not master_immune(self.color) and game.keys[GOLD]:
for lk in self.locks:
if master_immune(lk.color):
break
else:
l.append(AddKeyAction(GOLD,-1)
.then(DeleteObjectAction(self))
.describe_as(f"Use master key on {self}"))
return l
else:
return [close_action.describe_as(f"Stand next to {self}")]
class Goal(GameObject):
def _get_possible_actions(self,game):
return super()._get_possible_actions(game)
def _hsh(self):
return 0
def _cmpeq(self,other):
return True
def _repr(self):
return "Goal"
class Level:
def __init__(self,g=None):
self.objs = []
self.game = GameData() if g is None else g
def __eq__(self,other):
return self.objs == other.objs and self.game == other.game
def __hash__(self):
return hash(tuple(self.objs)) ^ (hash(self.game) << 4)
def add(self,obj):
obj.id = len(self.objs)
self.objs.append(obj)
return obj
def __repr__(self):
s = ""
for thing in self.objs:
if thing.accessible:
s += self._reprobj(thing)
return repr(self.game)+"--LEVEL--\n"+s
def _reprobj(self,obj,il=0):
s = il*" "
if not obj.exists:
s += "(removed)"
s += str(obj)+f"\n"
for thing in obj.grants_access_to:
s += self._reprobj(thing,il+1)
return s
@property
def existing(self):
yield from filter(operator.attrgetter("exists"),self.objs)
def dup(self):
other = Level(self.game.dup())
for obj in self.objs:
other.add(obj.dup()).grants_access_to = list(map(operator.attrgetter("id"),obj.grants_access_to))
for obj in other.objs:
obj.grants_access_to = list(map(other.objs.__getitem__,obj.grants_access_to))
return other
def solved(self):
for obj in self.existing:
if obj.accessible and isinstance(obj,Goal):
return True
return False
def solve(self,multisolution=False,step=1,cache=None):
if cache is None:
cache = {}
if self.solved():
return ("",)
ac = []
solns = []
for i,obj in enumerate(self.objs):
if obj.exists:
for j in range(len(obj.get_possible_actions(self.game))):
ac.append((i,j))
for oid,aid in ac:
experiment = self.dup()
experiment.objs[oid].get_possible_actions(experiment.game)[aid].use(experiment.game)
if experiment not in cache:
cache[experiment] = experiment.solve(multisolution,step+1,cache)
for soln in cache[experiment]:
this_step = str(self.objs[oid].get_possible_actions(self.game)[aid])
solns.append(f"{step}. {this_step}\n{soln}")
if not multisolution:
return solns
return solns
def row(l,p=None,*v):
if v:
for i in v:
if p:
p = l.add(i.lb(p))
else:
p = l.add(i)
return p
return None
def parsn(n):
try:
return int(n)
except ValueError:
return None
def parse(ld):
a = Level()
i = 0
for l in filter(None,ld.splitlines()):
if i == 0 and l.startswith("~"):
for kc in l.removeprefix("~").split():
kc = kc.upper()
try:
a.game.keys[colors[kc]] = 0
except LookupError:
raise ValueError(f"Bad key color {kc}")
continue
obj = None
prer = None
if (whr := l.find(">")) != -1:
if l[:whr].isdigit():
prer = int(l[:whr])
l = l[whr+1:]
l = l.removeprefix(f"{i}:").strip()
if not l:
raise ValueError(f"Data missing for object #{i}")
c = l[0].lower()
argv = l[1:].split()
de = 0
if c == "g":
obj = Goal()
if len(argv):
raise ValueError(f"Parameters passed to goal #{i}")
elif c == "d":
if all(map("fpe".__contains__,argv[0].lower())):
for e in argv[0].lower():
if e == "f":
de |= FROZEN
elif e == "p":
de |= PAINTEED
elif e == "e":
de |= ERODED
argv = argv[1:]
dc = None
locks = []
for x in range(0,len(argv)-1,2):
if (ln := parsn(argv[x])) is not None:
argv[x+1] = argv[x+1].upper()
try:
lc = colors[argv[x+1]]
except LookupError:
raise ValueError(f"Bad lock color {argv[x+1]}")
locks.append(Lock(lc,ln))
else:
raise ValueError(f"Bad lock count {argv[x]}")
if len(argv)%2:
try:
argv[-1] = argv[-1].upper()
dc = colors[argv[-1]]
except LookupError:
raise ValueError(f"Bad door color {argv[-1]}")
elif len(locks) == 1:
dc = locks[0].color
else:
raise ValueError(f"Cannot determine door color for door #{i}")
obj = Door(dc,*locks,e=de)
elif c == "k":
if len(argv) != 2:
raise ValueError(f"k spec requires exactly 2 args, found {len(argv)}")
if (nm := parsn(argv[0])) is not None:
try:
argv[1] = argv[1].upper()
kc = colors[argv[1]]
except LookupError:
raise ValueError(f"Bad key color {argv[1]}")
obj = Key(kc,nm)
else:
raise ValueError(f"Bad key count {argv[0]}")
if obj:
if prer is not None:
obj = obj.locked_behind(a.objs[prer])
a.add(obj)
else:
raise ValueError(f"Bad line '{l}'")
i += 1
return a
def solvdump(l):
print(l)
slnz = l.solve()
for i,s in enumerate(slnz,1):
print(f"Solution #{i}:")
print(s)
def level_4_4_new_golden_gimmick():
return parse("""
~gold orange cyan
0:k 3 gold
1:d 3 gold
1>2:k 4 orange
2>3:d 0 orange
3>4:k 2 cyan
4>5:d 3 cyan
5>6:k 3 gold
1>7:d 5 gold
7>8:d 0 gold
8>9:g
1>10:d 2 orange
10>11:d 2 orange
11>12:k 2 cyan
12>13:d 2 cyan
13>14:k 2 gold
14>15:d 3 gold
15>16:k 5 gold
""")
def level_c106_3_door_effect_test():
return parse("""
~red cyan
0:d f 1 cyan
0>1:g
2:k 1 red
3:k 1 cyan
""")
def level_c106_4_door_curse_test():
return parse("""
~gold brown
0:d 5 gold
0>1:g
2:k 1 gold
3:k 1 brown
""")
def level_c122_1_door_combo_test():
return parse("""
~white orange
0:d 5 white orange
1:k 5 white
2:d -5 orange
2>3:g
""")
solvdump(a := level_c122_1_door_combo_test())
@00001H
Copy link
Author

00001H commented Jan 7, 2024

Now door effects, negative keys/doors, curses and bicolored doors should work.

Combo doors are untested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment