前言
最近在網上看到一個有意思的開源項目,基于快手團隊開發的開源ai斗地主——douzero做的一個“成熟”的ai,項目開源地址【https://github.com/tianqiraf/douzero_for_happydoudizhu – tianqiraf】。
今天我們就一起來學習下是如何制作一個基于douzero的出牌器,看看ai是如何來幫助斗地主的!
一、核心功能設計
首先這款出牌器是基于douzero開發的,核心是需要利用訓練好的ai模型來幫住我們,給出最優出牌方案。
其次關于出牌器,先要需要確認一個ai出牌角色,代表我們玩家自己。我們只要給這個ai輸入玩家手牌和三張底牌。確認好地主和農民的各個角色,告訴它三個人對應的關系,這樣就可以確定隊友和對手。
我們還要將每一輪其他兩人的出牌輸入,這樣出牌器就可以根據出牌數據,及時提供給我們最優出牌決策,帶領我們取得勝利!
那么如何獲取三者之間的關系呢?誰是地主?誰是農民?是自己一人作戰還是農民合作?自己玩家的手牌是什么?三張底牌是什么?這些也都需要在開局后確認好。
大致可以整理出要實現的核心功能如下:
ui設計排版布局
- 顯示三張底牌
- 顯示ai角色出牌數據區域,上家出牌數據區域,下家出牌數據區域,本局勝率區域
- ai玩家手牌區域
- ai出牌器開始停止
手牌和出牌數據識別
- 游戲剛開始根據屏幕位置,截圖識別ai玩家手牌及三張底牌
- 確認三者之間的關系,識別地主和農民角色,確認隊友及對手關系
- 識別每輪三位玩家依次出了什么牌,刷新顯示對應區域
ai出牌方案輸出
- 加載訓練好的ai模型,初始化游戲環境
- 每輪出牌判斷,根據上家出牌數據給出最優出牌決策
- 自動刷新玩家剩余手牌和本局勝率預測
二、實現步驟
1. ui設計排版布局
根據上述功能,首先考慮進行簡單的ui布局設計,使用的是pyqt5。核心設計代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
|
def setupui( self , form): form.setobjectname( "form" ) form.resize( 440 , 395 ) font = qtgui.qfont() font.setfamily( "arial" ) font.setpointsize( 9 ) font.setbold(true) font.setitalic(false) font.setweight( 75 ) form.setfont(font) self .winrate = qtwidgets.qlabel(form) self .winrate.setgeometry(qtcore.qrect( 240 , 180 , 171 , 61 )) font = qtgui.qfont() font.setpointsize( 14 ) self .winrate.setfont(font) self .winrate.setalignment(qtcore.qt.aligncenter) self .winrate.setobjectname( "winrate" ) self .initcard = qtwidgets.qpushbutton(form) self .initcard.setgeometry(qtcore.qrect( 60 , 330 , 121 , 41 )) font = qtgui.qfont() font.setfamily( "arial" ) font.setpointsize( 14 ) font.setbold(true) font.setweight( 75 ) self .initcard.setfont(font) self .initcard.setstylesheet("") self .initcard.setobjectname( "initcard" ) self .userhandcards = qtwidgets.qlabel(form) self .userhandcards.setgeometry(qtcore.qrect( 10 , 260 , 421 , 41 )) font = qtgui.qfont() font.setpointsize( 14 ) self .userhandcards.setfont(font) self .userhandcards.setalignment(qtcore.qt.aligncenter) self .userhandcards.setobjectname( "userhandcards" ) self .lplayer = qtwidgets.qframe(form) self .lplayer.setgeometry(qtcore.qrect( 10 , 80 , 201 , 61 )) self .lplayer.setframeshape(qtwidgets.qframe.styledpanel) self .lplayer.setframeshadow(qtwidgets.qframe.raised) self .lplayer.setobjectname( "lplayer" ) self .lplayedcard = qtwidgets.qlabel( self .lplayer) self .lplayedcard.setgeometry(qtcore.qrect( 0 , 0 , 201 , 61 )) font = qtgui.qfont() font.setpointsize( 14 ) self .lplayedcard.setfont(font) self .lplayedcard.setalignment(qtcore.qt.aligncenter) self .lplayedcard.setobjectname( "lplayedcard" ) self .rplayer = qtwidgets.qframe(form) self .rplayer.setgeometry(qtcore.qrect( 230 , 80 , 201 , 61 )) font = qtgui.qfont() font.setpointsize( 16 ) self .rplayer.setfont(font) self .rplayer.setframeshape(qtwidgets.qframe.styledpanel) self .rplayer.setframeshadow(qtwidgets.qframe.raised) self .rplayer.setobjectname( "rplayer" ) self .rplayedcard = qtwidgets.qlabel( self .rplayer) self .rplayedcard.setgeometry(qtcore.qrect( 0 , 0 , 201 , 61 )) font = qtgui.qfont() font.setpointsize( 14 ) self .rplayedcard.setfont(font) self .rplayedcard.setalignment(qtcore.qt.aligncenter) self .rplayedcard.setobjectname( "rplayedcard" ) self .player = qtwidgets.qframe(form) self .player.setgeometry(qtcore.qrect( 40 , 180 , 171 , 61 )) self .player.setframeshape(qtwidgets.qframe.styledpanel) self .player.setframeshadow(qtwidgets.qframe.raised) self .player.setobjectname( "player" ) self .predictedcard = qtwidgets.qlabel( self .player) self .predictedcard.setgeometry(qtcore.qrect( 0 , 0 , 171 , 61 )) font = qtgui.qfont() font.setpointsize( 14 ) self .predictedcard.setfont(font) self .predictedcard.setalignment(qtcore.qt.aligncenter) self .predictedcard.setobjectname( "predictedcard" ) self .threelandlordcards = qtwidgets.qlabel(form) self .threelandlordcards.setgeometry(qtcore.qrect( 140 , 10 , 161 , 41 )) font = qtgui.qfont() font.setpointsize( 16 ) self .threelandlordcards.setfont(font) self .threelandlordcards.setalignment(qtcore.qt.aligncenter) self .threelandlordcards.setobjectname( "threelandlordcards" ) self .stop = qtwidgets.qpushbutton(form) self .stop.setgeometry(qtcore.qrect( 260 , 330 , 111 , 41 )) font = qtgui.qfont() font.setfamily( "arial" ) font.setpointsize( 14 ) font.setbold(true) font.setweight( 75 ) self .stop.setfont(font) self .stop.setstylesheet("") self .stop.setobjectname( "stop" ) self .retranslateui(form) self .initcard.clicked.connect(form.init_cards) self .stop.clicked.connect(form.stop) qtcore.qmetaobject.connectslotsbyname(form) def retranslateui( self , form): _translate = qtcore.qcoreapplication.translate form.setwindowtitle(_translate( "form" , "ai歡樂斗地主--dragon少年" )) self .winrate.settext(_translate( "form" , "勝率:--%" )) self .initcard.settext(_translate( "form" , "開始" )) self .userhandcards.settext(_translate( "form" , "手牌" )) self .lplayedcard.settext(_translate( "form" , "上家出牌區域" )) self .rplayedcard.settext(_translate( "form" , "下家出牌區域" )) self .predictedcard.settext(_translate( "form" , "ai出牌區域" )) self .threelandlordcards.settext(_translate( "form" , "三張底牌" )) self .stop.settext(_translate( "form" , "停止" )) |
2. 手牌和出牌數據識別
接下來需要所有撲克牌的模板圖片與游戲屏幕特定區域的截圖進行對比,這樣才能獲取ai玩家手牌、底牌、每一輪出牌、三者關系(地主、地主上家、地主下家)。
識別ai玩家手牌及三張底牌:
我們可以截取游戲屏幕,根據固定位置來識別當前ai玩家的手牌和三張底牌。核心代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
# 牌檢測結果濾波 def cards_filter( self , location, distance): if len (location) = = 0 : return 0 loclist = [location[ 0 ][ 0 ]] count = 1 for e in location: flag = 1 # “是新的”標志 for have in loclist: if abs (e[ 0 ] - have) < = distance: flag = 0 break if flag: count + = 1 loclist.append(e[ 0 ]) return count # 獲取玩家ai手牌 def find_my_cards( self , pos): user_hand_cards_real = "" img = pyautogui.screenshot(region = pos) for card in allcards: result = pyautogui.locateall(needleimage = 'pics/m' + card + '.png' , haystackimage = img, confidence = self .myconfidence) user_hand_cards_real + = card[ 1 ] * self .cards_filter( list (result), self .myfilter) return user_hand_cards_real # 獲取地主三張底牌 def find_three_landlord_cards( self , pos): three_landlord_cards_real = "" img = pyautogui.screenshot(region = pos) img = img.resize(( 349 , 168 )) for card in allcards: result = pyautogui.locateall(needleimage = 'pics/o' + card + '.png' , haystackimage = img, confidence = self .threelandlordcardsconfidence) three_landlord_cards_real + = card[ 1 ] * self .cards_filter( list (result), self .otherfilter) return three_landlord_cards_real |
效果如下所示:
地主、地主上家、地主下家:
同理我們可以根據游戲屏幕截圖,識別地主的圖標,確認地主角色。核心代碼如下:
1
2
3
4
5
6
7
|
# 查找地主角色 def find_landlord( self , landlord_flag_pos): for pos in landlord_flag_pos: result = pyautogui.locateonscreen( 'pics/landlord_words.png' , region = pos, confidence = self .landlordflagconfidence) if result is not none: return landlord_flag_pos.index(pos) return none |
這樣我們就可以得到玩家ai手牌,其他玩家手牌(預測),地主三張底牌,三者角色關系,出牌順序。核心代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
# 坐標 self .myhandcardspos = ( 414 , 804 , 1041 , 59 ) # ai玩家截圖區域 self .lplayedcardspos = ( 530 , 470 , 380 , 160 ) # 左側玩家截圖區域 self .rplayedcardspos = ( 1010 , 470 , 380 , 160 ) # 右側玩家截圖區域 self .landlordflagpos = [( 1320 , 300 , 110 , 140 ), ( 320 , 720 , 110 , 140 ), ( 500 , 300 , 110 , 140 )] # 地主標志截圖區域(右-我-左) self .threelandlordcardspos = ( 817 , 36 , 287 , 136 ) # 地主底牌截圖區域,resize成349x168 def init_cards( self ): # 玩家手牌 self .user_hand_cards_real = "" self .user_hand_cards_env = [] # 其他玩家出牌 self .other_played_cards_real = "" self .other_played_cards_env = [] # 其他玩家手牌(整副牌減去玩家手牌,后續再減掉歷史出牌) self .other_hand_cards = [] # 三張底牌 self .three_landlord_cards_real = "" self .three_landlord_cards_env = [] # 玩家角色代碼:0-地主上家, 1-地主, 2-地主下家 self .user_position_code = none self .user_position = "" # 開局時三個玩家的手牌 self .card_play_data_list = {} # 出牌順序:0-玩家出牌, 1-玩家下家出牌, 2-玩家上家出牌 self .play_order = 0 self .env = none # 識別玩家手牌 self .user_hand_cards_real = self .find_my_cards( self .myhandcardspos) self .userhandcards.settext( self .user_hand_cards_real) self .user_hand_cards_env = [realcard2envcard[c] for c in list ( self .user_hand_cards_real)] # 識別三張底牌 self .three_landlord_cards_real = self .find_three_landlord_cards( self .threelandlordcardspos) self .threelandlordcards.settext( "底牌:" + self .three_landlord_cards_real) self .three_landlord_cards_env = [realcard2envcard[c] for c in list ( self .three_landlord_cards_real)] # 識別玩家的角色 self .user_position_code = self .find_landlord( self .landlordflagpos) if self .user_position_code is none: items = ( "地主上家" , "地主" , "地主下家" ) item, okpressed = qinputdialog.getitem( self , "選擇角色" , "未識別到地主,請手動選擇角色:" , items, 0 , false) if okpressed and item: self .user_position_code = items.index(item) else : return self .user_position = [ 'landlord_up' , 'landlord' , 'landlord_down' ][ self .user_position_code] for player in self .players: player.setstylesheet( 'background-color: rgba(255, 0, 0, 0);' ) self .players[ self .user_position_code].setstylesheet( 'background-color: rgba(255, 0, 0, 0.1);' ) # 整副牌減去玩家手上的牌,就是其他人的手牌,再分配給另外兩個角色(如何分配對ai判斷沒有影響) for i in set (allenvcard): self .other_hand_cards.extend([i] * (allenvcard.count(i) - self .user_hand_cards_env.count(i))) self .card_play_data_list.update({ 'three_landlord_cards' : self .three_landlord_cards_env, [ 'landlord_up' , 'landlord' , 'landlord_down' ][( self .user_position_code + 0 ) % 3 ]: self .user_hand_cards_env, [ 'landlord_up' , 'landlord' , 'landlord_down' ][( self .user_position_code + 1 ) % 3 ]: self .other_hand_cards[ 0 : 17 ] if ( self .user_position_code + 1 ) % 3 ! = 1 else self .other_hand_cards[ 17 :], [ 'landlord_up' , 'landlord' , 'landlord_down' ][( self .user_position_code + 2 ) % 3 ]: self .other_hand_cards[ 0 : 17 ] if ( self .user_position_code + 1 ) % 3 = = 1 else self .other_hand_cards[ 17 :] }) print ( self .card_play_data_list) # 生成手牌結束,校驗手牌數量 if len ( self .card_play_data_list[ "three_landlord_cards" ]) ! = 3 : qmessagebox.critical( self , "底牌識別出錯" , "底牌必須是3張!" , qmessagebox.yes, qmessagebox.yes) self .init_display() return if len ( self .card_play_data_list[ "landlord_up" ]) ! = 17 or \ len ( self .card_play_data_list[ "landlord_down" ]) ! = 17 or \ len ( self .card_play_data_list[ "landlord" ]) ! = 20 : qmessagebox.critical( self , "手牌識別出錯" , "初始手牌數目有誤" , qmessagebox.yes, qmessagebox.yes) self .init_display() return # 得到出牌順序 self .play_order = 0 if self .user_position = = "landlord" else 1 if self .user_position = = "landlord_up" else 2 |
效果如下:
3. ai出牌方案輸出
下面我們就需要用到douzero開源的ai斗地主了。douzero項目地址:https://github.com/kwai/douzero。我們需要將該開源項目下載并導入項目中。
創建一個ai玩家角色,初始化游戲環境,加載模型,進行每輪的出牌判斷,控制一局游戲流程的進行和結束。核心代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
# 創建一個代表玩家的ai ai_players = [ 0 , 0 ] ai_players[ 0 ] = self .user_position ai_players[ 1 ] = deepagent( self .user_position, self .card_play_model_path_dict[ self .user_position]) # 初始化游戲環境 self .env = gameenv(ai_players) # 游戲開始 self .start() def start( self ): self .env.card_play_init( self .card_play_data_list) print ( "開始出牌\n" ) while not self .env.game_over: # 玩家出牌時就通過智能體獲取action,否則通過識別獲取其他玩家出牌 if self .play_order = = 0 : self .predictedcard.settext( "..." ) action_message = self .env.step( self .user_position) # 更新界面 self .userhandcards.settext( "手牌:" + str (''.join( [envcard2realcard[c] for c in self .env.info_sets[ self .user_position].player_hand_cards]))[:: - 1 ]) self .predictedcard.settext(action_message[ "action" ] if action_message[ "action" ] else "不出" ) self .winrate.settext( "勝率:" + action_message[ "win_rate" ]) print ( "\n手牌:" , str (''.join( [envcard2realcard[c] for c in self .env.info_sets[ self .user_position].player_hand_cards]))) print ( "出牌:" , action_message[ "action" ] if action_message[ "action" ] else "不出" , ", 勝率:" , action_message[ "win_rate" ]) while self .have_white( self .rplayedcardspos) = = 1 or \ pyautogui.locateonscreen( 'pics/pass.png' , region = self .rplayedcardspos, confidence = self .landlordflagconfidence): print ( "等待玩家出牌" ) self .counter.restart() while self .counter.elapsed() < 100 : qtwidgets.qapplication.processevents(qeventloop.allevents, 50 ) self .play_order = 1 elif self .play_order = = 1 : self .rplayedcard.settext( "..." ) pass_flag = none while self .have_white( self .rplayedcardspos) = = 0 and \ not pyautogui.locateonscreen( 'pics/pass.png' , region = self .rplayedcardspos, confidence = self .landlordflagconfidence): print ( "等待下家出牌" ) self .counter.restart() while self .counter.elapsed() < 500 : qtwidgets.qapplication.processevents(qeventloop.allevents, 50 ) self .counter.restart() while self .counter.elapsed() < 500 : qtwidgets.qapplication.processevents(qeventloop.allevents, 50 ) # 不出 pass_flag = pyautogui.locateonscreen( 'pics/pass.png' , region = self .rplayedcardspos, confidence = self .landlordflagconfidence) # 未找到"不出" if pass_flag is none: # 識別下家出牌 self .other_played_cards_real = self .find_other_cards( self .rplayedcardspos) # 找到"不出" else : self .other_played_cards_real = "" print ( "\n下家出牌:" , self .other_played_cards_real) self .other_played_cards_env = [realcard2envcard[c] for c in list ( self .other_played_cards_real)] self .env.step( self .user_position, self .other_played_cards_env) # 更新界面 self .rplayedcard.settext( self .other_played_cards_real if self .other_played_cards_real else "不出" ) self .play_order = 2 elif self .play_order = = 2 : self .lplayedcard.settext( "..." ) while self .have_white( self .lplayedcardspos) = = 0 and \ not pyautogui.locateonscreen( 'pics/pass.png' , region = self .lplayedcardspos, confidence = self .landlordflagconfidence): print ( "等待上家出牌" ) self .counter.restart() while self .counter.elapsed() < 500 : qtwidgets.qapplication.processevents(qeventloop.allevents, 50 ) self .counter.restart() while self .counter.elapsed() < 500 : qtwidgets.qapplication.processevents(qeventloop.allevents, 50 ) # 不出 pass_flag = pyautogui.locateonscreen( 'pics/pass.png' , region = self .lplayedcardspos, confidence = self .landlordflagconfidence) # 未找到"不出" if pass_flag is none: # 識別上家出牌 self .other_played_cards_real = self .find_other_cards( self .lplayedcardspos) # 找到"不出" else : self .other_played_cards_real = "" print ( "\n上家出牌:" , self .other_played_cards_real) self .other_played_cards_env = [realcard2envcard[c] for c in list ( self .other_played_cards_real)] self .env.step( self .user_position, self .other_played_cards_env) self .play_order = 0 # 更新界面 self .lplayedcard.settext( self .other_played_cards_real if self .other_played_cards_real else "不出" ) else : pass self .counter.restart() while self .counter.elapsed() < 100 : qtwidgets.qapplication.processevents(qeventloop.allevents, 50 ) print ( "{}勝,本局結束!\n" . format ( "農民" if self .env.winner = = "farmer" else "地主" )) qmessagebox.information( self , "本局結束" , "{}勝!" . format ( "農民" if self .env.winner = = "farmer" else "地主" ), qmessagebox.yes, qmessagebox.yes) self .env.reset() self .init_display() |
到這里,整個ai斗地主出牌流程基本已經完成了。
三、出牌器用法
按照上述過程,這款ai出牌器已經制作完成了。后面應該如何使用呢?如果不想研究源碼,只想使用這款ai斗地主出牌器,驗證下效果,該怎么配置環境運行這個ai出牌器呢?下面就開始介紹。
1. 環境配置
首先我們需要安裝這些第三方庫,配置相關環境,如下所示:
1
2
3
4
5
6
7
8
9
|
torch = = 1.9 . 0 gitpython = = 3.0 . 5 gitdb2 = = 2.0 . 6 pyautogui = = 0.9 . 50 pyqt5 = = 5.13 . 0 pyqt5 - sip = = 12.8 . 1 pillow> = 5.2 . 0 opencv - python rlcard |
2. 坐標調整確認
我們可以打開游戲界面,將游戲窗口模式下最大化運行,把ai出牌器程序窗口需要移至右下角,不能遮擋手牌、地主標志、底牌、歷史出牌這些關鍵位置。
其次我們要確認屏幕截圖獲取的各個區域是否正確。如果有問題需要進行區域位置坐標調整。
1
2
3
4
5
6
|
# 坐標 self .myhandcardspos = ( 414 , 804 , 1041 , 59 ) # 我的截圖區域 self .lplayedcardspos = ( 530 , 470 , 380 , 160 ) # 左邊截圖區域 self .rplayedcardspos = ( 1010 , 470 , 380 , 160 ) # 右邊截圖區域 self .landlordflagpos = [( 1320 , 300 , 110 , 140 ), ( 320 , 720 , 110 , 140 ), ( 500 , 300 , 110 , 140 )] # 地主標志截圖區域(右-我-左) self .threelandlordcardspos = ( 817 , 36 , 287 , 136 ) # 地主底牌截圖區域,resize成349x168 |
3. 運行測試
當所有環境配置完成,各區域坐標位置確認無誤之后,下面我們就可以直接運行程序,測試效果啦~
首先我們運行ai出牌器程序,打開游戲界面,進入游戲。當玩家就位,手牌分發完畢,地主身份確認之后,我們就可以點擊畫面中開始按鈕,讓ai來幫助我們斗地主了。
基于這個douzero項目做一個“成熟”的ai,項目開源地址【https://github.com/tianqiraf/douzero_for_happydoudizhu – tianqiraf】。
今天我們就到這里,明天繼續努力!
如果本篇博客有任何錯誤,請批評指教,不勝感激 !
到此這篇關于我用python做個ai出牌器斗地主把把贏的文章就介紹到這了,更多相關python自動出牌器內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://blog.csdn.net/hhladminhhl/article/details/119304504?spm=1001.2014.3001.5501