Computer Science Canada

[Tutorial] Basic Game Programming: Minesweeper, Part 2

Author:  DemonWasp [ Thu Jul 23, 2009 12:36 pm ]
Post subject:  [Tutorial] Basic Game Programming: Minesweeper, Part 2

Prerequisites
This is part 2 of the Basic Game Programming tutorial. Part one can be found here, and should be completed before beginning this part.


Program Design - Laying the Foundation, Continued
We have a bit more to do before we can even start on the game logic (game rules and how to play, etc). For example, even though we have images, we still don't have any way of drawing the map accurately. To do this, let's establish a set of requirements for how we draw the map:


  • For simplicity, the map's squares will consume the entire run window.
  • We will base the game on a defined number of squares horizontally and vertically, and will scale our images so they fit on the given space (our run window).
  • We will define the map to be square (number of rows is the same as the number of columns), and our run window will also be square.
  • Since input will be mostly with the mouse, we will hide the cursor.
  • We will manually update the screen to prevent flickering.


Scaling the Images
The requirements imply that we will be required to scale our images before using them. Helpfully, the Pic module built into Turing has a method for just this purpose: Pic.Scale(). This allows us to re-scale images; however, it creates a new image each time, so we should probably do this as rarely as possible. So, we'll do it once right as the program starts, rather than each time we draw the map.

The code is relatively straight-forward:
Turing:

var game_size_x : int := 20
var game_size_y : int := 20

% Determine real-valued scaling ratios
var scale_x, scale_y : real
scale_x := maxx / game_size_x
scale_y := maxy / game_size_y
   
% Integer scaling ratios
var scale_x_int : int := floor ( scale_x )
var scale_y_int : int := floor ( scale_y )
   
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 )

% This part uses the same clever trick described in the last part - since the numbers are all sequential, we can handle them neatly this way.   
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


Setting up our Run Window
This part is pretty simple and ends up being our very first bit of code:

Turing:
View.Set ( "graphics:800;800; offscreenonly; nocursor" )


This tells Turing to set up a window for graphics (rather than text), with a resolution of 800x800 pixels. This window will not display a flashing input cursor, and it won't redraw itself until we call View.Update().


Drawing the Map
To draw our map, we will simply "visit" every square of the map, drawing it on the screen as appropriate. This will require two for-loops. We will also wrap the map-drawing up into a procedure so we can refer to it simply; we'll call this procedure draw_map().

I'll separate this into two main parts: drawing the icons for each square of the map, and drawing a set of rulers to neatly separate icons.

Drawing the Icons
Turing:

    % Draw icons for individual squares
    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 statements are like if statements that test against a variety of values - here, we test the user_state of (x, y) against each possible condition.
            case user_state ( x, y ) of
                label USER_NONE :
                    Pic.Draw ( pic_none, draw_x, draw_y, picCopy )
                   
                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 :
                    Pic.Draw ( pic_flag, draw_x, draw_y, picCopy )
                   
                label USER_QUESTION :
                    Pic.Draw ( pic_question, draw_x, draw_y, picCopy )
            end case 
       
        end for
    end for


Drawing the Rulers
Turing:

    % Draw a ruler to separate squares visually
    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


Putting it Together
Turing:


% DRAW THE MAP
% Taking into account which squares have been revealed by the user.
procedure draw_map()
    % Draw icons for individual squares
    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 :
                    Pic.Draw ( pic_none, draw_x, draw_y, picCopy )
                   
                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 :
                    Pic.Draw ( pic_flag, draw_x, draw_y, picCopy )
                   
                label USER_QUESTION :
                    Pic.Draw ( pic_question, draw_x, draw_y, picCopy )
            end case 
       
        end for
    end for
   
    % Draw a ruler to separate squares visually
    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
   
    % Show our updates on the screen
    View.Update()
end draw_map


You might wonder why we draw the grid AFTER we draw the icons. Why not draw it first? The reason is because when we draw the images, they would overwrite our rulers, whereas we want our rulers to appear on top of the icons.

Clever observers will note that we're missing some possible states in our drawing mechanism here - for example, there's no code for "incorrectly placed flag", represented by the image in pic_flagged_empty. This will come later, as it requires some game logic first.


Starting the Game Logic
Game logic refers to the game rules and game play. This is what makes the game "Minesweeper" rather than "Battleship", which requires largely the same stuff up until now.

Placing the Mines
Now that we can draw the map, we need to place some mines on the game board. This is more complicated than it sounds - we need to place a specific number of mines randomly, without putting two mines in one square. We also need to determine the numbers in each square. For our purposes, we'll have one mine for every ten squares in the game.

Here's an idea of how we'll do this:
1. For each mine, randomly choose locations until we find one that's empty.
2. Once we've found an empty location, mark the mine there (MAP_MINE) and add one to all the surrounding non-mine squares - this determines the number shown in each square.

The code is pretty simple. We'll separate the task into two procedures. The first, place_mines(), does part 1, above. It uses the second, place_mine ( x, y : int ), to do part 2.
Turing:

% PLACES A MINE at the specified location.
% Also handles the counters at each corner.
procedure place_mine ( x, y : int )
    map ( x, y ) := MAP_MINE
    var check_x, check_y : int

    % Add one to surrounding squares
    for x_off : -1 .. 1
        for y_off : -1 .. 1
            check_x := x+x_off
            check_y := y+y_off
            if check_x > 0 and check_x <= game_size_x and check_y > 0 and check_y <= game_size_y  % Stay inside the map - don't go off the edges.
                and map ( check_x, check_y ) not= MAP_MINE then
                map ( check_x, check_y ) += 1
            end if
        end for
    end for
end place_mine


% PLACE ALL THE MINES
% Places the specified number of mines about the map at random, ensuring no overlaps.
procedure place_mines ()
    var num_mines : int := game_size_x * game_size_y div 10  % One mine per ten squares
    var place_x, place_y : int
    for i : 1..num_mines
        loop
            place_x := Rand.Int ( 1, game_size_x )
            place_y := Rand.Int ( 1, game_size_y )
            if map ( place_x, place_y ) not= MAP_MINE then
                place_mine ( place_x, place_y )
                exit
            end if
        end loop
    end for
end place_mines


Why do I have them in this order (part 2 first, then part 1)? Because the code for part 1 has to be able to call the code for part 2, which means that part 2 must have already been specified before part 1 appears.


Getting some Results
This is all great, but we still haven't seen anything interesting out of our program! Let's do a little bit of work to get it to draw the map we've worked so hard on...

Turing:

% TEMPORARY CODE TO TEST
for x : 1..100
    for y : 1..100
        map ( x, y ) := MAP_EMPTY
        user_state ( x, y ) := USER_REVEALED  % We want to be able to see the map!
    end for
end for

place_mines()

draw_map()


Running this, we notice that - hooray! - it draws a map with a bunch of mines shown, and the numbers around them are correct! (The mines have exploded because we set every square to be revealed, just as if you'd clicked on it).


Next Lesson
In the next lesson, we'll start detecting user input (from the mouse) and continue implementing game logic. Users will be able to click to reveal squares and right-click to cycle between flags, question marks, and unmarked. As always, questions, comments and concerns are welcome.

You can find the cumulative code from Part 1 and Part 2 attached.

Author:  chopperdudes [ Thu Jul 23, 2009 6:17 pm ]
Post subject:  RE:[Tutorial] Basic Game Programming: Minesweeper, Part 2

hmm i'm interested to see how you would code the fields. when i made my version (in 2 hours of boredom), i think the most logical (and simple) way to code that would be DFS or BFS. dunno if you'd consider that "basic".

Author:  DemonWasp [ Thu Jul 23, 2009 6:20 pm ]
Post subject:  RE:[Tutorial] Basic Game Programming: Minesweeper, Part 2

If by "fields" you mean "sections without any mines", then it's actually really easy to do with a really simple recursive procedure that's about 5-10 lines long; we'll be seeing that in either Part 3 or Part 4.

Author:  chopperdudes [ Thu Jul 23, 2009 6:38 pm ]
Post subject:  RE:[Tutorial] Basic Game Programming: Minesweeper, Part 2

yeah that was what i meant, and yeah with a recursive procedure, like i said perhaps depth first search type, then it'd be short and simple, but originally i thought you weren't gonna use recursion so i was interested to see how you would do it non-recursively.

Author:  DemonWasp [ Thu Jul 23, 2009 7:10 pm ]
Post subject:  RE:[Tutorial] Basic Game Programming: Minesweeper, Part 2

It can be done without recursion, but it's not as easy. Basically, you end up emulating recursion with a manually-managed stack.

The method I've worked up isn't tail-recursive, but if it were it could be refactored into a loop rather than a recursive case.

I went with the easiest-possible algorithm for this game: speed is a non-issue, but complexity and algorithmic analysis are largely beyond beginner programmers.


: