Cycling with Rubik's

Pyth, 66 63 bytes

l.uum.rW}Hdd@_sm_B.iFP.>c3Zk3xZHG_r_Xz\'\39Nf!s}RTcZ2y=Z"UDLRFB

Try it online: Demonstration or Test Suite. Notice that the program is kinda slow and the online compiler is not able to compute the answer for RU2D'BD'. But be assured, that it can compute it on my laptop in about 12 seconds.

The program (accidentally) also accepts 2 for double moves.

Full Explanation:

Parse scramble:

First I'll deal with the prime marks ' in the input strings. I simply replace these with 3 and run-length decode this string. Since Pyth's decoding format requires the number in front of the char, I reverse the string beforehand. _r_Xz\'\39. So afterwards I reverse it back.

Describe the solved cube state:

=Z"UDLRFB assigns the string with all 6 moves to Z.

We can describe a cube state by describing the location for each cube piece. For instance we can say that the edge, that should be at UL (Up-Left) is currently at FR (Front-Right). For this I need to generate all pieces of the solved cube: f!s}RTcZ2yZ. yZ generates all possible subsets of "UDLRFB". This obviously also generates the subset "UDLRFB" and the subset "UD". The first one doesn't make any sense, since there is no piece that is visible from all 6 sides, and the second one doesn't make any sense, since there is no edge piece, that is visible from the top and the bottom. Therefore I remove all the subsets, that contain the subsequence "UD", "LR" or "FB". This gives me the following 27 pieces:

'', 'U', 'D', 'L', 'R', 'F', 'B', 'UL', 'UR', 'UF', 'UB', 'DL', 'DR', 'DF', 'DB', 
'LF', 'LB', 'RF', 'RB', 'ULF', 'ULB', 'URF', 'URB', 'DLF', 'DLB', 'DRF', 'DRB'

This also includes the empty string and all the six 1-letter strings. We could interpret them as the piece in the middle of the cube and the 6 center pieces. Obviously they are not required (since they don't move), but I'll keep them.

Doing some moves:

I'll do some string translations to perform a move. To visualize the idea look at the corner piece in URF. What happens to it, when I do an R move? The sticker on the U face moves to the B face, the sticker F moves to the U face and the sticker on the R face stays at the R face. We can say, that the piece at URF moves to the position BRU. This pattern is true for all the pieces on the right side. Every sticker that is on the F face moves to the U face when an R move is performed, every sticker that is on the U face moves to the B face, every sticker on the B moves to D and every sticker on D moves to F. We can decode the changes of an R move as FUBD.

The following code generates all the 6 necessary codes:

_sm_B.iFP.>c3Zk3
['BRFL', 'LFRB', 'DBUF', 'FUBD', 'RDLU', 'ULDR']
    ^       ^       ^       ^       ^       ^
 U move  D move  L move  R move  F move  B move

And we perform a move H to the cube state G as followed:

m.rW}[email protected]
m              G   map each piece d in G to:
 .rW   d              perform a rotated translation to d, but only if:
    }Hd                  H appears in d (d is currently on the face H)
            xZH           get the index of H in Z
        @...              and choose the code in the list of 6 (see above)

Count the number of repeats:

The rest is pretty much trivial. I simply perform the input scramble to the solved cube over and over until I reach a position that I previously visited.

l.uu<apply move H to G><parsed scramble>N<solved state>
u...N   performs all moves of the scramble to the state N
.u...   do this until cycle detected, this returns all intermediate states
l       print the length

GAP, 792 783 782 749 650 Bytes

This seems to be working. If it messes up with something let me know.

Thanks to @Lynn for suggesting that I decompose some of the primitive moves.

Thanks to @Neil for suggesting that instead of Inverse(X) I use X^3.

Usage example: f("R");

R:=(3,39,21,48)(6,42,24,51)(9,45,27,54)(10,12,18,16)(13,11,15,17);L:=(1,46,19,37)(4,49,22,40)(7,52,25,43)(30,36,34,28)(29,33,35,31);U:=(1,10,27,28)(2,11,26,29)(3,12,25,30)(37,43,45,39)(40,44,42,38);A:=R*L^3*F*F*B*B*R*L^3;D:=A*U*A;;F:=(1,3,9,7)(2,6,8,4)(10,48,36,43)(13,47,33,44)(16,46,30,45);B:=(27,25,19,21)(26,22,20,24)(39,28,52,18)(38,31,53,15)(37,34,54,12);d:=NewDictionary((),true);AddDictionary(d,'R',R);AddDictionary(d,'L',L);AddDictionary(d,'U',U);AddDictionary(d,'D',D);AddDictionary(d,'F',F);AddDictionary(d,'B',B);f:=function(s) local i,p,b,t;p:=();
for c in s do if c='\'' then t:=t^2;else t:=LookupDictionary(d,c);fi;p:=p*t;od;return Order(p);end;

Here is the ungolfed code with a bit of explanation

  # Here we define the primitive moves
R:=(3,39,21,48)(6,42,24,51)(9,45,27,54)(10,12,18,16)(13,11,15,17);
L:=(1,46,19,37)(4,49,22,40)(7,52,25,43)(30,36,34,28)(29,33,35,31);
U:=(1,10,27,28)(2,11,26,29)(3,12,25,30)(37,43,45,39)(40,44,42,38);
#D:=(7,34,21,16)(8,35,20,17)(9,36,19,18)(48,46,52,54)(47,49,53,51);
F:=(1,3,9,7)(2,6,8,4)(10,48,36,43)(13,47,33,44)(16,46,30,45);
B:=(27,25,19,21)(26,22,20,24)(39,28,52,18)(38,31,53,15)(37,34,54,12);

# Here we define D in terms of other primitive moves, saving on bytes
# Thanks @Lynn
# This is actually doable with a maximum of 3 of the primitive moves
# if a short enough sequence can be found.
D:=U^(R*L^3*F*F*B*B*R*L^3);

# create dictionary and add moves to it with appropriate char labels
d:=NewDictionary((),true);
AddDictionary(d,'R',R);
AddDictionary(d,'L',L);
AddDictionary(d,'U',U);
AddDictionary(d,'D',D);
AddDictionary(d,'F',F);
AddDictionary(d,'B',B);

f:=function(s)
    local c,p,t;

    # p will become the actual permutation passed to the function
    p:=();
 
    for c in s do
        if c='\'' then
            # The last generator we mutiplied (that we still have in t)
            # should have been its inverse. Compensate by preparing to
            # multiply it two more times to get t^3=t^-1. Thanks @Neil.
            t:=t^2;
        else
            t:=LookupDictionary(d,c);
        fi;
        p:=p*t;
    od;

    return Order(p);

end;

Mathematica, 413 401 bytes

Evaluate[f/@Characters@"RFLBUD"]=LetterNumber@"ABFEJNRMDAEHIMQPCDHGLPTOBCGFKOSNADCBILKJEFGHQRST"~ArrayReshape~{6,2,4};
r[c_,l_]:=(b=Permute[c,Cycles@f@l];MapThread[(b[[#,2]]=Mod[b[[#,2]]+{"F","B","L","R"}~Count~l{-1,1,-1,1},#2])&,{f@l,{3,2}}];b);
p@s_:=Length[c={#,0}&~Array~20;NestWhileList[Fold[r,#,Join@@StringCases[s,x_~~t:""|"'":>Table[x,3-2Boole[t==""]]]]&,c,(Length@{##}<2||c!=Last@{##})&,All]]-1

Explanations

A Rubik's Cube is made up with 20 movable cubies (8 corners, 12 edges). Each cubie can be given a number:

corners:

N   starting position
1     UFR
2     UBR
3     UBL
4     UFL
5     DFR
6     DBR
7     DBL
8     DFL

edges:

N   starting position
9     UF
10    UR
11    UB
12    UL
13    FR
14    BR
15    BL
16    FL
17    DF
18    DR
19    DB
20    DL

Note that when the cube is twisted, the cubies are generally not on their starting positions any longer. For example, when R is done, the cubie 1 moves from UFR to a new position UBR.

In such notation, a 90 degree turn can be described by 8 movements of cubies. For example, R is described by

from  to
UFR   UBR
UBR   DBR
DBR   DFR
DFR   UFR
UR    BR
BR    DR
DR    FR
FR    UR

Since each cubie has a unique starting position, each position has a unique starting cubie. That is to say, rule UFR->UBR is just 1->2 (means that R takes the cubie on the starting position of cubie 1 to the starting position of cubie 2). Thus, R can be simplified further to a cycle

Cycles[{{1,2,6,5}, {10,14,18,13}}]

To fully solve a Rubik's Cube, we also need to align the cubies to their corresponding starting orientations. The faces of a cube is painted in different colors, the scheme that I often use when solving cubes is

face color
U    yellow
D    white
F    red
B    orange
R    green
L    blue

When we analyzing the orientations of corners, colors other than yellow or white are ignored, and yellow and white are considered as the same color.

Suppose cubie 1 is on its starting position UFR, the yellow facet may be aligned to three different faces. We use an integer to represent these cases,

0  yellow on U  (correct)
1  yellow on R  (120 degree clockwise)
2  yellow on F  (120 degree counterclockwise)

Suppose cubie 1 is on DFL, its three possible orientations are

0  yellow on D  (correct)
1  yellow on L  (120 degree clockwise)
2  yellow on F  (120 degree counterclockwise)

When we analyzing the orientations of edges, red and orange are ignored, and yellow and white are ignored only if the edge has a green or blue facet.

Suppose cubie 10 is on its starting position UR, the green facet may be aligned to two different faces. Its two possible orientations are

0  green on R  (correct)
1  green on U  (180 degree)

Suppose cubie 10 is on DF, its two possible orientations are

0  green on D  (correct)
1  green on F  (180 degree)

An array is used to store the state of a cube. The starting state of a cube is

{{1,0},{2,0},{3,0},{4,0},{5,0},{6,0},{7,0},{8,0},{9,0},{10,0},{11,0},{12,0},{13,0},{14,0},{15,0},{16,0},{17,0},{18,0},{19,0},{20,0}}

which means that every cubies are on their starting position with correct orientation.

After R, the state of the cube becomes

{{5,2},{1,1},{3,0},{4,0},{6,1},{2,2},{7,0},{8,0},{9,0},{13,1},{11,0},{12,0},{18,1},{10,1},{15,0},{16,0},{17,0},{14,1},{19,0},{20,0}}

which means that cubie 5 is now on position 1 (UFR) with orientation 2, cubie 1 is now on position 2 (UBR) with orientation 1, cubie 3 is now still on position 3 (UBL) with orientation 0, and so on.


Test cases

p["FF'"]            (* 1   *)
p["R"]              (* 4   *)
p["RUR'U'"]         (* 6   *)
p["LLUUFFUURRUU"]   (* 12  *)
p["LUFFRDRBF"]      (* 56  *)
p["LF"]             (* 105 *)
p["UFFR'DBBRL'"]    (* 120 *)
p["FRBL"]           (* 315 *)