CubeletInfo[,,] PerformRotationOnCubelets(CubeletInfo[,,] cubelets, FaceRotation rot, bool setParent = false)
    {
        var newCubelets = new CubeletInfo[3, 3, 3];

        for (int x = 0; x < 3; x++)
        {
            for (int y = 0; y < 3; y++)
            {
                for (int z = 0; z < 3; z++)
                {
                    if (x != 1 || y != 1 || z != 1)
                    {
                        if (rot.OnAxis(x, y, z))
                        {
                            if (setParent)
                            {
                                cubelets[x, y, z].Cubelet.parent = OnAxis;
                            }
                            var otherCubelet = cubelets[rot.MapX(x, y, z), rot.MapY(x, y, z), rot.MapZ(x, y, z)];
                            newCubelets[x, y, z] = new CubeletInfo(otherCubelet.Cubelet, rot.Rotation(90) * otherCubelet.Rotation);
                        }
                        else
                        {
                            newCubelets[x, y, z] = cubelets[x, y, z];
                        }
                    }
                }
            }
        }
        return(newCubelets);
    }
    IEnumerator WaitForSerialNumber()
    {
        yield return(null);

        var table      = @"L’,F’;D’,U’;U,B’;F,B;L,D;R’,U;U’,F;B’,L’;B,R;D,L;R,D’;F’,R’".Split(';').Select(row => row.Split(',').Select(str => _moves[str]).ToArray()).ToArray();
        var colorNames = "Yellow|Blue|Red|Green|Orange|White".Split('|');

        // Find a combination of colors that generates a non-trivial solution
        int retries = 10;

retry:

        var colors = Enumerable.Range(0, 6).ToArray().Shuffle();
        var columnShifts = newArray(colors[0] + 1, colors[1] + 1, colors[2] + 1);
        var serialIgnore = colors[3];
        var colR         = colors[4]; // color of the R (right) face

        var ser    = Bomb.GetSerialNumber().Remove(serialIgnore, 1);
        var rows   = ser.Select(ch => ch >= '0' && ch <= '9' ? ch - '0' : ch - 'A' + 10).Select(n => (n / 3 + columnShifts[n % 3]) % table.Length).ToArray();
        var moves1 = (colR >= 1 && colR <= 3)
            ? rows.SelectMany(r => table[r]).ToList()
            : rows.Select(r => table[r][0]).Concat(rows.Select(r => table[r][1])).ToList();

        var moves2 = moves1.ToList();

        switch (colR)
        {
        case 2:     // red
        case 0:     // yellow
            for (int i = 0; i < 5; i++)
            {
                moves2[i] = moves2[i].Reverse;
            }
            break;

        case 3:     // green
        case 5:     // white
            for (int i = 0; i < 5; i++)
            {
                var t = moves2[i];
                moves2[i]     = moves2[9 - i];
                moves2[9 - i] = t;
            }
            break;
        }

        // Now try to minimize the sequence
        var ix = 0;

        _solveMoves.Clear();
        _solveMoves.AddRange(moves2);
        while (ix < _solveMoves.Count)
        {
            var n        = 1;
            var affected = new List <int>();
            for (int j = ix + 1; j < _solveMoves.Count; j++)
            {
                if (_solveMoves[j] == _solveMoves[ix])
                {
                    n++;
                    affected.Add(j);
                }
                else if (_solveMoves[j] == _solveMoves[ix].Reverse)
                {
                    n--;
                    affected.Add(j);
                }
                else if (!_solveMoves[ix].OppositeSide.Contains(_solveMoves[j]))
                {
                    break;
                }
            }

            switch ((n % 4 + 4) % 4)
            {
            case 0:
                // the moves cancel each other out completely.
                for (int k = affected.Count - 1; k >= 0; k--)
                {
                    _solveMoves.RemoveAt(affected[k]);
                }
                _solveMoves.RemoveAt(ix);
                ix = 0;
                continue;

            case 3:
                // e.g. 3 of the same move ⇒ reverse move
                for (int k = affected.Count - 1; k >= 0; k--)
                {
                    _solveMoves.RemoveAt(affected[k]);
                }
                _solveMoves[ix] = _solveMoves[ix].Reverse;
                ix = 0;
                continue;
            }

            ix++;
        }

        // At this point, if the sequence is shorter than 8 moves, we want to generate a different sequence of colors.
        if (_solveMoves.Count < 8 && retries-- > 0)
        {
            goto retry;
        }

        // Set all the stickers to the desired colors and populate the _cubelets array
        _cubelets = new CubeletInfo[3, 3, 3];
        for (int x = 0; x < 3; x++)
        {
            for (int y = 0; y < 3; y++)
            {
                for (int z = 0; z < 3; z++)
                {
                    if (x != 1 || y != 1 || z != 1)
                    {
                        _cubelets[x, y, z] = new CubeletInfo(OffAxis.Find(string.Format("Cubelet ({0}, {1}, {2})", x, y, z)), Quaternion.identity);
                        for (int i = 0; i < _faces.Length; i++)
                        {
                            var sticker = _cubelets[x, y, z].Cubelet.Find(string.Format("{0} sticker", _faces[i]));
                            if (sticker != null)
                            {
                                sticker.GetComponent <MeshRenderer>().material = StickerMaterials[colors[i]];
                            }
                        }
                    }
                }
            }
        }

        _colorblind = ColorblindMode.ColorblindModeActive;
        if (_colorblind)
        {
            SetColorblindMode();
        }
        for (var i = 0; i < ColorblindTexts.Length; i++)
        {
            ColorblindTexts[i].text = colorNames[colors[i / 2]].Substring(0, 1);
        }

        Debug.LogFormat("[Rubik's Cube #{0}] Face colors: {1}", _moduleId, string.Join(", ", new[] { 0, 1, 2, 4, 3 }.Select(i => string.Format("{0}={1}", _faces[i], colorNames[colors[i]])).ToArray()));
        Debug.LogFormat("[Rubik's Cube #{0}] Column shifts: U={1}, L={2}, F={3}", _moduleId, columnShifts[0], columnShifts[1], columnShifts[2]);
        Debug.LogFormat("[Rubik's Cube #{0}] Ignoring serial number character #{1}: {2}", _moduleId, serialIgnore + 1, string.Join(", ", rows.Select((r, rIx) => string.Format("{0}={1}/{2}", ser[rIx], table[r][0].Name, table[r][1].Name)).ToArray()));
        if (colR >= 1 && colR <= 3)
        {
            Debug.LogFormat("[Rubik's Cube #{0}] R face is red/green/blue. Moves now: {1}", _moduleId, string.Join(" ", moves1.Select(m => m.Name).ToArray()));
        }
        else
        {
            Debug.LogFormat("[Rubik's Cube #{0}] R face is NOT red/green/blue. Moves now: {1}", _moduleId, string.Join(" ", moves1.Select(m => m.Name).ToArray()));
        }
        if (colR == 0 || colR == 2)
        {
            Debug.LogFormat("[Rubik's Cube #{0}] R face is red/yellow: change the first five moves to their opposites.", _moduleId);
        }
        else if (colR == 3 || colR == 5)
        {
            Debug.LogFormat("[Rubik's Cube #{0}] R face is green/white: reverse the order of all the moves.", _moduleId);
        }
        Debug.LogFormat("[Rubik's Cube #{0}] Solution: {1}", _moduleId, string.Join(" ", moves2.Select(m => m.Name).ToArray()));
        Debug.LogFormat("[Rubik's Cube #{0}] Minimized solution: {1}", _moduleId, string.Join(" ", _solveMoves.Select(m => m.Name).ToArray()));

        _cubeletsSolved = new Transform[3, 3, 3];
        for (int x = 0; x < 3; x++)
        {
            for (int y = 0; y < 3; y++)
            {
                for (int z = 0; z < 3; z++)
                {
                    if (x != 1 || y != 1 || z != 1)
                    {
                        _cubeletsSolved[x, y, z] = _cubelets[x, y, z].Cubelet;
                    }
                }
            }
        }

        Module.OnActivate += delegate
        {
            var pusherMoveInfos = @"
                B  = F002 C002 E311 B311 
                B’ = B111 E111 C022 F022 
                D  = B211 H211 A201 D201 
                D’ = D001 A001 H011 B011 
                F  = H111 K111 I022 L022 
                F’ = L002 I002 K311 H311 
                L  = C032 I032 G301 A301 
                L’ = A101 G101 I012 C012 
                R  = D101 J101 L012 F012 
                R’ = F032 L032 J301 D301 
                U  = J001 G001 K011 E011 
                U’ = E211 K211 G201 J201 
            "
                                  .Split('\n')
                                  .Select(s => s.Trim())
                                  .Where(s => s.Length > 1)
                                  .Select(s => s.Split('='))
                                  .Select(inf => new
            {
                Move      = _moves[inf[0].Trim()],
                Pushers   = inf[1].Trim().Split(' ').Select(str => Pushers[str[0] - 'A']).ToArray(),
                Rotations = inf[1].Trim().Split(' ').Select(str => new Vector3((str[1] - '0') * 90, (str[2] - '0') * 90, (str[3] - '0') * 90)).ToArray()
            })
                                  .ToArray();

            for (int pix = 0; pix < Pushers.Length; pix++)
            {
                SetPusherEvents(new Pusher(
                                    selectable: Pushers[pix],
                                    moves: pusherMoveInfos.Where(inf => inf.Pushers.Contains(Pushers[pix])).Select(inf => inf.Move).ToArray(),
                                    moveIndexes: pusherMoveInfos.Where(inf => inf.Pushers.Contains(Pushers[pix])).Select(inf => Array.IndexOf(inf.Pushers, Pushers[pix])).ToArray(),
                                    localPos: pix % 3 == 0 ? new Vector3(3.01f, 4 * (pix / 9) - 2, 4 * ((pix / 3) % 3) - 2) : pix % 3 == 1 ? new Vector3(4 * (pix / 9) - 2, 4 * ((pix / 3) % 3) - 2, -3.01f) : new Vector3(4 * (pix / 9) - 2, 3.01f, 4 * ((pix / 3) % 3) - 2),
                                    localAngles: pusherMoveInfos.Where(inf => inf.Pushers.Contains(Pushers[pix])).Select(inf => inf.Rotations[Array.IndexOf(inf.Pushers, Pushers[pix])]).ToArray()
                                    ));
            }

            if (!_mysteryHidden)
            {
                PerformInitialRotations();
            }

            Bomb.OnBombExploded += delegate
            {
                if (!_isSolved && _performedMoves.Count > 0)
                {
                    Debug.LogFormat("[Rubik's Cube #{0}] Moves performed before bomb exploded: {1}", _moduleId, string.Join(" ", _performedMoves.Reverse().Select(m => m.Name).ToArray()));
                }
            };

            Reset.OnInteract += delegate
            {
                Reset.AddInteractionPunch();
                Audio.PlayGameSoundAtTransform(KMSoundOverride.SoundEffect.ButtonPress, Reset.transform);
                if (_isSolved || _performedMoves.Count == 0)
                {
                    return(false);
                }
                Debug.LogFormat("[Rubik's Cube #{0}] Moves performed before reset: {1}", _moduleId, string.Join(" ", _performedMoves.Reverse().Select(m => m.Name).ToArray()));
                _queue.Enqueue(_resetSpeed);
                while (_performedMoves.Count > 0)
                {
                    _queue.Enqueue(_performedMoves.Pop().Reverse);
                }
                _queue.Enqueue(_normalRotationSpeed);
                return(false);
            };

            MainSelectable.OnCancel += delegate
            {
                if (_selectedPusher != null)
                {
                    _selectedPusher.MeshRenderer.enabled = false;
                    _selectedPusher = null;
                }
                return(true);
            };
        };
    }