対戦Blokusの思考ルーチンをRubyで書けるようにしてみた

というわけで昨日の Ruby のオブジェクトを Java に渡す記事をふまえて、いよいよ「対戦 Blokus」の思考ルーチンを実際に Ruby で書いてみる。
「対戦 Blokus」の思考ルーチンは、interface IComputer の implements である abstruct class AbstractComputer を継承したクラスとして実装、そのクラス名を blokus.properties に記述することで組み込まれる。
このクラス(あるいは相当品)を Ruby で書けるようにするのが目指すゴールだ。


以下、ターゲットアプリケーションに沿って具体的に説明していくが、「共通インターフェースを持つクラスを properties に書いておいて、必要なときにインスタンス化して使う」という Java で一般的な設計に適応するケースとして読んでもらえれば、ほかのアプリケーションでも参考にしてもらえるのではないかと思う。


これを果たすには、昨日の記事に加えてもう2つ TIPS が必要になる。

詳しくはまた機会があれば。


というわけで必要なものは、

ScriptComputer.java
AbstractComputer を継承した proxy class。interface IComputer のメソッド呼び出しを Ruby インスタンスに中継する。
ScriptComputerFactory.java
Ruby インスタンスを生成する factory interface。ScriptComputer のコンストラクタにおいて、この factory によってインスタンスを得る。
RubyRandomComputer.rb
Ruby による思考ルーチンの実装。IComputer を include してある。今回はひとまず「対戦 Blokus」にすでにあるランダムプレイヤーを Ruby に移植している。
factory
RubyRandomComputerのインスタンスを生成するfactory。

となる。一つずつ見ていこう。

/* ScriptComputer.java */
package ch.jpn.taoe.blokus.client.com;
import ch.jpn.taoe.blokus.client.IComputer;
import ch.jpn.taoe.blokus.common.BoardPoint;
import ch.jpn.taoe.blokus.data.*;

public class ScriptComputer extends AbstractComputer {

    private static ScriptComputerFactory factory = null;
    public static void setFactory(ScriptComputerFactory fac) {
        factory = fac;
    }

    private IComputer rcom = null;
    public ScriptComputer(String name, Game game, Player me) {
        super(name, game, me);
        rcom = factory.create(this, name, game, me);
    }

    public void doNextTurn(BoardPoint lastPoint) {
        rcom.doNextTurn(lastPoint);
    }
    public void doTurnChange() {
        rcom.doTurnChange();
    }
    public void doEndGame(int[] scores) {
        rcom.doEndGame(scores);
    }
    public void doAddBloku(Player player, BoardPoint point, Blokus bloku) {
        rcom.doAddBloku(player, point, bloku);
    }
    public void doAckAdd() {
        rcom.doAckAdd();
    }
    public void doNakAdd() {
        rcom.doNakAdd();
    }

    public boolean isMyTurn2() {
        return isMyTurn();
    }
}

static メンバにファクトリを保持、コンストラクタではそのファクトリを用いて ruby の思考ルーチンインスタンスを生成している。
あとは IComputer の各メソッド呼び出しをそのまま Ruby インスタンスに中継しているだけだ。

com.Ruby.type=Ruby
com.Ruby.description=Random for Ruby
com.Ruby.class=ch.jpn.taoe.blokus.client.com.ScriptComputer

ScriptComputer を定義したので、blokus.properties にも適当に追加しておく。
あ、そうそう。一応 Jython でもそのまま利用できることを期待して、 "ScriptComputer" という名称にしてみた。どうかな〜。使えるかな〜。

package ch.jpn.taoe.blokus.client.com;
import ch.jpn.taoe.blokus.client.IComputer;
import ch.jpn.taoe.blokus.data.Game;
import ch.jpn.taoe.blokus.data.Player;

public interface ScriptComputerFactory {
    public IComputer create(IComputer self, String name, Game game, Player me);
}

こちらは factory のインターフェース。
工夫点は ScriptComputer のインスタンスRuby 側にも渡しておくようにしたことか。AbstractComputer を継承することで提供されるいくつかの機能を、これを通じて Ruby 側からも利用できるよう図っている。

require 'java'
include_class 'ch.jpn.taoe.blokus.client.IComputer'
include_class 'ch.jpn.taoe.blokus.client.com.ScriptComputer'
include_class 'ch.jpn.taoe.blokus.client.com.ScriptComputerFactory'
include_class 'ch.jpn.taoe.blokus.common.BlokusUtilities'
include_class 'ch.jpn.taoe.blokus.common.BoardPoint'
include_class 'ch.jpn.taoe.blokus.data.Blokus'

class RubyRandomComputer
  include IComputer
  def initialize(computer, name, game, me)
    puts "RubyRandomComputer init"
    @com = computer
    @name = name
    @game = game
    @player = me
    @isUsedMap_ = {}
  end

  def doNextTurn(lastPoint); end
  def doTurnChange
    sendRandomCommand if @com.isMyTurn2
  end
  def doEndGame(scores); end
  def doAddBloku(player, point, bloku); end
  def doAckAdd; end
  def doNakAdd
    sendRandomCommand
  end

  private
  def sendRandomCommand
    if !BlokusUtilities.checkExistsBlokuSpace(@game, @player)
      @com.operationSender.sendPassCommand;
    else
      blokuside = Blokus::BlokuSide::values
      blokudirection = Blokus::BlokuDirection::values

      board = @game.board
      1.upto(100) do
        maxPoint = board.maxPoint
        x = rand(maxPoint.getX + 1)
        y = rand(maxPoint.getY + 1)
        point = BoardPoint.valueOf(x, y)

        type = getRandomBloku
        side = blokuside[ rand(blokuside.length) ]
        direction = blokudirection[ rand(blokudirection.length) ]
        blokus = Blokus.new(@player, type)
        blokus.side = side
        blokus.direction = direction

        if board.checkSetBloku(point, blokus)
          @com.operationSender.sendAddCommand(point, blokus)
          @isUsedMap_[type] = true
          return
        end
      end
      @com.operationSender.sendPassCommand
    end
  end

  def getRandomBloku
    blokutype = Blokus::BlokuType::values
    while true
      type = blokutype[ rand(blokutype.length) ]
      return type unless @isUsedMap_.has_key?(type)
    end
  end
end

Ruby による思考ルーチン実装。
一部都合により改変しているものの、基本的には「対戦 Blokus」のランダムプレイヤールーチンである RandomMoveComputer.java のほぼ逐語訳だ。Java 版の半分くらいの長さで書けるのが嬉しいところ。
IComputer インターフェースを include しているが、呼ばれるはずのないメソッドは実装せずに済んでいる。それだけで JRuby 使ってみたくなっちゃう Java の人もいるかも?(笑)

ScriptComputer.factory = fac = ScriptComputerFactory.new
def fac.create(computer, name, game, me)
  RubyRandomComputer.new(computer, name, game, me)
end

最後に factory を作成し、ScriptComputerFactory のスタティックメンバに設定している。
こういう一つしかないオブジェクトをわざわざクラスを定義するところから始めなくても良いのも Java にはできない芸当なので嬉しい。


さて、あとは jirb から Blokus を起動し、同コンソールで上記 Ruby スクリプトも実行してあげれば、Ruby で記述された思考ルーチンが動作する。
簡単、と言い切ってみたいところなのだが、はてさてどうだろう?*1
本体のソースには手を加えていないのも結構ポイントか。とはいえ、「properties に記述されたクラスを必要に応じてインスタンス化」という設計になっていてくれたお陰であり、いつでもそうというわけにはいかない。残念ながら。


ああ、そうそう。もう一つだけ。
思考ルーチンは当然ながら強くするために改変を惜しまないものである。
例えばこのランダムルーチンにしても、「せめて大きいパーツから使って欲しい」と改造したくなった、としよう。そのとき、プログラムを改編して再起動……する必要はない。
jirb から起動していれば、JRuby のコンソールが開いた状態になっている訳なので、そこに例えば次のようにたたき込む。

class RubyRandomComputer
  Blokus5 = [
    Blokus::BlokuType::I5, Blokus::BlokuType::L5, Blokus::BlokuType::Y5,
    Blokus::BlokuType::F5, Blokus::BlokuType::N5, Blokus::BlokuType::V5,
    Blokus::BlokuType::W5, Blokus::BlokuType::X5, Blokus::BlokuType::T5,
    Blokus::BlokuType::P5, Blokus::BlokuType::Z5, Blokus::BlokuType::C5
  ]

  def getRandomBloku
    blokutype = Blokus::BlokuType::values
    while true
      type = if rand < 0.7
        Blokus5[ rand(Blokus5.length) ]
      else
        blokutype[ rand(blokutype.length) ]
      end
      return type unless @isUsedMap_.has_key?(type)
    end
  end
end

改変分だけを再定義している。これで次の対戦からはこの新しいコード(7割の確率で5コマのパーツを置こうとする)が有効になっている。アプリを再起動する必要はない。


ちょっとは楽しげな未来が見えてきただろうか。

*1:ここまでたどり着くのにいろいろハマったので、いつになく慎重