吳昊品游戲核心算法 Round 15 —— 吳昊教你玩德州撲克(模擬+標志位存儲)


 梭哈

  梭哈,又稱沙蟹,學名Five Card Stud,五張種馬,是撲克游戲的一種。以五張牌的排列、組合決定勝負。游戲開始時,每名玩家會獲發一張底牌(此牌只能在最后才翻開);當派發第二張牌后,便由牌面較佳者決定下注額,其他人有權選擇“跟”、“加注”、“放棄”或“清底”。當五張牌派發完畢后,各玩家翻開所有底牌來比較,梭哈在全世界紙牌游戲地位非常高,深受人們的喜愛。游戲在國內和港台地區廣泛流傳,其特點為:上手容易、對抗性強,既有技巧也有一定的運氣成分,因此流傳非常廣泛,五張牌(梭哈)高手必須具備良好的記憶力、綜合的判斷力、冷靜的分析能力再加上些許運氣。該游戲緊張刺激,集益智和樂趣於一身。

 

  各種牌型

     ● High Card:雜牌(不屬於下面任何一種)。根據牌從大到小的順序依次比較。

     ● Pair:有一對,加3張雜牌組成。先比較對的大小,再從大到小的順序比較雜牌。

     ● Two Pairs:有兩對,加1帳雜牌。先從大到小比較對的大小,再比較雜牌。

     ● Three of a Kind:有3張值相同的牌。比較這個值即可。

     ● Straingt:一條龍。即5張牌連續。比較最大的一張牌即可。

     ● Flush:清一色。即5張牌花色相同。和雜牌一樣比較。

     ● Full House:3張值相同的牌,加上一對。比較三張相同的值即可。

      ● Four of a kind:有4張牌相同,即相當於一副“炸彈”。

     ● Straight flush:同花順。即5張牌花色相同,並且連續。例如同花色的34567。

  各種花色

  花(club),方塊(diamond),紅桃(heart)和黑桃(spade)—— 在后面的控制台輸入中會以C,D,H,S表示

 各種花色的數值

  每種花色會有一個對應的數值,分別為2,3,4,5,6,7,8,9,10,jack,queen,ace(J,Q,K)牌型大小的比較

  牌型大小的比較

  牌型比較:Straight flush > Four of a kind > Full House > Flush > Straingt > Three of a Kind > Two Pairs > Pair > High Card。

 

  數字比較:A>K>Q>J>10>9>8>7>6>5>4>3>2

 

  花式比較:黑桃>紅桃>草花>方塊

 

  關於A2345,這手牌可以算順子,但大小在各種撲克中不一樣,梭哈里是第2大順(例如賭神里就是這樣),德州中卻是最小的順子。

  這里闡述了德州撲克和梭哈的一些區別(具體的區別我在后文中也會詳細解釋),這里說明一下,為了我們這里的控制台程序有一定的抽象性,就是說我們屏蔽了花色比較的細節,只考慮數字的比較和牌型的比較,這也是為了之后的計分方便。

 

  傳統規則

  各家一張底牌,底牌要到決勝負時才可翻開。從發第二張牌開始,每發一張牌,以牌面大者為先,進行下注。有人下注,想繼續玩下去的人,選擇跟,跟注后會下注到和上家相同的籌碼,或可選擇加注,各家如果覺得自己的牌況不妙,不想繼續,可以選擇放棄,認賠等待牌局結束,先前跟過的籌碼,亦無法取回。

  最后一輪下注是比賽的關鍵,在這一輪中,玩家可以進行梭哈,所謂梭哈是押上所有未放棄的玩家所能夠跟的最大籌碼。等到下注的人都對下注進行表態后,便掀開底牌一決勝負。這時,牌面最大的人可贏得桌面所有的籌碼。

 現代規則

  在現代的拓展中可以有2到10個玩家同時玩這個游戲。發牌前,必須先下基本的注額。每位玩家發兩張牌。一張暗牌,一張明牌。第一圈,擁有最大的明牌的玩家首先發言,他可以下注、不下注(讓牌)或蓋牌(放棄)也可以全壓(梭哈)。其他玩家可以跟注(有玩家全壓時必須全壓)、加注或蓋牌(放棄)。然后發明牌。第二圈和第三圈如此類推。第四圈玩家要以他們手上的牌組合成最大的牌型,擁有最大的明牌的玩家首先發言,他可以下注、梭哈(又稱全壓)(攤牌)或蓋牌(放棄)。其他玩家可以跟注或蓋牌(放棄)。最后,每位玩家要比牌型的大小以確定贏家。牌最大的玩家贏得所有桌上的賭金。

 各種術語

 

(1)全梭:以最小玩家的金幣數目為每個玩家梭哈時下注的最大數目,但是最大下注數目由房間確定。

(2)封頂:以最小玩家的金幣數目的50%為每個玩家梭哈時下注的最大數目。但是最大下注數目依然為房間確定。最高封頂為 100萬金幣。

 各種規則

(1)先發給各家一張底牌,底牌除本人外,要到決勝負時才可翻開。

(2)從發第二張牌開始,每發一張牌,以牌面發展最佳者為優先,進行下注。

(3)有人下注,想繼續玩下去的人,要按“跟注”鍵,跟注后會下注到和上家相同的籌碼,或可選擇加注。根據房間的設定,可以在特定的時間選擇“梭”,梭哈是加入桌面允許的最大下注。

(4)各家如果覺得自己的牌況不妙,不想繼續,可以按“放棄”鍵放棄下注,先前跟過的籌碼,亦無法取回。

(5)牌面最大的人可贏得桌面所有的籌碼。當多家放棄,已經下的注不能收回,並且贏家的底牌不掀開。

(6)紙牌種類:港式五張牌游戲用的是撲克牌,取各門花色的牌中的“8、9、10、J、Q、K、A”,共28張牌。

  關於梭哈(或者以其改編的德州撲克)的具體技巧,我會在Round 15后面的具體AI中再闡述,其中還包括一些心理戰,這些都是目前的AI所或缺的。

  我們這里先實現一個牌型比較程序,對於給定的兩手牌,我們的程序可以將其進行相應的處理。這里,輸入的每一行有十張牌(前面五張是黑方的,后面五張是白方的),借鑒我的Round 2之“吳昊教你玩斗地主”中的方式,我們利用“數字+字母”的方式來表征一張牌的兩個標准特征,也就是點數和花色,而五張牌又可以構成一手牌。

  在輸出中,如果黑方獲勝,我們輸出“Black wins”,如果白方獲勝,我們輸出“White wins”,如果是平局的話,我們輸出“Tie”。

 

  我們Source(ZOJ 1111)中的獨特的技巧

  關於Source的選擇問題,我有考慮過一些常見的模擬算法,比如“yllfever的專欄”和“hoodlum1980(發發)的技術博客”,但是,其代碼都過於冗長,所以這里給出了一種獨特的方法,將字符運算轉為了數字運算(利用數字來進行字符存儲),所以,亮點在於數據結構的編排,算法的復雜程度也變高了一些。

  Source的數據結構剖析

  首先,如何利用一個32位的整型int變量來存儲一副牌?

  我們用兩個數組分別存儲一副牌的點數和花色信息:

  char *deck=“23456789TJQKA”(T代表10,也就是ten)

  char *suit=“CDHS”( 梅花(club),方塊(diamond),紅桃(heart)和黑桃(spade))

  存儲一張牌的時候,考慮到一個int類型的數值是32位的,那么,第0位和第1位可以存儲花色信息(恰好有四種花色信息——2^2),后面四位來存儲數值(2^4),所以,感覺在空間上面還是有很大的浪費的,畢竟前面26位都沒有使用了。所以說,在讀取撲克牌的數值的時候,要右移兩位。

  黑白雙方的牌利用數組來表示:int deal[2][6];

  利用count計數器數組來記錄重復的值 int count[13];

  利用rank來標記梭哈游戲的每一種規則 int rank;

  一手牌中不同值的個數為 int number[9]={5,4,3,1,1,5,2,1,1},對應每一種規則的點數不同的牌的數目;

  利用二維數組int value[2][7]來保存牌的大小

  牌型剖析

(1)       在比較的時候,首先要比較最大的那個,所以,首先對撲克牌進行降序排序:qsort(deal[i],5,sizeof(int),&compare),其中i可以選擇0或者是1來對應白方和黑方

(2)       統計5張牌地數值重復的個數,將統計的結果放在數組count中

(3)       分別對13張撲克進行枚舉,級別放在變量rank中(這也就是牌型剖析的內容

(A)如果重復的次數為2,可以判定為1個對子,2個對子或者葫蘆

(B)如果重復的次數為3,可以判定為1個條子或者葫蘆

(C)如果重復的次數是4,可以判定為鐵支

(D)如果重復的次數是5,相鄰牌的值相差為1,可以判定為順子(已經排序過了)

(E)如果重復的次數是5,五張牌的花色都一樣,可以判定為同花

(F)同時滿足條件(D)和條件(E),則可以判定為同花順

 (4)   進行級別的判斷的時候,可以保存不同牌的值

 (5)   在value中存放的是number的值,rank的值以及number個不同牌的值(位數由低到高)兩家通過級別rank和牌的大小number進行比較,決定勝負。

 

  示范輸入: 2H 3D 5S 9C KD 2C 3H 4S 8C AH

 示范輸出: White wins.

  Source中用到了很多位標志存儲的技巧(可以借鑒對溢出的一些處理)

 

  1   // 輸入輸出函數的控制 
  2   #include<stdio.h> 
  3   // 這兩個頭文件主要為了開啟qsort和memset函數 
  4   #include<memory.h> 
  5  #include<stdlib.h> 
  6   
  7   // 一副牌的值和花色 
  8    char *deck= " 23456789TJQKA ",*suit= " CDHS "
  9   
 10   // 一手牌中不同值的個數(按照rank進行排列的) 
 11    int number[ 9]={ 5, 4, 3, 1, 1, 5, 2, 1, 1}; 
 12   
 13   // 這里定義了一個排序因子,后來會在qsort中調用 
 14    int compare( const  void *a, const  void *b) 
 15  { 
 16     // 在返回時,認定a,b為int類型的變量(最開始a,b未定型)按照int的寬度讀指針所在的數值 
 17      return ((*(( int *)b))-(*(( int *)a)));    
 18  } 
 19   
 20   int main() 
 21  { 
 22     // 為主函數開啟各種數據結構 
 23      int deal[ 2][ 6];  // 發兩家的牌 
 24      int value[ 2][ 7];  // 兩家牌的大小(包括number和rank) 
 25      int count[ 13];  // 撲克牌中每種牌值的重復次數 
 26      int *p_deal;  // 指向數組deal一行的指針 
 27      int *p_value;  // 指向數組value一行的指針 
 28      int i,j;  // 輔助變量 
 29      char card[ 10];  // 一張牌 
 30      while( 1// 持續不斷地讀到文件尾 
 31     { 
 32       // 存儲每行的10張牌的點數和花色 
 33        for(i= 0;i< 2;i++) 
 34      { 
 35         for(j= 0;j< 5;j++) 
 36        { 
 37           // 說明讀到文件尾 
 38            if(scanf( " %s ",card)==EOF)  return  0
 39           // 保存每張牌,利用strchr函數比對,前兩位方花色信息,后四位放點數信息 
 40           deal[i][j]=((strchr(deck,card[ 0])-deck)<< 2)+(strchr(suit,card[ 1])-suit);                
 41        }                
 42      }         
 43       int rank;  // 等級信息 
 44        int k;  // 記錄某種規則出現的次數 
 45       memset(value, 0, sizeof(value));  // 將value數組清0,對於有些編譯器而言,這個過程可以忽略 
 46        // 分別處理每一家的牌 
 47        for(i= 0;i< 2;i++) 
 48      { 
 49        qsort(deal[i], 5, sizeof( int),&compare);  // 將5張牌降序排序 
 50         p_deal=deal[i]; 
 51        p_value=value[i]; 
 52        memset(count, 0, sizeof(count));  // 將計數器清0 
 53          // 利用計數器來記錄每張牌重復的個數(這里的原理和麻將(吳昊系列的新年特別篇)的原理是一樣的) 
 54          for(j= 0;j< 5;j++) 
 55          count[p_deal[j]>> 2]++; 
 56        rank= 0// rank初始置0 
 57          // 以下分別處理每一張牌,這里的點數一共13種,從最有威力的開始,相當於牌型分析的過程 
 58          for(j= 12;j>= 0;j--) 
 59        { 
 60           // 如果有重復的牌的話 
 61            if(count[j]> 1)      
 62          { 
 63             // 由於同一點數的牌只可能有四張,故不可能出現count[j]為5這種情況 
 64              switch(count[j]) 
 65            { 
 66               // 有一個對子 
 67                case  2:     
 68                    switch(rank) 
 69                   { 
 70                      case  0: rank= 1; p_value[ 2]=j;  break// 第一個對子 
 71                       case  1: rank= 2; p_value[ 3]=j;  break// 第二個對子 
 72                       case  3: rank= 6break// 存在一個三條           
 73                    }           
 74                    break
 75               case  3
 76                    // 這樣寫防止錯誤 
 77                     if( 0==rank) rank= 3; // 只有一個三條 
 78                     else 
 79                   { 
 80                      // 存在一個葫蘆 
 81                      rank= 6;     
 82                      // 記錄這個三條的值 
 83                      p_value[ 2]=j; 
 84                   } 
 85                    break
 86               case  4
 87                    // 存在一個鐵支 
 88                    rank= 7
 89                    // 記錄該鐵支的值 
 90                    p_value[ 2]=j; 
 91                    break
 92            }                    
 93          }             
 94           // 剩下的可能性只有count[j]=1,也就是單張牌 
 95            // 現在來考慮這5張牌是否有可能為順子,同花或者更進一步地,是同花順這種情況 
 96            if(rank< 6
 97          { 
 98             // k有助於我們判斷出是順子,同花還是同花順,這里置k的初始值為3 
 99              // 首先判斷花色,利用k的第0位來判斷 
100              for(j= 1;j< 5;j++) 
101               if((p_deal[j]& 3)!=(p_deal[ 0]& 3)) 
102              { 
103                k&= 2; // 因為2的二進制表示為"10",這樣與了之后可以置第0位為0 
104                  break;                                
105              }          
106             // 然后我們來判斷牌的點數是不是順的,利用k的第1位為判斷 
107              for(j= 1;j< 5;j++) 
108               if((p_deal[j]>> 2)!=(p_deal[j- 1]>> 2)- 1
109              { 
110                k&= 1; // 因為1的二進制表示為"01",這樣與了之后可以置第1位為0 
111                  break;                                      
112              } 
113             // 現在我們可以加以判斷了,因為一共四種情況,利用k的第0位和第1位就可以快速進行分類 
114              if(k== 1) rank= 5// 同花 
115              if(k== 2) rank= 4// 順子 
116              if(k== 3) rank= 8// 同花順 
117           } 
118           // 記錄順子的最大值,這里首先要判定確實是一個順子的最小值(這里由於是順子,最大值和最小值一樣) 
119            if((rank== 4)||(rank== 8)) 
120          { 
121            p_value[ 2]=p_value[ 4]>> 2;                         
122          } 
123           // 保存散牌或者同花(散牌)的所有值 
124            if((rank== 0)||(rank== 5)) 
125          { 
126             for(j= 0;j< 5;j++) 
127            { 
128              p_value[j+ 2]=(p_deal[j]>> 2);                
129            }                        
130          } 
131           // 當只有一個對子的時候,為了防止對子相等的情況,還是需要保留除了對子以外的其余牌的值 
132            if(rank== 1
133          { 
134             // 這里的k有另外的含義,其標識數組下標的位置 
135             k= 3
136             for(j= 0;j< 5;j++) 
137            { 
138               if((p_deal[j]>> 2)!=p_value[ 2]) 
139                p_value[k++]=(p_deal[j]>> 2);                
140            }           
141          } 
142           // 當有兩個對子的時候(也就是有一張牌是單牌),這里同上,保存除了對子以外的那張牌的值 
143            if(rank== 2
144          { 
145             // 還是先找到對應的數組下標 
146             k= 4
147             for(j= 0;j< 5;j++) 
148            { 
149               if((p_deal[j]>> 2)!=p_value[ 2]) 
150              { 
151                 if((p_value[j]>> 2)!=p_value[ 3]) 
152                { 
153                  p_value[k++]=(p_value[j]>> 2);                               
154                }                              
155              }                
156            }           
157          } 
158           // value的第一位放置等級值,而第0位放置這個等級所對應的牌的張數 
159           p_value[ 1]=rank; 
160          p_value[ 0]=number[rank]; 
161        } 
162      }                
163       int match=value[ 0][ 0];  // 讀第一家牌中的不同值的個數 
164        int *hand1=value[ 0];  // 讀第一家牌的具體情況 
165        int *hand2=value[ 1];  // 讀第二家牌的具體情況 
166        // 兩家牌比大小,每比較一次,將不同值牌的個數--,換一張不同值的牌,首先比較的還是等級rank 
167        while(*(++hand1)==*(++hand2)) 
168      { 
169         // 一直到所有的牌比完位置 
170          if((--match)< 0
171           break;                             
172      } 
173       // 最后的判斷了! 
174        if(match< 0) printf( " Tie.\n "); 
175       else  if(*hand1>*hand2) printf( " Black wins.\n "); 
176       else printf( " White wins.\n "); 
177    } 
178     return  0;    
179  } 
180   

 

   梭哈的變種——德州撲克

  七張牌梭哈是五張牌梭哈的變體,大約誕生於20世紀初,因為上述其四明一暗的方式暴露過多信息,同時由於不易成牌,容易作弊等緣故,七張牌梭哈更為流行,並且在美國德州撲克誕生前是最為流行的玩法,而至今該玩法還有眾多玩家,在WSOP等世界級大賽中也有其項目。中國一直到1999年澳門的和記娛樂城才引進了這一歐洲流行游戲。

  七張牌梭哈通常以有限注的形式游戲(當然無限注也可了),它沒有公共牌並可供2-9人游戲。開始時每人發兩張面朝下和一張面朝上的牌;兩張底牌和一張門牌。在游戲的過程中,每個游戲的玩家再發3張面朝上的牌和最后1張面朝下的牌,他們必須在攤牌時用其中的5張牌組成一手牌。拿到最大一手牌的玩家贏得本輪和底池。

 

  德州撲克與梭哈的區別

 梭哈是先發一張底牌一張明牌,根據牌面大小可以選擇加注或放棄,然后一次發剩余的3張牌,每發一張都有加注和放棄的權利。
  德州撲克是先發兩張底牌,根據底牌下注過牌或放棄,然后在發5張明牌,明牌是所有人的牌,第一次發3張明牌,然后每次一張,每次都有加注或放棄的權利。最后由手里的兩張底牌加上5張明牌組合出你手里最大的牌的組合和其他對手比較。
  比牌梭哈和德州撲克一樣,都是同花大順最大,其次四條,然后葫蘆(3+2),同花,順子,三條,兩對,一對,散牌最小。(如果是真人賭博的話,要詢問好同花和順子的大小,因為有些地方是順子贏同花,其他不變)
  梭哈和德州撲克下注也有所區別;梭哈一般都要求有最低限度的籌碼,低於最低限度的籌碼不能進行游戲。德州撲克在這一點上有所改善,只要你還有超過底注的多余籌碼就可以繼續這個游戲,當然你贏得也是你底注+上你多余的籌碼。
  壓注叫法:如果你想一次性贏光對手的籌碼或者將自己的籌碼全部作為賭注---梭哈叫梭哈,德州撲克叫all in

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM