Rubik's Cube Solver mit 3D-Darstellung

  • C#

Es gibt 85 Antworten in diesem Thema. Der letzte Beitrag () ist von Switcherlapp97.

    @StarGate01
    Darum habe ich ja geschrieben "Möglichkeit" :P
    Aber es wäre schon extrem geil, wenn das mit der Webcam realisierbar wäre ;)
    RubiksCubeSolver


    Jetzt im Showroom

    Rubik's Cube Solver - Schritt 1: Erstes Kreuz

    Hallo,

    Wie ich bereits geschrieben habe, arbeite ich derzeit an einem Rubiks Cube Solver. Jetzt habe ich es geschafft, dass mein Programm schon das erste Kreuz auf der Bottom-Ebene lösen kann :thumbsup:
    Ich habe die Bottom-Ebene bewusst als die Ebene für das erste Kreuz gewählt, da später die erste Seite des Cubes immer unten gehalten wird und ich so eine Drehung des kompletten Würfels vermeide.

    Vorgehensweise

    Vorbereitungen
    - die Farbe der unteren Fläche ermitteln
    - einen neuen RubikManager erstellen, der gelöst ist und mit dem man die Zielpositionen der einzelnen Steine herausfinden kann
    - die Flächen des neu erstellten RubikManager so wie die Mittelsteine des Eingabecubes färben.
    - alle Kanten, die eine Fläche in der Farbe der Bottom Ebene gefärbt haben in eine Liste hinzufügen

    Lösen der Kanten (Schritte, die für alle Kanten abgearbeitet werden müssen)
    - Zielposition ermitteln
    - die Kante auf die Top-Ebene drehen
    - die Kante über die Zielposition auf der Top-Ebene rotieren
    - die Kante an die Zielposition drehen
    - falls die Kante verkehrt herum an der Zielposition liegt mit dem Algorithmus Fi D Ri Di flippen

    :!: Als Grundlage habe ich das Programm VirtualRubik von @StarGate01 verwendet. Falls ihr den Code selbst testen wollt, downloadet euch die Version 1.2 und setzt den Code dort ein.

    Code
    Cube3D.cs

    Die isEdge-Methode gibt zurück, ob ein Cube mit der eingegebenen Koordinaten eine Kante ist oder nicht:

    C#-Quellcode

    1. public static Boolean isEdge(RubikPosition Position)
    2. {
    3. return ((Position == (RubikPosition.TopLayer | RubikPosition.FrontSlice | RubikPosition.MiddleSlice_Sides))
    4. || (Position == (RubikPosition.TopLayer | RubikPosition.BackSlice | RubikPosition.MiddleSlice_Sides))
    5. || (Position == (RubikPosition.TopLayer | RubikPosition.RightSlice | RubikPosition.MiddleSlice))
    6. || (Position == (RubikPosition.TopLayer | RubikPosition.LeftSlice | RubikPosition.MiddleSlice))
    7. || (Position == (RubikPosition.MiddleLayer | RubikPosition.FrontSlice | RubikPosition.RightSlice))
    8. || (Position == (RubikPosition.MiddleLayer | RubikPosition.FrontSlice | RubikPosition.LeftSlice))
    9. || (Position == (RubikPosition.MiddleLayer | RubikPosition.BackSlice | RubikPosition.RightSlice))
    10. || (Position == (RubikPosition.MiddleLayer | RubikPosition.BackSlice | RubikPosition.LeftSlice))
    11. || (Position == (RubikPosition.BottomLayer | RubikPosition.FrontSlice | RubikPosition.MiddleSlice_Sides))
    12. || (Position == (RubikPosition.BottomLayer | RubikPosition.BackSlice | RubikPosition.MiddleSlice_Sides))
    13. || (Position == (RubikPosition.BottomLayer | RubikPosition.RightSlice | RubikPosition.MiddleSlice))
    14. || (Position == (RubikPosition.BottomLayer | RubikPosition.LeftSlice | RubikPosition.MiddleSlice)));
    15. }


    RubikManager.cs

    Ich habe die Rotate90-Methode ein bisschen angepasst, damit das mit den Drehrichtungen stimmt:

    C#-Quellcode

    1. public void Rotate90(Cube3D.RubikPosition layer, bool direction, int steps)
    2. {
    3. if (!Rotating)
    4. {
    5. Rotating = true;
    6. rotationLayer = layer;
    7. rotationStep = (double)90 / (double)steps;
    8. rotationTarget = 90;
    9. if (direction && (layer == Cube3D.RubikPosition.BottomLayer || layer == Cube3D.RubikPosition.BackSlice || layer == Cube3D.RubikPosition.RightSlice || layer == Cube3D.RubikPosition.MiddleSlice_Sides) ||
    10. !direction && (layer == Cube3D.RubikPosition.TopLayer || layer == Cube3D.RubikPosition.FrontSlice || layer == Cube3D.RubikPosition.LeftSlice ||
    11. layer == Cube3D.RubikPosition.MiddleLayer || layer == Cube3D.RubikPosition.MiddleSlice))
    12. {
    13. rotationStep *= (-1);
    14. rotationTarget = -90;
    15. }
    16. }
    17. }

    Ich habe in der RubikManager noch eine Methode komplett neu erstellt. Es ist die getFaceColor-Methode, die von einem Cube an einer bestimmten Position die Farbe einer bestimmten Fläche zurückgibt.

    C#-Quellcode

    1. public Color getFaceColor(Cube3D.RubikPosition position, Face3D.FacePosition face)
    2. {
    3. return RubikCube.cubes.First(c => c.Position.HasFlag(position)).Faces.First(f => f.Position == face).Color;
    4. }


    CubeSolver.cs

    Für das Lösen des Cubes habe ich eine neue Klasse erstellt (CubeSolver). Hier der bisherige Code der Klasse:

    C#-Quellcode

    1. class CubeSolver
    2. {
    3. RubikManager Manager;
    4. private RubikManager standardCube = new RubikManager();
    5. public CubeSolver(RubikManager rubik)
    6. {
    7. Manager = rubik;
    8. }
    9. public Stack<LayerMove> SolveFirstCross()
    10. {
    11. List<LayerMove> Moves = new List<LayerMove>();
    12. //Step 1: Get the color of the bottom layer to start with the first cross
    13. Color bottomColor = Manager.getFaceColor(Cube3D.RubikPosition.BottomLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.MiddleSlice, Face3D.FacePosition.Bottom);
    14. //Step 2: Change colors of the faces
    15. standardCube.setFaceColor(Cube3D.RubikPosition.TopLayer, Face3D.FacePosition.Top,
    16. Manager.getFaceColor(Cube3D.RubikPosition.TopLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.MiddleSlice, Face3D.FacePosition.Top));
    17. standardCube.setFaceColor(Cube3D.RubikPosition.BottomLayer, Face3D.FacePosition.Bottom,
    18. Manager.getFaceColor(Cube3D.RubikPosition.BottomLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.MiddleSlice, Face3D.FacePosition.Bottom));
    19. standardCube.setFaceColor(Cube3D.RubikPosition.RightSlice, Face3D.FacePosition.Right,
    20. Manager.getFaceColor(Cube3D.RubikPosition.RightSlice | Cube3D.RubikPosition.MiddleSlice | Cube3D.RubikPosition.MiddleLayer, Face3D.FacePosition.Right));
    21. standardCube.setFaceColor(Cube3D.RubikPosition.LeftSlice, Face3D.FacePosition.Left,
    22. Manager.getFaceColor(Cube3D.RubikPosition.LeftSlice | Cube3D.RubikPosition.MiddleSlice | Cube3D.RubikPosition.MiddleLayer, Face3D.FacePosition.Left));
    23. standardCube.setFaceColor(Cube3D.RubikPosition.FrontSlice, Face3D.FacePosition.Front,
    24. Manager.getFaceColor(Cube3D.RubikPosition.MiddleLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.FrontSlice, Face3D.FacePosition.Front));
    25. standardCube.setFaceColor(Cube3D.RubikPosition.BackSlice, Face3D.FacePosition.Back,
    26. Manager.getFaceColor(Cube3D.RubikPosition.MiddleLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.BackSlice, Face3D.FacePosition.Back));
    27. //Step 3: Get edges with of the bottom face
    28. IEnumerable<Cube3D> whiteEdges = Manager.RubikCube.cubes.Where(c => Cube3D.isEdge(c.Position)).Where(c => c.Faces.Count(f => f.Color == bottomColor) == 1);
    29. //Step 4: Solve the first cross
    30. foreach (Cube3D c in whiteEdges)
    31. {
    32. //Step 4.1: Get the target position and the second color of the edge
    33. Cube3D.RubikPosition targetPosition = standardCube.RubikCube.cubes.First(cu => ScrambledEquals(cu.Colors, c.Colors)).Position;
    34. Color secondColor = c.Colors.First(co => co != bottomColor && co != Color.Black);
    35. //Step 4.2: Rotate to target position
    36. if (c.Position != targetPosition)
    37. {
    38. //Rotate to top layer
    39. Cube3D.RubikPosition layer = FacePosToCubePos(c.Faces.First(f => (f.Color == bottomColor || f.Color == secondColor)
    40. && f.Position != Face3D.FacePosition.Top && f.Position != Face3D.FacePosition.Bottom).Position);
    41. if (c.Position.HasFlag(Cube3D.RubikPosition.MiddleLayer))
    42. {
    43. Manager.Rotate90Sync(layer, true);
    44. if (RefreshCube(c).Position.HasFlag(Cube3D.RubikPosition.TopLayer))
    45. {
    46. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    47. Manager.Rotate90Sync(layer, false);
    48. }
    49. else
    50. {
    51. for (int i = 0; i < 2; i++) Manager.Rotate90Sync(layer, false);
    52. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    53. Manager.Rotate90Sync(layer, true);
    54. }
    55. }
    56. if (c.Position.HasFlag(Cube3D.RubikPosition.BottomLayer)) for (int i = 0; i < 2; i++) Manager.Rotate90Sync(layer, true);
    57. //Rotate over target position
    58. Cube3D.RubikPosition targetLayer = FacePosToCubePos(standardCube.RubikCube.cubes.First(cu => ScrambledEquals(cu.Colors, c.Colors))
    59. .Faces.First(f => f.Color == secondColor).Position);
    60. while (!RefreshCube(c).Position.HasFlag(targetLayer)) Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    61. //Rotate to target position
    62. for (int i = 0; i < 2; i++) Manager.Rotate90Sync(targetLayer, true);
    63. }
    64. //Step 4.3: Flip the incorrect orientated edges with the algorithm: Fi D Ri Di
    65. if (c.Faces.First(f => f.Position == Face3D.FacePosition.Bottom).Color != bottomColor)
    66. {
    67. Cube3D.RubikPosition frontLayer = FacePosToCubePos(c.Faces.First(f => f.Color == bottomColor).Position);
    68. Manager.Rotate90Sync(frontLayer, false);
    69. Manager.Rotate90Sync(Cube3D.RubikPosition.BottomLayer, true);
    70. Cube3D.RubikPosition rightSlice = FacePosToCubePos(RefreshCube(c).Faces.First(f => f.Color == secondColor).Position);
    71. Manager.Rotate90Sync(rightSlice, false);
    72. Manager.Rotate90Sync(Cube3D.RubikPosition.BottomLayer, false);
    73. }
    74. }
    75. Moves.Reverse();
    76. return new Stack<LayerMove>(Moves);
    77. }
    78. private Cube3D.RubikPosition FacePosToCubePos(Face3D.FacePosition position)
    79. {
    80. switch (position)
    81. {
    82. case Face3D.FacePosition.Top:
    83. return Cube3D.RubikPosition.TopLayer;
    84. case Face3D.FacePosition.Bottom:
    85. return Cube3D.RubikPosition.BottomLayer;
    86. case Face3D.FacePosition.Left:
    87. return Cube3D.RubikPosition.LeftSlice;
    88. case Face3D.FacePosition.Right:
    89. return Cube3D.RubikPosition.RightSlice;
    90. case Face3D.FacePosition.Back:
    91. return Cube3D.RubikPosition.BackSlice;
    92. case Face3D.FacePosition.Front:
    93. return Cube3D.RubikPosition.FrontSlice;
    94. default:
    95. return Cube3D.RubikPosition.None;
    96. }
    97. }
    98. private Cube3D RefreshCube(Cube3D cube)
    99. {
    100. return Manager.RubikCube.cubes.First(cu => ScrambledEquals(cu.Colors, cube.Colors));
    101. }
    102. public static bool ScrambledEquals<T>(IEnumerable<T> list1, IEnumerable<T> list2)
    103. {
    104. var cnt = new Dictionary<T, int>();
    105. foreach (T s in list1)
    106. {
    107. if (cnt.ContainsKey(s))
    108. {
    109. cnt[s]++;
    110. }
    111. else
    112. {
    113. cnt.Add(s, 1);
    114. }
    115. }
    116. foreach (T s in list2)
    117. {
    118. if (cnt.ContainsKey(s))
    119. {
    120. cnt[s]--;
    121. }
    122. else
    123. {
    124. return false;
    125. }
    126. }
    127. return cnt.Values.All(c => c == 0);
    128. }
    129. }


    Form1.cs

    In der Form habe ich dann einen neuen Button erstellt (button7) und mit einem Klick auf diesen Button wird das erste Kreuz gelöst:

    C#-Quellcode

    1. private void button7_Click(object sender, EventArgs e)
    2. {
    3. CubeSolver cs = new CubeSolver(rubikManager);
    4. moveStack = cs.SolveFirstCross();
    5. }


    Ich verstehe allerdings nicht, wieso die 3D-Darstellung sich immer gleich mitdreht wenn ich die Rotate90Sync in der CubeSolver-Klasse aufrufe. Wie kann ich das noch verändern, dass der Cube in der Klasse unabhängig vom Cube in der 3D-Darstellung gedreht werden kann?

    Danke
    Switcherlapp97
    Dateien
    RubiksCubeSolver


    Jetzt im Showroom

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Switcherlapp97“ ()

    Nice! Ich sehs mir gerade durch.
    Aber ich hab einen Tipp schon mal so: nimm bitte die Version 1.3 von VirtualRubik als Basis.
    Das ist das vorerst letzte Update wo zentrale Funktionen geändert wurden. Die nächsten Updates werden nicht mehr so tief eingreifen.
    Außerdem sind in der 1.3 ein paar nervige Bugs gefixed, und die neue UI dürfte Dir auch hilfreich sein :)
    Wenn Du willst, nehme ich Dein Programm auch als "offiziellen" Fork in mein Topic auf.

    Zu Deiner Frage:
    Du müsstest wohl zwei Instanzen des Würfels erstellen, eine zum Rendern und eine zum rumdrehen.
    SᴛᴀʀGᴀᴛᴇ01
    Gut dann werde ich nun die Version 1.3 als Grundlage für mein Projekt verwenden ;)

    StarGate01 schrieb:

    Wenn Du willst, nehme ich Dein Programm auch als "offiziellen" Fork in mein Topic auf.

    Ich habe nichts dagegen :D

    Ich wäre allerdings froh, wenn du mir das mit den zwei Instanzen genauer erklären könntest :S
    RubiksCubeSolver


    Jetzt im Showroom
    Hallo,

    Nun löst mein Code schon die komplette untere Ebene. Den Code und weitere Informationen zum RubiksCubeSolver Projekt findet ihr auf GitHub :)

    Gruß
    Switcherlapp97
    RubiksCubeSolver


    Jetzt im Showroom
    Hallo,

    Ich habe es nun geschafft den Cube mit Hilfe von Code zu lösen. Für das Lösen verwende ich aktuell die Anfängermethode (Projekt-Download von GitHub). Nach dem Fertigstellen des Codes habe ich 1000x mit 50 Moves verdrehte Würfel von meinem Programm lösen lassen und die Anzahl der benötigten Moves sowie die benötigte Zeit in eine CSV-Datei geschrieben und dann in Excel ein paar Werte ausgerechnet. Hier das Ergebnis:

    Zeit
    Durchschnittlich benötigte Zeit: 475ms
    Kürzeste Zeit: 270ms
    Längste Zeit: 880ms

    Moves
    Durchschnittlich benötigte Moves: 290
    Geringste Anzahl an Moves: 190
    Größte Anzahl an Moves: 366

    Die Werte der benötigten Moves sind für mich noch nicht zufriedenstellend, wenn man bedenkt, dass jeder Rubiks Cube in maximal 20 Zügen gelöst werden kann. Diese Zahl werde ich sicher nicht erreichen, da bei 20 Zügen die absolut schnellste Lösung berechnet wird, die nicht wirklich auf einen Lösealgorithmus zurückgeht. Ich möchte die durchschnittliche Anzahl der Moves auf jeden Fall wenn möglich noch ordentlich reduzieren.

    CubeSolver.cs

    Quellcode

    1. using System;
    2. using System.Collections.Generic;
    3. using System.Linq;
    4. using System.Text;
    5. using System.Drawing;
    6. namespace VirtualRubik
    7. {
    8. class CubeSolver
    9. {
    10. RubikManager Manager;
    11. private RubikManager standardCube = new RubikManager();
    12. public void Solve()
    13. {
    14. SolveFirstCross();
    15. CompleteFirstLayer();
    16. CompleteMiddleLayer();
    17. SolveCrossTopLayer();
    18. CompleteLastLayer();
    19. }
    20. public CubeSolver(RubikManager rubik)
    21. {
    22. Manager = rubik;
    23. //Change colors of the faces
    24. standardCube.setFaceColor(Cube3D.RubikPosition.TopLayer, Face3D.FacePosition.Top,
    25. Manager.getFaceColor(Cube3D.RubikPosition.TopLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.MiddleSlice, Face3D.FacePosition.Top));
    26. standardCube.setFaceColor(Cube3D.RubikPosition.BottomLayer, Face3D.FacePosition.Bottom,
    27. Manager.getFaceColor(Cube3D.RubikPosition.BottomLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.MiddleSlice, Face3D.FacePosition.Bottom));
    28. standardCube.setFaceColor(Cube3D.RubikPosition.RightSlice, Face3D.FacePosition.Right,
    29. Manager.getFaceColor(Cube3D.RubikPosition.RightSlice | Cube3D.RubikPosition.MiddleSlice | Cube3D.RubikPosition.MiddleLayer, Face3D.FacePosition.Right));
    30. standardCube.setFaceColor(Cube3D.RubikPosition.LeftSlice, Face3D.FacePosition.Left,
    31. Manager.getFaceColor(Cube3D.RubikPosition.LeftSlice | Cube3D.RubikPosition.MiddleSlice | Cube3D.RubikPosition.MiddleLayer, Face3D.FacePosition.Left));
    32. standardCube.setFaceColor(Cube3D.RubikPosition.FrontSlice, Face3D.FacePosition.Front,
    33. Manager.getFaceColor(Cube3D.RubikPosition.MiddleLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.FrontSlice, Face3D.FacePosition.Front));
    34. standardCube.setFaceColor(Cube3D.RubikPosition.BackSlice, Face3D.FacePosition.Back,
    35. Manager.getFaceColor(Cube3D.RubikPosition.MiddleLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.BackSlice, Face3D.FacePosition.Back));
    36. }
    37. //Solve the first cross on the bottom layer
    38. private void SolveFirstCross()
    39. {
    40. int Moves = 0;
    41. //Step 1: Get the color of the bottom layer to start with the first cross
    42. Color bottomColor = Manager.getFaceColor(Cube3D.RubikPosition.BottomLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.MiddleSlice, Face3D.FacePosition.Bottom);
    43. //Step 3: Get edges with the color of the bottom face
    44. IEnumerable<Cube3D> bottomEdges = Manager.RubikCube.cubes.Where(c => Cube3D.isEdge(c.Position)).Where(c => c.Colors.Count(co => co == bottomColor) == 1);
    45. //Step 4: Solve the first cross
    46. foreach (Cube3D c in bottomEdges)
    47. {
    48. //Step 4.1: Get the target position and the second color of the edge
    49. Cube3D.RubikPosition targetPosition = standardCube.RubikCube.cubes.First(cu => ScrambledEquals(cu.Colors, c.Colors)).Position;
    50. Color secondColor = c.Colors.First(co => co != bottomColor && co != Color.Black);
    51. //Step 4.2: Rotate to target position
    52. if (c.Position != targetPosition)
    53. {
    54. //Rotate to top layer
    55. Cube3D.RubikPosition layer = FacePosToCubePos(c.Faces.First(f => (f.Color == bottomColor || f.Color == secondColor)
    56. && f.Position != Face3D.FacePosition.Top && f.Position != Face3D.FacePosition.Bottom).Position);
    57. if (c.Position.HasFlag(Cube3D.RubikPosition.MiddleLayer))
    58. {
    59. Moves++;
    60. Manager.Rotate90Sync(layer, true);
    61. if (RefreshCube(c).Position.HasFlag(Cube3D.RubikPosition.TopLayer))
    62. {
    63. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    64. Manager.Rotate90Sync(layer, false);
    65. }
    66. else
    67. {
    68. for (int i = 0; i < 2; i++) Manager.Rotate90Sync(layer, true);
    69. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    70. Manager.Rotate90Sync(layer, true);
    71. }
    72. }
    73. if (c.Position.HasFlag(Cube3D.RubikPosition.BottomLayer)) for (int i = 0; i < 2; i++) Manager.Rotate90Sync(layer, true);
    74. //Rotate over target position
    75. Cube3D.RubikPosition targetLayer = FacePosToCubePos(standardCube.RubikCube.cubes.First(cu => ScrambledEquals(cu.Colors, c.Colors))
    76. .Faces.First(f => f.Color == secondColor).Position);
    77. while (!RefreshCube(c).Position.HasFlag(targetLayer)) Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    78. //Rotate to target position
    79. for (int i = 0; i < 2; i++) Manager.Rotate90Sync(targetLayer, true);
    80. }
    81. //Step 4.3: Flip the incorrect orientated edges with the algorithm: Fi D Ri Di
    82. if (c.Faces.First(f => f.Position == Face3D.FacePosition.Bottom).Color != bottomColor)
    83. {
    84. Cube3D.RubikPosition frontLayer = FacePosToCubePos(RefreshCube(c).Faces.First(f => f.Color == bottomColor).Position);
    85. Manager.Rotate90Sync(frontLayer, false);
    86. Manager.Rotate90Sync(Cube3D.RubikPosition.BottomLayer, true);
    87. Cube3D.RubikPosition rightSlice = FacePosToCubePos(RefreshCube(c).Faces.First(f => f.Color == secondColor).Position);
    88. Manager.Rotate90Sync(rightSlice, false);
    89. Manager.Rotate90Sync(Cube3D.RubikPosition.BottomLayer, false);
    90. }
    91. }
    92. }
    93. private void CompleteFirstLayer()
    94. {
    95. //Step 1: Get the color of the bottom layer
    96. Color bottomColor = Manager.getFaceColor(Cube3D.RubikPosition.BottomLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.MiddleSlice, Face3D.FacePosition.Bottom);
    97. //Step 2: Get corners with the color of the bottom face
    98. IEnumerable<Cube3D> bottomCorners = Manager.RubikCube.cubes.Where(c => Cube3D.isCorner(c.Position)).Where(c => c.Colors.Count(co => co == bottomColor) == 1);
    99. //Step 3: Complete the first layer
    100. foreach (Cube3D c in bottomCorners)
    101. {
    102. //3.1 Get the target position
    103. Cube3D.RubikPosition targetPosition = standardCube.RubikCube.cubes.First(cu => ScrambledEquals(cu.Colors, c.Colors)).Position;
    104. //3.2 Rotate to target position
    105. if (c.Position != targetPosition)
    106. {
    107. //Rotate to top layer
    108. if (c.Position.HasFlag(Cube3D.RubikPosition.BottomLayer))
    109. {
    110. Face3D leftFace = RefreshCube(c).Faces.First(f => f.Position != Face3D.FacePosition.Bottom && f.Color != Color.Black);
    111. Cube3D.RubikPosition leftSlice = FacePosToCubePos(leftFace.Position);
    112. Manager.Rotate90Sync(leftSlice, false);
    113. if (RefreshCube(c).Position.HasFlag(Cube3D.RubikPosition.BottomLayer))
    114. {
    115. Manager.Rotate90Sync(leftSlice, true);
    116. leftFace = RefreshCube(c).Faces.First(f => f.Position != Face3D.FacePosition.Bottom && f.Color != leftFace.Color && f.Color != Color.Black);
    117. leftSlice = FacePosToCubePos(leftFace.Position);
    118. Manager.Rotate90Sync(leftSlice, false);
    119. }
    120. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    121. Manager.Rotate90Sync(leftSlice, true);
    122. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    123. }
    124. //Rotate over target position
    125. Cube3D.RubikPosition targetPos = Cube3D.RubikPosition.None;
    126. foreach (Cube3D.RubikPosition p in GetFlags(targetPosition))
    127. {
    128. if (p != Cube3D.RubikPosition.BottomLayer)
    129. targetPos |= p;
    130. }
    131. while (!RefreshCube(c).Position.HasFlag(targetPos)) Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    132. }
    133. //Rotate to target position with the algorithm: Li Ui L U
    134. Face3D leftFac = RefreshCube(c).Faces.First(f => f.Position != Face3D.FacePosition.Top && f.Position != Face3D.FacePosition.Bottom && f.Color != Color.Black);
    135. Cube3D.RubikPosition leftSlic = FacePosToCubePos(leftFac.Position);
    136. Manager.Rotate90Sync(leftSlic, false);
    137. if (!RefreshCube(c).Position.HasFlag(Cube3D.RubikPosition.TopLayer))
    138. {
    139. Manager.Rotate90Sync(leftSlic, true);
    140. leftFac = RefreshCube(c).Faces.First(f => f.Position != Face3D.FacePosition.Top && f.Position != Face3D.FacePosition.Bottom && f.Color != leftFac.Color && f.Color != Color.Black);
    141. leftSlic = FacePosToCubePos(leftFac.Position);
    142. }
    143. else Manager.Rotate90Sync(leftSlic, true);
    144. while (RefreshCube(c).Faces.First(f => f.Color == bottomColor).Position != Face3D.FacePosition.Bottom)
    145. {
    146. Manager.Rotate90Sync(leftSlic, false);
    147. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    148. Manager.Rotate90Sync(leftSlic, true);
    149. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    150. }
    151. }
    152. }
    153. private void CompleteMiddleLayer()
    154. {
    155. //Step 1: Get the color of the bottom and top layer
    156. Color bottomColor = Manager.getFaceColor(Cube3D.RubikPosition.BottomLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.MiddleSlice, Face3D.FacePosition.Bottom);
    157. Color topColor = Manager.getFaceColor(Cube3D.RubikPosition.TopLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.MiddleSlice, Face3D.FacePosition.Top);
    158. //Step 2: Get the egdes of the middle layer
    159. IEnumerable<Cube3D> middleEdges = Manager.RubikCube.cubes.Where(c => Cube3D.isEdge(c.Position)).Where(c => c.Colors.Count(co => co == bottomColor || co == topColor) == 0);
    160. //Step 3: Complete the middle layer
    161. foreach (Cube3D c in middleEdges)
    162. {
    163. //3.1 Get the target position
    164. Cube3D.RubikPosition targetPosition = standardCube.RubikCube.cubes.First(cu => ScrambledEquals(cu.Colors, c.Colors)).Position;
    165. //BUG:
    166. //Wenn ein Stein bereits richtig positioniert und ausgerichtet ist, aber falsch orientiert, passiert aktuell noch nichts
    167. //Check correct orientation
    168. List<Face3D> coloredFaces = new List<Face3D>();
    169. Manager.RubikCube.cubes.Where(cu => Cube3D.isCenter(cu.Position)).ToList().ForEach(cu => coloredFaces.Add(cu.Faces.First(f => f.Color != Color.Black)));
    170. bool correctOrientation = c.Faces.Count(f => coloredFaces.Count(cf => cf.Color == f.Color && cf.Position == f.Position) == 1) == 2;
    171. //3.2 Rotate to target position
    172. if (c.Position != targetPosition || (c.Position == targetPosition && !correctOrientation))
    173. {
    174. //Rotate to top layer
    175. if (!c.Position.HasFlag(Cube3D.RubikPosition.TopLayer))
    176. {
    177. Face3D frontFace = c.Faces.First(f => f.Color != Color.Black);
    178. Cube3D.RubikPosition frontSlice = FacePosToCubePos(frontFace.Position);
    179. Face3D face = c.Faces.First(f => f.Color != Color.Black && f.Color != frontFace.Color);
    180. Cube3D.RubikPosition slice = FacePosToCubePos(face.Position);
    181. Manager.Rotate90Sync(slice, true);
    182. if (RefreshCube(c).Position.HasFlag(Cube3D.RubikPosition.TopLayer))
    183. {
    184. Manager.Rotate90Sync(slice, false);
    185. //Algorithm to the right: U R Ui Ri Ui Fi U F
    186. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    187. Manager.Rotate90Sync(slice, true);
    188. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    189. Manager.Rotate90Sync(slice, false);
    190. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    191. Manager.Rotate90Sync(frontSlice, false);
    192. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    193. Manager.Rotate90Sync(frontSlice, true);
    194. }
    195. else
    196. {
    197. Manager.Rotate90Sync(slice, false);
    198. //Algorithm to the left: Ui Li U L U F Ui Fi
    199. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    200. Manager.Rotate90Sync(slice, false);
    201. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    202. Manager.Rotate90Sync(slice, true);
    203. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    204. Manager.Rotate90Sync(frontSlice, true);
    205. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    206. Manager.Rotate90Sync(frontSlice, false);
    207. }
    208. }
    209. //Rotate to start position for the algorithm
    210. IEnumerable<Cube3D> middles = Manager.RubikCube.cubes.Where(cu => Cube3D.isCenter(cu.Position)).Where(m => m.Colors.First(co => co != Color.Black)
    211. == RefreshCube(c).Faces.First(f => f.Color != Color.Black && f.Position != Face3D.FacePosition.Top).Color &&
    212. RemoveFlag(m.Position,Cube3D.RubikPosition.MiddleLayer) == RemoveFlag(RefreshCube(c).Position, Cube3D.RubikPosition.TopLayer));
    213. while (middles.Count() < 1)
    214. {
    215. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    216. middles = Manager.RubikCube.cubes.Where(cu => Cube3D.isCenter(cu.Position)).Where(m => m.Colors.First(co => co != Color.Black)
    217. == RefreshCube(c).Faces.First(f => f.Color != Color.Black && f.Position != Face3D.FacePosition.Top).Color &&
    218. RemoveFlag(m.Position, Cube3D.RubikPosition.MiddleLayer) == RemoveFlag(RefreshCube(c).Position, Cube3D.RubikPosition.TopLayer));
    219. }
    220. //Rotate to target position
    221. Face3D frontFac = RefreshCube(c).Faces.First(f => f.Color != Color.Black && f.Position != Face3D.FacePosition.Top);
    222. Cube3D.RubikPosition frontSlic = FacePosToCubePos(frontFac.Position);
    223. Cube3D.RubikPosition slic = Cube3D.RubikPosition.None;
    224. foreach (Cube3D.RubikPosition p in GetFlags(targetPosition))
    225. {
    226. if (p != Cube3D.RubikPosition.MiddleLayer && p!= frontSlic)
    227. slic |= p;
    228. }
    229. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    230. if (!RefreshCube(c).Position.HasFlag(slic))
    231. {
    232. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    233. //Algorithm to the right: U R Ui Ri Ui Fi U F
    234. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    235. Manager.Rotate90Sync(slic, true);
    236. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    237. Manager.Rotate90Sync(slic, false);
    238. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    239. Manager.Rotate90Sync(frontSlic, false);
    240. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    241. Manager.Rotate90Sync(frontSlic, true);
    242. }
    243. else
    244. {
    245. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    246. //Algorithm to the left: Ui Li U L U F Ui Fi
    247. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    248. Manager.Rotate90Sync(slic, false);
    249. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    250. Manager.Rotate90Sync(slic, true);
    251. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    252. Manager.Rotate90Sync(frontSlic, true);
    253. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    254. Manager.Rotate90Sync(frontSlic, false);
    255. }
    256. }
    257. }
    258. }
    259. private void SolveCrossTopLayer()
    260. {
    261. //Step 1: Get the color of the top layer to start with cross on the last layer
    262. Color topColor = Manager.getFaceColor(Cube3D.RubikPosition.TopLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.MiddleSlice, Face3D.FacePosition.Top);
    263. //Step 2: Get edges with the color of the top face
    264. IEnumerable<Cube3D> topEdges = Manager.RubikCube.cubes.Where(c => Cube3D.isEdge(c.Position)).Where(c => c.Colors.Count(co => co == topColor) == 1);
    265. //Step 3: Solve the cross on the top layer
    266. while (topEdges.Where(c => c.Faces.First(f => f.Position == Face3D.FacePosition.Top).Color == topColor).Count() < 4)
    267. {
    268. if (topEdges.Where(c => c.Faces.First(f => f.Position == Face3D.FacePosition.Top).Color == topColor).Count() != 0)
    269. {
    270. while ((Manager.getFaceColor(Cube3D.RubikPosition.TopLayer | Cube3D.RubikPosition.LeftSlice | Cube3D.RubikPosition.MiddleSlice, Face3D.FacePosition.Top) != topColor))
    271. {
    272. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    273. }
    274. }
    275. if (Manager.getFaceColor(Cube3D.RubikPosition.TopLayer | Cube3D.RubikPosition.FrontSlice | Cube3D.RubikPosition.MiddleSlice_Sides, Face3D.FacePosition.Top) == topColor)
    276. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    277. Manager.Rotate90Sync(Cube3D.RubikPosition.FrontSlice, true);
    278. Manager.Rotate90Sync(Cube3D.RubikPosition.RightSlice, true);
    279. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    280. Manager.Rotate90Sync(Cube3D.RubikPosition.RightSlice, false);
    281. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    282. Manager.Rotate90Sync(Cube3D.RubikPosition.FrontSlice, false);
    283. topEdges = Manager.RubikCube.cubes.Where(c => Cube3D.isEdge(c.Position)).Where(c => c.Colors.Count(co => co == topColor) == 1);
    284. }
    285. //Step 4: Move the edges of the cross to their target positions
    286. while (topEdges.Where(c => c.Position == GetTargetPosition(c)).Count() < 4)
    287. {
    288. IEnumerable<Cube3D> correctEdges = topEdges.Where(c => c.Position == GetTargetPosition(c));
    289. while (correctEdges.Count() < 2)
    290. {
    291. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    292. correctEdges = correctEdges.Select(cE => RefreshCube(cE));
    293. }
    294. Cube3D.RubikPosition rightSlice = FacePosToCubePos(correctEdges.First().Faces
    295. .First(f => f.Color != topColor && f.Color != Color.Black).Position);
    296. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    297. correctEdges = correctEdges.Select(cE => RefreshCube(cE));
    298. if (correctEdges.Count(c => c.Position.HasFlag(rightSlice)) == 0)
    299. {
    300. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    301. }
    302. else
    303. {
    304. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    305. correctEdges = correctEdges.Select(cE => RefreshCube(cE));
    306. rightSlice = FacePosToCubePos(correctEdges.First(cE => !cE.Position.HasFlag(rightSlice)).Faces
    307. .First(f => f.Color != topColor && f.Color != Color.Black).Position);
    308. }
    309. //Algorithm: R U Ri U R U U Ri
    310. Manager.Rotate90Sync(rightSlice, true);
    311. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    312. Manager.Rotate90Sync(rightSlice, false);
    313. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    314. Manager.Rotate90Sync(rightSlice, true);
    315. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    316. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    317. Manager.Rotate90Sync(rightSlice, false);
    318. topEdges = topEdges.Select(tE => RefreshCube(tE));
    319. while (correctEdges.Count() < 2)
    320. {
    321. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    322. correctEdges = correctEdges.Select(cE => RefreshCube(cE));
    323. }
    324. }
    325. }
    326. private void CompleteLastLayer()
    327. {
    328. //Step 1: Get the color of the top layer to start with cross on the last layer
    329. Color topColor = Manager.getFaceColor(Cube3D.RubikPosition.TopLayer | Cube3D.RubikPosition.MiddleSlice_Sides | Cube3D.RubikPosition.MiddleSlice, Face3D.FacePosition.Top);
    330. //Step 2: Get edges with the color of the top face
    331. IEnumerable<Cube3D> topCorners = Manager.RubikCube.cubes.Where(c => Cube3D.isCorner(c.Position)).Where(c => c.Position.HasFlag(Cube3D.RubikPosition.TopLayer));
    332. //Step 3: Bring corners to their target position
    333. while (topCorners.Where(c => c.Position == GetTargetPosition(c)).Count() < 4)
    334. {
    335. IEnumerable<Cube3D> correctCorners = topCorners.Where(c => c.Position == GetTargetPosition(c));
    336. Cube3D.RubikPosition rightSlice;
    337. if (correctCorners.Count() != 0)
    338. {
    339. Cube3D firstCube = correctCorners.First();
    340. Face3D rightFace = firstCube.Faces.First(f => f.Color != Color.Black && f.Position != Face3D.FacePosition.Top);
    341. rightSlice = FacePosToCubePos(rightFace.Position);
    342. Manager.Rotate90Sync(rightSlice, true);
    343. if (RefreshCube(firstCube).Position.HasFlag(Cube3D.RubikPosition.TopLayer))
    344. {
    345. Manager.Rotate90Sync(rightSlice, false);
    346. }
    347. else
    348. {
    349. Manager.Rotate90Sync(rightSlice, false);
    350. rightSlice = FacePosToCubePos(firstCube.Faces.First(f => f.Color != rightFace.Color && f.Color != Color.Black && f.Position != Face3D.FacePosition.Top).Position);
    351. }
    352. }
    353. else rightSlice = Cube3D.RubikPosition.RightSlice;
    354. Cube3D.RubikPosition leftSlice = GetOppositeFace(rightSlice);
    355. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    356. Manager.Rotate90Sync(rightSlice, true);
    357. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    358. Manager.Rotate90Sync(leftSlice, false);
    359. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    360. Manager.Rotate90Sync(rightSlice, false);
    361. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, false);
    362. Manager.Rotate90Sync(leftSlice, true);
    363. topCorners = topCorners.Select(tC => RefreshCube(tC));
    364. correctCorners = correctCorners.Select(cC => RefreshCube(cC));
    365. }
    366. //Step 4: Orientation of the corners on the top layer
    367. topCorners = topCorners.Select(tC => RefreshCube(tC));
    368. Face3D rightFac = RefreshCube(topCorners.First()).Faces.First(f => f.Color != Color.Black && f.Position != Face3D.FacePosition.Top);
    369. Cube3D.RubikPosition rightSlic = FacePosToCubePos(rightFac.Position);
    370. Manager.Rotate90Sync(rightSlic, true);
    371. if (RefreshCube(topCorners.First()).Position.HasFlag(Cube3D.RubikPosition.TopLayer))
    372. {
    373. Manager.Rotate90Sync(rightSlic, false);
    374. }
    375. else
    376. {
    377. Manager.Rotate90Sync(rightSlic, false);
    378. rightSlic = FacePosToCubePos(topCorners.First().Faces.First(f => f.Color != rightFac.Color && f.Color != Color.Black && f.Position != Face3D.FacePosition.Top).Position);
    379. }
    380. foreach (Cube3D c in topCorners)
    381. {
    382. while (!RefreshCube(c).Position.HasFlag(rightSlic))
    383. {
    384. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    385. }
    386. Manager.Rotate90Sync(rightSlic, true);
    387. if (RefreshCube(c).Position.HasFlag(Cube3D.RubikPosition.TopLayer))
    388. {
    389. Manager.Rotate90Sync(rightSlic, false);
    390. }
    391. else
    392. {
    393. Manager.Rotate90Sync(rightSlic, false);
    394. Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    395. }
    396. //Algorithm: Ri Di R D
    397. while (RefreshCube(c).Faces.First(f => f.Position == Face3D.FacePosition.Top).Color != topColor)
    398. {
    399. Manager.Rotate90Sync(rightSlic, false);
    400. Manager.Rotate90Sync(Cube3D.RubikPosition.BottomLayer, false);
    401. Manager.Rotate90Sync(rightSlic, true);
    402. Manager.Rotate90Sync(Cube3D.RubikPosition.BottomLayer, true);
    403. }
    404. }
    405. topCorners = topCorners.Select(tC => RefreshCube(tC));
    406. while (topCorners.Count(tC => tC.Position == GetTargetPosition(tC)) != 4) Manager.Rotate90Sync(Cube3D.RubikPosition.TopLayer, true);
    407. }
    408. private Cube3D.RubikPosition FacePosToCubePos(Face3D.FacePosition position)
    409. {
    410. switch (position)
    411. {
    412. case Face3D.FacePosition.Top:
    413. return Cube3D.RubikPosition.TopLayer;
    414. case Face3D.FacePosition.Bottom:
    415. return Cube3D.RubikPosition.BottomLayer;
    416. case Face3D.FacePosition.Left:
    417. return Cube3D.RubikPosition.LeftSlice;
    418. case Face3D.FacePosition.Right:
    419. return Cube3D.RubikPosition.RightSlice;
    420. case Face3D.FacePosition.Back:
    421. return Cube3D.RubikPosition.BackSlice;
    422. case Face3D.FacePosition.Front:
    423. return Cube3D.RubikPosition.FrontSlice;
    424. default:
    425. return Cube3D.RubikPosition.None;
    426. }
    427. }
    428. private Cube3D.RubikPosition RemoveFlag(Cube3D.RubikPosition oldPosition, Cube3D.RubikPosition item)
    429. {
    430. return oldPosition &= ~item;
    431. }
    432. private Cube3D.RubikPosition GetTargetPosition(Cube3D cube)
    433. {
    434. return standardCube.RubikCube.cubes.First(cu => ScrambledEquals(cu.Colors, cube.Colors)).Position;
    435. }
    436. private Cube3D RefreshCube(Cube3D cube)
    437. {
    438. return Manager.RubikCube.cubes.First(cu => ScrambledEquals(cu.Colors, cube.Colors));
    439. }
    440. private Cube3D.RubikPosition GetOppositeFace(Cube3D.RubikPosition layer)
    441. {
    442. switch (layer)
    443. {
    444. case Cube3D.RubikPosition.TopLayer:
    445. return Cube3D.RubikPosition.BottomLayer;
    446. case Cube3D.RubikPosition.BottomLayer:
    447. return Cube3D.RubikPosition.TopLayer;
    448. case Cube3D.RubikPosition.FrontSlice:
    449. return Cube3D.RubikPosition.BackSlice;
    450. case Cube3D.RubikPosition.BackSlice:
    451. return Cube3D.RubikPosition.FrontSlice;
    452. case Cube3D.RubikPosition.LeftSlice:
    453. return Cube3D.RubikPosition.RightSlice;
    454. case Cube3D.RubikPosition.RightSlice:
    455. return Cube3D.RubikPosition.LeftSlice;
    456. default:
    457. return Cube3D.RubikPosition.None;
    458. }
    459. }
    460. static IEnumerable<Enum> GetFlags(Enum input)
    461. {
    462. foreach (Enum value in Enum.GetValues(input.GetType()))
    463. if (input.HasFlag(value))
    464. yield return value;
    465. }
    466. private static bool ScrambledEquals<T>(IEnumerable<T> list1, IEnumerable<T> list2)
    467. {
    468. var cnt = new Dictionary<T, int>();
    469. foreach (T s in list1)
    470. {
    471. if (cnt.ContainsKey(s))
    472. {
    473. cnt[s]++;
    474. }
    475. else
    476. {
    477. cnt.Add(s, 1);
    478. }
    479. }
    480. foreach (T s in list2)
    481. {
    482. if (cnt.ContainsKey(s))
    483. {
    484. cnt[s]--;
    485. }
    486. else
    487. {
    488. return false;
    489. }
    490. }
    491. return cnt.Values.All(c => c == 0);
    492. }
    493. }
    494. }


    Jetzt noch eine andere Frage:
    Ich würde gerne für die Eingabe eines ungelösten Cubes eine Webcam verwenden und die Farben mit einem aufgenommenen Bild von der Webcam festlegen, um somit mühselige Eingabearbeit zu ersparen. Wie kann ich das verwirklichen?

    Vielen Dank
    Switcherlapp97
    RubiksCubeSolver


    Jetzt im Showroom