#!/usr/bin/env python3 # what's the most sugar canes that can be planted with a single water source? # restriction: all plants must be at the same height. # restriction: the water source must be at that same height. # this version is suitable for use with dispensers, # and requires no additional preparation. from z0 import Goal, SolverFor, Unary, BitVec, Bool, \ If, IfUnary, Implies, And, Or, Not, Totalizer, sat, unsat, \ binreduce If, IfBitVec = IfUnary, If import sys # configure terminal colors def esc(x): return "\x1B[" + str(x) + "m" color_none = esc(0) + esc(90) color_plant = esc(102) + esc(30) color_level_lo = esc(44) + esc(30) color_level_hi = esc(104) + esc(30) color_level_zero_lo = esc(100) + esc(34) color_level_zero_hi = esc(100) + esc(92) color_reset = esc(0) def get(el): if el is None: return None elif isinstance(el, Bool): return model[el.name] elif isinstance(el, BitVec): return model[el.name] elif isinstance(el, Unary): return model[el.name].bit_length() else: assert False, el return 0 def render(level, plant, block): if plant is None: return color_none + ":" if level is None: return color_none + ";" # erroneous if plant == 1: return color_plant + "X" if block == 1: # upper if level == 0: return color_level_zero_hi + "O" else: return color_level_hi + str(level) else: # lower if level == 0: return color_level_zero_lo + "0" else: return color_level_lo + str(level) def render_short(short): if short is None: return color_none + ":" if short == 0: return color_level_zero_lo + "0" else: return color_level_lo + str(short) ### start_level = 8 size = 1 + 2 * (start_level - 2 + # upper start_level + 1) # lower halfsize = size // 2 if 1: symmetry = 4 # rotational cross_heuristic = True no_preparation = True previous = 200 # best: 240 without preparation, 260 with. level_max = start_level max_upper = start_level - 1 else: symmetry = 4 cross_heuristic = True no_preparation = False previous = 0 level_max = start_level #+ 1 max_upper = 1 def mandist(x, y): return abs(x - halfsize) + abs(y - halfsize) def make2d(f, w=size, h=size): return [[f(x, y) for x in range(w)] for y in range(h)] # generate the unobstructed diamond shape by manhattan distances. usable = make2d(lambda x, y: mandist(x, y) <= halfsize) usables = sum(1 if t else 0 for row in usable for t in row) plant_len = usables.bit_length() def makeuse(f, usable=usable): return make2d(lambda x, y: f(x, y) if usable[y][x] else None) # since the water level is strictly decreasing, # we can skip generating equations for blocks where usable[y][x] == False. xy_fmt = "{:02X}{:02X}" if symmetry == 4 else "x{:02}y{:02}" xyi_fmt = "{:02X}{:02X}u{:01X}" if symmetry == 4 else "x{:02}y{:02}u{:01X}" levels = makeuse(lambda x, y: Unary("L" + xy_fmt.format(x, y), level_max)) blocks = makeuse(lambda x, y: Bool("B" + xy_fmt.format(x, y))) plants = makeuse(lambda x, y: Bool("P" + xy_fmt.format(x, y))) # find the shortest path down to the lower level by flowing backwards. # this needs only a smaller section than the previous variables. short_usable = makeuse(lambda x, y: (no_preparation and mandist(x, y) <= start_level)) shorts = makeuse(lambda x, y: Unary("S" + xy_fmt.format(x, y), level_max), short_usable) arrays = (levels, blocks, plants, shorts) if symmetry == 1: pass elif symmetry == 2: for array in arrays: for y_a in range(halfsize + 1): y_b = size - y_a - 1 for x_a in range(size): x_b = size - x_a - 1 array[y_b][x_b] = array[y_a][x_a] elif symmetry == 4: middle = halfsize + 1 for array in arrays: for y in range(middle): for x in range(middle): # bottom right corner: # equivalent to mirroring both axes. array[size - y - 1][size - x - 1] = array[y][x] # bottom left corner: # as x increases, y decreases. # as y increases, x increases. array[size - x - 1][y] = array[y][x] # top right corner: # as x increases, y increases. # as y increases, x decreases. array[x][size - y - 1] = array[y][x] else: raise Exception(f"unsupported value of symmetry: {symmetry}") def check(x, y): return x >= 0 and y >= 0 and x < size and y < size and usable[y][x] def maybe(x, y): return (levels[y][x], blocks[y][x], shorts[y][x]) if check(x, y) else None def xyiter(width, height): for y in range(height): for x in range(width): yield x, y ### g = Goal() if symmetry == 1: sym_width, sym_height = size, size elif symmetry == 2: sym_width, sym_height = size, (size + 1) // 2 elif symmetry == 4: sym_width, sym_height = (size + 1) // 2, (size + 1) // 2 plant_pairs = [] for x, y in xyiter(sym_width, sym_height): if not usable[y][x]: continue level_var = levels[y][x] block_var = blocks[y][x] plant_var = plants[y][x] short_var = shorts[y][x] for var in (level_var, short_var): if var is None: continue # bound the water levels. g.add(var <= start_level) distance = mandist(x, y) # the center block is the water source block. centered = distance == 0 # plants must be on blocks. g.add(Implies(plant_var, block_var)) # plants cannot be placed in water! g.add(Implies(plant_var, level_var == 0)) if cross_heuristic and (x == halfsize or y == halfsize): # 8765432 876543210 # 0123456 789ABCDEF step_kink = max_upper if distance < step_kink: g.add(level_var == start_level - distance) g.add(block_var == True) g.add(plant_var == False) elif distance == start_level + step_kink: g.add(level_var == 0) g.add(block_var == True) g.add(plant_var == True) else: g.add(level_var == max(start_level + step_kink - distance, 0)) g.add(block_var == False) g.add(plant_var == False) elif centered: # configure the water source. g.add(level_var == start_level) g.add(block_var == True) g.add(plant_var == False) # take account for symmetry. plant_worth = 1 if symmetry == 2: plant_worth = 2 if y != halfsize else 1 elif symmetry == 4: if x != halfsize: plant_worth *= 2 if y != halfsize: plant_worth *= 2 plant_pairs.append((plant_var, plant_worth)) # gather information on cardinal directions. neighbors = [maybe(x - 1, y), # west, "left" maybe(x + 1, y), # east, "right" maybe(x, y - 1), # north, "up" maybe(x, y + 1)] # south, "down" neighbors = [n for n in neighbors if n is not None] # gather information about neighboring cells. max_level_hi = 0 max_level_lo = 0 max_short = 0 for n_level, n_block, n_short in neighbors: max_level_hi = Unary.max(max_level_hi, If(n_block, n_level, 0)) max_level_lo = Unary.max(max_level_lo, If(n_block, 0, n_level)) if n_short is not None: max_short = Unary.max(max_short, n_short) # some convenience... upper = block_var lower = Not(block_var) any_level = level_var != 0 #assert max_upper == start_level - 1, "unimplemented (FIXME)" if max_upper < start_level: g.add(Implies(And(upper, any_level), level_var > start_level - max_upper)) # this isn't necessary, but might assist the solver: # NOTE: should probably include max_level_lo > 0 as well. # NOTE: this turns out to be necessary for strictly correct no_preparation. g.add(Implies(And(upper, Not(any_level)), plant_var)) # plants must be placed next to water alongside and below its block. g.add(Implies(plant_var, max_level_lo != 0)) if not no_preparation: short = True elif short_var is None: short = False else: # find the shortest path down from the upper level. # annoyingly, this is how water flows in minecraft. # the usual flow, but backwards. g.add(Implies(And(upper, max_short != 0), short_var == max_short - 1)) g.add(Implies(lower, short_var == start_level)) source_short_var = shorts[halfsize][halfsize] short = short_var == source_short_var + distance # trying some alternative definitions: if not centered: g.add(Implies(upper, level_var == If(And(short, max_level_hi != 0), max_level_hi - 1, 0))) g.add(Implies(lower, level_var == If(max_level_hi != 0, start_level, If(max_level_lo != 0, max_level_lo - 1, 0)))) if 1: plant_sum = BitVec("Plants", plant_len, 0) plant_scores = [IfBitVec(var, worth, 0, plant_len) for var, worth in plant_pairs] #plant_sum = sum(plant_scores) plant_sum = binreduce(plant_scores, lambda a, b: a + b) else: plant_scores = sum(([var] * worth for var, worth in plant_pairs), []) plant_sum = Totalizer(plant_scores) # TODO: optimal(?) sum reduction (assuming inputs are 1-bit) would be: # A = 1 + 1 + 1 = 3 (2 bits) # B = A + A + 1 = 3 + 3 + 1 = 7 (3 bits) # C = B + B + 1 = 7 + 7 + 1 = 15 (4 bits) # etc. s = SolverFor("QF_BV") # Quantifier-Free Bit-Vectors s.add(g) satisfiable = s.check() print("check:", satisfiable) sys.stdout.flush() if len(sys.argv) > 1 and sys.argv[1] == "check": sys.exit(0) while satisfiable == sat: #satisfiable = s.check(plant_sum > previous) g.add(plant_sum > previous) satisfiable = s.check() print("check:", satisfiable) if satisfiable != sat: break model = s.model() #previous = model['Plants'] # doesn't work since this is *before* summing previous = sum(worth if model[var.name] else 0 for var, worth in plant_pairs) print("plants:", previous) water = sum(1 if var is not None and model[var.name] > 0 else 0 for row in levels for var in row) print("water: ", water) #print(model) #for k, v in model.items(): # if v != 0: # print(k, v) lines = "" for rows in zip(levels, plants, blocks, shorts): all_row = [[get(el) for el in row] for row in rows] lines += "".join(render(level, plant, block) for level, plant, block, _ in zip(*all_row)) if no_preparation: # shortest distance debugging lines += " " lines += "".join(render_short(short) for _, _, _, short in zip(*all_row)) lines += color_reset + "\n" print(lines) sys.stdout.flush()