||Potts, Summer 2000|
|Assigned||Thursday, July 6, 2000|
|Purpose||Text-Mode Graphics, Timer and Mouse Interrupts|
|Checkpoint Date||July 12 - 5:00pm|
|Due Date||July 20 - 5:00pm|
Who says you need windows to play a good game of Mines? Whoever it was, they're wrong. Here we will implement a game of Mines, much like the Windows MineSweeper. It will have two modes: Beginner and Advanced, defined as 10x10 and 30x30 playfields. To support this on the same text screen, the Beginner field must be stretched so each Mine square is 3x3 screen characters.
Hopefully you already know how Mines is played. If not, take an opportunity to try out MineSweeper, in Start>Programs>Accessories>Games. The playfield is full of occasional mines, which you have to locate. Your only clue is when you are next to one or more mines, you know how many are in adjacent squares. Using only this information, you can locate all mines in virtually any randomly generated minefield.
For this MP, you will be implementing a version of this game in text mode video. You will also support the mouse: right-clicking a location will cycle between No Idea, Mine, and Not Sure; left-clicking will check the spot for an actual mine -- if there is, then you lose.
Callbacks and Interrupt Service Routines
Hardware Interrupts are generated when various hardware events occur. Examples include the timer, which triggers 18.2 times per second, the keyboard, which triggers for every key press and release, and the mouse which triggers for any movement or button press or release. These interrupts are the hardware's way of saying "I need attention," so that the rest of the time the CPU can completely ignore such devices, and just execute code.
When the interrupt happens, the CPU is informed. The current opcode finishes execution, and then the current program is interrupted. This is when your Interrupt Service Routine (ISR) is executed. As it happens inbetween effectively random lines of your program, it needs to save all registers, including the flags. The hardware takes care of half of this (thus pushing of the flags); the other half you must take care of by using IRET instead of RET (to pop the flags).
But watch out - this wonderful piece of advice is only necessarily useful when you are directly writing the ISR. In the case of the mouse, you are writing a callback routine (http://www.ece.uiuc.edu/ece291/class-resources/helppc/interrup.txt.html has some good information on this, if you search for mouse, or interrupt 33h) so you only need to preserve your registers. The actual ISR which is calling your callback will preserve the flags, and call your procedure as a FAR procedure.
The timer will be used to keep track of how long the player has been playing. This is generally used to then form a high score table, but we'll just use it to report back to the user. Every time your timer ISR is called, increment the tick counter, and then use it to calculate the total time. Once calculated, display it to the screen using
BINASCand a custom implementation of
DSPMSGthat draws directly to the screen (in
TimerISR). While technically incrementing the second counter for every 18 ticks will yield an incorrect time, implementing this truly correctly is optional.
When your mouse handler is called, some important information will be in the registers. Unfortunately, it's not quite in the format you want. While you can take the mouse button status
DXneed to be changed. The ISR will supply you with the pixel positions of the mouse cursor, but it only draws the cursor in increments of text characters, which are 8x8 pixels in the 80x50 character mode we are using. Not only that,
HandleInputroutine expects the X and Y coordinates to be relative to the upper-left corner of the board, which is not the same as the screen's upper-left corner.
The Field (Internal)
To keep track of the data in this game of mines, we have created two arrays. The first, called
PlayField, is a series of spaces, words, and lines that represent everything on the screen except the actual mines - think of it as equivalent the window border. Since we carefully do not modify any of this area during the remainder of the program, we can display this once and be done with it. Display it with the attributes as defined as PLAYFIELD_ATTR.
The second array is a 30x30 array, called
MineField, defined as
EMPTY. What is
EMPTYthough? If you look at the Constants section, we have declared quite a lot of them this time:
EMPTY EQU 0
REALMINE EQU 16
VISIBLE EQU 32
GUESS_MINE EQU 64
GUESS_MAYBE EQU 128
If you consider this as a bitfield within the byte, we have several separate options:
You need all of these because a given location can be a mine, or not, can be visible or not, and if invisible, it can have one of three attributes set: no guess, guess of a mine or unknown. The lower four bits are used to make our life easier later on - when displaying the number of mines, or recursing to display the entire empty area - by holding a nybble sized number. Beware before you start adding this number to '0' that you have limited it to 0-15 by somehow chopping off the high bits.
For example, an undisplayed mine that hasn't been guessed at will hold
0001xxxxb; a displayed non-mine with 3 adjacent mines will hold
xx100011b; and an undisplayed non-mine with 5 adjacent mines, guessed as a mine will hold
The Field (External)
The player will not want to read the memory locations directly. Not only would it be a pain, he could see from the beginning where the mines were, and the game would no longer be a game. To make it actually entertaining, the board must be drawn to the screen. Drawing it properly, you will find, is a complicated procedure. These are the steps you must implicitly follow. although turning this into code is for you to do:
- If the
VISIBLEbit is set
- If it is a mine
- Use attributes
MINE_ATTRas the background and foreground attributes
- Use character
- If it is not a mine
- Use attribute
UNCOVERED_ATTRas the background attribute
- If it has no neighboring mines
- Use a space as the character
- If it has neighboring mines
- Use attribute
NUMBER_ATTRas the foreground attribute
- Use the number of mines as the character
- If the
VISIBLEbit is not set
- Use attribute
COVERED_ATTRas the background attribute
- Use the corresponding
_CHARfor the foreground attribute and character, or space if neither apply
- If it is not the center of the 3x3 mine-block in Beginner mode, use spaces for the character, and use the corresponding background attribute.
One suggestion for later debugging is to override the visible bit in your DrawField routine, thus enabling you to verify your map was properly generated. It has limited use, however, so you are encouraged to find more methods for debugging your errors. Codeview can be used, but you must invoke it with the
/margument to tell it not to use the mouse since your program needs it:
CV /50 /m mp3.
playmode- use the appropriate constant
randto a chosen value and do not call
MineFieldaccurately reflects the number of adjacent mines. This is the hardest (or at least the most annoying) part of this procedure.
randset to the current tick count
DSfor this as it just makes things complicated.
PLAYFIELD_ATTRconstant as your attribute
updateto 1 if the
TimerSecwas incremented; do not increment
MouseClick(just a copy of the relevant byte)
MouseX(in characters from the upper-left of the board)
MineFieldto the screen so the player can see it.
TimerSec) and Mines remaining (as stored in
Mines) to the screen
updateis 1 to avoid flicker
MouseClick, MouseX, MouseY, MineField
MineField, MineCount, MouseClick=0, AX=GameStatus
MouseYare valid in the given
MouseCallbackdoesn't check if you are within the
GUESS_MINEis set; ignore Right Clicks if
RevealSquaresshould be called
GUESS_MAYBE] in that order, updating
MineCountif it changes to
GUESS_MINE; increment when changing from
TotalSquares; no other condition is correct or necessary, but note that if MineCount is not 0, you must go through your array and mark all
MineField, AX=MouseY, BX=MouseX
MineField, Update, UncoveredCount
UncoveredCount, and set
RevealSquareson the eight adjacent squares
MouseClick) or ESC
MineField, AX(as returned by HandleInput)
GUESS_MINEbit on each
REAL_MINElocation and set
MineCountto 0 before calling
DrawScreenif the player won
VISIBLEbit on each
REAL_MINElocation before calling
DrawScreenif the player lost
MouseClickin a loop that also uses the construct in
MAINto poll for ESC