いものやま。

雑多な知識の寄せ集め

「BirdHead」を遊べるようにしてみた。(その3)

昨日はモデル全体の構成について説明した。

今日はアクションを表すAction列挙型とプレイヤーを表すPlayerプロトコルを実装していく。

Action列挙型

まずはAction列挙型から。

//==============================
// BirdHead
//------------------------------
// Action.swift
//==============================

import Foundation

enum Action: Equatable {
  case Play([Int])
  case Discard([Int])

  static func play(cards: [Int]) -> Action {
    let sorted = cards.sort { $0 < $1 }
    return Action.Play(sorted)
  }

  static func play(cards: Int...) -> Action {
    let sorted = cards.sort { $0 < $1 }
    return Action.Play(sorted)
  }

  static func discard(cards: [Int]) -> Action {
    let sorted = cards.sort { $0 < $1 }
    return Action.Discard(sorted)
  }

  static func discard(cards: Int...) -> Action {
    let sorted = cards.sort { $0 < $1 }
    return Action.Discard(sorted)
  }
}

func ==(lhs: Action, rhs: Action) -> Bool {
  switch lhs {
  case let .Play(lhsCards):
    switch rhs {
    case let .Play(rhsCards):
      return lhsCards == rhsCards
    case .Discard:
      return false
    }
  case let .Discard(lhsCards):
    switch rhs {
    case .Play:
      return false
    case let .Discard(rhsCards):
      return lhsCards == rhsCards
    }
  }
}

アクションの種類としては、

  • プレイ 場に出ているカード以上のカードを出す
  • ディスカード 手札の一番弱いカードを捨てる

の2種類。
それぞれ、どのカードをプレイ/ディスカードしたのかの情報も必要なので、タプルを伴った列挙型として定義している。

そして、プレイ/ディスカードされたカードの情報はソートされていた方が扱いやすいので、ファクトリメソッドを用意してある。
(なお、ホントはさらに既存のイニシャライザをprivateにしたかったのだけど、方法が分からなかった・・・)

ここでちょっと注意したいのが、Arrayのソートメソッド。

Swift 2.0より前では、

  • Array#sort()では、その配列自体を破壊的にソートする
  • Array#sorted()では、ソートされた新しい配列を生成して返す

となっていたのが、Swift 2.0では、

  • Array#sort()では、ソートされた新しい配列を生成して返す
  • Array#sortInPlace()では、その配列自体を破壊的にソートする

というふうに変更された。
これは互換性に関わる重大な変更で、特にArray#sort()は同じ名前のメソッドなのに挙動が全く違うので、注意が必要。

あと、アクションが同じかどうかの判断が出来るように、Equatableプロトコルに準拠するようにして、アクション同士の==オペレータを定義してある。

動作確認

アクション同士の==オペレータの動作を確認するために、簡単なテストコードを書いた。

// ActionTest.swift

import Foundation

let playA = Action.play(2, 3)
let playB = Action.play([3, 2])
let playC = Action.play(4)
let discardA = Action.discard(2, 3)
let discardB = Action.discard([3, 2])
let discardC = Action.discard(4)

if playA == playB {
  print("OK")
} else {
  print("NG")
}

if playA != playC {
  print("OK")
} else {
  print("NG")
}

if discardA == discardB {
  print("OK")
} else {
  print("NG")
}

if discardA != discardC {
  print("OK")
} else {
  print("NG")
}

if playA != discardA {
  print("OK")
} else {
  print("NG")
}

if playA != discardC {
  print("OK")
} else {
  print("NG")
}

if discardA != playC {
  print("OK")
} else {
  print("NG")
}

Makefileをちょっと修正してビルド、実行させると、ちゃんと動作することを確認できる。

Playerプロトコル

次はPlayerプロトコル
実際には、このプロトコルに準拠した具体的なクラスを書いていくことになる。

//==============================
// BirdHead
//------------------------------
// Player.swift
//==============================

import Foundation

protocol Player: class {
  var name: String { get }
  var isCom: Bool { get }
  func select(view: GameInfo.PlayerView) throws -> Action
  func learn(minusPoint: Int)
}

extension Player {
  func learn(minusPoint: Int) {}
}

enum PlayerError: ErrorType {
  case SelectIsCanceled
}

まず、読み取り専用のプロパティとして、プレイヤーの名前と、AIかどうかの真偽値。
それと、アクションを選択させるselect()メソッドと、学習させるためのlearn()メソッドを用意してある。

Player#select(_: GameInfo.PlayerView)メソッドでは、「BirdHead」を遊べるようにしてみた。(その2) - いものやま。で説明した通り、プレイヤーにはプレイヤー・ビューのみ参照させるようにしている。

なお、ここでは例外も投げるようにしているけれど、これは入力をキャンセルして終了させたりすることが出来るようにするため。
YWFを実装していたときはまだ例外が使えなかったので、「同じ出口」でなんとかせざるを得なく、オプショナル型を返すしかなくなってたけれど(そして、それゆえ酷いコードになってたけど)、例外が使えるようになったので、まともなインタフェースを用意することが出来るようになった。

その、入力をキャンセルするための例外が、PlayerError.SelectIsCanceled。
ホントはPlayerプロトコル内で定義して、Player.Error.SelectIsCanceledとしたかったけど、プロトコル内で列挙型を定義することは出来なかったので、このような形になっている。

そして、面白いのがプロトコルの拡張。
これもSwift 2.0で追加された機能で、これを使うことでプロトコルに「デフォルトの実装」を与えることが出来るようになっている。
もちろん、あくまで「拡張」なので、格納型プロパティを新たに定義したりすることは出来ず、それゆえ、やれることにも限りはあるのだけど、オブザーバのように、自分の関心のある通知だけ処理をして、それ以外の通知は無視したいといった場合、デフォルトの実装として空実装が用意されていると、とても便利。
プレイヤーの場合、アクションの選択は実装することが必須だけれど、学習を行うかどうかはオプショナルなので、デフォルトの実装として空実装を用意しておいて、学習を行う場合にはこれをちゃんと定義するようにした。

今日はここまで!