Creating Minesweeper in Flash
Tutorial parts
Part 2: Programming the game
Creating the code
We are going to need two ActionScript files for this game. One will hold all of the game logic, the other will be for the cells.
Create two new files, Game.as and Cell.as, and save them in a folder called Minesweeper. Your FLA file should be outside that folder. There is a 3rd (location.as) but it is so simple you can just copy and paste it from the source link above.
File / folder structure
The file and folder structure is very important because Flash uses this structure to map the structure of the application itself. In this case Game.as is in a package called Minesweeper (the folder) and a class called Game (game.as), and Cell.as is Minesweeper.Cell. Open your two ActionScript files and we can begin putting some code in!
Game.as
Game.as is going to control our actual game. It is going to do things like check if we've won, generate new games and keep track of how many mines are remaining. We start by setting up the package and class information I mentioned above:
package Minesweeper
{
import flash.display.MovieClip;
public class Game extends MovieClip
{
}
}
When you have done that the next step is to bind this class to our movie. Go back to our FLA file and in the document's Properties you'll see something like the following. You need to put Minesweeper.game where I wrote it.
Assigning the document class
Now we're ready to create some methods and variables that will control the game.
Properties
These properties are all static which means only one instance of them will ever exist at a time. They are also declared either public or private depending on whether they need to be accessed from outside our Game class. The TextFormat 'LargeText 'is a const, that means we define it once and it is read only.
private static const LargeText:TextFormat = new TextFormat(); LargeText.color = 0x333333; LargeText.size = 16; LargeText.font = "Arial"; public static var STAGE:Stage; public static var Playing:Boolean; private static var Rows:int; private static var Columns:int; private static var Mines:int; public static var MinesRemaining:int; private static var Seconds:int; public static var Grid:Array; private static var Clock:Timer; public static var FlagMode:Boolean; private static var GameBoard:MovieClip; private static var TimerField:TextField; public static var MinesField:TextField; public static var WinNotice:MovieClip;
Methods
Our game begins with the construction of the Game object. In our constructor we're going to assign the stage to our static variable STAGE, and set an event listener that will initialise the game after it is loaded.
public function Game() { STAGE = this.stage; loaderInfo.addEventListener(Event.COMPLETE, Initialise); }
Our initialise function is going to create our game elements and then begin a game on Hard.
public function Initialise(e:Event):void { // Set up our button handlers this.EasyButton.addEventListener(MouseEvent.MOUSE_UP, StartEasy); this.MediumButton.addEventListener(MouseEvent.MOUSE_UP, StartMedium); this.HardButton.addEventListener(MouseEvent.MOUSE_UP, StartHard); // Set up our clock Clock = new Timer(1000); Clock.addEventListener(TimerEvent.TIMER, Tick); // Set up our game board which holds all of the cells GameBoard = new MovieClip(); GameBoard.buttonMode = true; GameBoard.useHandCursor = true; this.addChild(GameBoard); // Set up our timer and mine display fields TimerField = new TextField(); TimerField.x = 15; TimerField.y = 354; TimerField.defaultTextFormat = LargeText; TimerField.selectable= false; this.addChild(TimerField); MinesField = new TextField(); MinesField.x = 151; MinesField.y = 354; MinesField.defaultTextFormat = LargeText; MinesField.selectable = false; this.addChild(MinesField); // Set up our win notice WinNotice = new Minesweeper.WinNotice(); WinNotice.x = 0; WinNotice.y = 56; WinNotice.visible = false; this.addChild(WinNotice); // Set up our key handler for placing flags STAGE.addEventListener(KeyboardEvent.KEY_DOWN, FlagModeOn); STAGE.addEventListener(KeyboardEvent.KEY_UP, FlagModeOff); StartHard(null); }
Starting a game
Because we have three different difficulty settings that each configure the game a certain way we have 4 methods to start games. 3 of them just set the difficulty configuration and then the 4th is shared by all of them and actually starts it.
private static function StartEasy(e:MouseEvent):void { Rows = 9; Columns = 9; Mines = 10; StartGame(); } private static function StartMedium(e:MouseEvent):void { Rows = 16; Columns = 16; Mines = 40; StartGame(); } private static function StartHard(e:MouseEvent):void { Rows = 30; Columns = 16; Mines = 99; StartGame(); } private static function StartGame():void { // Reset the game data Playing = true; MinesRemaining = Mines; Seconds = 0; FlagMode = false; TimerField.text = "0:00"; MinesField.text = MinesRemaining + " mines"; WinNotice.visible = false; // Reset the game grid while(GameBoard.numChildren > 0) GameBoard.removeChildAt(0); GameBoard.x = (600 - (Rows * 19)) / 2; GameBoard.y = (400 - (Columns * 19)) / 2; // Set up the mines Grid = new Array(Rows); var cell:Cell; var x:int; var y:int; for(x=0; x<Rows; x++) { Grid[x] = new Array(Columns); for(y=0; y<Columns; y++) { cell = new Cell(x, y); cell.x = x * 19; cell.y = y * 19; Grid[x][y] = cell; GameBoard.addChild(cell); } } // Place the mines var adjacentcells:Array; var i:int; var ax:int; var ay:int; var acell:Cell; while(Mines > 0) { // Choose a random cell to put the mine in x = Math.round(Math.random() * Rows - 1); y = Math.round(Math.random() * Columns - 1); // Make sure it's a valid cell if(!InRange(x, y)) continue; cell = Grid[x][y]; if(cell.Mined) continue; // We've placed a mine Mines--; cell.Mined = true; cell.Adjacent = 0; // Determine the cells adjacent to our new mine adjacentcells = Cell.AdjacentCells(x, y); if(adjacentcells.length == 0) continue; // Increase their adjacent count on all valid cells for(i=0; i<adjacentcells.length; i++) { ax = adjacentcells[i].X; ay = adjacentcells[i].Y; acell = Grid[ax][ay]; if(!acell.Mined) { acell.Adjacent += 1; } } } // Start the clock Clock.start(); }
Key handlers
The key handlers control what happens when we click a cell - are we placing a mine or question mark, or are we revealing the cell?
private static function FlagModeOn(e:KeyboardEvent):void { if(e.keyCode == Keyboard.SPACE) { FlagMode = true; } } private static function FlagModeOff(e:KeyboardEvent):void { if(e.keyCode == Keyboard.SPACE) { FlagMode = false; } }
The Clock
We count the seconds that have elapsed during a game by using a Timer which 'ticks' every 1000 milliseconds. We set the timer up in the Initialise method, now we need to set up the handler:
private static function Tick(e:TimerEvent):void { Seconds++; var minutes:int = Math.floor(Seconds / 60); var seconds:int = Seconds % 60; TimerField.text = minutes + ":" + (seconds > 9 ? seconds : "0" + seconds); }
The CheckWin method
Each time a user clicks on a grid cell we need to check if they've just beaten the game.
public static function CheckWin():void { var cell:Cell; for(var x:int=0; x<Rows; x++) { for(var y:int=0; y<Columns; y++) { cell = Grid[x][y]; // If a cell has a mine but isn't flagged then we can't have won if(cell.Mined == true && !cell.Flagged) return; } } Playing = false; Clock.stop(); WinNotice.visible = true; }
The Lose method
When a player clicks a mine the game is lost. This method handles that situation:
public static function Lose():void { var cell:Cell; for(var x:int=0; x<Rows; x++) { for(var y:int=0; y<Columns; y++) { cell = Grid[x][y]; Cell.FinalReveal(cell); } } Playing = false; Clock.stop(); }
The InRange method
When the game initialises cells are randomly selected to place mines in, and when adjacent cells are determined it's done just by +/-ing our way through the x/y coordinates. Each time we need to check if the coordinates we have gotten are valid. The InRange method does that for us:
public static function InRange(x:int, y:int):Boolean { return (x > -1 && y > -1 && x < Rows && y < Columns && Grid[x][y] != null && Grid[x][y].Ignore == false); }
Our final method, ClearEmptySpaces
In traditional Minesweeper when you click a cell any empty adjacent spaces are revealed, that's how you get large sections that are revealed if you click a sweet spot. We use a recursive method to find and keep on finding empty adjacent cells to clear up. It is recursive because it calls itself and keeps doing so until there are no more to reveal.
public static function ClearEmptySpaces(x:int, y:int):void { var adjacentcells:Array = Cell.AdjacentCells(x, y); if(adjacentcells.length == 0) return; var cell:Cell; var ax:int; var ay:int; for(var i:int=0; i<adjacentcells.length; i++) { ax = adjacentcells[i].X; ay = adjacentcells[i].Y; cell = Grid[ax][ay]; if(cell.Mined || cell.Flagged) continue; if(cell.Adjacent == 0) { Cell.Blank(cell); ClearEmptySpaces(ax, ay); } else { Cell.Reveal(cell); } } }
Tutorial parts
