# -*- Mode: Python -*- # 8 colors: # red # yellow # green # blue # purple # indigo (pink) # orange # chartreuse (light green) # # grouped in fours: # ropi # gybc # # 12 pieces: # co,cr, cp # yi, yo, yr # gp, gi, go # bp, bi, br # # 0 co # 1 cr # 2 cp # 3 yi # 4 yo # 5 yr # 6 gp # 7 gi # 8 go # 9 bp # a bi # b br # layout: # from above, 0-1-2 on top, 9-a-b on bottom. # # 8 0 3 # # 7 A B 4 # # 2 1 # 9 # # 6 5 # 0 1 2 3 4 5 6 7 8 9 10 11 solved = ['br', 'cr', 'yr', 'bp', 'cp', 'co', 'yo', 'yi', 'bi', 'go', 'gi', 'gp'] # these will flip, depending on which way # the puzzle is oriented... a, b = 1, 0 # this scores the harder version of the puzzle. def score_hard (p): s = 0 if p[0][a] == p[1][a] == p[2][a]: s += 1 if p[1][b] == p[4][b] == p[5][b]: s += 1 if p[5][a] == p[6][a] == p[9][a]: s += 1 if p[2][b] == p[6][b] == p[7][b]: s += 1 if p[7][a] == p[8][a] == p[10][a]: s += 1 if p[0][b] == p[8][b] == p[3][b]: s += 1 if p[9][b] == p[10][b] == p[11][b]: s += 1 return s # scores the easier. # Note: always start with the ropi side on top, # otherwise these are reversed? def score_easy (p): s = 0 if p[0][a] == p[1][a] == p[2][a]: s += 1 if p[1][b] == p[4][b] == p[5][b]: s += 1 if p[5][a] == p[6][a] == p[9][a]: s += 1 if p[2][b] == p[6][b] == p[7][b]: s += 1 if p[7][a] == p[8][a] == p[10][a]: s += 1 if p[0][b] == p[8][b] == p[3][b]: s += 1 if p[9][b] == p[10][b] == p[11][b]: s += 1 return s # absolute doesn't work very well. I think this is because it # requires a particular orientation, and since our moves are described # in a way that requires 'disorienting' the puzzle, it can't easily # find a solution that way... # ok, now I changed it to 'rotate' the puzzle, but since the 'walk' # created by 0..12 is arbitrary, 'close' solutions are actually not # very close in any sense that is satisfying to humans. score_table = {} for p in range (12): score_table[solved[p]] = p def score_absolute (p): # first, rotate around the 'br' piece. r = range (12) l = list (p) i = l.index ('br') for x in range (12): r[x] = p[(x + i) % 12] # now, compute deltas... delta = 0 for i in range (12): j = score_table[r[i]] delta += abs (j-i) return -delta # moves cannot put the 'other' side's color on the top, # otherwise the score() function gets broken. [you can't # solve both versions of the puzzle simultaneously!] # (0,1,2,3,4,5,6,7,8,9,10,11) layout # : (, ) op_dict = { # cw rotation '+': ((2,0,1,7,8,3,4,5,6,11,9,10), 0), # cw rotate top 'T': ((2,0,1,3,4,5,6,7,8,9,10,11), 1), # flip puzzle (i.e., grab <6> and put it at <0>) 'F': ((6,5,9,2,1,4,11,10,7,3,8,0), 0), # cw rotate right front 'flower' 'R': ((0,5,2,3,1,4,6,7,8,9,10,11), 1), } ops = [ (v, k) for (k, v) in op_dict.items() ] class Stop (Exception): pass def check (p): p = list(p[:]) p.sort() s = solved[:] s.sort() if p != s: raise ValueError, 'invalid board' def move (p, op): return tuple ([p[i] for i in op]) def flat_path (path): r = [] while path is not None: name, path = path r.append (name) r.reverse() return r def _search (p, d, seen, limit, last, best, path): moves = [] for (op, rescore), name in ops: p2 = move (p, op) if not seen.has_key (p2): if rescore: moves.append ((score (p2), (p2, name))) else: moves.append ((last, (p2, name))) # try to sort by closest-to-solved moves.sort (reverse=True) for s, (p2, name) in moves: #print '%s %s %s' % (' '*d, p2, name) if d < limit: if s > best[0][2]: best[0] = flat_path ((name, path)), p2, s if s == 7: raise Stop seen[p2] = s _search (p2, d+1, seen, limit, s, best, (name, path)) else: break def search (p0, limit=12): global seen seen = {} best = [([], p0, score(p0))] path = None check (p0) s0 = score (p0) print 'initial score=', s0 try: _search (p0, 0, seen, limit, s0, best, path) except Stop: print 'Got it!' print 'best:' print ' ', p0, score (p0) path, p, s = best[0] p2 = p0 for name in path: p2 = move (p2, op_dict[name]) print name, score (p2), p2 print '-' * 80 print s, p print 'positions searched: %d' % (len(seen)) if __name__ == '__main__': import sys t0 = ('br', 'cr', 'bp', 'yi', 'cp', 'co', 'yo', 'gi', 'bi', 'yr', 'go', 'gp') if '-e' in sys.argv: sys.argv.remove ('-e') score = score_easy print 'scoring for easy version [corners, not faces]...' elif '-a' in sys.argv: sys.argv.remove ('-a') score = score_absolute else: score = score_hard print 'scoring for hard version [faces, not corners]...' a, b = 1, 0 if len(sys.argv) > 1: depth = int(sys.argv[1]) else: depth = 20 search (t0, depth)