├── .gitignore ├── README.md ├── log.txt ├── main_ddz.py ├── myclass.py └── myutil.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | Model* 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 斗地主 2 | deecamp斗地主 3 | 4 | ## master分支 5 | 提供了可以结合AI的程序引擎,在next_moves中提供按照规则的出牌所有可能性,需要自己实现从next_moves中选择所出的牌(myutil中的choose方法),默认random 6 | 7 | ## web分支 8 | 1.页面展示,提供可视化调试方法 9 | 10 | 2.可以选择跟人对战 11 | ### 使用方法 12 | #### 1.启动server.py 13 | #### 2.访问http://127.0.0.1:5000/ddz 14 | 15 | ## rl_pdqn分支 16 | 模仿OpenAI,提供了可以结合RL的程序引擎,可以选择对手为random或陈潇规则(cxgz)或自身(self),但是训练时只能训练一个且为player 1。该分支rl模型为prioritized_dqn,具体模型参考https://github.com/MorvanZhou/Reinforcement-learning-with-tensorflow 。 17 | 18 | 目前胜率 vs random(90%), cxgz(44%) 19 | 20 | ## multi-rl分支 21 | 模仿OpenAI,提供了可以结合RL的程序引擎,可以同时训练多个rl player 22 | 23 | ## mcts分支 24 | mcts暴力解决(TODO:由于deepcopy牌局的回复速度比较慢,1000/6s) 25 | 26 | ## contributor 27 | Deecamp第五组 28 | -------------------------------------------------------------------------------- /log.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thuxugang/doudizhu/3eaf83079d8c4cf1eb19b235751d32c6dd48ce88/log.txt -------------------------------------------------------------------------------- /main_ddz.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Jul 13 21:55:58 2017 4 | 5 | @author: XuGang 6 | """ 7 | from myclass import Cards, Player, PlayRecords, WebShow 8 | from myutil import game_init 9 | import jsonpickle 10 | import time 11 | import copy 12 | class Game(object): 13 | 14 | def __init__(self, model): 15 | #初始化一副扑克牌类 16 | self.cards = Cards() 17 | 18 | #play相关参数 19 | self.end = False 20 | self.last_move_type = self.last_move = "start" 21 | self.playround = 1 22 | self.i = 0 23 | self.yaobuqis = [] 24 | 25 | #choose模型 26 | self.model = model 27 | 28 | #发牌 29 | def game_start(self): 30 | 31 | #初始化players 32 | self.players = [] 33 | for i in range(1,4): 34 | self.players.append(Player(i)) 35 | 36 | #初始化扑克牌记录类 37 | self.playrecords = PlayRecords() 38 | 39 | #发牌 40 | game_init(self.players, self.playrecords, self.cards) 41 | 42 | 43 | #返回扑克牌记录类 44 | def get_record(self): 45 | web_show = WebShow(self.playrecords) 46 | return jsonpickle.encode(web_show, unpicklable=False) 47 | 48 | #游戏进行 49 | def next_move(self): 50 | 51 | self.last_move_type, self.last_move, self.end, self.yaobuqi = self.players[self.i].go(self.last_move_type, self.last_move, self.playrecords, self.model) 52 | if self.yaobuqi: 53 | self.yaobuqis.append(self.i) 54 | else: 55 | self.yaobuqis = [] 56 | #都要不起 57 | if len(self.yaobuqis) == 2: 58 | self.yaobuqis = [] 59 | self.last_move_type = self.last_move = "start" 60 | if self.end: 61 | self.playrecords.winner = self.i+1 62 | self.i = self.i + 1 63 | #一轮结束 64 | if self.i > 2: 65 | #playrecords.show("=============Round " + str(playround) + " End=============") 66 | self.playround = self.playround + 1 67 | #playrecords.show("=============Round " + str(playround) + " Start=============") 68 | self.i = 0 69 | 70 | 71 | if __name__=="__main__": 72 | 73 | begin = time.time() 74 | game_ddz = Game("random") 75 | game_ddz.game_start() 76 | for j in range(10000): 77 | #game_ddz = copy.deepcopy(game_ddz) 78 | i = 0 79 | while(game_ddz.playrecords.winner == 0): 80 | #game_ddz.playrecords.show(str(i)) 81 | game_ddz.next_move() 82 | i = i + 1 83 | print(game_ddz.playrecords.winner) 84 | print(time.time()-begin) 85 | -------------------------------------------------------------------------------- /myclass.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Jul 13 21:55:58 2017 4 | 5 | @author: XuGang 6 | """ 7 | #import numpy as np 8 | from myutil import card_show, choose 9 | 10 | 11 | ############################################ 12 | # 扑克牌相关类 # 13 | ############################################ 14 | class Cards(object): 15 | """ 16 | 一副扑克牌类,54张排,abcd四种花色,小王14-a,大王15-a 17 | """ 18 | def __init__(self): 19 | #初始化扑克牌类型 20 | self.cards_type = ['1-a-12', '1-b-12','1-c-12','1-d-12', 21 | '2-a-13', '2-b-13','2-c-13','2-d-13', 22 | '3-a-1', '3-b-1','3-c-1','3-d-1', 23 | '4-a-2', '4-b-2','4-c-2','4-d-2', 24 | '5-a-3', '5-b-3','5-c-3','5-d-3', 25 | '6-a-4', '6-b-4','6-c-4','6-d-4', 26 | '7-a-5', '7-b-5','7-c-5','7-d-5', 27 | '8-a-6', '8-b-6','8-c-6','8-d-6', 28 | '9-a-7', '9-b-7','9-c-7','9-d-7', 29 | '10-a-8', '10-b-8','10-c-8','10-d-8', 30 | '11-a-9', '11-b-9','11-c-9','11-d-9', 31 | '12-a-10', '12-b-10','12-c-10','12-d-10', 32 | '13-a-11', '13-b-11','13-c-11','13-d-11', 33 | '14-a-14', '15-a-15'] 34 | #初始化扑克牌类 35 | self.cards = self.get_cards() 36 | 37 | #初始化扑克牌类 38 | def get_cards(self): 39 | cards = [] 40 | for card_type in self.cards_type: 41 | cards.append(Card(card_type)) 42 | #打乱顺序 43 | #np.random.shuffle(cards) 44 | return cards 45 | 46 | class Card(object): 47 | """ 48 | 扑克牌类 49 | """ 50 | def __init__(self, card_type): 51 | self.card_type = card_type 52 | #名称 53 | self.name = self.card_type.split('-')[0] 54 | #花色 55 | self.color = self.card_type.split('-')[1] 56 | #大小 57 | self.rank = int(self.card_type.split('-')[2]) 58 | 59 | #判断大小 60 | def bigger_than(self, card_instance): 61 | if (self.rank > card_instance.rank): 62 | return True 63 | else: 64 | return False 65 | 66 | class PlayRecords(object): 67 | """ 68 | 扑克牌记录类 69 | """ 70 | def __init__(self): 71 | #当前手牌 72 | self.cards_left1 = [] 73 | self.cards_left2 = [] 74 | self.cards_left3 = [] 75 | 76 | #可能出牌选择 77 | self.next_moves1 = [] 78 | self.next_moves2 = [] 79 | self.next_moves3 = [] 80 | 81 | #出牌记录 82 | self.next_move1 = [] 83 | self.next_move2 = [] 84 | self.next_move3 = [] 85 | 86 | #出牌记录 87 | self.records = [] 88 | 89 | #胜利者 90 | #winner=0,1,2,3 0表示未结束,1,2,3表示winner 91 | self.winner = 0 92 | 93 | #展示 94 | def show(self, info): 95 | print(info) 96 | card_show(self.cards_left1, "player 1", 1) 97 | card_show(self.cards_left2, "player 2", 1) 98 | card_show(self.cards_left3, "player 3", 1) 99 | card_show(self.records, "record", 3) 100 | 101 | 102 | ############################################ 103 | # 出牌相关类 # 104 | ############################################ 105 | class Moves(object): 106 | """ 107 | 出牌类,单,对,三,三带一,三带二,顺子,炸弹 108 | """ 109 | def __init__(self): 110 | #出牌信息 111 | self.dan = [] 112 | self.dui = [] 113 | self.san = [] 114 | self.san_dai_yi = [] 115 | self.san_dai_er = [] 116 | self.bomb = [] 117 | self.shunzi = [] 118 | 119 | #牌数量信息 120 | self.card_num_info = {} 121 | #牌顺序信息,计算顺子 122 | self.card_order_info = [] 123 | #王牌信息 124 | self.king = [] 125 | 126 | #下次出牌 127 | self.next_moves = [] 128 | #下次出牌类型 129 | self.next_moves_type = [] 130 | 131 | #获取全部出牌列表 132 | def get_moves(self, cards_left): 133 | 134 | #统计牌数量/顺序/王牌信息 135 | for i in cards_left: 136 | #王牌信息 137 | if i.rank in [14,15]: 138 | self.king.append(i) 139 | #数量 140 | tmp = self.card_num_info.get(i.name, []) 141 | if len(tmp) == 0: 142 | self.card_num_info[i.name] = [i] 143 | else: 144 | self.card_num_info[i.name].append(i) 145 | #顺序 146 | if i.rank in [13,14,15]: #不统计2,小王,大王 147 | continue 148 | elif len(self.card_order_info) == 0: 149 | self.card_order_info.append(i) 150 | elif i.rank != self.card_order_info[-1].rank: 151 | self.card_order_info.append(i) 152 | 153 | #王炸 154 | if len(self.king) == 2: 155 | self.bomb.append(self.king) 156 | 157 | #出单,出对,出三,炸弹(考虑拆开) 158 | for k, v in self.card_num_info.items(): 159 | if len(v) == 1: 160 | self.dan.append(v) 161 | elif len(v) == 2: 162 | self.dui.append(v) 163 | self.dan.append(v[:1]) 164 | elif len(v) == 3: 165 | self.san.append(v) 166 | self.dui.append(v[:2]) 167 | self.dan.append(v[:1]) 168 | elif len(v) == 4: 169 | self.bomb.append(v) 170 | self.san.append(v[:3]) 171 | self.dui.append(v[:2]) 172 | self.dan.append(v[:1]) 173 | 174 | #三带一,三带二 175 | for san in self.san: 176 | for dan in self.dan: 177 | #防止重复 178 | if dan[0].name != san[0].name: 179 | self.san_dai_yi.append(san+dan) 180 | for dui in self.dui: 181 | #防止重复 182 | if dui[0].name != san[0].name: 183 | self.san_dai_er.append(san+dui) 184 | 185 | #获取最长顺子 186 | max_len = [] 187 | for i in self.card_order_info: 188 | if i == self.card_order_info[0]: 189 | max_len.append(i) 190 | elif max_len[-1].rank == i.rank - 1: 191 | max_len.append(i) 192 | else: 193 | if len(max_len) >= 5: 194 | self.shunzi.append(max_len) 195 | max_len = [i] 196 | #最后一轮 197 | if len(max_len) >= 5: 198 | self.shunzi.append(max_len) 199 | #拆顺子 200 | shunzi_sub = [] 201 | for i in self.shunzi: 202 | len_total = len(i) 203 | n = len_total - 5 204 | #遍历所有可能顺子长度 205 | while(n > 0): 206 | len_sub = len_total - n 207 | j = 0 208 | while(len_sub+j <= len(i)): 209 | #遍历该长度所有组合 210 | shunzi_sub.append(i[j:len_sub+j]) 211 | j = j + 1 212 | n = n - 1 213 | self.shunzi.extend(shunzi_sub) 214 | 215 | #获取下次出牌列表 216 | def get_next_moves(self, last_move_type, last_move): 217 | #没有last,全加上,除了bomb最后加 218 | if last_move_type == "start": 219 | moves_types = ["dan", "dui", "san", "san_dai_yi", "san_dai_er", "shunzi"] 220 | i = 0 221 | for move_type in [self.dan, self.dui, self.san, self.san_dai_yi, 222 | self.san_dai_er, self.shunzi]: 223 | for move in move_type: 224 | self.next_moves.append(move) 225 | self.next_moves_type.append(moves_types[i]) 226 | i = i + 1 227 | #出单 228 | elif last_move_type == "dan": 229 | for move in self.dan: 230 | #比last大 231 | if move[0].bigger_than(last_move[0]): 232 | self.next_moves.append(move) 233 | self.next_moves_type.append("dan") 234 | #出对 235 | elif last_move_type == "dui": 236 | for move in self.dui: 237 | #比last大 238 | if move[0].bigger_than(last_move[0]): 239 | self.next_moves.append(move) 240 | self.next_moves_type.append("dui") 241 | #出三个 242 | elif last_move_type == "san": 243 | for move in self.san: 244 | #比last大 245 | if move[0].bigger_than(last_move[0]): 246 | self.next_moves.append(move) 247 | self.next_moves_type.append("san") 248 | #出三带一 249 | elif last_move_type == "san_dai_yi": 250 | for move in self.san_dai_yi: 251 | #比last大 252 | if move[0].bigger_than(last_move[0]): 253 | self.next_moves.append(move) 254 | self.next_moves_type.append("san_dai_yi") 255 | #出三带二 256 | elif last_move_type == "san_dai_er": 257 | for move in self.san_dai_er: 258 | #比last大 259 | if move[0].bigger_than(last_move[0]): 260 | self.next_moves.append(move) 261 | self.next_moves_type.append("san_dai_er") 262 | #出炸弹 263 | elif last_move_type == "bomb": 264 | for move in self.bomb: 265 | #比last大 266 | if move[0].bigger_than(last_move[0]): 267 | self.next_moves.append(move) 268 | self.next_moves_type.append("bomb") 269 | #出顺子 270 | elif last_move_type == "shunzi": 271 | for move in self.shunzi: 272 | #相同长度 273 | if len(move) == len(last_move): 274 | #比last大 275 | if move[0].bigger_than(last_move[0]): 276 | self.next_moves.append(move) 277 | self.next_moves_type.append("shunzi") 278 | else: 279 | print("last_move_type_wrong") 280 | 281 | #除了bomb,都可以出炸 282 | if last_move_type != "bomb": 283 | for move in self.bomb: 284 | self.next_moves.append(move) 285 | self.next_moves_type.append("bomb") 286 | 287 | return self.next_moves_type, self.next_moves 288 | 289 | 290 | #展示 291 | def show(self, info): 292 | print(info) 293 | #card_show(self.dan, "dan", 2) 294 | #card_show(self.dui, "dui", 2) 295 | #card_show(self.san, "san", 2) 296 | #card_show(self.san_dai_yi, "san_dai_yi", 2) 297 | #card_show(self.san_dai_er, "san_dai_er", 2) 298 | #card_show(self.bomb, "bomb", 2) 299 | #card_show(self.shunzi, "shunzi", 2) 300 | #card_show(self.next_moves, "next_moves", 2) 301 | 302 | 303 | ############################################ 304 | # 玩家相关类 # 305 | ############################################ 306 | class Player(object): 307 | """ 308 | player类 309 | """ 310 | def __init__(self, player_id): 311 | self.player_id = player_id 312 | self.cards_left = [] 313 | 314 | #展示 315 | def show(self, info): 316 | self.total_moves.show(info) 317 | card_show(self.next_move, "next_move", 1) 318 | #card_show(self.cards_left, "card_left", 1) 319 | 320 | #根据next_move同步cards_left 321 | def record_move(self, playrecords): 322 | #playrecords中records记录[id,next_move] 323 | if self.next_move_type in ["yaobuqi", "buyao"]: 324 | self.next_move = self.next_move_type 325 | playrecords.records.append([self.player_id, self.next_move_type]) 326 | else: 327 | playrecords.records.append([self.player_id, self.next_move]) 328 | for i in self.next_move: 329 | self.cards_left.remove(i) 330 | #同步playrecords 331 | if self.player_id == 1: 332 | playrecords.cards_left1 = self.cards_left 333 | playrecords.next_moves1.append(self.next_moves) 334 | playrecords.next_move1.append(self.next_move) 335 | elif self.player_id == 2: 336 | playrecords.cards_left2 = self.cards_left 337 | playrecords.next_moves2.append(self.next_moves) 338 | playrecords.next_move2.append(self.next_move) 339 | elif self.player_id == 3: 340 | playrecords.cards_left3 = self.cards_left 341 | playrecords.next_moves3.append(self.next_moves) 342 | playrecords.next_move3.append(self.next_move) 343 | #是否牌局结束 344 | end = False 345 | if len(self.cards_left) == 0: 346 | end = True 347 | return end 348 | 349 | #出牌 350 | def go(self, last_move_type, last_move, playrecords, model): 351 | #所有出牌可选列表 352 | self.total_moves = Moves() 353 | #获取全部出牌列表 354 | self.total_moves.get_moves(self.cards_left) 355 | #获取下次出牌列表 356 | self.next_move_types, self.next_moves = self.total_moves.get_next_moves(last_move_type, last_move) 357 | #在next_moves中选择出牌方法 358 | self.next_move_type, self.next_move = choose(self.next_move_types, self.next_moves, last_move_type, model) 359 | #记录 360 | end = self.record_move(playrecords) 361 | #展示 362 | #self.show("Player " + str(self.player_id)) 363 | #要不起&不要 364 | yaobuqi = False 365 | if self.next_move_type in ["yaobuqi","buyao"]: 366 | yaobuqi = True 367 | self.next_move_type = last_move_type 368 | self.next_move = last_move 369 | 370 | return self.next_move_type, self.next_move, end, yaobuqi 371 | 372 | 373 | ############################################ 374 | # 网页展示类 # 375 | ############################################ 376 | class WebShow(object): 377 | """ 378 | 网页展示类 379 | """ 380 | def __init__(self, playrecords): 381 | 382 | #胜利者 383 | self.winner = playrecords.winner 384 | 385 | #剩余手牌 386 | self.cards_left1 = [] 387 | for i in playrecords.cards_left1: 388 | self.cards_left1.append(i.name+i.color) 389 | self.cards_left2 = [] 390 | for i in playrecords.cards_left2: 391 | self.cards_left2.append(i.name+i.color) 392 | self.cards_left3 = [] 393 | for i in playrecords.cards_left3: 394 | self.cards_left3.append(i.name+i.color) 395 | 396 | #可能出牌 397 | self.next_moves1 = [] 398 | if len(playrecords.next_moves1) != 0: 399 | next_moves = playrecords.next_moves1[-1] 400 | for move in next_moves: 401 | cards = [] 402 | for card in move: 403 | cards.append(card.name+card.color) 404 | self.next_moves1.append(cards) 405 | self.next_moves2 = [] 406 | if len(playrecords.next_moves2) != 0: 407 | next_moves = playrecords.next_moves2[-1] 408 | for move in next_moves: 409 | cards = [] 410 | for card in move: 411 | cards.append(card.name+card.color) 412 | self.next_moves2.append(cards) 413 | self.next_moves3 = [] 414 | if len(playrecords.next_moves3) != 0: 415 | next_moves = playrecords.next_moves3[-1] 416 | for move in next_moves: 417 | cards = [] 418 | for card in move: 419 | cards.append(card.name+card.color) 420 | self.next_moves3.append(cards) 421 | 422 | #出牌 423 | self.next_move1 = [] 424 | if len(playrecords.next_move1) != 0: 425 | next_move = playrecords.next_move1[-1] 426 | if next_move in ["yaobuqi", "buyao"]: 427 | self.next_move1.append(next_move) 428 | else: 429 | for card in next_move: 430 | self.next_move1.append(card.name+card.color) 431 | self.next_move2 = [] 432 | if len(playrecords.next_move2) != 0: 433 | next_move = playrecords.next_move2[-1] 434 | if next_move in ["yaobuqi", "buyao"]: 435 | self.next_move2.append(next_move) 436 | else: 437 | for card in next_move: 438 | self.next_move2.append(card.name+card.color) 439 | self.next_move3 = [] 440 | if len(playrecords.next_move3) != 0: 441 | next_move = playrecords.next_move3[-1] 442 | if next_move in ["yaobuqi", "buyao"]: 443 | self.next_move3.append(next_move) 444 | else: 445 | for card in next_move: 446 | self.next_move3.append(card.name+card.color) 447 | 448 | #记录 449 | self.records = [] 450 | for i in playrecords.records: 451 | tmp = [] 452 | tmp.append(i[0]) 453 | tmp_name = [] 454 | #处理要不起 455 | try: 456 | for j in i[1]: 457 | tmp_name.append(j.name+j.color) 458 | tmp.append(tmp_name) 459 | except: 460 | tmp.append(i[1]) 461 | self.records.append(tmp) 462 | 463 | 464 | -------------------------------------------------------------------------------- /myutil.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Jul 13 21:55:58 2017 4 | 5 | @author: XuGang 6 | """ 7 | import numpy as np 8 | 9 | 10 | #展示扑克函数 11 | def card_show(cards, info, n): 12 | 13 | #扑克牌记录类展示 14 | if n == 1: 15 | print(info) 16 | names = [] 17 | for i in cards: 18 | names.append(i.name+i.color) 19 | print(names) 20 | #Moves展示 21 | elif n == 2: 22 | if len(cards) == 0: 23 | return 0 24 | print(info) 25 | moves = [] 26 | for i in cards: 27 | names = [] 28 | for j in i: 29 | names.append(j.name+j.color) 30 | moves.append(names) 31 | print(moves) 32 | #record展示 33 | elif n == 3: 34 | print(info) 35 | names = [] 36 | for i in cards: 37 | tmp = [] 38 | tmp.append(i[0]) 39 | tmp_name = [] 40 | #处理要不起 41 | try: 42 | for j in i[1]: 43 | tmp_name.append(j.name+j.color) 44 | tmp.append(tmp_name) 45 | except: 46 | tmp.append(i[1]) 47 | names.append(tmp) 48 | print(names) 49 | 50 | 51 | #在Player的next_moves中选择出牌方法 52 | def choose(next_move_types, next_moves, last_move_type, model): 53 | 54 | if model == "random": 55 | return choose_random(next_move_types, next_moves, last_move_type) 56 | 57 | #random 58 | def choose_random(next_move_types, next_moves, last_move_type): 59 | #要不起 60 | if len(next_moves) == 0: 61 | return "yaobuqi", [] 62 | else: 63 | #start不能不要 64 | if last_move_type == "start": 65 | r_max = len(next_moves) 66 | else: 67 | r_max = len(next_moves)+1 68 | r = np.random.randint(0,r_max) 69 | #添加不要 70 | if r == len(next_moves): 71 | return "buyao", [] 72 | 73 | return next_move_types[r], next_moves[r] 74 | 75 | #发牌 76 | def game_init(players, playrecords, cards): 77 | 78 | #洗牌 79 | np.random.shuffle(cards.cards) 80 | #排序 81 | p1_cards = cards.cards[:18] 82 | p1_cards.sort(key=lambda x: x.rank) 83 | p2_cards = cards.cards[18:36] 84 | p2_cards.sort(key=lambda x: x.rank) 85 | p3_cards = cards.cards[36:] 86 | p3_cards.sort(key=lambda x: x.rank) 87 | players[0].cards_left = playrecords.cards_left1 = p1_cards 88 | players[1].cards_left = playrecords.cards_left2 = p2_cards 89 | players[2].cards_left = playrecords.cards_left3 = p3_cards 90 | 91 | 92 | 93 | 94 | 95 | 96 | --------------------------------------------------------------------------------