Programming C, C++, Java, PHP, Ruby, Turing, VB
Computer Science Canada 
Programming C, C++, Java, PHP, Ruby, Turing, VB  

Username:   Password: 
 RegisterRegister   
 [Tutorial] Basic Game Programming: Minesweeper, Part 4
Index -> Programming, Turing -> Turing Tutorials
View previous topic Printable versionDownload TopicRate TopicSubscribe to this topicPrivate MessagesRefresh page View next topic
Author Message
DemonWasp




PostPosted: 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.
Sponsor
Sponsor
Sponsor
sponsor
DemonWasp




PostPosted: 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:

Download
 Filename:  Minesweeper Part3-4.t
 Filesize:  9.46 KB
 Downloaded:  726 Time(s)

corriep




PostPosted: 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 Smile
DemonWasp




PostPosted: 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




PostPosted: 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




PostPosted: 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:   
   Index -> Programming, Turing -> Turing Tutorials
View previous topic Tell A FriendPrintable versionDownload TopicRate TopicSubscribe to this topicPrivate MessagesRefresh page View next topic

Page 1 of 1  [ 6 Posts ]
Jump to:   


Style:  
Search: