見出し画像

1-1 3枚麻雀でツモ切りAIとガチバトル!!

さて、ふざけたタイトルになってしまいましたが、今回からプログラミングに入っていきたいと思います。

今回は機械学習の必要ない部分のコードを書いてみましたので見てってください。


3枚麻雀とは??

とりあえずしばらくの間は、勉強として3枚麻雀で機械学習の勉強を進めていこうと思っています。

今回扱う3枚麻雀はこんな感じのものを想定しています

・使う牌は萬子の1~9と發・中
手牌は2枚、1枚引いてきて3枚(普通の麻雀はそれぞれ13,14)
順子(1,2,3のようなやつ)か、刻子(1,1,1のようなやつ)が1組完成すれば勝ち
・ツモ(引いてきた牌で3枚組が完成する)だけでなく、ロン(相手が今切った1枚と、手の2枚で3枚組が完成する)でもアガリ
・点数はなし、どんな手でも1点
・鳴きなし、見逃しなし、フリテンなし

と、まあ、シンプルなゲームです。

なぜ3枚麻雀で始めようと思ったかというと、

・シンプルなので最善手を探しやすい
→AIが期待通り強くなってくれそう?

・相手の手牌を絞り込めそう
→AIに相手の手を予想させるのもおもしろいかも?

・PCのスペック的にちゃんとした麻雀ができるか心配

・pythonやったことないのでちゃんとした麻雀ができるか心配

・複雑なことやってると疲れてやめそう

...主に下3つです。


実装にあたって

実装にあたっては、以下のサイトを参考にさせていただきました。

盤面と、プレイヤーの情報を、管理者が間で管理をすることで、Playerが直接盤面にアクセスできないようにする、仲介する、っていうことみたいですね。ふむふむ。

というわけで、考えた設計はざっくりこんな感じです。

プレゼンテーション1

まあ、ざっくりと、必要なものがあったらあとで足しましょう。

とりあえず、定数の定義をこんな感じで決めておきます。

PAI_STR = ["None","一","二","三","四","五","六","七","八","九","None","①","②","③","④","⑤","⑥","⑦","⑧","⑨","None","1","2","3","4","5","6","7","8","9","None","東","南","西","北","白","發","中"]

# 使用する牌
PAI = [1,2,3,4,5,6,7,8,9,36,37]
# 1種類の牌の枚数
SAME_PAI_NUM = 4
# 人数
PLAYER_NUM = 2
# 手牌の枚数
HAND_NUM = 2

麻雀牌は、1~9,11~19,21~29に数牌を入れるのがいいみたいなので、それに従いました。


pg1 麻雀卓

卓は、山牌、手牌、捨牌を管理します。手牌に関連して、アガリかどうかを判定する機能も麻雀卓に持たせました。

ツモる時にツモ判定、切ったときにロン判定をするようにしています。

# 麻雀卓
class MjTable:
	def __init__(self):

		# 山
		deck = []
		for i in PAI:
			for _ in range(SAME_PAI_NUM):
				deck.append(i)
		random.shuffle(deck)
		self.deck = deck
		self.deck_pointer = 0

		# 手牌
		self.hand = [[] for _ in range(PLAYER_NUM)]
		for i in range(PLAYER_NUM):
			for _ in range(HAND_NUM):
				self.hand[i].append(self.deck[self.deck_pointer])
				self.deck_pointer += 1
		
		# 捨牌
		self.dispai = [[] for _ in range(PLAYER_NUM)]

	def tsumo(self,player):
		'''
		戻り値はアガリかどうかのブール値
		'''
		self.hand[player].append(self.deck[self.deck_pointer])
		self.deck_pointer += 1
		return self.__check_tsumo(player)

	def cut(self,player,pai):
		'''
		戻り値はロンできるプレイヤーのリスト
		'''
		if pai in self.hand[player]:
			self.hand[player].remove(pai)
			self.dispai[player].append(pai)
		else:
			raise ValueError()
		return self.__check_ron(player,pai)

	def display_table(self,player=-1):
		print("卓の状況")
		if player == -1:
			print("山 : ",[PAI_STR[pai] for pai in self.deck[self.deck_pointer:len(self.deck)]])
		for i in range(PLAYER_NUM):
			print("プレイヤー" + str(i+1) + "捨牌 : ",[PAI_STR[pai] for pai in self.dispai[i]])
		for i in range(PLAYER_NUM):
			print("プレイヤー" + str(i+1) + "手牌 : ",[PAI_STR[pai] for pai in self.hand[i]])

	def __check_tsumo(self,player):
		if self.__check_agari(player):
			return True
		return False
	
	def __check_ron(self,player,pai):
		ron_players = []
		for i in range(PLAYER_NUM):
			if i == player:
				continue
			self.hand[i].append(pai)
			if self.__check_agari(i):
				ron_players.append(i)
			del self.hand[i][-1]
		return ron_players

	def __check_agari(self,player):
		# とりあえず3枚麻雀用
		hand = copy.copy(self.hand[player])
		hand.sort()
		least = hand[0]
		if hand[1] == least and hand[2] == least:
			# 3枚同じ牌
			return True
		elif hand[1] == least + 1 and hand[2] == least + 2:
			# 3枚が連番
			return True
		else:
			return False

	def check_draw(self):
		return len(PAI) * SAME_PAI_NUM == self.deck_pointer


pg2 管理役

Observerさんには、ゲームの進行を管理してもらいます。もうちょっと機能があったほうがしないでもないですが、まあ今のところはこんなんでいいでしょう。

プレイヤーには、actメソッドで行動をさせます。自分の手牌と捨て牌の情報をもとに、各人のもつ知能で切る牌を選択してもらいます。
それ以外に、各プレイヤーにはget_pleyer_numメソッドと、get_resultメソッドを持ってもらいます。
前者はプレイヤー番号をあらかじめ教えておくことで、捨て牌をボンと渡されたときにどれが自分のか分かるようにしておくためのもので、後者は勝敗が決まった時にそれを通知するためのメソッドになっています。
※名前を返すだけのget_nameメソッドを追記しました。

class Observer:
	def __init__(self,players,max_game=1,is_disp=False):
		self.players = players
		[self.players[i].get_player_number(i) for i in range(len(self.players))]
		self.max_game = max_game
		self.is_disp = is_disp
		self.win_counter = [0 for _ in range(len(players))]
		self.draw_counter = 0

	def progress(self):
		for _ in range(self.max_game):
			self.__progress_game()
		print("最終結果")
		for i in range(len(self.players)):
			print(self.players[i].get_name() + " : " + str(self.win_counter[i]) + "勝")
		print("引き分け : " + str(self.draw_counter) + "回")

	def __progress_game(self):
		table = MjTable()
		winner = []
		turn = random.randrange(0,PLAYER_NUM)
		while table.check_draw() == False:
			if table.tsumo(turn):
				winner = [turn]
				break
			if self.is_disp:table.display_table()
			act = self.players[turn].act(table.hand[turn],table.dispai)
			ron_players = table.cut(turn,act)
			if 0 < len(ron_players):
				winner = ron_players
				break
			if self.is_disp:table.display_table()
			turn = (turn+1)%len(self.players)
		[player.get_result(winner,turn) for player in self.players]
		if 0 < len(winner):
			# とりあえずダブロンなし
			self.win_counter[winner[0]] += 1
		else:
			self.draw_counter += 1


pg3 はじめてのCOM

では、このゲームに参加するプレイヤーを作っていきます。

ひとまず自分で戦って試してみたいので、動くだけのやつです。

class PlayerTsumogiri:
	def __init__(self):
		pass

	def get_name(self):
		return "ツモ切りマシーン"

	def get_player_number(self,number):
		pass

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

	def get_result(self,winner,turn):
		pass

シンプルイズベストって感じですね

actメソッドで選択しているhand[2]は、手牌の3枚目、つまり引いてきた牌ということになります。

というわけで、名を「ツモ切りマシーン」、今爆誕いたしました。

手始めに、ツモ切りマシーン同士で10000戦ほど戦わせてみました。

obs = Observer([PlayerTsumogiri(),PlayerTsumogiri()],10000,False)
obs.progress()
最終結果
ツモ切りマシーン : 2737勝
ツモ切りマシーン : 2745勝
引き分け : 4518回

いい勝負でした。ダイジェストで見たいですね。やっていることは引いた牌を捨てることだけなんですが。

ツモ切りしてるだけなのに半分以上勝負がついてるのがおもしろいです。つまりは、配牌の時点で2回に1回はどちらかが聴牌しているということになります。


pg4 自分を作る

ツモ切りマシーンと戦うユーザーを定義します。

1~3の数字の入力で切る牌を選びます。あがりが出るとスンっと終わってしまうのがあじけなかったので、勝ち負けのメッセージをちょいとつけました。

class PlayerHuman:
	def __init__(self):
		self.player_number = -1

	def get_name(self):
		return "人間"

	def get_player_number(self,number):
		self.player_number = number

	def act(self,hand,discard):
		print("手牌 : ",[PAI_STR[pai] for pai in hand])
		print("自捨牌 : ",[PAI_STR[pai] for pai in discard[0]])
		print("敵捨牌 : ",[PAI_STR[pai] for pai in discard[1]])
		while True:
			try:
				num = input("切る牌を1~3で選んでください : ")
				if 0 < int(num) and int(num) < 4:
					print("")
					return hand[int(num)-1]
				print("1から3で選んでください。")
			except Exception as _:
				print("入力が不正です")

	def get_result(self,winner,turn):
		if len(winner) == 0:
			print("流局です")
		elif winner[0] == self.player_number:
			if turn == self.player_number:
				print("ツモです!")
			else:
				print("ロンです!")
		else:
			if turn == self.player_number:
				print("放銃しました...")
			else:
				print("アガられてしまいました...")

では、早速戦ってみましょう。

手牌 :  ['五', '四', '中']
自捨牌 :  []
敵捨牌 :  []
切る牌を13で選んでください :3

実行中はこんな感じ。嫌いじゃない。

ロンです!
最終結果
ツモ切りマシーン : 0勝
人間 : 1勝
引き分け : 0回

なんとか人間の尊厳を守ることができました。よかったです。


まとめ

というわけで、ゲームの環境は整えることができました。今回コンピューター同士戦わせてみて結構面白かったので、機械学習に入る前にもう少しコンピューターを作って遊びたいと思っています。

ご意見ご感想などありましたら、コメントまでお寄せください。

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