RAS Syndrome

冗長。

ゲームとは何か

趣味の一環で、ボードゲームに関するプログラムを書いた。

書きながら、「色々なボードゲームに汎用的に適用できる書き方はどんなだろう」と思考を巡らせ続けた。 あまりに凝りすぎて、「ボードゲームとは何なのか?」「そもそもゲームって何?」ってところまで考える羽目になった。

ゲームといっても色々だ。 デジタルなテレビゲームもあれば、アナログなボードゲームもある。 スポーツだってゲームな気がするし、鬼ごっこも多分ゲームだ。 私が学生時代に友人とよくやっていた、「1から20の中から好きな数を同時に言い合い、一番大きかった人が勝ち」というゲームだってゲームだろう。

学術的な定義とかは、おそらくもう決まっている。 だが、私自身はそんな定義を知らなくてもなんとなく、「これはゲームだ」「これはゲームじゃない」と判断している。

今回はその感覚を分析し、整理し、"個人的なゲームの定義"としてまとめてみた。

以下は目次。

要素の洗い出し

ゲームに一番必須な要素とは何か。

おそらくプレイヤーでは無いだろうか。

じゃあプレイヤーが集まって何かわちゃわちゃやってたら全部ゲームだろうか。 そんなことは無い。 プレイヤーは皆、ルールに基づいて行動している。

ルールとは何だろう。 基本的にまずは、「何をして良いか」「何をしてはいけないか(又は、何をしたらペナルティが与えられるか)」だと思う。 一言で言えば、可能/禁止アクションといったところだろう。

さて、ここまでの定義で言えば"おままごと"もゲームに該当することになる。 しかし、私には"おままごと"がゲームであるようには思えない(人によって違うかもしれない)。 なぜなら、"おままごと"には勝敗が無いからだ。

では、勝敗を競うことがゲームの必須条件だろうか。 これもまた違うように思う。 例えば賭け麻雀は、何位になるか以上に「何点勝つか」「何点負けるか」をプレイヤーは意識する。 また、一人用ゲームでは競う相手が居ないが、それでも「クリアできたか」とか「クリアまでのタイム」だとかをプレイヤーは意識する。

要するに、必要なのはゲームに対する明確な結果の定義なのだ。 そしてこれはルールで定めるべきもう一つの大事な要素でもある。

プレイヤーはより良い結果の為に競いあう。 中には、「結果よりも楽しむ過程が大事」というプレイヤーがいたり、勝つことよりゲームを混乱させることを優先するトリッキーなプレイヤーもいたりするだろう。 しかし、だからといって結果の定義が無くてもいい訳では無い。 結果の定義がなければ、大多数のプレイヤーはアクション選択の指針を失い、ゲームはほとんど成立しなくなる。

尚、結果が必須だとしてしまうと、"おままごと"と同様に『どうぶつの森』などもゲームでは無いということになってしまう。 しかし私はそう言ってしまっても良いかなと思っている。 『どうぶつの森』は確かにテレビゲーム機をプラットフォームとしているが、趣旨は「バーチャル空間での生活体験の提供」だ。 私にとってそれは狭い意味での"ゲーム"では無い。 (一応注釈しておくが、『どうぶつの森』の価値を貶める為に言っているのでは無い。『どうぶつの森』は楽しい。)

要素の整理

ここまでで出てきたキーワードを並べよう。

  • プレイヤー
  • アクション
  • ゲーム結果
  • ルール
    1. 可能/禁止アクション
    2. 結果判定

ルールを少し深掘りする。

ルール1. 可能/禁止アクション

とはつまり、アクションに対し可能か禁止か判定する規則のことだ。 つまり、アクションに対しTrue or Falseのブール型を返す関数として表現できる。

何かプログラミング言語で表現してみよう。 私が知っている中で型の示し方が一番綺麗なのがScalaなので、Scalaで書いてみる。 知らない方でも雰囲気は伝わるだろう。

尚、私のScala歴は10分ぐらいだ。

def isAvailable(action: Action): Boolean

すると、これでは足りないことに気付く。 よく考えれば、Actionが可能かどうかはそれ単体では判断できないのだ。全てはゲームの状況次第である。 即ち、ここまでのアクションの積み重ねを考慮し、現在のゲームの状況を算出してから検討する必要がある。

def isAvailable(currentAction: Action, actionHistory: Array[Action])

とりあえずこれで進む。*1

ルール2. 結果判定

同様にScalaで表現する。

def getGameResult(actionHisotry: Array[Action]): GameResult

これもゲームの状況を算出する必要がある。算出したらそれに対し、ゲームの結果を返す。 上記の通り、結果は単純な勝敗とは限らないので、ゲームの種類に応じてGameResultは柔軟に形を変えることになる。

この返し値が、nullとか、NullObjectとか、"結果が出ない事を示すオブジェクト"とかであった場合に、ゲームが続行されるイメージだ。

説明変数「ゲームの状態」を導入

さて、上の手順において優秀なプログラマーの方々は、「ゲームの状況を算出する」という処理を早く共通化したくてうずうずしていることだろう。 せっかくなのでやってみる。 あまり深い意味はないが、呼び方をゲームの状態に変える。

// ルール3. ゲームの状態の算出法
def getGameState(actionHistory: Array[Action]): GameState

// ルール1. 可能/禁止アクション
def isAvailable(action: Action, state: GameState): Boolean

// ルール2. 結果判定
def getGameResult(state: GameState): GameResult

以下の図式となる。

旧ルール1 = 新ルール1 + 新ルール3
旧ルール2 = 新ルール2 + 新ルール3

これで各ルールは、より現実に即した形となった。

例えば将棋で、「1手目はこう指して、2手目はこう指して、...、71手目にこう指した時(=アクション履歴)は先手勝ちですか?」という聞き方は普通しないだろう。 それよりは、盤面(=ゲームの状態)を指し示して、「先手番でこうなったら先手勝ちですか?」と聞くのが普通だ。 つまり、ルール2.はゲームの状態を引数とする方が普通というわけだ。 この場合、ルール3.として「1手目はこう指して2手目は...」がどのような盤面に繋がっていくのかを追加で定義しなければならない。

注意したいのは、これはただのリファクタリングだということだ。 ルールの使い勝手がよくなったというだけで、これらのルールで表現できるゲームの幅が広がったというわけではない。

新ルールで表現されたゲームを旧ルールで表現し直すには、今のリファクタリングの逆を行えば良い。 例えばオセロなら、

## ルール1. 可能な手
8x8マスのボードがあるとする。(マス目の表記法については省略。)
はじめにe4d5に黒石、d4e5に白石が置いてあるとする。
石が置かれると、同じ色で挟まれた石は、挟んだ石と同じ色に変わる。

棋譜を「置かれた石の位置の履歴」と解釈し、上記規則に則ってボードに石が置かれたと想定すると、手番プレイヤーは以下の手を打つことが可能である。(手番の説明は省略。)

* 自色の石を置くと敵石の色を挟めるような位置を宣言する。(又は、ボードがある場合そこに石を置き、ボードの状態を上記規則に従って変更する。)

## ルール2. 結果判定
8x8マスのボードがあるとする。(マス目の表記法については省略。)
はじめにe4d5に黒石、d4e5に白石が置いてあるとする。
石が置かれると、同じ色で挟まれた石は、挟んだ石と同じ色に変わる。

棋譜を「置かれた石の位置の履歴」と解釈し、上記規則に則ってボードに石が置かれたと想定すると、以下のように勝敗を判定することができる。
* ボードに石が置かれていないマスがある場合、勝敗未決(試合続行)。
* 上記以外の場合、自色の石がより多くボードに置かれていた方のプレイヤーが勝ち。

というWETなルールとなる。 まるでボードを実際に使ってはいけないかのような、脳内オセロ限定みたいな書き方になった。 (しかしボードを置いてはいけないとは言っていない。普通のオセロにも対応できる。)

今やったことはまるでトランスパイルである。 ES6のコードをES5で表現し直しているようなものだ。 しかし、ES6とES5でできることは本質的には変わらない。 それと同じだ。 新ルールは旧ルールと比べ、シンタックスが増えて便利になっただけなのである。 表現の幅は本質的には変わっていない。

まあ、だからどうしたということは特に無いのだが。 そんなわけでとりあえず、リファクタリングを更に進める。

アクションの再解釈

これまでアクションの定義は特にしなかった(=プリミティブな型として扱っていた)。 しかしゲームの状態という変数を導入した今、アクションの意味合いが表現できるようになる。 アクションとは、ゲームの状態を変更するもの。 つまり変更前のゲーム状態に対して変更後のゲーム状態を返す関数だ。

def action(state: GameState): GameState

ちなみにGameStateはイミュータブルなものとして考えている。 GameStateがミュータブルで、自由に書き換えていいのなら、別に値を返す必要はない。

さて、ゲームによってアクションは色々考えられる。 将棋なら「76歩」もあれば「投了」もある。 ボクシングなら「右手を相手のこめかみに向かって右上水平角30°から時速40kmでぶつける」等といくらでも複雑になるだろう。

とりあえず、ゲーム毎にアクションのバリエーションはある程度決まってくる。 ここではアクションの生成法を定義することで、アクションに固有の形式を与えてみよう。

def createAction(args: Any): GameState => GameState

アクションの型は基本的にGameState => GameStateな関数なのだが、実際はGameState => GameStateの中でもある一定のパターンしかあり得ない。 それをこの高階ファクトリ関数によって示している。 これはつまり、より狭い意味での型を定義している、ってことになるんじゃないかなぁと思ったり思わなかったり。 ファクトリ関数が型の役割をするというのは、JavaScriptの発想から影響を受けた。

まあしかし、この辺は言語によって表現方法が変わってくるだろう。 関数をファーストクラスとして扱うのが難しい場合は、アクションをオブジェクトにしてしまっても良い。 っていうかその方が綺麗な気もする。 以下は、「関数っぽい役割だけど実際はオブジェクト」という形でアクションを実装したRubyの例。

class Action
  def applyTo(state)
    raise 'Not implemented.'
  end
end

class AddSomethingAction < Action
  def initialize(something)
    @something = something
  end

  def applyTo(target)
    target + @something
  end
end

add5 = AddSomethingAction.new(5)
puts add5.applyTo(100)  # -> 105

ちなみにPythonだと、__call__メソッドをオーバーライドすることで、関数っぽく呼べるオブジェクトが作れる。 おそらくこれが最も綺麗だろう。

さて、これでアクションは、どういう状態変化を起こせるかを自身に定義できるようになった。 以前はこれらの定義は、ルール3の中で表現される想定であった。

// ルール3. ゲームの状態の算出法
def getGameState(actionHistory: Array[Action]): GameState

今やルール3の中身は、全てアクションの定義に委譲されている。

// ルール3-1: 初期状態
def initialState(): GameState

// ルール3-2: アクション定義
def createAction(args: Any): GameState => GameState

// ルール3. ゲームの状態の算出法
def getGameState(actionHistory: Array[GameState => GameState]): GameState = {
  actionHistory.foldLeft(initialState())((state, action) => action(state))
}

Scalaの実装に立ち入り過ぎて、さすがに勉強時間の追加を強いられた。 まあしかし、言いたいことは 「初期状態にアクション履歴内のアクションを全部順番に適用すれば、今のゲームの状態が得られるよ。」 ということだけだ。 もちろん、図式は以下のようになる。

ルール3 = ルール3-1 + ルール3-2

というわけで、ルールを分解して番号を振り直し、新体系としてまとめ直そう。

// ルール1: 初期状態
def initialState(): GameState

// ルール2: アクション定義
def createAction(args: Any): GameState => GameState

// ルール3. 可能/禁止アクション
def isAvailable(action: GameState => GameState, state: GameState): Boolean

// ルール4. 結果判定
def getGameResult(state: GameState): GameResult

先程と同様、こちらもリファクタリングに過ぎない。

物理ルールの存在

ルール2(アクション定義)について。 例えばジェンガについて考えてみよう。

def createAction(args: 力加減とか): GameState => GameState = {
  // 力加減が弱い場合:ジェンガの山がよほどグラグラしてない限り崩すことはないような関数を返す
  // 力加減が強い場合:どんなに安定していたジェンガの山もあっという間に崩すような関数を返す
}

みたいなイメージだ。 しかし、ジェンガの説明書にこんなことは書いていない。 書いてあるのは「抜いて崩れたら負け」ということだけだ。 これはつまり、「ジェンガを抜く」というアクションによって「ジェンガの山」という状態がどう変化するかを、物理法則に一任しているのである。

今回の思索の最終的な目標は、定義したゲームのルールをプログラムに落とし込むことである。 なので、物理空間では自明のことであっても、サイバー空間において自明でないことは全て明記する必要がある。 具体的に言えば、物理エンジンを実装しなければいけない。

これを踏まえた時に、特に注意しなければいけないのはゲームの状態の時間変化だ。 プレイヤーがアクションをせずとも状態は勝手に推移してしまうのである。 そのため、先程使用した以下の前提が使えなくなる。

// ゲームの状態の算出法
def getGameState(actionHistory: Array[GameState => GameState]): GameState = {
  actionHistory.foldLeft(initialState())((state, action) => action(state))
}

まあしかし、この辺を完全に反映させるのは骨が折れる。 というか折れたのは私の心です。 この辺の明記は今回は諦めることにする。

追加ルール「ゲームの状態のアクセシビリティ

物理法則について色々なパターンを考えてみると、その中の一つに 「伏せられているカードは見えない」 「相手の手札(手牌)は見えない」 というような、認知に関する物理法則があることに気付く。

プレイヤーが何を認知できるかは、ここまで見落としていた大事なファクターだ。 将棋やオセロ等は完全情報ゲームなので関係ない話だが、麻雀やトランプにおいては欠かせない。

一言で言うと、ゲームの状態のアクセシビリティである。 以下のように定義してみよう。

def getGameStateInformation(state: GameState, player: Player): GameStateInformation

PlayerGameStateにアクセスことはできない。 代わりにgetGameStateInformation関数を通し、GameStateInformationを得ることができる。

尚、これは実装面の話になるが、GameStateInformationGameStateと同じインターフェースにするのが良さそうだ。 プレイヤーがアクセスできない情報はnullにしたりNullObjectにしたり、アクセスするとエラーを吐くような仕組みにする。 これにより、GameStateGameStateInformationも今までのルール関数を共通で使用することができるようになる。

追加しないルール「ゲームフロー

ボードゲーム等の説明書では、よくはじめに「ゲームの流れ」と言うものがある。 これは私のゲーム定義には載せないことにする。 なぜならこれまでの定義で、ゲームの流れ(以下、ゲームフロー)は表現できるからだ。

例えば、もう一度オセロのルールを定義してみよう。

## ルール1: 初期状態
8x8マスのボードがある。(マス目の表記法については省略。)
e4d5に黒石、d4e5に白石が置いてあるとする。
**黒番である。**

## ルール2: アクション定義
* 自色の石を1つ置く。
この際、同じ色で挟まれた石は、挟んだ石と同じ色に変わる。
**また、白番の場合は黒番に、黒番の場合は白番に変わる。**
* パスする。

## ルール3. 可能/禁止アクション
* **自分の手番でない場合、取れるアクションは無い。**
* 敵石の色を挟めるような位置にしか自石を置けない。
* 自石を置けない場合のみ、パスをすることができる。

## ルール4. 結果判定
* **ボードに石が置かれていないマスがある場合、勝敗未決(試合続行)。**
* 上記以外の場合、自色の石がより多くボードに置かれていた方のプレイヤーが勝ち。

## ルール5. ゲームの状態のアクセシビリティ
プレイヤーはボードを自由に見て良い。

以前と比べ、随分と整理できているように感じる。まあそれは置いといて。

オセロのゲームフローは以下の通りだろう。

1. 黒が打つ
2. 白が打つ
3. ボードに石が置かれていないマスがある場合、1.へ。そうで無い場合、勝敗を判定する。

このフローは全て、上のルール定義の強調部分から完全に読み取れる情報である。 というわけで、ゲームフローはゲームの状態等と同じく説明変数にすぎない。 リファクタリングと称してまた括り出してもいいが、今回は特に意義を感じないのでなんとなくやらないことにする。

結論

ゲームとは、 「プレイヤーが、以下のルールに沿って、アクションを選択し、結果を決める営み」 である。

// ルール1: 初期状態
def initialState(): GameState

// ルール2: アクション定義
def createAction(args: Any): GameState => GameState

// ルール3. 可能/禁止アクション
def isAvailable(action: GameState => GameState, state: GameState): Boolean

// ルール4. 結果判定
def getGameResult(state: GameState): GameResult

// ルール5. ゲームの状態のアクセシビリティ
def getGameStateInformation(state: GameState, player: Player): GameStateInformation

おわり。

*1:オブジェクト指向信者の方は、早くisAvailableをActionクラスのメソッドにしたくてうずうずするかもしれない。 しかし今はまだ色々と整理中の段階だ。その辺の最適化は待っていただきたい。

ただ、Action型の中に色々とアクションについての情報を示す属性が入っていることはこの段階で想定している。 例えば、player(誰のアクションか)、timing(ゲーム開始後何秒後に行われたアクションか)等が考えられる。

このような単純な属性の整理は"データ型"の設計であり、メソッドを含めた"クラス"設計よりも前の段階で行えると考えている。 具体的に言えば、以下のような手順を踏むのがクラス設計への基本的な道のりだと私は考えている。

  1. データ型の整理
  2. やりたい事を関数に分解しながら洗い出し
  3. 関数とデータ型をどう組み合わせてクラスとして固めるか決定