罗大佑有歌云:“无聊的日子总是会写点无聊的歌曲……”,我不是歌手,我是程序员,于是无聊的日子总是会写点无聊的程序。程序不能太大,不然没有时间完成;程序应该有趣,不然就达不到消磨时间的目的;程序应该有那么一点挑战性,不然即使写完了也没有进步。
金钩钓鱼游戏是我儿时经常玩的一种扑克牌游戏,规则非常简单,两个玩家,一旦牌发到手里之后,接下来每个人出什么牌基本上已经就定了,玩家没有自己做决策的机会,所以这个游戏很容易用程序自动模拟出来。
(一)关于金钩钓鱼游戏
基本规则(简化版):两个玩家(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次),纵轴为每次游戏所对应的出牌手数。