Initial commit

This commit is contained in:
Vincentzyx 2021-07-28 19:47:43 +08:00
commit fd2e73b0e0
131 changed files with 3691 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

114
.gitignore vendored Normal file
View File

@ -0,0 +1,114 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.8" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@ -0,0 +1,35 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="78" name="Python" />
</Languages>
</inspection_tool>
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="E501" />
<option value="E302" />
<option value="E303" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N803" />
<option value="N806" />
<option value="N802" />
<option value="N801" />
</list>
</option>
</inspection_tool>
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/DouZero_For_HappyDouDiZhu-master.iml" filepath="$PROJECT_DIR$/.idea/DouZero_For_HappyDouDiZhu-master.iml" />
</modules>
</component>
</project>

36
BidHelper.py Normal file
View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Created by: Vincentzyx
from douzero.env.game import GameEnv
from douzero.evaluation.deep_agent import DeepAgent
RealCard2EnvCard = {'3': 3, '4': 4, '5': 5, '6': 6, '7': 7,
'8': 8, '9': 9, 'T': 10, 'J': 11, 'Q': 12,
'K': 13, 'A': 14, '2': 17, 'X': 20, 'D': 30}
card_play_model_path_dict = {
'landlord': "baselines/douzero_WP/landlord.ckpt",
'landlord_up': "baselines/douzero_WP/landlord_up.ckpt",
'landlord_down': "baselines/douzero_WP/landlord_down.ckpt"
}
user_position = "landlord" # 玩家角色代码0-地主上家, 1-地主, 2-地主下家
ai_players = [0, 0]
ai_players[0] = user_position
ai_players[1] = DeepAgent(user_position, card_play_model_path_dict[user_position])
env = GameEnv(ai_players)
card_play_data_list = {}
def GetWinRate(cards):
env.reset()
card_play_data_list.update({
'three_landlord_cards': [RealCard2EnvCard[i] for i in "333"],
'landlord': [RealCard2EnvCard[i] for i in cards],
'landlord_up': [RealCard2EnvCard[i] for i in "33333333333333333"],
'landlord_down': [RealCard2EnvCard[i] for i in "33333333333333333"]
})
env.card_play_init(card_play_data_list)
action_message = env.step(user_position)
win_rate = float(action_message["win_rate"].replace("%",""))
return win_rate

69
BidModel.py Normal file
View File

@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Created by: Vincentzyx
import os
import torch
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.data.dataset import Dataset
import time
def EnvToOnehot(cards):
Env2IdxMap = {3:0,4:1,5:2,6:3,7:4,8:5,9:6,10:7,11:8,12:9,13:10,14:11,17:12,20:13,30:14}
cards = [Env2IdxMap[i] for i in cards]
Onehot = torch.zeros((4,15))
for i in range(0, 15):
Onehot[:cards.count(i),i] = 1
return Onehot
def RealToOnehot(cards):
RealCard2EnvCard = {'3': 0, '4': 1, '5': 2, '6': 3, '7': 4,
'8': 5, '9': 6, 'T': 7, 'J': 8, 'Q': 9,
'K': 10, 'A': 11, '2': 12, 'X': 13, 'D': 14}
cards = [RealCard2EnvCard[c] for c in cards]
Onehot = torch.zeros((4,15))
for i in range(0, 15):
Onehot[:cards.count(i),i] = 1
return Onehot
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(60, 512)
self.fc2 = nn.Linear(512, 512)
self.fc3 = nn.Linear(512, 512)
self.fc4 = nn.Linear(512, 512)
self.fc5 = nn.Linear(512, 512)
self.fc6 = nn.Linear(512, 1)
self.dropout5 = nn.Dropout(0.5)
self.dropout3 = nn.Dropout(0.3)
self.dropout1 = nn.Dropout(0.1)
def forward(self, input):
x = self.fc1(input)
x = torch.relu(self.dropout1(self.fc2(x)))
x = torch.relu(self.dropout3(self.fc3(x)))
x = torch.relu(self.dropout5(self.fc4(x)))
x = torch.relu(self.dropout5(self.fc5(x)))
x = self.fc6(x)
return x
UseGPU = False
device = torch.device('cuda:0')
net = Net()
net.eval()
if UseGPU:
net = net.to(device)
if os.path.exists("bid_weights.pkl"):
net.load_state_dict(torch.load('bid_weights.pkl'))
def predict(cards):
input = RealToOnehot(cards)
if UseGPU:
input = input.to(device)
input = torch.flatten(input)
win_rate = net(input)
return win_rate[0].item() * 100

67
FarmerModel.py Normal file
View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Created by: Vincentzyx
import os
import torch
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.data.dataset import Dataset
import time
def EnvToOnehot(cards):
Env2IdxMap = {3:0,4:1,5:2,6:3,7:4,8:5,9:6,10:7,11:8,12:9,13:10,14:11,17:12,20:13,30:14}
cards = [Env2IdxMap[i] for i in cards]
Onehot = torch.zeros((4,15))
for i in range(0, 15):
Onehot[:cards.count(i),i] = 1
return Onehot
def RealToOnehot(cards, llc):
RealCard2EnvCard = {'3': 0, '4': 1, '5': 2, '6': 3, '7': 4,
'8': 5, '9': 6, 'T': 7, 'J': 8, 'Q': 9,
'K': 10, 'A': 11, '2': 12, 'X': 13, 'D': 14}
cards = [RealCard2EnvCard[c] for c in cards]
llcs = [RealCard2EnvCard[c] for c in llc]
Onehot = torch.zeros((7,15))
for i in range(0, 15):
Onehot[:cards.count(i),i] = 1
Onehot[4:llcs.count(i)+4,i] = 1
return Onehot
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(105, 512)
self.fc2 = nn.Linear(512, 512)
self.fc3 = nn.Linear(512, 512)
self.fc4 = nn.Linear(512, 512)
self.fc5 = nn.Linear(512, 512)
self.fc6 = nn.Linear(512, 1)
self.dropout5 = nn.Dropout(0.5)
self.dropout3 = nn.Dropout(0.3)
self.dropout1 = nn.Dropout(0.1)
def forward(self, input):
x = self.fc1(input)
x = torch.relu(self.dropout3(self.fc2(x)))
x = torch.relu(self.dropout5(self.fc3(x)))
x = torch.relu(self.dropout5(self.fc4(x)))
x = torch.relu(self.dropout5(self.fc5(x)))
x = self.fc6(x)
x = torch.sigmoid(x)
return x
Nets = {"up": Net(), "down": Net()}
if os.path.exists("landlord_up_weights.pkl"):
Nets["up"].load_state_dict(torch.load("landlord_up_weights.pkl"))
Nets["up"].eval()
if os.path.exists("landlord_down_weights.pkl"):
Nets["down"].load_state_dict(torch.load("landlord_down_weights.pkl"))
Nets["down"].eval()
def predict(cards, llc, type="up"):
net = Nets[type]
x = torch.flatten(RealToOnehot(cards, llc))
y = net(x)[0].item()
return y * 100

452
GameHelper.py Normal file
View File

@ -0,0 +1,452 @@
# -*- coding: utf-8 -*-
# Created by: Vincentzyx
import win32gui
import win32ui
import win32api
from ctypes import windll
from PIL import Image
import cv2
import pyautogui
import matplotlib.pyplot as plt
import numpy as np
import os
import time
import threading
from win32con import WM_LBUTTONDOWN, MK_LBUTTON, WM_LBUTTONUP, WM_MOUSEMOVE
import multiprocessing as mp
from PyQt5 import QtGui, QtWidgets, QtCore
from PyQt5.QtCore import QTime, QEventLoop
Pics = {}
ReqQueue = mp.Queue()
ResultQueue = mp.Queue()
Processes = []
def GetSingleCardQueue(reqQ, resQ, Pics):
while True:
while not reqQ.empty():
image, i, sx, sy, sw, sh, checkSelect = reqQ.get()
result = GetSingleCard(image, i, sx, sy, sw, sh, checkSelect, Pics)
del image
if result is not None:
resQ.put(result)
time.sleep(0.01)
def ShowImg(image):
plt.imshow(image)
plt.show()
def DrawRectWithText(image, rect, text):
img = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR)
x, y, w, h = rect
img2 = cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
img2 = cv2.putText(img2, text, (x, y + 20), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
return Image.fromarray(cv2.cvtColor(img2, cv2.COLOR_BGR2RGB))
def CompareCard(card):
order = {"3": 0, "4": 1, "5": 2, "6": 3, "7": 4, "8": 5, "9": 6, "T": 7, "J": 8, "Q": 9, "K": 10, "A": 11, "2": 12,
"X": 13, "D": 14}
return order[card]
def CompareCardInfo(card):
order = {"3": 0, "4": 1, "5": 2, "6": 3, "7": 4, "8": 5, "9": 6, "T": 7, "J": 8, "Q": 9, "K": 10, "A": 11, "2": 12,
"X": 13, "D": 14}
return order[card[0]]
def CompareCards(cards1, cards2):
if len(cards1) != len(cards2):
return False
cards1.sort(key=CompareCard)
cards2.sort(key=CompareCard)
for i in range(0, len(cards1)):
if cards1[i] != cards2[i]:
return False
return True
def GetListDifference(l1, l2):
temp1 = []
temp1.extend(l1)
temp2 = []
temp2.extend(l2)
for i in l2:
if i in temp1:
temp1.remove(i)
for i in l1:
if i in temp2:
temp2.remove(i)
return temp1, temp2
def FindImage(fromImage, template, threshold=0.9):
w, h, _ = template.shape
fromImage = cv2.cvtColor(np.asarray(fromImage), cv2.COLOR_RGB2BGR)
res = cv2.matchTemplate(fromImage, template, cv2.TM_CCOEFF_NORMED)
loc = np.where(res >= threshold)
points = []
for pt in zip(*loc[::-1]):
points.append(pt)
return points
def GetSingleCard(image, i, sx, sy, sw, sh, checkSelect, Pics):
cardSearchFrom = 0
AllCardsNC = ['rD', 'bX', '2', 'A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3']
currCard = ""
ci = cardSearchFrom
while ci < len(AllCardsNC):
if "r" in AllCardsNC[ci] or "b" in AllCardsNC[ci]:
result = pyautogui.locate(needleImage=Pics["m" + AllCardsNC[ci]], haystackImage=image,
region=(sx + 50 * i, sy - checkSelect * 25, sw, sh), confidence=0.9)
if result is not None:
cardPos = (sx + 50 * i + sw // 2, sy - checkSelect * 25 + sh // 2)
cardSearchFrom = ci
currCard = AllCardsNC[ci][1]
cardInfo = (currCard, cardPos)
return cardInfo
break
else:
outerBreak = False
for card_type in ["r", "b"]:
result = pyautogui.locate(needleImage=Pics["m" + card_type + AllCardsNC[ci]],
haystackImage=image,
region=(sx + 50 * i, sy - checkSelect * 25, sw, sh), confidence=0.9)
if result is not None:
cardPos = (sx + 50 * i + sw // 2, sy - checkSelect * 25 + sh // 2)
cardSearchFrom = ci
currCard = AllCardsNC[ci]
cardInfo = (currCard, cardPos)
outerBreak = True
return cardInfo
break
if outerBreak:
break
if ci == len(AllCardsNC) - 1 and checkSelect == 0:
checkSelect = 1
ci = cardSearchFrom - 1
ci += 1
return None
def RunThreads():
for file in os.listdir("pics"):
info = file.split(".")
if info[1] == "png":
tmpImage = Image.open("pics/" + file)
Pics.update({info[0]: tmpImage})
for ti in range(20):
p = mp.Process(target=GetSingleCardQueue, args=(ReqQueue, ResultQueue, Pics))
p.start()
def LocateOnImage(image, template, region=None, confidence=0.9):
if region is not None:
x, y, w, h = region
imgShape = image.shape
image = image[y:y+h, x:x+w,:]
res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
if (res >= confidence).any():
return True
else:
return None
class GameHelper:
def __init__(self):
self.ScreenZoomRate = 1.25
self.Pics = {}
self.PicsCV = {}
self.Handle = win32gui.FindWindow("Hlddz", None)
self.Interrupt = False
for file in os.listdir("pics"):
info = file.split(".")
if info[1] == "png":
tmpImage = Image.open("pics/" + file)
imgCv = cv2.imread("pics/" + file)
self.Pics.update({info[0]: tmpImage})
self.PicsCV.update({info[0]: imgCv})
def Screenshot(self, region=None): # -> (im, (left, top))
hwnd = self.Handle
# im = Image.open(r"C:\Users\q9294\Desktop\llc.png")
# im = im.resize((1796, 1047))
# return im, (0,0)
left, top, right, bot = win32gui.GetWindowRect(hwnd)
width = right - left
height = bot - top
width = int(width / self.ScreenZoomRate)
height = int(height / self.ScreenZoomRate)
hwndDC = win32gui.GetWindowDC(hwnd)
mfcDC = win32ui.CreateDCFromHandle(hwndDC)
saveDC = mfcDC.CreateCompatibleDC()
saveBitMap = win32ui.CreateBitmap()
saveBitMap.CreateCompatibleBitmap(mfcDC, width, height)
saveDC.SelectObject(saveBitMap)
result = windll.user32.PrintWindow(hwnd, saveDC.GetSafeHdc(), 0)
bmpinfo = saveBitMap.GetInfo()
bmpstr = saveBitMap.GetBitmapBits(True)
im = Image.frombuffer(
"RGB",
(bmpinfo['bmWidth'], bmpinfo['bmHeight']),
bmpstr, 'raw', 'BGRX', 0, 1)
win32gui.DeleteObject(saveBitMap.GetHandle())
saveDC.DeleteDC()
mfcDC.DeleteDC()
win32gui.ReleaseDC(hwnd, hwndDC)
im = im.resize((1796, 1047))
if region is not None:
im = im.crop((region[0], region[1], region[0] + region[2], region[1] + region[3]))
if result:
return im, (left, top)
else:
return None, (0, 0)
def LocateOnScreen(self, templateName, region, confidence=0.9):
image, _ = self.Screenshot()
return pyautogui.locate(needleImage=self.Pics[templateName],
haystackImage=image, region=region, confidence=confidence)
def ClickOnImage(self, templateName, region=None, confidence=0.9):
image, _ = self.Screenshot()
result = pyautogui.locate(needleImage=self.Pics[templateName], haystackImage=image, confidence=confidence, region=region)
if result is not None:
self.LeftClick((result[0],result[1]))
def GetCardsState(self, image):
st = time.time()
states = []
cardStartPos = pyautogui.locate(needleImage=self.Pics["card_edge"], haystackImage=image,
region=(313, 747, 1144, 200), confidence=0.85)
if cardStartPos is None:
return []
sx = cardStartPos[0] + 10
cardSearchFrom = 0
sy, sw, sh = 770, 50, 55
for i in range(0, 20):
haveWhite = pyautogui.locate(needleImage=self.Pics["card_white"], haystackImage=image,
region=(sx + 50 * i, sy, 50, 50), confidence=0.8)
if haveWhite is not None:
break
result = pyautogui.locate(needleImage=self.Pics["card_upper_edge"], haystackImage=image,
region=(sx + 50 * i, 720, sw, 38), confidence=0.9)
checkSelect = 0
if result is not None:
result = pyautogui.locate(needleImage=self.Pics['card_overlap'], haystackImage=image,
region=(sx + 50 * i, 750, sw, 38), confidence=0.85)
if result is None:
checkSelect = 1
states.append(checkSelect)
print("GetStates Costs ", time.time()-st)
return states
def GetCardsMulti(self, image):
st = time.time()
cardStartPos = pyautogui.locate(needleImage=self.Pics["card_edge"], haystackImage=image,
region=(313, 747, 1144, 200), confidence=0.85)
if cardStartPos is None:
return [],[]
sx = cardStartPos[0] + 10
AllCardsNC = ['rD', 'bX', '2', 'A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3']
hand_cards = []
select_map = []
cardSearchFrom = 0
sy, sw, sh = 770, 50, 55
for i in range(0, 20):
haveWhite = pyautogui.locate(needleImage=self.Pics["card_white"], haystackImage=image,
region=(sx + 50 * i, sy, 60, 60), confidence=0.8)
if haveWhite is not None:
break
result = pyautogui.locate(needleImage=self.Pics["card_upper_edge"], haystackImage=image,
region=(sx + 50 * i, 720, sw, 50), confidence=0.9)
checkSelect = 0
if result is not None:
result = pyautogui.locate(needleImage=self.Pics['card_overlap'], haystackImage=image,
region=(sx + 50 * i, 750, sw, 50), confidence=0.85)
if result is None:
checkSelect = 1
select_map.append(checkSelect)
ReqQueue.put((image, i, sx, sy, sw, sh, checkSelect))
QtWidgets.QApplication.processEvents(QEventLoop.AllEvents, 10)
st = time.time()
while len(hand_cards) != len(select_map):
while not ResultQueue.empty():
hand_cards.append(ResultQueue.get())
time.sleep(0.01)
QtWidgets.QApplication.processEvents(QEventLoop.AllEvents, 10)
hand_cards.sort(key=CompareCardInfo, reverse=True)
print("GetCardsMP Costs ", time.time()-st)
return hand_cards, select_map
def GetCards(self, image):
st = time.time()
imgCv = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR)
cardStartPos = pyautogui.locate(needleImage=self.Pics["card_edge"], haystackImage=image,
region=(313, 747, 1144, 200), confidence=0.85)
if cardStartPos is None:
return [],[]
sx = cardStartPos[0] + 10
AllCardsNC = ['rD', 'bX', '2', 'A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3']
hand_cards = []
select_map = []
cardSearchFrom = 0
sy, sw, sh = 770, 50, 55
for i in range(0, 20):
haveWhite = pyautogui.locate(needleImage=self.Pics["card_white"], haystackImage=image,
region=(sx + 50 * i, sy, 60, 60), confidence=0.8)
if haveWhite is not None:
break
result = pyautogui.locate(needleImage=self.Pics["card_upper_edge"], haystackImage=image,
region=(sx + 50 * i, 720, sw, 50), confidence=0.9)
checkSelect = 0
if result is not None:
result = pyautogui.locate(needleImage=self.Pics['card_overlap'], haystackImage=image,
region=(sx + 50 * i, 750, sw, 50), confidence=0.85)
if result is None:
checkSelect = 1
select_map.append(checkSelect)
currCard = ""
ci = cardSearchFrom
while ci < len(AllCardsNC):
if "r" in AllCardsNC[ci] or "b" in AllCardsNC[ci]:
result = LocateOnImage(imgCv, self.PicsCV["m" + AllCardsNC[ci]], region=(sx + 50 * i, sy - checkSelect * 25, sw, sh), confidence=0.91)
# result = pyautogui.locate(needleImage=self.Pics["m" + AllCardsNC[ci]], haystackImage=image,
# region=(sx + 50 * i, sy - checkSelect * 25, sw, sh), confidence=0.9)
if result is not None:
cardPos = (sx + 50 * i + sw // 2, sy - checkSelect * 25 + sh // 2)
cardSearchFrom = ci
currCard = AllCardsNC[ci][1]
cardInfo = (currCard, cardPos)
hand_cards.append(cardInfo)
else:
outerBreak = False
for card_type in ["r", "b"]:
result = LocateOnImage(imgCv, self.PicsCV["m" + card_type + AllCardsNC[ci]], region=(sx + 50 * i, sy - checkSelect * 25, sw, sh), confidence=0.91)
# result = pyautogui.locate(needleImage=self.Pics["m" + card_type + AllCardsNC[ci]],
# haystackImage=image,
# region=(sx + 50 * i, sy - checkSelect * 25, sw, sh), confidence=0.9)
if result is not None:
cardPos = (sx + 50 * i + sw // 2, sy - checkSelect * 25 + sh // 2)
cardSearchFrom = ci
currCard = AllCardsNC[ci]
cardInfo = (currCard, cardPos)
hand_cards.append(cardInfo)
outerBreak = True
break
if outerBreak:
break
if ci == len(AllCardsNC) - 1 and checkSelect == 0:
checkSelect = 1
ci = cardSearchFrom - 1
ci += 1
QtWidgets.QApplication.processEvents(QEventLoop.AllEvents, 10)
print("GetCards Costs ", time.time()-st)
return hand_cards, select_map
def LeftClick(self, pos):
x, y = pos
lParam = win32api.MAKELONG(x, y)
win32gui.PostMessage(self.Handle, WM_MOUSEMOVE, MK_LBUTTON, lParam)
win32gui.PostMessage(self.Handle, WM_LBUTTONDOWN, MK_LBUTTON, lParam)
win32gui.PostMessage(self.Handle, WM_LBUTTONUP, MK_LBUTTON, lParam)
def SelectCards(self, cards):
cards = [card for card in cards]
tobeSelected = []
tobeSelected.extend(cards)
image, windowPos = self.Screenshot()
handCardsInfo, states = self.GetCards(image)
cardSelectMap = []
for card in handCardsInfo:
c = card[0]
if c in tobeSelected:
cardSelectMap.append(1)
tobeSelected.remove(c)
else:
cardSelectMap.append(0)
clickMap = []
handcards = [c[0] for c in handCardsInfo]
for i in range(0, len(cardSelectMap)):
if cardSelectMap[i] == states[i]:
clickMap.append(0)
else:
clickMap.append(1)
while 1 in clickMap:
for i in range(0, len(clickMap)):
if clickMap[i] == 1:
self.LeftClick(handCardsInfo[i][1])
break
time.sleep(0.1)
if self.Interrupt:
break
image, _ = self.Screenshot()
states = self.GetCardsState(image)
clickMap = []
for i in range(0, len(cardSelectMap)):
if cardSelectMap[i] == states[i]:
clickMap.append(0)
else:
clickMap.append(1)
QtWidgets.QApplication.processEvents(QEventLoop.AllEvents, 10)
# for file in os.listdir("pics"):
# info = file.split(".")
# if info[1] == "png":
# tmpImage = Image.open("pics/" + file)
# imgBGR = cv2.imread("pics/" + file)
# Pics.update({info[0]: tmpImage})
if __name__ == "__main__":
mp.freeze_support()
class A:
def __init__(self):
pass
Pics = {}
PicsCV = {}
Handle = win32gui.FindWindow("Hlddz", None)
form = A()
form.MyHandCardsPos = (250, 764, 1141, 70) # 我的截图区域
form.LPlayedCardsPos = (463, 355, 380, 250) # 左边截图区域
form.RPlayedCardsPos = (946, 355, 380, 250) # 右边截图区域
form.LandlordFlagPos = [(1281, 276, 110, 140), (267, 695, 110, 140), (424, 237, 110, 140)] # 地主标志截图区域(右-我-左)
form.ThreeLandlordCardsPos = (753, 32, 287, 136) # 地主底牌截图区域resize成349x168
form.PassBtnPoss = (686, 659, 419, 100)
GameHelper = GameHelper()
# img, _ = GameHelper.Screenshot()
img = Image.open(r"C:\Users\q9294\Desktop\cardselect.png")
img2 = Image.open(r"pics/card_corner.png")
img = img.resize((1796, 1047))
# imgcv = cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)
# st = time.time()
# re = LocateOnImage(imgcv, GameHelper.PicsCV["card_edge"], region=None, confidence=0.999)
# print(re)
# print(time.time()-st)
# st = time.time()
# re = pyautogui.locate(needleImage=GameHelper.Pics["card_edge"], haystackImage=img, confidence=0.9)
# print(re)
# print(time.time()-st)
# st = time.time()
img, _ = GameHelper.Screenshot()
cards, _= GameHelper.GetCards(img)
# cards = "".join([i[0] for i in cards])
print(cards)
print(len(cards))
# et = time.time()
# print(et - st)
# pos2 = pyautogui.locate(needleImage=Pics["card_edge"], haystackImage=img, confidence=0.9)
# pos = FindImage(img, PicsCV["card_corner"], threshold=0.7)
# print(pos)
# print(pos2)
# for p in pos:
# img = DrawRectWithText(img, (p[0], p[1], 50, 50), "p1")
# img = DrawRectWithText(img, (pos2[0], pos2[1], 50, 50), "p2")
# img = DrawRectWithText(img, (sx+50*i, sy-checkSelect*25,sw,sh), c)
# img = DrawRectWithText(img, form.LPlayedCardsPos, "LPlayed")
# img = DrawRectWithText(img, form.RPlayedCardsPos, "RPlayed")
# img = DrawRectWithText(img, form.MyHandCardsPos, "MyCard")
# img = DrawRectWithText(img, form.ThreeLandlordCardsPos, "ThreeLLCPos")
# img = DrawRectWithText(img, form.LandlordFlagPos[0], "RFlag")
# img = DrawRectWithText(img, form.LandlordFlagPos[1], "MyFlag")
# img = DrawRectWithText(img, form.LandlordFlagPos[2], "LFlag")
# img = DrawRectWithText(img, form.PassBtnPoss, "Btns")
# ShowImg(img)
exit()

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

64
LandlordModel.py Normal file
View File

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Created by: Vincentzyx
import os
import torch
from torch import nn
from torch.utils.data import DataLoader
from torch.utils.data.dataset import Dataset
import time
def EnvToOnehot(cards):
Env2IdxMap = {3:0,4:1,5:2,6:3,7:4,8:5,9:6,10:7,11:8,12:9,13:10,14:11,17:12,20:13,30:14}
cards = [Env2IdxMap[i] for i in cards]
Onehot = torch.zeros((4,15))
for i in range(0, 15):
Onehot[:cards.count(i),i] = 1
return Onehot
def RealToOnehot(cards):
RealCard2EnvCard = {'3': 0, '4': 1, '5': 2, '6': 3, '7': 4,
'8': 5, '9': 6, 'T': 7, 'J': 8, 'Q': 9,
'K': 10, 'A': 11, '2': 12, 'X': 13, 'D': 14}
cards = [RealCard2EnvCard[c] for c in cards]
Onehot = torch.zeros((4,15))
for i in range(0, 15):
Onehot[:cards.count(i),i] = 1
return Onehot
class Net(nn.Module):
def __init__(self):
super().__init__()
self.fc1 = nn.Linear(60, 512)
self.fc2 = nn.Linear(512, 512)
self.fc3 = nn.Linear(512, 512)
self.fc4 = nn.Linear(512, 512)
self.fc5 = nn.Linear(512, 512)
self.fc6 = nn.Linear(512, 1)
self.dropout5 = nn.Dropout(0.5)
self.dropout3 = nn.Dropout(0.3)
self.dropout1 = nn.Dropout(0.1)
def forward(self, input):
x = self.fc1(input)
x = torch.relu(self.dropout3(self.fc2(x)))
x = torch.relu(self.dropout5(self.fc3(x)))
x = torch.relu(self.dropout5(self.fc4(x)))
x = torch.relu(self.dropout5(self.fc5(x)))
x = self.fc6(x)
return x
net = Net()
net.eval()
if os.path.exists("landlord_weights.pkl"):
net.load_state_dict(torch.load('landlord_weights.pkl'))
else:
print("landlord_weights.pkl not found")
def predict(cards):
cards_onehot = torch.flatten(RealToOnehot(cards))
y_predict = net(cards_onehot)
return y_predict[0].item() * 100

217
MainWindow.py Normal file
View File

@ -0,0 +1,217 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(703, 421)
font = QtGui.QFont()
font.setFamily("Arial")
font.setPointSize(9)
font.setBold(True)
font.setItalic(False)
font.setWeight(75)
Form.setFont(font)
Form.setWindowOpacity(0.8)
self.WinRate = QtWidgets.QLabel(Form)
self.WinRate.setGeometry(QtCore.QRect(480, 150, 201, 61))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(14)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.WinRate.setFont(font)
self.WinRate.setAlignment(QtCore.Qt.AlignCenter)
self.WinRate.setObjectName("WinRate")
self.InitCard = QtWidgets.QPushButton(Form)
self.InitCard.setGeometry(QtCore.QRect(80, 360, 121, 41))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(14)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.InitCard.setFont(font)
self.InitCard.setStyleSheet("")
self.InitCard.setObjectName("InitCard")
self.UserHandCards = QtWidgets.QLabel(Form)
self.UserHandCards.setGeometry(QtCore.QRect(40, 160, 421, 41))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(14)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.UserHandCards.setFont(font)
self.UserHandCards.setAlignment(QtCore.Qt.AlignCenter)
self.UserHandCards.setObjectName("UserHandCards")
self.LPlayer = QtWidgets.QFrame(Form)
self.LPlayer.setGeometry(QtCore.QRect(20, 80, 201, 61))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(9)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.LPlayer.setFont(font)
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.setFamily("等线")
font.setPointSize(14)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.LPlayedCard.setFont(font)
self.LPlayedCard.setAlignment(QtCore.Qt.AlignCenter)
self.LPlayedCard.setObjectName("LPlayedCard")
self.RPlayer = QtWidgets.QFrame(Form)
self.RPlayer.setGeometry(QtCore.QRect(250, 80, 201, 61))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(16)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
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.setFamily("等线")
font.setPointSize(14)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.RPlayedCard.setFont(font)
self.RPlayedCard.setAlignment(QtCore.Qt.AlignCenter)
self.RPlayedCard.setObjectName("RPlayedCard")
self.Player = QtWidgets.QFrame(Form)
self.Player.setGeometry(QtCore.QRect(480, 80, 201, 61))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(9)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.Player.setFont(font)
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, 201, 61))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(14)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.PredictedCard.setFont(font)
self.PredictedCard.setStyleSheet("")
self.PredictedCard.setFrameShape(QtWidgets.QFrame.Panel)
self.PredictedCard.setLineWidth(1)
self.PredictedCard.setAlignment(QtCore.Qt.AlignCenter)
self.PredictedCard.setObjectName("PredictedCard")
self.ThreeLandlordCards = QtWidgets.QLabel(Form)
self.ThreeLandlordCards.setGeometry(QtCore.QRect(270, 20, 161, 41))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(16)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.ThreeLandlordCards.setFont(font)
self.ThreeLandlordCards.setAlignment(QtCore.Qt.AlignCenter)
self.ThreeLandlordCards.setObjectName("ThreeLandlordCards")
self.Stop = QtWidgets.QPushButton(Form)
self.Stop.setGeometry(QtCore.QRect(230, 360, 111, 41))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(14)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.Stop.setFont(font)
self.Stop.setStyleSheet("")
self.Stop.setObjectName("Stop")
self.SwitchMode = QtWidgets.QPushButton(Form)
self.SwitchMode.setGeometry(QtCore.QRect(370, 360, 121, 41))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(14)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.SwitchMode.setFont(font)
self.SwitchMode.setStyleSheet("")
self.SwitchMode.setObjectName("SwitchMode")
self.AutoStart = QtWidgets.QPushButton(Form)
self.AutoStart.setGeometry(QtCore.QRect(520, 360, 111, 41))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(14)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.AutoStart.setFont(font)
self.AutoStart.setStyleSheet("")
self.AutoStart.setObjectName("AutoStart")
self.BidWinrate = QtWidgets.QLabel(Form)
self.BidWinrate.setGeometry(QtCore.QRect(50, 220, 241, 41))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(12)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.BidWinrate.setFont(font)
self.BidWinrate.setObjectName("BidWinrate")
self.PreWinrate = QtWidgets.QLabel(Form)
self.PreWinrate.setGeometry(QtCore.QRect(50, 270, 241, 41))
font = QtGui.QFont()
font.setFamily("等线")
font.setPointSize(12)
font.setBold(False)
font.setItalic(False)
font.setWeight(50)
self.PreWinrate.setFont(font)
self.PreWinrate.setObjectName("PreWinrate")
self.retranslateUi(Form)
self.InitCard.clicked.connect(Form.init_cards)
self.Stop.clicked.connect(Form.stop)
self.SwitchMode.clicked.connect(Form.switch_mode)
self.AutoStart.clicked.connect(Form.beforeStart)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Hi"))
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", "停止"))
self.SwitchMode.setText(_translate("Form", "单局"))
self.AutoStart.setText(_translate("Form", "自动开始"))
self.BidWinrate.setText(_translate("Form", "叫牌预估胜率:"))
self.PreWinrate.setText(_translate("Form", "局前预估胜率:"))

481
MainWindow.ui Normal file
View File

@ -0,0 +1,481 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>703</width>
<height>421</height>
</rect>
</property>
<property name="font">
<font>
<family>Arial</family>
<pointsize>9</pointsize>
<weight>75</weight>
<italic>false</italic>
<bold>true</bold>
</font>
</property>
<property name="windowTitle">
<string>Hi</string>
</property>
<property name="windowOpacity">
<double>0.800000000000000</double>
</property>
<widget class="QLabel" name="WinRate">
<property name="geometry">
<rect>
<x>480</x>
<y>150</y>
<width>201</width>
<height>61</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>14</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>评分</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
<widget class="QPushButton" name="InitCard">
<property name="geometry">
<rect>
<x>80</x>
<y>360</y>
<width>121</width>
<height>41</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>14</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="text">
<string>开始</string>
</property>
</widget>
<widget class="QLabel" name="UserHandCards">
<property name="geometry">
<rect>
<x>40</x>
<y>160</y>
<width>421</width>
<height>41</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>14</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>手牌</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
<widget class="QFrame" name="LPlayer">
<property name="geometry">
<rect>
<x>20</x>
<y>80</y>
<width>201</width>
<height>61</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>9</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<widget class="QLabel" name="LPlayedCard">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>201</width>
<height>61</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>14</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>上家出牌区域</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</widget>
<widget class="QFrame" name="RPlayer">
<property name="geometry">
<rect>
<x>250</x>
<y>80</y>
<width>201</width>
<height>61</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>16</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<widget class="QLabel" name="RPlayedCard">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>201</width>
<height>61</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>14</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>下家出牌区域</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</widget>
<widget class="QFrame" name="Player">
<property name="geometry">
<rect>
<x>480</x>
<y>80</y>
<width>201</width>
<height>61</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>9</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<widget class="QLabel" name="PredictedCard">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>201</width>
<height>61</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>14</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="lineWidth">
<number>1</number>
</property>
<property name="text">
<string>AI出牌区域</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</widget>
<widget class="QLabel" name="ThreeLandlordCards">
<property name="geometry">
<rect>
<x>270</x>
<y>20</y>
<width>161</width>
<height>41</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>16</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>地主牌</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
<widget class="QPushButton" name="Stop">
<property name="geometry">
<rect>
<x>230</x>
<y>360</y>
<width>111</width>
<height>41</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>14</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="text">
<string>停止</string>
</property>
</widget>
<widget class="QPushButton" name="SwitchMode">
<property name="geometry">
<rect>
<x>370</x>
<y>360</y>
<width>121</width>
<height>41</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>14</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="text">
<string>单局</string>
</property>
</widget>
<widget class="QPushButton" name="AutoStart">
<property name="geometry">
<rect>
<x>520</x>
<y>360</y>
<width>111</width>
<height>41</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>14</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="text">
<string>自动开始</string>
</property>
</widget>
<widget class="QLabel" name="BidWinrate">
<property name="geometry">
<rect>
<x>50</x>
<y>220</y>
<width>241</width>
<height>41</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>12</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>叫牌预估胜率:</string>
</property>
</widget>
<widget class="QLabel" name="PreWinrate">
<property name="geometry">
<rect>
<x>50</x>
<y>270</y>
<width>241</width>
<height>41</height>
</rect>
</property>
<property name="font">
<font>
<family>等线</family>
<pointsize>12</pointsize>
<weight>50</weight>
<italic>false</italic>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>局前预估胜率:</string>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>InitCard</sender>
<signal>clicked()</signal>
<receiver>Form</receiver>
<slot>init_cards()</slot>
<hints>
<hint type="sourcelabel">
<x>200</x>
<y>360</y>
</hint>
<hint type="destinationlabel">
<x>250</x>
<y>292</y>
</hint>
</hints>
</connection>
<connection>
<sender>Stop</sender>
<signal>clicked()</signal>
<receiver>Form</receiver>
<slot>stop()</slot>
<hints>
<hint type="sourcelabel">
<x>230</x>
<y>360</y>
</hint>
<hint type="destinationlabel">
<x>233</x>
<y>220</y>
</hint>
</hints>
</connection>
<connection>
<sender>SwitchMode</sender>
<signal>clicked()</signal>
<receiver>Form</receiver>
<slot>switch_mode()</slot>
<hints>
<hint type="sourcelabel">
<x>472</x>
<y>366</y>
</hint>
<hint type="destinationlabel">
<x>480</x>
<y>277</y>
</hint>
</hints>
</connection>
<connection>
<sender>AutoStart</sender>
<signal>clicked()</signal>
<receiver>Form</receiver>
<slot>beforeStart()</slot>
<hints>
<hint type="sourcelabel">
<x>613</x>
<y>372</y>
</hint>
<hint type="destinationlabel">
<x>646</x>
<y>291</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>init_cards()</slot>
<slot>start()</slot>
<slot>stop()</slot>
<slot>switch_mode()</slot>
<slot>beforeStart()</slot>
</slots>
</ui>

147
MainWindowUI.py Normal file
View File

@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'MainWindow.ui'
#
# Created by: PyQt5 UI code generator 5.13.0
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(440, 450)
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.SwitchMode = QtWidgets.QPushButton(Form)
self.SwitchMode.setGeometry(QtCore.QRect(60, 380, 121, 41))
font = QtGui.QFont()
font.setFamily("Arial")
font.setPointSize(14)
font.setBold(True)
font.setWeight(75)
self.SwitchMode.setFont(font)
self.SwitchMode.setStyleSheet("")
self.SwitchMode.setObjectName("SwitchMode")
self.AutoStart = QtWidgets.QPushButton(Form)
self.AutoStart.setGeometry(QtCore.QRect(260, 380, 111, 41))
font = QtGui.QFont()
font.setFamily("Arial")
font.setPointSize(14)
font.setBold(True)
font.setWeight(75)
self.AutoStart.setFont(font)
self.AutoStart.setStyleSheet("")
self.AutoStart.setObjectName("AutoStart")
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)
self.SwitchMode.clicked.connect(Form.switch_mode)
self.AutoStart.clicked.connect(Form.beforeStart)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
_translate = QtCore.QCoreApplication.translate
Form.setWindowTitle(_translate("Form", "Hi"))
self.WinRate.setText(_translate("Form", ""))
self.InitCard.setText(_translate("Form", "开始"))
self.SwitchMode.setText(_translate("Form", "单局"))
self.AutoStart.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", "停止"))

21
README.md Normal file
View File

@ -0,0 +1,21 @@
# DouZero_For_HLDDZ_FullAuto: 将DouZero用于欢乐斗地主自动化
* 本项目基于[DouZero](https://github.com/kwai/DouZero) 和 [DouZero_For_Happy_DouDiZhu](https://github.com/tianqiraf/DouZero_For_HappyDouDiZhu)
* 环境配置请移步项目DouZero
* 模型默认为ADP更换模型请修改main.py中的模型路径
* 运行main.py即可
* 在原 [DouZero_For_Happy_DouDiZhu](https://github.com/tianqiraf/DouZero_For_HappyDouDiZhu) 的基础上加入了自动出牌,基于手牌自动叫牌,加倍,同时修改截屏方式为窗口区域截屏,游戏原窗口遮挡不影响游戏进行。
* **请勿把游戏界面最小化,否则无法使用**
## 说明
* 欢乐斗地主使用窗口模式运行
* **本项目仅供学习以及技术交流,请勿用于其它目的,否则后果自负。**
## 使用步骤
1. 点击游戏中开始游戏后点击程序的`自动开始`
## 潜在Bug
* 有较低几率把出牌识别为不出,从而卡在自己出牌阶段。
## 鸣谢
* 本项目基于[DouZero](https://github.com/kwai/DouZero) [DouZero_For_Happy_DouDiZhu](https://github.com/tianqiraf/DouZero_For_HappyDouDiZhu)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

BIN
baselines/sl/landlord.ckpt Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
bid_weights.pkl Normal file

Binary file not shown.

0
douzero/__init__.py Normal file
View File

2
douzero/dmc/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from .dmc import train
from .arguments import parser

53
douzero/dmc/arguments.py Normal file
View File

@ -0,0 +1,53 @@
import argparse
parser = argparse.ArgumentParser(description='DouZero: PyTorch DouDizhu AI')
# General Settings
parser.add_argument('--xpid', default='douzero',
help='Experiment id (default: douzero)')
parser.add_argument('--save_interval', default=30, type=int,
help='Time interval (in minutes) at which to save the model')
parser.add_argument('--objective', default='adp', type=str, choices=['adp', 'wp'],
help='Use ADP or WP as reward (default: ADP)')
# Training settings
parser.add_argument('--gpu_devices', default='0', type=str,
help='Which GPUs to be used for training')
parser.add_argument('--num_actor_devices', default=1, type=int,
help='The number of devices used for simulation')
parser.add_argument('--num_actors', default=5, type=int,
help='The number of actors for each simulation device')
parser.add_argument('--training_device', default=0, type=int,
help='The index of the GPU used for training models')
parser.add_argument('--load_model', action='store_true',
help='Load an existing model')
parser.add_argument('--disable_checkpoint', action='store_true',
help='Disable saving checkpoint')
parser.add_argument('--savedir', default='douzero_checkpoints',
help='Root dir where experiment data will be saved')
# Hyperparameters
parser.add_argument('--total_frames', default=100000000000, type=int,
help='Total environment frames to train for')
parser.add_argument('--exp_epsilon', default=0.01, type=float,
help='The probability for exploration')
parser.add_argument('--batch_size', default=32, type=int,
help='Learner batch size')
parser.add_argument('--unroll_length', default=100, type=int,
help='The unroll length (time dimension)')
parser.add_argument('--num_buffers', default=50, type=int,
help='Number of shared-memory buffers')
parser.add_argument('--num_threads', default=4, type=int,
help='Number learner threads')
parser.add_argument('--max_grad_norm', default=40., type=float,
help='Max norm of gradients')
# Optimizer settings
parser.add_argument('--learning_rate', default=0.0001, type=float,
help='Learning rate')
parser.add_argument('--alpha', default=0.99, type=float,
help='RMSProp smoothing constant')
parser.add_argument('--momentum', default=0, type=float,
help='RMSProp momentum')
parser.add_argument('--epsilon', default=1e-5, type=float,
help='RMSProp epsilon')

231
douzero/dmc/dmc.py Normal file
View File

@ -0,0 +1,231 @@
import os
import threading
import time
import timeit
import pprint
from collections import deque
import torch
from torch import multiprocessing as mp
from torch import nn
from .file_writer import FileWriter
from .models import Model
from .utils import get_batch, log, create_env, create_buffers, create_optimizers, act
mean_episode_return_buf = {p:deque(maxlen=100) for p in ['landlord', 'landlord_up', 'landlord_down']}
def compute_loss(logits, targets):
loss = ((logits.squeeze(-1) - targets)**2).mean()
return loss
def learn(position,
actor_models,
model,
batch,
optimizer,
flags,
lock):
"""Performs a learning (optimization) step."""
device = torch.device('cuda:'+str(flags.training_device))
obs_x_no_action = batch['obs_x_no_action'].to(device)
obs_action = batch['obs_action'].to(device)
obs_x = torch.cat((obs_x_no_action, obs_action), dim=2).float()
obs_x = torch.flatten(obs_x, 0, 1)
obs_z = torch.flatten(batch['obs_z'].to(device), 0, 1).float()
target = torch.flatten(batch['target'].to(device), 0, 1)
episode_returns = batch['episode_return'][batch['done']]
mean_episode_return_buf[position].append(torch.mean(episode_returns).to(device))
with lock:
learner_outputs = model(obs_z, obs_x, return_value=True)
loss = compute_loss(learner_outputs['values'], target)
stats = {
'mean_episode_return_'+position: torch.mean(torch.stack([_r for _r in mean_episode_return_buf[position]])).item(),
'loss_'+position: loss.item(),
}
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), flags.max_grad_norm)
optimizer.step()
for actor_model in actor_models:
actor_model.get_model(position).load_state_dict(model.state_dict())
return stats
def train(flags):
"""
This is the main funtion for training. It will first
initilize everything, such as buffers, optimizers, etc.
Then it will start subprocesses as actors. Then, it will call
learning function with multiple threads.
"""
plogger = FileWriter(
xpid=flags.xpid,
xp_args=flags.__dict__,
rootdir=flags.savedir,
)
checkpointpath = os.path.expandvars(
os.path.expanduser('%s/%s/%s' % (flags.savedir, flags.xpid, 'model.tar')))
T = flags.unroll_length
B = flags.batch_size
# Initialize actor models
models = []
assert flags.num_actor_devices <= len(flags.gpu_devices.split(',')), 'The number of actor devices can not exceed the number of available devices'
for device in range(flags.num_actor_devices):
model = Model(device=device)
model.share_memory()
model.eval()
models.append(model)
# Initialize buffers
buffers = create_buffers(flags)
# Initialize queues
actor_processes = []
ctx = mp.get_context('spawn')
free_queue = []
full_queue = []
for device in range(flags.num_actor_devices):
_free_queue = {'landlord': ctx.SimpleQueue(), 'landlord_up': ctx.SimpleQueue(), 'landlord_down': ctx.SimpleQueue()}
_full_queue = {'landlord': ctx.SimpleQueue(), 'landlord_up': ctx.SimpleQueue(), 'landlord_down': ctx.SimpleQueue()}
free_queue.append(_free_queue)
full_queue.append(_full_queue)
# Learner model for training
learner_model = Model(device=flags.training_device)
# Create optimizers
optimizers = create_optimizers(flags, learner_model)
# Stat Keys
stat_keys = [
'mean_episode_return_landlord',
'loss_landlord',
'mean_episode_return_landlord_up',
'loss_landlord_up',
'mean_episode_return_landlord_down',
'loss_landlord_down',
]
frames, stats = 0, {k: 0 for k in stat_keys}
position_frames = {'landlord':0, 'landlord_up':0, 'landlord_down':0}
# Load models if any
if flags.load_model and os.path.exists(checkpointpath):
checkpoint_states = torch.load(
checkpointpath, map_location="cuda:"+str(flags.training_device)
)
for k in ['landlord', 'landlord_up', 'landlord_down']:
learner_model.get_model(k).load_state_dict(checkpoint_states["model_state_dict"][k])
optimizers[k].load_state_dict(checkpoint_states["optimizer_state_dict"][k])
for device in range(flags.num_actor_devices):
models[device].get_model(k).load_state_dict(learner_model.get_model(k).state_dict())
stats = checkpoint_states["stats"]
frames = checkpoint_states["frames"]
position_frames = checkpoint_states["position_frames"]
log.info(f"Resuming preempted job, current stats:\n{stats}")
# Starting actor processes
for device in range(flags.num_actor_devices):
num_actors = flags.num_actors
for i in range(flags.num_actors):
actor = ctx.Process(
target=act,
args=(i, device, free_queue[device], full_queue[device], models[device], buffers[device], flags))
actor.start()
actor_processes.append(actor)
def batch_and_learn(i, device, position, local_lock, position_lock, lock=threading.Lock()):
"""Thread target for the learning process."""
nonlocal frames, position_frames, stats
while frames < flags.total_frames:
batch = get_batch(free_queue[device][position], full_queue[device][position], buffers[device][position], flags, local_lock)
_stats = learn(position, models, learner_model.get_model(position), batch,
optimizers[position], flags, position_lock)
with lock:
for k in _stats:
stats[k] = _stats[k]
to_log = dict(frames=frames)
to_log.update({k: stats[k] for k in stat_keys})
plogger.log(to_log)
frames += T * B
position_frames[position] += T * B
for device in range(flags.num_actor_devices):
for m in range(flags.num_buffers):
free_queue[device]['landlord'].put(m)
free_queue[device]['landlord_up'].put(m)
free_queue[device]['landlord_down'].put(m)
threads = []
locks = [{'landlord': threading.Lock(), 'landlord_up': threading.Lock(), 'landlord_down': threading.Lock()} for _ in range(flags.num_actor_devices)]
position_locks = {'landlord': threading.Lock(), 'landlord_up': threading.Lock(), 'landlord_down': threading.Lock()}
for device in range(flags.num_actor_devices):
for i in range(flags.num_threads):
for position in ['landlord', 'landlord_up', 'landlord_down']:
thread = threading.Thread(
target=batch_and_learn, name='batch-and-learn-%d' % i, args=(i,device,position,locks[device][position],position_locks[position]))
thread.start()
threads.append(thread)
def checkpoint(frames):
if flags.disable_checkpoint:
return
log.info('Saving checkpoint to %s', checkpointpath)
_models = learner_model.get_models()
torch.save({
'model_state_dict': {k: _models[k].state_dict() for k in _models},
'optimizer_state_dict': {k: optimizers[k].state_dict() for k in optimizers},
"stats": stats,
'flags': vars(flags),
'frames': frames,
'position_frames': position_frames
}, checkpointpath)
# Save the weights for evaluation purpose
for position in ['landlord', 'landlord_up', 'landlord_down']:
model_weights_dir = os.path.expandvars(os.path.expanduser(
'%s/%s/%s' % (flags.savedir, flags.xpid, position+'_weights_'+str(frames)+'.ckpt')))
torch.save(learner_model.get_model(position).state_dict(), model_weights_dir)
timer = timeit.default_timer
try:
last_checkpoint_time = timer() - flags.save_interval * 60
while frames < flags.total_frames:
start_frames = frames
position_start_frames = {k: position_frames[k] for k in position_frames}
start_time = timer()
time.sleep(5)
if timer() - last_checkpoint_time > flags.save_interval * 60:
checkpoint(frames)
last_checkpoint_time = timer()
end_time = timer()
fps = (frames - start_frames) / (end_time - start_time)
position_fps = {k:(position_frames[k]-position_start_frames[k])/(end_time-start_time) for k in position_frames}
log.info('After %i (L:%i U:%i D:%i) frames: @ %.1f fps (L:%.1f U:%.1f D:%.1f) Stats:\n%s',
frames,
position_frames['landlord'],
position_frames['landlord_up'],
position_frames['landlord_down'],
fps,
position_fps['landlord'],
position_fps['landlord_up'],
position_fps['landlord_down'],
pprint.pformat(stats))
except KeyboardInterrupt:
return
else:
for thread in threads:
thread.join()
log.info('Learning finished after %d frames.', frames)
checkpoint(frames)
plogger.close()

69
douzero/dmc/env_utils.py Normal file
View File

@ -0,0 +1,69 @@
"""
Here, we wrap the original environment to make it easier
to use. When a game is finished, instead of mannualy reseting
the environment, we do it automatically.
"""
import numpy as np
import torch
def _format_observation(obs, device):
"""
A utility function to process observations and
move them to CUDA.
"""
position = obs['position']
device = torch.device('cuda:'+str(device))
x_batch = torch.from_numpy(obs['x_batch']).to(device)
z_batch = torch.from_numpy(obs['z_batch']).to(device)
x_no_action = torch.from_numpy(obs['x_no_action'])
z = torch.from_numpy(obs['z'])
obs = {'x_batch': x_batch,
'z_batch': z_batch,
'legal_actions': obs['legal_actions'],
}
return position, obs, x_no_action, z
class Environment:
def __init__(self, env, device):
""" Initialzie this environment wrapper
"""
self.env = env
self.device = device
self.episode_return = None
def initial(self):
initial_position, initial_obs, x_no_action, z = _format_observation(self.env.reset(), self.device)
initial_reward = torch.zeros(1, 1)
self.episode_return = torch.zeros(1, 1)
initial_done = torch.ones(1, 1, dtype=torch.bool)
return initial_position, initial_obs, dict(
done=initial_done,
episode_return=self.episode_return,
obs_x_no_action=x_no_action,
obs_z=z,
)
def step(self, action):
obs, reward, done, _ = self.env.step(action)
self.episode_return += reward
episode_return = self.episode_return
if done:
obs = self.env.reset()
self.episode_return = torch.zeros(1, 1)
position, obs, x_no_action, z = _format_observation(obs, self.device)
reward = torch.tensor(reward).view(1, 1)
done = torch.tensor(done).view(1, 1)
return position, obs, dict(
done=done,
episode_return=episode_return,
obs_x_no_action=x_no_action,
obs_z=z,
)
def close(self):
self.env.close()

187
douzero/dmc/file_writer.py Normal file
View File

@ -0,0 +1,187 @@
# Copyright (c) Facebook, Inc. and its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import datetime
import csv
import json
import logging
import os
import time
from typing import Dict
# import git
# def gather_metadata() -> Dict:
# date_start = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')
# # gathering git metadata
# try:
# repo = git.Repo(search_parent_directories=True)
# git_sha = repo.commit().hexsha
# git_data = dict(
# commit=git_sha,
# branch=repo.active_branch.name,
# is_dirty=repo.is_dirty(),
# path=repo.git_dir,
# )
# except git.InvalidGitRepositoryError:
# git_data = None
# # gathering slurm metadata
# if 'SLURM_JOB_ID' in os.environ:
# slurm_env_keys = [k for k in os.environ if k.startswith('SLURM')]
# slurm_data = {}
# for k in slurm_env_keys:
# d_key = k.replace('SLURM_', '').replace('SLURMD_', '').lower()
# slurm_data[d_key] = os.environ[k]
# else:
# slurm_data = None
# return dict(
# date_start=date_start,
# date_end=None,
# successful=False,
# git=git_data,
# slurm=slurm_data,
# env=os.environ.copy(),
# )
class FileWriter:
def __init__(self,
xpid: str = None,
xp_args: dict = None,
rootdir: str = '~/palaas'):
if not xpid:
# make unique id
xpid = '{proc}_{unixtime}'.format(
proc=os.getpid(), unixtime=int(time.time()))
self.xpid = xpid
self._tick = 0
# metadata gathering
if xp_args is None:
xp_args = {}
self.metadata = gather_metadata()
# we need to copy the args, otherwise when we close the file writer
# (and rewrite the args) we might have non-serializable objects (or
# other nasty stuff).
self.metadata['args'] = copy.deepcopy(xp_args)
self.metadata['xpid'] = self.xpid
formatter = logging.Formatter('%(message)s')
self._logger = logging.getLogger('palaas/out')
# to stdout handler
shandle = logging.StreamHandler()
shandle.setFormatter(formatter)
self._logger.addHandler(shandle)
self._logger.setLevel(logging.INFO)
rootdir = os.path.expandvars(os.path.expanduser(rootdir))
# to file handler
self.basepath = os.path.join(rootdir, self.xpid)
if not os.path.exists(self.basepath):
self._logger.info('Creating log directory: %s', self.basepath)
os.makedirs(self.basepath, exist_ok=True)
else:
self._logger.info('Found log directory: %s', self.basepath)
# NOTE: remove latest because it creates errors when running on slurm
# multiple jobs trying to write to latest but cannot find it
# Add 'latest' as symlink unless it exists and is no symlink.
# symlink = os.path.join(rootdir, 'latest')
# if os.path.islink(symlink):
# os.remove(symlink)
# if not os.path.exists(symlink):
# os.symlink(self.basepath, symlink)
# self._logger.info('Symlinked log directory: %s', symlink)
self.paths = dict(
msg='{base}/out.log'.format(base=self.basepath),
logs='{base}/logs.csv'.format(base=self.basepath),
fields='{base}/fields.csv'.format(base=self.basepath),
meta='{base}/meta.json'.format(base=self.basepath),
)
self._logger.info('Saving arguments to %s', self.paths['meta'])
if os.path.exists(self.paths['meta']):
self._logger.warning('Path to meta file already exists. '
'Not overriding meta.')
else:
self._save_metadata()
self._logger.info('Saving messages to %s', self.paths['msg'])
if os.path.exists(self.paths['msg']):
self._logger.warning('Path to message file already exists. '
'New data will be appended.')
fhandle = logging.FileHandler(self.paths['msg'])
fhandle.setFormatter(formatter)
self._logger.addHandler(fhandle)
self._logger.info('Saving logs data to %s', self.paths['logs'])
self._logger.info('Saving logs\' fields to %s', self.paths['fields'])
if os.path.exists(self.paths['logs']):
self._logger.warning('Path to log file already exists. '
'New data will be appended.')
with open(self.paths['fields'], 'r') as csvfile:
reader = csv.reader(csvfile)
self.fieldnames = list(reader)[0]
else:
self.fieldnames = ['_tick', '_time']
def log(self, to_log: Dict, tick: int = None,
verbose: bool = False) -> None:
if tick is not None:
raise NotImplementedError
else:
to_log['_tick'] = self._tick
self._tick += 1
to_log['_time'] = time.time()
old_len = len(self.fieldnames)
for k in to_log:
if k not in self.fieldnames:
self.fieldnames.append(k)
if old_len != len(self.fieldnames):
with open(self.paths['fields'], 'w') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(self.fieldnames)
self._logger.info('Updated log fields: %s', self.fieldnames)
if to_log['_tick'] == 0:
# print("\ncreating logs file ")
with open(self.paths['logs'], 'a') as f:
f.write('# %s\n' % ','.join(self.fieldnames))
if verbose:
self._logger.info('LOG | %s', ', '.join(
['{}: {}'.format(k, to_log[k]) for k in sorted(to_log)]))
with open(self.paths['logs'], 'a') as f:
writer = csv.DictWriter(f, fieldnames=self.fieldnames)
writer.writerow(to_log)
# print("\nadded to log file")
def close(self, successful: bool = True) -> None:
self.metadata['date_end'] = datetime.datetime.now().strftime(
'%Y-%m-%d %H:%M:%S.%f')
self.metadata['successful'] = successful
self._save_metadata()
def _save_metadata(self) -> None:
with open(self.paths['meta'], 'w') as jsonfile:
json.dump(self.metadata, jsonfile, indent=4, sort_keys=True)

119
douzero/dmc/models.py Normal file
View File

@ -0,0 +1,119 @@
"""
This file includes the torch models. We wrap the three
models into one class for convenience.
"""
import numpy as np
import torch
from torch import nn
class LandlordLstmModel(nn.Module):
def __init__(self):
super().__init__()
self.lstm = nn.LSTM(162, 128, batch_first=True)
self.dense1 = nn.Linear(373 + 128, 512)
self.dense2 = nn.Linear(512, 512)
self.dense3 = nn.Linear(512, 512)
self.dense4 = nn.Linear(512, 512)
self.dense5 = nn.Linear(512, 512)
self.dense6 = nn.Linear(512, 1)
def forward(self, z, x, return_value=False, flags=None):
lstm_out, (h_n, _) = self.lstm(z)
lstm_out = lstm_out[:,-1,:]
x = torch.cat([lstm_out,x], dim=-1)
x = self.dense1(x)
x = torch.relu(x)
x = self.dense2(x)
x = torch.relu(x)
x = self.dense3(x)
x = torch.relu(x)
x = self.dense4(x)
x = torch.relu(x)
x = self.dense5(x)
x = torch.relu(x)
x = self.dense6(x)
if return_value:
return dict(values=x)
else:
if flags is not None and flags.exp_epsilon > 0 and np.random.rand() < flags.exp_epsilon:
action = torch.randint(x.shape[0], (1,))[0]
else:
action = torch.argmax(x,dim=0)[0]
return dict(action=action)
class FarmerLstmModel(nn.Module):
def __init__(self):
super().__init__()
self.lstm = nn.LSTM(162, 128, batch_first=True)
self.dense1 = nn.Linear(484 + 128, 512)
self.dense2 = nn.Linear(512, 512)
self.dense3 = nn.Linear(512, 512)
self.dense4 = nn.Linear(512, 512)
self.dense5 = nn.Linear(512, 512)
self.dense6 = nn.Linear(512, 1)
def forward(self, z, x, return_value=False, flags=None):
lstm_out, (h_n, _) = self.lstm(z)
lstm_out = lstm_out[:,-1,:]
x = torch.cat([lstm_out,x], dim=-1)
x = self.dense1(x)
x = torch.relu(x)
x = self.dense2(x)
x = torch.relu(x)
x = self.dense3(x)
x = torch.relu(x)
x = self.dense4(x)
x = torch.relu(x)
x = self.dense5(x)
x = torch.relu(x)
x = self.dense6(x)
if return_value:
return dict(values=x)
else:
if flags is not None and flags.exp_epsilon > 0 and np.random.rand() < flags.exp_epsilon:
action = torch.randint(x.shape[0], (1,))[0]
else:
action = torch.argmax(x,dim=0)[0]
return dict(action=action)
# Model dict is only used in evaluation but not training
model_dict = {}
model_dict['landlord'] = LandlordLstmModel
model_dict['landlord_up'] = FarmerLstmModel
model_dict['landlord_down'] = FarmerLstmModel
class Model:
"""
The wrapper for the three models. We also wrap several
interfaces such as share_memory, eval, etc.
"""
def __init__(self, device=0):
self.models = {}
self.models['landlord'] = LandlordLstmModel().to(torch.device('cuda:'+str(device)))
self.models['landlord_up'] = FarmerLstmModel().to(torch.device('cuda:'+str(device)))
self.models['landlord_down'] = FarmerLstmModel().to(torch.device('cuda:'+str(device)))
def forward(self, position, z, x, training=False, flags=None):
model = self.models[position]
return model.forward(z, x, training, flags)
def share_memory(self):
self.models['landlord'].share_memory()
self.models['landlord_up'].share_memory()
self.models['landlord_down'].share_memory()
def eval(self):
self.models['landlord'].eval()
self.models['landlord_up'].eval()
self.models['landlord_down'].eval()
def parameters(self, position):
return self.models[position].parameters()
def get_model(self, position):
return self.models[position]
def get_models(self):
return self.models

205
douzero/dmc/utils.py Normal file
View File

@ -0,0 +1,205 @@
import os
import typing
import logging
import traceback
import numpy as np
from collections import Counter
import time
import torch
from torch import multiprocessing as mp
from .env_utils import Environment
from douzero.env import Env
Card2Column = {3: 0, 4: 1, 5: 2, 6: 3, 7: 4, 8: 5, 9: 6, 10: 7,
11: 8, 12: 9, 13: 10, 14: 11, 17: 12}
NumOnes2Array = {0: np.array([0, 0, 0, 0]),
1: np.array([1, 0, 0, 0]),
2: np.array([1, 1, 0, 0]),
3: np.array([1, 1, 1, 0]),
4: np.array([1, 1, 1, 1])}
shandle = logging.StreamHandler()
shandle.setFormatter(
logging.Formatter(
'[%(levelname)s:%(process)d %(module)s:%(lineno)d %(asctime)s] '
'%(message)s'))
log = logging.getLogger('doudzero')
log.propagate = False
log.addHandler(shandle)
log.setLevel(logging.INFO)
# Buffers are used to transfer data between actor processes
# and learner processes. They are shared tensors in GPU
Buffers = typing.Dict[str, typing.List[torch.Tensor]]
def create_env(flags):
return Env(flags.objective)
def get_batch(free_queue,
full_queue,
buffers,
flags,
lock):
"""
This function will sample a batch from the buffers based
on the indices received from the full queue. It will also
free the indices by sending it to full_queue.
"""
with lock:
indices = [full_queue.get() for _ in range(flags.batch_size)]
batch = {
key: torch.stack([buffers[key][m] for m in indices], dim=1)
for key in buffers
}
for m in indices:
free_queue.put(m)
return batch
def create_optimizers(flags, learner_model):
"""
Create three optimizers for the three positions
"""
positions = ['landlord', 'landlord_up', 'landlord_down']
optimizers = {}
for position in positions:
optimizer = torch.optim.RMSprop(
learner_model.parameters(position),
lr=flags.learning_rate,
momentum=flags.momentum,
eps=flags.epsilon,
alpha=flags.alpha)
optimizers[position] = optimizer
return optimizers
def create_buffers(flags):
"""
We create buffers for different positions as well as
for different devices (i.e., GPU). That is, each device
will have three buffers for the three positions.
"""
T = flags.unroll_length
positions = ['landlord', 'landlord_up', 'landlord_down']
buffers = []
for device in range(torch.cuda.device_count()):
buffers.append({})
for position in positions:
x_dim = 319 if position == 'landlord' else 430
specs = dict(
done=dict(size=(T,), dtype=torch.bool),
episode_return=dict(size=(T,), dtype=torch.float32),
target=dict(size=(T,), dtype=torch.float32),
obs_x_no_action=dict(size=(T, x_dim), dtype=torch.int8),
obs_action=dict(size=(T, 54), dtype=torch.int8),
obs_z=dict(size=(T, 5, 162), dtype=torch.int8),
)
_buffers: Buffers = {key: [] for key in specs}
for _ in range(flags.num_buffers):
for key in _buffers:
_buffer = torch.empty(**specs[key]).to(torch.device('cuda:'+str(device))).share_memory_()
_buffers[key].append(_buffer)
buffers[device][position] = _buffers
return buffers
def act(i, device, free_queue, full_queue, model, buffers, flags):
"""
This function will run forever until we stop it. It will generate
data from the environment and send the data to buffer. It uses
a free queue and full queue to syncup with the main process.
"""
positions = ['landlord', 'landlord_up', 'landlord_down']
try:
T = flags.unroll_length
log.info('Device %i Actor %i started.', device, i)
env = create_env(flags)
env = Environment(env, device)
done_buf = {p: [] for p in positions}
episode_return_buf = {p: [] for p in positions}
target_buf = {p: [] for p in positions}
obs_x_no_action_buf = {p: [] for p in positions}
obs_action_buf = {p: [] for p in positions}
obs_z_buf = {p: [] for p in positions}
size = {p: 0 for p in positions}
position, obs, env_output = env.initial()
while True:
while True:
obs_x_no_action_buf[position].append(env_output['obs_x_no_action'])
obs_z_buf[position].append(env_output['obs_z'])
with torch.no_grad():
agent_output = model.forward(position, obs['z_batch'], obs['x_batch'], flags=flags)
_action_idx = int(agent_output['action'].cpu().detach().numpy())
action = obs['legal_actions'][_action_idx]
obs_action_buf[position].append(_cards2tensor(action))
position, obs, env_output = env.step(action)
size[position] += 1
if env_output['done']:
for p in positions:
diff = size[p] - len(target_buf[p])
if diff > 0:
done_buf[p].extend([False for _ in range(diff-1)])
done_buf[p].append(True)
episode_return = env_output['episode_return'] if p == 'landlord' else -env_output['episode_return']
episode_return_buf[p].extend([0.0 for _ in range(diff-1)])
episode_return_buf[p].append(episode_return)
target_buf[p].extend([episode_return for _ in range(diff)])
break
for p in positions:
if size[p] > T:
index = free_queue[p].get()
if index is None:
break
for t in range(T):
buffers[p]['done'][index][t, ...] = done_buf[p][t]
buffers[p]['episode_return'][index][t, ...] = episode_return_buf[p][t]
buffers[p]['target'][index][t, ...] = target_buf[p][t]
buffers[p]['obs_x_no_action'][index][t, ...] = obs_x_no_action_buf[p][t]
buffers[p]['obs_action'][index][t, ...] = obs_action_buf[p][t]
buffers[p]['obs_z'][index][t, ...] = obs_z_buf[p][t]
full_queue[p].put(index)
done_buf[p] = done_buf[p][T:]
episode_return_buf[p] = episode_return_buf[p][T:]
target_buf[p] = target_buf[p][T:]
obs_x_no_action_buf[p] = obs_x_no_action_buf[p][T:]
obs_action_buf[p] = obs_action_buf[p][T:]
obs_z_buf[p] = obs_z_buf[p][T:]
size[p] -= T
except KeyboardInterrupt:
pass
except Exception as e:
log.error('Exception in worker process %i', i)
traceback.print_exc()
print()
raise e
def _cards2tensor(list_cards):
"""
Convert a list of integers to the tensor
representation
See Figure 2 in https://arxiv.org/pdf/2106.06135.pdf
"""
if len(list_cards) == 0:
return torch.zeros(54, dtype=torch.int8)
matrix = np.zeros([4, 13], dtype=np.int8)
jokers = np.zeros(2, dtype=np.int8)
counter = Counter(list_cards)
for card, num_times in counter.items():
if card < 20:
matrix[:, Card2Column[card]] = NumOnes2Array[num_times]
elif card == 20:
jokers[0] = 1
elif card == 30:
jokers[1] = 1
matrix = np.concatenate((matrix.flatten('F'), jokers))
matrix = torch.from_numpy(matrix)
return matrix

View File

View File

@ -0,0 +1,44 @@
import torch
import numpy as np
from douzero.env.env import get_obs
def _load_model(position, model_path):
from douzero.dmc.models import model_dict
model = model_dict[position]()
model_state_dict = model.state_dict()
if torch.cuda.is_available():
pretrained = torch.load(model_path, map_location='cuda:0')
else:
pretrained = torch.load(model_path, map_location='cpu')
pretrained = {k: v for k, v in pretrained.items() if k in model_state_dict}
model_state_dict.update(pretrained)
model.load_state_dict(model_state_dict)
if torch.cuda.is_available():
model.cuda()
model.eval()
return model
class DeepAgent:
def __init__(self, position, model_path):
self.model = _load_model(position, model_path)
def act(self, infoset):
# 只有一个合法动作时直接返回,这样会得不到胜率信息
# if len(infoset.legal_actions) == 1:
# return infoset.legal_actions[0], 0
obs = get_obs(infoset)
z_batch = torch.from_numpy(obs['z_batch']).float()
x_batch = torch.from_numpy(obs['x_batch']).float()
if torch.cuda.is_available():
z_batch, x_batch = z_batch.cuda(), x_batch.cuda()
y_pred = self.model.forward(z_batch, x_batch, return_value=True)['values']
y_pred = y_pred.detach().cpu().numpy()
best_action_index = np.argmax(y_pred, axis=0)[0]
best_action = infoset.legal_actions[best_action_index]
best_action_confidence = y_pred[best_action_index]
# print(best_action, best_action_confidence, y_pred)
return best_action, best_action_confidence

View File

@ -0,0 +1,9 @@
import random
class RandomAgent():
def __init__(self):
self.name = 'Random'
def act(self, infoset):
return random.choice(infoset.legal_actions)

View File

@ -0,0 +1,183 @@
import random
from rlcard.games.doudizhu.utils import CARD_TYPE
EnvCard2RealCard = {3: '3', 4: '4', 5: '5', 6: '6', 7: '7',
8: '8', 9: '9', 10: 'T', 11: 'J', 12: 'Q',
13: 'K', 14: 'A', 17: '2', 20: 'B', 30: 'R'}
RealCard2EnvCard = {'3': 3, '4': 4, '5': 5, '6': 6, '7': 7,
'8': 8, '9': 9, 'T': 10, 'J': 11, 'Q': 12,
'K': 13, 'A': 14, '2': 17, 'B': 20, 'R': 30}
INDEX = {'3': 0, '4': 1, '5': 2, '6': 3, '7': 4,
'8': 5, '9': 6, 'T': 7, 'J': 8, 'Q': 9,
'K': 10, 'A': 11, '2': 12, 'B': 13, 'R': 14}
class RLCardAgent(object):
def __init__(self, position):
self.name = 'RLCard'
self.position = position
def act(self, infoset):
try:
# Hand cards
hand_cards = infoset.player_hand_cards
for i, c in enumerate(hand_cards):
hand_cards[i] = EnvCard2RealCard[c]
hand_cards = ''.join(hand_cards)
# Last move
last_move = infoset.last_move.copy()
for i, c in enumerate(last_move):
last_move[i] = EnvCard2RealCard[c]
last_move = ''.join(last_move)
# Last two moves
last_two_cards = infoset.last_two_moves
for i in range(2):
for j, c in enumerate(last_two_cards[i]):
last_two_cards[i][j] = EnvCard2RealCard[c]
last_two_cards[i] = ''.join(last_two_cards[i])
# Last pid
last_pid = infoset.last_pid
action = None
# the rule of leading round
if last_two_cards[0] == '' and last_two_cards[1] == '':
chosen_action = None
comb = combine_cards(hand_cards)
min_card = hand_cards[0]
for _, acs in comb.items():
for ac in acs:
if min_card in ac:
chosen_action = ac
action = [char for char in chosen_action]
for i, c in enumerate(action):
action[i] = RealCard2EnvCard[c]
#print('lead action:', action)
# the rule of following cards
else:
the_type = CARD_TYPE[0][last_move][0][0]
chosen_action = ''
rank = 1000
for ac in infoset.legal_actions:
_ac = ac.copy()
for i, c in enumerate(_ac):
_ac[i] = EnvCard2RealCard[c]
_ac = ''.join(_ac)
if _ac != '' and the_type == CARD_TYPE[0][_ac][0][0]:
if int(CARD_TYPE[0][_ac][0][1]) < rank:
rank = int(CARD_TYPE[0][_ac][0][1])
chosen_action = _ac
if chosen_action != '':
action = [char for char in chosen_action]
for i, c in enumerate(action):
action[i] = RealCard2EnvCard[c]
#print('action:', action)
elif last_pid != 'landlord' and self.position != 'landlord':
action = []
if action is None:
action = random.choice(infoset.legal_actions)
except:
action = random.choice(infoset.legal_actions)
#import traceback
#traceback.print_exc()
assert action in infoset.legal_actions
return action
def card_str2list(hand):
hand_list = [0 for _ in range(15)]
for card in hand:
hand_list[INDEX[card]] += 1
return hand_list
def list2card_str(hand_list):
card_str = ''
cards = [card for card in INDEX]
for index, count in enumerate(hand_list):
card_str += cards[index] * count
return card_str
def pick_chain(hand_list, count):
chains = []
str_card = [card for card in INDEX]
hand_list = [str(card) for card in hand_list]
hand = ''.join(hand_list[:12])
chain_list = hand.split('0')
add = 0
for index, chain in enumerate(chain_list):
if len(chain) > 0:
if len(chain) >= 5:
start = index + add
min_count = int(min(chain)) // count
if min_count != 0:
str_chain = ''
for num in range(len(chain)):
str_chain += str_card[start+num]
hand_list[start+num] = int(hand_list[start+num]) - int(min(chain))
for _ in range(min_count):
chains.append(str_chain)
add += len(chain)
hand_list = [int(card) for card in hand_list]
return (chains, hand_list)
def combine_cards(hand):
'''Get optimal combinations of cards in hand
'''
comb = {'rocket': [], 'bomb': [], 'trio': [], 'trio_chain': [],
'solo_chain': [], 'pair_chain': [], 'pair': [], 'solo': []}
# 1. pick rocket
if hand[-2:] == 'BR':
comb['rocket'].append('BR')
hand = hand[:-2]
# 2. pick bomb
hand_cp = hand
for index in range(len(hand_cp) - 3):
if hand_cp[index] == hand_cp[index+3]:
bomb = hand_cp[index: index+4]
comb['bomb'].append(bomb)
hand = hand.replace(bomb, '')
# 3. pick trio and trio_chain
hand_cp = hand
for index in range(len(hand_cp) - 2):
if hand_cp[index] == hand_cp[index+2]:
trio = hand_cp[index: index+3]
if len(comb['trio']) > 0 and INDEX[trio[-1]] < 12 and (INDEX[trio[-1]]-1) == INDEX[comb['trio'][-1][-1]]:
comb['trio'][-1] += trio
else:
comb['trio'].append(trio)
hand = hand.replace(trio, '')
only_trio = []
only_trio_chain = []
for trio in comb['trio']:
if len(trio) == 3:
only_trio.append(trio)
else:
only_trio_chain.append(trio)
comb['trio'] = only_trio
comb['trio_chain'] = only_trio_chain
# 4. pick solo chain
hand_list = card_str2list(hand)
chains, hand_list = pick_chain(hand_list, 1)
comb['solo_chain'] = chains
# 5. pick par_chain
chains, hand_list = pick_chain(hand_list, 2)
comb['pair_chain'] = chains
hand = list2card_str(hand_list)
# 6. pick pair and solo
index = 0
while index < len(hand) - 1:
if hand[index] == hand[index+1]:
comb['pair'].append(hand[index] + hand[index+1])
index += 2
else:
comb['solo'].append(hand[index])
index += 1
if index == (len(hand) - 1):
comb['solo'].append(hand[index])
return comb

View File

@ -0,0 +1,73 @@
from douzero.env.game import GameEnv
from .deep_agent import DeepAgent
EnvCard2RealCard = {3: '3', 4: '4', 5: '5', 6: '6', 7: '7',
8: '8', 9: '9', 10: 'T', 11: 'J', 12: 'Q',
13: 'K', 14: 'A', 17: '2', 20: 'X', 30: 'D'}
RealCard2EnvCard = {'3': 3, '4': 4, '5': 5, '6': 6, '7': 7,
'8': 8, '9': 9, 'T': 10, 'J': 11, 'Q': 12,
'K': 13, 'A': 14, '2': 17, 'X': 20, 'D': 30}
AllEnvCard = [3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7,
8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12,
12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 17, 17, 17, 17, 20, 30]
def evaluate(landlord, landlord_up, landlord_down):
# 输入玩家的牌
user_hand_cards_real = input("请输入你的手牌, 例如 333456789TJQKA2XD:")
# user_hand_cards_real = "34666777899TJJKA22XD"
use_hand_cards_env = [RealCard2EnvCard[c] for c in list(user_hand_cards_real)]
# 输入玩家角色
user_position_code = int(input("请输入你的角色[0地主上家, 1地主, 2地主下家]:"))
# user_position_code = 1
user_position = ['landlord_up', 'landlord', 'landlord_down'][user_position_code]
# 输入三张底牌
three_landlord_cards_real = input("请输入三张底牌, 例如 2XD:")
# three_landlord_cards_real = "2XD"
three_landlord_cards_env = [RealCard2EnvCard[c] for c in list(three_landlord_cards_real)]
# 整副牌减去玩家手上的牌,就是其他人的手牌,再分配给另外两个角色如何分配对AI判断没有影响
other_hand_cards = []
for i in set(AllEnvCard):
other_hand_cards.extend([i] * (AllEnvCard.count(i) - use_hand_cards_env.count(i)))
card_play_data_list = [{}]
card_play_data_list[0].update({
'three_landlord_cards': three_landlord_cards_env,
['landlord_up', 'landlord', 'landlord_down'][(user_position_code + 0) % 3]: use_hand_cards_env,
['landlord_up', 'landlord', 'landlord_down'][(user_position_code + 1) % 3]: other_hand_cards[0:17] if (user_position_code + 1) % 3 != 1 else other_hand_cards[17:],
['landlord_up', 'landlord', 'landlord_down'][(user_position_code + 2) % 3]: other_hand_cards[0:17] if (user_position_code + 1) % 3 == 1 else other_hand_cards[17:]
})
# 生成手牌结束,校验手牌数量
if len(card_play_data_list[0]["three_landlord_cards"]) != 3:
print("底牌必须是3张\n")
return
if len(card_play_data_list[0]["landlord_up"]) != 17 or \
len(card_play_data_list[0]["landlord_down"]) != 17 or \
len(card_play_data_list[0]["landlord"]) != 20:
print("初始手牌数目有误\n")
return
# print(card_play_data_list)
card_play_model_path_dict = {
'landlord': landlord,
'landlord_up': landlord_up,
'landlord_down': landlord_down}
print("创建代表玩家的AI...")
players = {}
players[user_position] = DeepAgent(user_position, card_play_model_path_dict[user_position])
env = GameEnv(players)
for idx, card_play_data in enumerate(card_play_data_list):
env.card_play_init(card_play_data)
print("开始出牌\n")
while not env.game_over:
env.step()
print("{}胜,本局结束!\n".format("农民" if env.winner == "farmer" else "地主"))
env.reset()

BIN
landlord_down_weights.pkl Normal file

Binary file not shown.

BIN
landlord_up_weights.pkl Normal file

Binary file not shown.

BIN
landlord_weights.pkl Normal file

Binary file not shown.

570
main.py Normal file
View File

@ -0,0 +1,570 @@
# -*- coding: utf-8 -*-
# Created by: Raf
# Modify by: Vincentzyx
import GameHelper as gh
from GameHelper import GameHelper
import os
import sys
import time
import threading
import pyautogui
import win32gui
from PIL import Image
import multiprocessing as mp
from PyQt5 import QtGui, QtWidgets, QtCore
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsPixmapItem, QInputDialog, QMessageBox
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtCore import QTime, QEventLoop
from MainWindow import Ui_Form
from douzero.env.game import GameEnv
from douzero.evaluation.deep_agent import DeepAgent
import BidModel
import LandlordModel
import FarmerModel
EnvCard2RealCard = {3: '3', 4: '4', 5: '5', 6: '6', 7: '7',
8: '8', 9: '9', 10: 'T', 11: 'J', 12: 'Q',
13: 'K', 14: 'A', 17: '2', 20: 'X', 30: 'D'}
RealCard2EnvCard = {'3': 3, '4': 4, '5': 5, '6': 6, '7': 7,
'8': 8, '9': 9, 'T': 10, 'J': 11, 'Q': 12,
'K': 13, 'A': 14, '2': 17, 'X': 20, 'D': 30}
AllEnvCard = [3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7,
8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12,
12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 17, 17, 17, 17, 20, 30]
AllCards = ['rD', 'bX', 'b2', 'r2', 'bA', 'rA', 'bK', 'rK', 'bQ', 'rQ', 'bJ', 'rJ', 'bT', 'rT',
'b9', 'r9', 'b8', 'r8', 'b7', 'r7', 'b6', 'r6', 'b5', 'r5', 'b4', 'r4', 'b3', 'r3']
helper = GameHelper()
helper.ScreenZoomRate = 1.25 # 请修改屏幕缩放比
class MyPyQT_Form(QtWidgets.QWidget, Ui_Form):
def __init__(self):
super(MyPyQT_Form, self).__init__()
self.setupUi(self)
self.setWindowFlags(QtCore.Qt.WindowMinimizeButtonHint | # 使能最小化按钮
QtCore.Qt.WindowCloseButtonHint) # 窗体总在最前端 QtCore.Qt.WindowStaysOnTopHint
self.setFixedSize(self.width(), self.height()) # 固定窗体大小
# self.setWindowIcon(QIcon('pics/favicon.ico'))
window_pale = QtGui.QPalette()
# window_pale.setBrush(self.backgroundRole(), QtGui.QBrush(QtGui.QPixmap("pics/bg.png")))
self.setPalette(window_pale)
self.Players = [self.RPlayer, self.Player, self.LPlayer]
self.counter = QTime()
# 参数
self.MyConfidence = 0.95 # 我的牌的置信度
self.OtherConfidence = 0.9 # 别人的牌的置信度
self.WhiteConfidence = 0.95 # 检测白块的置信度
self.LandlordFlagConfidence = 0.9 # # 检测地主标志的置信度
self.ThreeLandlordCardsConfidence = 0.9 # 检测地主底牌的置信度
self.PassConfidence = 0.85
self.WaitTime = 1 # 等待状态稳定延时
self.MyFilter = 40 # 我的牌检测结果过滤参数
self.OtherFilter = 25 # 别人的牌检测结果过滤参数
self.SleepTime = 0.1 # 循环中睡眠时间
self.RunGame = False
self.AutoPlay = False
# 坐标
self.MyHandCardsPos = (250, 764, 1141, 70) # 我的截图区域
self.LPlayedCardsPos = (463, 355, 380, 250) # 左边截图区域
self.RPlayedCardsPos = (946, 355, 380, 250) # 右边截图区域
self.LandlordFlagPos = [(1281, 276, 110, 140), (267, 695, 110, 140), (424, 237, 110, 140)] # 地主标志截图区域(右-我-左)
self.ThreeLandlordCardsPos = (763, 37, 287, 136) # 地主底牌截图区域resize成349x168
self.PassBtnPos = (686, 659, 419, 100)
self.GeneralBtnPos = (616, 631, 576, 117)
# 信号量
self.shouldExit = 0 # 通知上一轮记牌结束
self.canRecord = threading.Lock() # 开始记牌
self.card_play_model_path_dict = {
'landlord': "baselines/douzero_ADP/landlord.ckpt",
'landlord_up': "baselines/douzero_ADP/landlord_up.ckpt",
'landlord_down': "baselines/douzero_ADP/landlord_down.ckpt"
}
# cards = self.find_three_landlord_cards(self.ThreeLandlordCardsPos)
# print(cards)
# exit()
def init_display(self):
self.WinRate.setText("评分")
self.InitCard.setText("开始")
self.UserHandCards.setText("手牌")
self.LPlayedCard.setText("上家出牌区域")
self.RPlayedCard.setText("下家出牌区域")
self.PredictedCard.setText("AI出牌区域")
self.ThreeLandlordCards.setText("地主牌")
self.SwitchMode.setText("自动" if self.AutoPlay else "单局")
for player in self.Players:
player.setStyleSheet('background-color: rgba(255, 0, 0, 0);')
def switch_mode(self):
self.AutoPlay = not self.AutoPlay
self.SwitchMode.setText("自动" if self.AutoPlay else "单局")
def init_cards(self):
self.RunGame = True
GameHelper.Interrupt = False
self.init_display()
# 玩家手牌
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)]
for testCount in range(1, 5):
if len(self.three_landlord_cards_env) > 3:
self.ThreeLandlordCardsConfidence += 0.05
elif len(self.three_landlord_cards_env) < 3:
self.ThreeLandlordCardsConfidence -= 0.05
else:
break
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("开始对局")
print("手牌:",self.user_hand_cards_real)
print("地主牌:",self.three_landlord_cards_real)
# 生成手牌结束,校验手牌数量
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
# 创建一个代表玩家的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)
try:
self.start()
except:
self.stop()
def sleep(self, ms):
self.counter.restart()
while self.counter.elapsed() < ms:
QtWidgets.QApplication.processEvents(QEventLoop.AllEvents, 50)
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"])
if action_message["action"] == "":
helper.ClickOnImage("pass_btn", region=self.PassBtnPos)
else:
helper.SelectCards(action_message["action"])
tryCount = 20
result = helper.LocateOnScreen("play_card", region=self.PassBtnPos, confidence=0.85)
while result is None and tryCount > 0:
if not self.RunGame:
break
print("等待出牌按钮")
self.detect_start_btn()
tryCount -= 1
result = helper.LocateOnScreen("play_card", region=self.PassBtnPos, confidence=0.85)
self.sleep(100)
helper.ClickOnImage("play_card", region=self.PassBtnPos, confidence=0.85)
self.sleep(2200)
self.detect_start_btn()
self.play_order = 1
elif self.play_order == 1:
self.RPlayedCard.setText("...")
pass_flag = helper.LocateOnScreen('pass',
region=self.RPlayedCardsPos,
confidence=self.PassConfidence)
self.detect_start_btn()
while self.RunGame and self.have_white(self.RPlayedCardsPos) == 0 and pass_flag is None:
print("等待下家出牌")
self.sleep(100)
pass_flag = helper.LocateOnScreen('pass', region=self.RPlayedCardsPos,
confidence=self.PassConfidence)
self.detect_start_btn()
self.sleep(200)
# 未找到"不出"
if pass_flag is None:
# 识别下家出牌
self.RPlayedCard.setText("等待动画")
self.sleep(1200)
self.RPlayedCard.setText("识别中")
self.other_played_cards_real = self.find_other_cards(self.RPlayedCardsPos)
print("下家出牌", self.other_played_cards_real)
self.sleep(500)
# 找到"不出"
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
self.sleep(500)
elif self.play_order == 2:
self.LPlayedCard.setText("...")
self.detect_start_btn()
pass_flag = helper.LocateOnScreen('pass', region=self.LPlayedCardsPos,
confidence=self.PassConfidence)
while self.RunGame and self.have_white(self.LPlayedCardsPos) == 0 and pass_flag is None:
print("等待上家出牌")
self.detect_start_btn()
self.sleep(100)
pass_flag = helper.LocateOnScreen('pass', region=self.LPlayedCardsPos,
confidence=self.PassConfidence)
self.sleep(200)
# 不出
# 未找到"不出"
if pass_flag is None:
# 识别上家出牌
self.LPlayedCard.setText("等待动画")
self.sleep(1200)
self.LPlayedCard.setText("识别中")
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 "不出")
self.sleep(500)
else:
pass
self.sleep(100)
print("{}胜,本局结束!\n".format("农民" if self.env.winner == "farmer" else "地主"))
# QMessageBox.information(self, "本局结束", "{}胜!".format("农民" if self.env.winner == "farmer" else "地主"),
# QMessageBox.Yes, QMessageBox.Yes)
self.detect_start_btn()
def find_landlord(self, landlord_flag_pos):
for pos in landlord_flag_pos:
result = helper.LocateOnScreen("landlord_words", region=pos,
confidence=self.LandlordFlagConfidence)
if result is not None:
return landlord_flag_pos.index(pos)
return None
def detect_start_btn(self):
result = helper.LocateOnScreen("change_player_btn", region=(667, 741, 934, 404))
if result is not None:
self.RunGame = False
self.stop()
result = helper.LocateOnScreen("yes_btn", region=(680, 661, 435, 225))
if result is not None:
helper.ClickOnImage("yes_btn", region=(680, 661, 435, 225))
self.sleep(1000)
result = helper.LocateOnScreen("get_award_btn", region=(680, 661, 435, 225))
if result is not None:
helper.ClickOnImage("get_award_btn", region=(680, 661, 435, 225))
self.sleep(1000)
result = helper.LocateOnScreen("yes_btn_sm", region=(669, 583, 468, 100))
if result is not None:
helper.ClickOnImage("yes_btn_sm", region=(669, 583, 468, 100))
self.sleep(200)
def find_three_landlord_cards(self, pos):
img, _ = helper.Screenshot()
img = img.crop((pos[0], pos[1], pos[0] + pos[2], pos[1] + pos[3]))
img = img.resize((349, 168))
three_landlord_cards_real = ""
for card in AllCards:
result = pyautogui.locateAll(needleImage=helper.Pics['o' + card], haystackImage=img,
confidence=self.ThreeLandlordCardsConfidence)
three_landlord_cards_real += card[1] * self.cards_filter(list(result), self.OtherFilter)
if len(three_landlord_cards_real) > 3:
three_landlord_cards_real = ""
for card in AllCards:
result = pyautogui.locateAll(needleImage=helper.Pics['o' + card], haystackImage=img,
confidence=self.ThreeLandlordCardsConfidence + 0.05)
three_landlord_cards_real += card[1] * self.cards_filter(list(result), self.OtherFilter)
if len(three_landlord_cards_real) < 3:
three_landlord_cards_real = ""
for card in AllCards:
result = pyautogui.locateAll(needleImage=helper.Pics['o' + card], haystackImage=img,
confidence=self.ThreeLandlordCardsConfidence + 0.1)
three_landlord_cards_real += card[1] * self.cards_filter(list(result), self.OtherFilter)
return three_landlord_cards_real
def find_my_cards(self, pos):
user_hand_cards_real = ""
img, _ = helper.Screenshot()
cards, _ = helper.GetCards(img)
for c in cards:
user_hand_cards_real += c[0]
# for card in AllCards:
# result = pyautogui.locateAll(needleImage=helper.Pics['m'+card], 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_other_cards(self, pos):
other_played_cards_real = ""
self.sleep(500)
img, _ = helper.Screenshot(region=pos)
for card in AllCards:
result = pyautogui.locateAll(needleImage=helper.Pics['o' + card], haystackImage=img,
confidence=self.OtherConfidence)
other_played_cards_real += card[1] * self.cards_filter(list(result), self.OtherFilter)
return other_played_cards_real
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
def have_white(self, pos): # 是否有白块
img, _ = helper.Screenshot()
result = pyautogui.locate(needleImage=helper.Pics["white"], haystackImage=img,
region=pos, confidence=self.WhiteConfidence)
if result is None:
return 0
else:
return 1
def stop(self):
try:
self.RunGame = False
self.env.game_over = True
self.env.reset()
self.init_display()
self.PreWinrate.setText("局前预估胜率:")
self.BidWinrate.setText("叫牌预估胜率:")
except AttributeError as e:
pass
if self.AutoPlay:
play_btn = helper.LocateOnScreen("change_player_btn", region=(667, 741, 934, 404))
while play_btn is None and self.AutoPlay:
play_btn = helper.LocateOnScreen("change_player_btn", region=(667, 741, 934, 404))
self.sleep(100)
if play_btn is not None:
helper.LeftClick((play_btn[0], play_btn[1]))
self.beforeStart()
def beforeStart(self):
GameHelper.Interrupt = True
thresholds = [
[75, 60],
[85, 70]
]
while True:
outterBreak = False
jiaodizhu_btn = helper.LocateOnScreen("jiaodizhu_btn", region=(765, 663, 116, 50))
qiangdizhu_btn = helper.LocateOnScreen("qiangdizhu_btn", region=(783, 663, 116, 50))
jiabei_btn = helper.LocateOnScreen("jiabei_btn", region=self.GeneralBtnPos)
self.detect_start_btn()
while jiaodizhu_btn is None and qiangdizhu_btn is None and jiabei_btn is None:
self.detect_start_btn()
print("等待加倍或叫地主")
self.sleep(100)
jiaodizhu_btn = helper.LocateOnScreen("jiaodizhu_btn", region=(765, 663, 116, 50))
qiangdizhu_btn = helper.LocateOnScreen("qiangdizhu_btn", region=(783, 663, 116, 50))
jiabei_btn = helper.LocateOnScreen("jiabei_btn", region=self.GeneralBtnPos)
if jiabei_btn is None:
img, _ = helper.Screenshot()
cards, _ = helper.GetCards(img)
cards_str = "".join([card[0] for card in cards])
win_rate = BidModel.predict(cards_str)
print("预计叫地主胜率:", win_rate)
self.BidWinrate.setText("叫牌预估胜率:" + str(round(win_rate, 2)) + "%")
is_stolen = 0
if jiaodizhu_btn is not None:
if win_rate > 55:
helper.ClickOnImage("jiaodizhu_btn", region=(765, 663, 116, 50), confidence=0.9)
else:
helper.ClickOnImage("bujiao_btn", region=self.GeneralBtnPos)
elif qiangdizhu_btn is not None:
is_stolen = 1
if win_rate > 60:
helper.ClickOnImage("qiangdizhu_btn", region=(783, 663, 116, 50), confidence=0.9)
else:
helper.ClickOnImage("buqiang_btn", region=self.GeneralBtnPos)
else:
pass
else:
llcards = self.find_three_landlord_cards(self.ThreeLandlordCardsPos)
print("地主牌:", llcards)
img, _ = helper.Screenshot()
cards, _ = helper.GetCards(img)
cards_str = "".join([card[0] for card in cards])
if len(cards_str) == 20:
win_rate = LandlordModel.predict(cards_str)
self.PreWinrate.setText("局前预估胜率:" + str(round(win_rate, 2)) + "%")
print("预估地主胜率:", win_rate)
else:
user_position_code = self.find_landlord(self.LandlordFlagPos)
user_position = "up"
while user_position_code is None:
user_position_code = self.find_landlord(self.LandlordFlagPos)
self.sleep(50)
user_position = ['up', 'landlord', 'down'][user_position_code]
win_rate = FarmerModel.predict(cards_str, llcards, user_position) - 5
print("预估农民胜率:", win_rate)
self.PreWinrate.setText("局前预估胜率:" + str(round(win_rate, 2)) + "%")
if win_rate > thresholds[is_stolen][0]:
chaojijiabei_btn = helper.LocateOnScreen("chaojijiabei_btn", region=self.GeneralBtnPos)
if chaojijiabei_btn is not None:
helper.ClickOnImage("chaojijiabei_btn", region=self.GeneralBtnPos)
else:
helper.ClickOnImage("jiabei_btn", region=self.GeneralBtnPos)
elif win_rate > thresholds[is_stolen][1]:
helper.ClickOnImage("jiabei_btn", region=self.GeneralBtnPos)
else:
helper.ClickOnImage("bujiabei_btn", region=self.GeneralBtnPos)
outterBreak = True
break
if outterBreak:
break
llcards = self.find_three_landlord_cards(self.ThreeLandlordCardsPos)
while len(llcards) != 3:
print("等待地主牌", llcards)
self.sleep(100)
llcards = self.find_three_landlord_cards(self.ThreeLandlordCardsPos)
self.sleep(4000)
self.init_cards()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setStyleSheet("""
QPushButton{
text-align : center;
background-color : white;
font: bold;
border-color: gray;
border-width: 2px;
border-radius: 10px;
padding: 6px;
height : 14px;
border-style: outset;
font : 14px;
}
QPushButton:hover{
background-color : light gray;
}
QPushButton:pressed{
text-align : center;
background-color : gray;
font: bold;
border-color: gray;
border-width: 2px;
border-radius: 10px;
padding: 6px;
height : 14px;
border-style: outset;
font : 14px;
padding-left:9px;
padding-top:9px;
}
QComboBox{
background:transparent;
border: 1px solid rgba(200, 200, 200, 100);
font-weight: bold;
}
QComboBox:drop-down{
border: 0px;
}
QComboBox QAbstractItemView:item{
height: 30px;
}
QLabel{
background:transparent;
font-weight: bold;
}
""")
my_pyqt_form = MyPyQT_Form()
my_pyqt_form.show()
sys.exit(app.exec_())

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
pics/bujiabei_btn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
pics/bujiao_btn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
pics/buqiang_btn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
pics/card_corner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
pics/card_edge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1002 B

BIN
pics/card_overlap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

BIN
pics/card_upper_edge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

BIN
pics/card_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

BIN
pics/change_player_btn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
pics/chaojijiabei_btn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
pics/get_award_btn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
pics/go_btn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
pics/jiabei_btn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
pics/jiaodizhu_btn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
pics/landlord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
pics/landlord_words.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
pics/mb2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
pics/mb3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
pics/mb4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
pics/mb5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
pics/mb6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
pics/mb7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
pics/mb8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
pics/mb9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
pics/mbA.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
pics/mbJ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
pics/mbK.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
pics/mbQ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
pics/mbT.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
pics/mbX.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
pics/mr2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
pics/mr3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
pics/mr4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
pics/mr5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
pics/mr6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
pics/mr7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
pics/mr8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
pics/mr9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
pics/mrA.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
pics/mrD.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
pics/mrJ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
pics/mrK.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
pics/mrQ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
pics/mrT.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
pics/ob2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
pics/ob3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
pics/ob4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
pics/ob5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
pics/ob6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
pics/ob7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
pics/ob8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
pics/ob9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
pics/obA.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Some files were not shown because too many files have changed in this diff Show More