Seiichi Yonezawa — How creativity is helped by failure

Dwemthy's ArrayをJavaScriptで置き換えると

きっかけはまたいつもの思いつきに過ぎないのですが、かの有名なDwemthy's Arrayを今更ですがJavaScriptで置き換えてみました。ちなみに日本語の翻訳はこちらで公開されています。Rubyで伝説的なハッカーといえば、Rubyのパパことまつもとゆきひろさん、Ruby on RailsのクリエイターことDavid Heinemeier Hansson通称DHH、そしてこの感動的ガイドを世に送りだしたWhy the lucky stiff。他にもRubyで有名な方はたくさんいますが、三本の指を選ぶならやはり_whyは欠かせません。もちろん_whyの功績は感動的ガイドだけではありませんし、リアルタイムで会ったこともなければ、まして面識すらないのですが、私がこうしてRubyを使っていることに計り知れない影響を与えてくれたヒーローに違い無いのです。

さて、そろそろ本題ですが、以前にもSinatraとjQueryを連携してサーバ型のRPGを作ったことがありましたが、今回は純粋なES5のJavaScriptのみでどこまでできるかどうか試してみました。他にもJavaで書いたバージョンがあったりするくらいなので、おそらく既に他の人がJavaScript版を公開しているかもしれませんが、せっかく書いたコードなので今回はいくつかコードに対して解説も加えていこうと思っています。

// Creature
;(function () {
  window['rand'] = function (number) {
    return Math.random() * number | 0
  }
})()

;(function () {
  function Creature(traits) {
    var yourHit
    var enemyHit
    var metaClass = function (name) {
      this.name = name
      this.life = traits.life
      this.strength = traits.strength
      this.charisma = traits.charisma
      this.weapon = traits.weapon
    }

    metaClass.prototype.hit = Creature.prototype.hit

    return metaClass
  }

  Creature.prototype.hit = function (damage) {
    var powerUp = rand(this.charisma)
    if (powerUp % 9 === 7) {
      this.life += powerUp / 4 | 0
      console.log("[" + this.name + " magick powers up " + powerUp + "!]")
    }
    this.life -= damage
    if (this.life <= 0) console.log("[" + this.name +" has died.]")
  }

  Creature.prototype.fight = function (enemy, weapon) {
    if (enemy.life <= 0) return
    if (this.life <= 0) {
      return console.log('[' + this.name + ' is too dead to fight!]')
    }

    yourHit = rand(this.strength + weapon)
    console.log("[You hit with " + yourHit + " points of damage!]")
    enemy.hit(yourHit)

    if (enemy.life > 0) {
      enemyHit = rand(enemy.strength + enemy.weapon)
      console.log("[Your enemy hit with " + enemyHit + " points of damage!]")
      this.hit(enemyHit)
    }
  }

  window['Creature'] = Creature
})()

まずはCreatureクラスです。これは後述のRabbitDragonの基となるクラスです。ここでのポイントは、高階関数?を使っていてnew Creature()するとmetaClassという関数を作り出します。これがJavaScriptの継承ですね。ES5ではクラスという概念がないので、クラスの引数に名前を与えることでドラゴンであればそのままドラゴンを返すようにしました。JavaScriptにはオブジェクトのプロパティがattr_accessorと同じような役割を担っているので、Rubyのinstance_evalでそれぞれのtraitsに対してインスタンス変数を与えたりしなくてもよさそうです。また、fight関数はもとのコードのままだと仮に最後のドラゴンを倒せた場合でも攻撃できてしまい、残酷なので慈悲を与えることにしました。

;(function () {
  var dwarr = []
  var creatures = {}

  creatures.IndustrialRaverMonkey = new Creature({
    life: 46,
    strength: 35,
    charisma: 91,
    weapon: 2
  })

  creatures.DwarvenAngel = new Creature({
    life: 540,
    strength: 6,
    charisma: 144,
    weapon: 50
  })

  creatures.AssistantViceTentacleAndOmbudsman = new Creature({
    life: 320,
    strength: 6,
    charisma: 144,
    weapon: 50,
  })

  creatures.TeethDeer = new Creature({
    life: 655,
    strength: 192,
    charisma: 19,
    weapon: 109
  })

  creatures.IntrepidDecomposedCyclist = new Creature({
    life: 901,
    strength: 560,
    charisma: 422,
    weapon: 105
  })

  creatures.Dragon = new Creature({
    life: 1340,
    strength: 451,
    charisma: 1020,
    weapon: 939
  })

  for (var name in creatures) {
    if (creatures.hasOwnProperty(name)) {
      dwarr.push(new creatures[name](name))
    }
  }

  window['DwemthysArray'] = function () {
    var enemy

    return function () {
      if (!dwarr.length && enemy && enemy.life <= 0) {
        console.log("[Whoa.  You decimated Dwemthy's Array!]")
        return enemy
      }

      if (!enemy || enemy.life <= 0) {
        enemy = dwarr.shift()
        console.log("[Get ready. " + enemy.name + " has emerged.]")
      }

      return enemy
    }
  }
})()

こちらは基となる配列部分です。_whyの魅力のひとつはやはり、単なるサンプルコードにあるうたかたのモンスターにすら強烈な個性を植え付けるほどの名付け方にあるかと思います。副アシスタント触手オンブズマンが現れたかと思いきや歯ジカが同じ配列に含まれているなんて。この数行のコードから_whyの魅力が溢れてやまないと思いませんか。本編から逸れてしまいましたが、もともと先ほどのコードで引数としてnameを与えていたのは、できるだけオリジナルのRubyに近づけたかったので敢えてオブジェクトにnameを加えないようにしました。こうすることで、プロパティをそのままnameに引用することができました。また、常にRabbit経由でfightが呼び出されるため、常にモンスターを返すようにしています。最初は末尾再帰を使おうかと思ったのですが、変数の中身は変わらないのでDwemthy's Arrayは参照を書き換えなくてもモンスターを倒した場合は直ちに次のモンスターを出現させることができるというわけです。

// Rabbit
;(function () {
  function Rabbit () {
    this.name = 'Rabbit'
    this.life = 10
    this.strength = 2
    this.charisma = 44
    this.weapon = 4
    this.bombs = 3
  }

  Rabbit.prototype.hit = Creature.prototype.hit

  Rabbit.prototype.fight = Creature.prototype.fight

  Rabbit.prototype["^"] = function (enemy) {
    this.fight(enemy, 13)
  }

  Rabbit.prototype["/"] = function (enemy) {
    this.fight(enemy, rand(enemy.life % 10) ** 2 + 4)
  }

  Rabbit.prototype["%"] = function (enemy) {
    var lettuce = rand(this.charisma)
    console.log("[Healthy lettuce gives you " + lettuce + " life points!!]")
    this.life += lettuce
    this.fight(enemy, 0)
  }

  Rabbit.prototype["*"] = function (enemy) {
    if (!this.bombs) {
      return console.log("[UHN!! You're out of bombs!!]")
    }
    this.bombs--
    this.fight(enemy, 86)
  }

  window['Rabbit'] = Rabbit
})()

ここであなたのクラスこと、勇敢なウサギの登場です。実はRabbitクラスは先述のCreatureクラスから継承されたクラスではないのですが、hitfight関数はCreatureから継承しました。当初はfightを継承せずに、Creatureで呼び出し、引数を逆にしていたのですが、enemy.fight(this, weapon)としていると、挙動は同じでも文脈の意味合いが変わってくるので書き換えました。また、Rubyでは可能だった^, /, %, *に対する関数はそのままだと定義できないのですが、prototype経由ならそれが可能になるという。こうして書いてみて思ったのですがJavaScriptの表現力も豊かだということに気づかされました。また、Rabbitにはweaponというプロパティが存在するようですが、それぞれの攻撃方法が数値固定されているためこちらは参照する方法がありません。おそらくブーメラン部分がここに当てはまると思うのですが、さすがにこれ以上弱くなられても困りますよね。

> var r = new Rabbit()
> var dwary = new DwemthysArray()
> r["/"](dwary())
[Get ready. IndustrialRaverMonkey has emerged.]
[You hit with 12 points of damage!]
[Your enemy hit with 12 points of damage!]
[Rabbit has died.]

ゲーム部分です。オリジナルはirbでプレイするのに対し、JavaScriptはブラウザのコンソールで操作します。もとのコードがr % dwaryなのに対して、r["%"](dwary())はなかなか健闘したほうだと我ながらに思いました(Rubyも本来省略されている括弧を足すとr.%(dwary())なのでほとんど同じです)。CoffeeScriptならr["%"] dwary()まで頑張れそうですけれども、カッコひとつ省略するためにCoffeeScriptで書くほどの労力を費やすかどうか悩みますね。また、node.jsは詳しくないのでわからないのですが、REPLを使えばコンソールでも遊べるかと思います。若干動作がまだ怪しい部分もありますので、コードはまだまだ調整が必要かもしれません。

以上がJavaScript版のDwemthy's Arrayです。_why dayにはまだ早いですが、お楽しみいただけたら嬉しいです。