いものやま。

雑多な知識の寄せ集め

変種オセロのUIを作ってみた。(その15)

昨日はボタンとボードにオブザーバパターンを実現した。

今日はそれを使ってゲームのプレイが出来るようにする。

ターンコントローラの実装

ゲームをプレイできるようにするためには、まず手番を管理する必要がある。
ということで、BoardNodeObserverプロトコルに準拠したターンコントローラを実装する。

//==============================
// YWF
//------------------------------
// TurnController.swift
//==============================

import Foundation

public class TurnController: BoardNodeObserver {
  // 続く

プロパティとイニシャライザ

まずはプロパティとイニシャライザ。

  // 続き

  private var badPlayer: Player
  private var goodPlayer: Player
  
  private var queue: NSOperationQueue
  
  public init(badPlayer: Player, goodPlayer: Player) {
    self.badPlayer = badPlayer
    self.goodPlayer = goodPlayer
    self.queue = NSOperationQueue()
  }

  // 続く

ターンコントローラは手の選択をPlayerプロトコルに準拠したクラスに委譲するので、それらをプロパティとして持つ。
また、手の選択をバックグラウンドで行う必要があるので、そのためのオペレーションキューを持つようにしている。

ゲームの開始

ゲームを開始させるためのインタフェースを実装する。

  // 続き
  
  public func start(node: BoardNode) {
    node.addObserver(self)
    
    let operation = NSBlockOperation {
      self.requestActionFor(node)
    }
    self.queue.addOperation(operation)
  }

  // 続く

まず、ボードからアクションが終わったことを通知してもらう必要があるので、自身をボードのオブザーバとして登録する。
そして、手番プレイヤーにアクションを選択させ、実行するオペレーションをオペレーションキューに登録する。

プロトコルへの準拠

続いて、BoardNodeObserverプロトコルに準拠するためのコード。

  // 続き

  public func boardNodeActionIsFinished(node: BoardNode) {
    let operation = NSBlockOperation {
      self.requestActionFor(node)
    }
    self.queue.addOperation(operation)
  }
  
  public func boardNodeSquareIsSelected(node: BoardNode, _ square: SquareNode) {
    // do nothing
  }

  // 続く

ボードのアクションが終わったなら、新しいボードについて、手番プレイヤーにアクションを選択させ、実行するオペレーションをバックグラウンドで行う。
(本当は終了判定もしないといけないのだけど、とりあえず後回し)

なお、ボードのマスが選択されたことに関しては特にやることがないので、何もしていない。

手の選択と実行

最後に、手番プレイヤーにアクションを選択させ、それを実行する処理。

  // 続き
  
  private func requestActionFor(node: BoardNode) {
    node.lightUpEnableSquares()
    
    let board = node.board
    let action: Board.Action
    if board.mustPass {
      action = .Pass
    } else {
      switch board.turn {
      case .Bad:
        action = self.badPlayer.select(board)!
      case .Good:
        action = self.goodPlayer.select(board)!
      default:
        action = .Pass
      }
    }
    
    node.lighDown()
    
    switch action {
    case let .Play(row, col):
      node.lightUpsquare(row, col)
      node.play(row, col)
    case let .Change(row, col):
      node.lightUpsquare(row, col)
      node.change(row, col)
    case .Pass:
      node.pass()
    }
  }
}

やってることは簡単。
手番プレイヤーにアクションを選択させ、それを実行しているだけ。
このとき、ついでにボードのライトアップなども行っている。

なお、パスしないといけないときにはアクションを無条件でパスにしているけれど、これはPassボタンへの対応を今回はとりあえず行わないから。
Passボタンに対応したら、修正する必要がある。

人間プレイヤーの実装

次に、人間プレイヤーの実装をする。

//==============================
// YWF
//------------------------------
// Human.swift
//==============================

import Foundation

public class Human: Player, BoardNodeObserver {
  // 続く

プロパティとイニシャライザ

まずはプロパティとイニシャライザ。

  // 続き

  private var boardNode: BoardNode
  private var selectedAction: Board.Action!
  
  private let queue: NSOperationQueue
  private var semaphore: NSOperation!
  
  public init(boardNode: BoardNode) {
    self.boardNode = boardNode
    self.selectedAction = nil
    self.queue = NSOperationQueue()
    self.semaphore = nil
  }
  
  // 続く

BoardNodeのインスタンスをプロパティとして持つのは、BoardNodeから入力を受け取る必要があるから。

で、分かりにくいと思うのが、selectedActionというプロパティの必要性と、queue、semaphoreというプロパティの意味。

これらは、BoardNodeからの入力待ちをするためのもの。

セマフォの使い方には、リソースへのアクセスの排他制御という使い方の他に、リソースの生産の通知という使い方がある。
これは、リソースの生産と消費を別スレッドで行う場合に用いられる方法で、リソースを消費するスレッドではセマフォのカウントが増えるまで待ち、リソースを生産するスレッドではリソースの生産が終わったらスレッドのカウントを増やすようにする。
そうすると、リソースを生産するスレッドとリソースを消費するスレッド間で、同期がとられるようになる。

今回の場合、手が選択される(=「選択された手」というリソースが生産される)まで待つ必要があるので、このセマフォの使い方をしている。

ただ、説明上セマフォと言ってきたけれど、実際の型を見てみるとNSOperationとなっていて、これはセマフォの型ではない。
けど、ここらへんがオペレーションオブジェクトの上手く設計されているところで、これをセマフォのように使うことが出来るようになっている。
詳細はあとのコードで。

Playerプロトコルへの準拠

Playerプロトコルに準拠するため、Player#select(_: Board)を実装する。

  // 続き

  public func select(board: Board) -> Board.Action? {
    self.waitForInput()
    return self.selectedAction
  }

  // 続く

ボードからの入力を待って、選択されたアクションを返してるだけ。

BoardNodeObserverプロトコルへの準拠

BoardNodeObserverプロトコルに準拠するためのコード。

  // 続き
  
  public func boardNodeActionIsFinished(node: BoardNode) {
    // do nothing
  }
  
  public func boardNodeSquareIsSelected(node: BoardNode, _ square: SquareNode) {
    let row = square.row
    let col = square.col
    if node.board.isPlayable(row, col) {
      self.selectedAction = Board.Action.Play(row, col)
    } else if node.board.isChangeable(row, col) {
      self.selectedAction = Board.Action.Change(row, col)
    }
    self.notifyInput()
  }

  // 続く

これも特に難しいことは特になく。
一番最後に、入力があったことを通知していることだけが、ちょっとしたポイント。

セマフォの処理

さて、お待ちかね(?)のセマフォの実装。

  // 続き

  private func waitForInput() {
    self.boardNode.addObserver(self)
    self.semaphore = NSBlockOperation {
      // do nothing
    }
    self.semaphore.waitUntilFinished()
    self.semaphore = nil
    self.boardNode.removeObserver(self)
  }
  
  private func notifyInput() {
    self.queue.cancelAllOperations()
    self.queue.addOperation(self.semaphore)
  }
}

といっても、タネは簡単。

オペレーションオブジェクトはキューに入れられなければ実行されないので、

  • オペレーションオブジェクトをキューに入れずに終了を待つと、セマフォのカウントが上がるのを待つのと同等になる。
  • キューに入れられていないオペレーショオブジェクトをキューに入れると、セマフォのカウントを上げるのと同等になる。

となっている。

なお、BoardNodeから変なときに通知が来てしまうと困ってしまうので、毎回オブザーバの登録/解除を行うようにしていることに注意。

動作確認

コントローラを書き換えて、動作確認を行う。

//==============================
// 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 badPlayer = Human(boardNode: boardNode)
    let goodPlayer = AlphaBetaCom(status: .Good, depth: 3)
    let turnController = TurnController(badPlayer: badPlayer, goodPlayer: goodPlayer)
    turnController.start(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)
  }
  
  // hide status bar.
  override func prefersStatusBarHidden() -> Bool {
    return true
  }
}

なお、人間同士の対戦も出来るのだけど、今回の実装でコンピュータとも対戦できるようになったので、相手としてアルファベータAIを指定してみた。
まだ各ボタンには対応していないし、終了判定とかも入れてないけど、とりあえずAI相手で遊べている。

今日はここまで!