麻雀AIの実装方針について
適当なAIなら*1、600行とかで書けるので、ベタで書いてもそんなに問題にならないが、まじめにやりだすと大変。
ところで、そこそこ見ればバグってないことが分かりそうな実装方針を思いついたのでメモしておく。
コードはScalaっぽい何かで書きます。
AIのインターフェースの確認
trait AI { def onHello(protocol : String, protocol_version : Int) : Join def onStartGame(id : Int, names : List[String]) : jp.wistery.mjai.None def onStartKyoku(bakaze : Pai, kyoku : Int, honba : Int, kyotaku : Int, oya : Int, dora_marker : Pai, tehais : List[List[Pai]]) : jp.wistery.mjai.None def onTsumo(actor : Int, pai : Pai) : OnTsumoResponse def onDahai(actor : Int, pai : Pai, tsumogiri : Boolean) : OnDahaiResponse def onReach(actor : Int) : jp.wistery.mjai.None def onReachAccepted(actor : Int, deltas : List[Int], scores : List[Int]) : jp.wistery.mjai.None def onPon(actor : Int, target : Int, pai : Pai, consumed : List[Pai]) : OnPonResponse def onKan(actor : Int, target : Int, pai : Pai, consumed : List[Pai]) : OnKanResponse def onChi(actor : Int, target : Int, pai : Pai, consumed : List[Pai]) : OnChiResponse def onHora(actor : Int, target : Int, pai : Pai, horaTehais : List[Pai], yakus : List[Yaku], fu : Int, fan : Int, horaPoints : Int, deltas : List[Int], scores : List[Int]) : jp.wistery.mjai.None def onRyukyoku(reason : String, tehais : List[List[Pai]], tenpais : List[Boolean], deltas : List[Int], scores : List[Int]) : jp.wistery.mjai.None def onEndKyoku() : jp.wistery.mjai.None def onEndGame() : jp.wistery.mjai.None }
いっぱい書いてあるけど前にまとめたやつをコードにしただけ。traitっていうのはJavaでいうところのinterfaceです。
OnChiResponseとかが具体的に何なのかは、脳内で補完してください。
Noneの前にやたらついてるのは、組み込みにNoneクラス(OptionのNone)があるため。
Componentインターフェースを定義
trait Component { def onHello(protocol : String, protocol_version : Int) : Unit def onStartGame(id : Int, names : List[String]) : Unit def onStartKyoku(bakaze : Pai, kyoku : Int, honba : Int, kyotaku : Int, oya : Int, dora_marker : Pai, tehais : List[List[Pai]]) : Unit def onTsumo(actor : Int, pai : Pai) : Unit def onDahai(actor : Int, pai : Pai, tsumogiri : Boolean) : Unit def onReach(actor : Int) : Unit def onReachAccepted(actor : Int, deltas : List[Int], scores : List[Int]) : Unit def onPon(actor : Int, target : Int, pai : Pai, consumed : List[Pai]) : Unit def onKan(actor : Int, target : Int, pai : Pai, consumed : List[Pai]) : Unit def onChi(actor : Int, target : Int, pai : Pai, consumed : List[Pai]) : Unit def onHora(actor : Int, target : Int, pai : Pai, horaTehais : List[Pai], yakus : List[Yaku], fu : Int, fan : Int, horaPoints : Int, deltas : List[Int], scores : List[Int]) : Unit def onRyukyoku(reason : String, tehais : List[List[Pai]], tenpais : List[Boolean], deltas : List[Int], scores : List[Int]) : Unit def onEndKyoku() : Unit def onEndGame() : Unit }
AIに似てるけど、返り値が全部Unit*2になってます。
AIの実装クラスの例
extendsって書いてあるけど、Javaで言うところのimplementsの意味です。
class HogeAI extends AI { var id : Int var tehai : TehaiComponent var kawa : KawaComponent var components : List[Component] ... 略 ... def onStartGame(id, names) = { this.id = id this.tehai = new TehaiComponent(id) this.kawa = new KawaComponent(id) this.components = List(this.tehai, this.kawa) return jp.wistery.mjai.None() } ... 略 ... def onTsumo(actor, pai) = { this.components.foreach(_.onTsumo(actor, pai)) if (tehai.isAgari()) return Hora(id, id, pai) else { val sute = new scala.util.Random().nextInt() % tehai.length return Dahai(id, tehai(sute), sute == tehai.length - 1) } } }
このように、各メソッドの先頭で必ずthis.components.foreach(_.ほげほげ)と書く。*3
画一的なインターフェースで委譲出来てるので、すっきり書ける。
あとは、各Componentに定義された便利なメソッドを使ってプログラミングすれば良い。
この例では手牌や河といった割と低レベルな(?)Componentを利用しているが、ルールごとにComponentを用意すると、そのルールに関する記述があるComponentの中にまとまるので見やすいと思う。
もちろん、Componentが更に子Componentを持っていて呼び出すことも考えられる。
というか、
- 手牌Component・河Componentといった低レベルなもの
- フリテンチェックComponent・パイの安全度管理Componentといった高レベルなもの
を作ると、綺麗にまとまる気がする。
このスタイルは、インスタンスの木構造の葉に近い部分に低レベルなComponentで同じ物がいっぱい出来るのが難だけど、目をつぶってしまっていいと思う。
共有するようにもできる*4ので、性能が気になるならそっちで。