天天看點

Java語言實作的有趣問題:你的鬥地主能拿多少炸?

  最近無聊,想知道一下玩鬥地主的話我能有多大的機率拿到炸彈(4張同點數牌 或 集齊大小王)。但是我機率學學得不好,于是想到用統計學來試試,随手寫了一個程式模拟一下鬥地主的發牌過程

  面向對象Card

  首先依據OOP思想,我把牌看作是一個對象,點數與花色是其屬性,為了處理大小王加入了Type屬性

  public class Card {

  Suit suit;

  Size size;

  Type type;

  Card(Suit suit, Size size) {

  this.suit = suit;

  this.size = size;

  this.type = Type.Ordinary;

  }

  Card(Type type) {

  if (type.equals(Type.Ordinary)) {

  throw new RuntimeException(非法參數);

  }

  this.type = type;

  }

  }

  三個屬性我都用了枚舉類來表示,純粹是因為既然是面向對象,那麼就純粹一點

  public enum Size {

  P3(0), P4(1), P5(2), P6(3), P7(4), P8(5), P9(6),

  P10(7), J(8), Q(9), K(10), A(11), P2(12);

  int sequence;

  Size(int sequence) {

  this.sequence = sequence;

  }

  }

  Size表示點數的大小,按從小到大排序。加入sequence屬性目的是為了在統計時友善處理

  public enum Suit {

  Spade(4), Heart(3), Club(2), Diamond(1);

  // 權重

  int weight;

  Suit(int weight) {

  this.weight = weight;

  }

  }

  Suit花色,加入了weight屬性作為大小權重,鬥地主花色不分大小,不過有些牌會區分,是以随手加一下

  public enum Type {

  Ordinary(0), LITTLE_JOKER(1), BIG_JOKER(2);

  int weight;

  Type(int weight) {

  this.weight = weight;

  }

  }

  Type牌類型,主要是為了特殊處理大小王,根據權重值,小王比大王小~

  計算過程

  首先抽象一下玩牌的幾個步驟,第一步:拿出一副牌;第二步:洗牌(随機打亂);第三步:發牌;第四步:Play(計算是否有炸彈);我簡化了發牌方法,放進主程式中,其他步驟具體實作如下

  

  static Card[] newCards() {

  // 牌組用數組表示

  Card[] cards = new Card[54];

  // 遊标i

  int i = 0;

  // 循環放牌 13種大小 * 4種花色 = 52

  for (Size point : Size.values()) {

  for (Suit suit : Suit.values()) {

  cards[i++] = new Card(suit, point);

  }

  }

  // 插入大小王

  cards[52] = new Card(Type.LITTLE_JOKER);

  cards[53] = new Card(Type.BIG_JOKER);

  return cards;

  }

  

  static Card[] shuffle(Card[] cards) {

  Random random = new Random();

  int len = cards.length;

  // 複雜的 O(n)

  // 周遊一副牌,每次循環随機取一張目前牌後面的一張牌(含目前牌)與目前牌交換

  // 在完全随機的情況下,每張牌在每個位置的機率應該一緻,共有 n! 種情況

  for (int i = 0;i len; i++) {

  int r = random.nextInt(len - i);

  change(i, r + i, cards);

  }

  return cards;

  }

  // 簡單的互動位置方法

  static void change(int a, int b, Card[] cards) {

  Card temp = cards[a];

  cards[a] = cards[b];

  cards[b] = temp;

  }

  static final int DOUBLE_JOKER = 3;

  static final int FULL_SUIT = 10;

  

  static boolean hasBoom(Card[] cards) {

  // 構造一個與Size數量等長的初始為0的數組

  int[] counter = new int[Size.values().length]; //初始化為0

  // 特殊處理大小王

  int weightOfJoker = 0;

  for (Card card: cards) {

  // 特殊處理大小王

  if (!card.type.equals(Type.Ordinary)) {

  weightOfJoker += card.type.weight;

  // 大小王權重和為3時即王炸

  if (weightOfJoker == DOUBLE_JOKER) {

  return true;

  }

  continue;

  }

  // 利用點數序列值為下标,加上權重值,和為10時即湊足4張牌

  counter[card.size.sequence] += card.suit.weight;

  if (counter[card.size.sequence] == FULL_SUIT) {

  return true;

  }

  }

  return false;

  }

  遊戲主方法,來算算地主和農民各有多少機率吧

  public static void main(String[] args) {

  long gameStart = System.currentTimeMillis();

  int gameTime = 100000;

  // 農民17張牌計數器

  int nongHasBoom = 0;

  // 地主20張牌計數器

  int diHasBoom = 0;

  // 運作遊戲

  for (int i = 0;i gameTime; i++) {

  // 拿到一副新牌

  Card[] poker = newCards();

  // 洗牌

  poker = shuffle(poker);

  // 發牌 在随機的情況下,連續發和分開發理論上不影響你拿牌的機率,簡化

  Card[] nong = Arrays.copyOf(poker, 17);

  Card[] di = Arrays.copyOfRange(poker, 17, 17 + 20);

  nongHasBoom += hasBoom(nong)? 1 : 0;

  diHasBoom += hasBoom(di)? 1 : 0;

  }

  long gameEnd = System.currentTimeMillis();

  System.out.println(String.format(地主炸彈機率 %f , 農民炸彈機率 %f, diHasBoom * 1.0 / gameTime, nongHasBoom * 0.1 / gameTime));

  System.out.println(String.format(運作時 %f s, (gameEnd - gameStart)/1000.0));

  }

  運作一次程式,發現運作速度還挺快,反正10萬次不足半秒,運作才發現,地主三張牌對拿炸彈的機率影響竟然這麼大,可以提升将近一倍的機率。當然程式是我随便寫的,可能存在不嚴謹導緻資料錯誤的地方,如果發現還請斧正。其次在枚舉類的書寫規範上,我偷了一些懶,沒有全部大寫~

Java語言實作的有趣問題:你的鬥地主能拿多少炸?