LabelButtonNodeの反応性の向上は出来た。
次は中断されたゲームの再開。
Historyクラス
まずは、ゲームを再開できるようにするために、棋譜を保存してやる必要がある。
けど、モデル(Boardクラス)で扱っているのは一つの盤面の情報だけで、棋譜全体の情報は持っていない。
棋譜の情報を持っているのは、undoを行う必要があるBoardNode。
ただ、これで棋譜の保存の機能とかまで持たせると、責務過多というか。
そういった機能は、本来モデルレベルで提供するべきもの。
ということで、棋譜情報をBoardNodeから切り出して、Historyとして実装する。
//============================== // YWF //------------------------------ // History.swift //============================== import Foundation public class History: NSObject { private static let fileName: String = "history.plist" private static var url: NSURL = { let fileManager = NSFileManager.defaultManager() let supportDirectory = fileManager.URLForDirectory(.ApplicationSupportDirectory, inDomain: NSSearchPathDomainMask.UserDomainMask, appropriateForURL: nil, create: true, error: nil)! return supportDirectory.URLByAppendingPathComponent(History.fileName) }() public class func fromSaveData() -> History? { if let savedData = NSDictionary(contentsOfURL: History.url) { var history = History() history.badPlayerInfo = savedData["badPlayerInfo"] as! [String: AnyObject] history.goodPlayerInfo = savedData["goodPlayerInfo"] as! [String: AnyObject] let historyInfo = savedData["historyInfo"] as! Array<Dictionary<String, AnyObject>> for savedAction in historyInfo { if savedAction["type"] as! String == "play" { let row = savedAction["row"] as! Int let col = savedAction["col"] as! Int history.pushAction(.Play(row, col)) } else if savedAction["type"] as! String == "change" { let row = savedAction["row"] as! Int let col = savedAction["col"] as! Int history.pushAction(.Change(row, col)) } else if savedAction["type"] as! String == "pass" { history.pushAction(.Pass) } } return history } else { return nil } } public var badPlayerInfo: [String: AnyObject] public var goodPlayerInfo: [String: AnyObject] private var historyInfo: Array<Dictionary<String, AnyObject>> private var boards: [Board] private var actions: [Board.Action] public private(set) var lastBoard: Board public var lastAction: Board.Action! { let count = self.actions.count if count > 0 { return self.actions[count - 1] } else { return nil } } public override init() { self.badPlayerInfo = [String: AnyObject]() self.goodPlayerInfo = [String: AnyObject]() self.boards = [Board]() self.actions = Array<Board.Action>() self.lastBoard = Board() self.historyInfo = Array<Dictionary<String, AnyObject>>() super.init() } public var count: Int { return self.actions.count } public func pushAction(action: Board.Action) { self.boards.append(self.lastBoard) self.actions.append(action) switch action { case let .Play(row, col): self.historyInfo.append(["type": "play", "row": row, "col": col]) self.lastBoard = self.lastBoard.play(row, col) case let .Change(row, col): self.historyInfo.append(["type": "change", "row": row, "col": col]) self.lastBoard = self.lastBoard.change(row, col) case let .Pass: self.historyInfo.append(["type": "pass"]) self.lastBoard = self.lastBoard.pass() } } public func popAction() -> Board.Action! { if self.actions.count > 0 { self.historyInfo.removeLast() self.lastBoard = self.boards.removeLast() return self.actions.removeLast() } else { return nil } } private func save() { var saveData = [String: AnyObject]() saveData["badPlayerInfo"] = self.badPlayerInfo saveData["goodPlayerInfo"] = self.goodPlayerInfo saveData["historyInfo"] = self.historyInfo (saveData as NSDictionary).writeToURL(History.url, atomically: true) } public func delete() { let fileManager = NSFileManager.defaultManager() fileManager.removeItemAtURL(History.url, error: nil) } }
棋譜として、プレイヤーの情報と、行ったアクションの情報を保持するようにしてある。
そして、今までBoardNodeのプロパティとして持っていたboardsプロパティとactionsプロパティは、こちらへ移動してある。
それと、ファイルへの保存/ファイルからの読み出しが出来るように、いくつかのメソッド(History.fromSaveData()、History#save()、History#delete())を用意した。
なお、プレイヤーの情報も保持するようにしているのは、シーンの状態を復元するときに、プレイヤーの情報がないと復元できないから。
ちょっと無理やりな感じで、あまりよくないのだけど・・・他に上手い方法が浮かばなかったので、とりあえずこれで。
BoardNodeの修正
上記のように、履歴に関する機能をHistoryに切り出したので、それに合わせてBoardNodeの修正を行う。
//============================== // YWF //------------------------------ // BoardNode.swift //============================== import SpriteKit public class BoardNode: SKSpriteNode { // 省略 private var history: History // 省略 public init(history: History) { self.history = history self.board = history.lastBoard // 省略 } // 省略 public func deleteHistory() { self.history.delete() } // 省略 public func play(row: Int, _ col: Int) { self.actionQueue.addOperationWithBlock { if !self.board.isPlayable(row, col) { return } let originalBoard = self.board self.history.pushAction(Board.Action.Play(row, col)) self.board = self.history.lastBoard // 省略 } } public func change(row: Int, _ col: Int) { self.actionQueue.addOperationWithBlock { if !self.board.isChangeable(row, col) { return } let originalBoard = self.board self.history.pushAction(Board.Action.Change(row, col)) self.board = self.history.lastBoard // 省略 } } public func pass() { self.actionQueue.addOperationWithBlock { if !self.board.mustPass { return } let originalBoard = self.board self.history.pushAction(Board.Action.Pass) self.board = self.history.lastBoard // 省略 } } public func undo() { self.actionQueue.addOperationWithBlock { if self.history.count == 0 { return } let currentBoard = self.board let lastAction = self.history.popAction()! let previousBoard = self.history.lastBoard self.board = previousBoard // 省略 } } // 省略 public var lastAction: Board.Action? { return self.history.lastAction } // 省略 }
今までactionsプロパティやboardsプロパティを使っていた部分を、historyプロパティを使うように置き換えている。
あと、イニシャライザではHistoryを引数にとるようにしてある。
GameSceneなどの修正
BoardNodeのイニシャライザを修正したので、それに合わせてGameSceneも修正する。
ついでに、簡易イニシャライザも用意。
//============================== // YWF //------------------------------ // GameScene.swift //============================== import SpriteKit import Security public class GameScene: SKScene, ButtonNodeObserver { // 省略 public init(size: CGSize, userName: String, userStatus: PieceNode.Status, opponentName: String, opponentPlayer: Player, history: History) { // 省略 self.boardNode = BoardNode(history: history) // 省略 } public convenience override init(size: CGSize) { let config = Config.getInstance() let humanStatus: PieceNode.Status let computerStatus: Board.Status switch config.firstMove { case .Random: var buf = UnsafeMutablePointer<UInt8>.alloc(1) SecRandomCopyBytes(kSecRandomDefault, 1, buf) let randomValue = buf.memory if randomValue % 2 == 0 { humanStatus = .Bad computerStatus = .Good } else { humanStatus = .Good computerStatus = .Bad } case .Human: humanStatus = .Bad computerStatus = .Good case .Computer: humanStatus = .Good computerStatus = .Bad } let computer: Player let computerName: String switch config.computerLevel { case .Easy: computer = RandomCom() computerName = "Computer - Easy" case .Normal: computer = AlphaBetaCom(status: computerStatus, depth: 3) computerName = "Computer - Normal" case .Hard: computer = AlphaBetaCom(status: computerStatus, depth: 5) computerName = "Computer - Hard" } var history = History() if humanStatus == .Bad { history.badPlayerInfo = ["name": "You", "isCom": false] history.goodPlayerInfo = ["name": computerName, "isCom": true] } else { history.badPlayerInfo = ["name": computerName, "isCom": true] history.goodPlayerInfo = ["name": "You", "isCom": false] } self.init(size: size, userName: "You", userStatus: humanStatus, opponentName: computerName, opponentPlayer: computer, history: history) } public convenience init(size: CGSize, history: History) { let userName: String let computerName: String let humanStatus: PieceNode.Status let computerStatus: Board.Status if history.badPlayerInfo["isCom"] as! Bool { humanStatus = .Good computerStatus = .Bad userName = history.goodPlayerInfo["name"] as! String computerName = history.badPlayerInfo["name"] as! String } else { humanStatus = .Bad computerStatus = .Good userName = history.badPlayerInfo["name"] as! String computerName = history.goodPlayerInfo["name"] as! String } let config = Config.getInstance() let computer: Player switch config.computerLevel { case .Easy: computer = RandomCom() case .Normal: computer = AlphaBetaCom(status: computerStatus, depth: 3) case .Hard: computer = AlphaBetaCom(status: computerStatus, depth: 5) } self.init(size: size, userName: userName, userStatus: humanStatus, opponentName: computerName, opponentPlayer: computer, history: history) } // 省略 public override func willMoveFromView(view: SKView) { self.turnController.stop() self.boardNode.deleteHistory() self.releaseObservers() } // 省略 }
ここで、簡易イニシャライザを用意してあるのは、楽をするためというのもあるのだけど、もうちょいツラい理由から。
それは何かというと、HistoryからGameSceneを復元しようとしたときに、Playerの復元もしないといけないのだけれど、Playerを復元するための情報にどのようにアクセスするのか、という問題があるから。
今までだと、StartSceneからGameSceneへ遷移するときに、StartSceneでConfigに基づいてPlayerを作成し、それをGameSceneのイニシャライザの引数に渡していたのだけど、HistoryからのGameSceneの復元をやろうとなると、GameSceneを復元するのはViewControllerなので、ViewControllerはどのようにPlayerを作成すればいいのかを知らなければならない。
そのための情報を格納するために、History#badPlayerInfoプロパティとHistory#goodPlayerInfoプロパティを使うことが出来るのだけど、これらは辞書なので、どんなキーがどのように使われているのかは、ドキュメントなどに書かない限り、外部インタフェースとして用意されていない。
なので、StartSceneでPlayerを作って、そのPlayerを復元させるための情報をHistoryに持たせるようにすると、ViewControllerはStartSceneの実装(=Playerを復元できるようにするためにどのように情報を持たせているのか)を知らないといけなくなってしまう。
これはかなりよろしくない。
そこで、かなりの苦肉の策として、GameSceneの中でPlayerを作るようにすれば、Playerを復元するための情報をどのように持たせているのかはGameScene自身が知っているので、Playerの復元が出来るようになる。
ただし、これを指定イニシャライザにして、元々のイニシャライザをなくしてしまうと(それも出来なくはない)、Configを使わないでGameSceneを作るということが出来なくなってしまうので、それはよくない。
そこで、元のイニシャライザはそのまま残して、それとは別に簡易イニシャライザを用意せざるをえない、と。
このあたり、もうちょい上手く作ってあげれば、キレイになりそうなんだけどね・・・
(リフレクションを使うとか)
とりあえず、GameSceneを上記のように修正したので、それに合わせて、StartScene、および、ViewControllerも修正。
//============================== // YWF //------------------------------ // StartScene.swift //============================== import SpriteKit public class StartScene: SKScene, PlayButtonNodeObserver, LabelButtonNodeObserver { // 省略 public func playButtonIsSelected(button: PlayButtonNode) { let scene = GameScene(size: self.size) let transition = SKTransition.fadeWithDuration(NSTimeInterval(1.0)) self.view?.presentScene(scene, transition: transition) } // 省略 }
//============================== // YWF //------------------------------ // GameViewController.swift //============================== import UIKit import SpriteKit class GameViewController: UIViewController { // 省略 override func viewWillAppear(animated: Bool) { // 省略 let scene: SKScene if let history = History.fromSaveData() { scene = GameScene(size: size, history: history) } else { scene = StartScene(size: size) } self.skView.presentScene(scene) } // 省略 }
とりあえずこれで下準備はOK。
あとは、ライフサイクルのイベントに合わせて、棋譜の保存を実行できるようにするだけ。
今日はここまで!