I doubt this is the best way, but it is a way of doing this. If you think about your input as a matrix like this
a1, b1, c1, d1
a2, b2, c2, d2
a3, b3, c3, d3
a4, b4, c4, d4
Then you're goal becomes choosing an random index at each iteration such that the new index is not in the same row nor the same column of the matrix as the previous index and such that the new element has not been chosen before. Putting that into code naively, it becomes
import random
shapes_and_colors=["a1","a2","a3","a4","b1","b2","b3","b4","c1","c2","c3","c4","d1","d2","d3","d4"]
nRows = 4
nCols = 4
inds = [(x,y) for x in range(nRows) for y in range(nCols)]
def make_random(someArr):
toRet = []
n = len(someArr)
for i in range(n):
possible = [thing for thing in someArr if thing not in toRet]
prev = poss = None
while poss is None:
next_val = random.choice(possible)
if next_val == prev:
#failed so try again
return make_random(someArr)
if not toRet or (next_val[0] != toRet[-1][0] and next_val[1] != toRet[-1][1]):
poss = next_val
prev = next_val
toRet += poss,
return toRet
ans= [thing for thing in make_random(shapes_and_colors)]
print ans
Outputs After A Couple Runs
['c3', 'd4', 'c1', 'd3', 'b1', 'a4', 'b3', 'c4', 'a3', 'b2', 'a1', 'c2', 'd1', 'a2', 'b4', 'd2']
['d4', 'b3', 'c1', 'a4', 'b2', 'c4', 'd3', 'a1', 'c3', 'a2', 'b4', 'd2', 'a3', 'b1', 'c2', 'd1']
Disclaimer
Since this is a totally naive approach, it sometimes gets stuck! So suppose the last two indices remaining are [(2, 2), (3, 2)]. Then, there is no possible way for the algorithm to proceed without breaking the restrictions. Right now, I'm handling it with a recursive call, which isn't ideal.