
-----------------------------------
DemonWasp
Thu Jul 23, 2009 12:36 pm

[Tutorial] Basic Game Programming: Minesweeper, Part 2
-----------------------------------
Prerequisites
This is part 2 of the Basic Game Programming tutorial. 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 

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:

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:

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

    % 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

    % 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


% 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.

% 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  0 and check_y 