14.6 Eights

13.2 节介绍了自上而下的开发,这种程序开发方式确定高层次的目标,如洗牌,并将其分解为更小的问题,如在数组中查找最小的元素或交换两个元素。

本节将介绍自下而上的开发(bottom-up development),它反过来做,即先找出需要的简单部分,再将它们组装成更复杂的算法。

根据 Crazy Eights 的规则,可确定需要的一些方法。

  • 创建整副扑克牌、弃牌堆、储备牌以及表示玩家的对象。

  • 发牌。

  • 检查游戏是否结束。

  • 如果没有了储备牌,就将弃牌堆洗一下,并将其作为储备牌。

  • 取牌。

  • 跟踪轮到谁出牌以及从一个玩家切换到下一个玩家。

  • 显示游戏的状态。

  • 进入下一轮前等待用户。

现在可以开始实现这些部分了。下面是封装游戏状态的 Eights 类定义的开头部分:

  1. public class Eights {
  2. private Player one;
  3. private Player two;
  4. private Hand drawPile;
  5. private Hand discardPile;
  6. private Scanner in;

这个版本只有两个玩家。本章末尾有一个练习,要求你修改这些代码,以支持更多玩家。

最后一个实例变量是一个 Scanner 对象,我们将在每次出牌后用它提示用户输入。下面的构造函数初始化所有的实例变量并发牌:

  1. public Eights() {
  2. Deck deck = new Deck("Deck");
  3. deck.shuffle();
  4. int handSize = 5;
  5. one = new Player("Allen");
  6. deck.deal(one.getHand(), handSize);
  7. two = new Player("Chris");
  8. deck.deal(two.getHand(), handSize);
  9. discardPile = new Hand("Discards");
  10. deck.deal(discardPile, 1);
  11. drawPile = new Hand("Draw pile");
  12. deck.dealAll(drawPile);
  13. in = new Scanner(System.in);
  14. }

接下来需要实现的是检查游戏是否结束的方法。只要有一个玩家手里没牌,游戏便结束了:

  1. public boolean isDone() {
  2. return one.getHand().empty() || two.getHand().empty();
  3. }

没有储备牌时,我们必须将弃牌堆洗一下。完成这种任务的方法如下:

  1. public void reshuffle() {
  2. Card prev = discardPile.popCard();
  3. discardPile.dealAll(drawPile);
  4. discardPile.addCard(prev);
  5. drawPile.shuffle();
  6. }

第 1 行保存 discardPile 最上面的那张牌;第 2 行将余下的牌转移到 drawPile。接下来,我们将保存的牌放回到 discardPile,并对 drawPile 执行洗牌操作。

现在可以在 draw 中调用 reshuffle 了:

  1. public Card draw() {
  2. if (drawPile.empty()) {
  3. reshuffle();
  4. }
  5. return drawPile.popCard();
  6. }

要从一个玩家切换到另一个玩家,我们可以像下面这样做:

  1. public Player nextPlayer(Player current) {
  2. if (current == one) {
  3. return two;
  4. } else {
  5. return one;
  6. }
  7. }

方法 nextPlayer 将当前玩家作为参数,并返回接下来轮到的玩家。

最后两个部分是 displayStatewaitForUser

  1. public void displayState() {
  2. one.display();
  3. two.display();
  4. discardPile.display();
  5. System.out.println("Draw pile:");
  6. System.out.println(drawPile.size() + " cards");
  7. }
  8. public void waitForUser() {
  9. in.nextLine();
  10. }

可用这些方法编写 takeTurn,用来执行玩家的一次出牌过程:

  1. public void takeTurn(Player player) {
  2. Card prev = discardPile.last();
  3. Card next = player.play(this, prev);
  4. discardPile.addCard(next);
  5. System.out.println(player.getName() + " plays " + next);
  6. System.out.println();
  7. }

takeTurn 读取弃牌堆最上面的那张牌,将其传递给前一节介绍过的 player.play,并将它返回的牌加入弃牌堆。

最后,我们用 takeTurn 和其他方法来编写 playGame

  1. public void playGame() {
  2. Player player = one;
  3. // 不断地玩,直到有人获胜
  4. while (!isDone()) {
  5. displayState();
  6. waitForUser();
  7. takeTurn(player);
  8. player = nextPlayer(player);
  9. }
  10. // 显示最终得分
  11. one.displayScore();
  12. two.displayScore();
  13. }

大功告成!注意,自下而上开发的结果与自上而下类似:有一个调用辅助方法的高级方法。主要差别在于实现解决方案的顺序不同。