Lab 12-1: 文字界面接龍撲克牌遊戲

 
實習目標 練習設計規劃物件系統
練習 bottom-up 的物件實作方式
 
步驟一 接龍這個撲克牌遊戲簡易的說明如下:
這是一個四個人玩的撲克牌遊戲, 一開始每個人發 13 張牌, 規定拿到黑桃七 (Spade 7) 的人要先出黑桃七, 接下來順時鐘方向輪流出牌直到所有牌都出完為止。

任何時候如果手上有點數為七的牌的話, 不管花色是什麼都是可以出的, 或是手上有剛好可以接續自己或是別人已經出的牌的點數, 不管是向上接或是向下接也都可以出, (例如: 如果黑桃七出了以後, 下一家如果有黑桃八, 黑桃六, 紅心七, 紅磚七, 梅花七都可以出)

規定如果手上有可以出的牌一定要由其中選一張出, 如果都沒有牌可以出的話, 可以從手上挑一張牌蓋起來, 等到所有四個人手上的牌都出完了, 算看看蓋的牌的總點數最低的為贏家

步驟二 由上面的描述中我們可以找到一些必要的物件, 例如:
  • 撲克牌 Card
  • 一疊 52 張牌, 可以洗牌可以進行發牌 CardDeck
  • 放在四個人中央已經出的四個花色的牌堆 CenterDeck
  • 四個玩牌的人 Player
  • 還有整個遊戲也是一個物件 Game

它們的靜態關係大致如下圖所示:

使用 bottom-up 的方式製作時, 可以挑功能比較單純的類別開始實作, 完整的物件導向設計其實應該要多在抽象層面設計所有的功能然後才開始實作, 可是大家並不熟悉物件導向設計的工具圖, 而且對於程式本身的運作也還不夠熟悉, 所以我們直接由實作來切入這個練習。

你喜歡的話, 也可以把手上的牌用一個容器物件把它組合起來, 每個人蓋在桌面的牌也可以用一個容器物件把它組織起來。

步驟三 Card 類別

首先我們挑 Card 類別來實作, 每一張牌有不同的花色 (Spade, Heart, Diamond, Club), 以及不同的點數 (1, 2, 3, ..., 13), 應該要設計資料欄位來存放這兩個屬性。

為了讓程式製作起來比較清楚, 我們選擇用 enum 敘述來定義四種花色 (Spade, Heart, Diamond, Club),

並且定義適當的建構元

它的界面我們暫時先不詳細設計, 慢慢地再加上來

步驟四 CardDeck 類別

接下來我們來定義洗牌和發牌用的牌堆 CardDeck, 很明顯地這個類別需要記錄所有 52 張牌, 在建構元中作出所有的 52 張牌, 需要提供洗牌的服務 shuffle(), 以及發牌的服務 deal()。

由於我們的遊戲一開始就把所有的牌都發完, 所以這個 CardDeck 物件可能發完牌就不需要了。

我們可以替這個類別加上 display(ostream &) 的功能, 如此我們可以用下列程式來測試, 當然在替 CardDeck 類別加上 display() 界面時, 你也會想到該替 Card 類別也加上一個 display(ostream &) 的界面。(請注意我們很早就開始寫測試程式囉!! 你現在對 C++ 的語法還不熟悉, 程式可以寫一點就測試一點, 不要等到通通寫完了再一起測, 一團亂就不好了)

    void main()
    {
        srand(time(0));
        CardDeck cardDeck;
        cout << "Before shuffling\n"; 
        cardDeck.display(cout); cout << endl;
        cout << "After shuffling\n"; 
        cardDeck.shuffle(); 
        cardDeck.display(cout);    
    }
範例程式

測試結果如下:

    Before shuffling
    S1 S2 S3 S4 S5 S6 S7 S8 S9 ST SJ SQ SK
    H1 H2 H3 H4 H5 H6 H7 H8 H9 HT HJ HQ HK
    D1 D2 D3 D4 D5 D6 D7 D8 D9 DT DJ DQ DK
    C1 C2 C3 C4 C5 C6 C7 C8 C9 CT CJ CQ CK
    
    After shuffling
    D1 ST D5 C9 CK D9 SQ S5 H4 C6 DK D3 CJ
    C3 S2 C7 H2 S6 HJ C4 D7 H5 C5 S7 H6 CT
    S8 H1 S3 DQ DJ H8 HK H7 HT H3 CQ SK H9
    D4 D2 S9 C8 DT HQ S4 S1 D8 C2 SJ D6 C1

如此我們知道我們的洗牌應該是成功的, 接下來應該是發牌的動作 deal(Player []), 由於 Player 類別還沒有開始撰寫, 而且 deal() 函式基本上就是一個簡單的迴圈, 所以可以先設計 Player 類別

在設計這個類別的時候你也許會有一個疑問, 平常我們在發牌的時候不是應該由四個人裡頭一個人作為莊家, 由他來洗牌和發牌嗎? 為什麼在這個程式裡我把洗牌發牌設計成 CardDeck 類別的功能呢? 很怪吧! 至少和實際世界裡玩牌的流程好像有一點不太一樣, 那為什麼會有這樣子的設計呢??

其實會有這樣的設計是從實作面衍生出來的, 如果你讓 CardDeck 物件的主要功能和實際玩牌時一模一樣, 那麼它的功能應該只是一個容器, 最主要就是記錄有哪些牌, 順序是什麼而已, 所有洗牌發牌的動作應該都設計到 Dealer 物件裡面, 這樣子會有兩個我們不喜歡的現象發生:

  1. Dealer 物件要密集地去操作 CardDeck 物件裡的 52 張牌, 物件之間會有很多資料的傳遞
  2. Dealer 物件或是 Player 物件的功能太多了, 變成一個超級大的類別

所以我們才會有像上述這樣的設計, 而且你可以這樣想, 如果在實際玩牌的時候有一個洗牌機可以幫你洗牌, 這樣子不是大家都比較愉快一些, 也比較不怕莊家欺騙, 不是嗎? 這個 CardDeck 就是這樣一個聰明的洗牌機 (有看過麻將的洗牌機吧!!)

步驟五 Player 類別的基本功能包括:

  • 很明顯地 Player 類別需要能夠儲存每個人手上的 13 張牌

  • 每一個 Player 應該擁有他自己的 ID, 如此在計算輸贏, 列印手上的牌時應該都比較方便

  • 同時配合 CardDeck 類別的 deal() 功能, Player 類別需要設計一個界面來接收 CardDeck 發給它的 13 張牌, 例如 addCard(Card), 在發牌之前也應該有一個清除每一個人手上牌組的動作, 例如 clearCards()

  • 為了配合測試, 我們也替它加上一個 display(ostream &) 的界面

  • 其它功能我們再慢慢地加上去
我們可以用下面的程式片段來測試
    void main()
    {
        srand(time(0));
        CardDeck cardDeck;
        cardDeck.shuffle();
    
        int i;
        Player *player[4];
        for (i=0; i<4; i++)
            player[i] = new Player(i); // 參數 i 為 player 的 ID
            
        cardDeck.deal(player);
        for (i=0; i<4; i++)
            player[i]->display(cout);
    }
範例程式

測試結果如下:

    Player 0:C1 CQ H5 CK DQ S1 ST SJ H6 DK D1 C4 C3
    Player 1:H9 D3 C5 S2 SQ H3 C2 DJ HT S6 S3 HQ H8
    Player 2:SK HJ S4 D8 H1 S5 S8 DT D2 D7 HK CT C7
    Player 3:C8 H7 CJ S7 S9 H4 D5 D4 C6 H2 D9 D6 C9
現在每個 Player 手上都有 13 張牌了, 下一個實習中我們開始來看看該怎樣來出牌了
步驟六 請將所完成的 project (去掉 debug/ 資料匣下的所有內容) 壓縮起來儲存在 cyber 上你的帳號內, 後面的實習課程可能需要使用這裡所完成的程式

C++ 物件導向程式設計課程 首頁

製作日期: 04/27/2004 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程系 Lagoon