Computer Science Canada Programming C, C++, Java, PHP, Ruby, Turing, VB   Username:   Password: Wiki   Blog   Search   Turing   Chat Room  Members
[Tutorial] Basic Game Programming: Minesweeper, Part 4
Author Message
DemonWasp

Posted: Fri Aug 21, 2009 7:20 pm   Post subject: [Tutorial] Basic Game Programming: Minesweeper, Part 4

Prerequisites
This is part 4 of the Basic Game Programming tutorial. Find the previous parts here: Part 1, Part 2 and Part 3; complete all of these first.

Detecting when a Player has Won or Lost
The conditions for when a player wins or loses are fairly simple in Minesweeper: the player loses if they ever uncover a mine, and win if they reveal the whole map correctly without exploding. We've already got code that will detect whenever a player explodes (this is in our main game loop), and it outputs "Boom, you're done!", then exits the game. This means we really only need to check for a player winning the game. First, we need to answer some important questions about how and when:

When do we need to detect whether a player has won or lost?
Well, a player could (theoretically) win on any click, whether they reveal a square or not, so clearly we'll have to check after every click, before continuing the game.

How do we check?
Well, we know what the underlying map looks like, and what the player's map-status looks like. We need to make sure that if the map at each square is MAP_MINE, then the player has USER_FLAGGED; if it's less than 10 (that is, either empty or a numbered square) then it needs to be USER_REVEALED.

We'll work on implementing the "how" part first. It's fairly easy to check that each square is correctly marked by the player. If the player has won, we'll have our new function return true...if they haven't won yet, it can return false. Remember, false doesn't imply that they've lost the game, just that they haven't won yet (losing is handled separately).

 Turing: % CHECK WHETHER THE USER has won yet. % Returns true if the user has revealed the entire map correctly; returns false otherwise. function check_for_win () : boolean     for x : 1..game_size_x         for y : 1..game_size_y             if map ( x, y ) = MAP_MINE then                 if user_state ( x, y ) not= USER_FLAGGED then                     result false                 end if             else                 if user_state ( x, y ) not= USER_REVEALED then                     result false                 end if             end if         end for     end for     result true end check_for_win

Now we need to insert a call to this function in our main loop, right after handling each click, and deal with the result from it. We put the following after the if statement dealing with mouse clicks:
 Turing: if check_for_win() then             Text.Locate ( 1, 1 )             put "YOU WIN!!"             exit         end if

Open Fields / Contiguous Empty Regions
Next, we need to handle what happens when a player clicks on an empty square - it reveals all adjacent squares, possibly opening up entire fields to the player. This makes the start of the game much more bearable, as you don't have to spend all your time opening up huge areas.

This is the most advanced part of this entire tutorial, so be forewarned: this requires a bit of unorthodox thinking. Specifically, we'll be using recursion. It's not as scary as it sounds.

The basic idea now is that whenever we "reveal" an empty square (by clicking on it or otherwise), it in turn reveals all squares next to it. If any of those squares are empty, they also reveal all nearby squares. If you think about it for a moment, it should become clear that this will do exactly what we need it to do - reveal all "fields". This naturally leads to this method:

 Turing: % REVEAL AN AREA of contiguous empty spaces % Called every time the user clicks on an empty space. procedure reveal_area( x, y : int)     user_state ( x, y ) := USER_REVEALED         for x_off : -1 .. 1         for y_off : -1 .. 1             if ( x_off not= 0 or y_off not= 0 ) and x+x_off > 0 and x+x_off <= game_size_x and y+y_off > 0 and y+y_off <= game_size_y then  % Don't check outside the game boundaries!                 if map ( x+x_off, y+y_off ) = MAP_EMPTY and user_state ( x+x_off, y+y_off ) not= USER_REVEALED then     % Don't reveal areas that are already revealed - think about this: why is this necessary? Try removing the second half of this if and running the program!                     reveal_area ( x+x_off, y+y_off )                 else                     user_state ( x+x_off, y+y_off ) := USER_REVEALED                 end if             end if         end for     end for end reveal_area

Now all we need to do is make sure that whenever we reveal a square, this method gets called. This is in our main game loop, which we modify fairly easily: under the case for a left-click on MAP_EMPTY, we add a call to reveal_area ( mouse_grid_x, mouse_grid_y ) before the call to draw_map(). This looks like the following:

 Turing: label MAP_EMPTY :                 reveal_area ( mouse_grid_x, mouse_grid_y )                 draw_map ()

What's Left?
The game works quite well, and all of the features are there, but there's something missing...POLISH! The game works, but it's utterly characterless, and it's a bit rough around the edges. What can we do to fix this?

1. Tell the user where they've gone wrong when they lose. The game needs to reveal which flags were correct and which flags were not whenever the player loses the game.
2. Make mouse-clicks actual clicks - not just mouse-down events. This isn't usually a problem at 20x20, but if you change game_size_x and game_size_y to 5, it becomes very difficult to right-click to make a flag, as it scrolls through the options too fast.
3. Make the WIN and LOSE messages more pleasant to read - and prevent them from overwriting the map so much.
4. Allow the player to replay after each win / lose.

Number 1 - Revealing Errors
The simplest way to reveal errors is to draw the map differently when the player loses - flags that are incorrectly placed on non-mine squares will be displayed as flagged_empty.bmp rather than flagged.bmp. This is a fairly simple change, but it requires that we modify a lot of our code. We'll introduce a new global variable called game_over; we'll initialize it as false but whenever the player clicks on a mine, it is set to true before calling draw_map. You should be able to make these changes yourself, so I won't show the code for them.

The biggest change is in draw_map(), where we have to change how the map is drawn based on whether game_over is true or not. The only changes we need to make are:
1. Any flags which are not on mines must be displayed as flagged_empty.bmp.
2. Any mines which have not been flagged must be displayed as mine.bmp.

Therefore, draw_map will be changed to the following:
 Turing: % DRAW THE MAP % Taking into account which squares have been revealed by the user. procedure draw_map()     var draw_x, draw_y : int     for x : 1..game_size_x         for y : 1..game_size_y             draw_x := round ( (x-1)*scale_x )             draw_y := round ( (y-1)*scale_y )                        case user_state ( x, y ) of                 label USER_NONE :                     if game_over and map ( x, y ) = MAP_MINE then           % CHANGE #2                         Pic.Draw ( pic_bomb, draw_x, draw_y, picCopy )                     else                         Pic.Draw ( pic_none, draw_x, draw_y, picCopy )                     end if                                     label USER_REVEALED :                     % If the user revealed a bomb, show an explosion. If the user revealed a non-bomb, show the numeral (or nothing if it's zero).                     if map ( x, y ) = MAP_MINE then                         Pic.Draw ( pic_boom, draw_x, draw_y, picCopy )                     else                         Pic.Draw ( pic_number(map(x,y)), draw_x, draw_y, picCopy )                     end if                                     label USER_FLAGGED :                     if game_over and map ( x, y ) not= MAP_MINE then                 % CHANGE #1                         Pic.Draw ( pic_flagged_empty, draw_x, draw_y, picCopy )                     else                         Pic.Draw ( pic_flag, draw_x, draw_y, picCopy )                     end if                                     label USER_QUESTION :                     Pic.Draw ( pic_question, draw_x, draw_y, picCopy )             end case                  end for     end for         for x : 0..game_size_x         Draw.Line ( round(x * scale_x), 0, round(x * scale_x), maxy, black)     end for         for y : 0..game_size_y         Draw.Line ( 0, round(y * scale_y), maxx, round(y * scale_y), black)     end for         View.Update() end draw_map

Number 2 - Fixing Mouse Clicks
What we really want to detect is not mouse-down events but mouse-up events: when the player lets the mouse button up on a square. Since we can only detect the current status of the mouse, we need to store how it was a moment ago, and then compare to how it is now - if the button was down, but is now up, we count that as a click instead.

We introduce the new variable last_mouse_button, which describes what the value of mouse_button was last time through the main game loop. We set it to 0 initially (why is this the best choice?). At the END of each iteration, we set last_mouse_button := mouse_button (this is right before we go to the top of the loop and get a new value for mouse_button). We change the condition on which a click is detected to require that last_mouse_button = (100 for right-click, 1 for left-click) and mouse_button = 0, indicating that we have "un-clicked" on that turn. The result of these changes looks like:

 Turing: % Play the game     loop         Mouse.Where ( mouse_x, mouse_y, mouse_button )         mouse_grid_x := floor ( mouse_x / scale_x ) + 1         mouse_grid_y := floor ( mouse_y / scale_y ) + 1                 % Ignore (clear) all clicks outside the play area.         if mouse_grid_x < 1 or mouse_grid_x > game_size_x or mouse_grid_y < 1 or mouse_grid_y > game_size_y then             mouse_button := 0         end if                 % Handle valid clicks (within the play area)         if last_mouse_button = 1 and mouse_button = 0 and user_state ( mouse_grid_x, mouse_grid_y ) = USER_NONE then    % left-click = reveal that square             user_state ( mouse_grid_x, mouse_grid_y ) := USER_REVEALED                         case map ( mouse_grid_x, mouse_grid_y ) of                 label MAP_MINE :                     game_over := true                     reveal_map()                     draw_map()                                 Text.Locate ( 1, 1 )                     %put "Boom, you're done!"                     Font.Draw ( "Boom, you're done!", 10, maxy - 50, large_font, brightred )                                     exit                 label MAP_EMPTY :                     reveal_area ( mouse_grid_x, mouse_grid_y )                     draw_map()                                     label :                     draw_map()             end case                     elsif last_mouse_button = 100 and mouse_button = 0 then   % right-click = cycle between none / flag / question             case user_state ( mouse_grid_x, mouse_grid_y ) of                 label USER_NONE :                     user_state ( mouse_grid_x, mouse_grid_y ) := USER_FLAGGED                 label USER_FLAGGED :                     user_state ( mouse_grid_x, mouse_grid_y ) := USER_QUESTION                 label USER_QUESTION :                     user_state ( mouse_grid_x, mouse_grid_y ) := USER_NONE                 label USER_REVEALED :                     % Do nothing otherwise             end case             draw_map()                     end if                 if check_for_win() then             %Text.Locate ( 1, 1 )             %put "YOU WIN!!"             Font.Draw ( "YOU WIN!", 10, maxy - 50, large_font, green )             exit         end if                 last_mouse_button := mouse_button      end loop

Number 3 - Nicer Win/Lose Messages
We can pretty up our game a little using the Font package, supplied with Turing. To do so, we'll draw our Win and Lose messages using fancy fonts. We'll have two fonts, one large and one small:

 Turing: % FONTS var large_font : int := Font.New ( "Arial:32" ) var small_font : int := Font.New ( "Arial:16" )

And we'll change our messages to be more appealing:

 Turing: %Text.Locate ( 1, 1 )                 %put "Boom, you're done!"                 Font.Draw ( "Boom, you're done!", 10, maxy - 50, large_font, brightred )

 Turing: if check_for_win() then         %Text.Locate ( 1, 1 )         %put "YOU WIN!!"         Font.Draw ( "YOU WIN!", 10, maxy - 50, large_font, green )         exit     end if

Number 4 - Allowing Replays
We want the player to be able to choose to replay after every win or loss. This is most easily done by putting our main game loop inside another loop; this other loop will only exit if the player DOESN'T want to play again. This also implies that we'll need to do a re-setup every time we launch the game For this reason, we'll also put all of our setup code into a procedure we can call easily: setup( size_x, size_y : int).

This new procedure is mostly just code we already had, but with the slight modification that it can handle changing the scale of game tiles before each round of the game. This isn't possible in our version of the game, but may be a modification you'd like to try on your own.

 Turing: % PREPARE FOR THE GAME % This is called whenever we want to begin a game. It clears old data and places new mines. procedure setup (size_x, size_y : int)     game_over := false     % Determine scaling     game_size_x := size_x     game_size_y := size_y     scale_x := maxx / game_size_x     scale_y := maxy / game_size_y     var scale_x_int : int := floor (scale_x)     var scale_y_int : int := floor (scale_y)     if (pic_flag >= 0) then           % Clean up after the previous setup() call...         Pic.Free (pic_flag)         Pic.Free (pic_none)         Pic.Free (pic_boom)         Pic.Free (pic_bomb)         Pic.Free (pic_question)     end if     pic_flag := Pic.Scale (pic_base_flag, scale_x_int, scale_y_int)     pic_none := Pic.Scale (pic_base_none, scale_x_int, scale_y_int)     pic_boom := Pic.Scale (pic_base_boom, scale_x_int, scale_y_int)     pic_bomb := Pic.Scale (pic_base_bomb, scale_x_int, scale_y_int)     pic_question := Pic.Scale (pic_base_question, scale_x_int, scale_y_int)     pic_flagged_empty := Pic.Scale (pic_base_flagged_empty, scale_x_int, scale_y_int)     for i : 0 .. 8         if (pic_number (i) >= 0) then             Pic.Free (pic_number (i))         end if         pic_number (i) := Pic.Scale (pic_base_number (i), scale_x_int, scale_y_int)     end for     % Initialize the map     for x : 1 .. 100         for y : 1 .. 100             map (x, y) := MAP_EMPTY             user_state (x, y) := USER_NONE         end for     end for end setup

Our new game loop is wrapped with the following:
 Turing: loop     % Set up for the next game     setup( 30, 20 )     Draw.Cls()     View.Update()     place_mines()     draw_map()     last_mouse_button := 0     % Main game loop goes here...         var response : char     %put "Try again? Y/N: "..     Font.Draw ( "Try again? Y/N: ", 10, maxy - 75, small_font, green )     View.Update()     response := getchar()     exit when response = "n" or response = "N" end loop

What's Next?
Well, this is the end of the Basic Game Programming tutorial. The game we've made so far is functional and gets the job done, but it isn't perfect. There are a lot of things missing or not quite right that you're welcome to try to fiddle with on your own. If you followed the lessons, you should now know the code well enough to continue working on the game without guidance.

Suggestions on how to make this game Better:
1. Add a timer. This would require that you draw the map as a smaller part of a screen and have a way of drawing the timer and a way of figuring out how many seconds have passed since the game started. I would recommend using Time.Elapsed().
2. Prevent the game from cycling so quickly - by default, the game will loop through the main game loop very very quickly - so fast that it's well beyond human perception. However, this leads to the Turing process using up the majority of the machine's CPU power, which can hinder or slow other applications, or force the machine to consume more power (laptops beware). You can fairly easily edit the game code so that each iteration of the main loop takes a minimum amount of time - see Time.DelaySinceLast() - to solve this problem.
3. Add controls to change the game size. Consider also changing the game window's size based on other settings.

As always, questions, comments and concerns are welcomed. This will not be my last tutorial, so please give any advice you have for me to take into consideration when I work on the next tutorial.

DemonWasp

Posted: Fri Aug 21, 2009 9:11 pm   Post subject: Re: [Tutorial] Basic Game Programming: Minesweeper, Part 4

Oops, forgot to post the completed source code.

Minesweeper Part3-4.t
Description:
Filename:  Minesweeper Part3-4.t
Filesize:  9.46 KB

corriep

Posted: Sun Aug 23, 2009 6:01 pm   Post subject: Re: [Tutorial] Basic Game Programming: Minesweeper, Part 4

The only thing this is missing from the real minesweeper is that you can get a bomb on your first click. If I could suggest anything it would be to wait until the player makes the first click, and then position the mines, making sure to not put any mines in the player's first square.

Bits 'n Karma for a great set of tutorials
DemonWasp

Posted: Sun Aug 23, 2009 6:39 pm   Post subject: RE:[Tutorial] Basic Game Programming: Minesweeper, Part 4

I'm becoming something of a karma junkie now...I write tutorials just to get my next fix! Thanks.

As for not hitting a bomb the first time, I'd never noticed that, but that's certainly a feature on the "could be added" list, as is a bomb counter, which I also failed to mention.
Johnny19931993

Posted: Mon Dec 07, 2009 3:42 pm   Post subject: RE:[Tutorial] Basic Game Programming: Minesweeper, Part 4

lol
what a coincidence
I posted a minesweeper game that I made four days before you posted the first part of this tutorial
http://compsci.ca/v3/viewtopic.php?t=21593
DemonWasp

Posted: Mon Dec 07, 2009 4:06 pm   Post subject: RE:[Tutorial] Basic Game Programming: Minesweeper, Part 4

So you had. Maybe that was my inspiration for creating the tutorial, though I seem to recall that I wrote the program over the course of several 90-minute bus rides, so maybe I'd started before you posted. Good job on yours as well.
 Display posts from previous: All Posts1 Day7 Days2 Weeks1 Month3 Months6 Months1 Year Oldest FirstNewest First

Page 1 of 1  [ 6 Posts ]
 Jump to:  Select a forum  CompSci.ca ------------ - Network News - General Discussion     General Forums   -----------------   - Hello World   - Featured Poll   - Contests     Contest Forums   -----------------   - DWITE   - [FP] Contest 2006/2008   - [FP] 2005/2006 Archive   - [FP] 2004/2005 Archive   - Off Topic     Lounges   ---------   - User Lounge   - VIP Lounge     Programming -------------- - General Programming     General Programming Forums   --------------------------------   - Functional Programming   - Logical Programming   - C     C   --   - C Help   - C Tutorials   - C Submissions   - C++     C++   ----   - C++ Help   - C++ Tutorials   - C++ Submissions   - Java     Java   -----   - Java Help   - Java Tutorials   - Java Submissions   - Ruby     Ruby   -----   - Ruby Help   - Ruby Tutorials   - Ruby Submissions   - Turing     Turing   --------   - Turing Help   - Turing Tutorials   - Turing Submissions   - PHP     PHP   ----   - PHP Help   - PHP Tutorials   - PHP Submissions   - Python     Python   --------   - Python Help   - Python Tutorials   - Python Submissions   - Visual Basic and Other Basics     VB   ---   - Visual Basic Help   - Visual Basic Tutorials   - Visual Basic Submissions     Education ----------- - Student Life   Graphics and Design ----------------------- - Web Design     Web Design Forums   ---------------------   - (X)HTML Help   - (X)HTML Tutorials   - Flash MX Help   - Flash MX Tutorials   - Graphics     Graphics Forums   ------------------   - Photoshop Tutorials   - The Showroom   - 2D Graphics   - 3D Graphics     Teams ------ - dTeam Public

 Style: Appalachia blueSilver eMJay subAppalachia subBlue subCanvas subEmjay subGrey subSilver subVereor Search: