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

いものやま。

雑多な知識の寄せ集め

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

ゲーム開発 Swift BirdHead

昨日はゲーム情報の実装を行った。

今日はプレイヤー・ビューの実装を行っていく。

GameInfo.PlayerViewクラス

プレイヤー・ビューはGameInfoクラスの内部クラスとして定義していく。

  // 続き

  class PlayerView {

  // 続く

なお、YWFのBoardと同じく、本質的には不変なオブジェクトとして作るので、本当は構造体にしたいところなんだけど、そうした場合、遅延格納型プロパティの参照がオブジェクトを変化させてしまうので、定数に代入することが出来なくなってしまうということで、クラスにしている。

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

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

 // 続き

    private(set) var hands: [Int]

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

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

    private init(gameInfo: GameInfo, playerIndex: Int) {
      self.hands = gameInfo.playerHands[playerIndex]
      self.trickCount = gameInfo.trickCount
      self.actionsInTrick = gameInfo.actionsInTrick
      self.usedCardCount = gameInfo.usedCardCount
      self.minusPointCards = gameInfo.minusPointCards
    }

    private init(playerView: PlayerView) {
      self.hands = playerView.hands
      self.trickCount = playerView.trickCount
      self.actionsInTrick = playerView.actionsInTrick
      self.usedCardCount = playerView.usedCardCount
      self.minusPointCards = playerView.minusPointCards
    }

  // 続く

基本的にはゲーム情報と同じなんだけど、手札は自分の手札だけが見えるようになっている。

合法手の生成

さて、ここからが肝心の合法手の生成。

  // 続き

    private(set) lazy var legalActions: [Action] = {
      [unowned self] in
      var actions: [Action] = []
      if self.actionsInTrick.count == 0 {
        var startIndex: Int = 0
        var nextIndex: Int = 1
        while startIndex < self.hands.count {
          let card = self.hands[startIndex]
          while (nextIndex < self.hands.count) &&
                (card == self.hands[nextIndex]) {
            nextIndex += 1
          }
          let maxCount = min(GameInfo.PlayMax, nextIndex - startIndex, self.hands.count - 1)
          for count in 1...maxCount {
            actions.append(Action.play([Int](count: count, repeatedValue: card)))
          }
          startIndex = nextIndex
          nextIndex += 1
        }
      } else {
        var lastPlayedCards: [Int] = []
        for action in self.actionsInTrick {
          switch action {
          case let .Play(cards):
            lastPlayedCards = cards
          case .Discard:
            break
          }
        }
        let myLowestCards = [Int](self.hands[0..<lastPlayedCards.count])
        let cardIndicesArray = playableCardIndicesArray(self.hands, lastPlayedCards, 0)
        for cardIndices in cardIndicesArray {
          var cards: [Int] = []
          for index in cardIndices {
            cards.append(self.hands[index])
          }
          if (actions.count == 0) && (cards != myLowestCards) {
            actions.append(Action.discard(myLowestCards))
          }
          actions.append(Action.play(cards))
        }
        if actions.count == 0 {
          actions.append(Action.discard(myLowestCards))
        }
      }
      return actions
    }()

  // 続く

合法手の生成は、2つの場合に分けられる:

  1. 自分がリードプレイヤーの場合
  2. 自分がリードプレイヤーでない場合
自分がリードプレイヤーの場合

リードプレイヤーの場合は、手札のそれぞれの強さのカードについて、1枚(可能なら2枚、3枚)プレイするというのが、合法手になる。

そこで、手札がソートされていることを前提として(これはGameInfoで手札を生成するときにソートしているので大丈夫)、それぞれの強さのカードの枚数を調べ、その枚数に応じて、1枚プレイ、2枚プレイ、3枚プレイのアクションを生成している。
(このとき、少なくとも手札が1枚は残るようにもしている)

自分がリードプレイヤーでない場合

こちらはけっこう難しい・・・

まず、現状で一番最後にプレイされたカードの組合せを調べる。
そうすると、合法手は、そのプレイされたカードの組合せ以上の組合せ、もしくは、手札で一番弱いカードの組合せとなる。

さて、この「一番最後にプレイされたカードの組合せ以上の組合せ」を作るのが、思いの外難しい・・・
数学的に記述すると、

  • 一番最後にプレイされたカードの組合せが (a)なら、 \left\{ (x) \: | \: x \in hands, x \ge a \right\}
  • 一番最後にプレイされたカードの組合せが (a, b) \: (\mbox{ただし、} a \le b)なら、 \left\{ (x, y) \: | \: x, y \in hands, x \le y, x \ge a, y \ge b \right\}
  • 一番最後にプレイされたカードの組合せが (a, b, c) \: (\mbox{ただし、} a \le b \le c)なら、 \left\{ (x, y, z) \: | \: x, y, z \in hands, x \le y \le z, x \ge a, y \ge b, z \ge c \right\}

なんだけど、こうやって記述される集合を作ろうとなると、一苦労。

こういったとき、関数型言語なら簡単だよなぁ、とも思うのだけど、まぁ、それはそれ。
ただここは、「関数型言語コンパイラの気持ち」になることで、実装が見えてくる。

関数型言語コンパイラなら、これをどう処理するかな、と考えると、例えば3つの組合せのときなら次のように再帰的に考えるはず:

 \left\{ \left(x, (y, z) \right) \: | \: x \in hands, x \ge a, (y, z) \in \left\{ \mbox{省略。ただし、} hands \mbox{は} x \mbox{より後ろの部分的な手札} \right\} \right\}

そこで、次のような再帰的に呼び出されるprivateな関数を、ファイルの先頭で定義しておく。

/* NOTE:
 * if last played card is [a], playable card index [i] is
 *   (hands[i] >= a)
 * if last played cards is [a, b], playable card indices [i, j] is
 *   (i < j) && (hands[i] >= a) && (hands[j] >= b)
 * if last played cards is [a, b, c], playable card indices [i, j, k] is
 *   (i < j < k) && (hands[i] >= a) && (hands[j] >= b) && (hands[k] >= c)
 * so [i], [i, j], [i, j, k] is described recursively
 *   - [i]
 *     (0 <= i) && (hands[i] >= a)
 *   - [i, j]
 *     (0 <= i) && (hands[i] >= a)
 *       && ((i+1) <= j) && (hands[j] >= b)
 *   - [i, j, k]
 *     (0 <= i) && (hands[i] >= a)
 *       && ((i+1) <= j) && (hands[j] >= b)
 *         && ((j+1) <= k) && (hands[k] >= c)
 */
private func playableCardIndicesArray(hands: [Int],
                                      _ lowerBound: [Int],
                                      _ offset: Int) -> [[Int]] {
  var ret: [[Int]] = []

  var newLowerBound = lowerBound
  let lowerCard = newLowerBound.removeAtIndex(0)

  var i = offset
  while i < hands.count {
    let card = hands[i]
    if card >= lowerCard {
      if newLowerBound.count == 0 {
        ret.append([i])
      } else if (i+1) < hands.count {
        let subCardIndicesArray = playableCardIndicesArray(hands,
                                                           newLowerBound,
                                                           i+1)
        for subCardIndices in subCardIndicesArray {
          var cardIndices = subCardIndices
          cardIndices.insert(i, atIndex: 0)
          ret.append(cardIndices)
        }
      }
    }

    // to return uniq combination:
    // for example, if hands is [2, 4_1, 4_2, 5, 6] and
    // lowerBound is [4, 4], this functions should return
    // [[4_1, 4_2], [4_1, 5], [4_1, 6], [5, 6]], not
    // [[4_1, 4_2], [4_1, 5], [4_1, 6], [4_2, 5], [4_2, 6], [5, 6]].
    // so skip same card.
    i += 1
    while (i < hands.count) && (card == hands[i]) {
      i += 1
    }
  }

  return ret
}

なお、先の数式の「 xより後ろの部分的な手札」の指定が簡単になるように、手札の数字そのものではなくて、インデックスを扱うようにしている。

やっていることは、まず条件を満たすような xのインデックスを見つけ、そのそれぞれについて、それより後ろの手札でサブ条件を満たす組合せの配列を得て、得られた組合せの先頭に xのインデックスを付け加える、というもの。
これでプレイ可能なカードのインデックスの組合せが全て得られる。

あとは、ここから合法手としてアクションを生成するだけ。
このとき、プレイ可能なカードのインデックスの組合せが手札の先頭でない場合には、ディスカードのアクションも追加している。
(手札の先頭がプレイ可能なカードの組合せになっている場合、ディスカードというアクションは存在しなくなることに注意)
それと、もしプレイ可能なカードのインデックスの組合せが存在しない場合も、ディスカードのアクションを追加している。
(この場合、選択できるのはディスカードのアクションだけとなる)

合法手のチェック

合法手の生成が出来たら、合法手のチェックは簡単。

  // 続き

    func isLegalAction(action: Action) -> Bool {
      return self.legalActions.indexOf(action) != nil
    }

  // 続く

Array#indexOf()は、引数で指定された要素が含まれていればそのインデックスを、そうでなければnilを返す。
なので、戻り値をnilと比較することで、指定されたアクションが合法手に含まれているかどうかが分かる。

アクションの実行

最後に、アクションの実行。
ただし、これを行ってもプレイヤー・ビューは不変で、新しいプレイヤー・ビューを生成するだけ。
感覚としては、「試しにこのアクションを行ってみると、どんなプレイヤー・ビューになるかな?」という感じのメソッドになっている。

  // 続き

    func tryAction(action: Action) throws -> PlayerView {
      guard self.isLegalAction(action) else {
        throw GameInfo.Error.IllegalAction
      }

      let newPlayerView = PlayerView(playerView: self)
      newPlayerView.actionsInTrick.append(action)

      let usedCards: [Int]
      switch action {
      case let .Play(cards):
        usedCards = cards
      case let .Discard(cards):
        usedCards = cards
      }

      var searchStartIndex = 0
      for usedCard in usedCards {
        newPlayerView.usedCardCount[usedCard]! += 1
        for i in searchStartIndex..<newPlayerView.hands.count {
          if usedCard == newPlayerView.hands[i] {
            _ = newPlayerView.hands.removeAtIndex(i)
            searchStartIndex = i
            break
          }
        }
      }

      return newPlayerView
    }
  }
}

まず最初に合法手のチェック。
合法手でなければ、例外が投げられるようになっている。

そのあとは、新しいプレイヤー・ビューを生成して、アクションの結果を反映していくだけ。

動作確認

さて、これでゲーム情報とプレイヤー・ビューの実装も出来たので、試しにちょっと動かしてみる。

// GameInfoTest.swift

import Foundation

let deck = Deck()
let game = GameInfo(deck: deck, playerCount: 4)

try! game.deal()
for i in 0..<game.playerCount {
  let playerView = try! game.playerViewFor(i)
  print("[Player \(i)]")
  print("hand: \(playerView.hands)")
}

for i in 0..<game.playerCount {
  let playerView = try! game.playerViewFor(i)
  let legalActions = playerView.legalActions
  print("legal actions: \(legalActions)")
  let action = legalActions[Random.getUniformedRandom(legalActions.count)]
  print("action: \(action)")
  try! game.doAction(action)
}

インスタンスを作って、ディール。
そして、各プレイヤーに1回ずつランダムな合法手を実行させるだけ。

Makefileを修正してビルドし、実行すると、以下のような感じ。

$ ./GameInfoTest 
[Player 0]
hand: [2, 3, 4, 4, 5, 6, 8, 8, 8, 10]
[Player 1]
hand: [3, 4, 5, 6, 7, 7, 9, 10, 10, 11]
[Player 2]
hand: [2, 3, 4, 4, 6, 8, 9, 9, 10, 11]
[Player 3]
hand: [2, 3, 3, 5, 6, 7, 8, 9, 10, 11]
legal actions: [GameInfoTest.Action.Play([2]), GameInfoTest.Action.Play([3]), GameInfoTest.Action.Play([4]), GameInfoTest.Action.Play([4, 4]), GameInfoTest.Action.Play([5]), GameInfoTest.Action.Play([6]), GameInfoTest.Action.Play([8]), GameInfoTest.Action.Play([8, 8]), GameInfoTest.Action.Play([8, 8, 8]), GameInfoTest.Action.Play([10])]
action: Play([8, 8, 8])
legal actions: [GameInfoTest.Action.Discard([3, 4, 5]), GameInfoTest.Action.Play([9, 10, 10]), GameInfoTest.Action.Play([9, 10, 11]), GameInfoTest.Action.Play([10, 10, 11])]
action: Play([9, 10, 11])
legal actions: [GameInfoTest.Action.Discard([2, 3, 4]), GameInfoTest.Action.Play([9, 10, 11])]
action: Play([9, 10, 11])
legal actions: [GameInfoTest.Action.Discard([2, 3, 3]), GameInfoTest.Action.Play([9, 10, 11])]
action: Play([9, 10, 11])

ちゃんと合法手が生成されていて、プレイも出来ていることが分かる。

今日はここまで!