From b125625059ff54575b5f1e483965068da050c8b0 Mon Sep 17 00:00:00 2001 From: ZOOEEER <42556555+ZOOEEER@users.noreply.github.com> Date: Fri, 21 Dec 2018 20:40:58 +0800 Subject: [PATCH 1/5] Add files via upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit player 之外的AIplayer --- AIplay.py | 289 ++++++++++++++++++++++++++++++++++++++++++++++++++ AIplayTest.py | 147 +++++++++++++++++++++++++ 2 files changed, 436 insertions(+) create mode 100644 AIplay.py create mode 100644 AIplayTest.py diff --git a/AIplay.py b/AIplay.py new file mode 100644 index 0000000..308584e --- /dev/null +++ b/AIplay.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Dec 15 10:02:59 2018 +AI based on Bridge Experience +@author: hanyl +""" + +from card import Card +#from model import Model +#from player import AIplayer +#常量定义 +NullColor,Club,Diamond,Heart,Spade=-1,0,1,2,3 +OutHand,NullPlayer,Declarer,OL,Dummy,OLMate=-1,-1,0,1,2,3 +North,West,South,East = 0,1,2,3 +#Declarer_p,OL_p,OLMate_p=0,1,2 +Colors=[Club,Diamond,Heart,Spade] +Players=[Declarer,OL,Dummy,OLMate] +Positions=[North,West,South,East] +Cards=range(52) +#Players_p=[Declarer_p,OL_p,OLMate_p] +Nums=range(13) +NullNum=-1 +NullCard=Card(-1) + + +class CTC(object): + """ + 牌表map的value,可扩充内容 + """ + def __init__(self,Player): + self.Player = Player #可取值-1,0,1,2,3,分别表示OutHand,Declarer……情况 + +class AIplay(object): + def __init__(self): + self.CardTable={} #字典的形式实现,key为Card类型,value为CTC + self.CardNumTable=[[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]] #[player][color],记录实际的张数,>=0时对其他人不可见 + #可以取值<0,表示该门已经垫了多少张/该门已经无牌,对外可见 + self.CardsNow={} #map,key=Card,value=int,初始值为0,存储当前玩家手上有的牌,value>0,表示该牌可出 + self.PlayerNow=NullPlayer #当前玩家的身份,可取枚举值0,1,2,3 + self.TrickLeadingColors={} #记录每一轮的LeadingColor,map,(轮数,花色) + self.TrickLeadingColors[-1]=NullColor + self.TrickLeadingColor=NullColor + self.LastTrickLeadingColor=NullColor + self.TrickNow=0 #当前的轮数,取值0-12 + self.FirstCard=NullCard + self.SecondCard=NullCard + self.ThirdCard=NullCard #本轮出的前三张牌 + #self.TrickCards=[self.FirstCard,self.SecondCard,self.ThirdCard] + self.DeclarerM=NullPlayer #表示庄家在Model模块中的位置 + self.TrickPosition=0 #取值1,2,3,4表示当前几号位出牌 + self.CardReturn=NullCard + self.Trump=NullColor #本局主色 + #self.ReturnNum=NullNum + #self.ReturnColor=NullColor + + + def setCardTable(self,Model): + """ + 叫完牌后,打牌之前调用一下 + 初始化CT,CNT,DeclarerM等信息 + """ + self.DeclarerM=(Model.win_bid_position)%4 #win_bid_position is int ,win_bid_position先假定为庄家 + #需要model里存一个庄家的位置!!! + self.OpenLeadingM=(self.DeclarerM-1)%4 + self.DummyM=(self.DeclarerM-2)%4 + self.OPMateM=(self.DeclarerM-3)%4 + #确定各角色在Modelplayer里的调用位置,接口,与enums中定义的玩家方位有关 + for player in Players: + for card in Model.players[(self.DeclarerM-player) % 4].cards: #-/+与enums中定义的玩家方位的转向(逆、顺)有关 + self.CardTable[card] = CTC(player) + for color in Colors: + self.CardNumTable[player][color] = Model.players[(self.DeclarerM-player) % 4].color_num[color] + + #下面的函数是从Model中获取信息的接口 + def getColorInfo(self,Model): + if Model.king_color is not None: + self.Trump= Model.king_color + else: + self.Trump = NullColor + if self.TrickPosition is not 1: #当前玩家几号位出牌 + self.TrickLeadingColors[self.TrickNow]=Model.first_played_color + else: + self.TrickLeadingColors[self.TrickNow]=NullColor#一号位玩家所引花色 + self.TrickLeadingColor=self.TrickLeadingColors[self.TrickNow] + self.LastTrickLeadingColor=self.TrickLeadingColors.get(self.TrickNow-1,NullColor) + + def getCardInfo(self,Model): + if self.TrickPosition is 4: + self.ThirdCard = Model.pie_history[(Model.current_player_position+1)%4] + self.SecondCard = Model.pie_history[(Model.current_player_position+2)%4] + self.FirstCard = Model.pie_history[(Model.current_player_position+3)%4] + elif self.TrickPosition is 3: + self.SecondCard = Model.pie_history[(Model.current_player_position+1)%4] + self.FirstCard = Model.pie_history[(Model.current_player_position+2)%4] + elif self.TrickPosition is 2: + self.FirstCard = Model.pie_history[(Model.current_player_position+1)%4] + if Model.current_player is not None: + for card in Model.current_player.cards: + self.CardsNow[card]=0 + + + def Position(self,P_P,P_D): + """ + 由庄家位置、当前位置计算当前玩家的角色 + """ + if P_P is P_D: + return Declarer + elif P_P is (P_D+2)%4: + return Dummy + elif P_P is (P_D+1)%4: + return OLMate + return OL + + def getTrickHistory(self,Model): + if Model.play_order is None: + self.TrickPosition=1 + else: + self.TrickPosition=Model.play_order+1 #出牌位置 + if Model.pie is not None: + self.TrickNow=Model.pie #当前是第几轮 + self.PlayerNow=self.Position(Model.current_player_position,self.DeclarerM) #当前玩家的角色 + self.getColorInfo(Model) + self.getCardInfo(Model) + self.ReturnNum = NullNum + self.ReturnColor = NullColor #置零 + + def refT(self,card,LC): #根据card的情况,更新CT和CNT; + player = self.CardTable[card].Player + if player is not OutHand: + self.CardNumTable[player][Card(card).color]-=1 + if Card(card).color is not LC: + self.CardNumTable[player][LC]-=1 + self.CardTable[card].Player = OutHand + + def refreshCardTable(self,Model): + """ + 根据上轮和本轮的出牌更新CardTable以及CardNumTable; + 在前者信息变动的时候,同时更新后者, + CNT存储了每个角色的牌的数量,当实际发生出牌时,数量减一; + 当本门没有换出其他花色时,可以达到负值,表示本门已经垫出几张牌;当小于0时,表明为所有人可知的信息 + """ + if self.TrickNow is not 0: + for player in Positions: + self.refT(Model.play_table.history[self.TrickNow-1][player],self.LastTrickLeadingColor) + if self.TrickPosition > 3: + self.refT(self.ThirdCard,self.TrickLeadingColor) + if self.TrickPosition > 2: + self.refT(self.SecondCard,self.TrickLeadingColor) + if self.TrickPosition > 1: + self.refT(self.FirstCard,self.TrickLeadingColor) + + def playPrepare(self,Model): + """ + 调用上述四个获取历史信息,得到所有本轮出牌需要的信息 + 更新CT,CNT + """ + self.getTrickHistory(Model) + self.refreshCardTable(Model) + + #牌的信息,接口函数 + def gCard(self,color,number): #由Color,Number生成Card对象 + return Card(color*13+number) + + #获取牌信息的函数 + #主要使用的是 cardsNum,haveCard,haveCardOut + def KnowElsePlayers(self,player,color): + for p in Players: #若有一人牌打完了,且大家都知道,且不是明手,那么就知道剩余一人的牌 + if self.CardNumTable[p][color] < 0 and p is not self.PlayerNow and p is not Dummy: + return True + return False + + def OutColorNum(self,player,color): + ocn = 0 + for c in Colors: #若其他花色都打完了 + if c is not color and self.CardNumTable[player][c]< 0: + ocn+=1 + return ocn + + def cardsNumG(self,player,color): #上帝视角God + n = self.CardNumTable[player][color] + if n<0: + return 0 + return n + + def cardsNum(self,player,color): + """ + return 某玩家player某种花色color的牌的数量 + >=0的返回值表示结果,-1表示不知道 + """ + if player is self.PlayerNow: #自己的牌自己知道 + return self.cardsNumG(player,color) + if player is Dummy: #明手的牌都知道 + return self.cardsNumG(player,color) + if player is Declarer and self.PlayerNow is Dummy: #Dummy知道Declarer的牌 + return self.cardsNumG(player,color) + if self.CardNumTable[player][color]<0: #如果是出完了 大家也知道 + return 0 + if self.KnowElsePlayers(player,color): #知道其余三人的情况,那也能推知剩余一人的情况 + return self.CardsNumG(player,color) + if self.OutColorNum(player,color)>=3: #如果其他三门都出完了,大家也知道 + return self.cardsNumG(player,color) + return -1 #表示不知道 + + def haveCardG(self,player,card):#上帝视角God + if self.CardTable[card].Player is player: + return True + return False + + def haveCard(self,player,card): + """ + return 判断某玩家player有无某张牌card + 1,0,-1 分别表示有,无,不知道 + """ + if player is self.PlayerNow: #自己的牌自己知道 + return self.haveCardG(player,card) + if player is Dummy: #Dummy的牌都知道 + return self.haveCardG(player,card) + if player is NullPlayer: + return 0 + if self.PlayerNow is Dummy and player is Declarer: #Dummy知道Declarer的牌 + return self.haveCardG(player,card) + if self.CardNumTable[player][card.color]<0: #如果是出完了 大家也知道 + return False + if self.KnowElsePlayers(player,card.color): #知道其余三人的情况,那也能推知剩余一人的情况 + return self.haveCardG(player,card) + if self.OutColorNum(player,card.color)>=3: #知道本人其余门的情况,能推知该门的情况 + return self.haveCardG(player,card) + return -1 + + def haveCardOut(self,card): + if self.CardTable[card].Player is OutHand: + return True + else: + return False + + def AvailableCards_1(self): #首先判断可出的牌,在CardsNow中赋值为1 + ccn = 0 #本轮可以出的牌数 + if self.TrickPosition > 1: #被动出牌玩家 + for card in list[self.CardsNow.keys()]:#遍历所有的牌 + if card.color is self.TrickLeadingColor: + self.CardsNow[card] += 1 + ccn += 1 + if ccn is 0: #包含两种情况,1.是首引玩家 2.是被动玩家,但没有同花色 + for card in list[self.CardsNow.keys()]: + self.CardsNow[card]+=1 #所有的牌都加1 + + #添加其他规则!让你的AI更智能! + + def CardSelect(self): + """ + 给CardsNow中的Card赋值,值大于1,代表该牌合法,值越大代表这张牌更适合现在的情况,最后对CardsNow中的card进行排序,输入最大的一个 + 主要的逻辑:将桥牌经验总结成规则,放入CardSelect的if分支中,可以通过对累加值进行调整实现优先级 + """ + #首先判断可出的牌,在CardsNow中赋值为1,_1代表第一条规则 + self.AvailableCards_1() + #添加其他规则!!DIY!! + + + def CardGet(self): + self.CardReturn=sorted(self.CardsNow.items(),key=lambda v:v[-1],reverse=1)[0][0] #按value从大到小排序,取其key + self.CardsNow = {} #字典置零 + + + def Play(self,Model): + self.PlayPrepare(Model) + self.CardSelect() + self.CardGet() + return self.CardReturn + + def reset(self): #只在需要回溯或者最终的时候调用,平时的记录是连续的 + self.CardTable={} #字典的形式实现,key为Card类型,value为CTC + self.CardNumTable=[[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]] #[player][color],记录实际的张数,>=0时对其他人不可见 + #可以取值<0,表示该门已经垫了多少张/该门已经无牌,对外可见 + self.CardsNow={} #map,key=Card,value=int,初始值为0,存储当前玩家手上有的牌,value>0,表示该牌可出 + self.PlayerNow=NullPlayer #当前玩家的身份,可取枚举值0,1,2,3 + self.TrickLeadingColors={} #记录每一轮的LeadingColor,map,(轮数,花色) + self.TrickLeadingColors[-1]=NullColor + self.TrickLeadingColor=NullColor + self.LastTrickLeadingColor=NullColor + self.TrickNow=0 #当前的轮数,取值0-12 + self.FirstCard=NullCard + self.SecondCard=NullCard + self.ThirdCard=NullCard #本轮出的前三张牌 + #self.TrickCards=[self.FirstCard,self.SecondCard,self.ThirdCard] + self.DeclarerM=NullPlayer #表示庄家在Model模块中的位置 + self.TrickPosition=0 #取值1,2,3,4表示当前几号位出牌 + self.CardReturn=NullCard + self.Trump=NullColor + diff --git a/AIplayTest.py b/AIplayTest.py new file mode 100644 index 0000000..2af79f6 --- /dev/null +++ b/AIplayTest.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Dec 15 15:57:11 2018 +AI based on Bridge Experience +AIplay Test + +@author: hanyl +""" + +from model import Model +from AIplay import AIplay,CTC +from random import shuffle + +NullCTC=CTC(-1) + +def Deal(M,N,w,x): + """ + 发牌程序,Model,N:发牌方式,w:庄家位置,x:发几张牌 + """ + n = N #只要取N与52互质,则能正好每人13张 + M.win_bid_position = (w)%4 #定义庄家的位置,win_bid_position是Dummy + for i in range(4): + for j in range(x): + n=(n+N)%52 + M.players[i].get_card(Card(n)) + +def TestSetF(AI,M): + """ + 测试setCardTable(self,Model):在初始化发牌后,52张牌很好 + """ + CNT=[[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]; + for w in Positions: + N = 1 + #for N in [1,3,5,7,9,11,15,17,19,21,23,25,27,29]: + Deal(M,N,w,13) #发牌 + AI.setCardTable(M) #set函数检验 + for c in Colors: + for n in Nums: + ctc = AI.CardTable.get(c*13+n,NullCTC) + if ctc is not NullCTC: + CNT[ctc.Player][c]+=1 #牌表 + tn=0 + for p in Players: + for c in Colors: + if AI.CardNumTable[p][c] is CNT[p][c]: + tn+=1 + #牌张表 + print(tn,end=' ') + print(CNT,end=' ') + CNT=[[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]; + AI.reset() + M.reset() + print("for N=",N) + +def TestGetHF(AI,M): + M.king_color = Club #方块 + M.play_order=2 #第几个出牌 + M.first_played_color=Diamond #梅花 + M.current_player_position=South # + M.pie=5 #第6轮,已完成5轮 + for i in Positions: + M.pie_history[i]=i+18 + M.current_player_position=2 + for i in range(M.pie): + AI.TrickLeadingColors[i]=0 #之前的记录 + pie={0:0,1:1,2:2,3:3,'win':3} + M.play_table.history.append(pie) + for i in range(4): + AI.CardTable[i]=CTC(AI.Position(i,DeclarerP))#伪造一个CardTable的前四项 + print(AI.CardTable[i].Player,end=' ') + print() + AI.CardTable[18]=CTC(-1) + AI.CardTable[21]=CTC(-1) + AI.getTrickHistory(M) + TGHFPrint(AI,1)#打印结果函数 + print(AI.CardNumTable) + AI.refreshCardTable(M) + print(AI.CardNumTable) + + + + +def TGHFPrint(AI,bl=1): + if bl: + print("AI.TrickPosition ",AI.TrickPosition) #=player_order+1 + print("AI.TrickLeadingColor ",AI.TrickLeadingColor) #first_played_color + print("AI.LastTrickLeadingColor ",AI.LastTrickLeadingColor) + print("AI.LeadingColors",AI.TrickLeadingColors) + print("AI.TrickNow ",AI.TrickNow)#pie + print("AI.PlayerNow ",AI.PlayerNow)# + print("AI.Card ",AI.FirstCard,AI.SecondCard,AI.ThirdCard) + + +def AIPlaySet(AI): + #发牌 + N = 52-12 + x=[i for i in range(N)] + shuffle(x) + for i in range(N): + card = x[i] + player = i%4 + color = int(card/13) + AI.CardTable[card]=CTC(player) + AI.CardNumTable[player][color]+=1 + for c in range(N,52): + AI.CardTable[c]=CTC(NullPlayer) + for c in Colors: + for n in Nums: + print(AI.CardTable[AI.gCard(c,n)].Player,end=' ') + print() + for p in Players: + print(AI.CardNumTable[p]) + ''' #cardsNumG(p,c) + for p in Players: + for c in Colors: + print(AI.cardsNumG(p,c),end=' ') + print() + ''' + ''' #haveCardG(c,p) + for c in range(N): + print(AI.haveCardG(AI.CardTable[c].Player,c),end=' ') + ''' + + AI.PlayerNow = Declarer + player = Declarer + for c in range(N): + print(AI.haveCard(AI.CardTable[c].Player,Card(c)),end=' ') + + + + +if __name__ == '__main__': + AI =AIplay() + #M = Model() + #TestSetF(AI,M) #检验setCardTable函数 + + ''' #检验GetHistory,refreshCardTable函数 + DeclarerP = South #庄家位置, + N = 5 #发牌因子 + Deal(M,N,DeclarerP,8) + AI.setCardTable(M) + TestGetHF(AI,M) + ''' + AIPlaySet(AI) + + + \ No newline at end of file From a5b9f015eca5fa33d9d300e34e7bb09029f8ac71 Mon Sep 17 00:00:00 2001 From: ZOOEEER <42556555+ZOOEEER@users.noreply.github.com> Date: Thu, 27 Dec 2018 16:29:06 +0800 Subject: [PATCH 2/5] Add files via upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI的测试代码 --- AI.py | 31 ++-- AIplay.py | 103 ++++++++--- AIplay_inter.py | 152 +++++++++++++++++ AIplay_unittest_self.py | 292 +++++++++++++++++++++++++++++++ bid_table.py | 26 ++- card.py | 18 +- controller.py | 369 +++++++++++++++++++++++++++------------- model.py | 4 +- play_table.py | 13 +- player.py | 63 ++++--- timeGUI.py | 44 +++-- utils.py | 87 +++------- 12 files changed, 925 insertions(+), 277 deletions(-) create mode 100644 AIplay_inter.py create mode 100644 AIplay_unittest_self.py diff --git a/AI.py b/AI.py index ac9301b..07cd1ed 100644 --- a/AI.py +++ b/AI.py @@ -8,19 +8,17 @@ class AI(object): def __init__(self): super().__init__() - self.color = np.array(13, dtype=np.int) - self.number = np.zeros(13, dtype=np.int) - self.isPlayed = np.array(13, dtype=np.int) - self.cardPoint = np.zeros(5) - self.colorNumber = np.zeros(4) + self.isPlayed = [False] * 13 + self.cardPoint = [0] * 5 + self.colorNumber = [0] * 4 # 重置AI def reset(self, color, number): self.color = color self.number = number - self.isPlayed = np.zeros(13, dtype=np.int) - self.colorNumber = np.zeros(4) - self.cardPoint = np.zeros(5) + self.isPlayed = [False] * 13 + self.cardPoint = [0] * 5 + self.colorNumber = [0] * 4 self.calculate_card_point() # 计算不同花色牌点数 @@ -35,24 +33,25 @@ def color_card_point(self, color): # 顺序出牌法,给出AI推荐出的牌 def ai_play(self, lastPlayedNumber, lastplayedColor, order, firstPlayedColor): - if order == 0: + # if order == 0: + if firstPlayedColor is None: for i in range(13): - if self.isPlayed[i] == 0: - self.isPlayed[i] = 1 + if not self.isPlayed[i]: + # self.isPlayed[i] = True return create_card(self.color[i], self.number[i]) for i in range(13): - if (self.color[i] == firstPlayedColor) and (self.isPlayed[i] == 0): - self.isPlayed[i] = 1 + if (self.color[i] == firstPlayedColor) and (not self.isPlayed[i]): + # self.isPlayed[i] = True return create_card(firstPlayedColor, self.number[i]) for i in range(13): - if self.isPlayed[i] == 0: - self.isPlayed[i] = 1 + if not self.isPlayed[i]: + # self.isPlayed[i] = True return create_card(self.color[i], self.number[i]) # 出牌 def remove(self, myColor, myNumber): for i in range(13): if (self.color[i] == myColor) and (self.number[i] == myNumber): - self.isPlayed[i] = 1 + self.isPlayed[i] = True diff --git a/AIplay.py b/AIplay.py index 308584e..550d87b 100644 --- a/AIplay.py +++ b/AIplay.py @@ -86,17 +86,19 @@ def getColorInfo(self,Model): def getCardInfo(self,Model): if self.TrickPosition is 4: - self.ThirdCard = Model.pie_history[(Model.current_player_position+1)%4] - self.SecondCard = Model.pie_history[(Model.current_player_position+2)%4] - self.FirstCard = Model.pie_history[(Model.current_player_position+3)%4] + self.ThirdCard = Model.trick_history[(Model.current_player_position+1)%4] + self.SecondCard = Model.trick_history[(Model.current_player_position+2)%4] + self.FirstCard = Model.trick_history[(Model.current_player_position+3)%4] elif self.TrickPosition is 3: - self.SecondCard = Model.pie_history[(Model.current_player_position+1)%4] - self.FirstCard = Model.pie_history[(Model.current_player_position+2)%4] + self.SecondCard = Model.trick_history[(Model.current_player_position+1)%4] + self.FirstCard = Model.trick_history[(Model.current_player_position+2)%4] elif self.TrickPosition is 2: - self.FirstCard = Model.pie_history[(Model.current_player_position+1)%4] + self.FirstCard = Model.trick_history[(Model.current_player_position+1)%4] + ''' if Model.current_player is not None: for card in Model.current_player.cards: self.CardsNow[card]=0 + ''' def Position(self,P_P,P_D): @@ -116,8 +118,8 @@ def getTrickHistory(self,Model): self.TrickPosition=1 else: self.TrickPosition=Model.play_order+1 #出牌位置 - if Model.pie is not None: - self.TrickNow=Model.pie #当前是第几轮 + if Model.trick is not None: + self.TrickNow=Model.trick #当前是第几轮 self.PlayerNow=self.Position(Model.current_player_position,self.DeclarerM) #当前玩家的角色 self.getColorInfo(Model) self.getCardInfo(Model) @@ -164,8 +166,14 @@ def gCard(self,color,number): #由Color,Number生成Card对象 #获取牌信息的函数 #主要使用的是 cardsNum,haveCard,haveCardOut def KnowElsePlayers(self,player,color): - for p in Players: #若有一人牌打完了,且大家都知道,且不是明手,那么就知道剩余一人的牌 - if self.CardNumTable[p][color] < 0 and p is not self.PlayerNow and p is not Dummy: + #若有一人牌打完了,且大家都知道,且不是明手,那么就知道剩余一人的牌 + if self.PlayerNow is not Dummy: + for p in Players: + if p is not self.PlayerNow and p is not Dummy and p is not player: + if self.CardNumTable[p][color] < 0: + return True + else: + if self.CardNumTable[OL][color] < 0 or self.CardNumTable[OLMate][color]<0: return True return False @@ -182,7 +190,7 @@ def cardsNumG(self,player,color): #上帝视角God return 0 return n - def cardsNum(self,player,color): + def cardsNumO(self,player,color): #O表示一次计算,One time """ return 某玩家player某种花色color的牌的数量 >=0的返回值表示结果,-1表示不知道 @@ -196,11 +204,45 @@ def cardsNum(self,player,color): if self.CardNumTable[player][color]<0: #如果是出完了 大家也知道 return 0 if self.KnowElsePlayers(player,color): #知道其余三人的情况,那也能推知剩余一人的情况 - return self.CardsNumG(player,color) + return self.cardsNumG(player,color) if self.OutColorNum(player,color)>=3: #如果其他三门都出完了,大家也知道 return self.cardsNumG(player,color) return -1 #表示不知道 - + def cardsNum(self,player,color): #多次计算 + t =[] + for i in range(4): + tj=[] + for j in range(4): + tj.append(0) + t.append(tj) + uk = 0 + for p in Players: + for c in Colors: + t[p][c]=self.cardsNumO(p,c) + if t[p][c] is -1: + uk += 1 + ukc = 0 #列未知的个数 + uka = 0 #行未知的个数 + for i in range(uk): #最多重复uk次,因为至少获取一个新知识,或者没有,没有则无法再有 + for p in Players: #逐行检查 + for c in Colors: + if t[p][c] is -1: + uka += 1 + cuk = c + if uka is 1: #只有一个不知道,那根据总的衡算,也应该能推知 + t[p][cuk]=self.cardsNumG(p,cuk) #由于不知道,肯定是大于等于0 + uka = 0 + for c in Colors: + for p in Players: + if t[p][c] is -1: + ukc += 1 + puk = p + if ukc is 1: + t[puk][c]=self.cardsNumG(puk,c) + ukc = 0 + return t[player][color] + + def haveCardG(self,player,card):#上帝视角God if self.CardTable[card].Player is player: return True @@ -213,18 +255,37 @@ def haveCard(self,player,card): """ if player is self.PlayerNow: #自己的牌自己知道 return self.haveCardG(player,card) - if player is Dummy: #Dummy的牌都知道 + elif player is Dummy: #Dummy的牌都知道 return self.haveCardG(player,card) + elif self.CardTable[card].Player is Dummy: + print("***",end='') + return 0 + elif self.CardTable[card].Player is self.PlayerNow: + #如果这张牌在已知的人的手里,则返回无 + print("---",end='') + return 0 if player is NullPlayer: return 0 + if self.CardTable[card].Player is OutHand: + return 0 if self.PlayerNow is Dummy and player is Declarer: #Dummy知道Declarer的牌 return self.haveCardG(player,card) if self.CardNumTable[player][card.color]<0: #如果是出完了 大家也知道 - return False + return 0 if self.KnowElsePlayers(player,card.color): #知道其余三人的情况,那也能推知剩余一人的情况 return self.haveCardG(player,card) + if self.PlayerNow is not Dummy: + for p in Players: + if p is not self.PlayerNow and p is not Dummy and self.cardsNum(p,card.color) is 0: + return self.haveCardG(player,card) + if self.PlayerNow is Dummy: + if self.cardsNum(OL,card.color) is 0 or self.cardsNum(OLMate,card.color) is 0: + return self.haveCardG(player,card) + + ''' if self.OutColorNum(player,card.color)>=3: #知道本人其余门的情况,能推知该门的情况 - return self.haveCardG(player,card) + return self.haveCardG(player,card) #知道本人其余门的情况,不能推断出本门的具体情况 + ''' return -1 def haveCardOut(self,card): @@ -235,13 +296,17 @@ def haveCardOut(self,card): def AvailableCards_1(self): #首先判断可出的牌,在CardsNow中赋值为1 ccn = 0 #本轮可以出的牌数 + self.CardsNow ={} + for c in Cards: + if self.CardTable[c].Player is self.PlayerNow: + self.CardsNow[c]=0 if self.TrickPosition > 1: #被动出牌玩家 - for card in list[self.CardsNow.keys()]:#遍历所有的牌 - if card.color is self.TrickLeadingColor: + for card in list(self.CardsNow.keys()):#遍历所有的牌 + if Card(card).color is self.TrickLeadingColor: self.CardsNow[card] += 1 ccn += 1 if ccn is 0: #包含两种情况,1.是首引玩家 2.是被动玩家,但没有同花色 - for card in list[self.CardsNow.keys()]: + for card in list(self.CardsNow.keys()): self.CardsNow[card]+=1 #所有的牌都加1 #添加其他规则!让你的AI更智能! diff --git a/AIplay_inter.py b/AIplay_inter.py new file mode 100644 index 0000000..1e7a99c --- /dev/null +++ b/AIplay_inter.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Dec 27 16:18:11 2018 + +@author: hanyl +""" + +from model import Model +from AIplay import AIplay,CTC +from random import shuffle + + +def Deal(M,N,w,x): + """ + 发牌程序,Model,N:发牌方式,w:庄家位置,x:发几张牌 + """ + n = N #只要取N与52互质,则能正好每人13张 + M.win_bid_position = (w)%4 #定义庄家的位置,win_bid_position是Dummy + for i in range(4): + for j in range(x): + n=(n+N)%52 + M.players[i].get_card(Card(n)) + +def TestSetF(AI,M): + """ + 测试setCardTable(self,Model):在初始化发牌后,52张牌很好 + """ + CNT=[[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]; + for w in Positions: + N = 1 + #for N in [1,3,5,7,9,11,15,17,19,21,23,25,27,29]: + Deal(M,N,w,13) #发牌 + AI.setCardTable(M) #set函数检验 + for c in Colors: + for n in Nums: + ctc = AI.CardTable.get(c*13+n,NullCTC) + if ctc is not NullCTC: + CNT[ctc.Player][c]+=1 #牌表 + tn=0 + for p in Players: + for c in Colors: + if AI.CardNumTable[p][c] is CNT[p][c]: + tn+=1 + #牌张表 + print(tn,end=' ') + print(CNT,end=' ') + CNT=[[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]; + AI.reset() + M.reset() + print("for N=",N) + +def TestGetHF(AI,M): + M.king_color = Club #方块 + M.play_order=2 #第几个出牌 + M.first_played_color=Diamond #梅花 + M.current_player_position=South # + M.trick=5 #第6轮,已完成5轮 + for i in Positions: + M.trick_history[i]=i+18 + M.current_player_position=2 + for i in range(M.trick): + AI.TrickLeadingColors[i]=0 #之前的记录 + trick={0:0,1:1,2:2,3:3,'win':3} + M.play_table.add(trick) + for i in range(4): + AI.CardTable[i]=CTC(AI.Position(i,DeclarerP))#伪造一个CardTable的前四项 + print(AI.CardTable[i].Player,end=' ') + print() + AI.CardTable[18]=CTC(-1) + AI.CardTable[21]=CTC(-1) + AI.getTrickHistory(M) + TGHFPrint(AI,1)#打印结果函数 + print(AI.CardNumTable) + AI.refreshCardTable(M) + print(AI.CardNumTable) + + + + +def TGHFPrint(AI,bl=1): + if bl: + print("AI.TrickPosition ",AI.TrickPosition) #=player_order+1 + print("AI.TrickLeadingColor ",AI.TrickLeadingColor) #first_played_color + print("AI.LastTrickLeadingColor ",AI.LastTrickLeadingColor) + print("AI.LeadingColors",AI.TrickLeadingColors) + print("AI.TrickNow ",AI.TrickNow)#trick + print("AI.PlayerNow ",AI.PlayerNow)# + print("AI.Card ",AI.FirstCard,AI.SecondCard,AI.ThirdCard) + + +def AIPlaySet(AI): + #发牌 + N = 52-12 + x=[i for i in range(N)] + shuffle(x) + for i in range(N): + card = x[i] + player = i%4 + color = int(card/13) + AI.CardTable[card]=CTC(player) + AI.CardNumTable[player][color]+=1 + for c in range(N,52): + AI.CardTable[c]=CTC(NullPlayer) + for c in Colors: + for n in Nums: + print(AI.CardTable[AI.gCard(c,n)].Player,end=' ') + print() + for p in Players: + print(AI.CardNumTable[p]) + ''' #cardsNumG(p,c) + for p in Players: + for c in Colors: + print(AI.cardsNumG(p,c),end=' ') + print() + ''' + ''' #haveCardG(c,p) + for c in range(N): + print(AI.haveCardG(AI.CardTable[c].Player,c),end=' ') + ''' + + AI.PlayerNow = Dummy + player = Dummy + for c in range(52): + print(AI.haveCardOut(c),end=' ') + for c in range(N): + print(AI.haveCard(AI.CardTable[c].Player,Card(c)),end=' ') + print() + for c in range(N): + print(AI.haveCard(player,Card(c)),end=' ') + + for c in range(52): + if AI.CardTable[c].Player is AI.PlayerNow: + AI.CardsNow[c]=0 + print() + print(AI.CardsNow) + + AI.CardSelect() + + + +if __name__ == '__main__': + AI =AIplay() + M = Model() + TestSetF(AI,M) #检验setCardTable函数 + + #检验GetHistory,refreshCardTable函数 + DeclarerP = South #庄家位置, + N = 5 #发牌因子 + Deal(M,N,DeclarerP,8) + AI.setCardTable(M) + TestGetHF(AI,M) + \ No newline at end of file diff --git a/AIplay_unittest_self.py b/AIplay_unittest_self.py new file mode 100644 index 0000000..179ab5c --- /dev/null +++ b/AIplay_unittest_self.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Dec 25 13:22:05 2018 + +@author: hanyl +""" +import unittest +import random +from AIplay import AIplay,CTC +from card import Card +from model import Model + +NullColor,Club,Diamond,Heart,Spade=-1,0,1,2,3 +OutHand,NullPlayer,Declarer,OL,Dummy,OLMate=-1,-1,0,1,2,3 +North,West,South,East = 0,1,2,3 +#Declarer_p,OL_p,OLMate_p=0,1,2 +Colors=[Club,Diamond,Heart,Spade] +Players=[Declarer,OL,Dummy,OLMate] +Positions=[North,West,South,East] +Cards=range(52) +#Players_p=[Declarer_p,OL_p,OLMate_p] +Nums=range(13) +NullNum=-1 +NullCard=Card(-1) +NullCTC=CTC(-1) + + +class AIplaytest_self(unittest.TestCase): + def setUp(self): + self.AI = AIplay() + x = [i for i in range(52)] + random.shuffle(x) #发牌 + for i in range(52): + card = x[i] + player = int(i/13) + color = int(card/13) + self.AI.CardTable[card]=CTC(player) + self.AI.CardNumTable[player][color]+=1 #把CT、CNT的内容写好 + + + Trick = 2 #墩数,可调整 + for t in range(Trick): + c = random.choice(Colors) #假设每一敦的主花色是这么随机选的 + for p in Players: + for i in range(4): + self.AI.CardNumTable[p][(c+i)%4]-=1 #先把这个花色的牌出一张 + if self.AI.CardNumTable[p][(c+i)%4] >= 0: #如果有就退出,否则换下一个花色 + for card in Cards: + if int(card/13) is (c+i)%4 and self.AI.CardTable[card].Player is p: + self.AI.CardTable[card].Player = OutHand + break + break + + ''' + for c in Colors: + for n in Nums: + print(AI.CardTable[AI.gCard(c,n)].Player,end=' ') + print() + for p in Players: + print(AI.CardNumTable[p]) + ''' + + + def tearDown(self): + self.AI.reset() + + def testgCard(self): + Cs=[] + Cts=[] + for c in Cards: + Cs.append(Card(c)) + for c in Colors: + for n in Nums: + Cts.append(self.AI.gCard(c,n)) + self.assertEqual(Cs, Cts, 'test gCard fail') + + def testKnowElsePlayer(self): + r = [] + for i in range(4): + rj=[] + for j in range(4): + rk=[] + for k in range(4): + rk.append(True) + rj.append(rk) + r.append(rj) + for self.AI.PlayerNow in Players: + for p in Players: + for c in Colors: + r[self.AI.PlayerNow][p][c]=self.AI.KnowElsePlayers(p,c) + print("Know else player.") + for p1 in Players: + for p2 in Players: + print(r[p1][p2]) + print() + print("Players NumTable") + for p in Players: + print(self.AI.CardNumTable[p]) + + def testOutColorNum(self): + t =[] + for i in range(4): + tj=[] + for j in range(4): + tj.append(0) + t.append(tj) + for p in Players: + for c in Colors: + t[p][c]=self.AI.OutColorNum(p,c) + print("OutColorNum") + for p in Players: + print(t[p]) + print("Players NumTable") + for p in Players: + print(self.AI.CardNumTable[p]) + + def testcardsNumG(self): + t =[] + for i in range(4): + tj=[] + for j in range(4): + tj.append(0) + t.append(tj) + for p in Players: + for c in Colors: + t[p][c]=self.AI.cardsNumG(p,c) + print("CardsNumG") + for p in Players: + print(t[p]) + print("Players NumTable") + for p in Players: + print(self.AI.CardNumTable[p]) + + + def testcardsNum(self): + r = [] + for i in range(4): + rj=[] + for j in range(4): + rk=[] + for k in range(4): + rk.append(True) + rj.append(rk) + r.append(rj) + for self.AI.PlayerNow in Players: + for p in Players: + for c in Colors: + r[self.AI.PlayerNow][p][c]=self.AI.cardsNum(p,c) + print("cardsNum:") + for p1 in Players: + for p2 in Players: + print(r[p1][p2]) + print() + print("Players NumTable:") + for p in Players: + print(self.AI.CardNumTable[p]) + + def testhaveCardG(self): + print("haveCardG_GOD_TRUE:") + for c in Colors: + for n in Nums: + print(self.AI.CardTable[self.AI.gCard(c,n)].Player,end=' ') + print() + print("haveCardG:") + t =[] + for i in range(4): + tj=[] + for j in range(52): + tj.append(0) + t.append(tj) + for p in Players: + for c in Colors: + for n in Nums: + if(self.AI.haveCardG(p,self.AI.gCard(c,n))): + print(p,end=' ') + else: + print("*",end=' ') + print() + print() + print("Players NumTable:") + for p in Players: + print(self.AI.CardNumTable[p]) + t =[] + for i in range(4): + tj=[] + for j in range(4): + tj.append(0) + t.append(tj) + for c in Cards: + if self.AI.CardTable[c].Player is not OutHand: + t[self.AI.CardTable[c].Player][int(c/13)]+=1 + for p in Players: + print(t[p]) + + def testhaveCard(self): + print("haveCardG_GOD_TRUE:") + for c in Colors: + for n in Nums: + print(self.AI.CardTable[self.AI.gCard(c,n)].Player,end=' ') + print() + print("Players NumTable:") + for p in Players: + print(self.AI.CardNumTable[p]) + print("haveCard:") + t =[] + for i in range(4): + tj=[] + for j in range(52): + tj.append(0) + t.append(tj) + for self.AI.PlayerNow in Players: + for p in Players: + for c in Colors: + for n in Nums: + if self.AI.haveCard(p,self.AI.gCard(c,n)) is -1: + print("*",end=' ') + elif self.AI.haveCard(p,self.AI.gCard(c,n)) is True: + print("1",end=' ') + else: + print("0",end=' ') + print() + print() + + + def testhaveCardOut(self): + print("haveCardG_GOD_TRUE:") + for c in Colors: + for n in Nums: + print(self.AI.CardTable[self.AI.gCard(c,n)].Player,end=' ') + print() + Cs=[] + for ca in Cards: + Cs.append(self.AI.haveCardOut(ca)) + kt = 0 + print("haveCardOut") + for c in Colors: + for n in Nums: + if (Cs[self.AI.gCard(c,n)]): + kt+=1 + print("-1",end=' ') + else: + print("*",end=' ') + print() + print("kt=",kt) + + def testAvailableCards_1(self): + pass + """ + for self.AI.TrickLeadingColor in Colors: + for self.AI.TrickPosition in range(1,5): + for self.AI.PlayerNow in Players: + self.AI.AvailableCards_1() + print("AvailableCardsList:",self.AI.TrickLeadingColor," ",self.AI.TrickPosition) + print(self.AI.CardsNow) + print() + """ + + + def testCardSelect(self): + pass + """ + for self.AI.TrickLeadingColor in Colors: + for self.AI.TrickPosition in range(1,5): + for self.AI.PlayerNow in Players: + self.AI.CardSelect() + print("AfterCardSelect:",self.AI.TrickLeadingColor," ",self.AI.TrickPosition) + print(self.AI.CardsNow) + print() + """ + + def testCardGet(self): + for self.AI.TrickLeadingColor in Colors: + for self.AI.TrickPosition in range(1,5): + for self.AI.PlayerNow in Players: + self.AI.CardSelect() + print("CardGet:",self.AI.TrickLeadingColor," ",self.AI.TrickPosition) + print(self.AI.CardsNow,end=" ") + self.AI.CardGet() + print(self.AI.CardReturn) + print() + + + +if __name__ =='__main__': + + unittest.main() + + + + + + diff --git a/bid_table.py b/bid_table.py index d77bf3b..3f7fb90 100644 --- a/bid_table.py +++ b/bid_table.py @@ -1,12 +1,14 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- +from utils import ReadOnlyIterable, PASS, bid_greater class BidTable(object): def __init__(self): super().__init__() self.history = [] - self.top = 0 + self.top = PASS + self.history_iter = ReadOnlyIterable(self.history) def add_bid(self, bid_player, bid_result): # 待修改 @@ -15,23 +17,31 @@ def add_bid(self, bid_player, bid_result): :param bid_result: 叫牌结果 :return: None """ - if bid_result > self.top: - self.top = bid_result + # if bid_result > self.top: + # self.top = bid_result # self.history[bid_result // 10][bid_result % 10] = bid_player + if self.check(bid_result): + self.top = bid_result self.history.append((bid_player, bid_result)) - return bid_result // 10, bid_result % 10, bid_player + return bid_player, bid_result + # def check(self, bid_result): + # return bid_result.number > self.top.number or ( + # bid_result.number == self.top.number and + # bid_result.number != 0 and bid_result.color > self.top.color) def check(self, bid_result): - return bid_result > self.top + """检测""" + return bid_greater(bid_result, self.top) def reset(self): - # self.history = [[None, None, None, None, None] for _ in range(8)] self.history.clear() - self.top = 0 + self.top = PASS def get_bid_result(self, i): bid_result = dict(self.history[-4:]).get(i, None) if bid_result is None: return '' + if bid_result.number == 0: + return 'PASS' else: - return ['♣', '♦', '♥', '♠', 'NT'][bid_result % 10] + str(bid_result // 10) + return ['♣', '♦', '♥', '♠', 'NT'][bid_result.color] + str(bid_result.number) diff --git a/card.py b/card.py index 14425f8..a83d4df 100644 --- a/card.py +++ b/card.py @@ -2,6 +2,11 @@ # -*- coding: utf-8 -*- from enums import Suit +from itertools import chain + + +def color2str(color): + return ('♣', '♦', '♥', '♠', 'NT')[color] class Card(int): @@ -9,16 +14,17 @@ class Card(int): color = property(lambda self: self // 13) number = property(lambda self: self % 13) suit = property(lambda self: Suit(self // 13)) - str_color = property(lambda self: ['♣', '♦', '♥', '♠', 'NT'][self // 13]) + str_color = property(lambda self: color2str(self // 13)) def card2str(card): number, str_color = card.number, card.str_color - number2str = {i: str(i+2) for i in range(9)} - number2str[9] = 'J' - number2str[10] = 'Q' - number2str[11] = 'K' - number2str[12] = 'A' + # number2str = {i: str(i+2) for i in range(9)} + # number2str[9] = 'J' + # number2str[10] = 'Q' + # number2str[11] = 'K' + # number2str[12] = 'A' + number2str = dict(zip(range(13), chain(map(str, range(2, 11)), 'JQKA'))) return str_color + number2str[number] diff --git a/controller.py b/controller.py index 6897b3e..ce2a385 100644 --- a/controller.py +++ b/controller.py @@ -2,26 +2,40 @@ # -*- coding: utf-8 -*- from model import Model -from card import fifty_two_cards, Card, card2str +from card import fifty_two_cards, color2str, card2str from enums import State, DateInfo from player import HumanPlayer +from AIplay import CTC,AIplay +from utils import PASS, MAX_BID_RESULT, bid_greater, BidResult from PyQt5.QtCore import QObject, pyqtSignal +from collections import namedtuple, Counter from queue import Queue import threading import random +from functools import partial import time -from collections import namedtuple -def wrapper(fun): - """装饰器,仅用于帮助分析函数调用时所在线程及运行时间,在最终版中应移去""" - def inner(*args, **kwargs): - t1 = time.time() - res = fun(*args, **kwargs) - t2 = time.time() - print(threading.current_thread().name, fun.__name__, 'take %s seconds' % (t2 - t1)) - return res - return inner +# def wrapper(fun): +# """装饰器,仅用于帮助分析函数调用时所在线程及运行时间,在最终版中应移去""" +# def inner(*args, **kwargs): +# t1 = time.time() +# res = fun(*args, **kwargs) +# t2 = time.time() +# print(threading.current_thread().name, fun.__name__, 'take %s seconds' % (t2 - t1)) +# return res +# return inner + +def wrapper_factory(before=lambda: None, after=lambda: None): + def wrapper(fun): + def inner(*args, **kwargs): + before() + res = fun(*args, **kwargs) + after() + return res + + return inner + return wrapper class GameAction(object): @@ -51,13 +65,14 @@ def execute(self): PlayerInfo = namedtuple('PlayerInfo', ['cards', 'played_card', 'is_visible']) +BidInfo = namedtuple('BidInfo', ['history', 'max_bid']) class Controller(QObject): """ Controller主要包括后台线程和供UI调用的函数,内部实现了桥牌游戏的全部逻辑。 后台线程实现借鉴了状态机思想(不过似乎并不是标准的状态机),其中状态是model.state, - 后台线程可能处于三种稳定状态Stop,Biding,Play(稳定是指这三种状态可长时间保持,在这三个状态下,后台线程可能阻塞)。 + 后台线程可能处于四种稳定状态Stop,Biding,Play,End(稳定是指这四种状态可长时间保持,在这四个状态下,后台线程可能阻塞)。 在不同状态下,后台线程产生不同行为;在相同状态下,后台线程产生同样的行为。 后台线程同时使用了事件机制,不同状态间的转换采用事件实现 """ @@ -65,60 +80,92 @@ class Controller(QObject): # 以及UI鼠标点击事件(如点击“新游戏”按钮)都能导致游戏状态及model中数据的改变, # 且两种改变来自两个线程。 # 我希望能将两种“改变”同等对待,采样同样的方法处理。不论事件来自何处,都是在后台线程处理 - # 只有后台线程能够改变model中的数据 + # 只有后台线程能够改变model中的数据,view不能 + + # 其实程序有一个潜在漏洞,那就是使用了多线程却没有加锁。 + # 虽然保证了View无法直接修改数据(Controller层也使用了事件机制,View可以通过向Controller发送事件,委托Controller修改数据), + # 只能读取数据,却没有保证读取的数据总是正确的。 + # —————————————————————————— + # | | View, write | View, read| + # —————————————————————————— + # | Controller,write | 不可能 | 有风险 | + # —————————————————————————— + # | Controller, read | 不可能 | 安全 | + # —————————————————————————— + + # 存在这样的可能,View读取一半旧的数据-->Controller修改了数据-->View读取一半新数据。 + # 归根结底,这种情况出现的原因是,View读取数据、更新界面与Controller修改数据的顺序是不确定的(线程调度顺序不确定), + # 存在交替进行的可能(在python中,由于全局变量锁的存在,不会出现同时进行的情况)。 + # 因而上述情况可能性很小,但并非绝对不可能(而且如果此软件使用其他具有真·多线程的语言改写,这种情况出现的可能性很大)。 + # 一种解决方法是,在Controller中,加上线程暂停一段时间的功能,给View层足够时间完成数据读写和界面更新, + # 使得在大多数情况下,不会出现这个问题。 + # 但是,如果要万无一失,最好保证View读取数据、更新界面具有原子性,即在读取数据、更新界面期间,Controller无法修改数据, + # 但如果加锁,就会不可避免的增加Controller层与View层的耦合。 # __init__函数之前的,都是为UI提供的接口 - # 信号 - output_signal = pyqtSignal(str) - view_update_signal = pyqtSignal() - - state = property(lambda self: self.__model.state) - max_bid = property(lambda self: self.__model.bid_table.top) - win_bid_position = property(lambda self: self.__model.win_bid_position) - king_color = property(lambda self: self.__model.king_color) - current_player_position = property(lambda self: self.__model.current_player_position) + output_signal = pyqtSignal(str) # 传递输出信息字符串的信号 + view_update_signal = pyqtSignal() # 通知view更新的信号 + + # 供view读取model数据的接口 + state = property(lambda self: self.__model.state, doc='游戏状态') + trick = property(lambda self: self.__model.trick, doc='游戏轮数(墩)') + max_bid = property(lambda self: self.__model.bid_table.top, doc='当前最大叫牌') + win_bid_position = property(lambda self: self.__model.win_bid_position, + doc='当前叫牌胜者') + king_color = property(lambda self: self.__model.king_color, doc='将牌花色') + current_player_position = property( + lambda self: self.__model.current_player_position, doc='当前角色') + play_table = property(lambda self: self.__model.play_table.history, + doc='出牌历史记录') + bid_table = property(lambda self: BidInfo(self.__model.bid_table.history_iter, self.__model.bid_table.top)) def send(self, data=None, info=None): """供GUI调用,用于给controller发送叫牌、出牌结果。""" - if info == DateInfo.Bid: + if info is None: + # 仅用于唤醒后台线程 + self.__received_data = None + self.__thread_event.set() + return + + if self.__thread_event.is_set(): + return + + if info == DateInfo.Bid and self.__model.state == State.Biding and \ + (self.__model.current_player is not None) and \ + self.__model.current_player.controlled_by_human and \ + (data is not None): # 来自GUI叫牌区的用户点击(叫牌结果) - if self.__model.state == State.Biding and ( - self.__model.current_player is not None) and \ - self.__model.current_player.controlled_by_human and ( - data is not None): - # and (data == 0 or self.check_bid_valid(data)): - if self.check_bid_valid(data): - self._received_data = data - else: - self._received_data = 0 - self._thread_event.set() - elif info == DateInfo.HumanPlay: + data = BidResult(*data) + if self.check_bid_valid(data): + self.__received_data = data + else: + self.__received_data = PASS + self.__thread_event.set() + elif info == DateInfo.HumanPlay and self.__model.state == State.Play\ + and self.__model.current_player_position == 0\ + and (data is not None): # 来自GUI人类玩家出牌 - if self.__model.state == State.Play and\ - self.__model.current_player_position == 0 and \ - (data is not None): - # 此时接收的data是牌的序号 - card = self.__model.current_player.cards[data] - # 出牌合法性检查 - if self.check_play_valid(card): - self._received_data = data - self._thread_event.set() - elif info == DateInfo.AIPlay: + # 此时接收的data是牌的序号 + if not (0 <= data < len(self.__model.current_player.cards)): + return + card = self.__model.current_player.cards[data] + # 出牌合法性检查 + if self.check_play_valid(card): + self.__received_data = data + self.__thread_event.set() + elif info == DateInfo.AIPlay and self.__model.state == State.Play and\ + self.__model.current_player_position == 2 and \ + self.__model.current_player.controlled_by_human and \ + (data is not None): # GUI队友AI出牌 - if self.__model.state == State.Play and \ - self.__model.current_player_position == 2 and \ - self.__model.current_player.controlled_by_human and \ - (data is not None): - # 此时接收的data是牌的序号 - card = self.__model.current_player.cards[data] - # 出牌合法性检查 - if self.check_play_valid(card): - self._received_data = data - self._thread_event.set() - elif info is None: - # 仅用于唤醒后台线程 - self._received_data = None - self._thread_event.set() + # 此时接收的data是牌的序号 + if not (0 <= data < len(self.__model.current_player.cards)): + return + card = self.__model.current_player.cards[data] + # 出牌合法性检查 + if self.check_play_valid(card): + self.__received_data = data + self.__thread_event.set() def get_bid_result(self, i): """ @@ -138,7 +185,13 @@ def get_player_info(self, i): if player.controlled_by_human or player.drink_tea: return PlayerInfo(player.cards_iter, played_card, True) else: - return PlayerInfo([None]*len(player.cards), played_card, False) + # return PlayerInfo([None]*len(player.cards), played_card, False) + return PlayerInfo(player.cards_iter, played_card, False) + + def notify(self): + """ 将所有事件set,唤醒后台线程 """ + self.send(None, None) + self.__delay_event.set() def new_game(self): """ @@ -149,44 +202,77 @@ def new_game(self): self.__action_queue.put_nowait(self.begin_game_action) self.notify() + def set_delay_time(self, delay_time): + """ 设置每次叫牌、出牌后的延时时间 """ + self.__delay_time = delay_time + def __init__(self, model=None): super().__init__() if model is None: self.__model = Model() else: self.__model = model - + if AIplay is None: + self.__AIplay = AIplay() + else: + self.__AIplay = AIplay # 状态转移采用事件机制,Queue存储了未处理的事件 self.on_state_functions = {} self.__action_queue = Queue() # 线程相关 - self._thread = threading.Thread(target=self.__run) - self._thread.setDaemon(True) - self._received_data = None - self._thread_event = threading.Event() # 用于控制线程同步,同时也是接收到数据的标志 - self._thread_event.clear() - self.notify = lambda: self.send() - self._thread.start() + self.__thread = threading.Thread(target=self.__run) + self.__thread.setDaemon(True) + self.__received_data = None + self.__thread_event = threading.Event() # 用于控制线程同步,同时也是接收到数据的标志 + self.__thread_event.clear() + self.__delay_event = threading.Event() # 用于延时,可以被中途唤醒 + self.__delay_event.clear() + self.__delay_time = 1.25 + # self.notify = partial(self.send, None, None) + self.__thread.start() # 预定义的GameAction - self.begin_game_action = GameAction('begin game action', target=self.__begin_game) - self.bid_end_action = GameAction('bid end action', target=self.__bid_end) - self.trick_begin_action = GameAction('trick begin action', target=self.__trick_begin) - self.trick_end_action = GameAction('trick end action', target=self.__trick_end) + self.begin_game_action = GameAction('begin game action', + target=self.__begin_game) + self.bid_end_action = GameAction('bid end action', + target=self.__bid_end) + self.trick_begin_action = GameAction('trick begin action', + target=self.__trick_begin) + self.trick_end_action = GameAction('trick end action', + target=self.__trick_end) self.calculate_score_action = GameAction('calculate score action', target=self.__calculate_score) + def __delay(self, immediately=False, timeout=None): + """ + 线程wait,延时一段时间,在此期间可通过事件唤醒 + :param immediately: 是否立即wait. True: 在调用处wait; False: 将wait加入事件队列 + :param timeout: 延时时间 + :return: + """ + if timeout is None: + delay_time = self.__delay_time + else: + delay_time = timeout + if not immediately: + self.__action_queue.put_nowait( + GameAction('delay action', target=self.__delay_event.wait, + timeout=delay_time)) + else: + self.__delay_event.wait(delay_time) + def __get_data(self): """ 后台线程取得来自UI的数据,与send函数配合,实现同步 :return:来自UI的数据 """ - if not self._thread_event.is_set(): - self._thread_event.wait() - data = self._received_data - self._received_data = None - self._thread_event.clear() + self.__thread_event.clear() + while not self.__thread_event.is_set(): + self.__thread_event.wait() + data = self.__received_data + self.__received_data = None + self.__thread_event.clear() return data def output(self, *messages, sep=' ', end='\n'): @@ -202,7 +288,6 @@ def output(self, *messages, sep=' ', end='\n'): :return: None """ # 可根据需要修改此函数实现,将消息发送至不同地方 - pass msg = sep.join((str(item) for item in messages)) + end self.output_signal.emit(msg) print(*messages, sep=sep, end=end) @@ -215,6 +300,9 @@ def check_bid_valid(self, bid_result): """ return self.__model.bid_table.check(bid_result) + def get_valid_play_results(self, i): + return self.__model.players[i].get_candidates(self.__model.first_played_color) + def check_play_valid(self, card): """检测出牌合法性""" if self.__model.first_played_color is None: @@ -223,7 +311,8 @@ def check_play_valid(self, card): if card.color == self.__model.first_played_color: # 花色与第一个出牌花色相同 return True - if self.__model.current_player.color_num[self.__model.first_played_color] == 0: + if self.__model.current_player.color_num[ + self.__model.first_played_color] == 0: # 第一个出牌的花色已经没了 return True return False @@ -243,7 +332,8 @@ def __deal(self, start_player_position=None): self.__model.current_player_position = random.randint(0, 3) for card in cards: self.__model.current_player.get_card(card) - self.__model.current_player_position = (self.__model.current_player_position + 1) % 4 + self.__model.current_player_position = ( + self.__model.current_player_position + 1) % 4 for player in self.__model.players: player.init_AI() @@ -260,9 +350,6 @@ def __bid_begin(self, start_player_position=None): self.__model.current_player_position = start_player_position else: self.__model.current_player_position = random.randint(0, 3) - # self.__model.king_color = None - # self.__model.pass_num = 0 - # self.__model.last_bid_number, self.__model.last_bid_color, self.__model.last_bid_player_position = None, None, None self.output('叫牌开始') self.__model.state = State.Biding @@ -273,12 +360,12 @@ def __reset(self): :return: """ self.__model.reset() - self._thread_event.clear() - self._received_data = None + self.__thread_event.clear() + self.__delay_event.clear() + self.__received_data = None while self.__action_queue.qsize() > 0: self.__action_queue.get_nowait() - @wrapper def __begin_game(self, deal_start_position=None, bid_start_position=None): """ 开始游戏事件的回调函数 @@ -286,8 +373,7 @@ def __begin_game(self, deal_start_position=None, bid_start_position=None): :param bid_start_position: 开始叫牌的玩家位置 :return: """ - # print('begin game', threading.current_thread().name) - self.output('begin game') + self.output('游戏开始') self.__reset() self.__deal(deal_start_position) @@ -298,9 +384,10 @@ def __begin_game(self, deal_start_position=None, bid_start_position=None): else: self.__model.current_player_position = random.randint(0, 3) - self.output('叫牌开始') self.__model.state = State.Biding self.view_update_signal.emit() + self.__delay() + # self.__action_queue.put_nowait(self.delay_action) def __biding(self): """ @@ -317,7 +404,7 @@ def __biding(self): if (self.__model.win_bid_position is not None) and self.__model.pass_num == 3: self.__action_queue.put_nowait(self.bid_end_action) return - if self.__model.bid_table.top == 74: # 7无将 + if self.__model.bid_table.top == MAX_BID_RESULT: # 7无将 self.__action_queue.put_nowait(self.bid_end_action) return @@ -327,6 +414,7 @@ def __biding(self): bid_result = self.__get_data() else: bid_result = self.__model.current_player.bid(self.__model.last_bid_number, self.__model.last_bid_color, self.__model.last_bid_player_position) + bid_result = BidResult(*bid_result) if bid_result is None: return @@ -335,24 +423,33 @@ def __biding(self): # 不合法的叫牌被转换成pass # 实际上合法性检验是由其他地方完成的 # 比如,对应人类叫牌结果,是由GUI通过send函数传递的,在这个函数内部会进行检查 - # 而AI的叫牌结果,AI自己应当保证叫牌合法;若AI给出了不合法的结果,认为是AI设计有疏漏,controller不会帮忙解决,只会将不合法的叫牌转化为pass - bid_result = 0 - if bid_result > 74: + # 而AI的叫牌结果,AI自己应当保证叫牌合法; + # 若AI给出了不合法的结果,认为是AI设计有疏漏,controller不会帮忙解决,只会将不合法的叫牌转化为pass + bid_result = PASS + # if bid_result > 74: + if bid_greater(bid_result, MAX_BID_RESULT): # 限定bid_result最大为7无将 - bid_result = 74 - - if bid_result == 0: + # bid_result = 74 + bid_result = MAX_BID_RESULT + + # if bid_result == 0: + if bid_result[0] == 0: + # 叫牌结果为pass,这里没有用和PASS比较的方法, + # 因为叫牌pass时,叫牌数一定是0,但却对颜色做出规定 + # bid_result == PASS未必得到想要的结果 self.__model.pass_num += 1 else: self.__model.pass_num = 0 - self.__model.last_bid_number, self.__model.last_bid_color, self.__model.last_bid_player_position = bid_result // 10, bid_result % 10, self.__model.current_player_position + self.__model.last_bid_number, self.__model.last_bid_color, self.__model.last_bid_player_position = bid_result[0], bid_result[1], self.__model.current_player_position self.__model.win_bid_position = self.__model.current_player_position self.__model.bid_table.add_bid(self.__model.current_player_position, bid_result) - self.output(self.__model.current_player_position, 'bidResult:', bid_result) + self.output(self.__model.current_player_position, bid_result) self.__model.current_player_position = (self.__model.current_player_position + 1) % 4 self.view_update_signal.emit() + self.__delay() + # self.__action_queue.put_nowait(self.delay_action) def __bid_end(self): """ @@ -364,15 +461,19 @@ def __bid_end(self): self.output('叫牌结束') if self.__model.king_color is not None: # 进入出牌状态,同时设置current_player_position为第一个出牌的人 - self.__model.state = State.Play - self.__model.trick = 0 - self.__action_queue.put_nowait(self.trick_begin_action) self.output('win bid position: {0}, king color: {1}'.format( self.__model.win_bid_position, ['♣', '♦', '♥', '♠', 'NT'][self.__model.king_color])) + self.__model.state = State.Play + self.__model.trick = 0 + self.__AIplay.setCardTable(self.__model) #AI在叫牌后初始化牌 + # self.__action_queue.put_nowait(self.delay_action) + self.__delay() + self.__action_queue.put_nowait(self.trick_begin_action) else: self.output('无人叫牌') self.__model.state = State.Stop + self.__received_data = None self.view_update_signal.emit() def __trick_begin(self): @@ -380,6 +481,7 @@ def __trick_begin(self): 开始出牌前进行一些准备工作,c初始化一些变量,在新一轮出牌开始时调用 :return: """ + # self.__model.state = State.Stop if self.__model.trick == 0: self.__model.current_player_position = ( self.__model.win_bid_position + 1) % 4 @@ -391,8 +493,10 @@ def __trick_begin(self): self.__model.play_order = 0 self.__model.last_played_number, self.__model.last_played_color, self.__model.first_played_color = None, None, None self.__model.state = State.Play - self.output('第{0}轮出牌开始'.format(self.__model.trick)) + self.output('第{}轮出牌开始'.format(self.__model.trick + 1)) + self.__delay() self.view_update_signal.emit() + # self.__action_queue.put_nowait(self.delay_action) def __play(self): """ @@ -404,18 +508,20 @@ def __play(self): self.__action_queue.put_nowait(self.trick_end_action) return - if self.__model.trick == 0 and self.__model.current_player_position == self.__model.drink_tea_player_position: + if self.__model.trick == 0 and self.__model.current_player_position \ + == self.__model.drink_tea_player_position: self.__model.current_player.drink_tea = True teammate_position = self.__model.current_player.teammate_position teammate = self.__model.players[teammate_position] - if isinstance(self.__model.current_player, HumanPlayer) or isinstance( - teammate, HumanPlayer): + if isinstance(self.__model.current_player, HumanPlayer) or \ + isinstance(teammate, HumanPlayer): # 人类玩家或人类玩家的队友明牌,则二者都由人类控制 teammate.controlled_by_human = True self.__model.current_player.controlled_by_human = True + self.view_update_signal.emit() - self.output('第{}轮,轮到玩家{}出牌'.format(self.__model.trick, - self.__model.current_player_position)) + self.output('第{}轮,轮到玩家{}出牌'.format(self.__model.trick + 1, + self.__model.current_player_position)) if self.__model.current_player.controlled_by_human: card_index = self.__get_data() @@ -423,24 +529,30 @@ def __play(self): return card = self.__model.current_player.lose_card_by_index(card_index) else: - card = self.__model.current_player.play(self.__model.last_played_number, - self.__model.last_played_color, - self.__model.trick, - self.__model.first_played_color) - self.__model.last_played_color, self.__model.last_played_number = card.number, card.color + card = self.__AIplay.play(self.__model) #AI根据Model情况出牌 + self.__model.current_player.lose_card(card) + ''' + card = self.__model.current_player.play( + self.__model.last_played_number, self.__model.last_played_color, + self.__model.trick, self.__model.first_played_color) + ''' + self.__model.last_played_color = card.number + self.__model.last_played_number = card.color if self.__model.play_order == 0: self.__model.first_played_color = card.color - self.output('第{}轮,玩家{}出牌{}'.format(self.__model.trick, + self.output('第{}轮,玩家{}出牌{}'.format(self.__model.trick + 1, self.__model.current_player_position, card2str(card))) self.__model.trick_history[self.__model.current_player_position] = card self.__model.play_order += 1 + self.__delay() self.view_update_signal.emit() self.__model.current_player_position = \ (self.__model.current_player_position + 1) % 4 + # self.__action_queue.put_nowait(self.delay_action) def __trick_end(self): """ @@ -450,7 +562,8 @@ def __trick_end(self): for (player, card) in sorted(list(self.__model.trick_history.items()), key=lambda x: x[0]): self.output(player, ':', card2str(card), end=', ') - self.output('将牌', self.king_color, 'first_played_color', self.__model.first_played_color) + self.output('将牌:', color2str(self.king_color), 'first_played_color:', + color2str(self.__model.first_played_color)) values = [] for position, card in self.__model.trick_history.items(): if card.color == self.__model.king_color: @@ -461,20 +574,25 @@ def __trick_end(self): v = card values.append((position, v)) values.sort(key=lambda x: x[-1]) - self.output(values) + print(values) self.__model.win_player_position = values[-1][0] self.__model.trick_history['win'] = self.__model.win_player_position self.__model.play_table.add(self.__model.trick_history) self.__model.trick_history = {} - self.output('第{}轮,玩家{}获胜'.format(self.__model.trick, + # trick(轮数)从0开始,但显示在界面上时,从1开始比较好 + self.output('第{}轮,玩家{}获胜'.format(self.__model.trick + 1, self.__model.win_player_position)) self.__model.trick += 1 if self.__model.trick == 13: # 出牌结束,产生计分事件 + # self.__action_queue.put_nowait(self.delay_action) + self.__delay() self.__action_queue.put_nowait(self.calculate_score_action) else: # 否则产生trick_begin事件 + # self.__action_queue.put_nowait(self.delay_action) + self.__delay() self.__action_queue.put_nowait(self.trick_begin_action) self.view_update_signal.emit() @@ -483,10 +601,11 @@ def __on_state(self): 若始终处于其中一个状态,则相应函数会被反复调用。 因而应保证下列每个函数多次调用不会使数据出错。 """ - functions = {State.Stop: self._thread_event.wait, + functions = {State.Stop: self.__thread_event.wait, State.Biding: self.__biding, State.Play: self.__play, - State.End: self._thread_event.wait} - return functions.get(self.__model.state, lambda: None)() # 实际上所有函数都返回None + State.End: self.__thread_event.wait} + return functions.get(self.__model.state, + lambda: None)() # 实际上所有函数都返回None def __process_game_action(self, game_action): """处理事件,事件可能使状态发生变化,事实上状态的转移也采用了事件机制""" @@ -505,9 +624,15 @@ def __run(self): def __calculate_score(self): # TODO: 完成分值的计算 - # 假设有些数据需要存放在Model中,可以往Model里添加成员, - # 但请确保新增成员有合适初值(一般为None),且在reset中重置 - pass - + counter_win = Counter(map(lambda x: x['win'], self.__model.play_table.history)) + human_team_win = counter_win[0] + counter_win[2] + if self.win_bid_position in (0, 2): + target = self.max_bid.number + 6 + else: + target = 14 - self.max_bid.number - 6 + if human_team_win >= target: + self.output('CONGRATULATION! YOU WIN!') + else: + self.output('YOU LOSE.') self.__model.state = State.End self.view_update_signal.emit() diff --git a/model.py b/model.py index 75f4774..df57137 100644 --- a/model.py +++ b/model.py @@ -19,7 +19,7 @@ def __init__(self): self.bid_table = BidTable() # 叫牌表 self.win_bid_position = None # 叫牌获胜的玩家 self.pass_num = 0 # pass数量 - self.last_bid_number = None + self.last_bid_number = 0 self.last_bid_color = None self.last_bid_player_position = None @@ -53,7 +53,7 @@ def reset(self): self.bid_table.reset() self.win_bid_position = None self.pass_num = 0 - self.last_bid_number = None + self.last_bid_number = 0 self.last_bid_color = None self.last_bid_player_position = None diff --git a/play_table.py b/play_table.py index 3b23519..195c14f 100644 --- a/play_table.py +++ b/play_table.py @@ -1,16 +1,19 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- +from utils import ReadOnlyIterable + class PlayTable(object): def __init__(self): - self.history = [] + self.__history = [] + self.history = ReadOnlyIterable(self.__history) - def add(self, pie): + def add(self, trick): """ - :param pie: 字典,包含一轮出牌信息;包含5个键0,1,2,3,'win',值分别4个玩家的出牌以及本轮胜者 + :param trick: 字典,包含一轮出牌信息;包含5个键0,1,2,3,'win',值分别4个玩家的出牌以及本轮胜者 :return: """ - self.history.append(pie) + self.__history.append(trick) def reset(self): - self.history.clear() + self.__history.clear() diff --git a/player.py b/player.py index 121878d..5d3e550 100644 --- a/player.py +++ b/player.py @@ -2,8 +2,9 @@ # -*- coding: utf-8 -*- from enums import Color +from utils import ReadOnlyIterable, PASS, BidResult +# from collections import namedtuple from AI import AI -from collections import Iterable # 找队友,player初始化用 @@ -12,22 +13,33 @@ def find_teammate(my_position): return (my_position + 2) % 4 -class Player(object): - class CardIterator(Iterable): - """ - 用于访问玩家手牌的只读接口 - """ - def __init__(self, player): - self._list = player.cards +# BidResult = namedtuple('BidResult', ['number', 'color']) +# PASS = BidResult(0, None) +# MAX_BID_RESULT = BidResult(7, 4) +# +# +# def bid_greater(b1, b2): +# return b1.number > b2.number or ( +# b1.number == b2.number and +# b1.number != 0 and b1.color > b2.color) - def __iter__(self): - return self._list.__iter__() - def __len__(self): - return self._list.__len__() - - def __getitem__(self, item): - return self._list.__getitem__(item) +class Player(object): + # class CardIterator(Iterable): + # """ + # 用于访问玩家手牌的只读接口 + # """ + # def __init__(self, player): + # self.__cards = player.cards + # + # def __iter__(self): + # return self.__cards.__iter__() + # + # def __len__(self): + # return self.__cards.__len__() + # + # def __getitem__(self, item): + # return self.__cards.__getitem__(item) def __init__(self, position): super().__init__() @@ -38,8 +50,8 @@ def __init__(self, position): self.position = position # 自己的位置 self.teammate_position = find_teammate(position) # 队友位置 self.ai = AI() # 每个玩家都有一个AI。AI只给出出牌建议,并不能决定出哪张牌 - # self.player_info = self.PlayerInfo(self) # 供GUI访问的接口 - self.cards_iter = Player.CardIterator(self) + # self.cards_iter = Player.CardIterator(self) # 供UI访问的手牌的只读接口 + self.cards_iter = ReadOnlyIterable(self.cards) # 供UI访问的手牌的只读接口 def get_card(self, card): self.cards.append(card) @@ -79,7 +91,10 @@ def reset(self): self.color_num = [0, 0, 0, 0] # 各花色手牌数量 self.card_num = 0 # 手牌总数 self.drink_tea = False # 是否喝茶 - # self.ai.reset() + + def get_candidates(self, first_played_color): + return dict(filter(lambda x: (x[-1].color == first_played_color) or ( + first_played_color is None), enumerate(self.cards))) class AIPlayer(Player): @@ -105,15 +120,19 @@ def bid(self, last_bid_number, last_bid_color, last_bid_player_position): if self.strategy == 0: # 如果是对家叫的牌,则不跟他抢, 0表示pass if last_bid_player_position is None: - return 0 + # return BidResult(0, color=None) + PASS if last_bid_player_position == self.teammate_position: - return 0 + # return BidResult(0, color=None) + PASS else: for name, color in Color.__members__.items(): number = self.bid_number(color.value) if number > last_bid_number: - return int(number * 10 + color.value) - return 0 + # return int(number * 10 + color.value) + return BidResult(number, color.value) + # return BidResult(0, color=None) + return PASS # 出牌 def play(self, last_played_number, last_played_color, order, first_played_color): diff --git a/timeGUI.py b/timeGUI.py index e101357..cc17ad4 100644 --- a/timeGUI.py +++ b/timeGUI.py @@ -162,7 +162,7 @@ def __init__(self, parent=None, controller=None): [Poker(self, 52) for _ in range(52)] points = [(240, 612, 371.5, 520), (4, 100, 100, 306.5), - (240, 4, 371.5, 93), (739, 100, 643, 306.5)] # 玩家手牌放置位置 + (240, 4, 371.5, 93), (739, 100, 643, 306.5)] # 玩家手牌放置位置 self.players = [QPlayer(i, *(points[i])) for i in range(4)] # 界面中玩家采用逆时针的顺序显示 ################################################### @@ -219,7 +219,8 @@ def mousePressEvent(self, e): text = "x: {0}, y: {1}".format(x, y) self.cLabel.setText(text) if 0 <= x < 5 and 0 <= y < 7: - bid_result = 10 * (y + 1) + x + # bid_result = 10 * (y + 1) + x + bid_result = y + 1, x self.controller.send(bid_result, DateInfo.Bid) # 手牌区 @@ -339,8 +340,12 @@ def bid_map(self, x, y): def draw_bid_update(self, qp): if not self.controller.max_bid: return - xb = self.controller.max_bid % 10 - yb = self.controller.max_bid // 10 - 1 + # xb = self.controller.max_bid % 10 + # yb = self.controller.max_bid // 10 - 1 + yb = self.controller.max_bid[0] - 1 + if yb < 0: + return + xb = self.controller.max_bid[1] if self.controller.win_bid_position is None: return #叫牌表更新 @@ -354,20 +359,23 @@ def draw_bid_update(self, qp): else: return - def draw_play_table(self,qp): - ''' 更新出牌表 ''' + def draw_play_table(self, qp): + """更新出牌表""" play_table = self.controller.play_table - rank = [2,3,0,1,'win'] - player_name = ['S','E','N','W'] + rank = [2, 3, 0, 1, 'win'] + player_name = ['S', 'E', 'N', 'W'] color_list = ['♣', '♦', '♥', '♠'] - poker_list = ['2','3','4','5','6','7','8','9','10','J','Q','K','A'] - for y in range(1,len(play_table)): - for x in range(1,5): - card_number = play_table[y-1][rank[x-1]] - index = card_number//13 - number = card_number%13 - qp.drawText(50 * x + 230, 20 * y + 215, color_list[index]+poker_list[number]) - qp.drawText(480, 20 * y + 215, player_name[play_table[y-1][rank[4]]] ) + poker_list = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', + 'K', 'A'] + for y in range(1, len(play_table) + 1): + for x in range(1, 5): + card_number = play_table[y - 1][rank[x - 1]] + index = card_number // 13 + number = card_number % 13 + qp.drawText(50 * x + 230, 20 * y + 215, + color_list[index] + poker_list[number]) + qp.drawText(480, 20 * y + 215, + player_name[play_table[y - 1][rank[4]]]) def draw_play_area(self, qp): qp.setBrush(QColor(180, 180, 180)) @@ -394,12 +402,12 @@ def draw_play_text(self, qp): player_list = ['N', 'W', 'S', 'E'] color_list = ['♣', '♦', '♥', '♠', 'NT'] if self.controller.win_bid_position is not None: - contract = '契约:{0}由{1}叫出'.format(str(self.controller.max_bid // 10) + color_list[self.controller.max_bid % 10], player_list[self.controller.win_bid_position]) + contract = '契约:{0}由{1}叫出'.format(str(self.controller.max_bid[0]) + color_list[self.controller.max_bid[1]], player_list[self.controller.win_bid_position]) qp.drawText(355, 193, contract) text_list = ['轮次', 'N出牌', 'W出牌', 'S出牌', 'E出牌', '获胜方'] for x in range(0, 6): qp.drawText(50 * x + 230, 215, text_list[x]) - for y in range(1, 13): + for y in range(1, 14): qp.drawText(246.5, 20 * y + 215, str(y)) qp.drawText(535, 20 * y + 215, '回溯') qp.drawText(232, 495, '总胜场') diff --git a/utils.py b/utils.py index 57931ee..b2fe771 100644 --- a/utils.py +++ b/utils.py @@ -1,69 +1,38 @@ -import numpy as np +from collections import Iterable, namedtuple -#手牌类 -class Card: - def __init__(self): - self.color = np.array(13, dtype=np.int) - self.number = np.zeros(13, dtype=np.int) - self.isPlayed = np.array(13, dtype=np.int) - self.cardNumber = np.zeros(5) - #初始化手牌对象 - def init(self, color, number): - self.color = color - self.number = number - self.isPlayed = np.zeros(13, dtype=np.int) - self.calculateCardNumber() +class ReadOnlyIterable(Iterable): + """ + 用于访问列表、字典、元组等的只读接口, + 可进行迭代、计算长度 + """ - #计算不同花色牌点数 - def calculateCardNumber(self): - for i in range(13): - if self.number[i] == 1: - self.cardNumber[self.color[i]] +=14 - else: - self.cardNumber[self.color[i]] += self.number[i] + def __init__(self, iterable): + self.iterable = iterable - #某花色手牌数 - def colorCardNumber(self, color): - return self.cardNumber[color] + def __iter__(self): + return self.iterable.__iter__() - #顺序出牌法 - def AIPlay(self, lastPlayedNumber, lastplayedColor, order): - if order == 1: - for i in range(13): - if self.isPlayed[i] == 0: - self.isPlayed[i] == 1 - return self.number[i], self.color[i] + def __len__(self): + return self.iterable.__len__() - for i in range(13): - if (self.color[i] == lastplayedColor) and ( self.isPlayed[i] == 0 ): - self.isPlayed[i] == 1 - return self.number[i], lastplayedColor + def __getitem__(self, item): + return self.iterable.__getitem__(item) + # value = self.iterable.__getitem__(item) + # if isinstance(value, Iterable) and not isinstance(value, str) \ + # and not isinstance(item, slice) \ + # and not isinstance(value, ReadOnlyIterable): + # return ReadOnlyIterable(value) + # else: + # return value - for i in range(13): - if self.isPlayed[i] == 0 : - self.isPlayed[i] == 1 - return self.number[i], self.color[i] - #检验出牌合法性,待补充 - def checkDisplayPrinciple(self,thisRoundColor, myColor, myNumber): - if self.cardNumber[thisRoundColor] == 0: return True - if myColor != thisRoundColor: return False - return True +BidResult = namedtuple('BidResult', ['number', 'color']) +PASS = BidResult(0, None) +MAX_BID_RESULT = BidResult(7, 4) - #人类玩家出牌 - def HunmanPlay(self, thisRoundColor, myColor, myNumber): - if self.checkDisplayPrinciple(thisRoundColor, myColor, myNumber): - self.remove( myColor, myNumber) - return True - else: - return False - def remove(self, myColor, myNumber): - for i in range(13): - if self.color[i] == myColor and self.number == myNumber: - self.isPlayed[i] = 0 - -if __name__ == '__main__': - - a = 1 +def bid_greater(b1, b2): + return b1.number > b2.number or ( + b1.number == b2.number and + b1.number != 0 and b1.color > b2.color) From feeb2ff6e0223c466c7d01491f319d84e4c0a9db Mon Sep 17 00:00:00 2001 From: hgz16 <43645026+hgz16@users.noreply.github.com> Date: Sat, 29 Dec 2018 10:30:35 +0800 Subject: [PATCH 3/5] Delete timeGUI.py --- timeGUI.py | 467 ----------------------------------------------------- 1 file changed, 467 deletions(-) delete mode 100644 timeGUI.py diff --git a/timeGUI.py b/timeGUI.py deleted file mode 100644 index cc17ad4..0000000 --- a/timeGUI.py +++ /dev/null @@ -1,467 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - -import sys -from PyQt5.QtWidgets import * -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from enums import State, DateInfo -from controller import Controller -import os - - -class Poker(QLabel): - """ - 扑克,继承自QLabel,是牌的图形显示 - """ - def __init__(self, parent, number, visible=False, *args, **kwargs): - """ - :param parent: 父窗口 - :param number: 牌,0~52,52表示牌的背面 - :param visible: 是否可见 - :param args: 传递给QLabel的位置参数 - :param kwargs: 传递给QLabel的关键字参数 - """ - super().__init__(parent, *args, **kwargs) - self.setPixmap(QPixmap('cards' + os.path.sep + str(number) + '.png')) - self.resize(57, 87) - self.setVisible(visible) - self.x, self.y = 0, 0 - - -class QPlayer(object): - """ - 玩家类,记录玩家手牌应当显示的位置 - """ - def __init__(self, position, beginpointx, beginpointy, playpointx, - playpointy): - self.hand_pokers = [] # 手牌 - self.played_poker = None # 刚出的牌 - self.bpx = beginpointx # 手牌位置 - self.bpy = beginpointy - self.px = playpointx # 刚出的牌的位置 - self.py = playpointy - self.interval = 20 # 间隔 - self.position = position - - def update(self, hand_pokers, played_poker): - """ - 更新手牌及打出的牌的显示 - :param hand_pokers: 手牌,Poker组成的list - :param played_poker: 刚出的牌,Poker - :return: None - """ - self.clear() - self.hand_pokers = hand_pokers - self.played_poker = played_poker - self.move() - - def clear(self): - """将所有牌移除(设为不可见)""" - for poker in self.hand_pokers: - poker.setVisible(False) - self.hand_pokers.clear() - if self.played_poker: - self.played_poker.setVisible(False) - self.played_poker = None - - def move_horizontal(self): - """使手牌沿水平方向摆放""" - for i, poker in enumerate(self.hand_pokers): - poker.move(self.bpx + self.interval * i, self.bpy) - poker.setVisible(True) - - def move_vertical(self): - """使手牌沿竖直方向摆放""" - for i, poker in enumerate(self.hand_pokers): - poker.move(self.bpx, self.bpy + (self.interval + 15) * i) - poker.setVisible(True) - - def move(self): - """将牌摆放到合适的位置""" - if self.position == 1 or self.position == 3: - self.move_vertical() - else: - self.move_horizontal() - if self.played_poker: - self.played_poker.move(self.px, self.py) - self.played_poker.setVisible(True) - - -####################################### -class WelcomePage(QMainWindow): - close_signal = pyqtSignal() - - def __init__(self): - super().__init__() - self.initUI() - - def initUI(self): - self.setWindowTitle('时光桥牌') - # 设置窗口的图标,引用当前目录下的time.png图片 - self.setWindowIcon(QIcon('time.png')) - self.setGeometry(300, 300, 600, 600) - - self.btn = QToolButton(self) - self.btn.setText("开始游戏") - self.btn.resize(100, 60) - self.btn.move(250, 400) - self.show() - - def closeEvent(self, event): - #是否确认退出 - reply = QMessageBox.question(self, 'Message', "Are you sure to quit?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No) - - if reply == QMessageBox.Yes: - event.accept() - else: - event.ignore() - - -class TimeBridgeGUI(QWidget): - def __init__(self, parent=None, controller=None): - super(TimeBridgeGUI, self).__init__(parent) - #坐标指示器 - grid = QGridLayout() - x = 1 - y = 0 - grid.setHorizontalSpacing(1) - grid.setVerticalSpacing(1) - grid.setContentsMargins(10, 10, 600, 600) - self.cText = "x: {0}, y: {1}".format(x, y) - self.pmText = "提示信息" - #self.setMouseTracking(True) - self.cLabel = QLabel(self.cText, self) - self.pmLabel = QLabel(self.pmText, self) - grid.addWidget(self.cLabel, 0, 0, Qt.AlignTop) - grid.addWidget(self.pmLabel, 1, 0, Qt.AlignTop) - - #grid_2 = QGridLayout() - #grid_2.setContentsMargins(60, 90, 60, 90) - self.text0 = "N" - self.text1 = "W" - self.text3 = "E" - self.text2 = "S" - self.label0 = QLabel(self.text0, self) - self.label1 = QLabel(self.text1, self) - self.label3 = QLabel(self.text3, self) - self.label2 = QLabel(self.text2, self) - self.label0.move(350, 120) - self.label1.move(80, 340) - self.label3.move(640, 340) - self.label2.move(350, 580) - - self.setLayout(grid) - - ############################################## - #player - # 桥牌游戏可能使用的全部牌的图像形式,包括52张正面向上的牌与52张背面向上的牌 - self._pokers = [Poker(self, i) for i in range(52)] + \ - [Poker(self, 52) for _ in range(52)] - - points = [(240, 612, 371.5, 520), (4, 100, 100, 306.5), - (240, 4, 371.5, 93), (739, 100, 643, 306.5)] # 玩家手牌放置位置 - self.players = [QPlayer(i, *(points[i])) for i in range(4)] # 界面中玩家采用逆时针的顺序显示 - ################################################### - - self.resize(800, 700) - self.setFixedSize(800, 700) - #self.setStyleSheet("background: black") - - self.controller = Controller() if controller is None else controller - self.connect_with_controller() - - def connect_with_controller(self): - """将controller内的view_update_signal连接至两个更新函数""" - # 信号的连接 - self.controller.view_update_signal.connect(self.update_players) - self.controller.view_update_signal.connect(self.update) - self.controller.output_signal.connect(self.pmLabel.setText) - - def get_poker(self, card): - """ - 根据数字形式的牌,找到对应的图像形式的牌 - :param card: 牌, Card类型 - :return: - """ - # 注意参数和返回值都可能是None - if card is None: - return None - return self._pokers[card] - - def update_players(self): - """ - 更新玩家手牌的显示 - :return: - """ - for i, player in enumerate(self.players): - hand_cards, played_card, is_visible = self.controller.get_player_info(i) - played_poker = self.get_poker(played_card) - if is_visible: - hand_pokers = [self.get_poker(card) for card in hand_cards] - else: - hand_pokers = [self.get_poker(52 + 13 * i + j) for j in - range(len(hand_cards))] - player.update(hand_pokers, played_poker) - - ################################################################################## - - def mousePressEvent(self, e): - # TODO: 在之后的版本中,下面的print函数可删除 - print(e.x(), e.y()) - - if 200 <= e.x() <= 600 and 180 <= e.y() <= 520: - # 叫牌区域 - x = int((e.x() - 200) / 80) - y = int((e.y() - 180) / 48) - text = "x: {0}, y: {1}".format(x, y) - self.cLabel.setText(text) - if 0 <= x < 5 and 0 <= y < 7: - # bid_result = 10 * (y + 1) + x - bid_result = y + 1, x - self.controller.send(bid_result, DateInfo.Bid) - - # 手牌区 - if self.players[0].bpy <= e.y() <= self.players[0].bpy + 87: - # 人类手牌区 - player = self.players[0] - info = DateInfo.HumanPlay - elif self.players[2].bpy <= e.y() <= self.players[2].bpy + 87: - # AI2 手牌区 - player = self.players[2] - info = DateInfo.AIPlay - else: - return - - if len(player.hand_pokers) > 0 and (player.bpx <= e.x() <= player.bpx + (len(player.hand_pokers) - 1) * player.interval + 57): - # 计算选中牌的下标 - clicklength = e.x() - player.bpx - if clicklength <= player.interval * len(player.hand_pokers): - card_index = clicklength // player.interval - else: - card_index = len(player.hand_pokers) - 1 - - # print(card_index) - self.controller.send(card_index, info) - - def paintEvent(self, e): - # print('paintEvent') - qp = QPainter() - qp.begin(self) - self.draw_player_area(qp) - - # print(self.controller.state) - if self.controller.state in (State.Play, State.End): - self.draw_play_area(qp) - self.draw_play_text(qp) - self.draw_play_table(qp) - # print('play draw') - # elif self.controller.state == State.BidEnd: - # #目前存在Bug - # #有时候这里的代码没被全部调用 - # self.label0.deleteLater() - # self.label1.deleteLater() - # self.label3.deleteLater() - # self.label2.deleteLater() - elif self.controller.state == State.Biding: - self.draw_bid_update(qp) - self.draw_bid_area(qp) - self.draw_bid_text(qp) - # print('draw end') - qp.end() - return - - def draw_player_area(self, qp): - col = QColor(0, 0, 0) - col.setNamedColor('#d4d4d4') - qp.setPen(col) - #基础区域 - qp.setBrush(QColor(180, 180, 180)) - qp.drawRect(240, 4, 297, 87) - qp.drawRect(240, 609, 297, 87) - qp.drawRect(4, 100, 57, 507) - qp.drawRect(739, 100, 57, 507) - - # qp.drawRect(200, 180, 400, 336) - - - def draw_bid_area(self, qp): - #叫牌区域 - pen = QPen(Qt.black, 1, Qt.SolidLine) - qp.setPen(pen) - - # 横线之间间隔48, 竖线之间间隔80 - delta_x, delta_y = 80, 48 - # 画横线 - for i in range(0, 8): - qp.drawLine(200, 180 + i * delta_y, 600, 180 + i * delta_y) - # 画竖线 - for i in range(0, 6): - qp.drawLine(200 + i * delta_x, 180, 200 + i * delta_x, 516) - - def draw_bid_text(self, qp): - colorList = ['♣', '♦', '♥', '♠', 'NT'] - qp.setPen(QColor(71, 53, 135)) - qp.setFont(QFont('', 20)) - for x in range(0, 5): - for y in range(1, 8): - text = '{0} {1}'.format(y, colorList[x]) - qp.drawText(223 + 80 * x, 162 + 48 * y, text) - #左上角提示区更新 - cp = self.controller.current_player_position - if cp is None: - return - - # 下面的代码我注释掉了, - # 在界面显示轮到谁出牌、叫牌是由Controller决定 - # controller有一个信号output_signal,被连接到pmLabel的setText函数 - # pmLabel打算给Controller输出信息用,当然GUI如果有某些信息也可以用它输出, - # 但轮到谁叫牌、轮到谁出牌,还是由Controller决定 - # self.pmText = '轮到{0}叫牌'.format(cp) - # self.pmLabel.setText(self.pmText) - #玩家叫牌提示信息更新 - - text = self.controller.get_bid_result(cp) - if cp == 0: - self.label0.setText(text) - elif cp == 1: - self.label1.setText(text) - elif cp == 2: - self.label2.setText(text) - elif cp == 3: - self.label3.setText(text) - - def bid_map(self, x, y): - # 将叫牌区格位映射到坐标 - return 80 * x + 200, 48 * y + 180, 80, 48 - - def draw_bid_update(self, qp): - if not self.controller.max_bid: - return - # xb = self.controller.max_bid % 10 - # yb = self.controller.max_bid // 10 - 1 - yb = self.controller.max_bid[0] - 1 - if yb < 0: - return - xb = self.controller.max_bid[1] - if self.controller.win_bid_position is None: - return - #叫牌表更新 - qp.setBrush(QColor(self.controller.win_bid_position * 20, 100 + self.controller.win_bid_position * 10, 230 - self.controller.win_bid_position * 15))#皮这一下就很开心 - qp.drawRect(*self.bid_map(xb, yb)) - qp.setBrush(QColor(200, 200, 200))#把失效区域涂灰 - for y in range(0, 7): - for x in range(0, 5): - if y < yb or (y == yb and x < xb): - qp.drawRect(*self.bid_map(x, y)) - else: - return - - def draw_play_table(self, qp): - """更新出牌表""" - play_table = self.controller.play_table - rank = [2, 3, 0, 1, 'win'] - player_name = ['S', 'E', 'N', 'W'] - color_list = ['♣', '♦', '♥', '♠'] - poker_list = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', - 'K', 'A'] - for y in range(1, len(play_table) + 1): - for x in range(1, 5): - card_number = play_table[y - 1][rank[x - 1]] - index = card_number // 13 - number = card_number % 13 - qp.drawText(50 * x + 230, 20 * y + 215, - color_list[index] + poker_list[number]) - qp.drawText(480, 20 * y + 215, - player_name[play_table[y - 1][rank[4]]]) - - def draw_play_area(self, qp): - qp.setBrush(QColor(180, 180, 180)) - qp.drawRect(371.5, 93, 57, 87) - qp.drawRect(371.5, 520, 57, 87) - qp.drawRect(100, 306.5, 57, 87) - qp.drawRect(643, 306.5, 57, 87) - qp.setBrush(QColor(130, 130, 130)) - qp.drawRect(200, 180, 400, 340) - qp.setBrush(QColor(201, 200, 205)) - qp.drawRect(225, 200, 350, 320) - pen = QPen(Qt.black, 1, Qt.SolidLine) - qp.setPen(pen) - qp.drawLine(200, 180, 600, 180) - qp.drawLine(200, 180, 200, 520) - qp.drawLine(600, 180, 600, 520) - qp.drawLine(200, 520, 600, 520) - for x in range(1, 16): - qp.drawLine(225, 20 * x + 200, 575, 20 * x + 200) - for x in range(1, 7): - qp.drawLine(50 * x + 225, 200, 50 * x + 225, 520) - - def draw_play_text(self, qp): - player_list = ['N', 'W', 'S', 'E'] - color_list = ['♣', '♦', '♥', '♠', 'NT'] - if self.controller.win_bid_position is not None: - contract = '契约:{0}由{1}叫出'.format(str(self.controller.max_bid[0]) + color_list[self.controller.max_bid[1]], player_list[self.controller.win_bid_position]) - qp.drawText(355, 193, contract) - text_list = ['轮次', 'N出牌', 'W出牌', 'S出牌', 'E出牌', '获胜方'] - for x in range(0, 6): - qp.drawText(50 * x + 230, 215, text_list[x]) - for y in range(1, 14): - qp.drawText(246.5, 20 * y + 215, str(y)) - qp.drawText(535, 20 * y + 215, '回溯') - qp.drawText(232, 495, '总胜场') - qp.drawText(237, 515, '总分') - - -class MainWindow(QMainWindow): - close_event = pyqtSignal() - - def __init__(self): - super().__init__() - self.setWindowTitle('时光桥牌') - self.setWindowIcon(QIcon('time.png')) - self.controller = Controller() - widget = TimeBridgeGUI(self, controller=self.controller) - self.setCentralWidget(widget) - # self.show() - - # 菜单项 - # TODO: 添加其他的菜单项 - self.bar = self.menuBar() - - self.item = self.bar.addMenu('选项') - self.new_game = QAction(self, text='新游戏') - self.item.addAction(self.new_game) - - self.connect_with_controller() - - def connect_with_controller(self): - """ - 将菜单项与controller中的槽函数连接起来 - :return: - """ - self.new_game.triggered.connect(self.controller.new_game) - - def closeEvent(self, event): - #是否确认退出 - reply = QMessageBox.question(self, 'Message', "是否确认退出?", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No) - if reply == QMessageBox.Yes: - event.accept() - else: - event.ignore() - - -if __name__ == "__main__": - App = QApplication(sys.argv) - main = MainWindow() - # main.show() - ex = WelcomePage() - ex.btn.clicked.connect(main.show) - ex.btn.clicked.connect(main.controller.new_game) - ex.btn.clicked.connect(ex.hide) - ex.close_signal.connect(ex.close) - ex.show() - sys.exit(App.exec_()) From 5fdced299721fe181f3dddbbeada5ed8a1ac8d34 Mon Sep 17 00:00:00 2001 From: hgz16 <43645026+hgz16@users.noreply.github.com> Date: Sat, 29 Dec 2018 10:30:52 +0800 Subject: [PATCH 4/5] Delete utils.py --- utils.py | 38 -------------------------------------- 1 file changed, 38 deletions(-) delete mode 100644 utils.py diff --git a/utils.py b/utils.py deleted file mode 100644 index b2fe771..0000000 --- a/utils.py +++ /dev/null @@ -1,38 +0,0 @@ -from collections import Iterable, namedtuple - - -class ReadOnlyIterable(Iterable): - """ - 用于访问列表、字典、元组等的只读接口, - 可进行迭代、计算长度 - """ - - def __init__(self, iterable): - self.iterable = iterable - - def __iter__(self): - return self.iterable.__iter__() - - def __len__(self): - return self.iterable.__len__() - - def __getitem__(self, item): - return self.iterable.__getitem__(item) - # value = self.iterable.__getitem__(item) - # if isinstance(value, Iterable) and not isinstance(value, str) \ - # and not isinstance(item, slice) \ - # and not isinstance(value, ReadOnlyIterable): - # return ReadOnlyIterable(value) - # else: - # return value - - -BidResult = namedtuple('BidResult', ['number', 'color']) -PASS = BidResult(0, None) -MAX_BID_RESULT = BidResult(7, 4) - - -def bid_greater(b1, b2): - return b1.number > b2.number or ( - b1.number == b2.number and - b1.number != 0 and b1.color > b2.color) From 96882496e292245b98391c14a5355c5284afe965 Mon Sep 17 00:00:00 2001 From: hgz16 <43645026+hgz16@users.noreply.github.com> Date: Sat, 29 Dec 2018 10:31:06 +0800 Subject: [PATCH 5/5] Delete controller.py --- controller.py | 638 -------------------------------------------------- 1 file changed, 638 deletions(-) delete mode 100644 controller.py diff --git a/controller.py b/controller.py deleted file mode 100644 index ce2a385..0000000 --- a/controller.py +++ /dev/null @@ -1,638 +0,0 @@ -#!/usr/bin/python3 -# -*- coding: utf-8 -*- - -from model import Model -from card import fifty_two_cards, color2str, card2str -from enums import State, DateInfo -from player import HumanPlayer -from AIplay import CTC,AIplay -from utils import PASS, MAX_BID_RESULT, bid_greater, BidResult -from PyQt5.QtCore import QObject, pyqtSignal -from collections import namedtuple, Counter -from queue import Queue -import threading -import random -from functools import partial -import time - - -# def wrapper(fun): -# """装饰器,仅用于帮助分析函数调用时所在线程及运行时间,在最终版中应移去""" -# def inner(*args, **kwargs): -# t1 = time.time() -# res = fun(*args, **kwargs) -# t2 = time.time() -# print(threading.current_thread().name, fun.__name__, 'take %s seconds' % (t2 - t1)) -# return res -# return inner - -def wrapper_factory(before=lambda: None, after=lambda: None): - def wrapper(fun): - def inner(*args, **kwargs): - before() - res = fun(*args, **kwargs) - after() - return res - - return inner - return wrapper - - -class GameAction(object): - """游戏事件,并非指图形界面的鼠标点击之类的事件,而是后台线程中的事件。 - 例如,游戏中,游戏进入新的阶段产生的事件;重新开始游戏的事件等。 - 在事件中封装事件相关信息,包括针对事件进行处理的函数""" - def __init__(self, name='', target=None, *args, **kwargs): - """ - :param name: - :param target: 处理事件的函数 - :param args: target的位置参数 - :param kwargs: target的关键字参数 - """ - self.name = name - self.func = target - self.args = args - self.kwargs = kwargs - - def execute(self): - """ - 执行事件回调函数 - :return: 返回self.func的返回值 - """ - if (self.func is not None) and callable(self.func): - return self.func(*self.args, **self.kwargs) - - -PlayerInfo = namedtuple('PlayerInfo', - ['cards', 'played_card', 'is_visible']) -BidInfo = namedtuple('BidInfo', ['history', 'max_bid']) - - -class Controller(QObject): - """ - Controller主要包括后台线程和供UI调用的函数,内部实现了桥牌游戏的全部逻辑。 - 后台线程实现借鉴了状态机思想(不过似乎并不是标准的状态机),其中状态是model.state, - 后台线程可能处于四种稳定状态Stop,Biding,Play,End(稳定是指这四种状态可长时间保持,在这四个状态下,后台线程可能阻塞)。 - 在不同状态下,后台线程产生不同行为;在相同状态下,后台线程产生同样的行为。 - 后台线程同时使用了事件机制,不同状态间的转换采用事件实现 - """ - # 引入事件,主要是考虑到游戏进行到新的阶段(比如叫牌结束), - # 以及UI鼠标点击事件(如点击“新游戏”按钮)都能导致游戏状态及model中数据的改变, - # 且两种改变来自两个线程。 - # 我希望能将两种“改变”同等对待,采样同样的方法处理。不论事件来自何处,都是在后台线程处理 - # 只有后台线程能够改变model中的数据,view不能 - - # 其实程序有一个潜在漏洞,那就是使用了多线程却没有加锁。 - # 虽然保证了View无法直接修改数据(Controller层也使用了事件机制,View可以通过向Controller发送事件,委托Controller修改数据), - # 只能读取数据,却没有保证读取的数据总是正确的。 - # —————————————————————————— - # | | View, write | View, read| - # —————————————————————————— - # | Controller,write | 不可能 | 有风险 | - # —————————————————————————— - # | Controller, read | 不可能 | 安全 | - # —————————————————————————— - - # 存在这样的可能,View读取一半旧的数据-->Controller修改了数据-->View读取一半新数据。 - # 归根结底,这种情况出现的原因是,View读取数据、更新界面与Controller修改数据的顺序是不确定的(线程调度顺序不确定), - # 存在交替进行的可能(在python中,由于全局变量锁的存在,不会出现同时进行的情况)。 - # 因而上述情况可能性很小,但并非绝对不可能(而且如果此软件使用其他具有真·多线程的语言改写,这种情况出现的可能性很大)。 - # 一种解决方法是,在Controller中,加上线程暂停一段时间的功能,给View层足够时间完成数据读写和界面更新, - # 使得在大多数情况下,不会出现这个问题。 - # 但是,如果要万无一失,最好保证View读取数据、更新界面具有原子性,即在读取数据、更新界面期间,Controller无法修改数据, - # 但如果加锁,就会不可避免的增加Controller层与View层的耦合。 - - # __init__函数之前的,都是为UI提供的接口 - output_signal = pyqtSignal(str) # 传递输出信息字符串的信号 - view_update_signal = pyqtSignal() # 通知view更新的信号 - - # 供view读取model数据的接口 - state = property(lambda self: self.__model.state, doc='游戏状态') - trick = property(lambda self: self.__model.trick, doc='游戏轮数(墩)') - max_bid = property(lambda self: self.__model.bid_table.top, doc='当前最大叫牌') - win_bid_position = property(lambda self: self.__model.win_bid_position, - doc='当前叫牌胜者') - king_color = property(lambda self: self.__model.king_color, doc='将牌花色') - current_player_position = property( - lambda self: self.__model.current_player_position, doc='当前角色') - play_table = property(lambda self: self.__model.play_table.history, - doc='出牌历史记录') - bid_table = property(lambda self: BidInfo(self.__model.bid_table.history_iter, self.__model.bid_table.top)) - - def send(self, data=None, info=None): - """供GUI调用,用于给controller发送叫牌、出牌结果。""" - if info is None: - # 仅用于唤醒后台线程 - self.__received_data = None - self.__thread_event.set() - return - - if self.__thread_event.is_set(): - return - - if info == DateInfo.Bid and self.__model.state == State.Biding and \ - (self.__model.current_player is not None) and \ - self.__model.current_player.controlled_by_human and \ - (data is not None): - # 来自GUI叫牌区的用户点击(叫牌结果) - data = BidResult(*data) - if self.check_bid_valid(data): - self.__received_data = data - else: - self.__received_data = PASS - self.__thread_event.set() - elif info == DateInfo.HumanPlay and self.__model.state == State.Play\ - and self.__model.current_player_position == 0\ - and (data is not None): - # 来自GUI人类玩家出牌 - # 此时接收的data是牌的序号 - if not (0 <= data < len(self.__model.current_player.cards)): - return - card = self.__model.current_player.cards[data] - # 出牌合法性检查 - if self.check_play_valid(card): - self.__received_data = data - self.__thread_event.set() - elif info == DateInfo.AIPlay and self.__model.state == State.Play and\ - self.__model.current_player_position == 2 and \ - self.__model.current_player.controlled_by_human and \ - (data is not None): - # GUI队友AI出牌 - # 此时接收的data是牌的序号 - if not (0 <= data < len(self.__model.current_player.cards)): - return - card = self.__model.current_player.cards[data] - # 出牌合法性检查 - if self.check_play_valid(card): - self.__received_data = data - self.__thread_event.set() - - def get_bid_result(self, i): - """ - :param i:玩家位置 - :return: 第i名玩家的叫牌结果(字符串形式) - """ - return self.__model.bid_table.get_bid_result(i) if self.__model.state == State.Biding else '' - - def get_player_info(self, i): - """ - 读取第i个玩家的信息的接口 - :param i: 玩家序号(位置) - :return: 命名元组PlayerInfo,其包含手牌、刚出的牌、是否应当显示3个元素 - """ - played_card = self.__model.trick_history.get(i, None) - player = self.__model.players[i] - if player.controlled_by_human or player.drink_tea: - return PlayerInfo(player.cards_iter, played_card, True) - else: - # return PlayerInfo([None]*len(player.cards), played_card, False) - return PlayerInfo(player.cards_iter, played_card, False) - - def notify(self): - """ 将所有事件set,唤醒后台线程 """ - self.send(None, None) - self.__delay_event.set() - - def new_game(self): - """ - UI“新游戏”按钮的槽函数 - :return: - """ - # print('new game', threading.current_thread().name) - self.__action_queue.put_nowait(self.begin_game_action) - self.notify() - - def set_delay_time(self, delay_time): - """ 设置每次叫牌、出牌后的延时时间 """ - self.__delay_time = delay_time - - def __init__(self, model=None): - super().__init__() - if model is None: - self.__model = Model() - else: - self.__model = model - if AIplay is None: - self.__AIplay = AIplay() - else: - self.__AIplay = AIplay - # 状态转移采用事件机制,Queue存储了未处理的事件 - self.on_state_functions = {} - self.__action_queue = Queue() - - # 线程相关 - self.__thread = threading.Thread(target=self.__run) - self.__thread.setDaemon(True) - self.__received_data = None - self.__thread_event = threading.Event() # 用于控制线程同步,同时也是接收到数据的标志 - self.__thread_event.clear() - self.__delay_event = threading.Event() # 用于延时,可以被中途唤醒 - self.__delay_event.clear() - self.__delay_time = 1.25 - # self.notify = partial(self.send, None, None) - self.__thread.start() - - # 预定义的GameAction - self.begin_game_action = GameAction('begin game action', - target=self.__begin_game) - self.bid_end_action = GameAction('bid end action', - target=self.__bid_end) - self.trick_begin_action = GameAction('trick begin action', - target=self.__trick_begin) - self.trick_end_action = GameAction('trick end action', - target=self.__trick_end) - self.calculate_score_action = GameAction('calculate score action', - target=self.__calculate_score) - - def __delay(self, immediately=False, timeout=None): - """ - 线程wait,延时一段时间,在此期间可通过事件唤醒 - :param immediately: 是否立即wait. True: 在调用处wait; False: 将wait加入事件队列 - :param timeout: 延时时间 - :return: - """ - if timeout is None: - delay_time = self.__delay_time - else: - delay_time = timeout - if not immediately: - self.__action_queue.put_nowait( - GameAction('delay action', target=self.__delay_event.wait, - timeout=delay_time)) - else: - self.__delay_event.wait(delay_time) - - def __get_data(self): - """ - 后台线程取得来自UI的数据,与send函数配合,实现同步 - :return:来自UI的数据 - """ - self.__thread_event.clear() - while not self.__thread_event.is_set(): - self.__thread_event.wait() - data = self.__received_data - self.__received_data = None - self.__thread_event.clear() - return data - - def output(self, *messages, sep=' ', end='\n'): - """ - 输出消息。 - 消息通过output_signal发出,若UI将此信号绑定至某个可显示组件上 - (将此信号连接至组件的setText函数), - 则可实现Controller向UI发送字符串信息。 - 各参数含义与print函数相同。 - :param messages:待输出的信息,数组类型 - :param sep: 各消息之间的分隔符 - :param end: 结尾符号 - :return: None - """ - # 可根据需要修改此函数实现,将消息发送至不同地方 - msg = sep.join((str(item) for item in messages)) + end - self.output_signal.emit(msg) - print(*messages, sep=sep, end=end) - - def check_bid_valid(self, bid_result): - """ - 检测叫牌合法性 - :param bid_result:叫牌结果 - :return: 布尔值 - """ - return self.__model.bid_table.check(bid_result) - - def get_valid_play_results(self, i): - return self.__model.players[i].get_candidates(self.__model.first_played_color) - - def check_play_valid(self, card): - """检测出牌合法性""" - if self.__model.first_played_color is None: - # 第一个出牌 - return True - if card.color == self.__model.first_played_color: - # 花色与第一个出牌花色相同 - return True - if self.__model.current_player.color_num[ - self.__model.first_played_color] == 0: - # 第一个出牌的花色已经没了 - return True - return False - - def __deal(self, start_player_position=None): - """ - 发牌 - :param start_player_position: 初始发牌玩家位置 - :return: None - """ - cards = list(fifty_two_cards) - random.shuffle(cards) - if isinstance(start_player_position, - int) and 0 <= start_player_position < 4: - self.__model.current_player_position = start_player_position - else: - self.__model.current_player_position = random.randint(0, 3) - for card in cards: - self.__model.current_player.get_card(card) - self.__model.current_player_position = ( - self.__model.current_player_position + 1) % 4 - for player in self.__model.players: - player.init_AI() - - self.output('发牌结束') - - def __bid_begin(self, start_player_position=None): - """ - 做一些叫牌开始前的准备工作,初始化叫牌相关的变量 - :param start_player_position: - :return: None - """ - if isinstance(start_player_position, - int) and 0 <= start_player_position < 4: - self.__model.current_player_position = start_player_position - else: - self.__model.current_player_position = random.randint(0, 3) - - self.output('叫牌开始') - self.__model.state = State.Biding - - def __reset(self): - """ - 重置所有数据,清空事件队列 - :return: - """ - self.__model.reset() - self.__thread_event.clear() - self.__delay_event.clear() - self.__received_data = None - while self.__action_queue.qsize() > 0: - self.__action_queue.get_nowait() - - def __begin_game(self, deal_start_position=None, bid_start_position=None): - """ - 开始游戏事件的回调函数 - :param deal_start_position: 开始发牌的玩家位置 - :param bid_start_position: 开始叫牌的玩家位置 - :return: - """ - self.output('游戏开始') - self.__reset() - - self.__deal(deal_start_position) - - if isinstance(bid_start_position, - int) and 0 <= bid_start_position < 4: - self.__model.current_player_position = bid_start_position - else: - self.__model.current_player_position = random.randint(0, 3) - - self.__model.state = State.Biding - self.view_update_signal.emit() - self.__delay() - # self.__action_queue.put_nowait(self.delay_action) - - def __biding(self): - """ - 叫牌,此函数的一次执行对应一名玩家的一次叫牌. - 叫牌结束,将产生bid_end事件 - :return: - """ - - # 没有人叫牌,或有一人叫、连续三人pass,或叫牌叫到了7无将 - # 跳转至下一状态 - if (self.__model.win_bid_position is None) and self.__model.pass_num == 4: - self.__action_queue.put_nowait(self.bid_end_action) - return - if (self.__model.win_bid_position is not None) and self.__model.pass_num == 3: - self.__action_queue.put_nowait(self.bid_end_action) - return - if self.__model.bid_table.top == MAX_BID_RESULT: # 7无将 - self.__action_queue.put_nowait(self.bid_end_action) - return - - self.output('轮到{}叫牌'.format(self.__model.current_player_position)) - - if self.__model.current_player.controlled_by_human: - bid_result = self.__get_data() - else: - bid_result = self.__model.current_player.bid(self.__model.last_bid_number, self.__model.last_bid_color, self.__model.last_bid_player_position) - bid_result = BidResult(*bid_result) - - if bid_result is None: - return - - if not self.__model.bid_table.check(bid_result): - # 不合法的叫牌被转换成pass - # 实际上合法性检验是由其他地方完成的 - # 比如,对应人类叫牌结果,是由GUI通过send函数传递的,在这个函数内部会进行检查 - # 而AI的叫牌结果,AI自己应当保证叫牌合法; - # 若AI给出了不合法的结果,认为是AI设计有疏漏,controller不会帮忙解决,只会将不合法的叫牌转化为pass - bid_result = PASS - # if bid_result > 74: - if bid_greater(bid_result, MAX_BID_RESULT): - # 限定bid_result最大为7无将 - # bid_result = 74 - bid_result = MAX_BID_RESULT - - # if bid_result == 0: - if bid_result[0] == 0: - # 叫牌结果为pass,这里没有用和PASS比较的方法, - # 因为叫牌pass时,叫牌数一定是0,但却对颜色做出规定 - # bid_result == PASS未必得到想要的结果 - self.__model.pass_num += 1 - else: - self.__model.pass_num = 0 - self.__model.last_bid_number, self.__model.last_bid_color, self.__model.last_bid_player_position = bid_result[0], bid_result[1], self.__model.current_player_position - self.__model.win_bid_position = self.__model.current_player_position - self.__model.bid_table.add_bid(self.__model.current_player_position, bid_result) - - self.output(self.__model.current_player_position, bid_result) - - self.__model.current_player_position = (self.__model.current_player_position + 1) % 4 - self.view_update_signal.emit() - self.__delay() - # self.__action_queue.put_nowait(self.delay_action) - - def __bid_end(self): - """ - 叫牌结束后的处理,确定将牌 - 若没有出现4人全pass,则产生trick_begin事件;否则,跳转至Stop状态 - :return: - """ - self.__model.king_color = self.__model.last_bid_color - self.output('叫牌结束') - if self.__model.king_color is not None: - # 进入出牌状态,同时设置current_player_position为第一个出牌的人 - self.output('win bid position: {0}, king color: {1}'.format( - self.__model.win_bid_position, - ['♣', '♦', '♥', '♠', 'NT'][self.__model.king_color])) - self.__model.state = State.Play - self.__model.trick = 0 - self.__AIplay.setCardTable(self.__model) #AI在叫牌后初始化牌 - # self.__action_queue.put_nowait(self.delay_action) - self.__delay() - self.__action_queue.put_nowait(self.trick_begin_action) - else: - self.output('无人叫牌') - self.__model.state = State.Stop - self.__received_data = None - self.view_update_signal.emit() - - def __trick_begin(self): - """ - 开始出牌前进行一些准备工作,c初始化一些变量,在新一轮出牌开始时调用 - :return: - """ - # self.__model.state = State.Stop - if self.__model.trick == 0: - self.__model.current_player_position = ( - self.__model.win_bid_position + 1) % 4 - self.__model.drink_tea_player_position = ( - self.__model.win_bid_position + 2) % 4 - else: - self.__model.current_player_position = self.__model.win_player_position - - self.__model.play_order = 0 - self.__model.last_played_number, self.__model.last_played_color, self.__model.first_played_color = None, None, None - self.__model.state = State.Play - self.output('第{}轮出牌开始'.format(self.__model.trick + 1)) - self.__delay() - self.view_update_signal.emit() - # self.__action_queue.put_nowait(self.delay_action) - - def __play(self): - """ - 出牌 - 此函数的一次执行对应一轮出牌 - :return: - """ - if self.__model.play_order == 4: - self.__action_queue.put_nowait(self.trick_end_action) - return - - if self.__model.trick == 0 and self.__model.current_player_position \ - == self.__model.drink_tea_player_position: - self.__model.current_player.drink_tea = True - teammate_position = self.__model.current_player.teammate_position - teammate = self.__model.players[teammate_position] - if isinstance(self.__model.current_player, HumanPlayer) or \ - isinstance(teammate, HumanPlayer): - # 人类玩家或人类玩家的队友明牌,则二者都由人类控制 - teammate.controlled_by_human = True - self.__model.current_player.controlled_by_human = True - self.view_update_signal.emit() - - self.output('第{}轮,轮到玩家{}出牌'.format(self.__model.trick + 1, - self.__model.current_player_position)) - - if self.__model.current_player.controlled_by_human: - card_index = self.__get_data() - if card_index is None: - return - card = self.__model.current_player.lose_card_by_index(card_index) - else: - card = self.__AIplay.play(self.__model) #AI根据Model情况出牌 - self.__model.current_player.lose_card(card) - ''' - card = self.__model.current_player.play( - self.__model.last_played_number, self.__model.last_played_color, - self.__model.trick, self.__model.first_played_color) - ''' - self.__model.last_played_color = card.number - self.__model.last_played_number = card.color - - if self.__model.play_order == 0: - self.__model.first_played_color = card.color - - self.output('第{}轮,玩家{}出牌{}'.format(self.__model.trick + 1, - self.__model.current_player_position, - card2str(card))) - - self.__model.trick_history[self.__model.current_player_position] = card - self.__model.play_order += 1 - self.__delay() - self.view_update_signal.emit() - self.__model.current_player_position = \ - (self.__model.current_player_position + 1) % 4 - # self.__action_queue.put_nowait(self.delay_action) - - def __trick_end(self): - """ - 一轮出牌结束后调用,在此函数中计算此轮胜者 - :return: - """ - for (player, card) in sorted(list(self.__model.trick_history.items()), - key=lambda x: x[0]): - self.output(player, ':', card2str(card), end=', ') - self.output('将牌:', color2str(self.king_color), 'first_played_color:', - color2str(self.__model.first_played_color)) - values = [] - for position, card in self.__model.trick_history.items(): - if card.color == self.__model.king_color: - v = 200 + card - elif card.color == self.__model.first_played_color: - v = 100 + card - else: - v = card - values.append((position, v)) - values.sort(key=lambda x: x[-1]) - print(values) - self.__model.win_player_position = values[-1][0] - self.__model.trick_history['win'] = self.__model.win_player_position - self.__model.play_table.add(self.__model.trick_history) - self.__model.trick_history = {} - # trick(轮数)从0开始,但显示在界面上时,从1开始比较好 - self.output('第{}轮,玩家{}获胜'.format(self.__model.trick + 1, - self.__model.win_player_position)) - - self.__model.trick += 1 - if self.__model.trick == 13: - # 出牌结束,产生计分事件 - # self.__action_queue.put_nowait(self.delay_action) - self.__delay() - self.__action_queue.put_nowait(self.calculate_score_action) - else: - # 否则产生trick_begin事件 - # self.__action_queue.put_nowait(self.delay_action) - self.__delay() - self.__action_queue.put_nowait(self.trick_begin_action) - self.view_update_signal.emit() - - def __on_state(self): - """当处于某个状态时,调用相应函数。 - 若始终处于其中一个状态,则相应函数会被反复调用。 - 因而应保证下列每个函数多次调用不会使数据出错。 - """ - functions = {State.Stop: self.__thread_event.wait, - State.Biding: self.__biding, State.Play: self.__play, - State.End: self.__thread_event.wait} - return functions.get(self.__model.state, - lambda: None)() # 实际上所有函数都返回None - - def __process_game_action(self, game_action): - """处理事件,事件可能使状态发生变化,事实上状态的转移也采用了事件机制""" - return game_action.execute() # 实际返回值不会被利用 - - def __run(self): - """ - 后台线程run函数 - :return: None - """ - while True: - while self.__action_queue.qsize() > 0: - game_event = self.__action_queue.get() - self.__process_game_action(game_event) - self.__on_state() - - def __calculate_score(self): - # TODO: 完成分值的计算 - counter_win = Counter(map(lambda x: x['win'], self.__model.play_table.history)) - human_team_win = counter_win[0] + counter_win[2] - if self.win_bid_position in (0, 2): - target = self.max_bid.number + 6 - else: - target = 14 - self.max_bid.number - 6 - if human_team_win >= target: - self.output('CONGRATULATION! YOU WIN!') - else: - self.output('YOU LOSE.') - self.__model.state = State.End - self.view_update_signal.emit()