いものやま。

雑多な知識の寄せ集め

変種オセロの仕上げをしてみた。(その5)

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。
あとは、ライフサイクルのイベントに合わせて、棋譜の保存を実行できるようにするだけ。

今日はここまで!