2005 Spring C++ 程式作業三:
        接龍 (Fantan) 遊戲設計

接龍 (Fantan) 遊戲設計

作業目標:

  1. 練習物件系統中繼承的設計

  2. 瞭解抽象類別的應用方法

  3. 練習事件驅動的程式設計方式

  4. 練習與其它系統物件合作

  5. 嘗試練習各種物件架構的設計

範例執行程式 (Random Player), 範例執行程式 (Smart Player) by legend,

請注意:

解壓縮後有一個 Fantan.exe 和一個 images\ 資料匣 裡面放的是所需要的影像檔案, 相對的關係要正確, 否則程式會找不到影像而用自己合成的圖片來執行

第一個程式中電腦扮演的三個玩家是用亂數來決定出牌的

第二個程式中電腦扮演的三個玩家都是用對自己比較有利的方式來出牌的

程式基本要求

這個接龍程式是大家常常四個人一起玩的遊戲, 並不是 Microsoft 那個一個人玩的接龍遊戲, 首先把所有的牌通通發完, 一人 13 張牌, 手上有黑桃七的人先出, 其他的人可以接在已經出的牌之後, 或是可以出紅心七, 磚塊七, 和梅花七。

如果手上沒有任何牌可以出的話, 可以蓋一張牌, 這張蓋住的牌以後就不能再出了, 同時其它三個人都不曉得你蓋了什麼牌, 這個遊戲一直完到所有手上 13 張牌都出完了才結束, 然後計算每個人所蓋的牌的總點數, 點數最少的是贏家

圖形界面使用方式

  1. 請下載下列含有圖形界面類別的程式 Fantan930416d.rar, 目前這個程式還沒有辦法執行

  2. 請繼承 CActionHandler 製作一個新的類別 CTestGame

  3. 在 CTestGame 中簡單實作所有純虛擬函式 dealCards(), player0Move(), player1Move(), player2Move(), userChoose() 例如:
    virtual int dealCards() { return 0;}
    virtual bool userChoose(Card card) { return true;}
    virtual bool player0Move() {return true;}
    virtual bool player1Move() {return true;}
    virtual bool player2Move() {return true;}

  4. 實作建構元 (ctor): CTestGame(CGraphicOutput *pGO):CActionHandler(pGO) {} 不需要預設建構元 (default ctor)

  5. 修改 maininit() 函式中動態配置的類別名稱 new CTestGame

  6. 編譯, 執行, 這已經是一個可以執行的程式了, 雖然現在看起來好像沒有做什麼

  7. 請檢查選單 (右鍵選單) 的功能, 已經可以控制動畫和音效了, 也可以看到遊戲參數調整和關於對話盒的內容

  8. "結束" 功能可以讓使用者結束應用程式, 剛才在 maininit() 中 new 的 CTestGame 物件會在這個時候被刪除掉, 你不需要另外寫一段程式去刪除

  9. 使用者選擇 "發牌" 功能後, 剛才在 CTestGame 類別中實作的 dealCards() 函式會被呼叫到, 你可以在函式內加入列印敘述 來驗證這件事
    TRACE("int CTestGame::dealCards()\n");
    並且以 dbwin32 應用程式來看到這個結果

  10. 在這個 dealCards() 函式內你的實作應該要包括下列: 準備 52 張牌, 洗牌, 並且把牌發給四個玩家, 然後呼叫
    m_pGO->setHand(int id, Card cards[]);
    在畫面上顯示每一個玩家所拿到的牌 (m_pGO 是一個 CActionHandler 類別裡的指標成員變數, 你的類別繼承了 CActionHandler, 所以可以直接用到, 這個指標變數指向一個提供視窗輸入輸出的物件, 這個物件提供例如上面這個 setHand() 的函式來設定每一個人手上的牌, 你只要給一個 id 和一個 13 張牌的陣列, 就可以把這些牌顯示在視窗界面中), 例如:
    int i;
    Card card[4][13];
    for (i=0; i<13; i++)
    {
        card[0][i] = Card(Spade, i+1);
        card[1][i] = Card(Heart, i+1);
        card[2][i] = Card(Diamond, i+1);
        card[3][i] = Card(Club, i+1);
    }
    for (i=0; i<4; i++)
        m_pGO->setHand(i, card[i]);
    注意你需要 include GraphicOutput.h 和 Card.h

    每一張牌的資料結構請運用在 Card.h 中定義好的 Card 類別, 上面函式的第一個參數 id 代表四個玩家, 在畫面左手邊的 id 是 0, 上方的玩家 id 是 1, 右方的玩家 id 是 2, 下方 (真正操作這個應用程式的使用者) id 是 3

    請注意當你在畫面上顯示時預設只有下方的玩家的牌是看得到的, 其它三個人的牌都是蓋住的

    如果你在測試程式時希望看到所有發出去牌的內容的話, 你可以在程式一開始的時候先呼叫 m_pGO->openAllCards();

  11. 請注意, 每次使用者選擇 "發牌/重新發牌時", UI 程式會自己把所有顯示的牌清除, 然後再呼叫一次
    CActionHandler::dealCards()

  12. 請注意 dealCards() 函式應該要傳回一個整數值, 這個整數資料必須是 0, 1, 2, 或 3, 代表四個玩家 應該由誰先開始出牌

    在這個接龍的遊戲中我們應該要由有黑桃七(Spade 7) 的玩家開始出牌, 第一張牌也必須要是黑桃七,

    因為在 dealCards() 函式中你自己寫的程式發玩牌了, 所以你應該可以測試出來哪一個玩家有黑桃七, 然後把這個玩家 的 id 值傳回去, 如此 UI 才知道下面該如何進行遊戲

  13. 接下來我們來看看另外四個自 CActionHandler 中繼承下來的函式的功能, 讓我們修改為
    bool CTestGame::userChoose(Card card)
    {
        TRACE("CTestGame::userChoose(Card card): suite=%d, face=%d\n", card.suite, card.face);
        return true;
    }
    bool CTestGame::player0Move() { TRACE("CTestGame::player0Move()\n"); return true; }
    bool CTestGame::player1Move() { TRACE("CTestGame::player1Move()\n"); return true; }
    bool CTestGame::player2Move() { TRACE("CTestGame::player2Move()\n"); return true; }

  14. 在 dbwin32 視窗內你可以看到, 如果你在 dealCards() 函式傳回 0 的話, 接下來 player0Move(), player1Move(), player2Move() 會依序被呼叫, 接下來如果使用者以滑鼠點選下方的某一張牌的話, userChoose(Card card) 就會被呼叫到, 同時被使用者選到的牌會當成參數 card 傳進來

    上面四個函式的回傳值事實上控制著 UI 如何繼續進行遊戲, 其中 playerXMove() 函式的回傳值如果是 true 的話, UI 會繼續依照順時針方向呼叫下一個 playerXMove() 函式,

    userChoose(Card) 函式的傳回值如果是 true 的話, UI 會繼續呼叫 player0Move() 函式, 如果是 false 的話, 代表你發現使用者挑錯牌了, (例如出的牌不能夠接上, 或是 該出黑桃七時不出), 你希望 UI 停下來讓使用者繼續挑選牌

  15. UI 呼叫 player0Move(), player1Move(), 以及 player2Move() 這三個函式時, 你的程式應該要替這三個人決定到底該怎麼出牌

  16. 不論是使用者或是你的程式決定了該出哪一張牌時, 基本上有兩種顯示方法,

    第一種是把牌接在四堆牌中適當的地方, 第二種是蓋牌 (就是當玩家沒有牌可以出時把某一張牌扣住的動作)

    你可以呼叫 CGraphicOutput 類別提供的 addCenter(Card) 來把所出的牌接在中間四堆牌中, 不過在呼叫之前應該先呼叫 CGraphicOutput 類別提供的 removeCard(int id, Card card) 從玩家手上的牌中移除, 因為我們知道在這個範例中 Spade 7 在左方玩家, 所以我們在 CTestGame::player0Move() 中加入

    m_pGO->removeCard(0, Card(Spade, 7)); // 由玩家手中移除
    m_pGO->addCenter(Card(Spade, 7)); // 接在中央牌堆中
    來顯示這張牌, 這兩個界面函式如果成功的話都會回傳 true, 你可以透過傳回值判斷你自己程式內的資料和 UI 內的資料是不是一致

  17. 如果你的程式判斷玩家沒有牌可以出時, 你的程式會決定蓋某一張牌, 此時可以用 CGraphicOutput 類別提供的 discardCard(int id, Card card) 來顯示, 例如, 在這個範例中 Heart 7 在上方玩家, 假設目前沒有牌可以出了, 上方玩家決定蓋住 Heart 7, (現在只是測試程式, 實際上不行這樣, 玩家有牌可以出的時候一定要出牌), 我們在 CTestGame::player1Move() 中加入
    ASSERT(m_pGO->discardCard(1, Card(Heart, 7)));
    來蓋住一張牌, 同樣地如果蓋牌的動作成功的話, UI 也會傳回 true, 你可以透過傳回值判斷你自己的程式和 UI 內的資料是不是一致

  18. 同樣地, 在 userChoose(Card) 函式中你的程式也需要判斷到底使用者是要出牌還是要蓋牌, 然後呼叫上述兩個函式之一來處理

  19. 你的程式需要判斷遊戲是否結束了 (就是所有的玩家手上都沒有牌了), 此時你需要判斷到底哪一個人贏了, 並且呼叫
    CGraphicOutput::gameOver(string gameOverMessage)
    將輸贏訊息傳入, 並且 return false 使得 UI 不再繼續呼叫 player0Move(), player1Move() 或是 player2Move()

    在你呼叫過 gameOver() 之後, UI 會顯示一個對話視窗來顯示你傳入的訊息, 同時也會把所有蓋住的牌翻開, 如此使用者可以檢查遊戲的結果

  20. 範例類別程式 Fantan930416c.rar

程式撰寫提要:

在上面這個 TestGame 類別中我們基本上只是測試一下 UI 類別提供的功能, 在這個作業中, 你需要作一個物件系統來完成整個程式, 你的類別需要負責下列工作

  1. 發牌

  2. 記錄每一個人手上有哪些牌

  3. 記錄中央牌堆上有哪些牌

  4. 使用者選擇出某一張牌之後, 程式需要判斷是否為允許出的牌, 是要接在哪一堆牌上, 還是要蓋牌

  5. 三個完全由電腦來模擬的玩家可以有不同的出牌方法, 例如手上有很多張牌時, 可以隨意出一張牌, 也可以挑一張後面還有重要牌要出的牌來出, 也可以盡量想辦法擋住別人的牌, 要蓋牌時可以隨意挑選, 也可以挑損失最小的, 或是挑讓別人損失最大的

  6. 你的程式需要判斷誰是最先出牌的一方

  7. 你的程式需要判斷遊戲是否結束了

  8. 你的程式須要判斷最後是誰贏了, 準備好結果的訊息列印出來
物件的設計圖可以如下:

重要觀念

  1. 請注意 CGraphicOutput 是一個抽象類別, 其中包含了基本的圖形輸出界面函式, 而把一些與圖形輸出界面無關的部份隱藏起來了, 你可以去觀察一下 UIClasses 裡的 CChildView 類別的定義就可以大概瞭解

  2. CActionHandler 也是一個抽象類別, 其中包含了需要實作的一些重要訊息處理函式, UI 部份的程式可以在完全不知道你的應用程式的類別狀況下撰寫, 就是依靠這個事先定義好的抽象類別, 這種界面的重用是物件導向系統中最有效率的一種重用方式

進一步增加程式的功能

原則上你想要增加什麼功能都是可以的, 記得在心得中寫出來, 這樣我才會注意到, 也才能夠幫你加一些分數, 現在程式還簡單, 你應該有辦法增加功能或是基於這個類別做一些好玩的應用。

C++ 程式設計課程 首頁

製作日期: 4/16/2004 by 丁培毅 (Pei-yih Ting)
E-mail: [email protected] TEL: 02 24622192x6615
海洋大學 工學院 資訊工程系 Lagoon