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

セルオートマトンでゲームは作れるか

作れそうだけど大変そう。

consomaton-game-lib

screenshot

前にセルオートマトンプログラミングパズルゲームconsomaton作った時から、セルオートマトンのルールを書くだけでゲームが作れたらお気軽ではないかと思っていた。ただセルオートマトンのルールは、あるセルとその周辺の状態の変化のみしか書けないという強烈な縛りがあるので、書けるゲームはかなり限られるだろうという予想はあった。でもまあ試すだけ試してみた。

ルールは左3列がbefore、右3列がafterとしてconsomatonと同様に書く。'==='はルール間のセパレータ。

===
v
   v

こう書くとvが毎フレーム一つ下に落ちるようになる。

ゲームだからランダムに出現する敵とか必要。なのでルールをランダムに発火させるための'r'コマンドってのを作った。

===r10
.  .
   v

コマンドはセパレータの右に書く。'r10'って書くと1/10の確率で発火する。

あとゲームにはスコアも必要。なので次はスコアを追加する's'コマンド。

===r10-s1
.  .
   v

's1'って書くとこのルールが発火したときに1点入る。

パッドやキーの入力を受け付けてそれに応じて発火するのも必要。'p'コマンドってのを作って'p>'で右入力で発火にした。

===p>
@   @

@を自機として右に動くルールができた。

同様に'p<'で左、だけど右のルールがすでに発火している時はこのルールは発火しなくても良い。なので'---'っていうセパレータにすると前のルールが発火済みの時は発火しないようにした。

---p<
 @ @

あとゲームオーバー条件も必要。プレイヤーがいなくなったらを表すための'n'コマンドと、発火したらゲームオーバになる'o'コマンドってのを追加。

===n@-o

ここまで書くとこれができる。

FALL V

このようにお気軽にゲームが書ける。

お気軽ではない。ただでさえルールで書けることに限りがあるのにさらにそれを謎のコマンド群で拡張しないとごく簡単なゲームも作れない。ルールの書く順番や謎のセパレータの使い分けも必要だったりして、手軽に書けると言えるかというと微妙である。

ついでに前に作ったサウンドジェネレータsounds-some-soundsで音が付けられるようにして、ドット絵ジェネレータpixel-art-genで絵が付けられるようにした。それらを駆使して、一番上のスクリーンショットにあるクロスハイウェイみたいのを作るソースコードは以下。

CROSSMAN source code

長いね。これを書くこと自体がパズルゲームだね。パズルゲームを解く苦行とゲームを作る苦行が同時に味わえるという点ではお得感があると言えましょう。

プログラミングパズルゲームが作りたかった

セルオートマトンプログラミングパズルゲームconsomatonというのを作った。

screenshot

ブラウザで遊べます

ソースコードはこちら (GitHub)

ゲームプログラミングを趣味としている者として、昔から作ってみたかったのがプログラミングパズルゲームだった。プログラミングパズルゲームってのはたとえばGoogleがアランチューリング生誕100周年で公開していたチューリングマシンのロジックパズルとかメイドイン俺くみたて道場とかTIS-100とか。カルネージハートとかよりはよりプログラミング感が全面に出ているタイプのゲーム。

ゲームとしてプログラミングを扱うにはなるべく簡単にコードが書けることが望ましい。スクラッチとかのパネルを置くタイプもいいんだけどこれですらちょっと面倒。なのでライフゲームに代表されるセル・オートマトンのルールを書くものにした。

ルールの書き方はVISCUITメガネに影響を受けている。ルールを適用するパターンとそれを適用した後のパターンを並べて記述することで、動作ルールをビジュアルに示すことができる。

問題の出し方はくみたて道場と同じ穴あき方式にした。プレイヤーはルールに文字を入れるだけで答えが作れるのでだいぶ手軽だ。

現バージョンにはいろいろ問題があるのだが、特に問題なのが回答に抜け穴が多すぎること。例えば10問目はパックマンっぽい動作をするのが回答の予定だったのだが、こうすると一撃でクリアー!だいぶひどい。変更不能なルールとか途中ゴール状態とかを導入する必要があるのだろう。

まあでも当初の目的のお手軽コーディング感は出せたのでそこは良かったと思います。理想的にはこの仕組みと同じ感じでゲームが作れるくらいの自由度があるとより良いのだけどね。

ES2015のProxyを使ってJavaScriptを改造して遊ぼう

ES2015にはProxyという仕組みがある。Proxyを使えばオブジェクトへの各種操作に割り込んで好き勝手な動作を定義できる。

ただProxyには問題が合って、サポートするプラットフォームが少ない。

どうもProxyはpolyfillやトランスパイラで実現するのが難しいらしく、BabelやTypeScriptではES5に変換できない。

なのでブラウザで対応してもらうしかないのだが、長らくFirefoxでしか動かなくて最近やっとChromeが49で対応した。Chromeで動くならギリギリ使っても良いか、というレベルにやっとなったところだ。

今回はProxyを使って関数を呼び出すだけでゲーム内アクターを生成できるようにすることで、アクター生成を簡単に書けるようにならないか、というのをやってみた。

Proxyは動作をフックした時の挙動を定義するオブジェクトを与えて作る。

            protoSpawn = new Proxy({}, functionToActorHook);

関数が呼ばれた時をフックするにはオブジェクトに対するgetをフックして、そのターゲットがfunctionの時の動作を書く。Actorを生成するとか。

const functionToActorHook = {
    get: function(target, name) {
        const targetObj = target[name];
        if (typeof targetObj === 'function') {
            return function(...args) {
                if (args.length > 0 && args[0].rename != null) {
                    name = args[0].rename;
                    args[0].rename = null;
                }
                const actor = new Actor(name);
                actor.generator = targetObj.apply(actor, args);
                return actor;
            }
        }
        return targetObj;
    }
}

こうしておけばこのProxyを介して関数呼び出しをした時に上記コードが呼ばれるようになる。

import {protoSpawn as ps, p5js as p, mech as m} from './protospawn';
import Actor from './actor';

function setPsCode() {
    ps.main = function*() {
        ps.ship({isPlayer: true, pos: {x: 50}});
        ps.ship({isPlayer: false});
    }
    ps.ship = function*(prop) {

ps.shipを呼ぶだけでActorが生成される。あとES2015を使っているのでついでにGeneratorも使ってyieldすることで関数途中で1フレーム待つのもできるようにした。

    ps.explosion = function*(prop) {
        this.set(prop);
        this.stroke = 'red';
        for (let i = 0; i < 15; i++) {
            this.size += 2;
            yield;
        }
        for (let i = 0; i < 15; i++) {
            this.size -= 2;
            yield;
        }
        this.remove();
    }

で、作ってみてなんなのだが、このアプローチはいろいろと問題が。

  • アクター生成を関数で置き換えること自体にそんなに価値がない。new Ship()とか書けばいいだけだし。あとアクター定義が複雑になってきたら素直にclassで書いたほうがコード補完とかの面で有利
  • yieldを使ってアクター動作を中断する仕組みを使う場面ってそんなにない。従来の1フレームごとのupdate関数がある方が便利なことも多い。必要ならtweeningっぽい仕組みを別途作れば良さそう
  • Generator関数の中のクロージャからyieldが呼べない。なのでlodashの_.timesとかが実質使えなくなる

別の方法を考えた方が良いね。

汎用ビデオゲーム記述言語VGDLとその処理系PyVGDL

2Dビデオゲームのメカクニスを記述するための言語VGDL (Video Game Description Language)というものを研究している人達がいる。

ゲーム内AIなどを研究するために必要な言語仕様の提案として始まったようだ。それを実際にパースして実行する処理系PyVGDLも作られている。

VGDLはパックマンとかフロッガーとかバルダーダッシュとかの古典的アクションゲームが簡単にかけるという触れ込みである。例えばフロッガーをVGDLで書いたコードは以下だ。

まずレベルの定義がある。

wwwwwwwwwwwwwwwwwwwwwwwwwwww
w           wGw            w
w00==000000===0000=====000=2
w0000====0000000000====00012
w00===000===000====0000===02
www   ww   www    www  wwwww
w   ----   ---   -  ----   w
w-     xxx       xxx    xx w
w -   ---     -   ---- --  w
w       A                  w
wwwwwwwwwwwwwwwwwwwwwwwwwwww

なんとなく見覚えのある画面がテキストで書いてある。次はゲーム内キャラの定義。

    SpriteSet
        forest > SpawnPoint stype=log prob=0.4  cooldown=10
        structure > Immovable
            water > color=BLUE
            goal  > color=GREEN
        log    > Missile   orientation=LEFT  speed=0.1 color=BROWN
        safety > Resource  limit=2 color=BROWN
        truck  > Missile   orientation=RIGHT 
            fasttruck  > speed=0.2  color=ORANGE
            slowtruck  > speed=0.1  color=RED
        wall > Immovable color=BLACK 

林とか水とかゴールとか丸太とか、フロッガー内のキャラクタ(スプライト)が定義されている。後で説明するが、SpawnPointとか、Missileとか、この辺の記述がキャラクタの動作を決めている。

その次はキャラ同士の衝突時に何が起こるかの定義。

    InteractionSet
        goal avatar  > killSprite
        avatar log   > changeResource resource=safety value=2
        avatar log   > pullWithIt
        avatar wall  > stepBack
        avatar water > killIfHasLess  resource=safety limit=0
        avatar water > changeResource resource=safety value=-1
        avatar truck > killSprite
        log    EOS   > killSprite
        truck  EOS   > wrapAround

avatarってのは自機キャラのことですなわちカエルだ。ゴールにカエルが突っ込んだらゴールを消す(killSprite)とか、壁に突っ込んだら戻る(stepBack)などの動作が書いてある。

次はゲームの終了条件。

    TerminationSet
        SpriteCounter stype=goal   limit=0 win=True
        SpriteCounter stype=avatar limit=0 win=False

画面上からゴールが無くなったら勝ち、カエルが無くなったら負け、ということだ。

最後は最初に書いたレベルがどのキャラに相当するか。

    LevelMapping
        G > goal
        0 > water
        1 > forest water
        2 > forest wall log
        - > slowtruck
        x > fasttruck
        = > log water

以上。非常にシンプルに書けている。

ただ本当にこれで汎用かと言うとちょっと何なところもある感じがする。SpriteSetとInteractionSetの部分がメカニクスというかゲームルール記述の肝なんだけど、ここにあるMissileとかstepBackとかの動作の定義はあらかじめ決められたセットから選ばれているようだ。

このコード内にある動作がその全てで、これの作り込みによって定義できるゲームのバリエーションは規定されているように思える。で、これら動作も汎用的かというと、どうかなあ、killIfSlowとか、これルナランダー専用動作ではないのかという気がしないでも。まあここの動作が十分な数揃っていればいいのかもしれないが。

最初に思ったのは、これPuzzleScriptに似ているなということ。

PuzzleScriptはその名の通りパズルを作るための専用言語。例えば倉庫番は以下で書ける。

LEVELS、RULES、WINCONDITIONS、LEGENDの順に見ればVGDLのサンプルに非常に似ている感じがする。PuzzleScriptのルール定義は本当にシンプルで、状態のパターンマッチと、その後の状態を記述するだけ。プレイヤーが荷物に向かって歩いたら、荷物を押す、は

[ > Player | Crate ] -> [ > Player | > Crate ]

こうだ。VISCUITのメガネにも少し似ている。この1行書くだけで、上下左右どこから押されても同じように動作するみたいなことはPuzzleScriptの処理系がよろしくやってくれるので、うまく使えば少ない記述量で複雑な動作も書ける。

PuzzleScriptはキャラクタの形や効果音などもちゃんと記述することができて、これだけでちゃんと体裁が整ったゲームを作ることができる。VGDLはあくまでゲームルールを記述するだけなのでこれだけではゲームとして色々足りない部分がある。

この2つどっちが先にできたのかと言うと、PyVGDLのinitial commitが2012年6月でPuzzleScriptのinitial commitが2013年9月だからPuzzleScriptのほうが後発だ。それらの間に関係があるかというと、よく分からない。よく分からないが、比較記事があった。

PuzzleScriptはターンベースのパズルゲーム記述にかなり特化、対してVGDLはもっと広くアクションゲームを書こうとしている点で異なるけど、ゲームの形式化という点でアプローチは似ているよね、という内容。アクションやパズル系ゲームの簡易記述を考えると、この2つのようなフォーマットに落ちるのが一つの妥当なアプローチなんだろう。

ゲームのルールのエッセンスさえ記述できれば良いという割り切りのもと、どれだけ簡易なゲーム記述言語を作ることができるか、というのを考えてみるのも楽しそうだ。VGDLの発展形や、それとは全く別な記述方式、いろんなアプローチが思いつけるのが理想だが、簡易だけど汎用的というバランスの良い言語を考えるのはたぶんそう簡単では無いね。

斬新なゲームメカニクスを目指した時の「やらかし」と「もがき」の制作過程が分かる本「組み立て×分解!ゲームデザイン」

筆者のkuniさんから献本いただいた。

ひどくおおざっぱに言うと、斬新なルール、ゲームメカニクスを持つゲームを作るに向けて、これは面白いだろうと思って作ったルールがイマイチな時、そこからどうやって工夫することで面白くすることができるか、それを書いた本。

題材のゲームはkuniさんが作ったいくつかのゲームを用いている。

例えば2章はmosserことまるぼうしかく内のしかくことフレイムテイルが題材。炎が燃え広がるというアルゴリズムを元に、単にクリックしたところが燃える凡ゲーから、炎をつける自機の導入、燃やせるテトロミノの導入などの様々な試行錯誤を経て、最終形のお尻に火が着いたスネークゲームというところに到達するまでが丁寧に解説されている。

こういった新しいルールを導入したパズルゲームやアクションゲームを作るためには何回もの試行錯誤を経てルールが出来上がっていくものだが、その制作過程を細かに語っている本は珍しく思う。こういったところは明文化されない個々人のノウハウになりがちなので、それが具体的に記載されていることがありがたい。その他にも、ルールを引き算や足し算で作る方法、ゲーム内の緩急の付け方、制約を設けることでゲームをゲームとして仕立てていく方法などなどが、具体的なゲーム制作体験に沿って書かれている。5章はみんな大好きパネキットでのいくつかの制約がなぜ必要だったのかにもちょいと触れられているよ。

定番のゲームではなく、ゲームの根幹のメカニクスがちょいと変わったゲームを作りたい人にはオススメ。記載が具体的な分、ゲームデザインに対して網羅的な内容では無いし、引用された豊富な既存ゲームに対する説明はおっさんゲーマーにとっては自明の内容で無駄な感じもするところはちょっと欠点だけど、それを差し引いても読んでいてとても楽しい本です。あとパネキットを代表とするkuniゲーが好きな人はぜひ手に取ると良いと思うよ。

ES2015のProxyを使った関数呼び出しのフック

ES2015にはProxyという仕組みがある。

Proxyを使うことでオブジェクトへの書き込みや読み込みをフックすることができ、メタプログラミング的なことがJavaScript上で実現できるようになる。

例えば、関数呼び出しをフックして呼び出し前に何か別の処理をしたい、と思ったら以下のように書ける。

const obj = {
  foo: (x) => {
    console.log(x);
  }
};

const proxy = new Proxy(obj, {
  get: function(target, name) {
    const targetObj = target[name];
    if (typeof targetObj === 'function') {
      return function(...args) {
        console.log(`call ${name} ${args}`);
        const result = targetObj.apply(this, args);
        return result;
      };
    } else {
      return targetObj;
    }
  }
});

window.onload = () => {
  proxy.foo(42);
}

Proxyのgetハンドラで値の読み込みをフックし、関数を読み込もうとしたら別の関数に差し替える。差し替えた関数の中ではapplyで元の関数を呼び出すけど、その前や後に任意の処理を挟み込むことができる。

ちなみにES5の制約でBabelはProxy未サポートなのでブラウザが対応しないかぎりProxyは使えない。

今までProxyはFirefoxかEdgeでしか使えなかったのだけど、Chromeも49から対応してくれるらしい。

だとするとそろそろProxyを使ったメタプログラミングに手を出してもいいかなあとも思う。あとTypeScriptでもES6ターゲットにすればProxy書いても文句言わないので使えそうだ。

クリックだけでプログラムが作れる夢のプログラミング環境作った

ウソです。いやウソではないか……誇張です。

screenshot

上のデモ開いて、左クリックでコード生成、右クリックでコード削除。運が良いと何かのグラフィックスを描くプログラムができる。あまりに何も描かないようだったら一旦右下の[Reset]を押して下さい。グラフィックスAPIp5.js利用。

左クリックで生成されるコードはRecurrentJSを使ったLSTMで作られている。LSTMやRNNをつかった文書生成はいろんなところでやられていて、有名どころだとThe Unreasonable Effectiveness of Recurrent Neural Networksがある。この記事ではLinuxソースコードを食わせてCのプログラムを作る例もある。ただ、自動生成でできる文やプログラムはいわゆるワードサラダで、文には意味が無いし、プログラムはコンパイルできない。

ならワードサラダなプログラムでも実行できる処理系を作ればいいんではないか、と思って作ったのがsarad。スタック指向ポーランド記法な言語。文を右から見ていって数値や変数をスタックに積み、演算子や関数を処理する時にスタックから取り出して引数にする。引数が足りない場合は強制的に0を割り当てることでエラーを吐かないようにしている。'if'や'while'などの基本的なフロー制御はあるが、突然'else'とかが出てきてもエラーは出さずに無視する。そうすることで、ワードサラダなプログラムでもエラーにはならずに無理やり実行される。

LSTMの学習に使う元データはprocessingのサンプルコードを使った。ただそのまま持ってくると自動生成の元にするには無駄にバリエーションがあるので、

  • 数は一旦すべて'D'に置換して学習し、コードを生成してからランダムに'0'~'9'を割り当てる
  • 変数名は'V0'から'V4'に強制変換する

として複数のコード片を混ぜあわせても破綻しにくいようにした。

現時点で課題は山積みで、

  • 意味のあるプログラムが生成される確率があまりに低い
  • なにも処理をしていない大量のデッドコードが生成される
  • プログラムの可読性が低く生成されたコードのどこを削ればいいか分からない
  • 'if'や'while'などブロックを使った長い文脈で意味のあるものを生成するのは大変

などなど。可読性の点からいうと、独自言語よりは既存のプログラミング言語が生成できて、デッドコード削除もされてた方が良いと思われる。もうちょっと考えます。