羅大佑有歌雲:“無聊的日子總是會寫點無聊的歌曲......”,我不是歌手,我是程序員,於是無聊的日子總是會寫點無聊的程序。程序不能太大,不然沒有時間完成;程序應該有趣,不然就達不到消磨時間的目的;程序應該有那么一點挑戰性,不然即使寫完了也沒有進步。
金鈎釣魚游戲是我兒時經常玩的一種撲克牌游戲,規則非常簡單,兩個玩家,一旦牌發到手里之后,接下來每個人出什么牌基本上已經就定了,玩家沒有自己做決策的機會,所以這個游戲很容易用程序自動模擬出來。
(一)關於金鈎釣魚游戲
基本規則(簡化版):兩個玩家(Player),一副撲克(Deck),大小王(Joker)可要可不要,我們的游戲假定包含大小王,洗牌(Shuffle)之后,每個玩家得到同樣數目的牌(27張),玩家任何時候不能看自己手里的牌,玩家依次出牌,每次出一張,輪到自己出牌時,抽出自己手中最底下的一張牌放到牌桌(Board)上,牌桌上的牌按照玩家出牌的順序擺成一條長鏈。J(鈎)是最特殊的一張牌,當某個玩家出到J時,便將牌桌上的所有牌都歸為己有,並放到自己牌池的最上面(與出牌時恰恰相反),此即所謂“金鈎釣魚”,此時牌桌清空,再由此玩家重新出牌。另外,當自己出的牌與牌桌上的某張牌點數相同時,便將牌桌中那張牌及其之后的牌都歸為己有(包含自己剛出的那張),再由此玩家重新出牌,比如牌桌上的牌為3,7,8,4,9,當某個玩家出了8,便將牌桌上的8,4,9連同自己剛出的8一並收回,派桌上剩下3,7。最后,誰手中的牌最先出完,誰就輸了。
(二)對於一副牌的建模
由於花色(Suit)對於此游戲並不重要,所以對撲克牌建模時省略了對花色的建模,同樣,由於不需要比較大小,牌的點數(Rank)可以用String來表示(其中王用"W"表示)。

package com.thoughtworks.davenkin.simplefishinggame;
public class Card {
private String rank;
public Card(String rank) {
this.rank = rank;
}
public String getRank() {
return rank;
}
}
一副撲克(Deck)由54張牌組成:

package com.thoughtworks.davenkin.simplefishinggame;
import java.util.ArrayList;
import java.util.Collections;
public class Deck {
ArrayList<Card> cards = new ArrayList<Card>();
public Deck() {
buildDeck();
}
private void buildDeck() {
buildNumberCards();
buildCard("J");
buildCard("Q");
buildCard("K");
buildCard("A");
buildJokerCard();
}
private void buildJokerCard() {
cards.add(new Card("W"));
cards.add(new Card("W"));
}
private void buildNumberCards() {
for (int rank = 2; rank <= 10; rank++) {
buildCard(rank);
}
}
private void buildCard(int rank) {
for (int index = 1; index <= 4; index++) {
cards.add(new Card(String.valueOf(rank)));
}
}
private void buildCard(String rank) {
for (int index = 1; index <= 4; index++) {
cards.add(new Card(rank));
}
}
public ArrayList<Card> getCards() {
return cards;
}
public void shuffle() {
Collections.shuffle(cards);
}
}
Deck不僅包含54張牌,還定義了洗牌(shuffle)等方法。
(三)對玩家的建模
玩家(Player)有自己的名字和自己手中所剩的牌,最重要的是出牌(playCard)成員方法:

package com.thoughtworks.davenkin.simplefishinggame;
import java.util.ArrayList;
import java.util.List;
public class Player {
ArrayList<Card> cards = new ArrayList<Card>();
String name;
public Player(String name) {
this.name = name;
}
public String getName() {
return name;
}
public ArrayList<Card> getCards() {
return cards;
}
public void obtainCards(List<Card> cardsToAdd) {
cards.addAll(cardsToAdd);
}
public void playCard(Board board) {
board.addCard(cards.get(0));
System.out.println(name + " played " + cards.get(0).getRank());
board.displayCards();
cards.remove(0);
}
public void displayCards() {
System.out.print("Cards for " + name + ": ");
for (Card card : cards) {
System.out.print(card.getRank() + " ");
}
System.out.println();
}
}
游戲開始需要發牌,專門定義了一個CardDistributor來發牌,每個玩家得到相同數量的牌。當然,發牌動作應該在洗牌之后:

package com.thoughtworks.davenkin.simplefishinggame;
import java.util.List;
public class CardDistributor {
public void distributeCards(Deck deck, List<Player> players) {
int cardsPerPlayer = deck.getCards().size() / players.size();
int startIndex = 0;
for (Player player : players) {
player.obtainCards(deck.getCards().subList(startIndex, cardsPerPlayer + startIndex));
startIndex += cardsPerPlayer;
}
}
}
玩家在出牌時,需要將自己手中的一張牌轉移到牌桌上(Board),而當Player出牌之后,牌桌應該確定是否有將被Player“釣”進的牌,於是在Borad中還定義了getCardsToBeFished方法:

package com.thoughtworks.davenkin.simplefishinggame;
import java.util.ArrayList;
import java.util.List;
public class Board {
ArrayList<Card> cards = new ArrayList<Card>();
public ArrayList<Card> getCards() {
return cards;
}
public void addCard(Card card) {
cards.add(card);
}
public List<Card> getCardsToBeFished() {
if (cards.size() == 1)
return null;
List<Card> cardsToBeFished;
Card lastCard = cards.get(cards.size() - 1);
if (lastCard.getRank().equals("J")) {
cardsToBeFished = cards;
} else {
cardsToBeFished = getCardsOfRangeFishing(lastCard);
}
return cardsToBeFished;
}
public void displayCards() {
System.out.print("Current cards on board:");
for (Card card : cards) {
System.out.print(card.getRank() + " ");
}
System.out.println();
}
public void removeFishedCards(List<Card> cardsToBeFished) {
int endIndex = getCards().indexOf(cardsToBeFished.get(0));
ArrayList<Card> newCards = new ArrayList<Card>();
newCards.addAll(cards.subList(0, endIndex));
cards = newCards;
}
private List<Card> getCardsOfRangeFishing(Card lastCard) {
int startIndex = -1;
for (Card card : cards) {
if (card == lastCard)
break;
if (card.getRank().equals(lastCard.getRank())) {
startIndex = cards.indexOf(card);
}
}
if (startIndex != -1)
return cards.subList(startIndex, cards.indexOf(lastCard) + 1);
return null;
}
}
(四) 對整個游戲的建模
整個游戲定義了一個FishingManager來集中管理,FishingManager包括所有玩家,牌桌等成員變量。

package com.thoughtworks.davenkin.simplefishinggame;
import java.util.ArrayList;
import java.util.ListIterator;
public class FishingManager implements FishingRuleChecker, AfterPlayListener {
ArrayList<Player> players = new ArrayList<Player>();
private Player currentPlayer;
Board board;
private ListIterator<Player> iterator;
public FishingManager() {
board = new Board();
}
private void resetPlayerIterator() {
iterator = players.listIterator();
}
public void addPlayers(ArrayList<Player> players) {
this.players.addAll(players);
resetPlayerIterator();
}
@Override
public Player nextPlayer() {
if (iterator.hasNext()) {
return iterator.next();
}
resetPlayerIterator();
return nextPlayer();
}
@Override
public Player whoFailed() {
ListIterator<Player> listIterator = players.listIterator();
while (listIterator.hasNext()) {
Player currentPlayer = listIterator.next();
if (currentPlayer.getCards().size() == 0)
return currentPlayer;
}
return null;
}
@Override
public void afterPlay() {
if (board.getCardsToBeFished() == null)
return;
doFish();
nextPlayer();
}
private void doFish() {
System.out.println(currentPlayer.getName() + " fished cards");
currentPlayer.obtainCards(board.getCardsToBeFished());
board.removeFishedCards(board.getCardsToBeFished());
currentPlayer.displayCards();
board.displayCards();
}
public void start() {
int count = 0;
while (true) {
currentPlayer = nextPlayer();
currentPlayer.displayCards();
currentPlayer.playCard(board);
afterPlay();
count++;
if (whoFailed() != null) {
break;
}
}
System.out.println(whoFailed().getName() + " has failed.");
System.out.println("Total: " + count + " rounds");
}
public static void main(String[] args) {
FishingManager manager = new FishingManager();
Player player1 = new Player("Kayla");
Player player2 = new Player("Samuel");
ArrayList<Player> players = new ArrayList<Player>();
players.add(player1);
players.add(player2);
Deck deck = new Deck();
deck.shuffle();
CardDistributor distributor = new CardDistributor();
distributor.distributeCards(deck, players);
manager.addPlayers(players);
manager.start();
}
}
FishingManager還應該包含游戲規則,比如決定輸贏和玩家出牌順序等,於是定義一個游戲規則接口FishingRuleChecker,並使FishingManager實現FishingRuleChecker接口:

package com.thoughtworks.davenkin.simplefishinggame;
public interface FishingRuleChecker {
Player nextPlayer();
Player whoFailed();
}
同時,當每個玩家出牌之后,FishingManager應該決定是否有魚上鈎,並執行釣魚操作,於是定義了一個AfterPlayListener接口,FishingManager也實現了
AfterPlayListener接口:

package com.thoughtworks.davenkin.simplefishinggame;
public interface AfterPlayListener {
public void afterPlay();
}
(五)有趣的現象
運行FinshingManager便可以自動模擬整個游戲過程,筆者比較感興趣的是:所有玩家一共出多少手牌之后游戲結束?於是筆者做了10000次模擬試驗,得到的結果為:最大14023手,最小66手,平均1303手,請數學高手幫忙證明一下是否有個統計學意義上的期望值。出牌次數分布圖如下:
上圖中,橫軸為游戲輪次(一共10000次),縱軸為每次游戲所對應的出牌手數。