#前回までの記事
Pythonで麻雀を作る#1
麻雀の基本的なシステムを構築し、NPC同士で対局を行えるようにした。
Pythonで麻雀を作る#2
コードの全体的な構成を整えた。また、コンソール上から打牌を選択できるモードを追加した。
#今回の目的
プレイヤー操作時に強制的に立直、和了していた問題点を修正し、立直するか、和了するかを選択できるようにする。
#変更点
Gameクラスを編集し、プレイヤー操作時には向聴数0で立直をするか選べるように、向聴数-1で和了するか、を選択できるようにした。
それに伴い、Janshiクラスに立直時に捨てられる牌を求めるメソッドを追加した。
また、立直棒のやりとりを実装した。
立直棒の本数はTakuクラスで管理を行う。
地味な変更点としてPress any key
としていた部分をPress Enter
に変更した
#実装
変更を行った3つのモジュール game.py
taku.py
janshi.py
を以下に示す。
game.py
from mahjong.constants import EAST, SOUTH, WEST, NORTH
import random
import rule
import janshi
import taku
# ゲームを進行するクラス
class Game:
WIND = {0: EAST, 1: SOUTH, 2: WEST, 3: NORTH}
KAZE = {EAST: '東', SOUTH: '南', WEST: '西', NORTH: '北'}
# hanchan=Falseで東風戦, Trueで半荘戦, play=FalseでNPC対戦の観戦モード, Trueでプレイヤーが操作するモード, aka=Trueで赤ドラあり(3枚)
def __init__(self, hanchan=False, play=False, aka=False):
a = 0
self.taku = taku.Taku(aka)
self.play = play
self.kansen = not play
self.aka = aka
self.hanchan = hanchan
self.janshi = [janshi.Janshi(self.play)]
for i in range(3):
self.janshi.append(janshi.Janshi())
self.junme = 1
self.chiicha = 0
self.oya = self.chiicha
self.ryukyoku = False
self.agari = False
# プレイヤーに名前をつける
def playername(self):
if self.play:
self.janshi[0].name = 'Player'
for i in range(1, 4):
self.janshi[i].name = 'NPC' + str(i)
else:
for i in range(0, 4):
self.janshi[i].name = 'NPC' + str(i + 1)
# その局における各家の風を求める
def kazegime(self):
for i in range(4):
self.janshi[(self.oya+i)%4].jikaze = self.WIND[i]
self.janshi[(self.oya+i)%4].jikaze_str = self.KAZE[self.janshi[(self.oya+i)%4].jikaze] + '家'
# 親決めを行う
def oyagime(self):
self.chiicha = random.randint(0, 3)
self.oya = self.chiicha
self.kazegime()
print('起家 ' + self.janshi[self.chiicha].name + '\n')
# 現在の場風を文字列で返す
def bakaze_str(self):
return self.KAZE[self.taku.bakaze]
# 局の開始時の処理
def kyokustart(self):
self.junme = 1
self.ryukyoku = False
self.agari = False
self.tobi = False
for i in range(4):
self.janshi[i].riichi = False
self.oya = (self.oya + self.taku.kyoku - 1) % 4
self.kazegime()
print(str(self.bakaze_str()) + str(self.taku.kyoku) + '局\n')
self.taku.yama = self.taku.hai.copy()
random.shuffle(self.taku.yama)
for i in range(4):
idx = (self.oya + i) % 4
self.janshi[idx].haipai(self.taku.yama)
self.janshi[idx].riipai()
# 観戦モードのときか自分の手牌のときだけ表示する
if self.kansen or self.janshi[idx].play:
print(self.janshi[idx].jikaze_str + '(' + self.janshi[idx].name + ')' + 'の手牌')
print([hai.str for hai in self.janshi[idx].tehai])
print('\n')
print('ドラ表示牌')
print([dora.str for dora in self.taku.dora_hyouji])
# 高速で流れて見づらいため入力待機
if self.play:
input('Press Enter')
# 和了時の処理
def agari_shori(self, agari_idx, tsumo):
janshi = self.janshi[agari_idx]
print(janshi.name + 'の和了')
result = rule.Rule.agari(janshi.tehai, self.taku.dora_hyouji, tsumo, janshi.riichi, janshi.jikaze, self.taku.bakaze)
print(result.yaku)
print(str(result.fu) + '符' + str(result.han) + '翻')
if janshi.jikaze == EAST:
print(str(result.cost['main']) + 'オール\n')
else:
print(str(result.cost['main']), str(result.cost['additional']) + '点\n')
janshi.get_tenbou(result.cost['main'] + result.cost['additional'] * 2)
janshi.get_tenbou(self.taku.riibou * 1000)
self.taku.riibou = 0
for i in range(4):
if i != agari_idx:
if i == self.oya:
self.janshi[i].lost_tenbou(result.cost['main'])
else:
self.janshi[i].lost_tenbou(result.cost['additional'])
# 流局時の処理
def ryuukyoku_shori(self):
self.ryukyoku = True
number_of_tenpai = 0
for janshi in self.janshi:
shantensu = rule.Rule.shantensuu(janshi.tehai)
if shantensu == 0:
print(janshi.name + '聴牌')
janshi.tenpai = True
number_of_tenpai += 1
else:
print(janshi.name + '不聴')
janshi.tenpai = False
if number_of_tenpai != 0:
for janshi in self.janshi:
if janshi.tenpai:
janshi.get_tenbou(3000/number_of_tenpai)
janshi.tenpai = False
else:
janshi.lost_tenbou(3000/(4-number_of_tenpai))
# 局が終わったときの処理
def finish_kyoku(self):
self.taku.kyoku += 1
for i in range(4):
print(self.janshi[i].name + ' ' + str(self.janshi[i].tenbou) + '点')
if(self.janshi[i].tenbou < 0):
print('トビ')
self.tobi = True
print('\n')
if self.play:
input('Press Enter')
# プレイヤーの順番のときの入力処理
def player_choice(self, player_idx, shantensu):
janshi = self.janshi[player_idx]
riichi = False
# 立直できる状況のときの処理
if shantensu == 0 and not janshi.riichi and len(self.taku.yama) > 17:
temp = input('立直しますか?(Y/N) : ')
if temp == 'Y' or temp == 'y':
riichi = True
# 立直するときの入力処理
if riichi:
riichi_idx = janshi.riichi_idx()
while 1:
try:
print(riichi_idx)
txt = '捨て牌の番号を入力してください' + str(riichi_idx) + ' : '
sutehai = int(input(txt))
except ValueError:
print('半角の整数で入力してください')
if not sutehai in riichi_idx:
print('その牌では立直できません')
else:
break
# すでに立直しているときはツモ切りを実行
elif janshi.riichi:
input('ツモ切りします(Press Enter) ')
sutehai = 13
# 通常時の処理
else:
while 1:
try:
sutehai = int(input('捨て牌の番号を入力してください(0~13) : '))
except ValueError:
print('0から13の整数を半角で入力してください')
if sutehai < 0 or 13 < sutehai:
print('0から13の整数を入力してください')
else:
break
# このメソッド内でjanshi.riichiをTrueにしてしまうと立直時にツモ切りしてしまうため
# riichiを戻し打牌処理を行ってからjanshi.riichiをTrueにする
return sutehai, riichi
# 1巡における処理
def ichijun(self):
print('\n' + str(self.junme) + '巡目')
for i in range(4):
# 東→南→西→北の巡で処理を行うための変数
idx = (self.oya + i) % 4
janshi = self.janshi[idx]
print(janshi.jikaze_str + '(' + janshi.name + ')' + 'の手番')
if self.kansen:
print([hai.str for hai in janshi.tehai], end=' ')
elif janshi.play:
print([str(j) + ': ' + janshi.tehai[j].str for j in range(len(janshi.tehai))], end=' ')
tsumohai = janshi.tsumo(self.taku.yama)
if self.kansen:
print(tsumohai.str)
elif janshi.play:
print('13: ' + tsumohai.str)
shantensu = rule.Rule.shantensuu(janshi.tehai)
# 副露, ロン, 振聴は未実装のため向聴数-1でNPCは強制的に和了
if shantensu == -1:
agari = False
if janshi.play:
temp = input('和了しますか?(Y/N) : ')
if temp =='Y' or temp == 'y':
agari = True
else:
agari =True
if agari:
self.agari_shori(idx, tsumo=True)
self.agari = True
self.finish_kyoku()
break
if self.kansen or janshi.play:
if shantensu == 0:
print('聴牌')
else:
print(str(shantensu) + '向聴')
riichi = False
# プレイヤーの行動選択処理
if janshi.play:
sutehai, riichi = self.player_choice(idx, shantensu)
# NPCの行動選択処理
else:
# 聴牌し、山が残り17牌以上なら強制的に立直する
if shantensu == 0 and not janshi.riichi and len(self.taku.yama) > 17:
print(janshi.jikaze_str + 'のリーチ')
riichi = True
sutehai = 13
# 打牌処理と打牌の表示
print('打 ' + janshi.dahai(sutehai).str)
# リーチ時の処理
if riichi:
janshi.lost_tenbou(1000)
self.taku.riibou += 1
janshi.riichi = True
# 山が残り14牌になったら流局処理
if len(self.taku.yama) == 14:
print('流局\n')
self.ryuukyoku_shori()
self.ryukyoku = True
self.finish_kyoku()
break
janshi.riipai()
print('\n')
self.junme += 1
# 全体のゲーム進行を行う
def game(self):
self.playername()
self.oyagime()
if self.hanchan:
kyokusuu = 8
else:
kyokusuu = 4
for i in range(kyokusuu):
self.kyokustart()
while 1:
self.ichijun()
if self.agari == True or self.ryukyoku == True:
break
if self.tobi == True:
break
# 南入の処理
if i == 3:
self.taku.bakaze = SOUTH
self.taku.kyoku = 1
janshi.py
import rule
from mahjong.constants import EAST, SOUTH, WEST, NORTH
# 雀士クラス 雀士の手牌、河、自風、ツモの管理
# 向聴数計算、点数計算、打牌の選択を行う
class Janshi:
# play=Trueとした場合, その雀士は自動で牌を切らずに入力された牌を切る
def __init__(self, play=False):
self.name = ''
self.jikaze = EAST
self.jikaze_str = '東家'
self.tehai = []
self.kawa = []
self.tenbou = 25000
self.riichi = False
self.tenpai =False
self.play = play
# 配牌をとる
def haipai(self, yama):
self.tehai = yama[0:13]
del yama[0:13]
# ツモを行う
def tsumo(self, yama):
hai = yama[0]
del yama[0]
self.tehai.append(hai)
return hai
# リーパイを行う
def riipai(self):
self.tehai = sorted(self.tehai, key = lambda t: t.number0to135)
# 点棒を受け取る
def get_tenbou(self, tensuu):
self.tenbou += int(tensuu)
# 点棒を支払う
def lost_tenbou(self, tensuu):
self.tenbou -= int(tensuu)
# 打牌を行う
def dahai(self, sutehai=13):
# 立直状態での処理
if self.riichi:
hai = self.tehai[13]
del self.tehai[13]
return hai
# プレイヤーが操作する場合の処理
if self.play:
hai = self.tehai[sutehai]
del self.tehai[sutehai]
self.kawa.append(hai)
# NPCの場合の処理
else:
shantenvalue = [i for i in range(len(self.tehai))]
for i in range(len(self.tehai)):
shantenvalue[i] = rule.Rule.shantensuu(self.tehai[:i] + self.tehai[i + 1:])
hai = self.tehai[shantenvalue.index(min(shantenvalue))]
del self.tehai[shantenvalue.index(min(shantenvalue))]
self.kawa.append(hai)
return hai
# 手牌中の立直時に打牌できる牌(打牌した際に向聴数が0となる牌)のインデックスを返す
def riichi_idx(self):
riichi_idx = []
for i in range(len(self.tehai)):
shantensuu = rule.Rule.shantensuu(self.tehai[:i] + self.tehai[i + 1:])
if shantensuu == 0:
riichi_idx.append(i)
return riichi_idx
taku.py
import hai
from mahjong.constants import EAST, SOUTH, WEST, NORTH
# 卓クラス 山, 局数, 場風, ドラ表示牌の管理
class Taku:
def __init__(self, aka=False):
self.bakaze = EAST
self.kyoku = 1
self.hai = []
self.aka = aka
self.hai = [hai.Hai(i, self.aka) for i in range(136)]
self.yama = self.hai.copy()
self.kancounter = 0
self.dora_hyouji = []
self.dora_hyouji.append(self.yama[-(6 + self.kancounter * 2)])
# 場に出たリー棒をカウントする
self.riibou = 0
# カンした際に実行 カンドラが増える(カン自体は未実装)
def kan(self):
self.kancounter += 1
self.dora_hyouji.append(self.yama[-(6 + self.kancounter * 2)])
#実行結果
南家(Player)の手番
['0: 1筒', '1: 3筒', '2: 6筒', '3: 6筒', '4: 6筒', '5: 1索', '6: 2索', '7: 2索', '8: 北', '9: 北', '10: 発', '11: 発', '12: 発'] 13: 3索
聴牌
立直しますか?(Y/N) : y
[6, 7]
捨て牌の番号を入力してください[6, 7] : 6
打 2索
西家(NPC1)の手番
NPC1の和了
[Menzen Tsumo, Riichi, Chiitoitsu]
25符4翻
3200 1600点
Player 22400点
NPC1 32400点
NPC2 23400点
NPC3 21800点
このようにしてプレイヤーが立直できる状態になると選択肢が出てYを入力することで立直ができるようになった。
また、少しわかりにくいかもしれないが、Playerは立直棒分の点を失っている(この局は東1局で全員持ち点25000点)。