Introduction
This C++ is fun series is intended to show that writing code in C++ can be as productive and fun as in other mainstream languages. In this second instalment, I will walk you through creating a tic tac toe game from scratch in C++. This article, and the entire series is targeted to developers that want to learn C++ or are curious about the capabilities of the language.
Many young people want to learn programming to write games. C++ is the most used language for writing games, though one needs to gain lots of programming experience before producing the next Angry Birds. A Tic Tac Toe game could be a good option to start, and in fact many years ago, it was the first game I wrote after I started learning C++. I hope this article can help both beginners and experienced developers, yet not very familiar with C++.
I am using Visual Studio 2012 for writing the source code for this article.
The Game
If you have not played Tic Tac Toe and are not familiar with the game, here is a description from Wikipedia.
Tic-tac-toe (or Noughts and crosses, Xs and Os) is a paper-and-pencil game for two players, X and O, who take turns marking the spaces in a 3×3 grid. The player who succeeds in placing three respective marks in a horizontal, vertical, or diagonal row wins the game.
The game is to be played between the computer and a human opponent. Either of the two can start first.
There are two things to implement for creating the game: the game logic and the game user interface. There are various possibilities for creating the UI on Windows, including Win32 API, MFC, ATL, GDI+, DirectX, etc. In this article, I will show how we can use the same game logic implementation to build applications using various technologies. We will create two applications, one with Win32 API and one with C++/CX for the Windows Runtime.
The Game Logic
A player can play a perfect game (that means win or draw) if he/she follows several simple rules in placing the next mark on the grid. The rules are described on Wikipedia, where you can also find the optimal strategy for the player that makes the first move.
The optimal strategies for both the first and second player are available on an xkcd drawing. Though it has several errors (it misses making winning moves in several situations and at least in one case is missing an X mark), I will be using this version for the playing strategy (with fixes the errors that I was able to find). Bear in mind, this means the computer will always play a perfect game. If you implement such a game, you probably want to also let the users win, in which case you need a different approach. But for the purpose of this article, this should suffice.
The first question that arises is what data structure can we use to model this picture in a C++ program. There could be different choices such as a tree, graph, an array or even bit fields (if one is really picky about memory consumption). The grid has 9 cells and the simplest choice in my opinion is to use an array of 9 integers, one for each cell: 0 can represent an empty cell, 1 a cell marked with X and 2 a cell marked with O. Let's take the following picture and see how it can be encoded.
The picture can be read like this:
- Place an X in cell (0,0). The grid can be encoded as:
1, 0, 0, 0, 0, 0, 0, 0, 0
- If the opponent places an O in cell (0,1) then place an X in cell (1,1). The grid encoding is now:
1, 2, 0, 0, 1, 0, 0, 0, 0
- If the opponent places an O in cell (0,2) then place an X in cell (2,2). The grid encoding is now:
1, 2, 2, 0, 1, 0, 0, 0, 1
. This represents a winning move. - ...
- If the opponent places an O in cell (2,2) then place an X in cell (2,0). The grid encoding is now:
1, 2, 0, 0, 1, 0, 1, 0, 2
. At this point, regardless of what move the opponent makes, X will win the game. - If the opponent places an O in cell (0,2) then place an X in cell (1,0). The grid encoding is now:
1, 2, 2, 1, 1, 0, 1, 0, 2
. This represents a winning move. - ...
With this in mind, we can go ahead and encode this in the program. We will use a std::array
for representing the 9 cells board. This is a fixed-size container, with the size known at compile time, that stores its elements in a contiguous memory area. To simplify using the same array type over and over, I will define an alias for it.
#include <array>
typedef std::array<char, 9> tictactoe_status;
The optimal strategies described above are then represented as a sequence (another array) of such arrays.
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
is the optimal strategy for the first player, and strategy_o
is the optimal strategy for the second player. If you look in the source code available with the article, you'll notice that the actual definition of these two arrays differs from what I have shown earlier.
tictactoe_status const strategy_x[] =
{
#include "strategy_x.h"
};
tictactoe_status const strategy_o[] =
{
#include "strategy_o.h"
};
This is a little trick, I'd argue, that allows us move the actual, long, content of the array in a separate file (the actual extension of these files is not important, it can be anything not just a C++ header) and keep the source file and the definition simple and clear. The content of the strategy_x.h and strategy_o.h files is brought into the source file during the pre-processing stage of the compilation, just like a regular header file. Here is a snippet of the strategy_x.h file.
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,
You should notice that if you use a C++11 compliant compiler, you can use a std::vector
instead of the C-like array. This is not available for Visual Studio 2012, but is supported in 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},
};
To define what player these numbers in the arrays represent, I am defining a enumeration called tictactoe_player
.
enum class tictactoe_player : char
{
none = 0,
computer = 1,
user = 2,
};
The game logic will be implemented in a class called tictactoe_game
. At a minimum, the class should have the following state:
- a boolean flag that indicates whether a game has started, represented by
started
- the current state of the game (the marks on the grid), represented by
status
- the set of moves that are possible to make in the future based on the current status of the game, represented by
strategy
.
class tictactoe_game
{
bool started;
tictactoe_status status;
std::set<tictactoe_status> strategy;
};
During the game, we will need to know whether the game is started, finished, and if finished, if any of the players won or the game ended in a draw. The tictactoe_game
provides three methods for this:
is_started
indicates whether a game has started is_victory
checks if the specified player has won the game is_finished
checks if the game has finished. A game is finished when one of the players wins or when the grid is full and players cannot make any additional moves.
bool is_started() const {return started;}
bool is_victory(tictactoe_player const player) const {return is_winning(status, player);}
bool is_finished() const
{
return is_full(status) ||
is_victory(tictactoe_player::user) ||
is_victory(tictactoe_player::computer);
}
The implementation of is_victory
and is_finished
actually relies on two private
methods, is_full
, that indicates whether the grid is full and no further moves are possible, and is_winning
, that indicates whether on a given grid, a given player has won. Their implementation should be straight forward to understand. is_full
counts the number of cells in the grid (array) that are empty (the value in the array is 0
), and returns true
if there is no such cell. is_winning
checks the lines, rows and the two diagonals of the grid to see if the given player has scored a winning streak.
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;});
}
When a player wins a game, we want to draw a line over the column, row or diagonal that won the game. Therefore, we need to know which is that winning line. Method get_winning_line
returns a pair of tictactoe_cell
s that indicate the two ends of the line. Its implementation is very similar to is_winning
: it checks the rows, columns and diagonals and if one is a winning line, it returns its two ends (coordinates). It may look a bit verbose, but I believe checking the lines this way is simpler than running loops of three iterations for rows, columns and diagonals.
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());
}
The only things left at this point is starting a new game and making a move (both for the computer and the user).
For starting a new game, we need to know what player makes the first move, so that we can pick the appropriate strategy (of the two available). We also have to reset the array representing the game grid. Method start()
initializes a new game. The set of possible future moves is reinitialized with the values from the strategy_x
or strategy_o
arrays. Notice that in the code below, strategy
is a std::set
, and strategy_x
and strategy_o
are arrays that have duplicate entries, because some of the positions in the tictactoe chart are duplicates. The set is a container of unique values and it preserves only the unique possible positions (for instance about half of the strategy_o
array is represented by duplicates). std::copy
from <algorithm>
is used to copy the content of the array into the set, and method assign()
is used to set a value (0 in our case) for all the elements of a std::array
.
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;
}
To place a move for a human player, all we need to do is make sure the selected cell is empty and if so, fill it with the appropriate mark. Method move()
takes the coordinates of the cell, the mark of the player and returns true
if the move was valid or false
otherwise.
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;
}
Making a move for the computer requires more work, because we have to find the next best move the computer should do. An overload of the move()
method looks up the next set of possible moves (the strategy) and then selects the best move from this set of possible moves. After making the move, it checks whether the move won the game for the computer and if so, marks the game as finished. The method returns the position where the computer placed its move.
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;
}
The lookup_strategy()
method iterates through the current possible moves to find what moves are possible from the current one. It takes advantage of the fact that an empty cell is represented by a 0 and any filled cell, is either 1 or 2, and both these values are greater than 0. A cell value can only transform from a 0 into a 1 or 2. Never a cell transforms from 1 into 2 or from 2 into 1.
When the game begins, the grid is represented as 0,0,0,0,0,0,0,0,0
and any move is possible from this position. That's why in the start()
method, we copied the entire set of moves. Once a player makes a move, the set of possible moves decreases. For instance, a player makes a move into the first cell. The grid is then represented as 1,0,0,0,0,0,0,0,0
. At this point, no move that has 0
or 2
on the first position in this array is possible anymore and should be filtered out.
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;
}
When selecting the next move, we must make sure we select a move that only differs by a single mark from the current position. If the current position is 1,2,0,0,0,0,0,0,0
and we must move for player 1, we can only select moves that have a single 1 on the last 7 elements in the array: 1,2,1,0,0,0,0,0,0
or 1,2,0,1,0,0,0,0,0
... or 1,2,0,0,0,0,0,0,1
. However, since more than just a single such move is available, we should pick the best one, and the best move is always a move that wins the game. Therefore, we have to check all the moves for a winning move. If no such winning move is available, then we can pick anyone.
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;
}
With this, we have everything in place for the game logic. For further details, you can read the code in files, game.h and game.cpp.
A Game Implementation with Win32 API
The first application that I'll be creating will use Win32 API for the user interface. If you are not familiar with Win32 programming, there are plenty of resources where you can learn more. I will only present some essential aspects for understanding how we can build the final application. Also, I will not show and explain every piece of code for this part, but since the code is available to download, you can browse and read it.
At a minimum, a Win32 application requires the following:
- An entry point, that conventionally is WinMain, not
main
. It takes parameters such as the handle to the current application instance, the command line and flags that indicate how the window should be shown. - A window class, that represents a template for creating a window. A window class contains a set of attributes used by the system, such as the class name, class styles (that are different from the window styles), icon, menu, background brush, pointer to the window procedure, etc. A window class is process specific and must be registered into the system prior to creating the window. To register the window class, use RegisterClassEx.
- A main window, created based on a window class. A window can be created with function CreateWindowEx.
- A window procedure, that is a function that processes messages for all the windows that were created based on a window class. The window procedure is associated with a window class, not a window.
- A message loop. A window can receive messages in two ways: with SendMessage, that directly calls the window procedure for the window and does not return until the window procedure has handled the message, or with PostMessage (or PostThreadMessage) that posts a messages to the message queue of the thread that created the window and returns without waiting for the thread to process the message. Therefore, the thread must run a loop where it fetches messages from the message queue and dispatches them to the window procedure.
You can find an example for simple Win32 application that registers a window class, creates a window and runs a message loop in MSDN. Conceptually, a Win32 application looks like this:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
WNDCLASS wc;
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;
}
This is not enough though, we still need a window procedure to handle the messages sent to the window, such as painting commands, destroying messages, menu commands, and anything else that is necessary to handle. A window procedure can look like this:
LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC dc = ::BeginPaint(hWnd, &ps);
::EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
::PostQuitMessage(0);
return 0;
case WM_COMMAND:
{
...
}
break;
}
return ::DefWindowProc(hWnd, message, wParam, lParam);
}
I like to write more object oriented code and less procedural, so I put together a few classes for wrapping a window class, a window and a device context. You can find the implementation for these classes (that are very small) in the attached source code in framework.h and framework.cpp.
WindowClass
is a RAII-style wrapper class for a window class. In the constructor, it fills a WNDCLASSEX structure and calls RegisterClassEx. In the destructor, it unregisters the window class by calling UnregisterClass. Window
is a thin wrapper over a HWND
exposing methods such as Create
, ShowWindow
and Invalidate
(their name should tell you what they do). It also has several virtual members representing message handlers that are called from the window procedure (OnPaint
, OnMenuItemClicked
, OnLeftButtonDown
). This window is intended to be derived in order to provide specific implementations. DeviceContext
is a RAII-style wrapper class for a device context (HDC
). In the constructor, it calls BeginPaint and in the destructor, it calls EndPaint.
The game main window is TicTacToeWindow
, derived from the Window
class. It overrides the virtual methods for handling messages. The declaration of this class is shown below:
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;
};
Method OnPaint()
handles the painting of the window. It has to draw the window background, the grid lines, the marks in the filled cells (if any) and if the game finished and a player won, a red line over the winning row, column or diagonal. To avoid flickering, we use double buffer technique: create an in-memory device context (compatible with the device context prepared for the window by a call to BeginPaint
), an in-memory bitmap compatible for the in-memory device context, paint on this bitmap, and then copy the content of the in-memory device context over the window device context.
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);
}
I will not list here the content of the DrawBackground
, DrawGrid
and DrawMarks
functions. They are not very complicated, and you can read the source code. The DrawMarks
function uses two bitmaps, ttt0.bmp and tttx.bmp, to draw the marks in the grid.
I will only show how to draw a red line over the winning row, column or diagonal. First, we have to check if the game is finished and if it is, then retrieve the winning line. If the two ends are both valid, then compute the center of the two cells, create and select a pen (a solid, 15 pixels width red line) and draw a line between the middle of the two cells.
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);
}
}
}
The main window has a menu with three items: ID_GAME_STARTUSER
that starts a game in which the user moves first, ID_GAME_STARTCOMPUTER
that starts a game in which the computer moves first and ID_GAME_EXIT
that closes the application. When a user clicks on one of the two start commands, we must start a game. If the computer moves first, then we should let if make the move and then, in both cases, redraw the window.
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;
}
}
The only thing left to take care in the window is handling the user mouse clicks on the window. When the user clicks on a point in the window client area, we should check which grid cell it is, and if it is empty, fill it with the user's mark. Then, if the game is not finished, let the computer make a move.
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);
}
}
}
Finally, we need to implement the WinMain
function, the entry point for the application. The code below is very similar to the one shown in the beginning on this section, with the difference that it uses my wrapper classes for the window class and the window.
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;
}
If you are not familiar with Win32 API programming, you may find this a bit cumbersome, even though, in my opinion, the amount of code I put together is relatively small and simple. However, you have to explicitly take care of all the initialization of objects, and creation of windows, handling of messages, etc. Hopefully, you can find the next section more appealing.
A Game App for Windows Runtime
The Windows Runtime is a new Windows runtime engine introduced in Windows 8. It lives alongside Win32 and has a COM-based API. Applications built for the Windows Runtime are so badly called "Windows Store" applications. They run in the Windows Runtime, not in a Windows store, but people in the marketing division at Microsoft probably had a creativity hiatus. Windows Runtime applications and components can be written in C++ either with the Windows Runtime C++ Template Library (WTL) or with C++ Component Extensions (C++/CX). In this article, I will use XAML and C++/CX to build an application similar in functionality with the desktop one created in the previous section.
When you create a Windows Store blank XAML application, the project the wizard creates is not actually empty, but contains all the files and settings for building and running a Windows Store application. What is empty is the main page of the application.
The only thing we have to care about for the purpose of this article is the main page. The XAML code is available in file MainPage.xaml and the code behind in MainPage.xaml.h and MainPage.xaml.cpp. The simple application I'd like to build looks like in the following image.
Here is how the XAML for the page may look like (in a real application, you probably want to use the application bar for actions like starting a new game, not buttons on the main page, but for simplicity, I'll put them on the page):
<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>
<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"/>
<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>
Unlike the Win32 desktop version of the game, in the Windows Runtime app, we don't have to explicitly take care of the painting of all the UI, but we still have to create the UI elements explicitly. For instance, when a user clicks in a cell during a game, we have to create a UI element that represents a mark. For this purpose, I will use the same bitmaps used in the desktop version (ttto.bmp and tttx.bmp) and display them in an Image
control. I will also draw a red line over the winning row, column or diagonal, and for this purpose, I will use a Line
shape.
We can directly add to the project the source code for the tictactoe_game
(game.h, game.cpp, strategy_x.h and strategy_o.h). Alternatively, we can export them from a separate, common, DLL but for simplicity I will just use the same source files. Then, we just have to add an object of type tictactoe_game
to the MainPage
class.
#pragma once
#include "MainPage.g.h"
#include "..\Common\game.h"
namespace TicTacToeWinRT
{
public ref class MainPage sealed
{
private:
tictactoe_game game;
};
}
There are basically three event handlers that we have to implement:
- Handler for the
Clicked
event for the "Start user" button - Handler for the
Clicked
event for the "Start computer" button - Handler for the
PointerReleased
event for the board's grid, called when the pointer (mouse, finger) is released from the grid.
The logic for the two button click handlers is very similar to what we did in the Win32 desktop application. First, we have to reset the game (will see a bit later what that means). If the user starts first, then we just initialize the game object with the correct strategy. If the computer starts first, in addition to the strategy initialization, we also let the computer actually perform a move and then display a mark in the cell where the computer made the move.
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);
}
The PlaceMark()
method creates a new Image
control, sets its Source
to either tttx.bmp or ttt0.bmp and adds the image control into the cell of the board grid where a move was made.
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);
}
When a new game starts, these Image
controls added during a game to the grid must be removed. That's what the ResetGame()
method does. In addition, it also removes the red line displayed over a winning line and the text that displays the result of a game.
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;
}
When the user presses the pointer over a cell of the board grid, we make a move if that cell is opened. If the game is not finished at this point, we let the computer do a move. When the game ends after the computer or the user made a move, we display the result in a text box and if one of the two players won, we draw a red line over the winning row, column or diagonal.
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);
}
And that is everything. You can build the application, start and play. It looks like this:
Conclusions
In this article, we’ve seen how we can create a simple game in C++ with different user interface using different technologies. We wrote the game logic once, using standard C++ and then used it to build two applications using totally different technologies: Win32 API, where we had to do more explicit work for things like creating a window and painting it, and Windows Runtime with XAML, where the framework did most of the work and we could concentrate on the game logic (and when we had to design the UI we did it in a rather declarative way, not only in XAML but also in the code behind). Among others, we saw how we could use standard containers such as std::array
and std::set
and how seamless we could use the pure C++ logic code in the C++/CX application for the Windows Runtime.
History
- 4th November, 2013: Initial version