というわけで昨日の Ruby のオブジェクトを Java に渡す記事をふまえて、いよいよ「対戦 Blokus」の思考ルーチンを実際に Ruby で書いてみる。
「対戦 Blokus」の思考ルーチンは、interface IComputer の implements である abstruct class AbstractComputer を継承したクラスとして実装、そのクラス名を blokus.properties に記述することで組み込まれる。
このクラス(あるいは相当品)を Ruby で書けるようにするのが目指すゴールだ。
以下、ターゲットアプリケーションに沿って具体的に説明していくが、「共通インターフェースを持つクラスを properties に書いておいて、必要なときにインスタンス化して使う」という Java で一般的な設計に適応するケースとして読んでもらえれば、ほかのアプリケーションでも参考にしてもらえるのではないかと思う。
これを果たすには、昨日の記事に加えてもう2つ TIPS が必要になる。
- Java の abstruct class を Ruby で extends することはできない。定義しても、インスタンス化しようとした時にエラー発生。インターフェースを使うべし。
- Ruby のクラスを Java に渡して、Java 側でインスタンス化させることはできない。Java 側で Ruby のインスタンスを生成させたければ factory を使うべし。
詳しくはまた機会があれば。
というわけで必要なものは、
- 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:ここまでたどり着くのにいろいろハマったので、いつになく慎重