麻雀AIの作り方(mjaiの使い方)

備忘録も兼ねてます。多少の間違いがあるかも。

対戦サーバの選択

こちら(http://togetter.com/li/230676)も参照。ただし若干古い。
現在は、mjai(http://gimite.net/pukiwiki/index.php?Mjai%20%CB%E3%BF%FDAI%C2%D0%C0%EF%A5%B5%A1%BC%A5%D0)一択と思われる。理由は省略。

とりあえず動くプログラム

こんな感じ。*1

#!/usr/bin/env ruby

require "socket"
require "json"
require "uri"

host = "gimite.net"
port = 11600
room = "manue-1kyoku"

socket = TCPSocket.new(host, port)
socket.sync = true
id = nil

socket.each_line() do |line|
  
  $stderr.puts("<-\t%s" % line.chomp())
  action = JSON.parse(line.chomp())
  case action["type"]
    when "hello"
      response = {
          "type" => "join",
          "name" => "tsumogiri",
          "room" => room,
      }
    when "start_game"
      id = action["id"]
      response = {"type" => "none"}
    when "end_game"
      break
    when "tsumo"
      if action["actor"] == id
        response = {
            "type" => "dahai",
            "actor" => id,
            "pai" => action["pai"],
            "tsumogiri" => true,
        }
      else
        response = {"type" => "none"}
      end
    when "error"
      break
    else
      response = {"type" => "none"}
  end
  $stderr.puts("->\t%s" % JSON.dump(response))
  socket.puts(JSON.dump(response))
  
end

実際に動かす場合、rubyをインストール、gem install json; ruby tsumogiri.rb。gimite氏のサーバでmjai-manueと1局戦(東1局のみ)ができる。

自分でサーバを立てる

サーバも手元で動かすほうが、ログを見れるので便利。

立てる側
$ sudo gem install mjai mjai-manue
$ mjai server --port=11600 --game_type=one_kyoku --room=default --log_dir=./log

でOK.

つなぐ側

上のコードで言うと、

host = "localhost"
port = 11600
room = "default"

に変える。

オプションの詳細
    • game_type=[one_kyoku|tonpu|tonnan] # ゲームのタイプ指定。デフォルトはone_kyoku
    • port=56565 # ポート指定。デフォルトは11600
    • room=foobar # ルーム名指定。デフォルトはdefault
    • repeat # 無限ゲーム。デフォルトはfalse
    • games=20 # ゲーム数指定。デフォルトは1
    • log_dir=./log # 牌譜出力ディレクトリ指定。必須

ビジュアライザ

ビジュアライザが付属している。

$ mjai convert log/2012-08-18-152342.mjson a.html
$ google-chrome a.html # firefoxでは動かないので注意

プロトコルの詳細

基本

こんな感じのjson

<- { "type": "tsumo", "actor": 1, "pai": "3s" }
-> { "type": "dahai", "actor": 1, "pai": "5p", "tsumogiri": false }

ただし、<- : サーバからクライアントへの通信

  • > : クライアントからサーバへの通信

サーバから1つメッセージが来たら、ちょうど1つのメッセージを返す。

hello
<- { "type": "hello", "protocol": "mjsonp", "protocol_version": 1 }
-> { "type": "join", "name": "wistery_k", "room": "default" }

要するに参加表明。適当。

start_game
<- { "type": "start_game", "id":2, "names": ["manue","manue","wistery_k","manue"]}
-> { "type": "none" }

自分のidと対局者の名前が来る。
idは0-indexedで、idが0の人が起家。
idは重要なので保存しておく必要がある。
noneを返す。

start_kyoku
<- {"type": "start_kyoku", "bakaze": "E","kyoku":1, "honba": 0, "kyotaku": 0, "oya": 0, "dora_marker" :"5sr", "tehais": [["?","?","?","?","?","?","?","?","?","?","?","?","?"],["?","?","?","?","?","?","?","?","?","?","?","?","?"],["1m","1m","3m","4m","7p","9p","9p","3s","7s","S","P","F","F"],["?","?","?","?","?","?","?","?","?","?","?","?","?"]] }
-> {"type":"none"}

局開始。場風、局数(1-indexed,例えば5は南1局)、本場、供託、親、ドラ表示牌、手牌が来る。
noneを返す。

tsumo
<- { "type": "tsumo", "actor": 2, "pai": "3s" }
-> { "type": "dahai", "actor": 2, "pai": "5p", "tsumogiri": false }
<- { "type": "tsumo", "actor": 2, "pai": "3s" }
-> { "type": "reach", "actor": 2 }
<- { "type": "tsumo", "actor": 2, "pai": "3s" }
-> { "type": "kan", "actor": 2, "pai": "5p", "consumed": ["5p", "5p", "5p"] }
<- { "type": "tsumo", "actor": 2, "pai": "3s" }
-> { "type": "hora", "actor": 2, "target": 2, "pai": "3s" }
<- { "type": "tsumo", "actor": 3, "pai": "?" }
-> { "type": "none" }

いずれかのプレーヤーがツモ。actorが自分の時はdahaiかreachかreachかhoraを返す。*2
actorが自分以外の場合はnoneを返す。

dahai
<- { "type": "dahai", "actor": 1, "pai": "2s", "tsumogiri": false }
-> { "type": "none" }
<- { "type": "dahai", "actor": 1, "pai": "2s", "tsumogiri": false }
-> { "type": "pon", "actor": 2, "target": 3, "pai": "2s", "consumed": ["2s", "2s"] }
<- { "type": "dahai", "actor": 1, "pai": "2s", "tsumogiri": false }
-> { "type": "kan", "actor": 2, "target": 3, "pai": "2s", "consumed": ["2s", "2s", "2s"] }
<- { "type": "dahai", "actor": 1, "pai": "2s", "tsumogiri": false }
-> { "type": "chi", "actor": 2, "target": 3, "pai": "2s", "consumed": ["1s", "3s"] }
<- { "type": "dahai", "actor": 1, "pai": "2s", "tsumogiri": false }
-> { "type": "hora", "actor": 2, "target": 1, "pai": "2s" }
<- { "type": "dahai", "actor": 1, "pai": "2s", "tsumogiri": false }
-> { "type": "none" }
<- { "type": "dahai", "actor": 2, "pai": "2s", "tsumogiri": false }
-> { "type": "none" }

打牌。自分以外の打牌にはpon, kan, chi, hora, noneのいずれか。
自分の打牌にはnone。*3

reach
<- { "type": "reach", "actor": 2 }
-> { "type": "dahai", "actor": 2, "pai": "5p", "tsumogiri": false }
<- { "type": "reach", "actor": 1 }
-> { "type": "none" }

誰かがリーチ宣言した。それが自分の場合、dahaiを返す。
自分以外の場合、noneを返す。

reach_accepted
<- { "type": "reach_accepted", "actor": 2, "deltas": [0,0,-1000,0], "scores": [22700,27900,19700,28700] }
-> { "type": "none" }

リーチが受理された(宣言牌がロンされなかった)。リーチ棒が出て点棒と供託が変更される。
常にnoneを返す。

pon
<- { "type": "pon", "actor": 2, "target": 3, "pai": "S", "consumed": ["S","S"] }
-> { "type": "dahai", "actor": 2, "pai": "5p", "tsumogiri": false }
<- { "type": "pon", "actor": 0, "target": 3, "pai": "S", "consumed": ["S","S"] }
-> { "type": "none" }

ポン。自分の場合、dahaiを返す。他人の場合、noneを返す。*4

chi

ponと同じ。

kan

ponと同じ。*5

hora
<- { "type": "hora", "actor":0, "target": 1, "pai": "9p", "hora_tehais": ["2p","2p","4p","5pr","6p","9p","9p","3s","4s","5s"], "yakus": [["akadora",1],["bakaze",1]], "fu": 30, "fan": 2, "hora_points": 2000, "deltas": [2000,-2000,0,0], "scores": [27000,24300,23700,25000] }
-> { "type": "none" }

誰かの和了。noneを返す。
hora_tehaisで手牌が来るが、鳴きは来ない。自分で覚えてれば知っているはずなので、必要ならそれを使う。

ryukyoku
<- { "type": "ryukyoku", "reason": "fanpai", "tehais": [["?","?","?","?","?","?","?","?","?","?"],["?","?","?","?","?","?","?","?","?","?","?","?","?"],["4m","6m","7p","8p","9p","3s","4s","5s","7s","8s","9s","C","C"],["?","?","?","?","?","?","?","?","?","?","?","?","?"]], "tenpais": [false,false,true,false], "deltas": [-1000,-1000,3000,-1000], "scores": [21700,26900,22700,27700] }
-> { "type": "none" }

流局。noneを返す。

end_kyoku
<- { "type": "end_kyoku" }
-> { "type": "none" }

局終わり。noneを返す。

end_game
<- { "type": "end_game" }

ゲーム終わり。これは例外的に何も返さないで良い(多分)。

error

まとめ

多分、最低限必要/重要な情報は網羅したはず。
Let's クソゲ!!

お願い

手前味噌気味ですが、、、これ(https://github.com/wistery-k/mjai-silica)を倒したらコメントか何かください
結構頑張ったつもりだけど、どうだろう。。。

*1:もちろんプログラミング言語rubyである必要はない。要は同じようにjsonをやり取りすればいい。

*2:九種九牌とかも多分あるけど、確認してない

*3:蛇足だが、自分の打牌による手牌の更新はtsumoに対してdahaiを返した時ではなく、このタイミングですることが推奨される。例えばバリデータで包んだ時に整合性が保たれるようにするため

*4:dahaiと同様、自分のフーロ牌はここで更新推奨。

*5:ルールの話だが、暗槓の場合だけ、リーチや更にカンもできるかもしれない。