読者です 読者をやめる 読者になる 読者になる

いものやま。

雑多な知識の寄せ集め

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

ゲーム開発 Swift BirdHead

昨日はアクションとプレイヤーの実装をした。

今日からは、ゲーム情報、および、プレイヤー・ビューの実装をしていく。

GameInfoObserverプロトコル

その前に、ゲーム情報のオブザーバになるGameInfoObserverプロトコルのインタフェースを定義しておく。

ビューやコントローラが通知を行って欲しいタイミングを考えると、以下のようなタイミング:

  • プレイヤーにカードを配った。
  • プレイヤーがアクションを行った。
  • 1トリック終わってトリックを取った人が決まった。
  • 1ディール終わってマイナス点を受け取る人が決まった。
  • ゲームが終わった。

そこで、それぞれに対する通知を用意し、そして、デフォルトの実装として空実装を用意した。

//==============================
// BirdHead
//------------------------------
// GameInfoObserver.swift
//==============================

import Foundation

protocol GameInfoObserver: class {
  func gameInfoDealtCard(card: Int, toPlayer playerIndex: Int)
  func gameInfoDidAction(action: Action, byPlayer playerIndex: Int)
  func gameInfoResolvedTrick(trickCount: Int, winner playerIndex: Int)
  func gameInfoResolvedDeal(loserIndices: [Int], lastCards: [Int])
  func gameInfoEnded()
}

extension GameInfoObserver {
  func gameInfoDealtCard(card: Int, toPlayer playerIndex: Int) {}
  func gameInfoDidAction(action: Action, byPlayer playerIndex: Int) {}
  func gameInfoResolvedTrick(trickCount: Int, winner playerIndex: Int) {}
  func gameInfoResolvedDeal(loserIndices: [Int], lastCards: [Int]) {}
  func gameInfoEnded() {}
}

(余談だけど、オブザーバのメソッド名ってどうしたらいいもんか、いつも悩む・・・AppleAPIに真似てそれっぽくしてるけど、正直、微妙)

GameInfoクラス

さて、いよいよGameInfoクラス。

//==============================
// BirdHead
//------------------------------
// GameInfo.swift
//==============================

import Foundation

/* この関数については後で説明する */
private func playableCardIndicesArray(hands: [Int],
                                      _ lowerBound: [Int],
                                      _ offset: Int) -> [[Int]] {
  // 今は省略
}

class GameInfo {

  // 続く

最初にちょこっとprivateな関数を定義しているのだけど、これは合法手を生成するためのもの。
詳細は後述。

例外と定数の定義

まずはゲーム情報に関係する例外と定数の定義から。

  // 続き

  enum Error: ErrorType {
    case AlreadyInDeal
    case NotInDeal
    case OutOfRange
    case IllegalAction
    case AlreadyEnd
  }

  static let HandMax: Int = 10
  static let PlayMax: Int = 3
  static let LosePoint: Int = 22

  // 続く

例外は、メソッドの事前条件が満たされていないときに投げるものを定義している感じ。

定数はそれぞれ、手札の上限、プレイ(ディスカード)できるカード枚数の上限、負けになるマイナス点を定義している。

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

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

  // 続き

  private var deck: Deck
  private(set) var playerCount: Int

  private(set) var inDeal: Bool
  private var playerHands: [[Int]]

  private(set) var trickCount: Int
  private(set) var turnPlayerIndex: Int
  private(set) var actionsInTrick: [Action]

  private(set) var usedCardCount: [Int: Int]
  private(set) var minusPointCards: [[Int]]

  private(set) var isEnd: Bool

  private var observers: [ObjectIdentifier: GameInfoObserver]

  init(deck: Deck, playerCount: Int) {
    self.deck = deck
    self.playerCount = playerCount

    self.inDeal = false
    self.playerHands = [[Int]]()
    for _ in 1...self.playerCount {
      self.playerHands.append([Int]())
    }

    self.trickCount = 0
    self.turnPlayerIndex = 0
    self.actionsInTrick = [Action]()

    self.usedCardCount = [Int: Int]()
    for card in Deck.MinCard...Deck.MaxCard {
      self.usedCardCount[card] = 0
    }
    self.minusPointCards = [[Int]]()
    for _ in 1...self.playerCount {
      self.minusPointCards.append([Int]())
    }

    self.isEnd = false

    self.observers = [ObjectIdentifier: GameInfoObserver]()
  }

  // 続く

プロパティとしては、

  • デッキ
  • プレイヤー人数
  • ディール中かどうか
  • 各プレイヤーの手札
  • 何トリック目か(0, 1, ...)
  • 手番プレイヤー
  • トリック中に行われたアクション
  • 使われたカードの数(マイナス点マーカーとしてデッキから取り除かれたカードを含む)
  • 各プレイヤーのマイナス点マーカーのカード
  • ゲームが終わったかどうか

という情報を用意している。

それと、通知を行う対象のオブザーバも保持している。
(このオブザーバの保持の仕方については、SwiftでSetの型パラメータにプロトコルを指定する方法について。 - いものやま。変種オセロのUIを作ってみた。(その14) - いものやま。を参照)

オブザーバの登録、削除、通知

次に、オブザーバ周り。

  // 続き

  func addObserver(observer: GameInfoObserver) {
    self.observers[ObjectIdentifier(observer)] = observer
  }
  
  func removeObserver(observer: GameInfoObserver) {
    self.observers.removeValueForKey(ObjectIdentifier(observer))
  }
  
  func removeAllObservers() {
    self.observers.removeAll()
  }
  
  private func notifyObserversGameInfoDealtCard(card: Int,
                                                toPlayer playerIndex: Int) {
    for (_, observer) in self.observers {
      observer.gameInfoDealtCard(card, toPlayer: playerIndex)
    }
  }
  
  private func notifyObserversGameInfoDidAction(action: Action,
                                                byPlayer playerIndex: Int) {
    for (_, observer) in self.observers {
      observer.gameInfoDidAction(action, byPlayer: playerIndex)
    }
  }

  private func notifyObserversGameInfoResolvedTrick(trickCount: Int,
                                                    winner playerIndex: Int) {
    for (_, observer) in self.observers {
      observer.gameInfoResolvedTrick(trickCount, winner: playerIndex)
    }
  }
  
  private func notifyObserversGameInfoResolvedDeal(loserIndices: [Int],
                                                   lastCards: [Int]) {
    for (_, observer) in self.observers {
      observer.gameInfoResolvedDeal(loserIndices,
                                    lastCards: lastCards)
    }
  }
  
  private func notifyObserversGameInfoEnded() {
    for (_, observer) in self.observers {
      observer.gameInfoEnded()
    }
  }

  // 続く

特に難しいことも面白いこともなく。
あえて言えば、RubyObject#__send__()みたいなことがSwiftでも出来れば、もう少しコードが短くなるんだけどなぁ、というくらい。
Rubyはちゃんとメッセージ送信を行うので、メソッド名を動的に指定して呼び出すということが出来る)

カードの分配

次はカードの分配。

  // 続き
  
  func deal() throws {
    guard !self.inDeal else {
      throw GameInfo.Error.AlreadyInDeal
    }
    guard !self.isEnd else {
      throw GameInfo.Error.AlreadyEnd
    }

    let shuffledDeck = self.deck.shuffle()
    while (self.playerHands[0].count < GameInfo.HandMax) &&
          (shuffledDeck.count >= self.playerCount) {
      for i in 0..<self.playerCount {
        let drawnCard = try! shuffledDeck.draw()
        self.playerHands[i].append(drawnCard)
        self.notifyObserversGameInfoDealtCard(drawnCard, toPlayer: i)
      }
    }

    for i in 0..<self.playerCount {
      self.playerHands[i].sortInPlace { $0 < $1 }
    }
    
    self.inDeal = true
  }

  // 続く

すでにディール中だったり、ゲームが終了している場合には、例外を投げるようにしている。

処理としては、デッキをシャッフルして、ドローし、各プレイヤーの手札に加えていっている。
そして、オブザーバに対して通知を行っている。

プレイヤー・ビューの生成

次に、プレイヤー・ビューの生成。

  // 続き

  func playerViewFor(playerIndex: Int) throws -> PlayerView {
    guard (0 <= playerIndex) && (playerIndex < self.playerCount) else {
      throw GameInfo.Error.OutOfRange
    }

    return PlayerView(gameInfo: self, playerIndex: playerIndex)
  }

  // 続く

こちらは、不正なプレイヤーが指定されたときには例外を投げるようにしている。

プレイヤー・ビューの生成はGameInfo.PlayerViewのイニシャライザで行っているので、詳細は後で。

アクションの実行

最後に、アクションの実行。

  // 続き

  func doAction(action: Action) throws {
    guard self.inDeal else {
      throw GameInfo.Error.NotInDeal
    }
    guard !self.isEnd else {
      throw GameInfo.Error.AlreadyEnd
    }

    let playerView = PlayerView(gameInfo: self, playerIndex: self.turnPlayerIndex)
    let newPlayerView = try playerView.tryAction(action)
    self.playerHands[self.turnPlayerIndex] = newPlayerView.hands
    self.actionsInTrick = newPlayerView.actionsInTrick
    self.usedCardCount = newPlayerView.usedCardCount
    self.notifyObserversGameInfoDidAction(action, byPlayer: self.turnPlayerIndex)

    self.turnPlayerIndex += 1
    self.turnPlayerIndex %= self.playerCount

    if self.actionsInTrick.count == self.playerCount {
      self.resolveTrick()
      if self.playerHands[0].count == 1 {
        self.resolveDeal()
      }
    }
  }

  // 続く

ディール中でなかったり、あるいはゲームが終わってる場合には、例外を投げるようにしている。

そして、処理なんだけど、一度プレイヤー・ビューを作ってそこでアクションを実行し、アクションの実行で新たに得られたプレイヤー・ビューから逆に情報を反映させる、ということを行っている。
なんでこんなことを行っているのかというと、合法手のチェックの関係。
プレイヤーがアクションを選択するときに、アクションが合法手かどうかをプレイヤーは知ることが出来ないといけないので、プレイヤー・ビューには合法手チェックのメソッドを用意する必要がある。
けど、それをゲーム情報の方でも実装するのは面倒。
そこで、プレイヤー・ビューの方でアクションを行い、そのときに合法手のチェックもするようにすれば、ゲーム情報側で合法手のチェックをする必要がなくなる。
そして、プレイヤーがアクションを行えば当然プレイヤーから見えている情報も更新されるので、逆にそのプレイヤーから見えている情報を使うことで大元のゲーム情報も更新することが出来る、と。
なお、プレイヤーから見えていない情報で更新されるものももちろんあるので、それについてはゲーム情報側で更新している。

こうしてアクションを実行したあとは、オブザーバにプレイヤーがアクションしたことを通知。
そして、トリックが終わっていればトリックの解決処理を、さらに、残りの手札が1枚になっていればディールの解決処理を行うようにしている。

トリックの解決

トリックの解決は、以下のとおり。

  // 続き

  private func resolveTrick() {
    var winnerOffset: Int = 0
    for offset in 0..<self.playerCount {
      let action = self.actionsInTrick.removeAtIndex(0)
      switch action {
      case .Play:
        winnerOffset = offset
      case .Discard:
        break
      }
    }
    self.turnPlayerIndex += winnerOffset
    self.turnPlayerIndex %= self.playerCount
    self.notifyObserversGameInfoResolvedTrick(self.trickCount,
                                              winner: self.turnPlayerIndex)

    self.trickCount += 1
  }

  // 続く

一番最後にプレイできたプレイヤーがトリックを取ったことになるので、そのプレイヤーを探し、次のリードプレイヤーにしている。
そして、トリックが終わったことをオブザーバに通知。
最後に、トリックの数を1増やしている。

ディールの解決

そして、ディールの解決は以下のとおり。

  // 続き

  private func resolveDeal() {
    var lastCards: [Int] = [Int]()
    for i in 0..<self.playerCount {
      lastCards.append(self.playerHands[i][0])
    }

    var loserIndices: [Int] = [Int]()
    var highestCard = 0
    for offset in 0..<self.playerCount {
      let playerIndex = (self.turnPlayerIndex + offset) % self.playerCount
      let card = self.playerHands[playerIndex].removeAtIndex(0)
      if card > highestCard {
        highestCard = card
        loserIndices = [playerIndex]
      } else if card == highestCard {
        loserIndices.append(playerIndex)
      }
    }

    for loserIndex in loserIndices {
      self.minusPointCards[loserIndex].append(highestCard)
      try! self.deck.removeCard(highestCard)

      var minusSum: Int = 0
      for minusPoint in self.minusPointCards[loserIndex] {
        minusSum += minusPoint
      }
      if minusSum >= GameInfo.LosePoint {
        self.isEnd = true
      }
    }

    self.notifyObserversGameInfoResolvedDeal(loserIndices,
                                             lastCards: lastCards)
    if self.isEnd {
      self.notifyObserversGameInfoEnded()
    }

    self.turnPlayerIndex = loserIndices.removeLast() + 1
    self.turnPlayerIndex %= self.playerCount
    self.usedCardCount = self.deck.removedCardCount
    self.trickCount = 0
    self.inDeal = false
  }

  // 続く

まずは、オブザーバに渡す情報として、各プレイヤーの最後の手札を配列に用意している。

次に、各プレイヤーの最後の手札を比べて、このディールで負けたプレイヤーを決定する。

負けたプレイヤーが決定したら、そのプレイヤーの出したカードをマイナス点マーカーとして処理。
このとき、もしマイナス点の合計が22点以上になっていれば、ゲーム終了のフラグを立てる。

そのあとは、オブザーバにディールが終わったことの通知。
ゲームが終了していれば、ゲームが終わったことも通知する。

最後に、次のディールの準備。
このディールで負けたプレイヤーのうち、一番最後のプレイヤーの左隣を次のリードプレイヤーにして、使われたカードの数やトリック数をセットして、ディール中かどうかのフラグをfalseにしている。


ここまででゲーム情報の実装は終わり。
ただ、GameInfo.swiftはもうちょっと続いて、プレイヤー・ビューの実装も行っている。
これについては、また明日。

今日はここまで!