"The Same Game" - A Simple Game from Start to FinishBy: Ben MarchantThe AlgorithmThe 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 ConditionThere 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: ConclusionIn 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 Code from Part 2Continue to Part 3: Adding new Difficulty Levels |