From 82a53b0d9e973f26f628342110ba09fe724f7ec7 Mon Sep 17 00:00:00 2001 From: templete Date: Fri, 5 May 2023 18:50:14 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86"=E6=8B=96?= =?UTF-8?q?=E6=8B=89=E6=9C=BA"=E7=9A=84=E6=B8=B8=E6=88=8F=E7=8E=AF?= =?UTF-8?q?=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/run_random.py | 2 +- rlcard/games/tractor/__init__.py | 0 rlcard/games/tractor/baselinePolicy.py | 86 ++ rlcard/games/tractor/example.py | 81 ++ rlcard/games/tractor/tractor_action.py | 143 +++ rlcard/games/tractor/tractor_cheat.py | 105 +++ rlcard/games/tractor/tractor_game.py | 871 ++++++++++++++++++ rlcard/games/tractor/tractor_gameUtil.py | 171 ++++ rlcard/games/tractor/tractor_player.py | 171 ++++ ...\347\224\250\350\257\264\346\230\216.docx" | Bin 0 -> 23983 bytes 10 files changed, 1629 insertions(+), 1 deletion(-) create mode 100644 rlcard/games/tractor/__init__.py create mode 100644 rlcard/games/tractor/baselinePolicy.py create mode 100644 rlcard/games/tractor/example.py create mode 100644 rlcard/games/tractor/tractor_action.py create mode 100644 rlcard/games/tractor/tractor_cheat.py create mode 100644 rlcard/games/tractor/tractor_game.py create mode 100644 rlcard/games/tractor/tractor_gameUtil.py create mode 100644 rlcard/games/tractor/tractor_player.py create mode 100644 "rlcard/games/tractor/\345\217\214\345\215\207\345\274\272\345\214\226\345\255\246\344\271\240\347\216\257\345\242\203\344\275\277\347\224\250\350\257\264\346\230\216.docx" diff --git a/examples/run_random.py b/examples/run_random.py index 9a2681288..06f74b2b1 100644 --- a/examples/run_random.py +++ b/examples/run_random.py @@ -38,7 +38,7 @@ def run(args): parser.add_argument( '--env', type=str, - default='leduc-holdem', + default='bridge', choices=[ 'blackjack', 'leduc-holdem', diff --git a/rlcard/games/tractor/__init__.py b/rlcard/games/tractor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rlcard/games/tractor/baselinePolicy.py b/rlcard/games/tractor/baselinePolicy.py new file mode 100644 index 000000000..3834901e4 --- /dev/null +++ b/rlcard/games/tractor/baselinePolicy.py @@ -0,0 +1,86 @@ +import random +import numpy as np +from functools import cmp_to_key + +import tractor_game +# from DNQ.mygame.TractorArtifice.cheater import mkDeck, cheator1 +from tractor_gameUtil import fenInd,getNum +from tractor_game import Tractor +from tractor_player import Player +env=Tractor() +def _baselinecmp(a,b):#比较两组牌大小。返回1是a大,返回0是b大 + return a[1]>b[1] and 1 or -1 +def baselineColdeck(env:Tractor,p: Player,undercard_i):#贪心的换牌算法,cards有8张 + li = [] + for i in range(4): + if i!=p.lordDecor and len(p.cards[i])>0: + li.append((i,len(p.cards[i]))) + li = sorted(li, key=cmp_to_key(_baselinecmp)) + mincard=0 + minnum=100 + for _ in li:#无脑扣每个类别里最小的牌 + kind,cardslen=_[0],_[1] + for card in reversed(p.cards[kind]): + if minnum>env.orderInd[card] and fenInd[getNum(card)]==0: + mincard,minnum=card,env.orderInd[card] + break + if mincard==0:#只有主牌,则从主牌里扣 + return p.cards[p.lordDecor][-1] + return mincard + +def bidFun_random(env,p,round,allActList):#回调函数 + n=len(allActList) + return random.randint(0,n-1) +def firstPolicyFun_random(env,p,usedAct,allActList):#回调函数 + n = len(allActList) + act_id = random.randint(0, n - 1) + # dfsPrintActList(allActList ) + # dfsPrintActList(allActList[act_id]) + # env.printAllInfo(p.id) + # print("") + return act_id +def otherPolicyFun_random(env, p, usedAct, allActList):#回调函数 + n = len(allActList) + act_id=random.randint(0, n - 1) + # dfsPrintActList(allActList ) + # dfsPrintActList(allActList[act_id]) + # env.printAllInfo(p.id) + # print("") + return act_id + +# def baselineAct(p,knowCards,cardsCnt,knowCards_seq,maxi,kind):#player_cards代表自己玩家的手牌,knowCards是已知前置位的牌,knowCards_seq代表第几个出 +# #cardsCnt代表knowCards每组牌的数量 +# #maxi是最大的玩家的位置,kind代表出的花色,级牌算主牌的花色 +# #player_cards通过排序被编码成了4个颜色,每个颜色又分为3组分别代表;oneList,doubleList,traList +# # +# cardsMax=knowCards[maxi] +# player_cards=p.toSortCardsList1(env) +# if knowCards_seq==0:#先出 +# #算跑了多少分,如果外置位分数大于50,则无脑出大的。外置位分数计算方式:200-自己的分数-底牌的分数(如果可见)-已经出去的别人的分数 +# #如果外置位分数不大于50,随机尽量出小的,可以出分 +# # +# pass +# elif knowCards_seq==1:#第二个出 +# #无脑大过0号,否则随机跟小牌且尽量不跟分 +# if len(cardsMax[2])>0:#敌方又拖拉机 +# if len(player_cards[kind][2])==0:#我方没拖拉机 +# if len(player_cards[kind][1])==0:#我方没对子 +# if len(player_cards[kind][0])>=cardsCnt:#这一类花色有牌可出 +# cards=baselineAct_followSmall1()#跟小牌 +# return +# +# # else:#个数大于它且比他多 +# for a in cardsMax[2]: # 看看有没有拖拉机 +# pass +# pass +# elif knowCards_seq == 2:#第三个 +# #如果1号大,就无脑大过1号 +# #如果0号大,且0号是王或级牌或大于1张的甩牌拖拉机对子,就跟分,没有分跟小牌 +# #如果0号大,且0号较小,无脑大过他 +# +# pass +# elif knowCards_seq == 3:#最后出牌,策略是: +# #如果我方大:就无脑跟分,能用分杀就用分杀,没有分就随机跟小牌; +# #如果敌方大:且没有分:能用分杀就用分杀,否则就随机跟小牌且尽量不跟分; +# #如果敌方大且有分:就尽量大过前面的,大不过就随机跟小牌且尽量不跟分 +# pass diff --git a/rlcard/games/tractor/example.py b/rlcard/games/tractor/example.py new file mode 100644 index 000000000..7b7b673ac --- /dev/null +++ b/rlcard/games/tractor/example.py @@ -0,0 +1,81 @@ + +#使用该游戏环境的样例 +import random + +from DNQ.mygame.TractorArtifice.game_env.tractor_cheat import mkDeck, cheator1 +from baselinePolicy import baselineColdeck +from tractor_action import dfsPrintActList +from tractor_game import Tractor +def bidFun(env,p,round,allActList):#回调函数 + n=len(allActList) + return random.randint(0,n-1) +def firstPolicyFun(env,p,usedAct,allActList):#回调函数 + n = len(allActList) + act_id = random.randint(0, n - 1) + # dfsPrintActList(allActList ) + # dfsPrintActList(allActList[act_id]) + # env.printAllInfo(p.id) + # print("") + return act_id +def otherPolicyFun(env, p, usedAct, allActList):#回调函数 + n = len(allActList) + act_id=random.randint(0, n - 1) + # dfsPrintActList(allActList ) + # dfsPrintActList(allActList[act_id]) + # env.printAllInfo(p.id) + # print("") + return act_id +def playAGame(env):#4个人双方随机游戏 + # deck1=[39, 39, 23, 12, 26, 5, 53, 1, 38, 30, 46, 54, 48, 40, 36, 6, 28, 46, 26, 18, 7, 16, 2, 27, 5, 22, 20, 47, 41, 41, 34, 8, 3, 31, 30, 13, 16, 23, 15, 48, 13, 51, 4, 37, 44, 33, 25, 52, 34, 9, 37, 21, 3, 17, 50, 29, 24, 51, 49, 38, 35, 43, 24, 6, 18, 32, 22, 29, 7, 20, 11, 19, 15, 36, 14, 42, 27, 45, 14, 12, 50, 45, 52, 31, 11, 42, 40, 47, 33, 54, 32, 8, 28, 21, 10, 49, 9, 25, 53, 44, 1, 4, 17, 19, 10, 2, 35, 43] + # deck1, setDecor, setNum, setDealer=mkDeck(cheator1) + # env.dealCards(deck1,bidFun,(setDecor, setNum , setDealer)) + env.dealCards(None, bidFun) + # env.printAllInfo() + env.setUnderCards(baselineColdeck)#换底牌,baselineColdeck是基于贪心的换底牌策略:无脑扣小排 + env.printAllInfo() + dfsPrintActList(env.players[env.dealer].cards[env.lordDecor]) + firstPlayerId=env.dealer + isTer=False + epoch=0 + while(not isTer):#开始出牌 + # env.printAllCards() + # print("轮次:",epoch," 先出牌玩家:",firstPlayerId) + act4 = [None,None,None,None] + # print("先出玩家:", firstPlayerId) + act4[firstPlayerId] = env.firstPolicy(firstPlayerId,firstPolicyFun)#获取动作 + # firstKind=env.getActKind(act4[firstPlayerId]) + + # print(env.players[firstPlayerId].cards_decorList) + # env.dfsPrintActList(sortCardList2[firstPlayerId]) + # env.dfsPrintActList(allAct[firstPlayerId],printCmp) + # env.dfsPrintActList(act[firstPlayerId] ) + # print(firstKind) + # act[firstPlayerId].println() + for i in range(1,4): + nextID=(firstPlayerId+i)%4 + act4[nextID]= env.otherPolicy(act4,firstPlayerId,nextID,otherPolicyFun) + + firstPlayerId,sc,isTer,endSc=env.step(act4,firstPlayerId)#评价谁赢,返回赢者id,本轮分数(双方都会得分),isTer是游戏有木有结束 + # reset + # env.printAllInfo(firstPlayerId,act4) + if isTer : + # env.printUnderCards() + sc=env.sumSc + winPlayer,playerId,grade=env.calcGameScore()#重置游戏,playerId==-1代表继续,否则代表先达到A的玩家。 + print(sc,winPlayer) + isTer=playerId!=-1 + return isTer,winPlayer,grade + epoch+=1 + return -1,-1 + +def train_game(trainMaxCnt): + env=Tractor() + for _ in range(trainMaxCnt): + env.reset_game() + while (True):#有先超过A的玩家就游戏结束 + isTer,winPlayer,grade=playAGame(env) + # print(env.levelOfBoth) + if isTer: + break + +train_game(1) \ No newline at end of file diff --git a/rlcard/games/tractor/tractor_action.py b/rlcard/games/tractor/tractor_action.py new file mode 100644 index 000000000..a79b87125 --- /dev/null +++ b/rlcard/games/tractor/tractor_action.py @@ -0,0 +1,143 @@ +import math +import random +from functools import cmp_to_key +import numpy as np + +from tractor_gameUtil import getNum, fenInd, printCard, cardToString, INF, getKind + + +class Action(): + def __init__(self, one=[],double=[],playerId=-1): # double里包含对子和拖拉机,如[[3,3],[4,4,5,5]] + self.one=one.copy() + self.double = double.copy() + self.len=len(one) + self.playerId=playerId + for dou in double: + self.len+=len(dou) + def add(self,one=[],double=[]): + for a in one: + self.one.append(a) + self.len += len(one) + for dou in double.copy(): + self.double.append(dou) + self.len += len(dou) + def addOne(self,a): + self.one.append(a) + self.len += 1 + def addDou(self,dou): + self.double.append(dou.copy()) + self.len+=len(dou) + def setDou(self,i,dou): + if i1 + def getDouleCnt(self):#返回对子数量 + return (self.len-len(self.one))//2 + def getDouleLen(self):#返回对子数组长度 + return len(self.double) + def isSeq(self):#是否为甩牌 + return len(self.double)+len(self.one)>1 + def getFen(self): + sc=0 + for dou in self.double: + for a in dou: + num = getNum(a) # 点数,[1,13]王是14 + sc += fenInd[num] # 分数 + for a in self.one: + num = getNum(a) # 点数,[1,13]王是14 + sc += fenInd[num] # 分数 + return sc + def print(self,i=0): + print("act"+str(i)+":",end="") + i=0 + for dou in self.double: + for a in dou: + printCard(a,i) + i+=1 + for a in self.one: + printCard(a,i) + i+=1 + def println(self,i=0): + self.print(i) + print("") + def toString(self): + ans="" + for dou in self.double: + for a in dou: + ans+=cardToString(a) + for a in self.one: + ans+=cardToString(a) + return ans + + def tolist(self): + li=self.one.copy() + for dou in self.double: + for a in dou: + li.append(a) + return li + def getKind(self,env): + if len(self.one)>0: + return getKind(self.one[0],env.lordDecor,env.lordNum) + return getKind(self.double[0][0], env.lordDecor, env.lordNum) + def sort(self,env): + self.one.sort(key=cmp_to_key(env._sortCardList_cmp1)) + for dou in self.double: + dou.sort(key=cmp_to_key(env._sortCardList_cmp1)) + def getMinCard(self,env):#返回最小的牌 + mincard=0 + minOrder=INF + for dou in self.double: + for a in dou: + if minOrder>env.orderInd[a]: + minOrder =env.orderInd[a] + mincard=a + for a in self.one: + if minOrder > env.orderInd[a]: + minOrder = env.orderInd[a] + mincard = a + return Action([a]) + +def __dfsPrintActList(newLi, li0,printFun=None):#printFun是打印这张牌的条件 + if isinstance(li0,np.ndarray): + n=li0.shape[0] + else: + n=len(li0) + + for i in range(n): + if isinstance(li0[i],int) or isinstance(li0[i],np.int32) or isinstance(li0[i],np.int64): + if printFun==None or printFun(li0[i]): + newLi.append(cardToString(li0[i])) + elif isinstance(li0[i],Action): + if printFun==None or printFun(li0[i]): + newLi.append(li0[i].toString()) + else: + t=[] + __dfsPrintActList(t,li0[i]) + if printFun==None or printFun(t): + newLi.append(t) +def dfsPrintActList(li,printFun=None): + newLi=[] + if isinstance(li,int) or isinstance(li,np.int32): + newLi.append(cardToString(li)) + elif isinstance(li,Action): + newLi.append(li.toString()) + else: + __dfsPrintActList(newLi,li,printFun) + print(newLi) + +def cardsListToAction(env,pid,cards):#cards只有一个类型 + li=(env.sortCardList2(cards)) + li=(env.sortCardList1(li[0], li[1], li[2])) + dou=[] + for a in li[1]: + dou.append([a,a]) + for tra in li[2]: + dou.append([tra[i//2] for i in range(len(tra)*2)]) + act = Action(li[0], dou, playerId=pid) + act.sort(env) + return act diff --git a/rlcard/games/tractor/tractor_cheat.py b/rlcard/games/tractor/tractor_cheat.py new file mode 100644 index 000000000..d715a1f1c --- /dev/null +++ b/rlcard/games/tractor/tractor_cheat.py @@ -0,0 +1,105 @@ +import numpy as np + +from DNQ.mygame.TractorArtifice.game_env.tractor_gameUtil import getKind, INF, printCard, CARDS_CNT, CARDS_CNT2, \ + NameTodecorId, decorName, stringToCardId, UNDERCARD_CNT + + +def __mkdeck_push(deck1,i,yu,pid,without,setDecor, setNum): + if deck1[i] == 0: + j = 0 + while (j < len(yu)): + if without[pid][getKind(yu[j], setDecor, setNum)] == 0: + break + j += 1 + if j == len(yu): + j -= 1 + print("有冲突,放置了b:", end=" ") + printCard(yu[j]) + deck1[i] = yu[j] + yu.pop(j) + elif deck1[i] == INF: + deck1[i] = yu[0] + yu.pop(0) +numind={"A":1,"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"J":1,"Q":1,"K":1} +def mkDeck(fun):#作弊洗牌,测试用 + deck1 = np.zeros((CARDS_CNT), dtype='int') + ind=np.zeros((CARDS_CNT2+1), dtype='int') + yu=[] + without=[[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]#without[i][j]==1代表玩家i不能有颜色j的牌 + cardDir,setDecor,setNum,setDealer=fun() + setDecor=NameTodecorId[setDecor] + for i in range(4):#设置每个玩家的手牌 + dir=cardDir[i] + deck1_i=0 + for j in range(4): + strDec=decorName[j] + if strDec in dir: + for k in range(len(dir[strDec])): + if dir[strDec][k]!="大王" and dir[strDec][k]!="小王" and (dir[strDec][k] in numind ): + dir[strDec][k]=strDec+dir[strDec][k] + a=stringToCardId[dir[strDec][k]] + deck1[deck1_i*4+i] = a + if a!=INF: + ind[a]+=1 + if ind[a]>2: + print("错误") + return None + deck1_i+=1 + without[i][j]=1 + for i in range(len(cardDir[4])):#cardDir[4]代表牌堆的底牌 + a = stringToCardId[cardDir[4][i]] + deck1[CARDS_CNT-i-1]=a + if a != INF: + ind[a] += 1 + if ind[a] > 2: + print("错误") + return None + + for i in range(1,CARDS_CNT2+1): + if ind[i]==1: + yu.append(i) + elif ind[i]==0: + yu.append(i) + yu.append(i) + print(without) + for i in range(CARDS_CNT-UNDERCARD_CNT,CARDS_CNT): + __mkdeck_push(deck1, i, yu, setDealer, without, setDecor, setNum) + # print(deck1) + for k in range(4): + for i in range(k,CARDS_CNT-UNDERCARD_CNT,4): + __mkdeck_push(deck1,i,yu,k,without,setDecor, setNum) + # print(deck1) + # print(deck1) + # ind = np.zeros((CARDS_CNT2 + 1), dtype='int') + # for a in deck1: + # ind[a]+=1 + # print(ind) + return deck1,setDecor,setNum,setDealer + +def cheator1():##作弊器 + setDecor="♠" + setNum=2 + setDealer=0 + dir=[{},{},{},{},[]] + dir[0]["♠"]=["A","A"] + dir[0]["♣"] = ["7","7","6","6","5","5"] + # dir[0]["♦"] = [] + dir[0]["♥"] = ["7","7","6","6","5","5"] + + # dir[1]["♠"] = ["A", "A"] + dir[1]["♣"] = ["8", "8", "10", "A", "A"] + # dir[1]["♦"] = ["8", "8", "10", "A", "A"] + dir[1]["♥"] = ["K", "K", "Q", "Q", "8", "8"] + + dir[2]["♠"] = ["Q", "Q","3", "3"] + # dir[2]["♣"] = ["7", "7", "6", "6", "5", "5"] + # dir[2]["♦"] = [] + dir[2]["♥"] = ["A", "A"] + + # dir[3]["♠"] = ["9", "9", "J", "J"] + # dir[3]["♣"] = [] + # dir[3]["♦"] = [] + # dir[3]["♥"] = [] + + + return dir,setDecor,setNum,setDealer \ No newline at end of file diff --git a/rlcard/games/tractor/tractor_game.py b/rlcard/games/tractor/tractor_game.py new file mode 100644 index 000000000..22fe0389f --- /dev/null +++ b/rlcard/games/tractor/tractor_game.py @@ -0,0 +1,871 @@ +import math +import random +from functools import cmp_to_key + +import numpy as np + +from tractor_action import Action, dfsPrintActList, cardsListToAction +from tractor_gameUtil import CARDS_CNT2, HANDCARD_CNT, UNDERCARD_CNT, CARDS_CNT, getKindAndNum, INF, getKind, fenInd, \ + getNum, decorName, getDecor, toCardIndex, printCard, printCardln +from tractor_player import Player + + +class Tractor(): + def __init__(self): + self.undeck = [] # 存数组 + self.reset_game() + def __reset(self): + self.players = [Player(0), Player(1), Player(2), Player(3)] # 0代表没有 + self.lordDecor = -1 # 主牌花色 + self.sumSc = 0 # 闲家得分 + self.underCards = [] + self.nowDealCardPlayer = 0 # 当前到发牌的玩家 + self.round_i = 0 # 轮数 + self.deck = [i%CARDS_CNT2+1 for i in range(CARDS_CNT)] + self.useCards_i=0#放回deck + + def reset_game(self):#重置游戏进度 + self.__reset() + self.roundGame_i=0 + self.levelOfBoth = [2, 2] + self.dealer = -1 + self.lordNum = 2 # 级牌 + for i in range(4): + self.players[i].setNum(2) + def calcGameScore(self):#结算一局游戏的分数,更新级牌,但不重置游戏进度,preSc代表上局闲家的分数,返回-1代表继续游戏,返回庄家编号代表谁先达到A + preSc=self.sumSc + self.__reset() + grade = self.getGrade(preSc)#结算等级 + if grade>=0:#换庄 + g=grade + self.dealer =(self.dealer+1)%4#换庄 + a = self.levelOfBoth[self.dealer%2] + if 2<=a <= 5: # 5,10,k不能跳 + self.levelOfBoth[self.dealer%2] = min(a + g, 5) + elif 50: + return self.getWinPlayer(),self.dealer,grade + else: + g = -grade + self.dealer = (self.dealer + 2) % 4 # 换同伙坐庄 + a = self.levelOfBoth[self.dealer%2] + if 2 <= a < 5: # 5,10,k不能跳 + self.levelOfBoth[self.dealer % 2] = min(a + g, 5) + elif 5 <= a < 10: + self.levelOfBoth[self.dealer % 2] = min(a + g, 10) + elif 10 <= a < 13: + self.levelOfBoth[self.dealer % 2] = min(a + g, 13) + else: + self.levelOfBoth[self.dealer % 2] = 1 + if a==1 or a==13 and g>1: + return self.getWinPlayer(),self.dealer,grade + self.lordNum=self.levelOfBoth[self.dealer%2] + self.roundGame_i+=1 + for i in range(4): + self.players[i].setNum(self.lordNum) + return self.getWinPlayer(),-1,grade + def getWinPlayer(self): + grade = self.getGrade(self.sumSc) # 结算等级 + winplayer=self.dealer + if grade>=0: + winplayer=(winplayer+1)%4 + return winplayer + def setDealerID(self,dealer): + self.players[dealer].dealerTag=0 + self.players[(dealer+1)%4].dealerTag = 2 + self.players[(dealer+2)%4].dealerTag = 1 + self.players[(dealer + 3) % 4].dealerTag =3 + + def getNowUsedCards(self): + cardList=self.deck[0:self.useCards_i] + return cardList + + def _updateSortCardList(self,sortCardList:list,act:list): + if sortCardList == None: + return + for a in act: # 刷新sortCardList + kind, num = getKindAndNum(a, self.lordDecor, self.lordNum) + li = sortCardList[kind] # 遍历这个花色 + for i in range(len(li[0])): # 从单牌里删除 + if li[0][i] == a: + li[0].pop(i) + break + for i in range(len(li[1])): # 从对子里删除 + if li[1][i] == a: + li[1].pop(i) + break + # print(sortCardList) + for i in range(len(li[2])): + # print(li[2]) + m = len(li[2][i]) + for j in range(m): # 遍历每个拖拉机 + if li[2][i][j] == a: + tra = li[2].pop(i) + beginTra = tra[:j] + endTra = tra[j + 1:] + if len(beginTra) > 1: # 剩下的长度大于1,放回li[2] + li[2].append(beginTra) + if len(endTra) > 1: # 剩下的长度大于1,放回li[2] + li[2].append(endTra) + break + else:#这段用来代替goto,表示找到if时,跳出2层循环。python用goto要单独安装包 + continue + break + # print(sortCardList) + li[2].sort(key=cmp_to_key(self._sortCardList_cmp2)) + + def checkReDealCards(self): + if self.lordDecor<4 and self.players[0].getLordCnt()+ self.players[2].getLordCnt()<10: + return True + if self.lordDecor<4 and self.players[1].getLordCnt()+ self.players[3].getLordCnt()<10: + return True + # for i in range(4): + # if self.players[i].getLordCnt()>=HANDCARD_CNT-2: #一个人几乎全是主牌,可以重开 + # return True + # fen=0 + # p=self.players[i] + # for j in range(HANDCARD_CNT): + # fen+=fenInd[getNum(int(p.cards[j]))] + # if fen<15: + # return True + return False + def printCardsList(self,li,up=-1): + if up<0: + up=len(li) + for i in range(up): + printCard(li[i],i) + print("") + def printAllCards(self): + for i in range(4): + self.players[i].printCards() + print("底牌",end=": ") + self.printCardsList(self.deck[-8:]) + # def getBidAction(self,): + + def CrossShuffle(self):#交叉洗牌 + for i in range(CARDS_CNT // 2-1,-1,-1): # 交叉洗牌 + rnd1, rnd2 = i, i * 2 + self.deck[rnd1], self.deck[rnd2] = self.deck[rnd2], self.deck[rnd1] + + def shuffleDeck(self,T=10): + up=len(self.deck)-1 + self.CrossShuffle() + # print(self.deck) + while(T>0): + self.CrossShuffle() + + for i in range(up, 0, -1): + rnd = random.randint(0, i) # 每次随机出0-i-1之间的下标 + self.deck[i], self.deck[rnd] = self.deck[rnd], self.deck[i] + T-=1 + + + def dealCards(self,beginDeckList,bidFun,lordInfo=(-1,-1,-1)):# + # 发牌,bidFun是叫牌策略 + #setDecor=-1,setNum=-1,setDealer=-1代表 + setDecor, setNum , setDealer=lordInfo[0],lordInfo[1],lordInfo[2] + if beginDeckList is None: + self.shuffleDeck() # 洗牌 + else: + for i in range(len(self.deck)): + self.deck[i]=beginDeckList[i] + # print(self.deck.tolist()) + self.deck_i = 0 + self.nowDealCardPlayer = 0 + for i in range(4): + self.players[i].initCards() + #bidFun(env,round,allActList) + isSet=setDecor!=-1 or setDealer!=-1 or setNum>0 + if isSet:#人为设置,调试用 + self.lordNum = setNum + + print("指定主牌:",decorName[setDecor]+str(setNum)," 庄家:",setDealer) + self.dealer = setDealer + self.bidAns = [toCardIndex(setDecor,self.lordNum)] + self.bidPlayer =setDealer + while (self.deck_i < CARDS_CNT - UNDERCARD_CNT): + self.__dealCard(bidFun) + self.setLord(setDecor, setDealer) + else: + self.bidAns=[] + self.bidPlayer =-1 + while (self.deck_i < CARDS_CNT - UNDERCARD_CNT): + self.__dealCard(bidFun) + if self.bidPlayer!=-1: + if self.dealer==-1: + self.setLord(getDecor(self.bidAns[0]), self.bidPlayer) + else: + self.setLord(getDecor(self.bidAns[0])) + else:# 如果没人叫主,则重新发牌 + # print("重新发牌") + self.dealCards(beginDeckList, bidFun, lordInfo) + for i in range(4): + p=self.players[i] + p.mergeLords() + self.sortPlayerHand(p) + self.setDealerID(self.dealer) + + self.undeck = [] # 存数组 + if self.checkReDealCards() and not isSet: # 满足一定条件可以重开 + # print("重新发牌") + self.dealCards(beginDeckList, bidFun, lordInfo) + + + def __dealCard(self,bidFun):#发牌 + card=self.deck[self.deck_i] + p=self.players[self.nowDealCardPlayer] + p.addCard(getKind(card,4,self.lordNum),card)#级牌放入无主里 + allActList=[[]]#空列表代表不叫牌 + for i in range(4): + a=toCardIndex(i, self.lordNum) + n=len(self.bidAns) + if p._lordnumAndKing_cnt[i]==1 and n==0: + allActList.append([a]) + elif p._lordnumAndKing_cnt[i]==2 and n==0: + allActList.append([a]) + allActList.append([a,a]) + elif p._lordnumAndKing_cnt[i]==2 and n==1: + allActList.append([a,a]) + if p._lordnumAndKing_cnt[4]==2 and (len(self.bidAns)<2 or len(self.bidAns)==2 and self.bidAns[0]<53):#对小王可以反无主 + allActList.append([53,53]) + if p._lordnumAndKing_cnt[5]==2 and (len(self.bidAns)<2 or len(self.bidAns)==2 and self.bidAns[0]<54):#对大王可以反无主 + allActList.append([54,54]) + # dfsPrintActList(allActList) + act_id = bidFun(self,p,self.deck_i//4,allActList) + act=allActList[act_id] + self.deck_i += 1 + # self.snatchLord_v0(self.nowDealCardPlayer) # 抢主 + + #self.printAllCards() + if len(act) > 0 : + # print("玩家"+str(self.nowDealCardPlayer)+"叫了:",end=" ") + dfsPrintActList(act) + self.bidAns=act + self.bidPlayer=self.nowDealCardPlayer + self.nowDealCardPlayer=(self.nowDealCardPlayer+1)%4 + + def setUnderCards(self,discardFun):#换牌, + p = self.players[self.dealer] + self.underCards=[] + for a in self.deck[-8:]: + p.addCard(getKind(a,self.lordDecor,self.lordNum),a) + self.sortPlayerHand(p) + for i in range(8): + a=discardFun(self,p,i)#选择八张底牌扣下 + self.underCards.append(a) + p.delCard(getKind(a,self.lordDecor,self.lordNum),a) + if len(self.underCards)!=UNDERCARD_CNT: + print("erro",self.underCards) + exit() + # dfsPrintActList(self.underCards) + self.deck[-8:]=self.underCards + self.sortPlayerHand(p) + + for i in range(4): + self.players[i].initCards_orderCards_cnt(self) + # for i in range(5): + # env.printCardsList(p.cards_decorList[i], p.cards_decorLen[i]) + # print("") + def setOrder(self):#设置单牌的大小 + self.orderInd = np.zeros((CARDS_CNT+1),dtype='int') + self.decorInd = np.zeros((CARDS_CNT + 1),dtype='int')#花色分类,大小王和级牌都算主牌里 + h=CARDS_CNT//2 + self.orderInd[0]=-1 + self.orderInd[h]=99#大王 + self.orderInd[h-1]= 98 # 小王 + for j in range(4): + for i in range(1,14): + self.orderInd[j*13+i]=i + self.decorInd[j * 13 + i] =j + self.orderInd[j * 13+1] =14#A更大 + for i in range(1, 14):#主牌更大 + self.orderInd[self.lordDecor * 13 + i]=i+40 + self.decorInd[self.lordDecor * 13 + i] = 4 + self.orderInd[self.lordDecor * 13 + 1]= 14+40 # A更大 + for i in range(4):#级牌 + self.orderInd[self.lordNum+i*13]=60 + self.decorInd[self.lordNum+i*13] = 4 + self.orderInd[self.lordNum + self.lordDecor * 13] = 61 + + #紧凑化 + ind=[0]*100 + for i in range(1,h+1): + d=int(self.orderInd[i]) + ind[d]+=1 + k=1 + for i in range(1,100): + if ind[i]>0: + ind[i] = k + k+=1 + for i in range(1,h+1): + self.orderInd[h+i] = self.orderInd[i]=ind[int(self.orderInd[i])] + self.decorInd[h+i] = self.decorInd[i] + self.unlordMax=self.lordDecor==4 and 13 or 12#无主就是13 + + def getDecor(self,id):#id从1开始 + return (id - 1) // 13 + def cmpCard(self,a,b):#比较两组牌大小。返回1是a大,返回0是b大 + if a==0 or b==0 : + return b==0 + # print(self.orderInd) + num1=self.orderInd[a] + num2=self.orderInd[b] + d1, d2 = self.getDecor(a), self.getDecor(b) + if num1<=self.unlordMax and num2<=self.unlordMax:#都不是主 + if d1==d2:#花色相同 + return num1>=num2 + else: + return d1>=d2 + elif num1>self.unlordMax and num2>self.unlordMax: # 都是主牌 + if num1==num2:#地位相同 + return d1>=d2 + else: + return num1>num2 + return num1>self.unlordMax + def _sortCardList_cmp(self,a,b):#比较两组牌大小。返回1是a大,返回-1是b大 + if self._ind[a]>1 and self._ind[b]>1 or self._ind[a]==1 and self._ind[b]==1: + num1 = self.orderInd[a] + num2 = self.orderInd[b] + if num1<=self.unlordMax and num2<=self.unlordMax:#都不是主牌 + if self.getDecor(a) == self.getDecor(b): # 花色相同 + return num1 >= num2 and -1 or 1 + return self.getDecor(a) > self.getDecor(b) and -1 or 1 + return num1>=num2 and -1 or 1 + elif self._ind[a]>1: + return -1 + return 1 + def _sortCardList_cmp1(self,a,b):#比较两组牌大小。返回1是a大,返回0是b大 + return (self.cmpCard(a,b) and -1 or 1) + def _sortCardList_cmp2(self,a,b):#比较两组牌大小。把拖拉机排序 + lena = len(a) + lenb = len(b) + num1 = self.orderInd[a[0]] + num2 = self.orderInd[b[0]] + if lena==lenb: + if num1<=self.unlordMax and num2<=self.unlordMax:#都不是主牌 + if self.getDecor(a[0]) == self.getDecor(b[0]): # 花色相同 + return num1 >= num2 and -1 or 1 + return self.getDecor(a[0]) > self.getDecor(b[0]) and -1 or 1 + return num1>=num2 and -1 or 1 + return lena>lenb and -1 or 1 + def sortCardList2(self,actList):#对卡牌排序,找出几个拖拉机,几个对子,几个单牌,会重叠,并且要求是相同颜色 + self._ind=np.zeros((CARDS_CNT2+1)) + n=len(actList) + cnt2 = 0 + for i in range(n): + self._ind[actList[i]]+=1 + if self._ind[actList[i]]==2: + cnt2+=1 + actList = sorted(actList, key=cmp_to_key(self._sortCardList_cmp)) + traList=[] + doubleList = [] + prePos=-1 + for i in range(cnt2-1):#找拖拉机 + next_i=(i+1)*2 + s = self.orderInd[actList[i * 2]] - self.orderInd[actList[next_i]] + if (s == 0 or s== 1): + if prePos==-1: + prePos=i*2 + elif prePos!=-1: + traList.append([actList[t] for t in range(prePos,i*2+2,2)]) + prePos=-1 + doubleList.append(actList[i*2]) + if cnt2!=0: + if prePos != -1 : + traList.append([actList[t] for t in range(prePos,cnt2*2,2)]) + doubleList.append(actList[(cnt2-1) * 2]) + traList = sorted(traList, key=cmp_to_key(self._sortCardList_cmp2)) + actList = sorted(actList, key=cmp_to_key(self._sortCardList_cmp1)) + return actList,doubleList,traList + def sortCardList1(self,actList,doubleList,traList):#对卡牌排序,找出几个拖拉机,几个对子,会把不同类别区分开(去重)//,要提前调用sortCardList2 + cnt2=len(doubleList) + ind=np.zeros(55,dtype=int) + for tra in traList: + for a in tra: + ind[a]+=1 + for a in doubleList: + ind[a] += 1 + doubleList1 = [] + actList1=[] + for a in actList: + if ind[a]==0: + actList1.append(a) + for a in doubleList: + if ind[a]==1: + doubleList1.append(a) + return actList1,doubleList1,traList + def _isLord(self,actList): + for a in actList: + if self.orderInd[a]<=self.unlordMax: + return False + return True + def isLord(self,act:Action): + if self._isLord(act.one) == False: + return False + for dou in act.double: + if self._isLord(dou)==False: + return False + return True + def getActKind(self,act:Action):#获得一个动作类别,如果什么都有或者为空返回INF + kind=INF + for a in act.one: + k=getKind(a,self.lordDecor,self.lordNum) + if kind!=INF and kind!=k: + return INF + kind=k + for dou in act.double: + for a in dou: + k = getKind(a, self.lordDecor, self.lordNum) + if kind != INF and kind != k: + return INF + kind = k + return kind + def _useCmpCards(self,act1:Action,act2:Action):#必须二者是同一种类别!!! + n=len(act1.double) + for i in range(n):#先判断2是否为对子 + for j in range(0,len(act1.double[i]),2): + if act2.double[i][j]!=act2.double[i][j+1]: + return True + if n>0:#有对子或者拖拉机,就看对应牌的大小 + for i in range(n): # 先判断act2是否为拖拉机 + tra1=act1.double[i] + tra2=act2.double[i] + for j in range(2, len(tra2), 2): + if abs(self.orderInd[tra2[j]]- self.orderInd[tra2[j-2]])>1:#是相邻的 + return True + if self.orderInd[tra1[0]] >= self.orderInd[tra2[0]]:#只有前面的牌比后面的牌大,就返回True + return True + return False + for i in range(len(act1.one)): # 最后判断单牌 + if self.orderInd[act1.one[i]] != self.orderInd[act2.one[i]]: # 单牌且不等 + return self.cmpCard(act1.one[i], act2.one[i]) + return True # 完全想等,先出的大 + + + def useCmpCards(self,act1:Action,act2:Action):#比较两组牌大小,保证两组牌一样多,且actList1为先出,actList2为后出。返回1是actList1大,否则actList2大 + n=act1.len + k1 = self.getActKind(act1)#判断什么类型 + k2 = self.getActKind(act2) + if k1==self.lordDecor and k2==self.lordDecor:#如果都是主牌,先判断拖拉机,在判断单对子 + return self._useCmpCards(act1,act2) + elif k1!=self.lordDecor and k2!=self.lordDecor:#如果都是非主牌,判断花色,花色相同比大小,否则先出的大。 + if k2!=k1:#act2是杂牌 + return 1 + return self._useCmpCards(act1,act2) + elif k1==self.lordDecor:#先出的是主牌,后出的不是,先出的大 + return 1 + else:#先出的不是主牌,后出的是主牌,要看主牌能否完全管上先出的。这里先出的一定是同一花色。主牌要杀拖拉机,只需要相应数量对子。 + for i in range(len(act1.double)): # 先判断2是否为对子或拖拉机 + for j in range(0, len(act1.double[i]), 2): + if act2.double[i][j] != act2.double[i][j + 1]: + return 1 + return 0 + + # def judgeRoundWin(self): + def getOrderID(self,a):#从0开始,非主牌是[0,11],主牌[0,15] + k = self.orderInd[int(a)] + if k > self.unlordMax: + k -= self.unlordMax + return int(k-1) + + def getMaxCards_cmp1(self,a): + return a==2 + def getMaxCards_cmp2(self,a): + return a>0 + def getMaxCards(self,p:Player): # 返回已知的玩家手里最大的牌,来判断先手玩家是否可以甩牌,sortCardsList2是一名玩家分好类的手牌,且会相互包含 + li=[] + sortCardsList2=p.toSortCardsList2(self) + for kind in range(5): + if kind==self.lordDecor:#主牌不能甩 + continue + k1 = p.getSelfMaxCard(kind, self.getMaxCards_cmp1)#从大到小找到第一个没有出过2张牌的牌的顺序号 + k2 = p.getSelfMaxCard(kind, self.getMaxCards_cmp2)#从大到小找到第一个出过0张牌的牌的顺序号 + act=Action() + for a in sortCardsList2[kind][0]: + k=self.getOrderID(a) + if k>=k1:#看比它大的有多少已经出过2张,如果都出过2张,说明它是最大的牌 + act.addOne(a) + for a in sortCardsList2[kind][1]:#看是不是最大的对子 + k = self.getOrderID(a) + if k>=k2:#看有多少出了0张,如果都没有出了0张 + act.addDou([a,a]) + maybeTra=[] + seqCnt=0 + + for i in range(p.orderCards_len[kind] - 1, -1,-1):#寻找所有可能的拖拉机 + if i == p.lordNumOrder: + c=0 + for j in range(4): + if j != p.lordDecor: + c+=p.lordNumSee_cnt[j]==0 + seqCnt+=c + if c<3 and seqCnt>0: + if seqCnt>1: + maybeTra.append((i,seqCnt)) + # maxSeqCnt = max(maxSeqCnt, seqCnt) + seqCnt = 0 + elif p.orderCards_cnt[kind][i]==0: + seqCnt+=1 + elif seqCnt>0: + if seqCnt > 1: + maybeTra.append((i+seqCnt,seqCnt)) + seqCnt = 0 + if seqCnt>1: + maybeTra.append((-1+seqCnt,seqCnt)) + # if p.id == 0: + # print(maybeTra) + for traList in sortCardsList2[kind][2]:#看自己的拖拉机 + n=len(traList) + k = self.getOrderID(traList[0]) + for tra in maybeTra: + # if p.id == 0: + # print(tra, n) + if tra[1]>=n and tra[0]>k:#可能的拖拉机的长度大于等于自己的拖拉机,且可能的拖拉机比我方的次序大。 + break + else:#正常结束循环,说明我方的拖拉机最大 + act.addDou([traList[i//2] for i in range(len(traList)*2)]) + if act.isSeq(): + li.append(act) + return li#返回一个5元列表,代表一定可以甩的牌、 + def updatesortCardList2(self): + self.sortCardsList2=[] + for i in range(4): + li=self.players[i].toSortCardsList2(self) + self.sortCardsList2.append(li) # 会重叠 + def _judgeSeqUse(self,act:Action):# + # 是否为甩牌,是否可以出 + self.updatesortCardList2() + sortCardList4=self.sortCardsList2 + if act.isSeq():#是甩牌 + mina=INF + card=0 + for a in act.one:#找出最小的牌 + if mina>self.orderInd[a]: + mina=self.orderInd[a] + card=a + kind, num = getKindAndNum(card,self.lordDecor,self.lordNum) + if card>0: + for pid in range(4): + if pid != act.playerId: + li=sortCardList4[pid] + if len(li[kind][0])>0 and self.orderInd[li[kind][0][0]]>self.orderInd[card]:#寻找这个花色所有比card顺序大的牌: + return True,False + + for pid in range(4): + if pid != act.playerId:#遍历每个人手牌的对子。 + li = sortCardList4[pid] + for dou in act.double: + kind, num = getKindAndNum(dou[0], self.lordDecor, self.lordNum) + lendou=len(dou)#它是li里面拖拉机牌数的2倍,比如3344,则li里面表示为34 + if lendou==2: + a=li[kind][1] + if len(a) > 0 and self.orderInd[a[0]] > self.orderInd[dou[0]]: # 寻找这个花色所有比card顺序大的牌: + return True, False + else: + tractors = li[kind][2] + for tra in tractors: + if len(tra)*2>=lendou and self.orderInd[tra[0]] > self.orderInd[dou[0]]: + return True, False + return True, True + return False,True + + def firstPolicy(self,firstPlayerId,firstPolicyFun):#第一个人出牌,policyFun是一个回调,代表如果有甩牌,甩牌的出牌策略 + p=self.players[firstPlayerId] + allActList=self.getFirstAllAction(p) + act_id=firstPolicyFun(self,p,Action([],[],playerId=p.id),allActList) + act = allActList[act_id] + if isinstance(act,Action) :#act是action代表要使用的动作 + # act.println() + p.useAction(self,act) + return act + else:#act是list代表要甩牌的类 + kind=act[0] + pHandCards=p.cards[kind] + cardsList=[] + act=Action([],[],playerId=p.id) + for i in range(len(pHandCards)): + allActList=[] + if i>=2: + allActList.append([]) + for i in range(len(pHandCards)): + a=pHandCards[i] + allActList.append([a]) + act_id=firstPolicyFun(self,p,act,allActList) + if len(allActList[act_id])==0: + break + p.useCardsList(self, allActList[act_id]) + cardsList.append(allActList[act_id][0]) + act = cardsListToAction(self, p.id, cardsList) + + + isSeq, canSeq = self._judgeSeqUse(act) + if isSeq and canSeq == False: # 如果不能甩 + # print("不能甩!!!") + if self.players[act.playerId].dealerTag < 2: # 是庄家甩牌失败 + self.sumSc += 10 + else: + self.sumSc = max(0, self.sumSc - 10) + p.addCardsList(self, cardsList) + act= act.getMinCard(self) + p.useAction(self, act) + return act + + def getFirstAllAction(self,p:Player):#返回作为先出玩家的所有可能的动作(不包含甩牌),sortCardsList1是一名玩家分好类的手牌,且会相互包含 + ans=[] + sortCardsList2= p.toSortCardsList2(self) # 会相互包含 + # maxCards=self.getMaxCards(sortCardsList1,p) + for i in range(5): + for a in sortCardsList2[i][0]: + ans.append(Action([a],playerId=p.id)) + for a in sortCardsList2[i][1]: + ans.append(Action([],[[a,a]],playerId=p.id)) + for tractor in sortCardsList2[i][2]:#拖拉机 + ltr=len(tractor) + for begin_i in range(ltr): + tractor_tmp = [tractor[begin_i],tractor[begin_i]] + for begin_j in range(begin_i+1,ltr): + tractor_tmp.append(tractor[begin_j]) + tractor_tmp.append(tractor[begin_j]) + ans.append(Action([],[tractor_tmp.copy()],playerId=p.id)) + for i in range(4): + if i!=self.lordDecor and len(p.cards[i])>1: + ans.append([i]) + return ans + def getAllAct_dfs(self,doubleList,doubleList_i,ansDown,nowList,nowList_i,n): + if nowList_i==n: + ansDown.append(nowList.copy()) + return + doubleList_len=len(doubleList) + for i in range(doubleList_i,doubleList_len): + if doubleList_len-i 0 + ansDown=self.getFollowAct_one(p,firstAct[0],kind) + if kind == self.lordDecor or c0 > 0: # 是主牌或副牌,但有这类副牌 + for i in range(c0): + a = toSortCardsList2[kind][0][i] + if self.orderInd[a] > self.orderInd[firstAct[0]]: + ansUp.append([a]) + else: # 用主牌杀 + for i in range(len(p.cards[self.lordDecor])): + a = p.cards[self.lordDecor][i] + ansUp.append([a]) + elif n == 2: + isHave = c1>0 + if kind == self.lordDecor or c1>0: # 这类牌有对子 + for a in toSortCardsList2[kind][1]: + if self.orderInd[a] > self.orderInd[firstAct[0]]: + ansUp.append([a,a]) + elif len(p.cards[kind]) == 0: # 用主牌杀 + for a in toSortCardsList2[self.lordDecor][1]: + ansUp.append([a,a]) + if c1>0: + for a in toSortCardsList2[kind][1]: + if self.orderInd[a] <= self.orderInd[firstAct[0]]: + ansDown.append([a, a]) + else: + ansDown.append([INF ,INF]) #比如先手出了66,而自己有7,则此时依然返回INFINF + else:#拖拉机 + #较大的拖拉机和较小的拖拉机 + if kind == self.lordDecor or c2>0: # 这类牌有拖拉机 + for tractor in toSortCardsList2[kind][2]: + for j in range(len(tractor)-n2+1):#比如最大的拖拉机是3344,而自己手里又55667788,则此时有3种出牌方式管上它 + isHave = True + if self.orderInd[tractor[j]]>self.orderInd[firstAct[0]]: + ansUp.append([tractor[j+k//2] for k in range(n)]) + else: + ansDown.append([tractor[j + k // 2] for k in range(n)]) + elif len(p.cards[kind]) == 0: # 用主牌杀 + for tractor in toSortCardsList2[self.lordDecor][2]: + for j in range(len(tractor)-n2+1): + ansUp.append([tractor[j+k // 2] for k in range(n)]) + + if not isHave:#如果没有同样长度的拖拉机,则从对子里出 + tempLi = [] + if len(toSortCardsList2[kind][1]) > n2: + self.getAllAct_dfs(toSortCardsList2[kind][1], 0, ansDown, tempLi, 0, n2) # 从对子里选取所有组合 + else: + for a in toSortCardsList2[kind][1]: + tempLi.append(a) + tempLi.append(a) + tempLi_len = len(tempLi) + for i in range(tempLi_len, n): + tempLi.append(INF) + # 剩余牌优先从这个颜色里选取 + #没有这个颜色再从其他任意牌选 + #比如先手是'<♥A>', '<♥A>', '<♥K>', '<♥K>', '<♥Q>', '<♥Q>',我有JJQQ9,此时会返回JJQQINFINF + ansDown.append(tempLi) + + return ansUp,ansDown,isHave + + def getFollowAct_one(self, p: Player, card_max, kind): # 返回小于某张牌的全部牌 + ans=[] + if len(p.cards[kind])>0:#大于0 + for i in range(len(p.cards[kind])): + a=p.cards[kind][i] + if self.orderInd[a]<=self.orderInd[card_max]: + ans.append([a]) + else:# + for i in range(5): + if i!=self.lordDecor: + for j in range(len(p.cards[i])): + ans.append([p.cards[i][j]]) + return ans + + def __otherPolicy(self, p:Player,firstAct:Action,usedAct,cards, otherPolicyFun): + allActList_up,allActList_down,isHave = self.getAllAction(p, cards) + allActList=allActList_down+allActList_up + act_id = otherPolicyFun(self, p, usedAct, allActList) + act_ans = allActList[act_id] + firstKind=firstAct.getKind(self) + doulasti=len(usedAct.double)-1 + p.useCardsList(self, act_ans) + usedList=[] + for i in range(len(act_ans)): + if act_ans[i]==INF: + if len(p.cards[firstKind])==0:#没有这个类别的牌就从其他牌里选 + allActList_1 = p.otherKindCards(firstKind) + else:#从这个类别里选 + allActList_1=p.cards[firstKind].copy() + act_id_1 = otherPolicyFun(self, p, usedAct, allActList_1) + # dfsPrintActList(allActList_1) + # print(act_id_1,act_ans) + act_ans[i]=allActList_1[act_id_1] + usedList.append(act_ans[i]) + p.useCardsList(self, [act_ans[i]]) + usedAct.setDou(doulasti, usedList.copy()) # 设置牌 + return act_ans + def otherPolicy(self,act4,firstPlayerID,nowPlayerId,otherPolicyFun): + # 第一个人出牌,otherPolicyFun是一个回调,代表如果跟牌太多, + p = self.players[nowPlayerId] + firstAct=act4[firstPlayerID] + act_ans = Action([], [], playerId=p.id) + for a in firstAct.one: + li = self.__otherPolicy(p,firstAct,act_ans,[a],otherPolicyFun) + act_ans.add(li) + for dou in firstAct.double: + act_ans.addDou([]) + li=self.__otherPolicy(p, firstAct, act_ans, dou, otherPolicyFun) + act_ans.setDou(len(act_ans.double)-1, li) # 设置牌 + return act_ans + + def step(self,actList4,first_playerId):#输入4个人的出牌组合,保证每个人出的牌数量一样多,返回赢得那个人和所得分数 + playerId=first_playerId + actList4[playerId].sort(self)# + for i in range(1,4): + k=(first_playerId+i)%4 + actList4[k].sort(self)# + if self.useCmpCards(actList4[playerId],actList4[k])==0:#actList4[k]大 + playerId=k + self.round_i+=1 + #找找到最大的那个人 + sc=0 + n=actList4[first_playerId].len#所有人的牌数量都是n + for i in range(4): + sc+=actList4[i].getFen() + if self.players[playerId].dealerTag>1: + self.sumSc+=sc + leftCardsCnt=0 + for i in range(4): + leftCardsCnt+= self.players[i].len + if leftCardsCnt==0:#游戏结束,结算底牌分数 + sc_under = rate=0 + # if self.players[playerId].dealerTag > 1: # 不是庄家赢 + for a in self.underCards: # 结算底牌分数 + num = getNum(a) # 点数,[1,13]王是14 + sc_under += fenInd[num] # 分数 + rate = 2 + if n > 1: # 判断倍数 + doubleCnt = actList4[playerId].getDouleCnt() * 2 + if doubleCnt > 0: # 每有一个对子,倍数+2 + rate = 2 + doubleCnt + else: # 普通甩牌没有对子,倍数+1 + rate = 2 + 1 # 普通甩牌为3倍 + if self.players[playerId].dealerTag>1:#不是庄家赢 + self.sumSc += sc_under*rate + self.undeck+=self.underCards + return playerId, sc,True,sc_under*rate + return playerId,sc,False,0#返回赢得玩家id,本轮得分,是否结束游戏,结算信息 + def getGrade(self,sc): + if sc<80: + return (sc==0 and 0 or (sc//40+1))-3#判断庄家升几级 + else: + return sc // 40-2#如果不足120仅仅换庄,不升级 + def setLord(self,kind,playerID=-1): + self.lordDecor=kind + if playerID!=-1: + self.dealer=playerID + for i in range(4): + self.players[i].setLord(kind,self.lordNum) + self.setOrder() + def getLord(self): + return self.lordNum+self.lordDecor*13 + + def sortPlayerHand(self,player): + for i in range(5): + player.cards[i].sort(key=cmp_to_key(self._sortCardList_cmp1)) + + def _getActListMaxCmp(self,a:Action,b:Action): + if a.len==b.len: + la,lb=a.getDouleCnt(),b.getDouleCnt() + if la==lb and la==0: + return self.orderInd[b.one[0]]-self.orderInd[a.one[0]] + elif la==lb: + for i in range(min(len(a.double),len(b.double))): + if len(a.double[i])!=len(b.double[i]): + return len(b.double[i])-len(a.double[i]) + else: + return self.orderInd[b.double[0][0]]-self.orderInd[a.double[0][0]] + return lb-la + return b.len-a.len + def getActListMax(self,allActionList):#从动作列表里寻找最大的动作 + for act in allActionList: + act.sort(self) + allActionList.sort(key=cmp_to_key(self._getActListMaxCmp)) + return allActionList[0] + + + def printAllInfo(self,nowpid=None, act=None): # act代表4个人每个人出的牌,类型是Action + if nowpid!=None: + print("当前玩家:"+str(nowpid), end=",") + print("双方等级", self.levelOfBoth,end=",") + print("主牌:", decorName[self.lordDecor] + str(self.lordNum), " 庄家:", self.dealer) + self.printAllCards() + if act!=None and isinstance(act[0],Action): + for i in range(4): + act[i].println(i) + print("当前闲家的分" + str(self.sumSc)) + def printUnderCards(self): # act代表4个人每个人出的牌 + self.printCardsList(self.deck[-8:]) + def getPlayNowGrade(self,playerId): + g=self.levelOfBoth[playerId%2] + if g==1: + g=14 + return g \ No newline at end of file diff --git a/rlcard/games/tractor/tractor_gameUtil.py b/rlcard/games/tractor/tractor_gameUtil.py new file mode 100644 index 000000000..e51f162e6 --- /dev/null +++ b/rlcard/games/tractor/tractor_gameUtil.py @@ -0,0 +1,171 @@ +import math +import random +from functools import cmp_to_key +import numpy as np + + +CARDS_CNT=108 +CARDS_CNT2=54 +UNDERCARD_CNT=8 +INF=1000 +HANDCARD_CNT = (CARDS_CNT - UNDERCARD_CNT) // 4 +fenInd=[0,0,0,0,0,5,0,0,0,0,10,0,0,10,0,0,0,0,0,0] +decorName=["♠","♥","♣","♦","王"] +NameTodecorId={"♠":0,"♥":1,"♣":2,"♦":3,"王":4} +numName=["","A","2","3","4","5","6","7","8","9","10","J","Q","K"] + +def getKind(id,dec,num): # id从1开始,判断类别,会把级牌和王算进主牌里 + if id<1: + return 0 + d = (id - 1) // 13 # 黑桃是0,红糖是1,梅花是2,方片是3,王是4 + n= (id - 1) % 13 + 1 # 点数 + if d==4 or n==num: + return dec + return d +def getDecor(id): # id从1开始,判断花色 + return int((id - 1) // 13) +def getNum(id): # id从1开始,判断点数 + return int((id - 1) % 13+1) +def getDecorAndNum(id): + if id<1: + return 0,0 + decor = (id - 1) // 13 # 黑桃是0,红糖是1,梅花是2,方片是3,王是4 + num = (id - 1) % 13 + 1 # 点数 + return int(decor),int(num) +def getKindAndNum(id,dec,num): + if id<1: + return 0,0 + d = (id - 1) // 13 # 黑桃是0,红糖是1,梅花是2,方片是3,王是4 + n = (id - 1) % 13 + 1 # 点数 + if d==4 or n==num:## 黑桃是0,红糖是1,梅花是2,方片是3,王是4 + return dec,num + return int(d),int(n) +def toCardIndex(decor,num): + return num+decor*13 +def cardToString(id,up=False):# + decor, num = getDecorAndNum(id) + if id == INF: + return "<任意>" + elif num == 0: + return "" + elif decor == 4: + return "<"+ (num == 1 and "小" or "大") + "王>" + else: + return "<" + decorName[decor] + numName[num] + ">" +stringToCardId={} +def initStringToCard(): + stringToCardId["INF"]=INF + stringToCardId["大王"] = 54 + stringToCardId["小王"] = 53 + for i in range(1,CARDS_CNT2+1): + stringToCardId[cardToString(i)]=i + stringToCardId[cardToString(i).lower()] = i + stringToCardId[cardToString(i)[1:-1]] = i + stringToCardId[str(i)] = i +initStringToCard() + +def printCard(id,i=0): + decor,num=getDecorAndNum(id) + if id==INF: + print("", end=" ") + elif num==0: + print("", end=" ") + elif decor==4: + print("", end=" ") + else: + print("",end=" ") +def printCardln(id,i=0): + printCard(id,i) + print("") +def snatchLord_v0(env,p,round,allActList):#发牌时候抢主的常规策略 + if env.lordDecor>=0: + return -1 + + for i in range(4): + p=env.players[id] + c=p.cards_decorLen[i]+p.cards_decorLen[4] + + if c>=0 :#没人亮过,超过均值就亮牌,且牌数大于等于4,主牌一共36个,平局每人9个 + for j in range(p.cards_decorLen[4]): + decor,num=getDecorAndNum(p.cards_decorList[4][j]) + if num ==env.lordNum and decor==i: # 有级牌 + return i + return -1 +def randomUpdateINF(roundId,p,act:list, kind):#随机选取动作,kind是第一个出牌的玩家的花色。返回种类和在cards_decorList中位置的编号 + actList=[] + for j in range(p.cards_decorLen[kind]):#先看本花色有木有 + if p.cards_decorList[kind][j]!=0: + actList.append((kind,j)) + if len(actList)==0:#本花色没有,去其它花色找 + for i in range(5): + if i==kind or p.cards_decorLen[i]==0: + continue + for j in range(p.cards_decorLen[i]): + if p.cards_decorList[i][j] != 0: + actList.append((i,j)) + ans=actList[random.randint(0,len(actList)-1)] + return ans[0],ans[1] +def getAllAct(self, sortCardsList, p, cardsList_max, kind): # sortCardsList是p玩家分好类的手牌 + # 返回所有大过之前最大的玩家的牌和小于之前玩家的牌 + # cardsList_max只能是单张,对子,连对 + # 小于玩家的牌有太多组合会被忽略 + n = len(cardsList_max) + ansUp = [] + ansDown = [] + if n == 1: # 单张牌 + ansDown = self.getFollowAct_one(p, cardsList_max[0], kind) + if kind == self.lordDecor or p.cards_decorLen[kind] > 0: # 是主牌或副牌,但有这类副牌 + for i in range(p.cards_decorLen[kind]): + a = p.cards_decorList[kind][i] + if self.orderInd[a] > self.orderInd[cardsList_max[0]]: + ansUp.append([a]) +def getActListFen(actList:list):#得到动作列表的所有分 + ans=0 + for act in actList: + ans+=act.getFen() + return ans +def searchFen(cardList): + ans=0 + for a in cardList: + num=getNum(a) + ans+=fenInd[num] + return ans + +# env=CC() +# env.dealCards(None,0,5,0)#发牌测试 +# # env.dfsPrintActList([9,27]) +# act1=Action([],[[4,4,6,6],[10,10]]) +# act2=Action([],[[8,8,9,9],[11,11]]) +# print(env.useCmpCards(act1,act2)) +# +# +# +# +# actList,doubleList,traList=env.sortCardList2([11,11,12,12,13,13,14,1,2,3,4,5,5,6,15,6,8,8,9,9]) +# print(actList,doubleList,traList) +# ansDown=[] +# nowList=[] +# begin_time = time.time() +# env.getAllAct_dfs(doubleList,0,ansDown,nowList,0,4) +# passed_time = time.time() - begin_time +# print(ansDown) +# print(passed_time) +# li=[39, 39, 23, 12, 26, 5, 53, 1, 38, 30, 46, 54, 48, 40, 36, 6, 28, 46, 26, 18, 7, 16, 27, 5, 22, 20, 47, 41, 41, 34, 8, 3, 31, 30, 13, 16, 23, 15, 48, 13, 51, 4, 37, 44, 33, 25, 52, 34, 9, 37, 21, 3, 17, 50, 29, 24, 51, 49, 38, 35, 43, 24, 6, 18, 32, 22, 29, 7, 20, 11, 19, 15, 36, 14, 42, 27, 45, 14, 12, 50, 45, 52, 31, 11, 42, 40, 47, 33, 54, 32, 8, 21, 10, 49, 9, 25, 53, 44, 1, 4, 17, 19, 10, 2, 35, 43,28,2] +# env.dealCards()#发牌测试 +# env=CC() +# env.dealCards() +# # actList,doubleList,traListt=env.sortCardList2(li) +# # print(env.dfsPrintActList(actList)) +# li=np.zeros(33,dtype=np.int32) +# li[0]=2 +# li = sorted(li, key=cmp_to_key(env._sortCardList_cmp1)) +# env.dfsPrintActList(li) +# print(env.orderInd) +# actList, doubleList, traListt =env.sortCardList2([1,3,53,53,5,5,31,31,3,4,4,6,6,8,8,11,12,1]) +# env.dfsPrintActList(actList) +# env.dfsPrintActList(doubleList) +# env.dfsPrintActList(traListt) + + + + diff --git a/rlcard/games/tractor/tractor_player.py b/rlcard/games/tractor/tractor_player.py new file mode 100644 index 000000000..f7c356aae --- /dev/null +++ b/rlcard/games/tractor/tractor_player.py @@ -0,0 +1,171 @@ +import math +import random +from functools import cmp_to_key + +import numpy as np + +from tractor_gameUtil import HANDCARD_CNT, CARDS_CNT2, printCard, getNum, getDecor, getKindAndNum, getKind, \ + getDecorAndNum + + +class Player(): + def __init__(self,id): + self.id=id + self.dealerTag=0 #0是庄家,1是庄家跟班,23是闲家 + self.initCards() + def initCards(self): + self.cards= [[] for i in range(5)] # 手牌 + self.uncards = [[] for i in range(5)] # 已经出的牌 + self.cards_lord=[]# 主牌 + self.len=0 + self.unlen = 0 + self._lordnumAndKing_cnt=np.zeros(6,dtype='int')# 自己所有牌中的级牌和王的数量,级牌是0,1,2,3.小王在4,大王在5 + + def initCards_orderCards_cnt(self,env):#初始化orderCards_cnt + z0 = np.zeros(13 + 5, dtype='int') + self.orderCards_cnt=[z0.copy(), z0.copy(), z0.copy(),z0.copy(), z0.copy()] + # orderCards_cnt[i][j]代表类别i的手牌中,顺序是j的有多少个。它包含了所有当前玩家可见的牌 + self.orderCards_len=[0, 0, 0, 0, 0]# 别人已经出的手牌数量 + self.lordNumSee_cnt=np.zeros(4,dtype='int')# 可见级牌数量 + + if self.lordDecor==4: + self.orderCards_len[4]=3#无花色情况下主牌有12张,顺序有3个 + for i in range(4): + self.orderCards_len[i]=13 + else: + for i in range(4): + self.orderCards_len[i] = 12 + self.orderCards_len[self.lordDecor] += 4 # 大小王级牌和主级牌 + self.lordNumOrder=env.getOrderID(54)-3#主级牌的顺序 + for i in range(5): + for j in range(len(self.cards[i])): + a=self.cards[i][j] + if self.isLevelCard(a):#是级牌 + self.lordNumSee_cnt[getDecor(a)]+=1 + k=env.getOrderID(a) + # print(a,k) + self.orderCards_cnt[i][k]+=1 + if self.dealerTag==0: + for a in env.underCards: + k=env.getOrderID(a) + kind,num=getKindAndNum(a,self.lordDecor,self.lordNum) + self.orderCards_cnt[kind][k] += 1 + if self.isLevelCard(a): + self.lordNumSee_cnt[getDecor(a)]+=1 + def isLevelCard(self,a): + return getNum(a)==self.lordNum and a<53 + def addCard(self,kind,card): + self.cards[kind].append(card) + self.len+=1 + decor,num=getDecorAndNum(card) + if num==self.lordNum: + self._lordnumAndKing_cnt[decor]+=1 + elif decor==4: + self._lordnumAndKing_cnt[num-1+4] += 1 + + def delCard(self,kind,card): + self.cards[kind].remove(card) + self.len-=1 + def delCards(self,cards): + for a in cards: + kind=getKind(a,self.lordDecor,self.lordNum) + self.cards[kind].remove(a) + self.len-=len(cards) + def mergeLords(self):#把级牌和大小王放入主牌所在花色,如果是无主则什么都不做 + if self.lordDecor!=4: + self.cards[self.lordDecor]+=self.cards[4] + self.cards[4]=[] + def group(self):#0是庄家,1是闲家 + return self.dealerTag//2 + def isDealer(self):#是否为庄家 + return self.dealerTag<2 + def setNum(self,num): + self.lordNum = num + def setLord(self,decor,num): + self.lordNum = num + self.lordDecor = decor + self.cards_lord = self.cards[decor] + def getLordCnt(self): + return len(self.cards[self.lordDecor]) + def getSelfMaxCard(self,decor,cmp):#cmp是比较函数,返回i,是牌的次序号,代表大于i的牌都满足cmp,i是从大到小第一个不满足cmp的牌顺序的编号。 + # 该函数返回自己牌以及自己见到的牌当中,某种花色从大到小第一张不满足cmp的顺序 + for i in range(self.orderCards_len[decor]-1,-1,-1): + if i==self.lordNumOrder: + for j in range(4): + if j!=self.lordDecor and not cmp(self.lordNumSee_cnt[j]): + return i + elif not cmp(self.orderCards_cnt[decor][i]): + return i + return -1 + def updateSortCardsList(self,env): + self.sortCardsList2=self.toSortCardsList2(env) + self.sortCardsList1=self.toSortCardsList1(self.sortCardsList2,env) + def toSortCardsList1(self,sortCardList2,env):#去重 + li=[] + for i in range(5): + # oneList, doubleList, traList = env.sortCardList1(self.cards_decorList[i]) + li.append((env.sortCardList1(sortCardList2[i][0],sortCardList2[i][1],sortCardList2[i][2]))) + return li + def toSortCardsList2(self,env):#相互包含 + li=[] + for i in range(5): + # oneList, doubleList, traList = env.sortCardList1(self.cards_decorList[i]) + li.append((env.sortCardList2(self.cards[i][0:len(self.cards[i])]))) + return li + def printCards(self): + print("玩家{}".format(self.id),end=" ") + k=0 + for i in range(5): + for j in range(len(self.cards[i])): + printCard(self.cards[i][j],k) + k+=1 + print("") + def addCardsList(self,env,cards): + cnt=0 + for a in cards: + if a<=0 or a>54: + continue + cnt+=1 + order = env.getOrderID(a) + kind, num = getKindAndNum(a,self.lordDecor,self.lordNum) + decor=getDecor(a) + self.uncards[kind].remove(a) + self.cards[kind].append(a) + env.undeck.remove(a) + for i in range(4): + if i != self.id: + env.players[i].orderCards_cnt[kind][order] -= 1 + if num == self.lordNum and decor < 4: + env.players[i].lordNumSee_cnt[decor] -= 1 + self.len+=cnt + self.unlen-=cnt + def useCardsList(self,env,cards): + cnt=0 + for a in cards: + if a<=0 or a>54: + continue + cnt+=1 + order = env.getOrderID(a) + kind, num = getKindAndNum(a,self.lordDecor,self.lordNum) + decor = getDecor(a) + self.cards[kind].remove(a) + self.uncards[kind].append(a) + env.undeck.append(a) + for i in range(4): + if i != self.id: + env.players[i].orderCards_cnt[kind][order] += 1 + if num == self.lordNum and decor < 4: + env.players[i].lordNumSee_cnt[decor] += 1 + self.len-=cnt + self.unlen+=cnt + def useAction(self,env,act): + self.useCardsList(env, act.one) + for dou in act.double: + self.useCardsList(env, dou) + + def otherKindCards(self, kind): # 获取所有kind以外类别的牌 + ans = [] + for i in range(5): + if i!=kind: + ans += self.cards[i].copy() + return ans diff --git "a/rlcard/games/tractor/\345\217\214\345\215\207\345\274\272\345\214\226\345\255\246\344\271\240\347\216\257\345\242\203\344\275\277\347\224\250\350\257\264\346\230\216.docx" "b/rlcard/games/tractor/\345\217\214\345\215\207\345\274\272\345\214\226\345\255\246\344\271\240\347\216\257\345\242\203\344\275\277\347\224\250\350\257\264\346\230\216.docx" new file mode 100644 index 0000000000000000000000000000000000000000..f8fa2d5fa30736475b67467946454d2c5fe328c3 GIT binary patch literal 23983 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 Date: Fri, 16 Jun 2023 16:13:01 +0800 Subject: [PATCH 2/2] add tractor game --- rlcard/envs/tractor.py | 8 ++++ rlcard/games/tractor/game.py | 67 ++++++++++++++++++++++++++++ rlcard/games/tractor/tractor_game.py | 33 ++++++++++---- 3 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 rlcard/envs/tractor.py create mode 100644 rlcard/games/tractor/game.py diff --git a/rlcard/envs/tractor.py b/rlcard/envs/tractor.py new file mode 100644 index 000000000..58c6b4ae2 --- /dev/null +++ b/rlcard/envs/tractor.py @@ -0,0 +1,8 @@ +import numpy as np +from collections import OrderedDict + +from rlcard.envs import Env +from rlcard.games.uno import Game +from rlcard.games.uno.utils import encode_hand, encode_target +from rlcard.games.uno.utils import ACTION_SPACE, ACTION_LIST +from rlcard.games.uno.utils import cards2list diff --git a/rlcard/games/tractor/game.py b/rlcard/games/tractor/game.py new file mode 100644 index 000000000..b1d7fd062 --- /dev/null +++ b/rlcard/games/tractor/game.py @@ -0,0 +1,67 @@ +import random + +from tractor_game import Tractor + +class TractorGame(Tractor): + def __init__(self): + super().__init__() # 调用父类的初始化方法 + self.allow_step_back = False + self.isBeginGame=False#是否是游戏开始阶段 + # 以下是rlcard的通用方法: + def init_game(self): + playerId=0 + if self.dealer!=-1:#不是-1说明是非初始阶段 + winPlayer,playerId,grade=self.calcGameScore() + if playerId!=-1: + self.reset_game() + return [], self.currentPlayer + else: + return [], (self.dealer+1)%4 + + def get_num_players(self): + return 4 + + def get_num_actions(self): + return 25 + + def configure(self, game_config): + """ + Specify some game specific parameters, such as number of players, initial chips, and dealer id. + If dealer_id is None, he will be randomly chosen + """ + self.num_players = game_config['game_num_players'] + # must have num_players length + self.init_chips = [game_config['chips_for_each']] * game_config["game_num_players"] + self.dealer_id = game_config['dealer_id'] + def step(self,act):#step + next_state=0 + player_id=0 + if self.game_stage=="bid": + player_id=0 + env.dealCards(None, bidFun) + ''' + 把回调拆开 + ''' + + elif self.game_stage=="ambush": + + elif self.game_stage == "play": + + + return next_state, player_id + + def step_back(self): + pass + def is_over(self): + return self.isTer + def get_player_id(self): + #返回当前出牌的玩家id + return self.currentPlayer + +def bidFun(env,p,round,allActList):#回调函数 + n=len(allActList) + return random.randint(0,n-1) +env=TractorGame() +env.reset_game() +env.dealCards(None, bidFun) +print(env.get_player_id()) diff --git a/rlcard/games/tractor/tractor_game.py b/rlcard/games/tractor/tractor_game.py index 22fe0389f..62dada1c8 100644 --- a/rlcard/games/tractor/tractor_game.py +++ b/rlcard/games/tractor/tractor_game.py @@ -19,7 +19,16 @@ def __reset(self): self.lordDecor = -1 # 主牌花色 self.sumSc = 0 # 闲家得分 self.underCards = [] - self.nowDealCardPlayer = 0 # 当前到发牌的玩家 + self.game_stage="bid"#bid,ambush,play,分别代表发牌,扣牌,出牌 + + ''' + 如果game_stage=="bid",则currentPlayer代表当前收到牌的玩家 + 如果game_stage=="ambush",则currentPlayer代表当前扣牌的玩家 + 如果game_stage=="play",则currentPlayer代表当前出牌的玩家 + ''' + self.currentPlayer = 0 + + self.isTer=False#游戏是否结束,它在step里会改变 self.round_i = 0 # 轮数 self.deck = [i%CARDS_CNT2+1 for i in range(CARDS_CNT)] self.useCards_i=0#放回deck @@ -176,7 +185,9 @@ def dealCards(self,beginDeckList,bidFun,lordInfo=(-1,-1,-1)):# self.deck[i]=beginDeckList[i] # print(self.deck.tolist()) self.deck_i = 0 - self.nowDealCardPlayer = 0 + self.currentPlayer=0 + if self.dealer != -1: + self.currentPlayer = (self.dealer + 1) % 4 for i in range(4): self.players[i].initCards() #bidFun(env,round,allActList) @@ -218,7 +229,7 @@ def dealCards(self,beginDeckList,bidFun,lordInfo=(-1,-1,-1)):# def __dealCard(self,bidFun):#发牌 card=self.deck[self.deck_i] - p=self.players[self.nowDealCardPlayer] + p=self.players[self.currentPlayer] p.addCard(getKind(card,4,self.lordNum),card)#级牌放入无主里 allActList=[[]]#空列表代表不叫牌 for i in range(4): @@ -239,19 +250,21 @@ def __dealCard(self,bidFun):#发牌 act_id = bidFun(self,p,self.deck_i//4,allActList) act=allActList[act_id] self.deck_i += 1 - # self.snatchLord_v0(self.nowDealCardPlayer) # 抢主 + # self.snatchLord_v0(self.currentPlayer) # 抢主 #self.printAllCards() if len(act) > 0 : - # print("玩家"+str(self.nowDealCardPlayer)+"叫了:",end=" ") + # print("玩家"+str(self.currentPlayer)+"叫了:",end=" ") dfsPrintActList(act) self.bidAns=act - self.bidPlayer=self.nowDealCardPlayer - self.nowDealCardPlayer=(self.nowDealCardPlayer+1)%4 + self.bidPlayer=self.currentPlayer + self.currentPlayer=(self.currentPlayer+1)%4 def setUnderCards(self,discardFun):#换牌, p = self.players[self.dealer] self.underCards=[] + self.game_stage = "ambush" + self.currentPlayer = self.dealer for a in self.deck[-8:]: p.addCard(getKind(a,self.lordDecor,self.lordNum),a) self.sortPlayerHand(p) @@ -268,6 +281,7 @@ def setUnderCards(self,discardFun):#换牌, for i in range(4): self.players[i].initCards_orderCards_cnt(self) + self.game_stage = "play" # for i in range(5): # env.printCardsList(p.cards_decorList[i], p.cards_decorLen[i]) # print("") @@ -575,6 +589,7 @@ def _judgeSeqUse(self,act:Action):# return False,True def firstPolicy(self,firstPlayerId,firstPolicyFun):#第一个人出牌,policyFun是一个回调,代表如果有甩牌,甩牌的出牌策略 + self.currentPlayer =firstPlayerId p=self.players[firstPlayerId] allActList=self.getFirstAllAction(p) act_id=firstPolicyFun(self,p,Action([],[],playerId=p.id),allActList) @@ -763,6 +778,7 @@ def __otherPolicy(self, p:Player,firstAct:Action,usedAct,cards, otherPolicyFun): usedAct.setDou(doulasti, usedList.copy()) # 设置牌 return act_ans def otherPolicy(self,act4,firstPlayerID,nowPlayerId,otherPolicyFun): + self.currentPlayer = nowPlayerId # 第一个人出牌,otherPolicyFun是一个回调,代表如果跟牌太多, p = self.players[nowPlayerId] firstAct=act4[firstPlayerID] @@ -811,7 +827,8 @@ def step(self,actList4,first_playerId):#输入4个人的出牌组合,保证每 if self.players[playerId].dealerTag>1:#不是庄家赢 self.sumSc += sc_under*rate self.undeck+=self.underCards - return playerId, sc,True,sc_under*rate + self.isTer =True + return playerId, sc,self.isTer,sc_under*rate return playerId,sc,False,0#返回赢得玩家id,本轮得分,是否结束游戏,结算信息 def getGrade(self,sc): if sc<80: