C/C++ Tutorial, Goal-Oriented Style (1) What do we try to achieve and how to make it?

目標

這是篇目標導向 (Goal-Oriended) 的 C/C++ 入門教學 ,針對的是完全沒學過 C/C++ 的初學者寫的。本章目標是讓完全不懂 C++ 的人,最後能自己寫出一個會和玩家對戰的井字遊戲 (tic-tac-toe)。一個簡單的井字遊戲需要包括以下功能

  • 輸入 (鍵入一個數字代表想要下的位置)
  • 輸出 (用文字畫出一個井字棋盤)
  • 各種判斷,包括有無贏家,九宮格是否已經全部畫滿了,或下一步要下在哪個位置。

不包括在這篇入門教學的部分

  • 網路功能的輸入和輸出
  • 圖形介面的輸出
  • 非必要的 C/C++ 語法

網路和繪圖並不包括在標準的 C++ 中,意思是像是 Linux 和 Windows 上要畫圖或是寫網路功能的程式長的都不太一樣,所以這篇文章不會討論該部分。

設定環境

以下方法是 “真正” 免費的 C/C++ 工具中,我認為安裝使用上最簡單的一種。

  • 確認使用的是 Windows 10,64bits,版本在 1607 以上。
    • 在 “設定 -> 系統 -> 關於” 可以找到相關訊息。
    • 如果版本太舊,”設定 -> 更新 -> 檢查更新” 去更新 Windows 10
  • “開始 -> Windows Powershell -> (滑鼠右鍵用系統管理者身分開啟) Windows Powershell”,在 Powershell 下,打入以下的指令安裝 WSL (Windows Subsystem for Linux)。
    • dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
  • 到 Windows Store 搜尋並安裝 Ubuntu,安裝完後點擊 Ubuntu 就會出現可以下指令的 Linux console
  • 在 Linux 下我們要安裝 C/C++ 的編譯器,並請順便安裝一個叫 make 的軟體,請在 console 執行下面三個指令,有可能會要求你的密碼。完成後請執行 gcc –version 和 make –version 確認有無問題。
    1. sudo apt update
    2. sudo apt install gcc
    3. sudo apt install make
  • 最後回到 Windows,安裝免費的文字編輯軟體 Notepad++,或是任何一種純文字 (txt) 的編輯工具,Windows 附屬應用程式的記事本很難用,不太建議。

檔案位置

在 Linux 下,可以用下面幾個指令變換與尋找檔案

  • “cd /” 跳到最上層
  • “cd /mnt” 跳到最上層的 mnt 目錄下
  • “cd mnt” 進入同一層的 mnt 子目錄下
  • “ls” 列出同一目錄下所有檔案和子目錄
  • “cd ..” 回到目錄的上一層

cd 是 change directory 的簡寫,ls 應該是 list 吧。

對 Linux 像是外接硬碟之類東西都放在 “/mnt” 下,而 Windows 的 C 槽也視為類似的東西,目錄是 “cd /mnt/c”。

作業:試著在 Windows 下的某個地方,建立並儲存一個文字檔案,然後進入 Linux 找到該檔案。

第一個 C/C++ 程式

用 Notepad++ 在 Windows 下開一個文字檔案 main.cpp,寫入以下的程式。

int main()
{
    return 0;
}

這是一個取名為 main 的 function,int 指的是該 function 需要回傳一個整數的結果,所以 function 最後有 return  0。C/C++ 語法裡,一行指令的結尾區要加個分號。在執行某個程式時,作業系統會尋找叫 main function 當初始點 (entry point)。因此,沒有 main function 的程式是無法執行的。

接下來,我們讓這個程式做點事情。

#include <iostream>

int main()
{
    std::cout << "hello" << std::endl;
    return 0;
}

std::cout 是用來輸出文字的 “物件” (物件的真正意義要到第三章才會提到),我們輸出了二個東西,一個是 “hello” 一個是 std::endl。”hello” 就是一個字串,std::endl 是換行的符號。程式要使用 std::cout 並需提前告知編譯器,所以會有 #include <iostream> 這行。

開始編譯,我們進入 Linux,進入該檔案的目錄,打入以下的指令。

  • g++ main.cpp

只要有用到 C++ 的 “功能”,就要用 g++ 來編譯,上面指令是叫 g++ 編譯 main.cpp 這個程式,因為沒有指定輸出檔名,所以 g++ 會自己產生預設的 a.out 這個執行檔。執行 a.out 時,用以下的指令。

  • ./a.out

看到結果了嗎? 畫面會出現一行字 “hello”,試著將換行符號拿掉再試一次看看有無不同。

最後,是輸入和輸出的最後一課

 

#include <iostream>
#include <string>

int main()
{
    std::string input;
    std::cout << "Text me something: "; 
    std::cin >> input;
    std::cout << "Your text is: " << input << std::endl;
    return 0;
}

上面的程式多了二個新東西,第一個是 std::string input,第二個是 std::cin。main function 內的四行做的事是

  1. 告知有個叫 input 的字串 (std::string)
  2. 輸出 “Text me something: “
  3. 讓使用者打字並存到 input 這個字串裡
  4. 輸出 “Your text is: ” 加上 input 字串,最後再換行。

儲存,執行,並玩看看這個程式。

作業:把上面的程式背起來,要熟練到再不偷看的情況下,能毫無錯誤的自行完成該程式,編譯並執行為止。

程式跟其他的技藝一樣,只有理解是不夠的,熟練度才是重點。而且程式的 Input 和 Output 是一切的基礎,如果連 IO 都需要查書看網路才寫得出來,那是很難再學習下去的了。

Function

開頭有說,在執行程式時,程式的進入點是 main function。既然有 “main” function,那當然有其它的 “sub” function (這種叫法好像只有再 VBA 看過)。接下來我們要將其他的 function 加入程式中。

自行打入以下的程式

#include <iostream>

void draw_board()
{
    std::cout << "123" << std::endl;
    std::cout << "456" << std::endl;   
    std::cout << "789" << std::endl;    
}

int main()
{
    draw_board();
    return 0;
}

我們寫了一個叫 draw_board() 的 function,不需要回傳任何結果,所以開頭是 void,裡面做的事情就是畫一個 tic-tac-toe 的板子。在 main function 中每當我們要畫板子,就用 draw_board() 呼叫即可。

但是輸出這樣的板子和實際上是不一樣的,因為 draw_board() 並不知道那些格子已經被劃過了,或是被誰劃過了,所以這個 function 除了畫一個空白的板子外,沒有其他的用途。因此我們必須要有辦法將某些訊息 “告知”  function。

自行打入下面的程式

#include <iostream>

void draw_board( int s1, int s2, int s3, 
                 int s4, int s5, int s6, 
                 int s7, int s8, int s9 )
{
    std::cout << s1 << s2 << s3 << std::endl;
    std::cout << s4 << s5 << s6 << std::endl;   
    std::cout << s7 << s8 << s9 << std::endl;    
}

int main()
{
    draw_board( 0, 0, 1,
                1, 0, 0, 
                2, 1, 2);
    return 0;
}

這次我們在 function 名字後面的括號裡加了九個 int 整數,命名為 s1 到 s9,用逗號分隔,意思是我們使用這個 function 的同時會提供九個數字,請將這九個數字輸出到畫面上。在使用時,為了美觀,我們用了些空格和換行,在 C/C++ 中,你可以自行加入自己喜歡的空格和排版來讓程式看起來比較易讀。

對井字遊戲來說,格子有三種狀態,無人勾選,玩家勾選,和電腦勾選。我們可以用數字來定義它,像是數字 0 是無人佔領的格子,數字 1 是玩家佔領的格子,數字 2 是電腦佔領的格子,這樣 draw_board 就有其意義了。可是井字遊戲是用圈圈和叉叉來表示的,寫個 0 1 2 沒人看得懂啊。所以我們就要引入判斷式 if-else 來達到這個功能了。

自行打入下面的程式

#include <iostream>

void draw_slot( int slot )
{
    if ( slot == 0 )
    {
        std::cout << ".";
    }
    else if ( slot == 1 )
    {
        std::cout << "O";
    }
    else if ( slot == 2 )
    {
        std::cout << "X";
    }
    else
    {
        // error occurred
    }
}

void draw_board( int s1, int s2, int s3, 
                 int s4, int s5, int s6, 
                 int s7, int s8, int s9 )
{
    draw_slot( s1 );
    draw_slot( s2 );
    draw_slot( s3 );
    std::cout << std::endl;
    
    draw_slot( s4 );
    draw_slot( s5 );
    draw_slot( s6 );
    std::cout << std::endl;
    
    draw_slot( s7 );
    draw_slot( s8 );
    draw_slot( s9 );
    std::cout << std::endl;
}

int main()
{
    draw_board( 0, 0, 1,
                1, 0, 0, 
                2, 1, 2);
    return 0;
}

程式看起來很長,但其實就是多了一個 draw_slot( int slot ) 的 function,裡面的邏輯是:

  1. 如果 slot 等於 0,劃出一個點
  2. 如果 slot 不等於 0,但等於 1,劃出一個圈
  3. 如果 slot 不等於 0 也不懂於 1,但等於 2,劃出一個叉
  4. 如果 slot 不等於 0 1 2,錯誤發生,程式中的 “// error occurred” 是給人看得附註,不是程式的一部份,真實的程式需要處裡這種 bug。

作業:把井字遊戲的格子畫好看一點,像是下面的例子。


O | X | .
---------
X | O | .
---------
. | . | X

接下來我們試試看多重的判斷式,請參考下面未完成的 get_winner() function。

int get_winner( int s1, int s2, int s3, 
                int s4, int s5, int s6, 
                int s7, int s8, int s9 )
{
    if (( s1 == 1 && s2 == 1 && s3 == 1 ) || 
        ( s4 == 1 && s5 == 1 && s6 == 1 ) || 
        ( s7 == 1 && s8 == 1 && s9 == 1 ))
    {
        return 1;
    }
    
    return 0;
}

int main()
{
    return 0;
}

我們想要一個 function 判斷有無勝利者,如果玩家勝利,回傳 1,如果電腦勝利,回傳 2,不然回傳 3。第一段判斷式是這樣寫的:

如果 s1 / s2 / s3 這三個格子都是 1,或是 s4 / s5 / s6 這三個格子都是 1 ,或是 s7 / s8 / s9 這三個格子都是 1,則勝利者是玩家,回傳 1 。

  • 括號內的判斷會先完成
  • 二個 = 是二邊是否相等的意思
  • 二個 & 是 AND 的意思
  • 二個 | 是 OR 的意思

但是在井字遊戲中,還有直線和橫線的贏法,而且還要判斷電腦贏的部分,所以是未完成。

作業:完成這個 get_winner() function

重複同樣的動作直到出現贏家,或是沒可用格子為止

在遊戲過程中,遊戲會一直持續到

  • 玩家贏了
  • 電腦贏了
  • 沒格子了

這類一直持續到某個情況發生為止的判斷式一般是用 while-loop,請自行打入下面的程式。


#include <iostream>

// return 1 if valid, else 0
int validate_move( int move, int s1, int s2, int s3, int s4, int s5, int s6, int s7, int s8, int s9 )
{
    return 0;
}

int main()
{
    int result;
    int player_move;
    int winner = 0;
    int s1 = 0, s2 = 0, s3 = 0;
    int s4 = 0, s5 = 0, s6 = 0;
    int s7 = 0, s8 = 0, s9 = 0;
    
    while ( winner == 0 )
    {
        std::cout << "What's your move? (1~9): " << std::endl; std::cin >> player_move;
        
        // check if such move is valid
        result = validate_move( player_move, s1, s2, s3, s4, s5, s6, s7, s8, s9 );
        
        if ( result == 0 )
        {
            std::cout << "Bad move, try again." << std::endl;
            continue;
        }
        
        // TODO: not complete
    }
    
    return 0;
}

 

首先,裡面多了一個 validate_move(),依照說明就是如果使用者輸入的格子號碼已經有人下了,就回傳 0,不然回傳 1。所以如果是 0 的時候,我們用 continue 從中間跳回 while-loop 的開頭重來一次。而 while-loop 會重複一直做到 winner 等於 1 才會停。

  • 在執行時。發現程式永遠無法結束,就按 CTRL+C 強制退出。

 完成你的第一個 C/C++ 遊戲程式

我們已經將寫一個井字遊戲所需要的所有東西都描述並運用過了,接下來該真的完成它了。程式的演算法如下:

  1. 畫棋盤,告知玩家 1到9的位置。
  2. 等待玩家輸入 1到9的數字。(while-loop)
  3. 確認玩家輸入的數字是否是空格,如果不是,回到 2。
    • validate_move()
  4. 將該空格代表的變數 (s1 ~ s9 ) 改為 1。
  5. 確認玩家是否贏了,如果是,告知並跳出 (break while-loop) 到步驟 10。
    • get_winner()
  6. 判斷是否還有空格,如果沒有,告知並跳出 (break while-loop) 到步驟 10。
  7. 判斷電腦要下哪個位置。
    • get_aimove();
  8. 確認電腦是否贏了,如果是,告知並跳出 (break while-loop) 到 步驟10。
    • get_winner()
  9. 判斷是否還有空格,如果沒有,告知並跳出 (break while-loop) 到步驟 10。
  10. 程式結束

 


#include <iostream>

int main()
{
    int result;
    int player_move, ai_move;
    int winner = 0;
    int s1 = 0, s2 = 0, s3 = 0;
    int s4 = 0, s5 = 0, s6 = 0;
    int s7 = 0, s8 = 0, s9 = 0;
    
    // draw the board
    draw_board( s1, s2, s3, s4, s5, s6, s7, s8, s9 );
        
    while ( winner == 0 )
    {
        // get player's move
        std::cout << "What's your move? (1~9): " << std::endl; std::cin >> player_move;
        
        // check if such move is valid
        result = validate_move( player_move, s1, s2, s3, s4, s5, s6, s7, s8, s9 );
        
        if ( result == 0 )
        {
            std::cout << "Bad move, try again." << std::endl;
            continue;
        }
        
        // set player's move
        if ( player_move == 1 ) { s1 = 1; }
        else if ( player_move == 2 ) { s2 = 1; }
        else if ( player_move == 3 ) { s3 = 1; }
        else if ( player_move == 4 ) { s4 = 1; }
        else if ( player_move == 5 ) { s5 = 1; }
        else if ( player_move == 6 ) { s6 = 1; }
        else if ( player_move == 7 ) { s7 = 1; }
        else if ( player_move == 8 ) { s8 = 1; }
        else if ( player_move == 9 ) { s9 = 1; }
        else { 
            // error handling
            std::cout << "Bad move, try again." << std::endl;
            continue;            
        }
        
        // check if player wins
        winner = get_winner( s1, s2, s3, s4, s5, s6, s7, s8, s9 );
        
        if ( winner == 1 ) 
        {
            std::cout << "Player wins!" << std::endl;
            break;
        }
        
        // get computer's move
        ai_move = get_aimove( s1, s2, s3, s4, s5, s6, s7, s8, s9 );

        // set computer's move
        if ( ai_move == 1 ) { s1 = 2; }
        else if ( ai_move == 2 ) { s2 = 2; }
        else if ( ai_move == 3 ) { s3 = 2; }
        else if ( ai_move == 4 ) { s4 = 2; }
        else if ( ai_move == 5 ) { s5 = 2; }
        else if ( ai_move == 6 ) { s6 = 2; }
        else if ( ai_move == 7 ) { s7 = 2; }
        else if ( ai_move == 8 ) { s8 = 2; }
        else if ( ai_move == 9 ) { s9 = 2; }
        
        // check if computer wins
        winner = get_winner( s1, s2, s3, s4, s5, s6, s7, s8, s9 );
        
        if ( winner == 2 ) 
        {
            std::cout << "AI wins!" << std::endl;
            break;
        }
        
        // check if no more slot
        result = has_slot( s1, s2, s3, s4, s5, s6, s7, s8, s9 );
        
        if ( result == 0 )
        {
            std::cout << "Draw!" << std::endl;
            break;
        }
        
        // draw the board
        draw_board( s1, s2, s3, s4, s5, s6, s7, s8, s9 );        
    }
    
    return 0;
}

作業:完成這個程式!

也許 get_aimove() 會是個挑戰,電腦是可以計算出所有可能的走法並取得最佳解,但演算法的部分不是這篇要提的東西,所以盡量寫吧! 🙂