top of page
Writer's pictureThe Tech Platform

"The Same Game": A Simple Game from Start to Finish - PART-2

Document/View Architecture

The document/view architecture is an interesting paradigm where we separate the actual application data from the displaying of that data to the user. The document contains all of the data while the view gets the data from the document and displays it to the user is some fashion. Here our data is the actual game board, the time it takes to complete the game and other related information. Our view displays the game board as colored blocks and allows the users to click them. The view handles the user interaction and modifies the game data in the document accordingly, then the view is updated to reflect the changes and the cycle continues.


By selecting the document/view architecture option in the MFC Application Wizard, the code base is generated along with all of the mechanics relating the two.


The Document: Keeping Your Data

It is finally time to get started coding. Before we can display anything on the screen we need data to back it up so we'll start with developing the document portion of the application followed by the display of that data.


First we'll create a class that represents our game board, let's call it CSameGameBoard. Create a new class by right-clicking on the SameGame project in the Solution Explorer and selecting "Add -> Class..." or "Add Class..." from the Project menu. We want a C++ class from the C++ group and click "Add". This will bring up the Generic C++ Class Wizard. Fill it out with the class name that we chose as it appears below.

Now let's fill in the game board class. Here is the code for the header file.

#pragma once

class CSameGameBoard
{
public:
  /*  Default Constructor */
  CSameGameBoard(void);
  /*  Destructor */
  ~CSameGameBoard(void);
  /*  Function to randomly setup the board */
  void SetupBoard(void);
  /*  Get the color at a particular location */
  COLORREF GetBoardSpace(int row, int col);
  /*  Accessor functions to get board size information */
  int GetWidth(void) const { return m_nWidth; }
  int GetHeight(void) const { return m_nHeight; }
  int GetColumns(void) const { return m_nColumns; }
  int GetRows(void) const { return m_nRows; }
  /*  Function to delete the board and free memory */
  void DeleteBoard(void);
private:
  /*  Function to create the board and allocate memory */
  void CreateBoard(void);
  /*  2D array pointer */
  int** m_arrBoard;
  /*  List of colors, 0 is background and 1-3 are piece colors */
  COLORREF m_arrColors[4];
  /*  Board size information */
  int m_nColumns;
  int m_nRows;
  int m_nHeight;
  int m_nWidth;
};

This class is conceptually quite simple. It contains a pointer, called m_arrBoard, to a two dimensional array of integers that represent empty (0) or one of the three colors (1-3). We add member variables to keep track of the rows (m_nRows), columns (m_nColumns), pixel width (m_nHeight) and height (m_nHeight). There are also functions to create, set up and delete the board.


The create method needs to allocate the two dimensional array to store the game board and initializes all of the blocks to empty. The setup method will reset the game board by randomly choosing a color for each space on the board. Finally the delete method de-allocates the memory that we are using for the game board to eliminate memory leaks.


In the board there is also an array COLORREF types. A COLORREF is simply a 32-bit unsigned integer that contains RGBA color values for MFC applications. This array contains the colors for the background, at index zero, and the block colors at indices one through three. These indices are the same numbers that are held in the two dimensional array of integers. In the constructor below we are using the RGB macro to create a COLORREF value from three integers representing the red, green and blue values.


Below is the implementation of the CSameGameBoard class, in SameGameBoard.cpp.

#include "StdAfx.h"
#include "SameGameBoard.h"

CSameGameBoard::CSameGameBoard(void)
: m_arrBoard(NULL),
  m_nColumns(15), m_nRows(15),
  m_nHeight(35),  m_nWidth(35)
{
  m_arrColors[0] = RGB(  0,  0,  0);
  m_arrColors[1] = RGB(255,  0,  0);
  m_arrColors[2] = RGB(255,255, 64);
  m_arrColors[3] = RGB(  0,  0,255);
}

CSameGameBoard::~CSameGameBoard(void)
{
  //  Simply delete the board
  DeleteBoard();
}

void CSameGameBoard::SetupBoard(void)
{
  //  Create the board if needed
  if(m_arrBoard == NULL)
    CreateBoard();
  //  Randomly set each square to a color
  for(int row = 0; row < m_nRows; row++)
    for(int col = 0; col < m_nColumns; col++)
      m_arrBoard[row][col] = (rand() % 3) + 1;
}

COLORREF CSameGameBoard::GetBoardSpace(int row, int col)
{
  //  Check the bounds of the array
  if(row < 0 || row >= m_nRows || col < 0 || col >= m_nColumns)
    return m_arrColors[0];
  return m_arrColors[m_arrBoard[row][col]];
}

void CSameGameBoard::DeleteBoard(void)
{
  //  Don't delete a NULL board
  if(m_arrBoard != NULL)
  {
    for(int row = 0; row < m_nRows; row++)
    {
      if(m_arrBoard[row] != NULL)
      {
        //  Delete each row first
        delete [] m_arrBoard[row];
        m_arrBoard[row] = NULL;
      }
    }
    //  Finally delete the array of rows
    delete [] m_arrBoard;
    m_arrBoard = NULL;
  }
}

void CSameGameBoard::CreateBoard(void)
{
  //  If there is already a board, delete it
  if(m_arrBoard != NULL)
    DeleteBoard();
  //  Create the array of rows
  m_arrBoard = new int*[m_nRows];
  //  Create each row
  for(int row = 0; row < m_nRows; row++)
  {
    m_arrBoard[row] = new int[m_nColumns];
    //  Set each square to be empty
    for(int col = 0; col < m_nColumns; col++)
      m_arrBoard[row][col] = 0;
  }
}

Now that we have our game board encapsulated into an object we can create an instance of that object in the document class. Remember that the document class contains all of our game data and it is separated from the view or display code. We will then set up the document as follows. Here is the header file, SameGameDoc.h (changes bolded).

#pragma once

#include "SameGameBoard.h"

class CSameGameDoc : public CDocument
{
protected: // create from serialization only
  CSameGameDoc();
  virtual ~CSameGameDoc();
  DECLARE_DYNCREATE(CSameGameDoc)

// Attributes
public:

  // Operations
public:

  /*  Functions for accessing the game board */
  COLORREF GetBoardSpace(int row, int col)
  { return m_board.GetBoardSpace(row, col); }
  void SetupBoard(void)   { m_board.SetupBoard(); }
  int GetWidth(void)      { return m_board.GetWidth(); }
  int GetHeight(void)     { return m_board.GetHeight(); }
  int GetColumns(void)    { return m_board.GetColumns(); }
  int GetRows(void)       { return m_board.GetRows(); }
  void DeleteBoard(void)  { m_board.DeleteBoard(); }


  // Overrides
public:
  virtual BOOL OnNewDocument();

protected:

  /*  Instance of the game board */
  CSameGameBoard m_board;


  // Generated message map functions
protected:
  DECLARE_MESSAGE_MAP()
};

Most of this code should look familiar to you except for a few things that are MFC specific. For now we can ignore the DECLARE_DYNCREATE and DECLARE_MESSAGE_MAP lines, they are boilerplate MFC directives.


At this point the document is actually a very simple wrapper to the game board class. In later articles we'll add more functionality that will require changes to the document but for now it is fairly simple. We added an instance of the game board class and then seven functions that call similar functions on the board. This will allow the view to access the board information via the document. The source file for the document (SameGameDoc.cpp) is also very simple since all of the functions that we added have their implementation in-line (changes bolded).

#include "stdafx.h"
#include "SameGame.h"

#include "SameGameDoc.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

// CSameGameDoc
IMPLEMENT_DYNCREATE(CSameGameDoc, CDocument)
BEGIN_MESSAGE_MAP(CSameGameDoc, CDocument)
END_MESSAGE_MAP()

// CSameGameDoc construction/destruction
CSameGameDoc::CSameGameDoc()
{
}

CSameGameDoc::~CSameGameDoc()
{
}

BOOL CSameGameDoc::OnNewDocument()
{
  if (!CDocument::OnNewDocument())
    return FALSE;

  //  Set (or reset) the game board
  m_board.SetupBoard();

  return TRUE;
}

Really all we added was the call to the SetupBoard function in the OnNewDocument handler in the document. All this does is allows the user to start a new game with the built-in accelerator Ctrl+N or from the menu File->New.


As we continue through the series of articles we'll be adding new functions to both the game board and the document to implement different features for the game but for now we are done with the document and are ready to display this information in the view.


The View: Drawing Your Game


Now that the document contains an initialized game board object we need to display this information to the user. This is where we can actually start to see our game come to life.


The first step is to add code to resize the window to the correct size. Right now the window is a default size that isn't what we want. We'll do this in the OnInitialUpdate override. The view class inherits a default OnInitialUpdate that sets up the view and we want to override it so that we can resize the window when the view is initially updated. This can be achieved by opening up the Properties Window from the CSameGameView header file (which will actually be called SameGameView.h). Do this by pressing Alt+Enter or from the menu View-> Properties Window (on some versions of Visual Studio, it will be View-> Other Windows -> Properties Window). Below is what you'll see in the properties window.


In the screenshot my cursor is hovering over the "Overrides" section; click on it. Look for the OnInitialUpdate option, click on it, click the dropdown as shown in the screenshot below and select "<Add> OnInitialUpdate".


This will add the OnInitialUpdate override to your view with some default code in it to call the CView implementation of the function. Then we just add a call to the ResizeWindow function that we will write. So this leaves us with the following in the header file (changes bolded).

#pragma once

class CSameGameView : public CView
{
protected: // create from serialization only
  CSameGameView();
    DECLARE_DYNCREATE(CSameGameView)

  // Attributes
public:
  CSameGameDoc* GetDocument() const;
  // Overrides
public:
  virtual void OnDraw(CDC* pDC);  // overridden to draw this view
  virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
protected:

  // Implementation
public:

  void ResizeWindow();

  virtual ~CSameGameView();
#ifdef _DEBUG
  virtual void AssertValid() const;
  virtual void Dump(CDumpContext& dc) const;
#endif

  // Generated message map functions
protected:
  DECLARE_MESSAGE_MAP()
public:
  virtual void OnInitialUpdate();
};

#ifndef _DEBUG  // debug version in SameGameView.cpp
inline CSameGameDoc* CSameGameView::GetDocument() const
{ return reinterpret_cast<CSameGameDoc*>(m_pDocument); }
#endif

While we're adding in the resize code, we also need to add drawing code to the CSameGameView class. The header and source files for the view already contain a function override called OnDraw. This is where we'll put the drawing code. Here is the full source code for the view (changes bolded).

#include "stdafx.h"
#include "SameGame.h"

#include "SameGameDoc.h"
#include "SameGameView.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

// CSameGameView
IMPLEMENT_DYNCREATE(CSameGameView, CView)
BEGIN_MESSAGE_MAP(CSameGameView, CView)
END_MESSAGE_MAP()

// CSameGameView construction/destruction
CSameGameView::CSameGameView()
{
}

CSameGameView::~CSameGameView()
{
}

BOOL CSameGameView::PreCreateWindow(CREATESTRUCT& cs)
{
  return CView::PreCreateWindow(cs);
}

// CSameGameView drawing

void CSameGameView::OnDraw(CDC* pDC) // MFC will comment out the argument name by default; uncomment it
{
  //  First get a pointer to the document
  CSameGameDoc* pDoc = GetDocument();
  ASSERT_VALID(pDoc);
  if(!pDoc)
    return;
  //  Save the current state of the device context
  int nDCSave = pDC->SaveDC();
  //  Get the client rectangle
  CRect rcClient;
  GetClientRect(&rcClient);
  //  Get the background color of the board
  COLORREF clr = pDoc->GetBoardSpace(-1, -1);
  //	Draw the background first
  pDC->FillSolidRect(&rcClient, clr);
  //  Create the brush for drawing
  CBrush br;
  br.CreateStockObject(HOLLOW_BRUSH);
  CBrush* pbrOld = pDC->SelectObject(&br);
  //	Draw the squares
  for(int row = 0; row < pDoc->GetRows(); row++)
  {
    for(int col = 0; col < pDoc->GetColumns(); col++)
    {
      //  Get the color for this board space
      clr = pDoc->GetBoardSpace(row, col);
      //  Calculate the size and position of this space
      CRect rcBlock;
      rcBlock.top = row * pDoc->GetHeight();
      rcBlock.left = col * pDoc->GetWidth();
      rcBlock.right = rcBlock.left + pDoc->GetWidth();
      rcBlock.bottom = rcBlock.top + pDoc->GetHeight();
      //  Fill in the block with the correct color
      pDC->FillSolidRect(&rcBlock, clr);
      //  Draw the block outline
      pDC->Rectangle(&rcBlock);
    }
  }
  //  Restore the device context settings
  pDC->RestoreDC(nDCSave);
  br.DeleteObject();
}


// CSameGameView diagnostics
#ifdef _DEBUG
void CSameGameView::AssertValid() const
{
  CView::AssertValid();
}

void CSameGameView::Dump(CDumpContext& dc) const
{
  CView::Dump(dc);
}

//  non-debug version is inline
CSameGameDoc* CSameGameView::GetDocument() const
{
  ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CSameGameDoc)));
  return (CSameGameDoc*)m_pDocument;
}
#endif //_DEBUG

void CSameGameView::OnInitialUpdate()
{
  CView::OnInitialUpdate();

  //  Resize the window
  ResizeWindow();

}


void CSameGameView::ResizeWindow()
{
  //  First get a pointer to the document
  CSameGameDoc* pDoc = GetDocument();
  ASSERT_VALID(pDoc);
  if(!pDoc)
    return;
  //  Get the size of the client area and the window
  CRect rcClient, rcWindow;
  GetClientRect(&rcClient);
  GetParentFrame()->GetWindowRect(&rcWindow);
  //  Calculate the difference
  int nWidthDiff = rcWindow.Width() - rcClient.Width();
  int nHeightDiff = rcWindow.Height() - rcClient.Height();
  //  Change the window size based on the size of the game board
  rcWindow.right = rcWindow.left +
    pDoc->GetWidth() * pDoc->GetColumns() + nWidthDiff;
  rcWindow.bottom = rcWindow.top +
    pDoc->GetHeight() * pDoc->GetRows() + nHeightDiff;
  //  The MoveWindow function resizes the frame window
  GetParentFrame()->MoveWindow(&rcWindow);
}

It is very simple to draw the game board, we are just going to loop through each row, column by column, and draw a colored rectangle. There is one argument to the OnDraw function and that is a CDC pointer. The CDC class is the base class for all device contexts. A device context is the generic interface to a device such as the screen or a printer. Here we'll use it to draw to the screen.


We first start the function by getting a pointer to the document so that we can get the board information. Next we call the SaveDC function from the device context. This function saves the state of the device context so that we can restore it after we are done.

//  Get the client rectangle
CRect rcClient;
GetClientRect(&rcClient);
//  Get the background color of the board
COLORREF clr = pDoc->GetBoardSpace(-1, -1);
//  Draw the background first
pDC->FillSolidRect(&rcClient, clr);

Next we need to color the background of the client area black so we get the dimensions of the client area by calling GetClientRect. A call to GetBoardSpace(-1,-1) on the document will return the background color and FillSolidRect will fill the client area with that background color.

//  Create the brush for drawing
CBrush br;
br.CreateStockObject(HOLLOW_BRUSH);
CBrush* pbrOld = pDC->SelectObject(&br);

...

//  Restore the device context settings
pDC->RestoreDC(nDCSave);
br.DeleteObject();

Now it is time to draw the individual rectangles. This is accomplished by drawing a colored rectangle and then drawing a black outline around it. We now have to create a brush object to do the outline. The type of brush we are creating, HOLLOW_BRUSH, is called hollow because when we draw a rectangle MFC will want to fill in the middle with some pattern. We don't want this so we'll use a hollow brush so that the previously drawn colored rectangle will show through. Creating a brush allocates GDI memory that we have to later delete so that we don't leak GDI resources.

//  Draw the squares
for(int row = 0; row < pDoc->GetRows(); row++)
{
  for(int col = 0; col < pDoc->GetColumns(); col++)
  {
    //  Get the color for this board space
    clr = pDoc->GetBoardSpace(row, col);
    //  Calculate the size and position of this space
    CRect rcBlock;
    rcBlock.top = row * pDoc->GetHeight();
    rcBlock.left = col * pDoc->GetWidth();
    rcBlock.right = rcBlock.left + pDoc->GetWidth();
    rcBlock.bottom = rcBlock.top + pDoc->GetHeight();
    //  Fill in the block with the correct color
    pDC->FillSolidRect(&rcBlock, clr);
    //  Draw the block outline
    pDC->Rectangle(&rcBlock);
  }
}

The nested for loops are very simple, they iterate row by row, column by column, getting the color of the corresponding board space from the document using the GetBoardSpace function from the document, calculating the size of the rectangle to color and then drawing the block. The drawing uses two functions, FillSolidRect() to fill in the colored portion of the block and Rectangle() to draw the outline of the block. This is what draws all of the blocks in the client area of our view.


The last function that we've inserted into the view is one to resize the window based on the dimensions of the game board. In later articles we'll allow the user to change the number of blocks and size of the blocks so this function will come in handy then too. Again we start by getting a pointer to the document followed by getting the size of the current client area and the current window.

//  Get the size of the client area and the window
CRect rcClient, rcWindow;
GetClientRect(&rcClient);
GetParentFrame()->GetWindowRect(&rcWindow);

Finding the difference between these two gives us the amount of space used by the title bar, menu and borders of the window. We can then add the differences back onto the size of the desired client area (# of blocks by # of pixels per block) to get the new window size.

//  Calculate the difference
int nWidthDiff = rcWindow.Width() - rcClient.Width();
int nHeightDiff = rcWindow.Height() - rcClient.Height();
//  Change the window size based on the size of the game board
rcWindow.right = rcWindow.left +
  pDoc->GetWidth() * pDoc->GetColumns() + nWidthDiff;
rcWindow.bottom = rcWindow.top +
  pDoc->GetHeight() * pDoc->GetRows() + nHeightDiff;
//  The MoveWindow function resizes the frame window

Finally the GetParentFrame function returns a pointer to the CMainFrame class that is the actual window for our game and we resize the window by calling MoveWindow.

GetParentFrame()->MoveWindow(&rcWindow);

Your game should now look similar to this:


Conclusion

In this article we've gone over some of the basics of the Microsoft Foundation Classes and the Document/View architecture that it provides. We've assembled a game board object that contains our game data and constructed a view that renders the data to the user. In our next article we'll go over event driven programming, respond to events from the user such as mouse clicks and finish up with a "playable" version of our game.



IMPLEMENTING THE ALGORITHM FOR DELETING BLOCKS


The Algorithm

The algorithm for deleting blocks is rather simple, start with the row and column and make sure there is an adjacent block with the same color. If so, change the color value to the background color. Then go in each direction and delete the adjacent block if it is the same. This process is repeated with each block recursively. Here is the DeleteBlocks function in its entirety.

int CSameGameBoard::DeleteBlocks(int row, int col)
{
  //  Make sure that the row and column are valid
  if(row < 0 || row >= m_nRows || col < 0 || col >= m_nColumns)
    return -1;
  //  Can't delete background blocks
  int nColor = m_arrBoard[row][col];
  if(nColor == 0)
    return -1;
  //	First check if there are any of the adjacent sides
  //  with the same color
  int nCount = -1;
  if((row - 1 >= 0 && m_arrBoard[row - 1][col] == nColor) ||
     (row + 1 < m_nRows && m_arrBoard[row + 1][col] == nColor) ||
     (col - 1 >= 0 && m_arrBoard[row][col - 1] == nColor) ||
     (col + 1 < m_nColumns && m_arrBoard[row][col + 1] == nColor))
  {
    //  Then call the recursive function to eliminate all 
    //  other touching blocks with same color
    m_arrBoard[row][col] = 0;
    nCount = 1;
    //  Recursive call for up
    nCount +=
      DeleteNeighborBlocks(row - 1, col, nColor, DIRECTION_DOWN);
    //  Recursive call for down
    nCount +=
      DeleteNeighborBlocks(row + 1,col, nColor, DIRECTION_UP);
    //  Recursive call for left
    nCount +=
      DeleteNeighborBlocks(row, col - 1, nColor, DIRECTION_RIGHT);
    //  Recursive call for right
    nCount +=
      DeleteNeighborBlocks(row, col + 1, nColor, DIRECTION_LEFT);
    //  Finally compact the board
    CompactBoard();
    //  Remove the count from the number remaining
    m_nRemaining -= nCount;
  }
  //  Return the total number of pieces deleted
  return nCount;
}

The first sections are to ensure that the row and column are valid and that the block selected isn't already a background block. Next comes a check if there is at least one adjacent block with the same color above, below, left or right of the block. If there is then the selected block is set to the background color (0) and the count is set to one. This is followed by four calls to DeleteNeighborBlocks(). The first call passes the row above (row - 1) in the same column with the color and then DIRECTION_DOWN. We pass this in because it tells the recursive function to skip the down direction because that is where the execution came from. This just cuts off a few extra steps and speeds the process up. This could be left out and the algorithm would still work correctly, but a little less efficiently. Once all four directions are checked the board is compacted and the number of blocks that were removed is subtracted from the total number of blocks remaining.


The DeleteNeighborBlocks function is very similar to the DeleteBlocks function. Again the first few lines make sure that the row and column are valid and that the specified block is the same color as the original block. After that we make three recursive calls to delete neighbors. We use the direction argument to decide which direction we came from and then skip this direction. This is just a little optimization that eliminates a frivolous recursive call.

int CSameGameBoard::DeleteNeighborBlocks(int row, int col, int color,
                                         Direction direction)
{
  //  Check if it is on the board
  if(row < 0 || row >= m_nRows || col < 0 || col >= m_nColumns)
    return 0;
  //  Check if it has the same color
  if(m_arrBoard[row][col] != color)
    return 0;
  int nCount = 1;
  m_arrBoard[row][col] = 0;
  //  If we weren't told to not go back up, check up
  if(direction != DIRECTION_UP)
    nCount +=
      DeleteNeighborBlocks(row - 1, col, color, DIRECTION_DOWN);
  //  If we weren't told to not go back down, check down
  if(direction != DIRECTION_DOWN)
    nCount +=
      DeleteNeighborBlocks(row + 1, col, color, DIRECTION_UP);
  //  If we weren't told to not go back left, check left
  if(direction != DIRECTION_LEFT)
    nCount +=
      DeleteNeighborBlocks(row, col - 1, color, DIRECTION_RIGHT);
  //  If we weren't told to not go back right, check right
  if(direction != DIRECTION_RIGHT)
    nCount +=
      DeleteNeighborBlocks(row, col + 1, color, DIRECTION_LEFT);
  //  Return the total number of pieces deleted
  return nCount;
}

At this point the adjacent, same colored blocks have been eliminated and changed to the background color so all that is left is to compact the board by moving all of the blocks down and the columns to the left.

void CSameGameBoard::CompactBoard(void)
{
  //  First move everything down
  for(int col = 0; col < m_nColumns; col++)
  {
    int nNextEmptyRow = m_nRows - 1;
    int nNextOccupiedRow = nNextEmptyRow;
    while(nNextOccupiedRow >= 0 && nNextEmptyRow >= 0)
    {
      //  First find the next empty row
      while(nNextEmptyRow >= 0 &&
            m_arrBoard[nNextEmptyRow][col] != 0)
        nNextEmptyRow--;
      if(nNextEmptyRow >= 0)
      {
        //  Then find the next occupied row from the next empty row
        nNextOccupiedRow = nNextEmptyRow - 1;
        while(nNextOccupiedRow >= 0 &&
              m_arrBoard[nNextOccupiedRow][col] == 0)
          nNextOccupiedRow--;
        if(nNextOccupiedRow >= 0)
        {
          //  Now move the block from occupied to empty
          m_arrBoard[nNextEmptyRow][col] =
            m_arrBoard[nNextOccupiedRow][col];
          m_arrBoard[nNextOccupiedRow][col] = 0;
        }
      }
    }
  }
  //  Then move everything from right to left
  int nNextEmptyCol = 0;
  int nNextOccupiedCol = nNextEmptyCol;
  while(nNextEmptyCol < m_nColumns && nNextOccupiedCol < m_nColumns)
  {
    //  First find the next empty column
    while(nNextEmptyCol < m_nColumns &&
          m_arrBoard[m_nRows - 1][nNextEmptyCol] != 0)
      nNextEmptyCol++;
    if(nNextEmptyCol < m_nColumns)
    {
      //  Then find the next column with something in it
      nNextOccupiedCol = nNextEmptyCol + 1;
      while(nNextOccupiedCol < m_nColumns &&
            m_arrBoard[m_nRows - 1][nNextOccupiedCol] == 0)
        nNextOccupiedCol++;
      if(nNextOccupiedCol < m_nColumns)
      {
        //  Move entire column to the left
        for(int row = 0; row < m_nRows; row++)
        {
          m_arrBoard[row][nNextEmptyCol] =
            m_arrBoard[row][nNextOccupiedCol];
          m_arrBoard[row][nNextOccupiedCol] = 0;
        }
      }
    }
  }
}

First we go column by column moving things down. Starting at the bottom row (m_nRows - 1) we loop looking for the next empty row. Once that is found, there is another loop that searches for the next occupied row. Once these two are located then the next empty row is filled with the next occupied row. This process is repeated until there are no more blocks to move down.


The second part of the function is almost identical to the first part except for the outer for loop. The reason we can eliminate the outer loop is that we only have to look at the bottom row in each column, if it is empty then the whole column is empty and we can move something into its place.


Finishing Condition

There is only one step left to accomplish our task of creating a "playable" game. That is to implement the IsGameOver function. The function checks for a valid move which is to say it checks each block to see if there is an adjacent block with the same color. When the first one is found, the function short-circuits and returns false immediately. There is no need to continue checking. The only way to tell in the game is actually over is to actually do a full search and ensure that there aren't any moves left.

bool CSameGameBoard::IsGameOver(void) const
{
  //  Go column by column, left to right
  for(int col = 0; col < m_nColumns; col++)
  {
    //  Row by row, bottom to top
    for(int row = m_nRows - 1; row >= 0; row--)
    {
      int nColor = m_arrBoard[row][col];
      //  Once we hit background, this column is done
      if(nColor == 0)
        break;
      else
      {
        //  Check above and right
        if(row - 1 >= 0 &&
           m_arrBoard[row - 1][col] == nColor)
          return false;
        else if(col + 1 < m_nColumns &&
                m_arrBoard[row][col + 1] == nColor)
          return false;
      }
    }
  }
  //  No two found adjacent
  return true;
}

The two for loops allow us to search column by column and row by row for valid moves. Because we search left to right we don't have to check to the left for adjacent blocks of the same color. Searching from bottom to top eliminates the need to check below for valid moves. This order of searching also allows us to optimize the IsGameOver function a little further. Once the color of the block is the background color we can skip the rest of the column because anything above it will be empty also (thanks to the CompactBoard function).


You should now be able to play the full game to completion and it should look something like this:



Conclusion

In this article we've gone from a game that didn't do anything other than draw the game board to a playable version of the SameGame. We discussed event driven programming as well as how to implement that using MFC. We created an event handler for the left mouse click and responded with the main game algorithm. In the next article we'll be adding more features to our game through the menus.



Source: Cprogramming.com

0 comments

Comments


bottom of page