对于斗地主残局,用python实现solver

 前面的话

前段时间呢,我发了一个朋友圈,是有关一个斗地主残局

刚刚看到有个人实现了残局的solver,缺点是在手牌多的时候时间复杂度还是挺高的。

minimax

     代码的核心思想是minimax。minimax可以拆解为两部分,mini和max,分别是最小和最大的意思。

     直观的理解是什么呢?就有点像A、B两个人下棋。A现在可以在N个点走棋,假设A在某个点走棋了,使得A的这一步的盘面评估分数最高;但是轮到B下的时候,就一定会朝着让A最不利的方向走,使得A的下一步必然按照B设定的轨迹来,而没法达到A在第一步时估算到这一步的最高盘面评分。

     在牌局中是一样的,如果农民的一手牌,让地主无论如何应对都不能赢的话,那么可以说农民有必胜策略;否则,农民必输。

核心逻辑

      我们可以用一个函数hand_out来模拟一个人的出牌过程。在现实生活中,一个人想要出牌的话,必然需要知道自己手上的所有牌:me_pokers,也需要知道上一手的出的牌:last_hand。如果我们要用这个函数来模拟两个人的出牌,则还需要知道对手当前的所有牌:enemy_pokers

      这个函数的返回值,是轮到我me_pokers出牌时,是否能够必赢牌。如果能赢则返回真,否则返回假。

def hand_out(me_pokers, enemy_pokers, last_hand)

      假设轮到我出牌时,如果我手上的牌都出完了,那么我将立刻知道我赢了;反之如果对手的牌都出完了,而我没有,则我失败了。

if not me_pokers:
    return True
if not enemy_pokers:
    return False

      因为现在轮到我出牌,所以我首先需要知道我现在能出的所有手牌组合。注意:这个组合中,包括 过牌(即不出牌)的策略。

all_hands = get_all_hands(me_pokers)

     

现在我们要对所有可能的手牌组合进行遍历。

首先我需要知道,上一手对方出的牌是什么。

  • 如果对方上一手选择过牌,或者没有上一手牌,那么我这一轮必须不能过牌,但是我可以出任意的牌
  • 如果对手上一手出了牌,则我必须要出一个比它更大的牌或者选择这一轮直接过牌(不出牌)

      关键点来了,在出完我的牌或选择过牌后,我们需要用一个递归调用来模拟对手下一步的行为。如果对手的下一次出牌不能获胜的话,则我这一次的出牌必胜;否则,对于我的每一个出牌选择,对手都能获胜的话,则我必败。

def hand_out(me_pokers, enemy_pokers, last_hand, cache):
    if not me_pokers:
        # 我全部过牌,直接获胜
        return True
    if not enemy_pokers:
        # 对手全部过牌,我失败
        return False
    # 获取我当前可以出的所有手牌组合,包括过牌
    all_hands = get_all_hands(me_pokers)
    # 遍历我的所有出牌组合,进行模拟出牌
    for hand in all_hands:
        # 如果上一轮对手出了牌,则这一轮我必须要出比对手更大的牌 或者 对手上一轮选择过牌,那么我只需出任意牌,但是不能过牌
        if (last_hand and can_comb2_beat_comb1(last_hand, hand)) or (not last_hand and hand['type'] != COMB_TYPE.PASS):
            # 模拟对手出牌,如果对手不能取胜,则我必胜
            if not hand_out(enemy_pokers, make_hand(me_pokers, hand), hand, cache):
                return True
        # 如果上一轮对手出了牌,但我这一轮选择过牌
        elif last_hand and hand['type'] == COMB_TYPE.PASS:
            # 模拟对手出牌,如果对手不能取胜,则我必胜
            if not hand_out(enemy_pokers, me_pokers, None, cache):
                return True
    # 如果之前的所有出牌组合均不能必胜,则我必败
    return False

构建

     以上核心逻辑理清楚后,构建破解器将变得十分简单。

     首先,我们要用数字来表示牌的大小,这里我们用3表示3,11来表示J,12表示Q,依次类推…… 

     其次,我们需要求出一个手牌的所有出牌组合,这里需要get_all_hands函数,具体实现比较繁琐但是很简单,就不在此赘述。

     然后,我们还需要一个牌力判断函数can_comb2_beat_comb1(comb1, comb2),这个函数用于比较两组手牌的牌力,看是否comb2可以击败comb1。唯一需要注意的一点,在斗地主的规则中,除了炸弹外,其他所有牌力均等,只有牌型一样时才能去比较。

     最后,我们需要一个模拟出牌函数make_hand(pokers, hand),用于求出在手牌为pokers的情况下打出一手牌hand后,剩下的手牌,实现也非常简单,只需简单的移除掉那些打出的牌即可。

最后


给出GitHub上有的源码

# -*- coding: UTF-8 -*-

# 牌型枚举
class COMB_TYPE:
	PASS, SINGLE, PAIR, TRIPLE, TRIPLE_ONE, TRIPLE_TWO, FOURTH_TWO_ONES, FOURTH_TWO_PAIRS, STRIGHT, BOMB, KING_PAIR = range(11)

# 根据牌,获取此副牌所有可能的牌型
# 牌型数据结构为牌类型,主牌,副牌
def get_all_hands(pokers):
	if not pokers:
		return []

	combs = [{'type':COMB_TYPE.PASS}]
	dic = {}
	for poker in pokers:
		dic[poker] = dic.get(poker, 0) + 1

	for poker in dic:
		if dic[poker] >= 1:
			# 单张
			combs.append({'type':COMB_TYPE.SINGLE, 'main':poker})
		if dic[poker] >= 2:
			# 对子
			combs.append({'type':COMB_TYPE.PAIR, 'main':poker})
		if dic[poker] >= 3:
			# 三带零
			combs.append({'type':COMB_TYPE.TRIPLE, 'main':poker})
			for poker2 in dic:
				if ALLOW_THREE_ONE and dic[poker2] >= 1 and poker2 != poker:
					# 三带一
					combs.append({'type':COMB_TYPE.TRIPLE_ONE, 'main':poker, 'sub':poker2})
				if ALLOW_THREE_TWO and dic[poker2] >= 2 and poker2 != poker:
					# 三带二
					combs.append({'type':COMB_TYPE.TRIPLE_TWO, 'main':poker, 'sub':poker2})
						
		if dic[poker] == 4:
			# 炸弹
			combs.append({'type':COMB_TYPE.BOMB, 'main':poker})
			if ALLOW_FOUR_TWO:
				pairs = []
				ones = []
				for poker2 in dic:
					if dic[poker2] == 1:
						ones.append(poker2)
					elif dic[poker2] == 2:
						pairs.append(poker2)
				for i in xrange(len(ones)):
					for j in xrange(i + 1, len(ones)):
						combs.append({'type':COMB_TYPE.FOURTH_TWO_ONES, 'main':poker, 'sub1':ones[i], 'sub2':ones[j]})
				for i in xrange(len(pairs)):
					combs.append({'type':COMB_TYPE.FOURTH_TWO_ONES, 'main':poker, 'sub1':pairs[i], 'sub2':pairs[i]})
					for j in xrange(i + 1, len(pairs)):
						combs.append({'type':COMB_TYPE.FOURTH_TWO_PAIRS, 'main':poker, 'sub1':pairs[i], 'sub2':pairs[j]})

	if 16 in pokers and 17 in pokers:
		# 王炸
		combs.append({'type':COMB_TYPE.KING_PAIR})

	# 所有顺子组合
	distincted_sorted_pokers = sorted(list(set(pokers)))
	lastPoker = distincted_sorted_pokers[0]
	sequence_num = 1
	i = 1
	while i < len(distincted_sorted_pokers):
		# 只有3-A能连成顺子
		if distincted_sorted_pokers[i] <= 14 and distincted_sorted_pokers[i] - lastPoker == 1:
			sequence_num += 1
			if sequence_num >= 5:
				j = 0
				while sequence_num - j >= 5:
					# 顺子
					combs.append({'type':COMB_TYPE.STRIGHT, 'main':sequence_num - j, 'sub':distincted_sorted_pokers[i]})
					j += 1
		else:
			sequence_num = 1
		lastPoker = distincted_sorted_pokers[i]
		i += 1

	return combs

# comb1先出,问后出的comb2是否能打过comb1
def can_comb2_beat_comb1(comb1, comb2):
	if comb2['type'] == COMB_TYPE.PASS:
		return False

	if not comb1 or comb1['type'] == COMB_TYPE.PASS:
		return True

	if comb1['type'] == comb2['type']:
		if comb1['type'] == COMB_TYPE.STRIGHT:
			if comb1['main'] != comb2['main']:
				return False
			else:
				return comb2['sub'] > comb1['sub']
		else:
			if comb1['main'] == comb2['main']:
				return comb2['sub'] > comb1['sub']
			else:
				return comb2['main'] > comb1['main']
	elif comb2['type'] == COMB_TYPE.BOMB or comb2['type'] == COMB_TYPE.KING_PAIR:
		return comb2['type'] > comb1['type']

	return False

# 给定牌pokers,求打出手牌hand后的牌
def make_hand(pokers, hand):
	poker_clone = pokers[:]
	if hand['type'] == COMB_TYPE.SINGLE:
		poker_clone.remove(hand['main'])
	elif hand['type'] == COMB_TYPE.PAIR:
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
	elif hand['type'] == COMB_TYPE.TRIPLE:
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
	elif hand['type'] == COMB_TYPE.TRIPLE_ONE:
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['sub'])
	elif hand['type'] == COMB_TYPE.TRIPLE_TWO:
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['sub'])
		poker_clone.remove(hand['sub'])
	elif hand['type'] == COMB_TYPE.FOURTH_TWO_ONES:
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['sub1'])
		poker_clone.remove(hand['sub2'])
	elif hand['type'] == COMB_TYPE.FOURTH_TWO_PAIRS:
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['sub1'])
		poker_clone.remove(hand['sub1'])
		poker_clone.remove(hand['sub2'])
		poker_clone.remove(hand['sub2'])
	elif hand['type'] == COMB_TYPE.STRIGHT:
		for i in xrange(hand['sub'], hand['sub'] - hand['main'], -1):
			poker_clone.remove(i)
	elif hand['type'] == COMB_TYPE.BOMB:
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
		poker_clone.remove(hand['main'])
	elif hand['type'] == COMB_TYPE.KING_PAIR:
		poker_clone.remove(16)
		poker_clone.remove(17)
	return poker_clone

# 模拟每次出牌,me_pokers为当前我的牌,enemy_pokers为对手的牌
# last_hand为上一手的手牌
def hand_out(me_pokers, enemy_pokers, last_hand, cache):
	if not me_pokers:
		return True
	if not enemy_pokers:
		return False
	key = str(me_pokers) + str(enemy_pokers) + str(last_hand)
	if key in cache:
		return cache[key]
	all_hands = get_all_hands(me_pokers)
	for hand in all_hands:
		if (last_hand and can_comb2_beat_comb1(last_hand, hand)) or (not last_hand and hand['type'] != COMB_TYPE.PASS):
			if not hand_out(enemy_pokers, make_hand(me_pokers, hand), hand, cache):
				cache[key] = True
				return True
		elif last_hand and hand['type'] == COMB_TYPE.PASS:
			if not hand_out(enemy_pokers, me_pokers, None, cache):
				cache[key] = True
				return True
	cache[key] = False
	return False

# 残局1
# 是否允许三带一
ALLOW_THREE_ONE = True
# 是否允许三带二
ALLOW_THREE_TWO = False
# 是否允许四带二
ALLOW_FOUR_TWO = True

lord = [17,16,11,11,9,9,9]
farmer = [3,3,3,3,4,5,6,7,10,10,14,14,14,14]
print hand_out(farmer, lord, None, {})

# 残局2
# # 是否允许三带一
# ALLOW_THREE_ONE = False
# # 是否允许三带二
# ALLOW_THREE_TWO = False
# # 是否允许四带二
# ALLOW_FOUR_TWO = True
# 
# lord = [14,14,11,11]
# farmer = [16,13,13,13,12,12,12,10,10,9,9,8,8]
# print hand_out(farmer, lord, None, {})





  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值