Awesome
Thinking Wt 3: creating a TicTacToe game
This article shows the way to create a TicTacToe game when using the Wt library. The previous article, Thinking Wt 2: creating a TicTacToe widget, serves as a starting point.
Overview
This article elaborates on Thinking Wt 2: creating a TicTacToe widget in which a TicTacToe widget was created. First the desired architecture is described, then the WtApplication is modified to handle multiple dialogs. Next, these dialogs are implemented.
Architecture
The architecture, from biggest to smallest, consists of:
- main creates a single class called WtApplication
- WtApplication (a derived class of Wt::WApplication) manages three dialogs, called WtTicTacToeAboutDialog, WtTicTacToeGameDialog and WtTicTacToeMenuDialog
- These three dialogs (all derived class of Wt::WContainerWidget) consist of multiple widgets.
Step 0: Starting point
The code below is the finishing point of the previous article, Thinking Wt 2: creating a TicTacToe widget. For this article, it serves as a starting point.
#include <boost/signals2.hpp>
#include <Wt/WApplication>
#include <Wt/WBreak>
#include <Wt/WBrush>
#include <Wt/WContainerWidget>
#include <Wt/WEnvironment>
#include <Wt/WEvent>
#include <Wt/WPaintDevice>
#include <Wt/WPaintedWidget>
#include <Wt/WPainter>
#include <Wt/WPen>
#include <Wt/WPushButton>
#include "tictactoe.h"
//---------------------------------------------------------------------------
struct WtTicTacToeWidget : public Wt::WPaintedWidget
{
WtTicTacToeWidget()
{
//Without resize, there is nothing to paint on
this->resize(GetWidth(),GetHeight());
this->clicked().connect(this,&WtTicTacToeWidget::OnClicked);
this->update();
}
boost::signals2::signal<void ()> m_signal_has_winner;
boost::signals2::signal<void ()> m_signal_state_changed;
int GetState() const
{
return m_tictactoe.GetWinner();
}
void Restart()
{
m_tictactoe = TicTacToe();
this->update();
}
protected:
void paintEvent(Wt::WPaintDevice *paintDevice)
{
Wt::WPainter painter(paintDevice);
const int width = GetWidth();
const int height = GetHeight();
//Set black pen
Wt::WPen pen = painter.pen();
pen.setCapStyle(Wt::RoundCap);
pen.setColor(Wt::WColor(255,255,255));
painter.setPen(pen);
painter.setBrush(Wt::WBrush(Wt::WColor(255,255,255)));
painter.drawRect(0.0,0.0,GetWidth(),GetHeight());
//Set thick white pen
pen.setColor(Wt::WColor(0,0,0));
const int line_width = std::min(width,height) / 15;
pen.setWidth(line_width);
painter.setPen(pen);
//Vertical lines
painter.drawLine(
((1*width)/3)+4, 0+(line_width/2),
((1*width)/3)-4,height-(line_width/2));
painter.drawLine(
((2*width)/3)-4, 0+(line_width/2),
((2*width)/3)+8,height-(line_width/2));
//Horizontal lines
painter.drawLine(
0+(line_width/2),((1*height)/3)+4,
width-(line_width/2),((1*height)/3)-4);
painter.drawLine(
0+(line_width/2),((2*height)/3)-4,
width-(line_width/2),((2*height)/3)+8);
for (int row=0; row!=3; ++row)
{
const int x1 = ((row + 0) * (width / 3)) + (line_width/1) + 4;
const int x2 = ((row + 1) * (width / 3)) - (line_width/1) - 4;
for (int col=0; col!=3; ++col)
{
const int y1 = ((col + 0) * (height / 3)) + (line_width/1) + 4;
const int y2 = ((col + 1) * (height / 3)) - (line_width/1) - 4;
const int state = m_tictactoe.GetSquare(row,col);
if (state == TicTacToe::player1)
{
//player1 = cross
painter.drawLine(x1,y1,x2,y2);
painter.drawLine(x1,y2,x2,y1);
}
else if (state == TicTacToe::player2)
{
//player1 = circle
painter.drawEllipse(x1,y1,x2-x1,y2-y1);
}
}
}
}
private:
TicTacToe m_tictactoe;
int GetWidth() const { return 300.0; }
int GetHeight() const { return 300.0; }
void OnClicked(const Wt::WMouseEvent& e)
{
if (m_tictactoe.GetWinner() != TicTacToe::no_winner) return;
const int x = 3 * e.widget().x / this->GetWidth();
if (x < 0 || x > 2) return;
const int y = 3 * e.widget().y / this->GetHeight();
if (y < 0 || y > 2) return;
if (m_tictactoe.CanDoMove(x,y))
{
m_tictactoe.DoMove(x,y);
//emit that the state has changed
this->m_signal_state_changed();
}
if (m_tictactoe.GetWinner() != TicTacToe::no_winner)
{
//emit that there is a winner
this->m_signal_has_winner();
}
this->update();
}
};
//---------------------------------------------------------------------------
struct WtTicTacToeDialog : public Wt::WContainerWidget
{
WtTicTacToeDialog()
: m_button(new Wt::WPushButton),
m_tictactoe(new WtTicTacToeWidget)
{
this->addWidget(m_tictactoe);
this->addWidget(new Wt::WBreak(this));
this->addWidget(m_button);
m_button->setText("Restart");
m_tictactoe->m_signal_state_changed.connect(
boost::bind(
&WtTicTacToeDialog::OnStateChanged,
this));
m_button->clicked().connect(
this,&WtTicTacToeDialog::OnRestart);
}
private:
Wt::WPushButton * const m_button;
WtTicTacToeWidget * const m_tictactoe;
void OnRestart()
{
m_tictactoe->Restart();
}
void OnStateChanged()
{
switch (m_tictactoe->GetState())
{
case TicTacToe::player1:
m_button->setText("Player 1 has won. Click to restart");
break;
case TicTacToe::player2:
m_button->setText("Player 2 has won. Click to restart");
break;
case TicTacToe::draw:
m_button->setText("Draw. Click to restart");
break;
case TicTacToe::no_winner:
m_button->setText("Restart");
break;
default:
assert(!"Should not get here");
break;
}
}
};
//---------------------------------------------------------------------------
struct WtApplication : public Wt::WApplication
{
WtApplication(const Wt::WEnvironment& env)
: Wt::WApplication(env),
m_dialog(new WtTicTacToeDialog)
{
this->setTitle("Thinking Wt 3: creating a TicTacToe game");
root()->addWidget(m_dialog);
}
private:
WtTicTacToeDialog * const m_dialog;
};
//---------------------------------------------------------------------------
Wt::WApplication *createApplication(
const Wt::WEnvironment& env)
{
return new WtApplication(env);
}
//---------------------------------------------------------------------------
int main(int argc, char **argv)
{
return WRun(argc, argv, &createApplication);
}
//---------------------------------------------------------------------------
Step 1: Implementing WtTicTacToeApplication
The purpose of a Wt::WApplication is to manage dialogs. In this example, there are three dialogs: the menu screen, the tic-tac-toe game and an about screen. At the construction of WtTicTacToeApplication, the menu screen dialog must be shown. All three dialogs have signals for the application to respond to:
struct WtTicTacToeApplication : public Wt::WApplication
{
WtTicTacToeApplication(const Wt::WEnvironment& env)
: Wt::WApplication(env)
{
this->setTitle("Thinking Wt 3: creating a TicTacToe game");
ShowMenu();
}
private:
void ShowAbout()
{
WtTicTacToeAboutDialog * const d = new WtTicTacToeAboutDialog;
d->m_signal_close.connect(
boost::bind(
&WtTicTacToeApplication::ShowMenu,
this));
root()->clear();
root()->addWidget(d);
}
void ShowMenu()
{
WtTicTacToeMenuDialog * const d = new WtTicTacToeMenuDialog;
d->m_signal_about.connect(
boost::bind(
&WtTicTacToeApplication::ShowAbout,
this));
d->m_signal_start.connect(
boost::bind(
&WtTicTacToeApplication::Start,
this));
root()->clear();
root()->addWidget(d);
}
void Start()
{
WtTicTacToeGameDialog * const d = new WtTicTacToeGameDialog;
d->m_signal_close.connect(
boost::bind(
&WtTicTacToeApplication::ShowMenu,
this));
root()->clear();
root()->addWidget(d);
}
};
The application keeps only one dialog active at a time. When it creates one of the three dialogs, their signals are set to the right slots in the application. The dialogs need not to be deleted, because the addWidget member function takes over this responsibility.
The dialogs are expected to have at least one signal:
- WtTicTacToeAbout: m_signal_close, which must take the application back to the menu
- WtTicTacToeGame: m_signal_close, which must take the application back to the menu
- WtTicTacToeMenu: m_signal_about and m_signal_start, to indicate that another dialog must be shown
Step 2: Implementing WtTicTacToeMenuDialog
The main purpose of WtTicTacToeMenu is to emit signals to indicate that another dialog must be shown. There is one signal for each dialog needed: m_signal_about for the about screen and m_signal_start for starting the game.
struct WtTicTacToeMenuDialog : public Wt::WContainerWidget
{
WtTicTacToeMenuDialog()
: m_button_about(new Wt::WPushButton),
m_button_start(new Wt::WPushButton)
{
this->addWidget(new Wt::WText("TicTacToe"));
this->addWidget(new Wt::WBreak(this));
this->addWidget(m_button_start);
this->addWidget(new Wt::WBreak(this));
this->addWidget(m_button_about);
m_button_about->setText("About");
m_button_start->setText("Start");
m_button_about->clicked().connect(
this,&WtTicTacToeMenuDialog::OnAbout);
m_button_start->clicked().connect(
this,&WtTicTacToeMenuDialog::OnStart);
}
boost::signals2::signal<void ()> m_signal_about;
boost::signals2::signal<void ()> m_signal_start;
private:
Wt::WPushButton * const m_button_about;
Wt::WPushButton * const m_button_start;
void OnAbout()
{
//emit that the about dialog must be opened
m_signal_about();
}
void OnStart()
{
//emit that the game must be started
m_signal_start();
}
};
Note that most work is in connecting the buttons to the signals.
Step 3: Implementing WtTicTacToeGameDialog
The WtTicTacToeGameDialog remains nearly the same as already developed in the previous article (Thinking Wt 2: creating a TicTacToe widget), except that it now needs a 'Close' button and a signal that it can emit on closing.
struct WtTicTacToeGameDialog : public Wt::WContainerWidget
{
WtTicTacToeGameDialog()
: m_button_close(new Wt::WPushButton),
m_button_restart(new Wt::WPushButton),
m_tictactoe(new WtTicTacToeWidget)
{
this->addWidget(m_tictactoe);
this->addWidget(new Wt::WBreak(this));
this->addWidget(m_button_restart);
this->addWidget(m_button_close);
m_button_close->setText("Close");
m_button_restart->setText("Restart");
m_tictactoe->m_signal_state_changed.connect(
boost::bind(
&WtTicTacToeGameDialog::OnStateChanged,
this));
m_button_close->clicked().connect(
this,&WtTicTacToeGameDialog::OnClose);
m_button_restart->clicked().connect(
this,&WtTicTacToeGameDialog::OnRestart);
}
boost::signals2::signal<void ()> m_signal_close;
private:
Wt::WPushButton * const m_button_close;
Wt::WPushButton * const m_button_restart;
WtTicTacToeWidget * const m_tictactoe;
void OnClose()
{
//emit that this dialog closes
m_signal_close();
}
void OnRestart()
{
m_tictactoe->Restart();
}
void OnStateChanged()
{
switch (m_tictactoe->GetState())
{
case TicTacToe::player1:
m_button_restart->setText("Player 1 has won. Click to restart");
break;
case TicTacToe::player2:
m_button_restart->setText("Player 2 has won. Click to restart");
break;
case TicTacToe::draw:
m_button_restart->setText("Draw. Click to restart");
break;
case TicTacToe::no_winner:
m_button_restart->setText("Restart");
break;
default:
assert(!"Should not get here");
break;
}
}
};
Step 4: Implementing WtTicTacToeAboutDialog
The WtTicTacToeAboutDialog purpose is to display text and a 'Close' button and emit m_signal_close.
struct WtTicTacToeAboutDialog : public Wt::WContainerWidget
{
WtTicTacToeAboutDialog()
: m_button_close(new Wt::WPushButton)
{
{
Wt::WTextArea * text = new Wt::WTextArea;
text->setText(
"CppThinkingWt3 (C) 2010 Richel Bilderbeek\n"
"From http://www.richelbilderbeek.nl/CppThinkingWt3.htm"
);
text->setMinimumSize(600,Wt::WLength::Auto);
this->addWidget(text);
}
this->addWidget(new Wt::WBreak);
this->addWidget(m_button_close);
m_button_close->setText("Close");
m_button_close->clicked().connect(
this,&WtTicTacToeAboutDialog::OnClose);
}
boost::signals2::signal<void ()> m_signal_close;
private:
Wt::WPushButton * const m_button_close;
void OnClose()
{
//emit that this dialog closes
m_signal_close();
}
};
Conclusion
This article elaborates on Thinking Wt 2: creating a TicTacToe widget and shows how to modify the WtApplication to handle multiple dialogs.
This article is followed up by Thinking Wt 4: polishing a TicTacToe game, in which the game is made to look nicer.