麻雀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ので、性能が気になるならそっちで。

*1:すべてのルールを網羅しない

*2:void

*3:「_.ほげほげ」は、「fun x -> x.ほげほげ」の糖衣構文

*4:最初に作っておいて、コンストラクタで参照を渡せばいい多分