溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么用C++編寫一個井字游戲

發(fā)布時間:2021-08-24 11:54:17 來源:億速云 閱讀:158 作者:chen 欄目:編程語言

這篇文章主要介紹“怎么用C++編寫一個井字游戲”,在日常操作中,相信很多人在怎么用C++編寫一個井字游戲問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”怎么用C++編寫一個井字游戲”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

游戲介紹

如果你沒有玩過井字游戲或者并不熟悉這個游戲,下面是來自維基百科的描述.

  井字游戲 (或者"圈圈和叉叉",Xs and Os) 是一個兩人的紙筆游戲,兩個人輪流在3X3的網(wǎng)格內(nèi)畫圈和叉. 當(dāng)一名玩家放置的標(biāo)志在水平,垂直或者對角線上成一條線即獲得勝利.

怎么用C++編寫一個井字游戲

這個游戲也可以人機(jī)對戰(zhàn),先手不固定.

創(chuàng)建這個程序的時候有2個關(guān)鍵的東西:程序的邏輯和程序的UI界面. 有許多在windows中創(chuàng)建用戶UI的方法, 包括 Win32  API, MFC, ATL, GDI+, DirectX, etc. 在這篇文章中,我將展示使用多種技術(shù)來實現(xiàn)同一個程序邏輯.  我們將新建2個應(yīng)用, 一個使用 Win32 API 另一個使用 C++/CX.

游戲邏輯

如果一個玩家在網(wǎng)格上放下一個標(biāo)記時,遵循幾個簡單的規(guī)則,那他就可以玩一個完美的游戲(意味著贏或者平局)。在Wikipedia上寫有這些規(guī)則,在里面你也可以找到先手玩家的最優(yōu)策略。

怎么用C++編寫一個井字游戲

在xkcd drawing上 有先手和后手玩家的最優(yōu)策略。盡管有幾個錯誤(在幾種情況下沒有走必勝的步驟,至少在一個情況下丟失了一個X標(biāo)記),我將使用這個版本作為游戲策略(修復(fù) 了那些我能找到的錯誤)。記住電腦總是玩一個完美的游戲。如果你實現(xiàn)了這樣一個游戲,你可能也想讓用戶贏,這種情況下你需要一個不同的方法。當(dāng)對本文的目 的,這個策略應(yīng)該足夠了。

提出的第一個問題是在C++程序中用什么數(shù)據(jù)結(jié)構(gòu)來表示圖像的模型。這可以有不同的選擇,比如樹、圖、數(shù)組或者位字段(如果真有人對內(nèi)存消耗很在意)。網(wǎng) 格有9個單元,我選擇的最簡單的使用對每個單元使用一個包含9個整數(shù)的數(shù)組:0表示空的單元,1表示單元被標(biāo)記為X,2表示單元被標(biāo)記為O。讓我們看下圖 以及它將被如何編碼。

怎么用C++編寫一個井字游戲

這幅圖可以這么理解:

  • 在單元(0,0)放X。網(wǎng)格可以編碼為:1,0,0,0,0,0,0,0,0

  • 如果對手在單元(0,1)放置O,那么在單元(1,1)放置X?,F(xiàn)在網(wǎng)格編碼為:1,2,0,0,1,0,0,0,0

  • 如果對手在單元(0,2)放置O,那么在單元(2,2)放置X?,F(xiàn)在網(wǎng)格編碼為:1,2,2,0,1,0,0,0,1

  • 如果對手在單元(2,2)放置O,那么在單元(2,0)放置X。現(xiàn)在網(wǎng)格編碼為:1,2,0,0,1,0,1,0,2。這時,無論對手怎么做,X都將贏得比賽。

  • 如果對手在單元(0,2)放置O,那么在單元(1,0)放置X?,F(xiàn)在網(wǎng)格編碼為:1,2,2,1,1,0,1,0,2。這表示的是一個贏得比賽的一步。

記住這個我們就可以開始在程序中對其編碼了。我們將使用一個std::array來表示一個9格板。這是個固定大小的容器,在編譯時就已知的大小,在連續(xù)的內(nèi)存區(qū)域存儲元素。為了避免一遍又一遍的使用相同數(shù)組類型,我將定義一個別名來簡化。

#include <array>  typedef std::array<char, 9> tictactoe_status;

上面描述的最優(yōu)策略用這樣的數(shù)組隊列(另一個數(shù)組)來表示。

tictactoe_status const strategy_x[] =  {    {1,0,0,0,0,0,0,0,0},    {1,2,0,0,1,0,0,0,0},    {1,2,2,0,1,0,0,0,1},    {1,2,0,2,1,0,0,0,1},    // ... };  tictactoe_status const strategy_o[] =  {    {2,0,0,0,1,0,0,0,0},    {2,2,1,0,1,0,0,0,0},    {2,2,1,2,1,0,1,0,0},    {2,2,1,0,1,2,1,0,0},    // ... };

strategy_x是先手玩家的最優(yōu)策略,strategy_o是后手玩家的最優(yōu)策略。如果你看了文中的源代碼,你將注意到這兩個數(shù)組的真實定義和我前面展示的不同。

tictactoe_status const strategy_x[] =  { #include "strategy_x.h" };  tictactoe_status const strategy_o[] =  { #include "strategy_o.h" };

這是個小技巧,我的理由是,它允許我們把真實的很長的數(shù)組內(nèi)容放在分開的文件中(這些文件的擴(kuò)展性不重要,它可以不僅僅是C++頭文件,也可以是其他任何 文件),保證源碼文件和定義簡單干凈。strategy_x.h和strategy_o.h文件在編譯的預(yù)處理階段就被插入到源碼文件中,如同正常的頭文 件一樣。下面是strategy_x.h文件的片斷。

// http://imgs.xkcd.com/comics/tic_tac_toe_large.png // similar version on http://upload.wikimedia.org/wikipedia/commons/d/de/Tictactoe-X.svg // 1 = X, 2 = O, 0 = unoccupied  1,0,0,0,0,0,0,0,0,  1,2,0,0,1,0,0,0,0, 1,2,2,0,1,0,0,0,1, 1,2,0,2,1,0,0,0,1, 1,2,0,0,1,2,0,0,1,

你應(yīng)該注意到,如果你使用支持C++11的編譯器,你可以使用一個std::vector而不是C類型的數(shù)組。Visual Studio 2012不支持這么做,但在Visual Studio 2013中支持。

std::vector<tictactoe_status> strategy_o =  {    {2, 0, 0, 0, 1, 0, 0, 0, 0},    {2, 2, 1, 0, 1, 0, 0, 0, 0},    {2, 2, 1, 2, 1, 0, 1, 0, 0},    {2, 2, 1, 0, 1, 2, 1, 0, 0},    {2, 2, 1, 1, 1, 0, 2, 0, 0}, };

為了定義這些數(shù)字表示的對應(yīng)玩家,我定義了一個叫做tictactoe_player的枚舉類型變量。

enum class tictactoe_player : char {    none = 0,    computer = 1,    user = 2, };

游戲的邏輯部分將會在被稱之為tictactoe_game 的類中實現(xiàn)。最基本的,這個 class 應(yīng)該有下面的狀態(tài):

  • 一個布爾值用來表示游戲是否開始了,命名為 started 。

  • 游戲的當(dāng)前狀態(tài)(網(wǎng)格上的標(biāo)記), 命名為 status 。

  • 根據(jù)當(dāng)前的狀態(tài)得到的之后可以進(jìn)行的下法的集合,命名為strateg

class tictactoe_game {    bool started;    tictactoe_status status;    std::set<tictactoe_status> strategy;        // ... };

在游戲的過程中,我們需要知道游戲是否開始了、結(jié)束了,如果結(jié)束了,需要判定是否有哪個玩家贏了或者最終兩個人打平。為此,tictactoe_game類提供了三個方法:

  • is_started()來表示游戲是否開始了

  • is_victory()來檢查是否有哪位玩家在游戲中獲勝

  • is_finished()來檢查游戲是否結(jié)束。當(dāng)其中某位玩家在游戲中獲勝或者當(dāng)網(wǎng)格被填滿玩家不能再下任何的棋子的時候,游戲結(jié)束。

bool is_started() const {return started;} bool is_victory(tictactoe_player const player) const {return is_winning(status, player);} bool is_finished() const  {

對于方法is_victory()和is_finished(),實際上是依賴于兩個私有的方法,is_full(),  用來表示網(wǎng)格是否被填滿并且不能再放下任何的棋子,以及方法is_winning,  用來表示在該網(wǎng)格上是否有某玩家勝出。它們的實現(xiàn)將會很容易被讀懂。is_full 通過計算網(wǎng)格中空的(在表示網(wǎng)格的數(shù)組中值為0)格子的數(shù)量,如果沒有這樣的格子那么將返回true。is_winning將會檢查這些連線,網(wǎng)格的行、列、以及對角線,依此來查看是否有哪位玩家已經(jīng)獲勝。

bool is_winning(tictactoe_status const & status, tictactoe_player const player) const {    auto mark = static_cast<char>(player);    return        (status[0] == mark && status[1] == mark && status[2] == mark) ||       (status[3] == mark && status[4] == mark && status[5] == mark) ||       (status[6] == mark && status[7] == mark && status[8] == mark) ||       (status[0] == mark && status[4] == mark && status[8] == mark) ||       (status[2] == mark && status[4] == mark && status[6] == mark) ||       (status[0] == mark && status[3] == mark && status[6] == mark) ||       (status[1] == mark && status[4] == mark && status[7] == mark) ||       (status[2] == mark && status[5] == mark && status[8] == mark); }  bool is_full(tictactoe_status const & status) const  {    return 0 == std::count_if(std::begin(status), std::end(status), [](int const mark){return mark == 0;}); }

當(dāng)一個玩家獲勝的時候,我們想給他所連成的線(行、列、或者對角線)上畫一條醒目的線段。因此首先我們得知道那條線使得玩家獲勝。我們使用了方法get_winning_line()來返回一對  tictactoe_cell,用來表示線段的兩端。它的實現(xiàn)和is_winning很相似,它檢查行、列和對角線上的狀態(tài)。它可能會看起來有點冗長,但是我相信這個方法比使用循環(huán)來遍歷行、列、對角線更加簡單。

struct tictactoe_cell {    int row;    int col;     tictactoe_cell(int r = INT_MAX, int c = INT_MAX):row(r), col(c)    {}     bool is_valid() const {return row != INT_MAX && col != INT_MAX;} };  std::pair<tictactoe_cell, tictactoe_cell> const get_winning_line() const {    auto mark = static_cast<char>(tictactoe_player::none);    if(is_victory(tictactoe_player::computer))       mark = static_cast<char>(tictactoe_player::computer);    else if(is_victory(tictactoe_player::user))       mark = static_cast<char>(tictactoe_player::user);     if(mark != 0)    {       if(status[0] == mark && status[1] == mark && status[2] == mark)           return std::make_pair(tictactoe_cell(0,0), tictactoe_cell(0,2));       if(status[3] == mark && status[4] == mark && status[5] == mark)          return std::make_pair(tictactoe_cell(1,0), tictactoe_cell(1,2));       if(status[6] == mark && status[7] == mark && status[8] == mark)          return std::make_pair(tictactoe_cell(2,0), tictactoe_cell(2,2));       if(status[0] == mark && status[4] == mark && status[8] == mark)          return std::make_pair(tictactoe_cell(0,0), tictactoe_cell(2,2));       if(status[2] == mark && status[4] == mark && status[6] == mark)          return std::make_pair(tictactoe_cell(0,2), tictactoe_cell(2,0));       if(status[0] == mark && status[3] == mark && status[6] == mark)          return std::make_pair(tictactoe_cell(0,0), tictactoe_cell(2,0));       if(status[1] == mark && status[4] == mark && status[7] == mark)          return std::make_pair(tictactoe_cell(0,1), tictactoe_cell(2,1));       if(status[2] == mark && status[5] == mark && status[8] == mark)          return std::make_pair(tictactoe_cell(0,2), tictactoe_cell(2,2));    }     return std::make_pair(tictactoe_cell(), tictactoe_cell()); }

現(xiàn)在我們只剩下添加開始游戲功能和為網(wǎng)格放上棋子功能(電腦和玩家兩者).

對于開始游戲,我們需要知道,由誰開始下第一個棋子,因此我們可以采取比較合適的策略(兩種方式都需要提供,電腦先手或者玩家先手都要被支持)。同時,我 們也需要重置表示網(wǎng)格的數(shù)組。方法start()對開始新游戲進(jìn)行初始化??梢韵碌钠宓牟呗缘募媳辉僖淮蔚某跏蓟? 從stategy_x  或者strategy_o進(jìn)行拷貝。從下面的代碼可以注意到,strategy是一個std::set,  并且strategy_x或者strategy_o都是有重復(fù)單元的數(shù)組,因為在tictoctoe表里面的一些位置是重復(fù)的。這個std::set  是一個只包含唯一值的容器并且它保證了唯一的可能的位置(例如對于strategy_o來說,有一半是重復(fù)的)。<algorithm>  中的std::copy算法在這里被用來進(jìn)行數(shù)據(jù)單元的拷貝,將當(dāng)前的內(nèi)容拷貝到std::set中,并且使用方法assign()來將 std::array的所有的元素重置為0。

void start(tictactoe_player const player) {    strategy.clear();    if(player == tictactoe_player::computer)       std::copy(std::begin(strategy_x), std::end(strategy_x),                  std::inserter(strategy, std::begin(strategy)));    else if(player == tictactoe_player::user)       std::copy(std::begin(strategy_o), std::end(strategy_o),                  std::inserter(strategy, std::begin(strategy)));                     status.assign(0);        started = true; }

當(dāng)玩家走一步時,我們需要做的是確保選擇的網(wǎng)格是空的,并放置合適的標(biāo)記。move()方法的接收參數(shù)是網(wǎng)格的坐標(biāo)、玩家的記號,如果這一步有效時返回真,否則返回假。

bool move(tictactoe_cell const cell, tictactoe_player const player) {    if(status[cell.row*3 + cell.col] == 0)    {       status[cell.row*3 + cell.col] = static_cast<char>(player);              if(is_victory(player))       {          started = false;       }              return true;    }     return false; }

電腦走一步時需要更多的工作,因為我們需要找到電腦應(yīng)該走的最好的下一步。重載的move()方法在可能的步驟(策略)集合中查詢,然后從中選擇最佳的一步。在走完這步后,會檢查電腦是否贏得這場游戲,如果是的話標(biāo)記游戲結(jié)束。這個方法返回電腦走下一步的位置。

tictactoe_cell move(tictactoe_player const player) {    tictactoe_cell cell;     strategy = lookup_strategy();     if(!strategy.empty())    {       auto newstatus = lookup_move();        for(int i = 0; i < 9; ++i)       {          if(status[i] == 0 && newstatus[i]==static_cast<char>(player))          {             cell.row = i/3;             cell.col = i%3;             break;          }       }        status = newstatus;        if(is_victory(player))       {          started = false;       }    }     return cell; }

lookup_strategy()方法在當(dāng)前可能的移動位置中迭代,來找到從當(dāng)前位置往哪里移動是可行的。它利用了這樣的一種事實,空的網(wǎng)格以0來表 示,任何已經(jīng)填過的網(wǎng)格,不是用1就是用2表示,而這兩個值都大于0。一個網(wǎng)格的值只能從0變?yōu)?或者2。不可能從1變?yōu)?或從2變?yōu)?。

當(dāng)游戲開始時的網(wǎng)格編碼為0,0,0,0,0,0,0,0,0來表示并且當(dāng)前情況下任何的走法都是可能的。這也是為什么我們要在thestart()方法 里把整個步數(shù)都拷貝出來的原因。一旦玩家走了一步,那能走的步數(shù)的set便會減少。舉個例子,玩家在第一個格子里走了一步。此時網(wǎng)格編碼為 1,0,0,0,0,0,0,0,0。這時在數(shù)組的第一個位置不可能再有0或者2的走法因此需要被過濾掉。

std::set<tictactoe_status> tictactoe_game::lookup_strategy() const {    std::set<tictactoe_status> nextsubstrategy;     for(auto const & s : strategy)    {       bool match = true;       for(int i = 0; i < 9 && match; ++i)       {          if(s[i] < status[i])             match = false;       }        if(match)       {          nextsubstrategy.insert(s);       }    }     return nextsubstrategy; }

在選擇下一步時我們需要確保我們選擇的走法必須與當(dāng)前的標(biāo)記不同,如果當(dāng)前的狀態(tài)是1,2,0,0,0,0,0,0,0而我們現(xiàn)在要為玩家1選擇走法那么 我們可以從余下的7個數(shù)組單元中選擇一個,可以是:1,2,1,0,0,0,0,0,0或1,2,0,1,0,0,0,0,0...  或1,2,0,0,0,0,0,0,1。然而我們需要選擇最優(yōu)的走法而不是僅僅只隨便走一步,通常最優(yōu)的走法也是贏得比賽的關(guān)鍵。因此我們需要找一步能使 我們走向勝利,如果沒有這樣的一步,那就隨便走吧。

tictactoe_status tictactoe_game::lookup_move() const {    tictactoe_status newbest = {0};    for(auto const & s : strategy)    {       int diff = 0;       for(int i = 0; i < 9; ++i)       {          if(s[i] > status[i])             diff++;       }        if(diff == 1)       {          newbest = s;          if(is_winning(newbest, tictactoe_player::computer))          {             break;          }       }    }     assert(newbest != empty_board);     return newbest; }

做完了這一步,我們的游戲的邏輯部分就完成了。更多細(xì)節(jié)請閱讀game.hgame.cpp中的代碼

一個用Win32 API實現(xiàn)的游戲

我將用Win32 API做用戶界面來創(chuàng)建第一個應(yīng)用程序。如果你不是很熟悉Win32  編程那么現(xiàn)在已經(jīng)有大量的資源你可以利用學(xué)習(xí)。為了使大家理解我們?nèi)绾蝿?chuàng)建一個最終的應(yīng)用,我將只講述一些必要的方面。另外,我不會把每一行代碼都展現(xiàn)并 解釋給大家,但是你可以通過下載這些代碼來閱讀瀏覽它。

一個最基本的Win32應(yīng)用需要的一些內(nèi)容:

  • 一個入口點,通常來說是WinMain,而不是main。它需要一些參數(shù)例如當(dāng)前應(yīng)用實例的句柄,命令行和指示窗口如何展示的標(biāo)志。

  • 一個窗口類,代表了創(chuàng)建一個窗口的模板。一個窗口類包含了一個為系統(tǒng)所用的屬性集合,例如類名,class style(不同于窗口的風(fēng)格),圖標(biāo),菜單,背景刷,窗口的指針等。一個窗口類是進(jìn)程專用的并且必須要注冊到系統(tǒng)優(yōu)先級中來創(chuàng)建一個窗口。使用RegisterClassEx來注冊一個窗口類。

  • 一個主窗口,基于一個窗口類來創(chuàng)建。使用CreateWindowEx可以創(chuàng)建一個窗口。

  • 一個窗口過程函數(shù),它是一個處理所有基于窗口類創(chuàng)建的窗口的消息的方法。一個窗口過程函數(shù)與窗口相聯(lián),但是它不是窗口。

  • 一個消息循環(huán)。一個窗口通過兩種方式來接受消息:通過SendMessage,直接調(diào)用窗口過程函數(shù)直到窗口過程函數(shù)處理完消息之后才返回,或者通過PostMessage (或 PostThreadMessage)把一個消息投送到創(chuàng)建窗口的線程的消息隊列中并且不用等待線程處理直接返回。因此線程必須一直運行一個從消息隊列接收消息和把消息發(fā)送給窗口過程函數(shù)的循環(huán)

你可以在 MSDN 中找到關(guān)于Win 32 應(yīng)用程序如何注冊窗口類、創(chuàng)建一個窗口、運行消息循環(huán)的例子。一個Win32的應(yīng)用程序看起來是這樣的:

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {    WNDCLASS wc;     // set the window class attributes    // including pointer to a window procedure        if (!::RegisterClass(&wc))       return FALSE;           HWND wnd = ::CreateWindowEx(...);    if(!wnd)       return FALSE;           ::ShowWindow(wnd, nCmdShow);         MSG msg;    while(::GetMessage(&msg, nullptr, 0, 0))    {       ::TranslateMessage(&msg);       ::DispatchMessage(&msg);    }     return msg.wParam;    }

當(dāng)然,這還不夠,我們還需要一個窗口過程函數(shù)來處理發(fā)送給窗口的消息,比如PAINT消息,DESTORY 消息,菜單消息和其它的一些必要的消息。一個窗口過程函數(shù)看起來是這樣的:

LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {    switch(message)    {    case WM_PAINT:       {          PAINTSTRUCT ps;          HDC dc = ::BeginPaint(hWnd, &ps);          // paint          ::EndPaint(hWnd, &ps);       }       break;     case WM_DESTROY:       ::PostQuitMessage(0);       return 0;     case WM_COMMAND:       {          ...       }       break;    }     return ::DefWindowProc(hWnd, message, wParam, lParam); }

我更喜歡寫面向?qū)ο蟮拇a,不喜歡面向過程,所以我用幾個類封裝了窗口類、窗口和設(shè)備描述表。你可以在附件的代碼framework.h framework.cpp  找到這些類的實現(xiàn)(它們非常小巧 )。

  • WindowClass類是對窗口類相關(guān)資源初始化的封裝,在構(gòu)造函數(shù)中,它初始化了WNDCLASSEX 的結(jié)構(gòu)并且調(diào)用 RegisterClassEx 方法。在析構(gòu)函數(shù)中,它通過調(diào)用 UnregisterClass 移除窗口的注冊。

  • Window類是通過對HWND封裝一些諸如Create,ShowWindow  和Invalidate的函數(shù)(它們的名字已經(jīng)告訴了你他們是做什么的)。它還有幾個虛成員代表消息句柄,它們會被窗口過程調(diào) 用 (OnPaint,OnMenuItemClicked,OnLeftButtonDown) 。這個window類將會被繼承來并提供具體的實現(xiàn)。

  • DeviceContex類是對設(shè)備描述表(HDC)的封裝。在構(gòu)造函數(shù)中它調(diào)用 BeginPaint 函數(shù)并且在析構(gòu)函數(shù)中調(diào)用  EndPaint 函數(shù)。

這個游戲的主要窗口是TicTacToeWindow類,它是從Window類繼承而來,它重載了虛擬方法來處理消息,該類的聲明是這樣的:

class TicTacToeWindow : public Window {    HANDLE hBmp0;    HANDLE hBmpX;    BITMAP bmp0;    BITMAP bmpX;     tictactoe_game game;     void DrawBackground(HDC dc, RECT rc);    void DrawGrid(HDC dc, RECT rc);    void DrawMarks(HDC dc, RECT rc);    void DrawCut(HDC dc, RECT rc);     virtual void OnPaint(DeviceContext* dc) override;    virtual void OnLeftButtonUp(int x, int y, WPARAM params) override;    virtual void OnMenuItemClicked(int menuId) override;  public:    TicTacToeWindow();    virtual ~TicTacToeWindow() override; };

MethodOnPaint()函數(shù)用來繪制窗口,它用來繪制窗口背景,網(wǎng)格線,填充的單元格(如果有的話),如果在游戲結(jié)束,玩家贏了,一條紅線在獲勝 行,列或?qū)蔷€的  標(biāo)記。為了避免閃爍,我們使用了雙緩沖技術(shù):創(chuàng)建一個內(nèi)存設(shè)備文本(通過調(diào)用toBeginPaint函數(shù)準(zhǔn)備窗口的設(shè)備文本來匹配),一個內(nèi)存中的位圖 匹配內(nèi)存設(shè)備文本,繪制該位圖,然后用窗口設(shè)備文本來復(fù)制內(nèi)存設(shè)備文本。

void TicTacToeWindow::OnPaint(DeviceContext* dc) {    RECT rcClient;    ::GetClientRect(hWnd, &rcClient);     auto memdc = ::CreateCompatibleDC(*dc);    auto membmp = ::CreateCompatibleBitmap(*dc, rcClient.right - rcClient.left, rcClient.bottom-rcClient.top);    auto bmpOld = ::SelectObject(memdc, membmp);        DrawBackground(memdc, rcClient);     DrawGrid(memdc, rcClient);     DrawMarks(memdc, rcClient);     DrawCut(memdc, rcClient);     ::BitBlt(*dc,        rcClient.left,        rcClient.top,        rcClient.right - rcClient.left,        rcClient.bottom-rcClient.top,       memdc,        0,        0,        SRCCOPY);     ::SelectObject(memdc, bmpOld);    ::DeleteObject(membmp);    ::DeleteDC(memdc); }

怎么用C++編寫一個井字游戲

我不會在這里列出DrawBackground,DrawGridand和 DrawMarksfunctions的內(nèi)容。他們不是很復(fù)雜,你可以閱讀源代碼。DrawMarksfunction使用兩個位圖,ttt0.bmp和tttx.bmp,繪制網(wǎng)格的痕跡。

怎么用C++編寫一個井字游戲怎么用C++編寫一個井字游戲

我將只顯示如何在獲勝行,列或?qū)蔷€繪制紅線。首先,我們要檢查游戲是否結(jié)束,如果結(jié)束那么檢索獲勝線。如果兩端都有效,然后計算該兩個小區(qū)的中心,創(chuàng)建和選擇一個畫筆(實心,15像素寬的紅色線)并且繪制兩個小區(qū)的中間之間的線。

void TicTacToeWindow::DrawCut(HDC dc, RECT rc) {    if(game.is_finished())    {       auto streak = game.get_winning_line();        if(streak.first.is_valid() && streak.second.is_valid())       {          int cellw = (rc.right - rc.left) / 3;          int cellh = (rc.bottom - rc.top) / 3;           auto penLine = ::CreatePen(PS_SOLID, 15, COLORREF(0x2222ff));          auto penOld = ::SelectObject(dc, static_cast<HPEN>(penLine));           ::MoveToEx(             dc,              rc.left + streak.first.col * cellw + cellw/2,              rc.top + streak.first.row * cellh + cellh/2,             nullptr);           ::LineTo(dc,             rc.left + streak.second.col * cellw + cellw/2,             rc.top + streak.second.row * cellh + cellh/2);           ::SelectObject(dc, penOld);       }    } }

主窗口有三個項目菜單,  ID_GAME_STARTUSER在用戶先移動時啟動一個游戲,  ID_GAME_STARTCOMPUTER在當(dāng)電腦先移動時啟動一個游戲,  ID_GAME_EXIT用來關(guān)閉應(yīng)用。當(dāng)用戶點擊兩個啟動中的任何一個,我們就必須開始一個游戲任務(wù)。如果電腦先移動,那么我們應(yīng)該是否移動,并且,在所有情況中,都要重新繪制窗口。

void TicTacToeWindow::OnMenuItemClicked(int menuId) {    switch(menuId)    {    case ID_GAME_EXIT:       ::PostMessage(hWnd, WM_CLOSE, 0, 0);       break;     case ID_GAME_STARTUSER:       game.start(tictactoe_player::user);       Invalidate(FALSE);       break;     case ID_GAME_STARTCOMPUTER:       game.start(tictactoe_player::computer);       game.move(tictactoe_player::computer);       Invalidate(FALSE);       break;    } }

現(xiàn)在只剩下一件事了,就是留意在我們的窗口中處理用戶單擊鼠標(biāo)的行為。當(dāng)用戶在我們的窗口客戶區(qū)內(nèi)按下鼠標(biāo)時,我們要去檢查是鼠標(biāo)按下的地方是在哪一個網(wǎng)格內(nèi),如果這個網(wǎng)格是空的,那我們就把用戶的標(biāo)記填充上去。之后,如果游戲沒有結(jié)束,就讓電腦進(jìn)行下一步的移動。

void TicTacToeWindow::OnLeftButtonUp(int x, int y, WPARAM params) {    if(game.is_started() && !game.is_finished())    {       RECT rcClient;       ::GetClientRect(hWnd, &rcClient);        int cellw = (rcClient.right - rcClient.left) / 3;       int cellh = (rcClient.bottom - rcClient.top) / 3;        int col = x / cellw;       int row = y / cellh;        if(game.move(tictactoe_cell(row, col), tictactoe_player::user))       {          if(!game.is_finished())             game.move(tictactoe_player::computer);           Invalidate(FALSE);       }    } }

最后,我們需要實現(xiàn)WinMain函數(shù),這是整個程序的入口點。下面的代碼與這部分開始我給出的代碼非常相似,不同的之處是它使用了我對窗口和窗口類進(jìn)行封裝的一些類。

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {    WindowClass wndcls(hInstance, L"TicTacToeWindowClass", MAKEINTRESOURCE(IDR_MENU_TTT), CallWinProc);        TicTacToeWindow wnd;    if(wnd.Create(       wndcls.Name(),        L"Fun C++: TicTacToe",        WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,        CW_USEDEFAULT,        CW_USEDEFAULT,        300,        300,        hInstance))    {       wnd.ShowWindow(nCmdShow);        MSG msg;       while(::GetMessage(&msg, nullptr, 0, 0))       {          ::TranslateMessage(&msg);          ::DispatchMessage(&msg);       }        return msg.wParam;    }     return 0; }

雖然我覺得我放在這里的代碼是相當(dāng)?shù)亩绦【?,但如果你不熟悉Win32 API程序設(shè)計,你仍然可能會覺得這些代碼有點復(fù)雜。無論如何,你都一定要清楚的了解對象的初始化、如何創(chuàng)建一個窗口、如何處理窗口消息等。但愿你會覺得下一部分更有趣。

一個Windows Runtime的游戲app

Windows Runtime是Windows 8引入的一個新的Windows運行時引擎.  它依附于Win32并且有一套基于COM的API. 為Windows Runtime創(chuàng)建的app通常很糟糕,被人稱為"Windows商店" 應(yīng)用.  它們運行在Windows Runtime上, 而不是Windows商店里, 但是微軟的市場營銷人員可能已經(jīng)沒什么創(chuàng)造力了. Windows  Runtime 應(yīng)用和組件可以用C++實現(xiàn),不管是用Windows Runtime C++ Template Library (WTL) 或者用  C++ Component Extensions (C++/CX)都可以.  在這里我將使用XAML和C++/CX來創(chuàng)建一個功能上和我們之前實現(xiàn)的桌面版應(yīng)用類似的應(yīng)用。

當(dāng)你創(chuàng)建一個空的Windows Store XAML應(yīng)用時向?qū)?chuàng)建的項目實際上并不是空的, 它包含了所有的Windows Store應(yīng)用構(gòu)建和運行所需要的文件和配置。但是這個應(yīng)用的main page是空的。

我們要關(guān)心對這篇文章的目的,唯一的就是主界面。 XAML代碼可以在應(yīng)用在文件MainPage.xaml中,和背后的MainPage.xaml.h MainPage.xaml.cpp的代碼。,我想建立簡單的應(yīng)用程序如下圖。

怎么用C++編寫一個井字游戲

下面是XAML的頁面可能看起來的樣子(在一個真實的應(yīng)用中,你可能要使用應(yīng)用程序欄來操作,如啟動一個新的游戲,主頁上沒有按鍵,但為了簡單起見,我把它們在頁面上)

<Page     x:Class="TicTacToeWinRT.MainPage"     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"     xmlns:local="using:TicTacToeWinRT"     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"     mc:Ignorable="d">           <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">       <Grid.RowDefinitions>          <RowDefinition Height="Auto" />          <RowDefinition Height="Auto" />          <RowDefinition Height="Auto" />          <RowDefinition Height="Auto" />       </Grid.RowDefinitions>              <TextBlock Grid.Row="0" Text="Fun C++: Tic Tac Toe"                   Foreground="White" FontSize="42" FontFamily="Segoe UI"                  Margin="10"                  HorizontalAlignment="Center" VerticalAlignment="Center"                  />        <TextBlock Grid.Row="1" Text="Computer wins!"                  Name="txtStatus"                  Foreground="LightGoldenrodYellow"                   FontSize="42" FontFamily="Segoe UI"                  Margin="10"                  HorizontalAlignment="Center" VerticalAlignment="Center" />              <Grid Margin="50" Width="400" Height="400" Background="White"             Name="board"             PointerReleased="board_PointerReleased"             Grid.Row="2">          <Grid.ColumnDefinitions>             <ColumnDefinition Width="1*" />             <ColumnDefinition Width="1*" />             <ColumnDefinition Width="1*" />          </Grid.ColumnDefinitions>          <Grid.RowDefinitions>             <RowDefinition Height="1*" />             <RowDefinition Height="1*" />             <RowDefinition Height="1*" />          </Grid.RowDefinitions>           <!-- Horizontal Lines -->          <Rectangle Grid.Row="0" Grid.ColumnSpan="3" Height="1" VerticalAlignment="Bottom" Fill="Black"/>          <Rectangle Grid.Row="1" Grid.ColumnSpan="3" Height="1" VerticalAlignment="Bottom" Fill="Black"/>          <Rectangle Grid.Row="2" Grid.ColumnSpan="3" Height="1" VerticalAlignment="Bottom" Fill="Black"/>          <!-- Vertical Lines -->          <Rectangle Grid.Column="0" Grid.RowSpan="3" Width="1" HorizontalAlignment="Right" Fill="Black"/>          <Rectangle Grid.Column="1" Grid.RowSpan="3" Width="1" HorizontalAlignment="Right" Fill="Black"/>          <Rectangle Grid.Column="2" Grid.RowSpan="3" Width="1" HorizontalAlignment="Right" Fill="Black"/>                                  </Grid>              <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Grid.Row="3">          <Button Name="btnStartUser" Content="Start user" Click="btnStartUser_Click" />          <Button Name="btnStartComputer" Content="Start computer" Click="btnStartComputer_Click"/>       </StackPanel>           </Grid> </Page>

與win32桌面版的游戲不同,在Windows  Runtime的程序中,我們不必關(guān)心用戶界面的繪制,但我們還得創(chuàng)建UI元素。比如,當(dāng)用戶在玩游戲的時候,在其中一個格子里單擊了鼠標(biāo),我們就必須創(chuàng) 建一個UI元素來表示一個標(biāo)記。為此,我會用在桌面版(too.bmp and  ttx.bmp)中用過的位圖,并且在圖像控件中顯示它們.。我還會在獲勝的行、列、或?qū)蔷€上畫一個紅色的線,為此,我會用到Lineshape類。

我們可以直接把tictactoe_game的源代碼(game.h, game.cpp, strategy_x.h and strategy_o.h)添加到工程里?;蛘呶覀兛梢园阉鼈儗?dǎo)出成一個單獨的DLL。為了方便,我使用了相同的源文件。然后我們必須添加一個tictactoe_game對象到MainPage類中。

#pragma once  #include "MainPage.g.h" #include "..\Common\game.h"  namespace TicTacToeWinRT {    public ref class MainPage sealed    {    private:       tictactoe_game game;        // ...     }; }

這里有3類基本的事件處理handler需要我們自己實現(xiàn):

  • 處理“Start user”按鈕的theClickedevent事件的handler

  • 處理“Start computer”按鈕的theClickedevent事件的handler

  • 處理面板網(wǎng)格的thePointerReleasedevent事件的handler,當(dāng)指針(鼠標(biāo)或者手勢)從網(wǎng)格釋放時被調(diào)用。

對這兩個按鈕點擊的handler,在邏輯上與我們在Win32桌面應(yīng)用中實現(xiàn)的類似。首先,我們必須要重置游戲(一會會看到這代表什么意思)。如果玩家 先開始,那么我們僅僅只需要用正確的策略來初始化游戲?qū)ο蟆H绻请娔X先開始,那我們除了要初始化策略,還要讓電腦呈現(xiàn)出真正走了一步并且在電腦走的那一 步的單元格上做上標(biāo)記。

void TicTacToeWinRT::MainPage::btnStartUser_Click(Object^ sender, RoutedEventArgs^ e) {    ResetGame();     game.start(tictactoe_player::user); }  void TicTacToeWinRT::MainPage::btnStartComputer_Click(Object^ sender, RoutedEventArgs^ e) {    ResetGame();     game.start(tictactoe_player::computer);    auto cell = game.move(tictactoe_player::computer);        PlaceMark(cell, tictactoe_player::computer); }

PlaceMark()方法創(chuàng)建了一個newImagecontrol控件,設(shè)定它的Source是tttx.bmp或者ttt0.bmp,并且把它添加到所走的那一步的面板網(wǎng)格上。

void TicTacToeWinRT::MainPage::PlaceMark(tictactoe_cell const cell, tictactoe_player const player) {    auto image = ref new Image();    auto bitmap = ref new BitmapImage(       ref new Uri(player == tictactoe_player::computer ? "ms-appx:///Assets/tttx.bmp" : "ms-appx:///Assets/ttt0.bmp"));    bitmap->ImageOpened += ref new RoutedEventHandler(        [this, image, bitmap, cell](Object^ sender, RoutedEventArgs^ e) {          image->Width = bitmap->PixelWidth;          image->Height = bitmap->PixelHeight;          image->Visibility = Windows::UI::Xaml::Visibility::Visible;    });     image->Source = bitmap;     image->Visibility = Windows::UI::Xaml::Visibility::Collapsed;    image->HorizontalAlignment = Windows::UI::Xaml::HorizontalAlignment::Center;    image->VerticalAlignment = Windows::UI::Xaml::VerticalAlignment::Center;     Grid::SetRow(image, cell.row);    Grid::SetColumn(image, cell.col);     board->Children->Append(image); }

當(dāng)開始一場新游戲時,這些在游戲過程中被添加到網(wǎng)格上的Imagecontrol控件需要被移除掉。這正是theResetGame()method方法所做的事情。此外,它還移除了游戲勝利時顯示的紅線和顯示游戲結(jié)果的文字。

void TicTacToeWinRT::MainPage::ResetGame() {    std::vector<Windows::UI::Xaml::UIElement^> children;     for(auto const & child : board->Children)    {       auto typeName = child->GetType()->FullName;       if(typeName == "Windows.UI.Xaml.Controls.Image" ||          typeName == "Windows.UI.Xaml.Shapes.Line")       {          children.push_back(child);       }    }     for(auto const & child : children)    {       unsigned int index;       if(board->Children->IndexOf(child, &index))       {          board->Children->RemoveAt(index);       }    }     txtStatus->Text = nullptr; }

當(dāng)玩家在一個單元格上點擊了一下指針,并且這個單元格是沒有被占據(jù)的,那我們就讓他走這一步。如果這時游戲還沒有結(jié)束,那我們也讓電腦走一步。當(dāng)游戲在玩 家或者電腦走過一步之后結(jié)束,我們會在一個text box中顯示結(jié)果并且如果有一方勝利,會在勝利的行,列或?qū)巧蟿澤霞t線。

void TicTacToeWinRT::MainPage::board_PointerReleased(Platform::Object^ sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs^ e) {    if(game.is_started() && ! game.is_finished())    {       auto cellw = board->ActualWidth / 3;       auto cellh = board->ActualHeight / 3;        auto point = e->GetCurrentPoint(board);       auto row = static_cast<int>(point->Position.Y / cellh);       auto col = static_cast<int>(point->Position.X / cellw);        game.move(tictactoe_cell(row, col), tictactoe_player::user);       PlaceMark(tictactoe_cell(row, col), tictactoe_player::user);        if(!game.is_finished())       {          auto cell = game.move(tictactoe_player::computer);          PlaceMark(cell, tictactoe_player::computer);           if(game.is_finished())          {             DisplayResult(                game.is_victory(tictactoe_player::computer) ?                 tictactoe_player::computer :                tictactoe_player::none);          }       }       else       {          DisplayResult(             game.is_victory(tictactoe_player::user) ?              tictactoe_player::user :             tictactoe_player::none);       }    } }  void TicTacToeWinRT::MainPage::DisplayResult(tictactoe_player const player) {    Platform::String^ text = nullptr;    switch (player)    {    case tictactoe_player::none:       text = "It's a draw!";       break;    case tictactoe_player::computer:       text = "Computer wins!";       break;    case tictactoe_player::user:       text = "User wins!";       break;    }     txtStatus->Text = text;     if(player != tictactoe_player::none)    {       auto coordinates = game.get_winning_line();       if(coordinates.first.is_valid() && coordinates.second.is_valid())       {          PlaceCut(coordinates.first, coordinates.second);       }    } }  void TicTacToeWinRT::MainPage::PlaceCut(tictactoe_cell const start, tictactoe_cell const end) {    auto cellw = board->ActualWidth / 3;    auto cellh = board->ActualHeight / 3;     auto line = ref new Line();    line->X1 = start.col * cellw + cellw / 2;    line->Y1 = start.row * cellh + cellh / 2;     line->X2 = end.col * cellw + cellw / 2;    line->Y2 = end.row * cellh + cellh / 2;     line->StrokeStartLineCap = Windows::UI::Xaml::Media::PenLineCap::Round;    line->StrokeEndLineCap = Windows::UI::Xaml::Media::PenLineCap::Round;    line->StrokeThickness = 15;    line->Stroke = ref new SolidColorBrush(Windows::UI::Colors::Red);     line->Visibility = Windows::UI::Xaml::Visibility::Visible;     Grid::SetRow(line, 0);    Grid::SetColumn(line, 0);    Grid::SetRowSpan(line, 3);    Grid::SetColumnSpan(line, 3);     board->Children->Append(line); }

到這里就全部結(jié)束了,你可以build它了,啟動它然后玩吧。它看起來像這樣:

怎么用C++編寫一個井字游戲

到此,關(guān)于“怎么用C++編寫一個井字游戲”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

c++
AI