1-2 適当に作ったAIたちで3枚麻雀のリーグ戦してみた

みなさんこんにちは。たんぴんです。実に3年半ぶりの更新となります。
連載モノとしては、とうに破綻しているものになりますが、久々にやりたい欲が出てきたので少し進めてみます。

さて、前回は最初の自作AI、「ツモ切りマシーン」を作成し、ツモ切りマシーン同士を戦わせてニヤニヤしていましたが、
今回はルールベースの対戦者を何人か用意して、そいつらで戦わせてみたいと思います。


ここで優勝した人には、強化学習をする際の対戦相手になってもらうかもしれません。

あんまりここで凝ったものを作ってしまっても仕方がないので、5行くらい分だけ、各プレイヤーたちに知能を持ってもらうことにします。

pg1 ツモ切りマシーンの対極者

前回のツモ切りマシーンくんには、致命的な欠陥がありました。
それは、「配牌で聴牌していなければ絶対にあがれない」ということです。

そんな欠点を完全克服した、第2号のCOMがこちらになります。

class PlayerTedashi:
	def __init__(self):
		pass

	def get_name(self):
		return "手出しマシーン"

	def get_player_number(self,number):
		pass

	def act(self,hand,discard):
		return hand[0]

	def get_result(self,winner,turn):
		pass

ツモ切りマシーンと比較すると、切る牌がhand[2]からhand[0]に修正されています。
現状理牌する機能は持っていないので、手牌の中で一番古い牌を切っていく、ということになります。

これにより、毎回手牌が入れ替わるので、聴牌していないためにアガリ放棄となってしまうことがなくなりました。

pg2 字牌コレクターと字牌整理

もし3枚麻雀を誰かに指導する立場なら、みなさんは最初に何を伝えるでしょうか。
私なら、「字牌から切りなさい」と言うと思います。恐らくこのゲームは字牌がかなり弱いです。

麻雀を打つ方なら分かると思いますが、このゲームは刻子(1,1,1)を作るより、順子(1,2,3)を作る方が数倍簡単です。

さらに、3枚麻雀では刻子待ち(手牌が、1,1 のような状態)ではあがり牌が1の残り2枚ですが、
順子待ちの場合は、(2,4)のような1種類の愚形待ちでも4枚、(2,3)のように2種類の良形(リャンメン)待ちでは8枚になります。
※普通の麻雀の刻子待ち(シャンポン待ち)の場合、2種類待てるので待ち枚数は4枚となります。

したがって、刻子しか作れない發と中は、弱いはずです。

class PlayerJi:
  def __init__(self):
    pass

  def get_name(self):
    return "三元牌マニア"

  def get_player_number(self,number):
    pass

  def act(self,hand,discard):
    if hand[0] != 36 and hand[0] != 37:
      return hand[0]
    elif hand[1] != 36 and hand[1] != 37:
      return hand[1]
    return hand[2]

  def get_result(self,winner,turn):
    pass

class PlayerNotJi:
  def __init__(self):
    pass

  def get_name(self):
    return "数学者"

  def get_player_number(self,number):
    pass

  def act(self,hand,discard):
    if hand[0] == 36 or hand[0] == 37:
      return hand[0]
    elif hand[1] == 36 or hand[1] == 37:
      return hand[1]
    return hand[2]

  def get_result(self,winner,turn):
    pass

三元牌マニア氏は、字牌を集めることを生業としているので、手牌の字牌でないものは切ってしまいます。
このロジックだと、手牌が「發、中」の状態で固定されてしまったりしますが、それはご愛敬です。

一方で数学者は、数字以外に興味がない(※実在の数学者様のことを言っているわけではないのであしからず。。)ので、数字の書いてない牌が手牌にある場合は切ってしまいます。

三元牌マニア氏は、下手するとツモ切りマシーンより戦績が悪くなるかもしれませんが、それでも自分の戦い方には誇りを持っているのでしょう。

pg3 こだわりの強い人

最後にもう1人、めちゃくちゃクセのつよい人を追加します。

実際の麻雀でも、面前へのこだわりや、常にホンイツを狙う人などこだわりがある人はいますが、この人のこだわりはさらに強いです。

class Player456:
  def __init__(self):
    pass

  def get_name(self):
    return "四五六太郎"

  def get_player_number(self,number):
    pass

  def act(self,hand,discard):
    if hand[0] != 4:
      return hand[0]
    elif hand[1] != 5:
      return hand[1]
    return hand[2]

  def get_result(self,winner,turn):
    pass

四五六太郎は、手牌の一番左を4、真ん中を5として6であがることに強いこだわりを持っています。
それ以外は聴牌していようと、していなかろうと気にしません。

この人は結構強そうで、理由としては

  • ロジック上、手に4が残るので、4,5でなくても聴牌しやすい

  • 見逃しの機能はないので、4,5で聴牌しているときは3が出てもついあがってしまう。

というところです。今回の優勝候補ですね。

いざ、リーグ戦!

リーグ戦です。
今回登場した4選手と、このゲーム最古参、既に4年目となるツモ切りマシーン選手の計5名が集まり、各10000戦ずつ対戦します。
また、エキシビジョンマッチとして同じプレイヤー同士も対戦させてみました。

果たして結果はどうなるでしょうか。まずは詳細結果から

最終結果
ツモ切りマシーン : 2678勝
ツモ切りマシーン : 2653勝
引き分け : 4669回

最終結果
ツモ切りマシーン : 2476勝
手出しマシーン : 5839勝
引き分け : 1685回

最終結果
ツモ切りマシーン : 2755勝
三元牌マニア : 4025勝
引き分け : 3220回

最終結果
ツモ切りマシーン : 2545勝
数学者 : 3834勝
引き分け : 3621回

最終結果
ツモ切りマシーン : 2408勝
四五六太郎 : 6906勝
引き分け : 686回

最終結果
手出しマシーン : 4712勝
手出しマシーン : 4681勝
引き分け : 607回

最終結果
手出しマシーン : 5628勝
三元牌マニア : 3184勝
引き分け : 1188回

最終結果
手出しマシーン : 4981勝
数学者 : 3655勝
引き分け : 1364回

最終結果
手出しマシーン : 4067勝
四五六太郎 : 5720勝
引き分け : 213回

最終結果
三元牌マニア : 3576勝
三元牌マニア : 3819勝
引き分け : 2605回

最終結果
三元牌マニア : 3406勝
数学者 : 3915勝
引き分け : 2679回

最終結果
三元牌マニア : 2866勝
四五六太郎 : 6692勝
引き分け : 442回

最終結果
数学者 : 3714勝
数学者 : 3587勝
引き分け : 2699回

最終結果
数学者 : 3394勝
四五六太郎 : 6098勝
引き分け : 508回

最終結果
四五六太郎 : 5021勝
四五六太郎 : 4878勝
引き分け : 101回

リーグ表にまとめるとこのようになりました。

リーグ戦結果

前評判通り四五六太郎選手が優勝となりました。おめでとうございます。

そして、2位と健闘したのは手出しマシーン選手
牌にこだわりのある三元牌マニア選手や数学者選手は、ランダムに手牌を入れ替えるだけの手出しマシーン選手に負けたことは悔しいのではないでしょうか。

ツモ切りマシーン選手は、全敗という結果にはなりましたが、どの試合でも同じくらいのアガリ回数をとることができているので、自分らしい戦いはできたと言えるのではないでしょうか。

おまけ

一応、ちゃんとしたやつも作ってみました。
下記のような優先順位で切る牌を選びます。
1. 両面があれば、それ以外の牌を捨てる。
2. 35,46,57のカンチャンがあれば、それ以外の牌を捨てる。
3. 13,24,68,79のカンチャンがあれば、それ以外の牌を捨てる。
4. 12,89のペンチャンがあれば、それ以外の牌を捨てる。
5. 11などの対子があれば、それ以外の牌を捨てる。
6. 聴牌していない場合は、字牌⇒1,9牌⇒2.8⇒3,7⇒4.6⇒5の優先順位で捨てる。

class PlayerNormal:
  def __init__(self):
    self.tenpai_lists = []
    # 両面
    self.tenpai_lists.append([[2,3],[3,4],[4,5],[5,6],[6,7],[7,8]])
    # 内カンチャン
    self.tenpai_lists.append([[3,5],[4,6],[5,7]])
    # 外カンチャン
    self.tenpai_lists.append([[1,3],[2,4],[6,8],[7,9]])
    # ペンチャン
    self.tenpai_lists.append([[1,2],[8,9]])
    # 対子
    self.tenpai_lists.append([[1,1],[2,2],[3,3],[4,4],[5,5],[6,6],[7,7],[8,8],[9,9],[36,36],[37,37]])

    # 浮き牌を切る優先順位
    self.ukihai_list = [36,37,1,9,2,8,3,7,4,6,5]
  def get_name(self):
    return "普通のロジック"

  def get_player_number(self,number):
    pass

  def act(self,hand,discard):
    # 先に手牌をソートしておく
    hand_tmp = copy.copy(hand)
    hand_tmp.sort()
    # 聴牌リストを上から順に確認し、あてはまるものがあれば、余りの牌を切る
    for tenpai_list in self.tenpai_lists:
      if hand_tmp[:1] == tenpai_list:
        return hand_tmp[2]
      elif hand_tmp[1:] == tenpai_list:
        return hand_tmp[0]
    # 聴牌でなければ浮き牌の優先順位を参照して切る牌を決める
    for pai in self.ukihai_list:
      if pai in hand_tmp:
        return pai
    return hand[2]

  def get_result(self,winner,turn):
    pass
最終結果
四五六太郎 : 3531勝
普通のロジック : 6447勝
引き分け : 22回

まあ、そりゃそうなるよね。

まとめ

おふざけ回でしたが、ツモ切りするだけでも25%くらいあがれることが分かりました。
次回からは機械学習に入りたいと思います。

この記事が気に入ったらサポートをしてみませんか?