#!/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')
