昨日の続き。
Playerプロトコルの修正
Undoに対応するためにPlayerプロトコルに少し修正を加える。
//============================== // YWF //------------------------------ // Player.swift //============================== public protocol Player { var isCom: Bool { get } func select(board: Board) -> Board.Action? func cancelSelect() }
Player#isComという計算型プロパティと、Player#cancelSelect()というメソッドを追加している。
これは、
- Undoでアクションの巻き戻しを行う場合、人間の手番までアクションを巻き戻す必要がある。
- アクションの選択中にUndoボタンが押された場合、そのアクションの選択をキャンセルする必要がある。
という理由から。
Humanクラスの修正
Playerプロトコルの修正にともなって、Humanクラスにも修正を入れる。
//============================== // YWF //------------------------------ // Human.swift //============================== import Foundation public class Human: Player, BoardNodeObserver, ButtonNodeObserver { public var isCom: Bool { return false } // 省略 private func notifyInput() { self.queue.cancelAllOperations() if self.semaphore != nil { self.queue.addOperation(self.semaphore) } } public func cancelSelect() { self.notifyInput() } }
キャンセルされたときには、単に入力があったことを通知して、アクションの選択待ち状態を止めるようにしている。
もちろん、そうなるとselec()で返されるアクションの値は不正で信頼できないものになるけれど、そこはTurnControllerの方で対処する。
それに、仮にTurnControllerでその不正なアクションを止められなかったとしても、不正なアクションはBoardNodeで無視されるので、特に問題はない。
一つ気をつけたいのが、Human#notifyInput()でセマフォのnilチェックを行うようにしているということ。
今までであれば、必ず入力待ちと入力があったことの通知が一対一対応していたので、このnilチェックは不要だったんだけど、Undoボタンが押されたときにも通知が来るようになったので、必ずしも一対一対応するとは限らなくなっている。
例えば、入力待ちになる→手が選択される→入力があったことの通知がされる→入力待ちが解除され、セマフォがnilになる(けどまだselect()からは戻っていない)→Undoボタンが押される→入力があったことが通知される→(まだselect()から戻ってきてないので)入力待ちの解除を行おうとする、という流れが発生すると、nilチェックを行っていない場合、nilをオペレーションキューに突っ込むことになってしまう。
AlphaBetaComクラスの修正
AlphaBetaComクラスについても、同様の修正を入れる。
//============================== // YWF //------------------------------ // AlphaBetaCom.swift //============================== public class AlphaBetaCom: Player { public var isCom: Bool { return true } // 省略 private var cancelled: Bool // 省略 public func select(board: Board) -> Board.Action? { self.cancelled = false var currentSelect: Board.Action! = .Pass var currentSelectValue = -1000 var opponentBestValue = 1000 for action in board.legalActions { if self.cancelled { break } let value = self.calculateActionValue(board, action, self.depth, currentSelectValue, opponentBestValue) if value > currentSelectValue { currentSelect = action currentSelectValue = value if currentSelectValue >= opponentBestValue { break } } } switch currentSelect! { case .Pass: println("pass.") case let .Play(row, col): println("play (\(row), \(col)).") case let .Change(row, col): println("change (\(row), \(col)).") } return currentSelect! } private func calculateActionValue(board: Board, _ action: Board.Action, _ depth: Int, _ selfBestValue: Int, var _ opponentBestValue: Int) -> Int { if self.cancelled { return 0 } let newBoard: Board switch action { case .Pass: newBoard = board.pass() case let .Play(row, col): newBoard = board.play(row, col) case let .Change(row, col): newBoard = board.change(row, col) } if (depth == 1) || newBoard.isGameEnd { if newBoard.win(self.status) { return 100 } else if newBoard.win(self.opponent) { return -100 } else { return newBoard.count(self.status) - newBoard.count(self.opponent) } } else { let sign = (newBoard.turn == self.status) ? 1 : -1 for newAction in newBoard.legalActions { if self.cancelled { return 0 } let value = self.calculateActionValue(newBoard, newAction, depth - 1, opponentBestValue, selfBestValue) if (value * sign) > (opponentBestValue * sign) { opponentBestValue = value } if (opponentBestValue * sign) >= (selfBestValue * sign) { break } } return opponentBestValue } } public func cancelSelect() { self.cancelled = true } }
cancelledというプロパティを追加して、キャンセルされた場合、この値がtrueになるようにしている。
そして、アクションの選択中にこのプロパティをこまめにチェックするようにして、trueになっていたらselect()から抜け出すようにしてある。
(こういうときに例外処理の仕組みがないと非常に不便。例外があれば例外を投げて一番上側でキャッチすればいいだけなのに・・・)
TurnControllerの修正
さて、いよいよTurnControllerの修正。
プロパティとイニシャライザ
Undoに対応させるために、プロパティとイニシャライザを少し修正。
//============================== // YWF //------------------------------ // TurnController.swift //============================== import Foundation public class TurnController: BoardNodeObserver, ButtonNodeObserver { private var boardNode: BoardNode private var undoButton: ButtonNode private var badPlayer: Player private var goodPlayer: Player private var queue: NSOperationQueue private var selectOperation: NSOperation! private var undoOperation: NSOperation! public init(boardNode: BoardNode, undoButton: ButtonNode, badPlayer: Player, goodPlayer: Player) { self.boardNode = boardNode self.undoButton = undoButton self.badPlayer = badPlayer self.goodPlayer = goodPlayer self.queue = NSOperationQueue() self.selectOperation = nil self.undoOperation = nil } // 続く
まず、プロパティにboardNodeとundoButtonを追加してある。
なお、boardNodeを追加したのは、Undoボタンを押されたときにボードに対してUndoを要求しないといけないのだけど、通知がボードからではなくボタンからくるので、通知内でボードへの参照が得られないから。
また、undoButtonについても、アクションの選択を行うときに、Undoボタンの有効/無効を切り替える必要がある場合があるので、やはり参照を保持しておく必要がある。
それと、オペレーションオブジェクトを参照するためのプロパティとして、selectOperationとundoOperationが追加してある。
ゲームの開始
// 続き public func start() { self.boardNode.addObserver(self) self.undoButton.addObserver(self) self.selectOperation = NSBlockOperation { self.requestAction() } self.queue.addOperation(self.selectOperation) } // 続く
ボードとUndoボタンへの参照を持つようにしたので、start()は引数としてボードへの参照を必要としなくなっている。
アクションの選択と実行
Undoボタンを押されることでアクションがキャンセルされる可能性が出ているので、以下のように修正。
// 続き private func requestAction() { self.boardNode.lightUpEnableSquares() if self.boardNode.board.move > 0 { self.undoButton.enable() } else { self.undoButton.disable() } let board = self.boardNode.board let action: Board.Action switch board.turn { case .Bad: action = self.badPlayer.select(board)! case .Good: action = self.goodPlayer.select(board)! default: action = .Pass } if !self.selectOperation.cancelled { self.boardNode.lighDown() switch action { case let .Play(row, col): self.boardNode.lightUpSquare(row, col) self.boardNode.play(row, col) case let .Change(row, col): self.boardNode.lightUpSquare(row, col) self.boardNode.change(row, col) case .Pass: self.boardNode.pass() } } } // 続く
アクションの選択でプレイヤーから返ってきた手を実行する前に、アクションの選択自体がキャンセルされていたら、何もしないようしている。
Undoボタンへの対応
Undoボタンが押されたときの処理は、以下のとおり。
// 続き public func buttonIsSelected(button: ButtonNode) { switch button.type { case .Undo: self.undoButton.disable() self.cancelSelectOperation() self.undoOperation = NSBlockOperation { self.boardNode.lighDown() self.boardNode.undo() } self.queue.addOperation(self.undoOperation) default: break } } private func cancelSelectOperation() { if (self.selectOperation != nil) && (!self.selectOperation.finished) { self.selectOperation.cancel() switch self.boardNode.board.turn { case .Bad: self.badPlayer.cancelSelect() case .Good: self.goodPlayer.cancelSelect() default: break } } } // 続く
まず、アクションの選択のオペレーションをキャンセル。
(ただし、オペレーションをキャンセルをしても、無理やりそのオペレーションが止められるわけではないのに注意。Swiftでの並列プログラミングについて調べてみた。(その4) - いものやま。も参照)
そして、アクションの選択を委譲しているプレイヤーにも選択をキャンセルさせる。
そのあと、ボードに対してUndoを要求している。
BoardNodeObserverプロパティへの準拠
Undoに対応するために、BoardNodeObserver#boardNodeActionIsFinished(_: BoardNode)の修正を行う。
// 続き public func boardNodeActionIsFinished(node: BoardNode) { if self.undoOperation != nil { if self.undoOperation.finished { let board = self.boardNode.board let turnPlayer = (board.turn == .Bad) ? self.badPlayer : self.goodPlayer if board.move > 0 && turnPlayer.isCom { // turn player is not human, // so undo again. self.undoOperation = NSBlockOperation { self.boardNode.undo() } self.queue.addOperation(self.undoOperation) return } else { // undo has been finished. self.undoOperation = nil if let lastAction = self.boardNode.lastAction { switch lastAction { case let .Play(row, col): self.boardNode.lightUpsquare(row, col, withOpponent: true) case let .Change(row, col): self.boardNode.lightUpsquare(row, col, withOpponent: true) default: break } } } } else { // do nothing, // because undo has been requested and not been finished. return } } self.selectOperation = NSBlockOperation { self.requestAction() } self.queue.addOperation(self.selectOperation) } public func boardNodeSquareIsSelected(node: BoardNode, _ square: SquareNode) { // do nothing } }
この通知がくるのは、2つの場合が考えられる。
すなわち、play/change/passが行われた場合と、undoが行われた場合。
undoが行われた場合、手番が人の手番になっているかどうかを確認している。
人の手番になっていればundoは終わったので、通常のアクションの選択と実行へ移る。
そうでない場合、さらにアクションの巻き戻しを行って、人の手番になるようにする。
play/change/passが行われた場合は、基本的に次のアクションの選択と実行をすればいいのだけど、ただ、ここで一つ注意がいる。
それは、undoOperationがnilでなく、かつ終了していないという場合。
どうしたらそんなことが起こるのかというと、アクションの選択をする→ボードでアクションがキューに入れられる→Undoボタンが押される→ボードでUndoがキューに入れられる→ボードでアクションが行われて通知が来る、というタイミングが考えられるから。
この場合、Undoのリクエストも処理されてから次のアクションの選択に移らないといけないので、何もせずに戻る必要がある。
(そして、Undoが行われてから再び通知がくるので、そこで適切な処理を行う)
Undoの動作確認
さて、コントローラをちょっと修正して、動作確認。
//============================== // YWF //------------------------------ // GameViewController.swift //============================== import UIKit import SpriteKit class GameViewController: UIViewController { @IBOutlet weak var skView: SKView! override func viewDidLoad() { BoardNode.loadAssets() ButtonNode.loadAssets() PieceNode.loadAssetsAndCreateTemplates() } override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) var size = self.view.bounds.size if UIDevice.currentDevice().userInterfaceIdiom == .Phone { size.width *= 2.0 size.height *= 2.0 } let scene = GameScene(size: size) scene.scaleMode = .AspectFill let layoutManager = LayoutManager.getInstanceFor(size) let backgroundTexture = SKTexture(imageNamed: "Background") let background = SKSpriteNode(texture: backgroundTexture) background.position = layoutManager.getPosition(.Background, relativeTo: .Scene) scene.addChild(background) let headerPosition = layoutManager.getPosition(.Header, relativeTo: .Background) if headerPosition != nil { let headerTexture = SKTexture(imageNamed: "Header") let header = SKSpriteNode(texture: headerTexture) header.position = headerPosition background.addChild(header) let logoTexture = SKTexture(imageNamed: "Logo") let logo = SKSpriteNode(texture: logoTexture) logo.position = layoutManager.getPosition(.Logo, relativeTo: .Header) header.addChild(logo) } var board = Board() let boardNode = BoardNode(board: board) boardNode.position = layoutManager.getPosition(.Board, relativeTo: .Background) background.addChild(boardNode) let undoButton = ButtonNode(type: .Undo) let passButton = ButtonNode(type: .Pass, enabled: false) let exitButton = ButtonNode(type: .Exit) undoButton.position = layoutManager.getPosition(.UndoButton, relativeTo: .Background) passButton.position = layoutManager.getPosition(.PassButton, relativeTo: .Background) exitButton.position = layoutManager.getPosition(.ExitButton, relativeTo: .Background) background.addChild(undoButton) background.addChild(passButton) background.addChild(exitButton) self.skView.presentScene(scene) let badPlayer = Human(boardNode: boardNode, passButton: passButton) let goodPlayer = AlphaBetaCom(status: .Good, depth: 3) let turnController = TurnController(boardNode: boardNode, undoButton: undoButton, badPlayer: badPlayer, goodPlayer: goodPlayer) turnController.start() } // hide status bar. override func prefersStatusBarHidden() -> Bool { return true } }
これを実行すると、Undoがちゃんと出来てるのが分かると思う。
今日はここまで!