Introduction
Renju is a famous game all over the world which is believed to originate from the East. In this network-supported game, people can connect to his/her opponent directly without any fuss of server. So it's sort of like a P2P network connection.
The connection requester plays the white side and goes second; the opponent (requestee) plays black side and goes first. Who's the first to align his/her 5 continuous pieces in a row in horizontal or vertical or diagonal direction is the winner. If black side wins first, the white side can go one more step because he/she goes second at the beginning.
Details of Code
To write the network part, I used CSocket
class from MFC and inherit my own socket classes from it. I write CListenSocket
class, which is used to monitor the hard-coded port to respond to any connection requests.
#include "stdafx.h"
#include "Renju.h"
#include "RenjuDoc.h"
#include "RenjuView.h"
#include "ListenSocket.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
CListenSocket::CListenSocket(CRenjuView* pView)
{
m_pView = pView;
}
CListenSocket::~CListenSocket()
{
}
#if 0
BEGIN_MESSAGE_MAP(CListenSocket, CSocket)
END_MESSAGE_MAP()
#endif // 0
void CListenSocket::OnAccept(int nErrorCode)
{
if(!m_pView->m_bLinked)
{
int result = AfxMessageBox(_T("Want to accept a new game?"), MB_YESNO);
if(result == IDYES)
{
m_pView->RequestAccepted(IDYES);
}
else
{
m_pView->RequestAccepted(IDNO);
}
}
else
{
m_pView->RequestAccepted(IDNO);
}
CSocket::OnAccept(nErrorCode);
}
I also write
CClientSocket
class, which is used to communicate with the opponent after the connection.
#include "stdafx.h"
#include "Renju.h"
#include "RenjuDoc.h"
#include "RenjuView.h"
#include "ClientSocket.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
CClientSocket::CClientSocket(CRenjuView* pView)
{
m_pView = pView;
}
CClientSocket::~CClientSocket()
{
}
#if 0
BEGIN_MESSAGE_MAP(CClientSocket, CSocket)
END_MESSAGE_MAP()
#endif // 0
void CClientSocket::OnReceive(int nErrorCode)
{
int buf[3];
Receive(buf, 3 * sizeof(int));
if(buf[0] == NEWGAME)
{
m_pView->SendMessage(WM_USER_NEWGAME);
}
else if(buf[0] == NONEWGAME)
{
m_pView->GetParent()->SetWindowText(_T("Renju - Game over"));
AfxMessageBox(_T("Your partner doesn't want to play a new game now"));
}
else if(buf[0] == DISCONNECT)
{
m_pView->m_bLinked = FALSE;
m_pView->m_bInGame = FALSE;
m_pView->DestroyClientSocket();
m_pView->ResetCoords();
m_pView->GetParent()->SetWindowText(_T("Renju - Ready"));
AfxMessageBox(_T("Your partner disconnected!"));
}
else if(buf[0] == ACCEPT)
{
m_pView->m_bMyTurn = FALSE;
m_pView->m_bLinked = TRUE;
m_pView->m_bOpponentWin = FALSE;
m_pView->m_bInGame = TRUE;
m_pView->ResetCoords();
m_pView->m_nColor = -1;
m_pView->GetParent()->SetWindowText(_T("Renju - Opponent's turn"));
}
else if(buf[0] == DECLINE)
{
AfxMessageBox(_T("Your partner cannot play with you right now!"));
m_pView->GetParent()->SetWindowText(_T("Renju - Ready"));
m_pView->DestroyClientSocket();
}
else if(buf[0] == SURRENDER)
{
m_pView->m_bMyTurn = FALSE;
m_pView->m_bInGame = FALSE;
m_pView->GetParent()->SetWindowText(_T("Renju - Game over"));
AfxMessageBox(_T("Your partner surrendered to you.\n"
"You won this game!"));
}
else
{
m_pView->OneStep(buf[1], buf[2]);
if(buf[0] == EVEN)
{
m_pView->m_bMyTurn = FALSE;
m_pView->m_bInGame = FALSE;
m_pView->GetParent()->SetWindowText(_T("Renju - Game over"));
AfxMessageBox(_T("Even game!"));
}
else if(buf[0] == ULOSE)
{
m_pView->m_bMyTurn = FALSE;
m_pView->m_bInGame = FALSE;
m_pView->GetParent()->SetWindowText(_T("Renju - Game over"));
AfxMessageBox(_T("Sorry!\n"
"You lost this game."));
}
else if(buf[0] == UWIN)
{
m_pView->m_bMyTurn = FALSE;
m_pView->m_bInGame = FALSE;
m_pView->GetParent()->SetWindowText(_T("Renju - Game over"));
AfxMessageBox(_T("Congratulations!\n"
"You won this game."));
}
else if(buf[0] == IWIN)
{
m_pView->m_bMyTurn = TRUE;
m_pView->m_bOpponentWin = TRUE;
m_pView->GetParent()->SetWindowText(_T("Renju - Your turn"));
}
else if(buf[0] == COORDS)
{
m_pView->m_bMyTurn = TRUE;
m_pView->GetParent()->SetWindowText(_T("Renju - Your turn"));
}
}
CSocket::OnReceive(nErrorCode);
}
I fulfilled most of the functions in the
CRenjuView
class. In it, I have two member variables of
CClientSocket
which are
m_pClientSocket
and
m_pClientSocket2
.
m_pClientSocket
is the main socket used in the game.
m_pClientSocket2
is like a backup one. Whenever a connection request comes to the listening socket and
m_pClientSocket
is used in a game, the program is responsible to create
m_pClientSocket2
and use it to tell the other side that the request cannot be accepted. Of course, if
m_pClientSocket
is not in a game, it will be used to do this work or accept the connection request. The part of code is in the function
void RequestAccepted(int accept)
.
void CRenjuView::RequestAccepted(int accept)
{
if(!m_pClientSocket)
{
m_pClientSocket = new CClientSocket(this);
if(!m_pListenSocket->Accept(*m_pClientSocket))
{
DestroyClientSocket();
GetParent()->SetWindowText(_T("Renju - Ready"));
AfxMessageBox(_T("You cannot accept this request!"));
}
if(accept == IDYES)
{
int buf[3] = {ACCEPT, 0, 0};
if(m_pClientSocket->Send(buf, 3 * sizeof(int)) == SOCKET_ERROR)
{
AfxMessageBox(_T("Connection interrupted!\n"
"Cannot open the new game."));
m_bLinked = FALSE;
m_bInGame = FALSE;
DestroyClientSocket();
GetParent()->SetWindowText(_T("Renju - Ready"));
return;
}
m_bMyTurn = TRUE;
m_bLinked = TRUE;
m_bOpponentWin = FALSE;
m_bInGame = TRUE;
ResetCoords();
m_nColor = 1;
GetParent()->SetWindowText(_T("Renju - Your turn"));
}
else
{
int buf[3] = {DECLINE, 0, 0};
m_pClientSocket->Send(buf, 3 * sizeof(int));
GetParent()->SetWindowText(_T("Renju - Ready"));
DestroyClientSocket();
}
return;
}
if(m_pClientSocket2)
DestroyClientSocket2();
m_pClientSocket2 = new CClientSocket(this);
if(!m_pListenSocket->Accept(*m_pClientSocket2))
{
DestroyClientSocket2();
GetParent()->SetWindowText(_T("Renju - Ready"));
AfxMessageBox(_T("You cannot accept this request!"));
}
int buf[3] = {DECLINE, 0, 0};
m_pClientSocket2->Send(buf, 3 * sizeof(int));
GetParent()->SetWindowText(_T("Renju - Ready"));
DestroyClientSocket2();
}
The current game condition is stored in a 2-D array
int m_nCoords[DIVISIONS][DIVISIONS]
. Each element can have one of three values 1(Black), -1(White) and 0(Unoccupied).
The message communication during the game is quite straightforward. I won't explain too much about it. I put the message information in an integer array
buf
which has 3 elements.
buf[0]
is used to store the pre_defined message type(I comment them in the code so that they are easy to be followed.).
buf[1]
and
buf[2]
are used to store the position information(x, y) of last step if applicable, so that the corresponding
m_Coords[x][y]
can be set.
As for the painting, I used the bitmaps and GDI to draw the board and pieces which is just a piece of cake to pros. To draw the round BMP of the piece, I used a mask bitmap(mask.bmp in ./res) to erase the edge of the original rectangular BMP. I learned it from Charles Petzold<programming windows="">. It's like
void CRenjuView::DrawPieces()
{
CClientDC dc(this);
CBitmap bmWhite, bmBlack, bmMask;
bmWhite.LoadBitmap(IDB_WHITEPIECE);
bmBlack.LoadBitmap(IDB_BLACKPIECE);
bmMask.LoadBitmap(IDB_MASK);
CDC dcMemWhite;
dcMemWhite.CreateCompatibleDC(&dc);
CBitmap* pOld1 = dcMemWhite.SelectObject(&bmWhite);
CDC dcMemBlack;
dcMemBlack.CreateCompatibleDC(&dc);
CBitmap* pOld2 = dcMemBlack.SelectObject(&bmBlack);
CDC dcMemMask;
dcMemMask.CreateCompatibleDC(&dc);
CBitmap* pOld3 = dcMemMask.SelectObject(&bmMask);
dcMemWhite.BitBlt(0, 0, 36, 36, &dcMemMask, 0, 0, SRCAND);
dcMemBlack.BitBlt(0, 0, 36, 36, &dcMemMask, 0, 0, SRCAND);
for(int i = 0; i < DIVISIONS; i++)
for(int j = 0; j < DIVISIONS; j++)
{
if(m_nCoords[i][j] == -1)
{
dc.BitBlt((i + 1) * CELLSIZE - 18,
(j + 1) * CELLSIZE - 18,
36, 36, &dcMemMask,
0, 0, 0x220326);
dc.BitBlt((i + 1) * CELLSIZE - 18,
(j + 1) * CELLSIZE - 18,
36, 36, &dcMemWhite,
0, 0, SRCPAINT);
}
if(m_nCoords[i][j] == 1)
{
dc.BitBlt((i + 1) * CELLSIZE - 18,
(j + 1) * CELLSIZE - 18,
36, 36, &dcMemMask,
0, 0, 0x220326);
dc.BitBlt((i + 1) * CELLSIZE - 18,
(j + 1) * CELLSIZE - 18,
36, 36, &dcMemBlack,
0, 0, SRCPAINT);
}
if(i == m_ptLastStep.x &&
j == m_ptLastStep.y &&
m_nCoords[i][j] != 0)
{
CPen pen(PS_SOLID, 1, RGB(192, 0, 0));
CPen* pOldPen = dc.SelectObject(&pen);
dc.SelectStockObject(NULL_BRUSH);
dc.Rectangle((i + 1) * CELLSIZE - 18,
(j + 1) * CELLSIZE - 18,
(i + 1) * CELLSIZE + 18,
(j + 1) * CELLSIZE + 18);
dc.SelectObject(pOldPen);
}
}
dcMemWhite.SelectObject(pOld1);
dcMemBlack.SelectObject(pOld2);
dcMemMask.SelectObject(pOld3);
}
Finally, to play the background MIDI music, I write my own
CMidi
class using MCI API and used it in the
CRenjuView
. A Timer is set to play the music continuously.
There're some details I left unexplained. If you find it hard to follow, please feel free to tell me. Thanks!