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

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

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'などブロックを使った長い文脈で意味のあるものを生成するのは大変

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

無限ランダムひどいアクションゲーム生成器への道

ゲームそれ自体を自動生成してくれる機械が欲しい。開発者はその機械が生成するゲームを遊んで良ゲーなら採用、クソゲーなら捨てる、その作業だけでゲームが作れる。夢の機械だ。

人が後で見て取捨選択する前提なら、出来上がるものの大半ではクソゲーでもいい。それよりも大切なのはゲームの持つルールというか、仕組みというか、ギミックというか、そういったものが十分なバリエーションを持って生成されること。似たようなゲームしか作られないのではつまらない。

どうすればそういったことができるか。一例として、プレイヤーがボタンを押した時に起こることを乱数で適当に作って組み合わせる、という方法が考えられる。例えばボタンを押した時の自機の加速度の変動パターンを適当に設定することで、上に加速すればジャンプ、右に加速すればスライディング、などの動きをいろいろ作れそうである。そういったことを自機だけでなく敵にも設定すれば、いろんなバリエーションのゲームができるかも。

そういったアプローチを採っている既存のものとして、Mechanic Minerがある。

この論文はゲームを作るAIとして有名なANGELINAプロジェクトのものである。Mechanic MinerではToggleable Game Mechanics (TGM)と呼ばれるボタンが押されるたびに自機の状態が切り替わる機構を開発し、リフレクションを使ってクラス内のフィールドに対して適用している。題材としてはレベル内のスタート地点からゴール地点へ到達するパズルジャンプアクションを使っており、自機の位置や重力に対して、ボタンが押されると倍になる、半分になる、+-が入れ替わるというTGMが用意されている。

Mechanic Minerでは遺伝的アルゴリズムを用いてランダムなTGMの組み合わせから適切なTGM群を選択している。選択のための適応度 (fitness)は、自機を適当に動かしてゴールに到達するまでにレベルのどの程度の範囲を移動できたかを用いている。レベルのあちこちを移動してやっとゴールに到達できる、というのが質の良いゲームと判定しているようだ。もちろんゴールに到達不能なものは排除される。

Mechanic Minerはこのジャンプアクションに比較的特化した作りになっている感じだったので、アクションゲーム全般にもうちょっと拡張可能なものが作れないかと思って、別のものを作ってみた。

デモゲームは以下で遊べます。

クソゲーを自動的に作る、という点のみクリアして、あとは失敗している気がする。自機や敵の動作はいろんなバリエーションが出ているが、ゲームとしてのルールとしてのバリエーションとまでは呼べない印象である。あとクソゲーを超えたプレイ不能ゲームがたくさん出てくる。ボタンを押しても何も起きないとか。

GameMechRandomizerではボタンを押した時に起こることとして、数値がn倍になる、nを足す、nになるという、より広範な動作を用意した。これらの動作がボタンを押した時、押している間、押すとトグルで切り替わる、押している間トグルで切り替わる、いずれかの条件で発生する。

適応度は2つのプレイヤーAIを作ることで評価した。一つは賢いもの、もう一つはバカなもの。それぞれのAIが自動生成されたゲームを遊んで、バカなものは多く死に、賢いものはそれほど死なない、そういったものが比較的遊べるゲームだという判断をして、適応度を高くする。逆にプレイヤーが賢かろうがバカだろうが死に方に大差ないのはゲームとして成り立ってないと考えることとした。賢いAIといっても、例えば自機を8の字で動かして敵弾に当たりにくくするとか、そのレベルの賢さだが。

GameMechRandomizerを使うことで、ある程度は汎用的なアクションゲームに対して新たな仕組みを導入することはできる。ある程度は。

課題は満載である。

  • 新しいゲームが生成されている、とはとても言えない。箱避けゲームとか箱撃ちゲームとか、もともとあるゲームに対してちょっとバリエーションを与えている、程度である

  • まともなゲームができない。クソゲーで良いといっても遊べすらしないゲームはいかがなものか

  • 遊べすらしないゲームを遊べるゲームへ調整する仕組みが必要だが、現状の遺伝的アルゴリズムベースのアプローチが妥当かどうかは不明である

  • 多分、ゲームの楽しさというものを測るもうちょっとまともな指標が必要

もうちょっと考えます。

SVGのPathの当たり判定を取る方法

Pathで囲まれたSVGの当たり判定を取ることができれば、ベクター形状の間で正確に当たり判定が取れる。現代版1ドットのエクスタシーを魅せることが可能だ。以下の様なコードを書いた。

stars stars

SVG自体にはバウンディングボックスを使ったおおまかな当たり判定の仕組みしかないので、厳密な当たり判定は自前でなんとかするしかない。

この記事にあるようにまずバウンディングボックスでの衝突を判定し、衝突している場合のみパスを使った厳密な当たり判定を取るのがパフォーマンスの面から見て望ましい。

バウンディングボックスはこのスレにあるように、getBBox()で取ったバウンディングボックスを自力でtransformすることで得た。

パスを使った当たり判定は、まじめにパスとパスの交差を計算するととても大変なので、いくつかの線分で近似するのが現実的だろう。SVGPathElementにはgetPointAtLength()というパスの始点から特定の長さの位置にある点を取る関数があるのでこれを利用する。getTotalLength()で取ったパス全体の長さを適当な数で割ってその長さごとの点で囲まれた多角形を当たり判定とする。

実際に当たり判定を行うのはsat-jsに丸投げ。ただsat-jsのPolygonの当たり判定は凸面のポリゴンにしか対応してないので、凹面の多角形は適当な凸面ポリゴンに分割する必要がある。今回は安直に多角形の中心点と各辺から成る三角形に分割した。

こうすればそこそこ正確に当たり判定を取れるはずだが、処理としては結構重たいかも。ゲームで使っていいスピードが出るかどうかは微妙かもしれず。

Elmのエラーメッセージが分かりやすくなっている

だいぶ前にElm触ってゲーム作った時はElmに色々と不満があったんだけど、あれからElmもだいぶバージョンアップして0.15.1ではエラーが人フレンドリーになったらしい。前回の不満がどれだけ解決されているか調べてみよう。

関数の引数の数間違いが謎の型エラーになったりする

module Test where

import Html exposing (text)

message2: String -> String -> String
message2 world world2 = "Hello, " ++ world ++ " " ++ world2

main = text (message2 "World?" "World!")

というメッセージを表示するだけのコードを書いて、

main = text (message2 "World?")

と引数の数を間違うと、

The 1st argument to function `text` has an unexpected type.

8|        text (message2 "World?")
                ^^^^^^^^^^^^^^^^^
As I infer the type of values flowing through your program, I see a conflict
between these two types:

    String

    String -> String

8行目のここの引数がStringで無くString -> Stringだぞと出る。素晴らしい。抜群に分かりやすくなっとる。

レコードのアップデートと追加構文を間違った時のパーサーのエラーが謎だった

main関数を

main =
  let
     world = {str = "World??"}
     world2 = {world | str2 = "World!!"}
  in text (message2 world2.str world2.str2)

とし、

     world2 = {world | str2 = "World!!", str3 = "World?!"}

と許可されていない複数のフィールド追加を行うと、

11|      world2 = {world | str2 = "World!!", str3 = "World?!"}
                                           ^
I am looking for one of the following things:

    a closing bracket '}'
    an expression
    an infix operator like (+)
    whitespace

とちゃんとここで'}'を閉じろと言う。素晴らしい。

型の宣言でのエラーや変数名の重複でその行番号を教えてくれない

message2: String -> String -> String2

とすると

Cannot find type `String2`

5| message2: String -> String -> String2
                                 ^^^^^^^
Maybe you want one of the following?

    String

こう。

     world = {str = "World??"}
     world = {world | str2 = "World!!"}

とすると

Naming multiple values `world` in a single let-expression makes
things ambiguous. When you say `world` which one do you want?

11|      world = {world | str2 = "World!!"}
         ^^^^^
Find all the values named `world` in this let-expression and
do some renaming. Make sure the names are distinct!

こう。完璧ではないか。

乱数シードに現在時刻が欲しい

これはエラーメッセージとは関係ないのだが、乱数の初期化のために現在時刻が欲しかったのだが、その方法が良く分からなかった。

Elmの中だけでなんとかしようと思わないでportを使ってJavaScriptから供給すればいいらいいぞ。

portはElmとJavaScriptの間で値をやりとりする仕組みで、JavaScriptからsendされた値をElmのSignalとして受け取ることができる。

Elm.fullscreen(Elm.Test, {beginTime: Date.now()});

JavaScriptからElmを起動する時に引数で与えた値を、

port beginTime: Float

main =
  let
     world = {str = toString beginTime}

とportを介してもらえば良い。portってSignalじゃなくてもいいのね。たぶん不変な値限定だけど。

requestAnimationFrame対応

ElmのPongの例とかだと画面の更新はSignal.map inSeconds (fps 35)とか使っていてタイマーベースだけど、できればこれはrequestAnimationFrameにしたい。

それにはelm-animation-frameを使えば良い。elm package install jwmerrill/elm-animation-frameでパッケージをインストールし、

main =
  let
     frameNumSignal = Signal.map toString 
       (Signal.foldp (+) beginTime AnimationFrame.frame)
     messageSignal = Signal.map (message2 "World?!") frameNumSignal
  in
    Signal.map text messageSignal

のようにAnimationFrame.frameをトリガに画面を更新すれば良いらしいぞ。

Elmイケているではないか

バージョンアップで着実に欠点が埋められている感じ。portを使ったJavaScriptとの連携がうまく使えれば、ゲーム作りにもいいかも。あとは良いIDEが欲しいよな。

物理エンジンMatter.jsをテキストでレンダリング

CRTっぽいテキスト画面をWebGLで作るLocatePrintを使って物理エンジンをレンダリングするというデモも作った。

consolephysics

物理エンジンMatter.jsを使っている。Matter.jsはもちろん自前のレンダラを持っているんだけどそれを他のレンダラで差し替えられる

差し替える独自レンダラはMatter.jsのRender.jsを参考に作れば良い。特にRender.bodyWireframes関数にボディの頂点を線でつなぐ一番単純な描画方法が実装されているので、これを真似るのが簡単だ。

デモのコードでは、頂点を線でつなぐ部分でテキストを書くようにした。画面をテキストの幅と高さ(40キャラ x 20キャラ)のグリッドに分割し、線とグリッドが交わるポイントをリストアップする。グリッド内の各キャラについて、それらポイントがどこにあるかに応じて書くテキストを変える。例えば上端右と下端左にポイントがあれば/、右端上と下端左にあればFという具合に。それらのパターンを全部配列で記述した。

後はボディごとに設定された色に従ってcolor, locate, printするだけ。昔ながらの画面で近代的な物理エンジンが動く妙な絵が作れる。楽しい。

昔のCRTっぽい画面をWebGLで作る

マイコンをつないだテレビみたいな画面を作りたいと思ってLocatePrintっていうコードを書いた。

skigame

こんな画面が作れる、し遊べる。locate, print, colorなどの命令でテキストを書けます。というかテキストしか書けない。

こういうブラウン管っぽい画面を作るにはWebGLをポストプロセスとして使うのが便利。

この記事に必要なことはほとんど書いてある。glfx.jsっていうWebGLのイメージエフェクトライブラリを使って、ブラウン管の丸みっぽく画面を歪ませ (bulgePinch)、ふちをちょっと暗くすれば (vignette)、昔懐かしのテレビっぽい絵になる。

ただこの記事だとスキャンラインはPNGイメージを重ねることで実現しているけど、これもできればWebGLのシェーダーでやりたい。あとパピコンAppleIIみたいな色のにじみもつけたい。

そういう時はglfx.jsにカスタムのエフェクトが追加できればいいんだけど、残念ながらglfx.js自体はそのような仕組みを用意してない、が、やればできないことはない。

glfx.jsのいくつかの関数を無理やりexportsして外部から使えるようにする。そうすればスキャンライン色のにじみを再現するシェーダーをglfx.js内で使うことができる。

WebGL使えばブラウザ上でも簡単にポストエフェクトで遊べて良いね。フラットなLCDを丸いCRTにするという後ろ向きな使い方でも楽しければ良いのだ。

BLCK4777の絵の作られ方も知っておきたい

JavaScriptデモBLCK4777音の作られ方について前に調べたので、ついでにその絵の方もどのように作られているか調べてみた。多分に憶測が入っているけど、多分以下の様な感じ、のはず。

前の記事で追ったAudio生成部分を除いてコードを整理してみると、以下になる。

ctx = b.getContext('2d');
p = 0;

window.onload = function() {
  b.style.background = "radial-gradient(circle,#345,#000)";
  b.style.position = "fixed";
  b.style.height = b.style.width = "100%";
  b.height = 720;
  h = b.style.left = b.style.top = A = f = C = 0;
  //       x, y, vx, vy,    r, color1, color2, idx,
  p = [    0, 0,  0,  0,  180,      2,      0,   1, // triangle 1
        -360, 0,  0,  0,   99,      1,      0,   2, // triangle 2
         360, 0,  0,  0,   99,      1,      0,   3, // triangle 3
       -2880, 0,  0,  0, 1280,      0,   1280,   0];// line
  u();
}

まず最初にCanvasのstyle設定や変数の初期化を行っている。変数'p'が重要で、画面に多数ある三角形はこの配列で制御されている。一つの三角形は8つのパラメタで管理されている。左からx, y座標, x, yの移動量, 大きさ, 色の設定用パラメタ2つ, インデックスだ。初期設定されているのは画面に最初からある横に並んでいる大三角形3つと、その大三角形を貫く線の4つだ。

function u() {
  requestAnimationFrame(u);
  g = f + 1;//audio.currentTime * 60;
  for (; g > f; h *= f % 1 ? 1 : 0.995) {
    s = Math.pow(Math.min(f / 5457, 1), 87) +
      Math.pow(1 - Math.min(f / 5457, 1), 8);
    if (f == [1280, 1599, 2175, 2469, 2777, 3183,
              3369, 3995, 4199, 4470, 4777, 5120][C]) {
      C++;
      h = 640;
    }
    b.width = 1280;
    ctx.translate(640, 360 + h / 45 * Math.random());
    ctx.rotate(A / 5457 - h / 5457);
    ctx.scale(1 + s * 8, 1 + s * 8);
    f++;

続いてrequestAnimationFrameで回る1フレーム毎の描画部分。gは音の経過時間、fは現在のフレーム数を表していて、これらの比較は絵と音の同期を取るために使われている。今回は音の部分を外してコードを整理してしまったため、gにはfより1フレーム先を設定し、同期処理を行わないようにした。

sは少しずつ引くカメラワークのために後でctx.scaleで使われている。

fと比較している配列は、大三角形が光るタイミングのフレーム数を表している。hに640を設定していることをトリガに、後で色の制御を行う。Cは何回光ったかに相当する値で、音の場面転換などにも使われる。

b.width = 1280は画面サイズを設定するとともに、画面を消去している。その後のctxに対する操作はカメラのブレや回転の制御用だ。

    i = p.length;
    for (; i; ) {
      y = p[i -= 7];
      x = p[i ^= 1];
      r = p[i + 4];
      l = p[i + 6];
      s = 2 * Math.random() + 1;
      t = s * 4;
      a = 122;
      if (640 > r) {
        // triangle
        // y += vy
        if (!(640 > Math.abs(p[i ^= 1] += p[i + 2]))) {
          p[i + 2] *= -1;
        }
        // x += vx
        if (!(640 > Math.abs(p[i ^= 1] += p[i + 2]))) {
          p[i + 2] *= -1;
        }

ここからpの中の各三角形の処理。まず三角形のパラメタを取り出す。rが640より小さいのは三角形で、それより大きいのは線だ。まず三角形の場合、x, y座標の移動、および画面端にぶつかった場合の反転処理をしている。i ^= 1がちょっと面妖だが、ビットのXORを取っていて、偶数を奇数、奇数を偶数にする処理をしている。これでy座標、次にx座標という処理が行える。

        t = Math.random() > p[i + 7] ||
          p[i + 7] == "22312131212313"[C] & h == 640;

三角形を光らせる処理。大三角形が光るフレーム数の時はhが640になるので、文字列と対応するインデックスを持つ三角形の色を設定する。それ以外の小さな三角形もたまに光るようになっている。

        w = x - A;
        if (!p[i + 2]) {
          if (r * r / 3 > w * w) {
            t = s * (r - Math.abs(w)) / 45 + 2;
            a = 2 * Math.random() + 5;
            p.push(A, 0, s * Math.sin(a += 599), s * Math.sin(a - 11),
                   s * t, C + s, 640, 0.995);
            s = 2 * Math.random() + 1;
            a = 2 * Math.random() + 5;
            p.push(A, 0, s * Math.sin(a += 599), s * Math.sin(a - 11),
                   s * t, C + s, 640, 0.995);
            s = 2 * Math.random() + 1;
            a = 2 * Math.random() + 2;
            p.push(A, 0, s * Math.sin(a += 599), s * Math.sin(a - 11),
                   s * t, C + s, 640, 0.995);
          }
        }
        a = p[i + 2] * y / 45;
        l = p[i + 6] = t ? 640 : 0.9 * l;
        t = r;

線が大三角形を貫く時に小さな三角形をばらまく処理。Aが線の先端のx座標を表しているので、それを使って衝突を判定、衝突している時はpにパラメタを追加することで、小さな三角形をばらまいている。

      } else {
        // line
        A = p[i]++;
      }

線の処理、x座標をインクリメントしているだけ。これで右に動く。

      if (!(g > f)) {
        s = r;
        ctx.beginPath();
        ctx.lineTo(x + s * Math.sin(a += 599), y - s * Math.sin(a - 11));
        s = t;
        ctx.lineTo(x + s * Math.sin(a += 599), y - s * Math.sin(a - 11));
        ctx.lineTo(x + s * Math.sin(a += 599), y - s * Math.sin(a - 11));
        ctx.shadowBlur = r;
        s = l;
        x = s * 2;
        a = p[i + 5];
        ctx.shadowColor = ctx.fillStyle = "rgb(" +
          [x + s * Math.sin(a += 599) | 1,
           x + s * Math.sin(a += 599) | 1,
           x + s * Math.sin(a += 599) | 1] + ")";
        ctx.fill();
      }
    }
  }
  ctx.fillText("BLCK4777", 90, 99);
};

描画処理。gfは音と絵の同期用。三角形のパラメタにしたがってctx.lineToで頂点座標を設定し、シャドウのブラー、色を設定、fillする。最後に固定のメッセージBLCK4777を表示。

うん、実は全体の流れは結構分かりやすかった。もちろん実際は細かなパラメタの値などに演出の妙がたくさんあって、その辺の加減が肝なんだけども、そこを深追いしなければ読み解けないこともなさそう。一見とっつきにくそうに見えるデモのコードも、ちゃんとフローに応じて読みやすい形に展開すれば、なんとか追うこともできるかも、という一条の光が得られました。

デモコーダーにはもちろんショートコーディングのテクニックも必要なんだけども、限られたコードでどのような演出をし、音との同期も考えた盛り上がりポイントをどう入れるか、っていうところの力量もかなり必要とされているんだなあという印象を受けた。BLCK4777だとh = 640のタイミングで、光り、音が変化し、パーティクルが飛び始める、これが短いコードの中で効率良く実現されているのが美しいですな。

追記:ここ2つの記事で分かった内容を受けて整理、コメントを足したBLCK4777のコードをGitHubに置いた