├── README.md ├── LICENSE ├── .gitignore └── doudizhu_solver.py /README.md: -------------------------------------------------------------------------------- 1 | # 斗地主残局破解器 2 | 具体思路请看我的博客文章:[用代码破解斗地主残局](http://wuzhiwei.net/doudizhu_solver/) 3 | 4 | 欢迎关注公众号: 5 | ![image](https://user-images.githubusercontent.com/1621110/215088294-fc24b001-23d3-40e4-be2e-1f50a0d6d936.png) 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tim Wu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /doudizhu_solver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # Author: Tim Wu 3 | # Author: Carl King 4 | 5 | 6 | # 牌型枚举 7 | class COMB_TYPE: 8 | PASS, SINGLE, PAIR, TRIPLE, TRIPLE_ONE, TRIPLE_TWO, FOURTH_TWO_ONES, FOURTH_TWO_PAIRS, STRIGHT, BOMB = range(10) 9 | 10 | 11 | # 3-14 分别代表 3-10, J, Q, K, A 12 | # 16, 18, 19 分别代表 2, little_joker, big_joker 13 | # 将 2 与其他牌分开是为了方便计算顺子 14 | # 定义 HAND_PASS 为过牌 15 | little_joker, big_joker = 18, 19 16 | HAND_PASS = {'type':COMB_TYPE.PASS, 'main': 0, 'component':[]} 17 | 18 | 19 | # 根据当前手牌,获取此牌所有可能出的牌型 20 | # 牌型数据结构为 {牌类型,主牌,包含的牌} 21 | # 同种牌类型可以通过主牌比较大小 22 | # 为方便比较大小, 将顺子按照不同长度分为不同牌型 23 | def get_all_hands(pokers): 24 | if not pokers: 25 | return [] 26 | 27 | # 过牌 28 | combs = [HAND_PASS] 29 | 30 | # 获取每个点数的数目 31 | dic = counter(pokers) 32 | 33 | # 王炸 34 | if little_joker in pokers and big_joker in pokers: 35 | combs.append({'type':COMB_TYPE.BOMB, 'main': big_joker, 'component': [big_joker, little_joker]}) 36 | 37 | # 非顺子, 非王炸 38 | for poker in dic: 39 | if dic[poker] >= 1: 40 | # 单张 41 | combs.append({'type':COMB_TYPE.SINGLE, 'main':poker, 'component':[poker]}) 42 | 43 | if dic[poker] >= 2: 44 | # 对子 45 | combs.append({'type':COMB_TYPE.PAIR, 'main':poker, 'component':[poker, poker]}) 46 | 47 | if dic[poker] >= 3: 48 | # 三带零 49 | combs.append({'type':COMB_TYPE.TRIPLE, 'main':poker, 'component':[poker, poker, poker]}) 50 | for poker2 in dic: 51 | if ALLOW_THREE_ONE and dic[poker2] >= 1 and poker2 != poker: 52 | # 三带一 53 | combs.append({'type':COMB_TYPE.TRIPLE_ONE, 'main':poker, 'component': [poker, poker, poker, poker2]}) 54 | if ALLOW_THREE_TWO and dic[poker2] >= 2 and poker2 != poker: 55 | # 三带二 56 | combs.append({'type':COMB_TYPE.TRIPLE_TWO, 'main':poker, 'component': [poker, poker, poker, poker2, poker2]}) 57 | 58 | if dic[poker] == 4: 59 | # 炸弹 60 | combs.append({'type':COMB_TYPE.BOMB, 'main':poker, 'component': [poker, poker, poker, poker]}) 61 | if ALLOW_FOUR_TWO: 62 | pairs = [] 63 | ones = [] 64 | for poker2 in dic: 65 | if dic[poker2] == 1: 66 | ones.append(poker2) 67 | elif dic[poker2] == 2: 68 | pairs.append(poker2) 69 | 70 | # 四带二单 71 | for i in range(len(ones)): 72 | for j in range(i + 1, len(ones)): 73 | combs.append({'type':COMB_TYPE.FOURTH_TWO_ONES, 'main':poker, \ 74 | 'component':[poker, poker, poker, poker, ones[i], ones[j]]}) 75 | 76 | # 四带二对 77 | for i in range(len(pairs)): 78 | combs.append({'type':COMB_TYPE.FOURTH_TWO_ONES, 'main':poker, \ 79 | 'component': [poker, poker, poker, poker, pairs[i], pairs[i]]}) 80 | for j in range(i + 1, len(pairs)): 81 | combs.append({'type':COMB_TYPE.FOURTH_TWO_PAIRS, 'main':poker, \ 82 | 'component': [poker, poker, poker, poker, pairs[i], pairs[i], pairs[j], pairs[j]]}) 83 | 84 | # 所有顺子组合 85 | # 以 COMB_TYPE.STRIGHT * len(straight) 标志顺子牌型, 不同长度的顺子是不同的牌型 86 | for straight in create_straight(list(set(pokers)), 5): 87 | combs.append({'type':COMB_TYPE.STRIGHT * len(straight), 'main': straight[0], 'component': straight}) 88 | 89 | # 返回所有可能的出牌类型 90 | return combs 91 | 92 | 93 | 94 | # 根据列表创建顺子 95 | def create_straight(list_of_nums, min_length): 96 | a = sorted(list_of_nums) 97 | lens = len(a) 98 | for start in range(0, lens): 99 | for end in range(start, lens): 100 | if a[end] - a[start] != end - start: 101 | break 102 | elif end - start >= min_length - 1: 103 | yield list(range(a[start], a[end] + 1)) 104 | 105 | 106 | 107 | # 统计列表中每个元素的个数 108 | def counter(pokers): 109 | dic = {} 110 | for poker in pokers: 111 | dic[poker] = pokers.count(poker) 112 | return dic 113 | 114 | 115 | 116 | # comb1 先出,问后出的 comb2 是否能打过 comb1 117 | # 1. 同种牌型比较 main 值, main 值大的胜 118 | # 2. 炸弹大过其他牌型 119 | # 3. 牌型不同, 后出为负 120 | def can_beat(comb1, comb2): 121 | if not comb2 or comb2['type'] == COMB_TYPE.PASS: 122 | return False 123 | 124 | if not comb1 or comb1['type'] == COMB_TYPE.PASS: 125 | return True 126 | 127 | if comb1['type'] == comb2['type']: 128 | return comb2['main'] > comb1['main'] 129 | elif comb2['type'] == COMB_TYPE.BOMB: 130 | return True 131 | else: 132 | return False 133 | 134 | 135 | 136 | # 给定 pokers,求打出手牌 hand 后的牌 137 | # 用 component 字段标志打出的牌, 可以方便地统一处理 138 | def make_hand(pokers, hand): 139 | poker_clone = pokers[:] 140 | for poker in hand['component']: 141 | poker_clone.remove(poker) 142 | return poker_clone 143 | 144 | 145 | 146 | # 模拟每次出牌, my_pokers 为当前我的牌, enemy_pokers 为对手的牌 147 | # last_hand 为上一手对手出的牌, cache 用于缓存牌局与胜负关系 148 | def hand_out(my_pokers, enemy_pokers, last_hand = None, cache = {}): 149 | # 牌局终止的边界条件 150 | if not my_pokers: 151 | return True 152 | 153 | if not enemy_pokers: 154 | return False 155 | 156 | # 如果上一手为空, 则将上一手赋值为 HAND_PASS 157 | if last_hand is None: 158 | last_hand = HAND_PASS 159 | 160 | # 从缓存中读取数据 161 | key = str((my_pokers, enemy_pokers, last_hand['component'])) 162 | if key in cache: 163 | return cache[key] 164 | 165 | # 模拟出牌过程, 深度优先搜索, 找到赢的分支则返回 True 166 | for current_hand in get_all_hands(my_pokers): 167 | # 转换出牌权有两种情况: 168 | # 1. 当前手胜出, 则轮到对方选择出牌 169 | # 2. 当前手 PASS, 且对方之前没有 PASS, 则轮到对方出牌 170 | if can_beat(last_hand, current_hand) or \ 171 | (last_hand['type'] != COMB_TYPE.PASS and current_hand['type'] == COMB_TYPE.PASS): 172 | if not hand_out(enemy_pokers, make_hand(my_pokers, current_hand), current_hand, cache): 173 | # print(True,' :', key) 174 | cache[key] = True 175 | return True 176 | 177 | # 遍历所有情况, 均无法赢, 则返回 False 178 | # print(False, ':', key) 179 | cache[key] = False 180 | return False 181 | 182 | 183 | # todo: 184 | # 1. 用出牌列表作为 last_hand 的值, 方便调用函数 185 | 186 | 187 | if __name__ == '__main__': 188 | import time 189 | start = time.clock() 190 | 191 | # 残局1 192 | # 是否允许三带一 193 | ALLOW_THREE_ONE = True 194 | # 是否允许三带二 195 | ALLOW_THREE_TWO = False 196 | # 是否允许四带二 197 | ALLOW_FOUR_TWO = True 198 | 199 | lord = [19,18,11,11,9,9,9] 200 | farmer = [3,3,3,3,4,5,6,7,10,10,14,14,14,14] 201 | result = hand_out(farmer, lord) 202 | 203 | 204 | # 残局2 205 | # # 是否允许三带一 206 | # ALLOW_THREE_ONE = False 207 | # # 是否允许三带二 208 | # ALLOW_THREE_TWO = False 209 | # # 是否允许四带二 210 | # ALLOW_FOUR_TWO = True 211 | 212 | # lord = [14,14,11,11] 213 | # farmer = [16,13,13,13,12,12,12,10,10,9,9,8,8] 214 | # result = hand_out(farmer, lord) 215 | 216 | elapsed = (time.clock() - start) 217 | 218 | print("Result:", result) 219 | print("Elapsed:", elapsed) 220 | --------------------------------------------------------------------------------