Pythonで麻雀を打ってみた(一人麻雀編)


概要

Pythonで麻雀が打てるプログラムを自作しています。

今回は一人麻雀、ただ牌をツモって切るだけのプログラムです

徐々に機能を追加して、麻雀AIを作るのが最終目標です。

ルール

  • 1~9までの数字が書かれた、萬子(マンズ)・筒子(ピンズ)・索子(ソーズ)と "東" "南" "西" "北" "白" "発" "中"と書かれた"字牌"の計34種類の牌を4セット、合計136枚使用します
  • プレイヤーは13枚の牌を手持ちにし(手牌)、残りの牌(山)から一牌引いては手牌1牌と入れ替えます。 手牌が特定の条件を満たすとゲームクリア(アガリ)するゲームです。
  • 山のうち14牌は王牌とよび、プレイヤーは中身を確認することもそこから牌を引くこともできません。王牌次第では上がれないこともあります。

実装

ソースコードはこんな感じです

#!/usr/bin/env python
# 麻雀プログラム
# 現状ツモって切るだけの一人麻雀しかできない
# 未実装:アガリ判定,手役判定,鳴き,河,
from enum import Enum
import sys
import random
import datetime


class Pai(Enum):  # 牌クラス 牌の種類を定義
    # 萬子
    m1 = '一萬'
    m2 = '二萬'
    m3 = '三萬'
    m4 = '四萬'
    m5 = '伍萬'
    m6 = '六萬'
    m7 = '七萬'
    m8 = '八萬'
    m9 = '九萬'
    # 筒子
    p1 = '一筒'
    p2 = '二筒'
    p3 = '三筒'
    p4 = '四筒'
    p5 = '五筒'
    p6 = '六筒'
    p7 = '七筒'
    p8 = '八筒'
    p9 = '九筒'
    # 索子
    s1 = '一索'
    s2 = '二索'
    s3 = '三索'
    s4 = '四索'
    s5 = '五索'
    s6 = '六索'
    s7 = '七索'
    s8 = '八索'
    s9 = '九索'
    # 字牌
    z1 = '東'
    z2 = '南'
    z3 = '西'
    z4 = '北'
    z5 = '白'
    z6 = '発'
    z7 = '中'

    def __str__(self):  # print関数での表示方法を定義
        return self.value


class Yama:  # 山クラス 山、王牌などの管理

    def __init__(self):
        self.yama = [*Pai] * 4  # 山の初期化
        random.shuffle(self.yama)

        self.wanpai = self.yama[0:14]  # 王牌の初期化
        del self.yama[0:14]

    def tsumo(self):  # 山の一番最後の牌を返す
        return self.yama.pop()

    def haipai(self):  # 13牌をプレイヤーに渡す
        haipai = self.yama[0:13]
        del self.yama[0:13]
        return haipai


class Player:  # プレイヤークラス 手牌の管理

    def __init__(self):
        self.tehai = []

    def haipai(self,pai):  # ゲーム開始時に13牌を山からとる
        self.tehai = pai
        self.tehai.sort(key = lambda x: str(x.name))
        print('配牌\n',*self.tehai,'\n')

    def tsumo(self,pai):  # 山から1牌とって手牌から一枚捨てる
        print(*self.tehai, 'ツモ', pai)
        self.tehai.append(pai)
        sutehai = self.tehai.pop(int(input()))
        # キーボード入力で切る牌を指定
        self.tehai.sort(key = lambda x: str(x.name))
        print('打',sutehai,'\n')
        return sutehai


random.seed(datetime.datetime.now())
yama1 = Yama()  # 山の生成
player1 = Player()  # プレイヤーの生成
player1.haipai(yama1.haipai())
while(len(yama1.yama)!=0):  # 山の牌数が0になるまで
   player1.tsumo(yama1.tsumo())  # 一枚ツモって切る

(編集履歴)

  • 2021/01/01 Python公式のコーディング規約 PEP8の存在を知り、読みやすさのために修正することにしました。エンコーディング設定の削除・importの書き方 等

  • 牌山を生成する箇所について、はじめはリスト包括表記で書いていましたが、*(アスタリスク)を用いることでもっとスマートに書けました。

self.yama = [pai for pai in Pai] * 4  # リスト包括表記

self.yama = [*Pai] * 4  # アスタリスクを用いた表記
# [Pai.m1,Pai.m2,...Pai.z7] * 4 と同じ意味

  • トップモジュールで手牌を出力するときにわざわざpai.valueと書いていましたが、特殊メソッド__str__をオーバーロードすることでprint文での出力を定義できました。
class Pai:
    def __str__(self):
        return self.value

tehai = [Pai.m1,Pai.p2,Pai.s3]
print(*tehai)  # print(tehai[0],tehai[1],tehai[2])と同じ意味
# 実行結果
# 一萬 二筒 三索

実行結果

コマンドプロンプト上で実行してみました。
手牌の表示、ソート、牌の入れ替え等が正常に行えてると思います。

課題

  • 文字で手牌を表示しているのでやや見にくい
  • アガリ判定が未実装

この2点を次の記事にします。

終わりに

pythonの練習のためにやってみました。
自分の書いたコードを公開するのは初めてなので突っ込みどころがあれば是非コメントで指摘していただけると嬉しいです。

参考記事

pythonの書き方については
https://note.nkmk.me/
こちらのサイトを参考にしました。