#!/usr/bin/env python3 import random from argparse import ArgumentParser parser = ArgumentParser( "Random instance using the faces of Boggle Classic dice." ) parser.add_argument("--qu_important", action='store_true', help="Instance fails submission ignoring Qu") parser.add_argument("--possible", action='store_true', help="Instance is possible") parser.add_argument("--sorted", action='store_true', help="Instance is already sorted") parser.add_argument("--impossible", action='store_true', help="Instance is impossible") parser.add_argument("--kill_greedy", action='store_true', help="Instance fails the greedy solution") parser.add_argument("--seed", type=int, default=0, help="seed") args = parser.parse_args() random.seed(args.seed) letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' def before(a, b): if a == "Q": return b >= "U" return a <= b def ignore_q(a, b): return a <= b def solve(lines, comparator=before): front = lines[0] sides: list[set[str]] = [set() for _ in range(16)] for line in lines[1:5]: for i, l in enumerate(line): sides[i].add(l) back = lines[-1] best = [ (0, front[0]), (2, back[0]) ] + [(1, letter) for letter in sides[0] if letter != front[0]] for i in range(1, 16): new_best = [] for letter, delta in [(front[i], 0)] + [(side, 1) for side in sides[i]] + [(back[i], 2)]: for turns, prefix in best: if comparator(prefix[-1], letter): new_best.append((turns + delta, prefix + letter)) best = new_best if best: return(min(best)) else: return('impossible') def greedy(lines): prefix = '@' # '@' < 'A' turns = 0 for i in range(16): vals = [(0, lines[0][i])] + [(2, lines[5][i])] + [(1, lines[j][i]) for j in [1,2,3,4]] filtered_vals = list(t[::-1] for t in vals if t[1] >= prefix[-1]) if not filtered_vals: return 'impossible' c, t = min(filtered_vals) prefix += c turns += t return turns, prefix def throw_die(faces): """ Correctly throw a six-sided die with the given faces. (This is not the same as shuffling the die faces; the geometry of who is on whose opposite face must be preserved.) Assumes the face order is top, *sides, bottom, with sides being opposites (3,4,2,5 on a standard die). TODO the visualiser uses a different order. """ # Convert to order top, bottom, side1, opposite-side1, side2, opposite-side2 faces = [faces[0], faces[5]] + list(faces[1:5]) i = random.randrange(6) # index of new top face newtop = faces[i] newbottom = faces[i + (1 if (i % 2) == 0 else -1)] # remaining sides: sides = faces[:] sides.remove(newtop) sides.remove(newbottom) # pick random cyclic rotation sides += sides sides = sides[2*random.randrange(2):][:4] return(newtop, *sides, newbottom) dice = [list(faces) for faces in ( "AACIOT", "ABILTY", "ABJMOQ", "ACDEMP", "ACELRS", "ADENVZ", "AHMORS", "BIFORX", "DENOSW", "DKNOTU", "EEFHIY", "EGKLUY", "EGINTV", "EHINPS", "ELPSTU", "GILRUW", ) ] while True: random.shuffle(dice) for i in range(16): dice[i] = throw_die(dice[i]) if args.sorted: dice.sort() lines = [] for i in range(6): lines.append(''.join(dice[j][i] for j in range(16))) res = solve(lines) if res == 'impossible' and args.possible: continue if res != 'impossible' and args.impossible: continue if args.qu_important: if res == solve(lines, ignore_q): continue if args.kill_greedy: if res == greedy(lines): continue if args.sorted and res[0] != 0: continue break print(*lines, sep='\n')