5. ルービックキューブ アプリ

### ・ 展開図

https://qiita.com/nak435/items/00e4d8877532c7d9d287

https://qiita.com/nak435/items/392749423e2d976ef8b8

#### ・ 3D 表示

https://qiita.com/nak435/items/d656ae9eb76e58244a8b

<br>今回は、展開図 と 3D の ルービックキューブ の動きを同期させ、さらに、解法アルゴリズムを導入します。



# 展開図 と 3D の ルービックキューブ の同期

これまでの記事で紹介した、2つの Managerクラス を統括する上位のManagerクラス を新設することで 実現します。

```mermaid
flowchart TB
  M[RubiksCubeManager]
  M--> RCM[RCManager（展開図）] & CM[CubeManager（3D）]
```

機能としては、面の回転要求とリセット要求を、下位の 展開図 と 3D に指示することです。同様に、Undo / Redo の回転要求も下位に指示することで、展開図 と 3D の ルービックキューブ の状態や回転を完全に同期させます。

_下位Managerクラスに実装していた、Undo / Redo 処理は削除します。_

```swift:統括Managerクラス
import SwiftUI

class RubiksCubeManager: ObservableObject {
    @MainActor static let share = RubiksCubeManager()
    @Published var moves = [String]()
    @Published var moved = ""
    @Published var baseCount = 0
    @Published var reqAnimation = true

    let undoManager = UndoManager()
    var rcManager = RCManager.share
    let cubeManager = CubeManager.share

    private init() {
        undoManager.groupsByEvent = false
    }
    
    func reset() {
        baseCount = 0
        moves.removeAll()
        undoManager.removeAllActions()
        
        rcManager.reset()
        cubeManager.reset()
        cubeManager.setFrontCam()
    }
    func move(_ _notation: String, continuous: Bool = false, undo: Bool = false) {
        var notation = _notation
        if undo {
            let reverse = ["U": "U'", "U'": "U", "U2": "U2", "F": "F'", "F'": "F", "F2": "F2", "R": "R'", "R'": "R", "R2": "R2", "D": "D'", "D'": "D", "D2": "D2", "B": "B'", "B'": "B", "B2": "B2", "L": "L'", "L'": "L", "L2": "L2"]
            if let undoNotation = reverse[notation] {
                notation = undoNotation
            } else {
                assertionFailure("notation undo failer [\(_notation)]")
            }
        }

        if !continuous {
            baseCount = 1
            moved = notation
        }

        undoManager.beginUndoGrouping()

            rcManager.rotate(notation)
            reqAnimation ? cubeManager.animate_move(notation) : cubeManager.static_move(notation)
            
            undoManager.registerUndo(withTarget: self) { unownedSelf in
                unownedSelf.move(notation, continuous: false, undo: !undo)
            }

        undoManager.endUndoGrouping()

        if reqAnimation { Task { try await Task.sleep(nanoseconds: 700_000_000) } }
    }
    func scramble() {
        var prevMove = "x", move = "y"
        let MoveNotation: [String] = ["U", "U'", "U2", "F", "F'", "F2", "R", "R'", "R2", "D", "D'", "D2", "B", "B'", "B2", "L", "L'", "L2"]
        moved = ""
        moves.removeAll()
        for _ in 0 ..< 30 {
            repeat {
                move = MoveNotation.randomElement()!
            } while move.first! == prevMove.first!
            moves.append(move)
            prevMove = move
        }
        
        print("Scramble 30 moves:", moves.joined(separator: " "))
        animationMove(message: "Scramble completed!")
    }
    private func animationMove(message: String? = nil) {
        baseCount = moves.count
        var doubleMove = false
        Timer.scheduledTimer(withTimeInterval: reqAnimation ? 0.7: 0.1, repeats: true) { timer in
            if !doubleMove {
                if self.moves.isEmpty {
                    timer.invalidate()
                    if let message { print(message) }
                }
                else {
                    self.moved = self.moves.first!
                    self.move(self.moved, continuous: true)
                    let move = self.moves.removeFirst()
                    if move.last! == "2" && self.reqAnimation {
                        doubleMove = true
                    }
                }
            } else {
                doubleMove = false
            }
        }
        
    }

}

extension RubiksCubeManager {
    var undoCount: Int { undoManager.undoCount }
    var redoCount: Int { undoManager.redoCount }
    var canUndo: Bool { undoManager.canUndo }
    var canRedo: Bool { undoManager.canRedo }
    func undo() { if canUndo { undoManager.undo() }}
    func redo() { if canRedo { undoManager.redo() }}
}
```

<br><br>

#  解法アルゴリズムの導入

https://qiita.com/nak435/items/b44254bd31296c4a5905

前回の記事でSwiftに移植した kociemba を、プロジェクトに追加するだけです。
一部の冗長なヘッダーファイル（xxx_h.swift）は削除して、ファイル構成を少し単純化しました。



### ・ 面データの生成

`kociemba.solve()`の引数に渡す `面データ` の生成は、 展開図 情報[`RCData`](https://qiita.com/nak435/items/00e4d8877532c7d9d287#%E9%85%8D%E7%BD%AE)に実装します。

```swift
extension RCData {
    func getCubeStringNotation() -> String {
        let RCPosition = ["U", "L", "F", "R", "B", "D"]
        func getFace(_ face: RCFace) -> String {
            var result = ""
            for row in 0 ..< 3 {
                for col in 0 ..< 3 {
                    result += RCPosition[face[row][col] / 9]
                }
            }
            return result
        }
        var block = ""
        block += getFace(self[0]) // U
        block += getFace(self[3]) // R
        block += getFace(self[2]) // F
        block += getFace(self[5]) // D
        block += getFace(self[1]) // L
        block += getFace(self[4]) // B
        return block
    }
}
```

_各キューブの色を数値（0〜53）で保持していたので、`面データ` の生成は 簡単です。_

<br><br><br>

<iframe width="904" height="648" src="https://www.youtube.com/embed/WHCqBP066pQ" title="ルービックキューブ アプリ" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


# コード

数・量とも増えたので、XcodeプロジェクトをZipしてアップしました。

https://www.nak435.com/RubiksCube/Projects/RCPlayer_qiita_4_Kociemba.zip


---

次回はマシンを作成します。

<br><br>
**以上**