Rで麻雀を攻略する

麻雀のデータをtidyに扱えるRパッケージの紹介

あきる(paithiov909)

誰?

TL;DR

  1. 麻雀のデータを扱えるRパッケージをつくっている
  2. 「Rっぽい書き方ができること」を重視している
  3. プレイヤーの手牌や、牌譜(対局ログ)をtidyに扱えるとそれっぽい

麻雀のデータってどんなの?

  • 麻雀をtidyに扱えると何が嬉しいのか🤔

日本における麻雀

Japanese Riichi Mahjong: 日本で遊ばれているルールの麻雀(不完全情報・ゼロサムゲーム)

  • ふつう4人で遊ぶ
  • 手牌が13枚ある状態から「山から1枚引き、要らない牌を1枚捨てる」などの操作を順番に繰り返し、役を完成させるゲーム
  • そうして持ち点をやりとりしていき、一連のゲームの終了時に持っていた点数で順位をつける

背景

日本における麻雀は、そこそこ複雑で、競技性の高いゲーム

→わりと大量の対局ログを扱いたい需要があるらしい

  1. プレイヤーの模倣それ自体に関心があるケースもある(麻雀ゲームのAIを開発したい、など)
  2. 「どんな状況で、どんな行動を選択すれば有利か」をデータから明らかにしたいケースも

モチベーション

状況(特徴量)を入力として与えたとき、有利な行動(ラベル)を返すモデルを学習してみたい

  • 状況(特徴量)
    • ゲームのある時点において「どの牌が、どこに、何枚あるか」
  • 有利な行動(ラベル)
    • どの牌を切るか/鳴くべきか/立直するか

「状況」をどう表現するか

  • 機械学習的には、おもに「ゲームのある時点において、どの牌が、どこに、何枚あるか」に関心がある!
  • そのように「状況」を特徴量化できれば、たとえば「この状況において🀄は当たり牌か?」といった予測タスクに帰着できる

麻雀牌の表現

  • 麻雀牌は、牌種(色)が[mpsz]の4種類
  • [mps]1-9の9ランク、z1-7の7ランクあって、それぞれが4枚ずつある
  • つまり、全部で(3*9+7)*4=136枚のタイルを用いる
  • [mps]5については、4枚中1枚ずつを「赤牌」に置き換えることも(データとしてはランクを0として扱う)
  • これらのほかに「裏向きの牌」を"_"で表すことにして、表現としてはz0に割り当てる

表現とタイルとの対応表

m0, p0, s0は赤牌

0 1 2 3 4 5 6 7 8 9
m 🀋 🀇 🀈 🀉 🀊 🀋 🀌 🀍 🀎 🀏
p 🀝 🀙 🀚 🀛 🀜 🀝 🀞 🀟 🀠 🀡
s 🀔 🀐 🀑 🀒 🀓 🀔 🀕 🀖 🀗 🀘
z 🀫 🀀 🀁 🀂 🀃 🀆 🀅 🀄

強化学習したい場合

牌種をチャンネルとして、牌を9(数値または字牌の種類)×4(牌数)の2次元の面で表した、畳み込みニューラルネットワークで構成する1

次のようなarrayを用意するということ

1 2 3 4 5 6 7 8 9
n1 1 0 1 1 1 1 0 0 0
n2 1 0 0 0 0 1 0 0 0
n3 1 0 0 0 0 0 0 0 0
n4 0 0 0 0 0 0 0 0 0

教師あり学習したい場合

たとえば、次のような特徴量(テーブルデータ)をつくる1

  • 場況情報(各プレイヤーの点棒、場風、自風、本場、ドラ等)
  • 見えている牌の情報(それぞれの牌が場に何枚見えているか)
  • 立直プレイヤーの捨て牌情報
    • 各牌を何枚ずつ切っているか
    • 各色(萬子/筒子/索子)、字牌の割合(全体と6巡目まで)
    • 最初の6巡でヤオチュウ牌(字牌と1,9)以外の割合、4,5,6の割合
    • 各色で最初に切った牌、2番目に切った牌、およびその数字の差
    • 各色で最後に切った牌、最後から2番目に切った牌、およびその数字の差
    • 立直宣言牌、およびその色
    • 各筋を切っているかどうか(1-4萬, 2-5萬, ….., 6-9ソウ)
    • 各色で最初/最後に切った3つの牌を3桁の数字で表現(3ピン→9ピン→4ピンなら394)

麻雀牌をtidyに扱う

  • 教師あり学習のケースは、Rでも真似できそう
  • 同じような特徴量をつくるケースを念頭に「Rっぽい書き方」ができるような機能を実装したい!

Tidy Data

Tidy datasets are easy to manipulate, model and visualize, and have a specific structure: each variable is a column, each observation is a row, and each type of observational unit is a table.

Hadley Wickham (2014). Tidy data.
The Journal of Statistical Software, 59.

Rではこれを扱うための枠組みが非常に強力なため、データを縦長のデータフレームとして持てるようにすると、「Rっぽい書き方」に着地させやすい

麻雀牌のTidy Data表現

麻雀牌をTidy Dataにする場合:

  1. 列(variable): どの牌が、どこに、何枚あるか
  2. 行(observation): 牌の観測
  3. テーブル(observational unit): ゲームのある時点において

つまりこんな感じ

牌種 場所ID 枚数
p2 1 3
s3 1 1
s4 1 1
s5 1 1
z1 1 3
z2 1 3
z3 1 1

Tidy Dataと他の表現

それぞれの牌が何枚かずつで一組になっていることを表せる形式であれば、データとしての意味は保ったまま、いくつかの表現ができる

  1. "p222s345z1112223"
  2. c("p2", "p2", "p2", "s3", "s4", "s5", "z1", "z1", "z1", "z2", "z2", "z2", "z3")
  3. Tidy Data

shikakusphereでの扱い

  1. "p222s345z1112223"
  2. c("p2", "p2", "p2", "s3", "s4", "s5", "z1", "z1", "z1", "z2", "z2", "z2", "z3")
  3. Tidy Data

→これらの表現のあいだで変換できたら便利そう

paistr()

手牌など、牌のまとまりを表現するための独自S3クラス

library(shikakusphere)
(hands <-
  paistr(
    c(
      "p222345z234567",
      "p11222345z12345",
      "m055z7z7,m78-9,z5555,z666=",
      "m055z77,m78-9,z5555,z666=,",
      "m123p055s789z1117*"
    )
  )
)
#> <skksph_paistr[5]>
#> [1] <12>'p222345z234567'             <13>'p11222345z12345'           
#> [3] <15>'m055z7z7,m78-9,z5555,z666=' <15>'m055z77,m78-9,z5555,z666=,'
#> [5] <13>'m123p055s789z1117*'

tidy(<skksph_paistr>)

1→3の変換(ただし、副露か純手牌かの情報は保持しない)

(hands <- tidy(hands))
#> # A tibble: 46 × 3
#>       id tile      n
#>    <int> <fct> <int>
#>  1     1 p2        3
#>  2     1 p3        1
#>  3     1 p4        1
#>  4     1 p5        1
#>  5     1 z2        1
#>  6     1 z3        1
#>  7     1 z4        1
#>  8     1 z5        1
#>  9     1 z6        1
#> 10     1 z7        1
#> # ℹ 36 more rows

lineup()

3→2の変換

(hands <- lineup(hands))
#> [[1]]
#>  [1] p2 p2 p2 p3 p4 p5 z2 z3 z4 z5 z6 z7
#> 38 Levels: _ m0 m1 m2 m3 m4 m5 m6 m7 m8 m9 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 ... z7
#> 
#> [[2]]
#>  [1] p1 p1 p2 p2 p2 p3 p4 p5 z1 z2 z3 z4 z5
#> 38 Levels: _ m0 m1 m2 m3 m4 m5 m6 m7 m8 m9 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 ... z7
#> 
#> [[3]]
#>  [1] m0 m5 m5 m7 m8 m9 z5 z5 z5 z5 z6 z6 z6 z7 z7
#> 38 Levels: _ m0 m1 m2 m3 m4 m5 m6 m7 m8 m9 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 ... z7
#> 
#> [[4]]
#>  [1] m0 m5 m5 m7 m8 m9 z5 z5 z5 z5 z6 z6 z6 z7 z7
#> 38 Levels: _ m0 m1 m2 m3 m4 m5 m6 m7 m8 m9 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 ... z7
#> 
#> [[5]]
#>  [1] m1 m2 m3 p0 p5 p5 s7 s8 s9 z1 z1 z1 z7
#> 38 Levels: _ m0 m1 m2 m3 m4 m5 m6 m7 m8 m9 p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 ... z7

lipai()

2→1の変換(tidy()を経由しているため、元のかたちに戻るわけではない)

(hands <- lipai(hands))
#> [1] "p222345z234567"    "p11222345z12345"   "m055789z555566677"
#> [4] "m055789z555566677" "m123p055s789z1117"

牌譜をtidyに扱う

  • Tidy Dataでは、観測の出現順には必ずしも意味がない
    • 順序に意味を持たせたい場合、観測の順序を表す列をつくっておき、dplyr::arannge()で並べ替えできるようにしておく
  • 手牌は含まれる牌の順序によらず、同じ意味(並べ方によって役が変わったりはしない)
  • しかし、自摸や捨て牌では何巡目の自摸・捨て牌だったかに意味があるため、順序を表す列がほしい

牌譜のデータ形式1

いくつかの形式があるが、天鳳のJSON形式がおそらくもっとも一般的

  • 本場ごとに1つのオブジェクト(各局・プレイヤーごとに配牌・自摸・打牌が配列として格納されている)
  • 自摸・打牌はプレイヤーごとにまとめられているため、プレイヤー内での順序はわかる
  • しかし、本場内における順序はよくわからない(基本的には起家から反時計回りに進むはずだが、副露があると順序が前後するため、パースする際に復元する必要あり)

牌譜のデータ形式2

Mjai Event JSONというのもある

  • Mjaiで使われていた形式
  • 自摸・打牌・副露などのイベントをそれぞれオブジェクトとして、それらの配列を持つ(イベントの順序が保持されている)
  • 天鳳JSONからMjai Event JSONへの変換はEquim-chan/mjai-reviewerに含まれているRust crateで可能

Mjai Event JSONの例

savvyを使って書いたRラッパーでtibbleにしたデータの例

json <- list.files(
  system.file("testdata/", package = "convlog"),
  pattern = "*.json$",
  full.names = TRUE
)
(dat <- convlog::read_tenhou6(json))
#> $game_info
#> # A tibble: 21 × 4
#>    game_id names        qijia aka  
#>      <int> <named list> <int> <lgl>
#>  1       1 <chr [4]>        4 TRUE 
#>  2       2 <chr [4]>        0 TRUE 
#>  3       3 <chr [4]>        0 TRUE 
#>  4       4 <chr [4]>        0 TRUE 
#>  5       5 <chr [4]>        0 TRUE 
#>  6       6 <chr [4]>        0 TRUE 
#>  7       7 <chr [4]>        0 TRUE 
#>  8       8 <chr [4]>        0 TRUE 
#>  9       9 <chr [4]>        0 TRUE 
#> 10      10 <chr [4]>        0 TRUE 
#> # ℹ 11 more rows
#> 
#> $round_info
#> # A tibble: 33 × 10
#>    game_id round_id bakaze dora_marker kyoku honba kyotaku   oya scores tehais  
#>      <int>    <int> <chr>  <chr>       <int> <int>   <int> <int> <list> <list>  
#>  1       1        1 E      3s              1     0       0     0 <int>  <chr[…]>
#>  2       2        1 E      4m              3     3       0     2 <int>  <chr[…]>
#>  3       3        1 E      6m              1     0       0     0 <int>  <chr[…]>
#>  4       4        1 E      E               4     0       0     3 <int>  <chr[…]>
#>  5       5        1 E      8p              2     2       0     1 <int>  <chr[…]>
#>  6       6        1 E      7s              1     0       0     0 <int>  <chr[…]>
#>  7       7        1 S      6s              4     0       0     3 <int>  <chr[…]>
#>  8       8        1 S      2m              4     1       1     3 <int>  <chr[…]>
#>  9       9        1 S      2m              1     0       0     0 <int>  <chr[…]>
#> 10      10        1 S      7s              2     2       0     1 <int>  <chr[…]>
#> # ℹ 23 more rows
#> 
#> $paifu
#> # A tibble: 3,255 × 12
#>    game_id round_id event_id type  actor target pai   tsumogiri consumed
#>      <int>    <int>    <int> <chr> <int>  <int> <chr> <lgl>     <list>  
#>  1       1        1        1 tsumo     0     NA S     NA        <NULL>  
#>  2       1        1        2 dahai     0     NA N     FALSE     <NULL>  
#>  3       1        1        3 tsumo     1     NA 2m    NA        <NULL>  
#>  4       1        1        4 dahai     1     NA 8p    FALSE     <NULL>  
#>  5       1        1        5 tsumo     2     NA 6p    NA        <NULL>  
#>  6       1        1        6 dahai     2     NA 8s    FALSE     <NULL>  
#>  7       1        1        7 tsumo     3     NA 1m    NA        <NULL>  
#>  8       1        1        8 dahai     3     NA P     FALSE     <NULL>  
#>  9       1        1        9 tsumo     0     NA 1m    NA        <NULL>  
#> 10       1        1       10 dahai     0     NA 1p    FALSE     <NULL>  
#> # ℹ 3,245 more rows
#> # ℹ 3 more variables: dora_marker <chr>, deltas <list>, ura_markers <list>

実践:特徴量をつくる

  • 牌譜をデータとして読み込めれば、いろいろと加工できる
  • ここでは試しに、リーチ宣言があった本場について、各本場で1つ目のリーチ直後の時点において「見えている牌」を集計してみる

関心があるイベントだけ残す

まずはじめに、リーチ宣言があった本場について、各本場で1つ目のリーチ直後の打牌までを残す

paifu <-
  dat[["paifu"]] |>
  dplyr::filter(
    # ここでは、ゲームの進行にかかわるイベントだけを残す
    type %in% c("tsumo", "dahai", "chi", "pon", "daiminkan", "kakan", "ankan", "reach")
  ) |>
  dplyr::filter(
    any(type == "reach"),
    # リーチ宣言は`tsumo->reach->dahai`の順なので
    # 次のようにすると、各本場で最初のリーチ直後の`dahai`までを取れる
    (dplyr::lag(type, default = "") == "reach") |>
      dplyr::consecutive_id() <= 2,
    .by = c(game_id, round_id)
  ) |>
  dplyr::mutate(pai = trans_tile(pai))

paifu
#> # A tibble: 848 × 12
#>    game_id round_id event_id type  actor target pai   tsumogiri consumed
#>      <int>    <int>    <int> <chr> <int>  <int> <chr> <lgl>     <list>  
#>  1       5        1        1 tsumo     1     NA m3    NA        <NULL>  
#>  2       5        1        2 dahai     1     NA z2    FALSE     <NULL>  
#>  3       5        1        3 tsumo     2     NA s9    NA        <NULL>  
#>  4       5        1        4 dahai     2     NA s9    TRUE      <NULL>  
#>  5       5        1        5 tsumo     3     NA z2    NA        <NULL>  
#>  6       5        1        6 dahai     3     NA m9    FALSE     <NULL>  
#>  7       5        1        7 tsumo     0     NA z7    NA        <NULL>  
#>  8       5        1        8 dahai     0     NA z2    FALSE     <NULL>  
#>  9       5        1        9 tsumo     1     NA z1    NA        <NULL>  
#> 10       5        1       10 dahai     1     NA z7    FALSE     <NULL>  
#> # ℹ 838 more rows
#> # ℹ 3 more variables: dora_marker <chr>, deltas <list>, ura_markers <list>

打牌を集計する

その時点までの打牌(捨て牌)は全員から見えるので、ふつうに集計すればよい

summary_dapai <-
  paifu |>
  dplyr::filter(type == "dahai") |>
  dplyr::summarize(n = dplyr::n(), .by = c(game_id, round_id, pai)) |>
  dplyr::mutate(
    tile = factor(pai, levels = shikakusphere::tiles[["cmajiang"]]),
    .keep = "unused"
  )

summary_dapai
#> # A tibble: 256 × 4
#>    game_id round_id     n tile 
#>      <int>    <int> <int> <fct>
#>  1       5        1     3 z2   
#>  2       5        1     3 s9   
#>  3       5        1     1 m9   
#>  4       5        1     2 z7   
#>  5       5        1     1 z3   
#>  6       5        1     1 m3   
#>  7       5        1     1 p4   
#>  8       5        1     2 z5   
#>  9       5        1     2 s6   
#> 10       5        1     1 p2   
#> # ℹ 246 more rows

手牌を集計する1

手牌は自分のものだけ見えるはずなので、どのプレイヤー視点かを決めてから集計する

ここでは、リーチした人の下家(右隣)の手牌について集計してみる

手牌を集計する2

まず先に、各本場におけるプレイヤーの配牌をtibbleにまとめておく

qipai <-
  dat[["round_info"]] |>
  dplyr::rowwise() |>
  dplyr::reframe(
    game_id = game_id,
    round_id = round_id,
    actor = 0:3,
    tehais
  ) |>
  dplyr::group_by(game_id, round_id, actor) |>
  dplyr::mutate(qipai = list(trans_tile(as.character(tehais))), .keep = "unused") |>
  dplyr::ungroup()

qipai
#> # A tibble: 132 × 4
#>    game_id round_id actor qipai     
#>      <int>    <int> <int> <list>    
#>  1       1        1     0 <chr [13]>
#>  2       1        1     1 <chr [13]>
#>  3       1        1     2 <chr [13]>
#>  4       1        1     3 <chr [13]>
#>  5       2        1     0 <chr [13]>
#>  6       2        1     1 <chr [13]>
#>  7       2        1     2 <chr [13]>
#>  8       2        1     3 <chr [13]>
#>  9       3        1     0 <chr [13]>
#> 10       3        1     1 <chr [13]>
#> # ℹ 122 more rows

手牌を集計する3

次に、リーチしたプレイヤーの下家をまとめあげて……

pov <- dplyr::filter(paifu, type == "reach") |>
  dplyr::select(game_id, round_id, actor) |>
  # プレイヤーのid [0...3] は反時計回りに振られている(0の下家は1, 対面は2, 上家が3)
  dplyr::mutate(shimocha = (actor + 1) %% 4, .keep = "unused")

pov
#> # A tibble: 14 × 3
#>    game_id round_id shimocha
#>      <int>    <int>    <dbl>
#>  1       5        1        3
#>  2       6        1        3
#>  3      10        1        1
#>  4      11        1        2
#>  5      13        1        1
#>  6      14        1        0
#>  7      16        1        3
#>  8      16        4        0
#>  9      16        6        1
#> 10      16        7        1
#> 11      16        9        0
#> 12      16       10        1
#> 13      18        1        0
#> 14      21        2        1

手牌を集計する4

次のようにすると、その時点における下家の手牌を再現できる

shoupai <- paifu |>
  dplyr::left_join(pov, by = dplyr::join_by(game_id, round_id)) |>
  dplyr::filter(actor == shimocha) |>
  dplyr::summarize(
    zimo = list(pai[which(type %in% c("tsumo", "chi", "pon", "daiminkan"))]),
    dapai = list(pai[which(type %in% c("dahai", "kakan", "ankan"))]),
    .by = c(game_id, round_id, actor)
  ) |>
  dplyr::left_join(qipai, by = dplyr::join_by(game_id, round_id, actor)) |>
  dplyr::reframe(
    game_id = game_id,
    round_id = round_id,
    player = actor,
    last_state = proceed(qipai, zimo, dapai) # この関数で手牌を再現する
  )

shoupai
#> # A tibble: 14 × 4
#>    game_id round_id player last_state             
#>      <int>    <int>  <int> <paistr>               
#>  1       5        1      3 <13>'m2405666p456s44z5'
#>  2       6        1      3 <13>'s1234567z223344'  
#>  3      10        1      1 <13>'m4456p999s2489z22'
#>  4      11        1      2 <13>'m456p56789s14689' 
#>  5      13        1      1 <13>'m33789p357s34778' 
#>  6      14        1      0 <13>'m67789s1367899z4' 
#>  7      16        1      3 <13>'m12330p23488s144' 
#>  8      16        4      0 <13>'m788889p24s45679' 
#>  9      16        6      1 <13>'m4567p11223346s3' 
#> 10      16        7      1 <13>'m1p2346s11233z223'
#> 11      16        9      0 <13>'m2235p11340s2278' 
#> 12      16       10      1 <13>'m136p347s44789z56'
#> 13      18        1      0 <13>'m22244p4405s2356' 
#> 14      21        2      1 <13>'m34p407s11223566'

手牌を集計する5

後は、ふつうに集計すればよい

summary_shoupai <- shoupai |>
  dplyr::reframe(
    tidy(last_state),
    .by = c(game_id, round_id, player)
  ) |>
  dplyr::select(!c(player, id))

summary_shoupai
#> # A tibble: 145 × 4
#>    game_id round_id tile      n
#>      <int>    <int> <fct> <int>
#>  1       5        1 m0        1
#>  2       5        1 m2        1
#>  3       5        1 m4        1
#>  4       5        1 m5        1
#>  5       5        1 m6        3
#>  6       5        1 p4        1
#>  7       5        1 p5        1
#>  8       5        1 p6        1
#>  9       5        1 s4        2
#> 10       5        1 z5        1
#> # ℹ 135 more rows

副露を集計する

副露は全員から見えるので、ふつうに集計できる。実際の副露メンツを表す文字列をつくるのがポイント

summary_fulou <- paifu |>
  dplyr::group_by(game_id, round_id, actor) |>
  dplyr::mutate(
    # `mjai_conv()`で副露メンツを表す文字列をつくれる
    pai = mjai_conv(type, pai, consumed, mjai_target(actor, target))
  ) |>
  dplyr::group_by(game_id, round_id) |>
  dplyr::filter(type %in% c("chi", "pon", "daiminkan", "ankan", "kakan")) |>
  dplyr::summarize(pai = paste0(pai, collapse = ",") |> paistr(), .groups = "keep") |>
  dplyr::reframe(tidy(pai)) |>
  dplyr::select(!id)

summary_fulou
#> # A tibble: 22 × 4
#>    game_id round_id tile      n
#>      <int>    <int> <fct> <int>
#>  1       5        1 m6        3
#>  2       5        1 m7        3
#>  3       5        1 s5        3
#>  4       5        1 z7        3
#>  5       6        1 p1        3
#>  6       6        1 p2        3
#>  7       6        1 p3        3
#>  8       6        1 z5        3
#>  9       6        1 z6        3
#> 10      11        1 m7        3
#> # ℹ 12 more rows

Tidy Dataを横に展開する

集計した縦長のデータを横長に展開する

feat <- list(dapai = summary_dapai, shoupai = summary_shoupai, fulou = summary_fulou) |>
  purrr::imap(\(tbl, name) {
    dplyr::mutate(tbl,
      where = name,
      tile = forcats::fct_drop(tile, only = "_") # 裏向きの牌を表す水準"_"をdropする
    )
  }) |>
  purrr::list_rbind() |>
  tidyr::pivot_wider(
    id_cols = c(game_id, round_id),
    names_from = c(where, tile),
    names_expand = TRUE,
    values_from = n,
    values_fill = 0
  )

feat
#> # A tibble: 14 × 113
#>    game_id round_id dapai_m0 dapai_m1 dapai_m2 dapai_m3 dapai_m4 dapai_m5
#>      <int>    <int>    <int>    <int>    <int>    <int>    <int>    <int>
#>  1       5        1        0        2        0        1        0        1
#>  2       6        1        0        1        0        0        0        0
#>  3      10        1        0        1        1        0        0        1
#>  4      11        1        0        1        0        0        0        0
#>  5      13        1        0        1        0        0        1        2
#>  6      14        1        0        2        0        1        1        0
#>  7      16        1        0        0        1        0        1        0
#>  8      16        4        0        2        1        0        2        2
#>  9      16        6        0        1        1        1        0        0
#> 10      16        7        0        3        1        0        0        0
#> 11      16        9        0        1        0        2        1        0
#> 12      16       10        0        0        1        0        0        0
#> 13      18        1        0        2        0        0        1        1
#> 14      21        2        0        2        0        0        2        1
#> # ℹ 105 more variables: dapai_m6 <int>, dapai_m7 <int>, dapai_m8 <int>,
#> #   dapai_m9 <int>, dapai_p0 <int>, dapai_p1 <int>, dapai_p2 <int>,
#> #   dapai_p3 <int>, dapai_p4 <int>, dapai_p5 <int>, dapai_p6 <int>,
#> #   dapai_p7 <int>, dapai_p8 <int>, dapai_p9 <int>, dapai_s0 <int>,
#> #   dapai_s1 <int>, dapai_s2 <int>, dapai_s3 <int>, dapai_s4 <int>,
#> #   dapai_s5 <int>, dapai_s6 <int>, dapai_s7 <int>, dapai_s8 <int>,
#> #   dapai_s9 <int>, dapai_z1 <int>, dapai_z2 <int>, dapai_z3 <int>, …

実践:ラベルをつくる

  • 実際にどういうラベルを用意するかは、モデルの学習のさせ方による
  • いずれにせよ、リーチしたプレイヤーの手牌について、当たり牌が何かを調べる必要がありそう

リーチした人を集める

まず、リーチしたプレイヤーをまとめる

reach_player <- paifu |>
  dplyr::filter(type == "reach") |>
  dplyr::mutate(who_reaches = actor, .keep = "unused") |>
  dplyr::select(game_id, round_id, who_reaches)

reach_player
#> # A tibble: 14 × 3
#>    game_id round_id who_reaches
#>      <int>    <int>       <int>
#>  1       5        1           2
#>  2       6        1           2
#>  3      10        1           0
#>  4      11        1           1
#>  5      13        1           0
#>  6      14        1           3
#>  7      16        1           2
#>  8      16        4           3
#>  9      16        6           0
#> 10      16        7           0
#> 11      16        9           3
#> 12      16       10           0
#> 13      18        1           3
#> 14      21        2           0

当たり牌を調べる

そして、リーチしたプレイヤーの手牌を再現し、それらの当たり牌を調べる

label <- paifu |>
  dplyr::left_join(reach_player, by = dplyr::join_by(game_id, round_id)) |>
  dplyr::filter(actor == who_reaches) |>
  dplyr::summarize(
    zimo = list(pai[which(type %in% c("tsumo", "chi", "pon", "daiminkan"))]),
    dapai = list(pai[which(type %in% c("dahai", "kakan", "ankan"))]),
    .by = c(game_id, round_id, actor)
  ) |>
  dplyr::left_join(qipai, by = dplyr::join_by(game_id, round_id, actor)) |>
  dplyr::reframe(
    game_id = game_id,
    round_id = round_id,
    player = actor,
    last_state = proceed(qipai, zimo, dapai)
  ) |>
  # `collect_tingpai()`で当たり牌を集められる
  dplyr::mutate(atari_pai = collect_tingpai(last_state)) |>
  tidyr::unnest_longer(atari_pai)

label
#> # A tibble: 26 × 5
#>    game_id round_id player last_state             atari_pai
#>      <int>    <int>  <int> <paistr>               <chr>    
#>  1       5        1      2 <13>'m11234p78s123406' p6       
#>  2       5        1      2 <13>'m11234p78s123406' p9       
#>  3       6        1      2 <13>'m1234567s555z777' m1       
#>  4       6        1      2 <13>'m1234567s555z777' m4       
#>  5       6        1      2 <13>'m1234567s555z777' m7       
#>  6      10        1      0 <13>'m234p44s12334789' s2       
#>  7      10        1      0 <13>'m234p44s12334789' s5       
#>  8      11        1      1 <13>'m123p1233445s123' p1       
#>  9      11        1      1 <13>'m123p1233445s123' p4       
#> 10      13        1      0 <13>'m4067799p340s789' m7       
#> # ℹ 16 more rows

特徴量と組み合わせる

たとえば「s2は当たり牌か」のみを2値で予測するモデルを学習したいなら、次のようにすればよい

dplyr::left_join(
  dplyr::summarize(label,
    label = ("s2" %in% atari_pai),
    .by = c(game_id, round_id)
  ),
  feat,
  by = dplyr::join_by(game_id, round_id)
)
#> # A tibble: 14 × 114
#>    game_id round_id label dapai_m0 dapai_m1 dapai_m2 dapai_m3 dapai_m4 dapai_m5
#>      <int>    <int> <lgl>    <int>    <int>    <int>    <int>    <int>    <int>
#>  1       5        1 FALSE        0        2        0        1        0        1
#>  2       6        1 FALSE        0        1        0        0        0        0
#>  3      10        1 TRUE         0        1        1        0        0        1
#>  4      11        1 FALSE        0        1        0        0        0        0
#>  5      13        1 FALSE        0        1        0        0        1        2
#>  6      14        1 FALSE        0        2        0        1        1        0
#>  7      16        1 TRUE         0        0        1        0        1        0
#>  8      16        4 FALSE        0        2        1        0        2        2
#>  9      16        6 FALSE        0        1        1        1        0        0
#> 10      16        7 FALSE        0        3        1        0        0        0
#> 11      16        9 FALSE        0        1        0        2        1        0
#> 12      16       10 FALSE        0        0        1        0        0        0
#> 13      18        1 FALSE        0        2        0        0        1        1
#> 14      21        2 FALSE        0        2        0        0        2        1
#> # ℹ 105 more variables: dapai_m6 <int>, dapai_m7 <int>, dapai_m8 <int>,
#> #   dapai_m9 <int>, dapai_p0 <int>, dapai_p1 <int>, dapai_p2 <int>,
#> #   dapai_p3 <int>, dapai_p4 <int>, dapai_p5 <int>, dapai_p6 <int>,
#> #   dapai_p7 <int>, dapai_p8 <int>, dapai_p9 <int>, dapai_s0 <int>,
#> #   dapai_s1 <int>, dapai_s2 <int>, dapai_s3 <int>, dapai_s4 <int>,
#> #   dapai_s5 <int>, dapai_s6 <int>, dapai_s7 <int>, dapai_s8 <int>,
#> #   dapai_s9 <int>, dapai_z1 <int>, dapai_z2 <int>, dapai_z3 <int>, …

まとめ

  • 麻雀のデータを「Rっぽい書き方」で扱えるパッケージをつくった
  • プレイヤーの手牌や牌譜をtidyに扱うようにすると、体験がよい
  • このRパッケージを使って、Rで麻雀を攻略しよう!

Enjoy✨