The Making of MineSweeper

From EDM2
Jump to: navigation, search

Written by David Charlap

Introduction

This document describes some of the design decisions and problems that I encountered when making my MineSweeper for OS/2 program. It assumes a basic knowledge of C and Presentation Manager.

I decided to write an OS/2 version of MineSweeper for many various reasons. The main reason was to learn a bit more about programming in the OS/2 presentation manager environment. I also wanted an OS/2 version of this game, since loading the Windows environment is a slow procedure. Although there is already a version of MineSweeper for OS/2 available, I found it to be lacking features of the Microsoft game that I wanted.

I chose to develop MineSweeper using the GCC/2 compiler, since I already had it installed on my computer at home. The code I have written should compile without change on IBM's C-Set/2 compiler or on the EMX/GCC compiler or on any other OS/2-compatible compiler.

The Game

Before discussing the design, allow me to explain the basic concept of the game.

MineSweeper is a game of logic, pitting the player against a clock. A grid of squares is presented, some of which contain mines. All the squares are covered at the game's outset. The object is to discover which covered squares contain mines under them using clues provided by the program.

A player uncovers a square by clicking on it with the mouse. If the square contains a mine, the player loses and play stops. If it does not contain a mine, a number is displayed on the square which indicates the number of mines that are adjacent to that square. When all the squares that do not contain mines are uncovered, the player wins and play stops.

To prevent mistakes, a player may place a flag on a covered square to indicate a suspected mine position. The computer will not allow a flagged square to be uncovered. The player may also place a question mark on a square to aid in visualizing the problem. The computer will allow a question-marked square to be uncovered.

The game is timed.

The Implementation

While there are many ways to implement such a game, I chose to copy the implementation that Microsoft used for their game. Partly because I was already familiar with it, and partly because it is an intuitive approach to the game.

How the Game Should Behave

The game is played in a window whose size is directly proportional to the size of the game grid. When the grid's dimensions are changed, the window is re-sized to accommodate the new grid. The top of the window contains the current elapsed time, a count of the number of mines left to be flagged, and a button that may be clicked for a quick restart of the game. Below this is the game grid.

Covered squares are drawn in a way to resemble buttons.

A user uncovers a square by clicking on it with mouse button 1. When the button is depressed over a covered square, the square is drawn as a depressed button. If the mouse is dragged over the game grid, the "depressed" button will follow the pointer. When the button is released, the square beneath it will be uncovered.

If an uncovered square has no adjacent mines, the program will automatically uncover all squares adjacent to it.

Flags and question marks are placed by clicking on covered squares with mouse button 2. The marks are placed as soon as the button is pressed.

As a shortcut, and an aid to solving, mouse button 3 is used to uncover all the squares around an uncovered square. When button 3 is clicked on an uncovered square, and there are sufficient flags surrounding the square to satisfy the number shown, all remaining squares will be uncovered. For two-button mouse users, pressing buttons 1 and 2 together (chord) will also cause this effect.

When the shortcut button is pressed, the squares surrounding the pointer will be drawn in the depressed position. As the mouse is dragged, the 3x3 area of depressed squares will track the mouse. When button 3 (or either button 1 or 2, if the chord is used) is released, the shortcut process will begin.

How to Code This

Coding this game play was not simple. My first attempt involved creating separate buttons for each square on the playfield. This way, the user could just click on a button, and the program would receive a WM_COMMAND message indicating which button was clicked.

This proved difficult for many reasons. First, it is hard to get a bitmap image on the face of a button. Although I have seen it done, I do not know how, and I did not want to take the time to learn. Second, and more important, buttons to not behave properly. Ordinary buttons revert to their "up" state if the mouse moves off of them while the mouse button is depressed, but there is no facility provided to then depress the adjacent button that the pointer is subsequently above. Also, facilities are not provided to have more than one button draw itself on the "down" state when one is clicked on.

In other words, getting the depressed button images to follow the mouse as it is dragged across the playfield is difficult to do, if not impossible, using button windows.

Instead, I decided to fake it. I would have multiple bitmaps in memory. One for each possible image that could be drawn on a grid square. I would then just draw the bitmaps on the squares. If the bitmaps are properly drawn, they should look just like buttons. For making the buttons depress, I would simply have additional bitmaps and draw them where appropriate.

There are only a few downsides to this approach. First of all, bitmaps can not be stretched without introducing distortions. This means that whatever size I pick for them is going to be used throughout the game, regardless of user preferences. Second, bitmaps are difficult to recolour when loaded out of resource files, so the colours will not necessarily match the system-defined button colours. But these are minor concerns, I feel, given the difficulty of implementing the game in any other way.

The Basic Internal Structure

Many variables are needed to keep track of the game. I chose to make most of the critical game variables global, so that they can be accessed from any procedure without large amounts of parameter passing. While this may be poor programming practice, it makes a game like this much easier to write and understand, in my opinion.

In addition to the expected variables, like the size of the grid and the number of mines, three arrays are used. The arrays are dynamically allocated and reallocated whenever the game grid changes size. All three arrays correspond to the game grid, one element corresponding to each square.

One array is boolean, containing the map of mines - TRUE if the square has a mine, FALSE otherwise.

The second array contains the visible screen. Each element contains a number, indicating what the user sees at that coordinate. This array is initialized to values corresponding to a blank raised button, but will change as play progresses. This is used primarily for displaying the screen.

The third array contains the count of mines adjacent to each square. Rather than compute the number each time a square is uncovered, the entire set is computed in advance. This way, it's a quick operation to get the correct number when a square is uncovered.

Other game variables include an array of bitmap handles for the drawing procedures, flags to an end-of-game condition, and the current timer count.

Using Bitmaps as Buttons

To implement the game grid, 18 bitmaps would be needed. I would need a blank square, squares with the numbers 1 through 8 on them, a "button" in the raised and depressed state, a "button" with a flag on it, a "button" with a question mark on it in the raised and depressed state, and bitmaps to indicate errors: an exploded mine, a misplaced flag, and an unflagged mine.

All these bitmaps were drawn (on 16x16 16 colour bitmaps) using the system-supplied icon editor. These were then all included in the resource file with lines like the following in mine.rc:

BITMAP MINE_BLANK_UP      MineBlankUp.BMP
BITMAP MINE_BLANK_DOWN    MineBlankDown.BMP
BITMAP MINE_FLAG          MineFlag.BMP
BITMAP MINE_QUESTION_UP   MineQuestionUp.BMP

Where names like MINE_BLANK_UP are constants defined in the program's header file.

Using bitmaps from a program's resources is fairly simple. First, a presentation space is required for drawing into. While a WinBeginPaint call will create a presentation space, I want to be able to draw bitmaps at times other than during WM_PAINT message processing. For this reason, I create a presentation space during processing of the WM_CREATE message and store its handle in a global variable. A presentation space may be created outside of paint message processing in the following way:

 hdc = WinOpenWindowDC(hwnd);
 sizl.cx = sizl.cy = 0;
 hps = GpiCreatePS(hab,
                   hdc,
                   &sizl,
                   PU_PELS |
                   GPIF_DEFAULT |
                   GPIT_MICRO |
                   GPIA_ASSOC);

Where hwnd is the window handle passed as part of the WM_CREATE message, sizl is a variable of type SIZEL, and hab is the anchor block created as part of the program's WinInitialize function. The value of hps returned is used for all painting operations throughout the program. It is also passed to WinBeginPaint during WM_PAINT message processing to prevent spurious presentation spaces from being created.

Once the presentation space exists, the bitmaps are all loaded from the program's resources with GpiLoadBitmap calls during the WM_CREATE message processing:

 hbitmap=GpiLoadBitmap(hps,
                       NULLHANDLE,
                       ID,
                       BOX_WIDTH,
                       BOX_HEIGHT);

Where ID is the resource-ID for the bitmap, as defined in the resource file, and BOX_WIDTH and BOX_HEIGHT are constants indicating the size of the bitmap. If the height and width of the bitmaps do not match the constants provided, they will be stretched to fit the dimensions requested. The bitmap handles returned in hbitmap are all stored so that they do not need to be loaded again.

Once handles are available for the bitmaps, displaying them is simple, using the WinDrawBitmap call:

 WinDrawBitmap (hps,
                hbitmap,
                NULL,
                &ptl,
                CLR_NEUTRAL,
                CLR_BACKGROUND,
                DBM_NORMAL);

Where hbitmap is the handle of the bitmap requested, and ptl is a POINTL structure containing the window-relative coordinate of the lower-left corner of the bitmap's target position.

The only other thing to consider is cleaning up when the program terminates. Although presentation spaces and handles will be released back to the system when the WinTerminate call is placed, it is good practice to release them manually, just to be sure nothing is left behind by accident. (Bugs in new systems, like OS/2 can cause this to happen, so it's better safe than sorry.) I place all cleanup instructions in the WM_DESTROY message handler. The following statements will delete bitmap handles and presentation spaces:

GpiDeleteBitmap(hbitmap);
GpiDestroyPS(hps);

Where hbitmap is a valid bitmap handle and hps is the presentation space created earlier. The GpiDeleteBitmap function should be called repeatedly, once for each bitmap handle used in the program.

Using the Mouse

Another significant problem of implementing the desired interface is that of getting a depressed "button" to track the mouse. For example, if the pointer is moved from one square to another while button 1 is pressed, the depressed button image on the screen should follow it.

For reasons outlined previously, normal buttons won't provide this effect. Instead, using bitmaps, the entire effect must be simulated by handling appropriate messages. To properly implement button 1's behaviour, we must handle the following messages: WM_BUTTON1DOWN, WM_BUTTON1UP, and WM_MOUSEMOVE.

WM_BUTTON1DOWN is generated whenever mouse button 1 is pressed. This may be the left or right button, depending on whether OS/2 is configured for left-handed mouse operation or not. Since we want the left-right buttons to swap when in left-handed operating mode anyway, we can simply deal with button 1 and not concern ourselves with left or right.

The WM_BUTTON1DOWN message provides the coordinates (window-relative) that the pointer is over when the button was pressed. These are delivered as the high and low words of the first message parameter and may be extracted with the following macros:

x = SHORT1FROMMP(mp1);
y = SHORT2FROMMP(mp1);

Before taking action, we must be sure these coordinates are valid. Any time the button is pressed while the pointer is within the window's client area will cause a WM_BUTTON1DOWN message to be sent, and these coordinates may not be over a segment of the game grid.

If the coordinates are invalid, we simply break from the switch statement, and allow the WinDefWindowProc to handle the message.

If the coordinate is valid, we then compute which grid square the coordinate is over. Since the grid is composed of same-sized rectangular regions, we have simply to subtract any margins and divide the coordinate by the width (or height) of a square to get the identity of the square.

Once the square is identified, we redraw it in its depressed form. In general, the depressed form of a square is identical to it's non-depressed form. The exceptions are the blank covered square and the question-mark covered square. These are depressible "buttons", and have different bitmaps for the depressed state and the raised state.

A few other steps must also be taken, aside from drawing the depressed button. We must set a flag in a static variable indicating that the mouse button is down, so that the WM_MOUSEMOVE message handler will know the mouse button's position. We must also store the grid coordinate of the depressed button.

Finally, we must capture the mouse. When the mouse is captured, the program will get all mouse messages, even if the pointer is over some other application's space. This is necessary, since the user may drag the pointer outside of our window and release the button there. If this happens, and the mouse isn't captured, the program will not receive the WM_BUTTON1UP message, and would think that the button is still down.

The mouse is captured and released with the following functions:

 /* To capture the mouse   */
 WinSetCapture (HWND_DESKTOP,
                hwnd);
 /* To release the capture */
 WinSetCapture (HWND_DESKTOP,
                NULLHANDLE);

Where hwnd is the window handle provided to the message handler.

The WM_BUTTON1UP message is returned when the user releases mouse button 1. Compared to the button down handler, this is simple. First, the capture on the mouse is released. Then, if the coordinate is over a covered-but-not-flagged square, we call the uncover function.

If this is the first click of the game, then the timer is started.

The WM_MOUSEMOVE message is sent to the application whenever the pointer position changes over the window. Normally, we will ignore these, passing them to the system's handler, WinDefWindowProc, but if button 1 is down, we want to take action first.

First, we must check if the pointer's position has moved to another square. If it is over the same square as our saved position, then we do nothing. If it is not over the same square, then we have to draw the previous square in it's raised position, and draw the square it's now over in the depressed position and save the new coordinate.

Managing the mouse button 2 is simpler, since flags and question marks are placed as soon as the button is depressed. When the WM_BUTTON2DOWN message is received and the pointer is over an appropriate square, the array element corresponding to the square is changed and the square is redrawn.

Managing the chord and button-3 sequences are similar to the button 1, except that nine boxes must now be drawn in the depressed state instead of one. This only gets a little ugly when the button 1 and button 2 messages must do double-duty to manage the chord sequence as well as their individual functions, but this is managed easily with some well-placed if() statements.

Uncovering Squares

Uncovering a square seems like a simple procedure, but introduces interesting problems. The concept is simple. The program checks if the square is uncoverable and aborts if it is not. It then checks if a mine is being uncovered and ends the game if it is. Then it does the actual uncovering, ending the game if the player wins.

In actuality, it's more complicated than this. As a courtesy, the program should never allow the user to uncover a mine on the first click, so if a mine is uncovered on the first click, the program should move that mine elsewhere. Also, if a square has no mines adjacent to it, the program should automatically uncover all the surrounding squares.

Step one is simple. Check if the square is covered and does not have a flag on it. If not, just return and do nothing. There should also be some sanity checking here, in case the program tries (in error) to uncover a square that isn't on the grid.

For step 2, the program must check if the square has a mine under it. If so, then the program must check if it is the first click. Moving the mine is tricky - the program must place a new mine, delete the old mine, and adjust the adjacency counters so all the numbers will still read proper values. Finally, it must then loop back to the start and retry uncovering the square.

If it's not the first click, the finding a mine is game over. The timer is stopped and the endgame flag is set, which effectively prevents further play, since all of the mouse button message handlers check if the game is over as a part of their processing.

If the square does not have a mine, however, it must be uncovered. In which case, the count of adjacent mines is fetched. If there are one or more adjacent mines, the bitmap for that number is assigned to that square, and it is displayed.

If the uncovered square has zero adjacent mines, all the adjacent squares must be uncovered. At first, I simply had the uncover procedure call itself recursively, but I found that very large amounts of empty space would cause stack overflow problems, so I had to use an iterative solution, instead. I have a separate procedure to handle this, now.

To iteratively calculate which squares to uncover, the program loops through all the squares. If it finds a covered square that is adjacent to an uncovered square with no adjacent mines, it sets the square to its uncovered state without calling the uncover procedure. It loops through the grid repeatedly until no changes are made to the grid.

The program maintains a count of covered squares at all times. The count is initialized to the number of squares in the grid and is decremented whenever a square is uncovered. When the count of covered squares equals the number of mines, a win situation is declared.

When the player wins, the timer is stopped, the endgame flag is set, and (if one of the three standard games is chosen) the score is compared to the high score, and if the high score is beaten, the score is recorded and the player may enter his name.

Saving and Restoring Settings

MineSweeper saves all critical game settings to an initialization file (MINE.INI) when it terminates and restores these settings from this file when restarting. The window's position, the high scores, and the game settings are among the items saved. This is all done using OS/2's profile management system.

The profile management system is a set of PM calls (all beginning with Prf) that manage INI files. An INI file is opened by calling PrfOpenProfile, closed by calling PrfCloseProfile, and is read from and written to with other API calls.

The calls I used, and the syntax for them are:

 hini = PrfOpenProfile (hab, "MINE.INI");
 PrfCloseProfile(hini);
 PrfWriteProfileData(hini, pszApp,
                     pszKey, &data,
                     sizeof(data));
 PrfWriteProfileString(hini, pszApp,
                       pszKey, pszString);
 PrfQueryProfileData(hini, pszApp,
                     pszKey, &buf, &buflen);
 PrfQueryProfileSize(hini, pszApp,
                     pszKey, &buflen);
 PrfQueryProfileString(hini, pszApp, , pszKey,
                       NULL, pszString, buflen);

PrfOpenProfile takes two arguments. The first is the anchor block that is created when WinInitialize is called. The second is the file name of the INI file. A variable of type HINI is returned. This HINI variable is used to reference the INI file for reading and writing.

Two preset HINI handles are also available: HINI_USERPROFILE and HINI_SYSTEMPROFILE may be used to access the user and system INI files, which are normally OS2.INI and OS2SYS.INI. I chose not to use these files, however, since software is required to delete entries from INI files. By keeping data in a separate initialization file, a user may erase all settings by simply deleting the MINE.INI file.

PrfCloseProfile takes one argument: the HINI variable returned by the PrfOpenProfile call. It closes the profile. The user and system INI files are not closable.

Data in an INI file is referenced by two keys: and application name and a key name. These two keys, together, are used to reference arbitrary-sized blocks of data in the INI file. These blocks may be either strings or binary data.

Almost all functions that read or write an INI file take the same first three parameters. The first being a valid HINI_INI handle, the second being a string containing the application name, and the third being the key name.

PrfWriteProfileData is used to write binary data to an INI file. It takes five parameters. The first three are the standard handle, app name and key name. The fourth is a pointer to the data, and the fifth is the length of the data.

PrfWriteProfileString is used to write null-terminated string data to an INI file. It takes four parameters. The first three are the standard three, and the fourth is the string.

PrfQueryProfileData is used to read binary data from an INI file. It takes five parameters. The first three are the standard three. The fourth parameter is a pointer to a buffer area to hold the data, and the fifth is a pointer to a long variable containing the size of the buffer. When the call completes, this variable will contain the actual number of bytes transferred.

PrfQueryProfileString is used to read string data from an INI file. It takes six parameters. The first three are the standard three. The fourth is a default string that will be supplied if the key can not be found in the INI file; I leave this parameter as a NULL, indicating that I don't want a default string. The fifth parameter is a pointer to the buffer that will contain the string, and the sixth parameter is the maximum string length. The call will return the actual number of bytes transferred into the buffer.

Finally, since I am dynamically allocating storage for these strings, I must find out the length of these strings before I actually read them in, in order to allocate a large enough buffer first. This is done with the PrfQueryProfileSize API call.

PrfQueryProfileSize takes four parameters. The first three are the usual three, and the fourth is a pointer to a ULONG variable. This variable will contain the number of bytes that the referenced data block contains.

Sizing the Window

In MineSweeper, the mines are always the same size. And I do not want any margins around the minefield. This means that the window must be re-sized to fit the minefield.

This is a two step process. First, the proper size must be calculated, then the window must be re-sized to fit.

Calculating the window size is trivial, but not immediately obvious. Obviously, the width must be greater than the width of one row of squares, and the height must be greater than the height of one columns of squares. But this is not enough. The size of the window includes ALL of the window, including borders, menus, and title bars. So, the border width must by queried from the system and added to the width estimate. And the menu bar height, the title bar height, and the border height must also be queried and added to the height estimates.

These values can be extracted with the WinQuerySysValue function:

     menuHeight = WinQuerySysValue(HWND_DESKTOP,
                                   SV_CYMENU);
  captionHeight = WinQuerySysValue(HWND_DESKTOP,
                                   SV_CYTITLEBAR);
   borderHeight = WinQuerySysValue(HWND_DESKTOP,
                                   SV_CYBORDER) * 2;
    borderWidth = WinQuerySysValue(HWND_DESKTOP,
                                   SV_CXBORDER) * 2;

WinQuerySysValue takes two parameters. The first is a valid desktop handle, which (under OS/2 version 2.0) is always HWND_DESKTOP, and the second is a constant indicating which value to extract. The following were used:

SV_CYMENU
The minimum height for a menu bar. If the menu font is too large, a menu bar will actually become larger than this height. Additionally, if a menu bar is too long for the window, and wraps onto two or more lines, this value will only be the height of one line. In other words, it's not really very accurate.
SV_CYTITLEBAR
The height of a titlebar
SV_CYBORDER
The height of a thin border. Multiply this by two to get both borders.
SV_CXBORDER
The width of a thin border. Multiply this by two to get both borders.

There are many other system values, but these are the only ones I needed. The actual width of the window is the width of the window's contents plus the border width. Unfortunately, the ambiguities of the menu bar's height do not make it that simple to generate the window's height.

While there may be better ways to calculate the menu bar's actual height, I chose a quick-and-dirty approach. I first make a best guess of the window's height, by adding the system height values to the height of the playfield (consisting of the game grid and the score region). I then set the window to that size with the WinSetWindowPos command. After that I use the WinQueryWindowRect command to get the actual size of the menu bar. This works, because everything in OS/2 is a window - I get the window handle for the menu, and then query the window's size - giving me the size of the menu. Using this size, I recalculate the height of the window and re-size it if it has changed. This all happens quickly, and no painting occurs during processing, so the user does not see the window change sizes.

The window's size and position is set with the following call:

WinSetWindowPos(hwnd, HWND_TOP,
                x, y, cx, cy,
                SWP_SIZE | SWP_MOVE |
                SWP_ZORDER | SWP_SHOW |
                SWP_ACTIVATE);

Where hwnd is the window handle. The second parameter is for placing the window in the stack of open windows; HWND_TOP is a constant that tells the system to put this window above all the others. x, and y are coordinates for the lower-left hand corner of the window. cx, and cy are the horizontal and vertical sizes of the window. The last parameter is a set of flags. The ones presented here tell OS/2 to re-size the window, reposition it, change it's "stack" position, make it visible, and give it focus, respectively.

The menu bar's size is fetched with the following calls:

hwndMenu = WinWindowFromID(hwndFrame, FID_MENU);
WinQueryWindowRect(hwndMenu, &rcl);

Where hwndMenu is the window handle of the menu bar, hwndFrame is the window handle of the frame window that owns the menu bar, and rcl is a RECTL structure that contains the dimensions of the window.

WinWindowFromID is an API call that returns the window handle of a child window. In this case, I want to know the handle of a menu-bar window, but I only know the handle of the frame window. So I use WinWindowFromID to extract the handle. It takes two parameters. The first is the handle of the parent window, and the second is the ID of the child. Menu bars that are created as part of a standard window always have an ID of FID_MENU.

WinQueryWindowRect fills in a RECTL structure with the extents of a window. Since the coordinates are window-relative, the bottom and left extents are always 0 and 0, leaving the other two extents containing the width and height of the window.