いものやま。

雑多な知識の寄せ集め

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

昨日はトランプゲーム「22」のルールをちょっと変えてテーマを乗せたゲーム「BirdHead」を考えた。

今日からは、このゲームを実際に(CUIで)遊べるようにしてみる。

カード、デッキ

まずは、カードとデッキを実装するところから。

といっても、BirdHeadの場合、カードは数字しか見ないので、これはただのIntでOK。
これが普通のトランプゲームだとスートとランクをもったオブジェクトにする必要があるし、22の場合もJ, Q, Kはマイナス点数が10点でカードの強さと別なので、クラスを作った方がよさそう。

そして、デッキの実装。

こちらは、マイナス点のマーカーとなったカードをデッキから取り除く必要があったり、ランダムにシャッフルしてプレイヤーに配る必要があるので、クラスを作った方がいい。

ここで、実装に2通りの方法が考えられて、

  • マイナス点のマーカーとしてデッキから取り除かれたカードの情報は、Deckクラスのクラスプロパティとして保持する。
    • そのDeckクラスからインスタンスを作ると、取り除かれたカード以外のカードをシャッフルしたDeckオブジェクトが作られる。
  • マイナス点のマーカーとしてデッキから取り除かれたカードの情報は、Deckオブジェクトのプロパティとして保持する。
    • そのDeckオブジェクトから、取り除かれたカード以外のカードをシャッフルしたShuffledDeckオブジェクトを作る。

最初は前者の方法で実装してみたのだけど、この場合、「Deckクラス」というクラスオブジェクトはプログラム中に1つしか存在できなくて、また、Swiftでクラスオブジェクトを一つのオブジェクトとして変数や定数から参照する方法がよく分からなかったので、後者へ切り替えた。
(というのは、ゲームの情報を保存することを考えたときに、クラスオブジェクトを保存するとなると、上記のような問題でちょっと頭が痛いから・・・これがクラスオブジェクトでなくて普通のオブジェクトなら、あまり難しく考える必要がない)

Randomクラス

さて、デッキを実装する前に、乱数生成器のクラスを定義しておく。
というのは、カードをシャッフルするときに必要になるし、のちのちランダムAIを実装しようとしたときにやっぱり必要になるので、クラスとして定義しておこうかな、と。
(もっとも、ここでいうクラスはRubyでいうモジュールだけど)

//==============================
// BirdHead
//------------------------------
// Random.swift
//==============================

import Foundation
import Security

class Random {
  static func getUniformedRandom(upperBound: Int) -> Int {
    let upper = UInt32(upperBound)
    let ignoreRegion = 0xffff_ffff - 0xffff_ffff % upper

    while true {
      let buf = UnsafeMutablePointer<UInt32>.alloc(1)
      SecRandomCopyBytes(kSecRandomDefault, 4, UnsafeMutablePointer<UInt8>(buf))
      let randomValue = buf.memory
      buf.dealloc(1)
      if randomValue < ignoreRegion {
        return Int(randomValue % upper)
      }
    }
  }

  private init() {}
}

乱数を生成しているコードの説明は、変種オセロをSwiftに移植してみた。(その10) - いものやま。を参照。

なお、このクラスをインスタンス化して使うことはないので、イニシャライザは呼べないようにしている。

Deckクラス

乱数生成器のクラスも用意できたので、いよいよDeckクラスの実装。

//==============================
// BirdHead
//------------------------------
// Deck.swift
//==============================

import Foundation

class Deck {

// 続く

例外の定義

まずは、デッキに関係する例外を定義しておく。
Swift 2.0では例外を使えるようになっていて、例外を定義する場合、ErrorTypeを継承した列挙型として定義することになっている。

  // 続き

  enum Error: ErrorType {
    case OutOfRange
    case NoCard
  }

  // 続く

それぞれの例外は、Deck.Error.OutOfRangeDeck.Error.NoCardといったふうに表現される。
それぞれ、引数で指定されたカードが範囲外であること(=2〜11でないこと)、カードがないこと、を意味する例外として用意した。

定数の定義

次に、デッキに関係した定数の定義。

  // 続き

  static let MinCard: Int = 2
  static let MaxCard: Int = 11
  static let CardCount: Int = 5

  // 続く

定数はそれぞれ、カードの最小値、カードの最大値、それぞれのカードの枚数。

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

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

  // 続き

  var removedCardCount: [Int: Int]
  var removedCards: [Int]

  init() {
    self.removedCardCount = [Int: Int]()
    for card in Deck.MinCard...Deck.MaxCard {
      self.removedCardCount[card] = 0
    }
    self.removedCards = [Int]()
  }

  // 続く

Deckクラスが持つ情報は、マイナス点マーカーとしてゲームから除外されたカードの情報。
これを、それぞれのカードの取り除かれた枚数と、取り除かれたカード全体の配列として参照することが出来るようにしている。

イニシャライザでは、それぞれのカードの取り除かれた枚数は0枚、取り除かれたカード全体の配列は空の配列として初期化している。

カードの削除、シャッフル、リセット

次は、Deckクラスの各メソッド。

  // 続き

  func removeCard(card: Int) throws {
    guard (Deck.MinCard <= card) && (card <= Deck.MaxCard) else {
      throw Deck.Error.OutOfRange
    }
    guard self.removedCardCount[card]! < Deck.CardCount else {
      throw Deck.Error.NoCard
    }
    self.removedCardCount[card]! += 1
    self.removedCards.append(card)
    self.removedCards.sortInPlace { $0 < $1 }
  }

  func shuffle() -> ShuffledDeck {
    return ShuffledDeck(deck: self)
  }

  func reset() {
    for (card, _) in self.removedCardCount {
      self.removedCardCount[card] = 0
    }
    self.removedCards = [Int]()
  }

  // 続く

それぞれ、ゲームからカードを取り除くメソッド、シャッフルしたデッキを作るメソッド、取り除いたカードをデッキに戻して最初の状態に戻すメソッド。

ちょっと注目したいのは、Deck#removeCard(_: Int)guard節。
これはSwift 2.0で追加された機能で、引数やオブジェクトの状態をチェックして、事前条件を満たしていない状態で呼び出された場合に、単に制御を呼び出し元に戻したり、あるいは例外を投げたりすることが出来るようになっている。
ゲームからカードを取り除く場合、範囲外のカードを指定されたり、すでになくなっているカードをさらに取り除くような呼び出しがくるとダメなので、そういったときには例外を投げるようにしている。
(こうやって例外を投げることが出来るようになったので、どこもかしもオプショナル型になって、nilチェックやオプショナルバインドだらけの可読性の低いコードになるのを防ぐことが出来るようになった)

Deck.ShuffledDeckクラス

Deckクラスはゲームから取り除かれたカードの情報を保持しているだけなので、実際に使うには、そこからシャッフルされたデッキを作る必要がある。
そのシャッフルされたデッキを表すクラスとして、Deckの内部クラスとしてDeck.ShuffledDeckクラスを実装していく。

  // 続き

  class ShuffledDeck {

  // 続く

なお、Swiftの内部クラスは外部クラスが単に名前空間として働くだけで、Javaの内部クラスのように外部クラスと密接な関わり合いがあるというわけではない。
(可視性の指定についても、Swiftはファイル/モジュール単位なので、Javaの内部クラスのようにしなくても、定義されているファイルが同じであれば、privateなプロパティにアクセスできる)
ただ、Deck.swiftというファイルで定義しているので、その中にprivateでないShuffledDeckというクラスがトップレベルで定義されているのも微妙。
もちろん、ShuffledDeck.swiftというファイルで定義することも可能なんだけど(特に、今回は可視性の制限もない)、そうするとこの2つに継承関係があるようにも見えるので、それも微妙。
ということで、今回は内部クラスとして定義することにしている。

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

さて、そんなDeck.ShuffledDeckクラスだけど、プロパティとイニシャライザは以下のとおり。

  // 続き

    private var cards: [Int]
    var count: Int {
      return self.cards.count
    }

    private init(deck: Deck) {
      self.cards = [Int]()
      for card in Deck.MinCard...Deck.MaxCard {
        if let removedCount = deck.removedCardCount[card] {
          if removedCount < Deck.CardCount {
            for _ in (removedCount+1)...Deck.CardCount {
              self.cards.append(card)
            }
          }
        }
      }

      for i in 0..<(self.cards.count-1) {
        let j = Random.getUniformedRandom(self.cards.count-i-1) + i
        let tmp = self.cards[i]
        self.cards[i] = self.cards[j]
        self.cards[j] = tmp
      }
    }

  // 続く

プロパティとして保持しているのは、カードの配列。
それと、計算型プロパティとして、カードの枚数を知ることが出来るようにしている。

イニシャライザでは、まずデッキの取り除かれたカードの情報から、残っているカードを配列として用意している。
そして、そのあとシャッフル。
シャッフルのアルゴリズムは、先頭のカードから順番に、そのカード以降のランダムなカードと交換するというものを使った。
(実際に動かしてみると、思った以上にちゃんとシャッフルしてくれる)

カードのドロー

さて、最後にカードのドロー。

  // 続き

    func draw() throws -> Int {
      guard self.cards.count > 0 else {
        throw Deck.Error.NoCard
      }
      return self.cards.removeAtIndex(0)
    }
  }
}

やっているのは、シャッフルされたカードの先頭から1枚カードを取り除いて返しているだけ。
なお、デッキにカードがもうない場合、例外を投げるようにしている。

動作確認

さて、動作確認。
簡単なテストコードを書いて動かしてみる。

// DeckTest.swift

import Foundation

func showRemovedCardCount(deck: Deck) {
  for card in Deck.MinCard...Deck.MaxCard {
    print("removed \(card): \(deck.removedCardCount[card]!)")
  }
}

func showRemovedCards(deck: Deck) {
  print("removed cards: \(deck.removedCards)")
}

let deck = Deck()

showRemovedCardCount(deck)
showRemovedCards(deck)

try! deck.removeCard(5)
try! deck.removeCard(3)
try! deck.removeCard(8)
try! deck.removeCard(5)
showRemovedCardCount(deck)
showRemovedCards(deck)

do {
  try deck.removeCard(2)
  print("remove card 2.")
  try deck.removeCard(1)
  print("remove card 1.")
} catch Deck.Error.OutOfRange {
  print("out of range.")
}

do {
  try deck.removeCard(11)
  print("remove card 11.")
  try deck.removeCard(12)
  print("remove card 12.")
} catch Deck.Error.OutOfRange {
  print("out of range.")
}

deck.reset()

do {
  try deck.removeCard(2)
  print("remove card 2.")
  try deck.removeCard(2)
  print("remove card 2.")
  try deck.removeCard(2)
  print("remove card 2.")
  try deck.removeCard(2)
  print("remove card 2.")
  try deck.removeCard(2)
  print("remove card 2.")
  try deck.removeCard(2)
  print("remove card 2.")
} catch Deck.Error.NoCard {
  print("no card.")
}

let shuffledDeck = deck.shuffle()
do {
  while true {
    let card = try shuffledDeck.draw()
    print("\(card), ", terminator: "")
  }
} catch Deck.Error.NoCard {
  print("no card.")
}

Swift 2.0では、例外を投げる可能性のあるメソッドを実行する場合、trytry?、もしくはtry!をつけて実行することになっている。

  • tryをつけて実行した場合、例外が発生すると例外オブジェクトが投げられるので、do-catchで例外を補足して処理するか、あるいはさらに呼び出し元に例外の処理を委譲する。
    例外が発生する可能性が普通にあり、そのときには通常フローと別の処理が必要になる場合、これを使うといい。
  • try?をつけて実行した場合、例外が発生するとnilが返される。(つまり、戻り値の型がオプショナル型になる)
    例外が発生する可能性があるけど、その場合に特別な処理が必要というわけではなく、単にそれ以降の処理を行わないようにするといった場合、これを使うといい。
  • try!をつけて実行した場合、例外が発生するとアプリが終了する。
    事前にチェックをしていて、例外が発生し得ない(発生したとしたらプログラミングミス)という場合、これを使うといい。

そして、ビルドして実行したいのだけど、毎回ファイルを指定してコンパイルするのだと大変なので、Makefileを用意。

# source files of module
SOURCE = Random.swift Deck.swift

# source files for test
TEST_SOURCE = DeckTest.swift

# test application name (DON'T EDIT)
TEST_APP = $(TEST_SOURCE:%.swift=%)

# make rules (DON'T EDIT)

testbuild: $(TEST_APP)

$(TEST_APP): $(SOURCE)

$(TEST_APP):%:%.swift
  cp $< main.swift
  xcrun -sdk macosx swiftc -o $@ main.swift $(SOURCE)
  rm main.swift

これを使ってビルドし、実行してみると、以下のような感じ。

$ make
cp DeckTest.swift main.swift
xcrun -sdk macosx swiftc -o DeckTest main.swift Random.swift Deck.swift
rm main.swift

$ ./DeckTest 
removed 2: 0
removed 3: 0
removed 4: 0
removed 5: 0
removed 6: 0
removed 7: 0
removed 8: 0
removed 9: 0
removed 10: 0
removed 11: 0
removed cards: []
removed 2: 0
removed 3: 1
removed 4: 0
removed 5: 2
removed 6: 0
removed 7: 0
removed 8: 1
removed 9: 0
removed 10: 0
removed 11: 0
removed cards: [3, 5, 5, 8]
remove card 2.
out of range.
remove card 11.
out of range.
remove card 2.
remove card 2.
remove card 2.
remove card 2.
remove card 2.
no card.
4, 10, 11, 5, 11, 3, 8, 11, 7, 5, 11, 7, 9, 6, 9, 3, 3, 6, 9, 7, 5, 9, 4, 5, 10, 3, 7, 6, 8, 10, 5, 10, 3, 6, 10, 4, 8, 6, 7, 4, 8, 9, 4, 8, 11, no card.

ちゃんと動作しているかはコードと見比べる必要があるけど、とりあえずちゃんと動いていることがこれで確認できる。
(ホントはちゃんと単体テスト作らないとなんだけどね・・・)

今日はここまで!