In [1]:
import forge
from puzzle.puzzlepedia import puzzlepedia
puzzle = puzzlepedia.parse("""
# NB: Column is implied from alpha-order. See #4.
items in {
  Alarm, Chills, Dread, Terror,  # Children’s Tinned Fears.
  Balls, Floss, Biscuits, Mints,  # Monster Treats.
  Anger, Boredom, Laughter, Sorrow,  # Salts.
  Collywobbles, Panic, Jeebies, Unease,  # Tinned Fears.
}
aisle in range(1, 4+1)
height in {three, four, five, six}
knocked in {down, up}

# Custom constraints needed.
model.disable_constraints()

groups = [
  [Alarm, Chills, Dread, Terror],  # Children’s Tinned Fears.
  [Balls, Floss, Biscuits, Mints],  # Monster Treats.
  [Anger, Boredom, Laughter, Sorrow],  # Salts.
  [Collywobbles, Panic, Jeebies, Unease],  # Tinned Fears.
]
front = [Alarm, Balls, Anger, Collywobbles]

def aisle_n(item):
  return sum(i*item[i] for i in range(1, 4+1))

#1 DONE: Each aisle was dedicated to one type.
for g in groups:
  for a, b in zip(g, g[1:]):
    all(a[i] == b[i] for i in aisle)
  # Each group is only in one aisle.
  sum(g[0][i] for i in aisle) == 1
for i in aisle:
  # Every aisle has at least one group.
  any(f[i] for f in front)

#2 DONE: In setup.

#3 DONE: Each aisle has a 1 knocked down (values are sync'd).
for g in groups:
  for a, b in zip(g, g[1:]):
    all(a[h] == b[h] for h in height)
for i in items:
  # Each item can only have 1 height.
  sum(i[h] for h in height) == 1
# Up one of each height was knocked down.
for h in height:
  sum(h[f] for f in front) == 1

#4 DONE: Implicit.

#5 DONE.
#5a One spill per row / column.
for g in groups:
  # All items are either down or up.
  all(x.down or x.up for x in g)
  # One knocked down in every group.
  sum(x.down for x in g) == 1
#5b One knocked down for every row.
for row in range(4):
  sum(g[row].down for g in groups) == 1

#6 DONE.
if Mints.down: Laughter.down

#7 DONE:
#7a 18 levels were knocked over.
# In setup. 3+4+5+6=18 is only possibility.
#7b 4 stack is not left-most.
all((f.four and f[1]) == False for f in front)

#8 DONE: The 3 stack is not left-most.
all((f.three and f[1]) == False for f in front)

#9 DONE.
#9a (Collywobbles is an example of "Fears")
Collywobbles == 1 or Collywobbles == 4
#9b
Collywobbles == three or Collywobbles == five

#10 DONE.
#10a "Zombie Fresh Mints are on the (...) corner"
Mints == 1 or Mints == 4
#10b Mints are on the opposite corner from something else knocked down.
# -> Something knocked over is TL or TR.
any(f.down and (f == 1 or f == 4) for f in front)
# -> The first item in the Mints category (Balls) is *not* down.
Balls.down == False

#11 TODO.
#11 "[six] was immediately to the right of an alliterative item."
#alliterative = [
#  Balls,  # Banshee Balls, 1st in aisle.
#  Floss,  # Fang Floss, 2nd in aisle.
#]
# -> alliterative item cannot be in aisle 4.
Balls != 4
# -> Balls' row doesn't have six.
Balls != six
# -> six is in one row higher than Balls.
#aisle_n(six) == aisle_n(Balls) + 1
#eligible_front = [Alarm, Anger, Collywobbles]
#any(i == six and )
# -> six has to be in row 1 or 2.
eligible_front_rows = [
  Alarm, Chills,
  Anger, Boredom,
  Collywobbles, Panic,
]
ineligible_back_rows = [
  Dread, Terror,
  Laughter, Sorrow,
  Jeebies, Unease,
]
#for i in ineligible_back_rows:
#  (i == six and i == down) == False
#if Balls == 1: sum(i == 2 and i == down and i == six for i in eligible_front_rows) == 1
Balls == 1
# HACK
#Laughter == up
#all(if f.down and f.six)

#12 DONE. ("Anger" is an example of Salts, "Alarm" is an example of Children's Fears.)
aisle_n(Anger) < aisle_n(Alarm)

# OBSERVATIONS:
# Aisle 1 is five because everything else is "right of" something.
# Aisle 4 is three because the fears are odd.
# Zombie Mints are 1 or 4 (because corner) but they also have alliteration and are
# to the left of six.
# If alliteration is 1 then six is 2.
if Alarm == 3: Alarm != six
# HACK.
if Laughter == 2: Laughter == up
if Sorrow == 2: Sorrow == up
""")

# Triangle numbers.
hack_map = {
  'three': 6,
  'four': 10,
  'five': 15,
  'six': 21,
}
hack_map2 = {
  'three': 3,
  'four': 4,
  'five': 5,
  'six': 6,
}
item_map = {
  'Chills': 'The Chills',
  'Dread': 'Creeping Dread',
  'Terror': 'Night Terror',
  'Balls': 'Banshee Balls',
  'Floss': 'Fang Floss',
  'Biscuits': 'Werewolf Biscuits',
  'Mints': 'Zombie Fresh Mints',
  'Anger': 'Salt Made Tears of Anger',
  'Boredom': 'Salt Made from Tears of Boredom',
  'Laughter': 'Salt Made from Tears of Laughter',
  'Sorrow': 'Salt Made from Tears of Sorrow',
  'Collywobbles': 'The Collywobbles',
  'Panic': 'Escalating Panic',
  'Jeebies': 'The Heebie-Jeebies',
  'Unease': 'A Vague Sense of Unease',
}
def extract(s):
  result = {}
  for line in s.split('\n'):
    if 'down' not in line:
      continue
    columns = line.split()
    item = columns[0]
    isle = int(columns[2])
    height = hack_map[columns[4]]
    result[isle] = (item, height)
  for i in range(1, 4+1):
    item, height = result[i]
    item = item_map.get(item, item)
    item_short = item.replace(' ', '')
    print(i, item, height + 1, item[height], item_short[height])
extract(puzzle.solutions()[0])


Default constraints disabled
Default constraints disabled
Default constraints disabled
Default constraints disabled
Default constraints disabled
1 Werewolf Biscuits 16 t s
2 Salt Made from Tears of Boredom 22 o r
3 Night Terror 11 o r
4 The Collywobbles 7 l l

In [95]:
len('Salt Made from Tears of Laughter')


Out[95]:
32

In [ ]: