3.3. Example: Some Simple Glass Bead GamesWe illustrate the basic concepts with two simple games that are actually variations of one game. The game pieces are glass beads that the user must move around to achieve certain arrangements. ("Google" the phrase glass bead game, or, even better, the German version Glasperlenspiel for the rich lore on the topic.) The picture below shows the initial arrangement of the glass beads in the two games:
The goal is to exchange the positions of the red and blue beads with as few moves as possible. Beads can be moved by dragging them with the mouse to an open cell that is next to the cell they occupy horizontally or vertically. In the game on the right diagonal moves are also allowed. Beads can traverse the green areas as long as they are "held" by the mouse. We set aside for the moment the details of rendering the images and focus instead on the manipulation of the beads by the user. We need to implement four message handlers: when the mouse button (or equivalent) is pushed down, when it is released, when the mouse moves, and the display method that is called when the window needs to be redrawn. We assume that a class CBead has been defined for the glass beads and that it includes a member m_pos that it is the smallest rectangle containing the bead. (See Bead.h and Bead.cpp for a simple implementation of the class, the picture above has been produced by a fancier implementation.) The array m_bead[] contains all the bead objects, m_N of them. For this game we also define a class CBox that is a rectangle with a member, m_content, that contains the index of the bead in that cell. If m_content is negative, the cell is unoccupied. (In this implementation CBox is an extension of the MFC class CRect.) We have an array m_Box[] of class CBox that contains all the cells (black tiles or discs). A member of the game class, m_tile, determines which version is being played. If set to true we have the game displayed on the left, otherwise we have the game on the right. Most of the code is not affected by the choice. Here is the message handler when the user presses the (mouse) button.
// Listing A
void CGame::ButtonDown(CPoint point)
{
m_hit = -1;
for(int i=0; i<m_N; i++) {
if(m_Box[i].m_content < 0) continue; // empty box
if(m_bead[m_Box[i].m_content].m_broken == TRUE) continue;
if(m_Box[i].PtInRect(point)==TRUE) {
m_hit = m_Box[i].m_content;
m_Box[i].m_content = -1;
m_old = i; // save the index for later use
}
if(m_hit >= 0) return;
}
}
m_hit is a member of the CGame class that is by default to, say, -1, but when the user picks a bead is set to the index of the latter. The above code uses a method of CBox that checks whether the cursor (point) is within the area if the rectangle surrounding the cell. The attribute m_broken is used to mark a bead that is no longer at play. When the mouse moves, the only thing we need to do in this simple game is to redraw the screen. However redrawing the whole screen all the time causes unpleasant visual effects, so we need to limit the redrawing. Here is a possible way of doing this.
// Listing B
void CGame::DrawMove(CPoint p)
{
CRect runion, r[2];
r[0] = m_bead[m_hit].m_pos; // old bead position
m_bead[m_hit].MoveTo(p);
r[1] = m_bead[m_hit].m_pos;// new bead position
runion.UnionRect(r, r+1);
DrawArea(&runion);
}
void CGame::MouseMove(CPoint point)
{
if(m_hit >= 0) DrawMove(point)
}
If a bead has been selected (m_hit >= 0) we simply redraw the part of the screen that contains the old and new position of the object that is being dragged by the user. For each of the two positions we copy the rectangle enclosing the object and then we find the union of these rectangles that is actually the smallest rectangle enclosing both positions. The implementation of the functions UnionRect() (of the CRect class) and MoveTo() (of the CBead class) are quite simple so the code of DrawMove() is quite general. Any dependence on the platform and the specific object is hidden within the DrawArea() function. The most challenging implementation is for handling the message of button up, when the user "drops" the bead in a new spot. At that time we must check whether the new position is legal and whether the goal of the game has been achieved.
// Listing C - part 1
void CGame::ButtonUp(CPoint point)
{
if(m_hit < 0) return;
int i = ValidLocation(point);
if(i < 0) {
WrongMove(i);
StartGame(NULL);
}
else {
m_move++;
DrawMove(m_Box[i].CenterPoint()); // center bead
m_hit = -1;
if(CheckGoal()==TRUE) StartGame(NULL);
}
}
The code uses four methods of the game object. ValidLocation() checks whether the bead has been dropped in an empty cell in which care returns the index of the cell, otherwise it returns a negative integer that may be used to indicate the reason the location is not valid. The following is one implementation of the method.
// Listing C - part 2
BOOL CGame::TooFar(int i)
{
int xold = m_old%m_n; int yold = m_old/m_n;
int xnew = i%m_n; int ynew = i/m_n;
if(m_tiles== TRUE) return abs(xnew-xold) + abs(ynew-yold) > 1;
else abs(xnew-xold)>1 || abs(ynew-yold)>1;
}
int CGame::ValidLocation(CPoint point)
{
for(int i=0; i<m_N; i++) {
if(m_bead[m_hit].InsideRect(&(m_Box[i])) == TRUE) {
if(TooFar(i) || m_Box[i].m_content >= 0) return -2;
m_Box[i].m_content = m_hit;
return i;
}
}
return -1;
}
The method WrongMove() displays a message for the user, depending on the reason, and the next statement restarts the game. (The NULL argument in the StartGame() method means that we keep the same parameters for the game.) CheckGoal() is the method that determines whether the game has been won by the player and one possible implementation is given below.
// Listing C - part 3
BOOL CGame::CheckGoal()
{
if( m_Box[0].m_content == m_N-1 && m_Box[m_N-1].m_content==0 ){
/* display message */
return TRUE;
}
else return FALSE;
}
In this game the goal is simple, but in more complex games, this method is the one that needs to be modified the most. Finally we must implement the drawing function. The core of it is shown below.
// Listing D
void CGame::Display(CDC *pDC)
{
/* ... */
if(m_tile==TRUE) for(int i=0; i < m_N; i++) pDC->Rectangle(m_Box+i);
else for(int i=0; i < m_N; i++) pDC->Ellipse(m_Box+i);
for(int i=0; i < m_N; i++) m_bead[i].Display(pDC);
/* ... */
}
The Display() method checks the value of m_broken and draws the bead accordingly. The game we just described is a special (and simple) case of what is called 15 puzzle that dates back to the 19th century. Most version express it in terms of tiles, so only horizontal and vertical moves are allowed. You can find several references if you "Google" the words 15 puzzle or you can look at some major sites that deal either with numerical versions (Wikipedia, MathWorld, "Cut-the-Knot") or with forming illustrations (for example, in the Everett Kaser site). Changing the simple games of this section to anyone of those requires mainly to change the game logic part that is contained in the method CheckGoal(). The bead display function has also to be changed (to make it show a number, for example). Very little, if anything has to be changed in the rest of the program.
|