Creating Sudoku in Flash

Tutorial parts

Part 2: Programming the game

Bookmark and Share

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 Sudoku. Your FLA file should be outside that folder.

File/folder structure 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 Sudoku (the folder) and a class called Game (game.as), and Cell.as is Sudoku.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, reveal a hint and start new games. We start by setting up the package and class information I mentioned above:

package Sudoku
{
    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 Sudoku.game where I wrote it.

Assigning the document class 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.

public static var STAGE:Stage;
public static var Playing:Boolean;
private static var Grid:Array;
private static var GridCells:Array;
public static var CellHolder:MovieClip;
private static var WinDialogue:Sudoku.WinDialogue;
public static var Highlighter:Shape;

As you can see they are pretty self-explanatory. The only one we haven't covered is the Highlighter, this is a rectangle we're going to draw and position over whichever cell is selected.

Methods

Our game begins with the construction of the Main 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 grid cells and set up the other elements of the game.

private function Initialise(e:Event):void
{
        // Set up our button click handlers
        this.NewGameButton.addEventListener(MouseEvent.MOUSE_UP, NewGame);
        this.HintButton.addEventListener(MouseEvent.MOUSE_UP, Hint);
        this.SolveButton.addEventListener(MouseEvent.MOUSE_UP, Solve);

        // Create the container movieclip for our cells
        CellHolder = new MovieClip();
        CellHolder.x = 10;
        CellHolder.y = 50;
        addChild(CellHolder);

        // Initialise our grid of cells
        Grid = new Array(9);
        GridCells = new Array(81);

        var gindex:int = 0;
        var cell:Cell;

        for(var x:int=0; x<9; x++)
        {
                Grid[x] = new Array(9);

                for(var y:int=0; y<9; y++)
                {
                        cell = new Cell(x, y);
                        Grid[x][y] = cell;
                        GridCells[gindex] = cell;
                        gindex++;
                }
        }

        // Create our highlighter to indicate which cell is being edited
        Highlighter = new Shape();
        Highlighter.graphics.lineStyle(0.1, 0x999999);
        Highlighter.graphics.moveTo(0, 0);
        Highlighter.graphics.lineTo(0, 47);
        Highlighter.graphics.lineTo(47, 47);
        Highlighter.graphics.lineTo(47, 0);
        Highlighter.graphics.lineTo(0, 0);
        Highlighter.graphics.endFill();
        Highlighter.visible = false;
        STAGE.addChild(Highlighter);

        // Create our win dialogue and hide it for later
        WinDialogue = new Sudoku.WinDialogue();
        WinDialogue.x = 10;
        WinDialogue.y = 239;
        WinDialogue.visible = false;
        STAGE.addChild(WinDialogue);

        // Finally start a game
        NewGame();
}

Starting a game

Our method for starting a game generates a Sudoku solution, resets all the grid cells and then reveals 28 randomly selected cells.

private static function NewGame(e:MouseEvent = null):void
{
        // Reset the grid
        ResetGrid();

        // Create a puzzle
        GeneratePuzzle();

        // Hide the win dialogue if it's visible
        WinDialogue.visible = false;

        var revealed:int = 28;

        while(revealed > 0)
        {
                var y = Random(0, 8);
                var x = Random(0, 8);

                if(Grid[y][x].Revealed == false)
                {
                        Grid[y][x].Revealed = true;
                        Cell.ShowAnswer(Grid[y][x]);
                        revealed--;
                }
        }

        // Set the status  to playing
        Playing = true;
}

Within our NewGame method you'll notice we're calling two methods, ResetGrid and GeneratePuzzle. Our ResetGrid method is very simple:

private static function ResetGrid():void
{
        // Go through each cell
        for(var i:int=0; i<81; i++)
        {
                // Reset it
                Cell.Reset(GridCells[i]);
        }
}

The GeneratePuzzle method is more complicated:

private static function GeneratePuzzle():void
{
    var index:int = 0;

    while(index < 81)
    {
        var number:int = 0;
        var values:Array = [1,2,3,4,5,6,7,8,9];

        while(values.length > 0 && number == 0)
        {
            var zindex:int = Random(0,  values.length);
            var z:int = values[zindex];
            values.splice(zindex, 1);

            if(!Conflicts(z, GridCells[index]))
            {
                number = z;
            }
        }

        if(number == 0)
        {
            var maxbacktrack:int = index > 10 ? 10 : index;
            var backtrack:int = Random(1, maxbacktrack);
            var backtracked:int = 0;

            while(backtracked < backtrack)
            {
                GridCells[index - backtracked].Answer = 0;
                backtracked++;
            }

            index -= backtrack;
        }
        else
        {
            GridCells[index].Answer = number;
            index++;
        }
    }
}

It doesn't take a lot of code to create a Sudoku solution, but it does take a bit of logic. What we're doing here is, until there are 81 answers (the 9x9 cells), we grab a random number and check if it is valid for that row, column and region. Whenever we find a number that is NOT valid we backtrack, that is we shift back to answers we'd already generated and create new ones. The amount we backtrack is random and up to 10 answers.

This is to ensure we don't run into a scenario where we lock up computers because we've created an unsolveable puzzle. The random amount to backtrack helps a lot because if we use a fixed amount then we can still end up in a scenario where the loop cannot complete.

Inside GeneratePuzzle we are calling another method, Conflicts. This method checks to make sure that a number doesn't already exist in a row, column or grid.

private static function Conflicts(number:int, cell:Sudoku.Cell):Boolean
{
        // Check the rows and columns
        for(var i:int=0; i<9; i++)
        {
                if(Grid[cell.X][i].Answer == number || Grid[i][cell.Y].Answer == number)
                {
                        return true;
                }
        }

        // Check the region it's in
        for(var x:int=cell.RegionRowStart; x<cell.RegionRowStart+3; x++)
        {
                for(var y:int=cell.RegionColStart; y<cell.RegionColStart+3; y++)
                {
                        if(Grid[x][y].Answer == number)
                        {
                                return true;
                        }
                }
        }

        return false;
}

Hints and solving the puzzle

The hint button allows players to reveal a single cell's answer when they get stuck:

private static function Hint(e:MouseEvent):void
{
        if(!Playing)
        return;

        if(AvailableCells() == false)
        return;

        // Because we're not sure which cell to reveal we have to
        // keep doing this till we find one
        var done:Boolean = false;
        var cell:Cell;

        while(!done)
        {
                // Select a random cell
                var row:int = Random(0, 8);
                var column:int = Random(0, 8);
                cell = Grid[row][column];

                // If it's not revealed then we do so
                if(cell.Revealed == false)
                {
                        Cell.ShowAnswer(cell);

                        // Check if the game's over now
                        CheckWin();

                        return;
                }
        }
}

The Hint method relies on two other methods in Game.as, AvailableCells and CheckWin. AvailableCells is a check that is performed to make sure that there is a blank cell we can reveal the answer to. If the player has entered an answer then we don't want to overwrite their guess.

private static function AvailableCells():Boolean
{
        // Go through each cell
        for(var i:int=0; i<81; i++)
        {
                // Check if it's empty
                if(Cell.GetAnswer(GridCells[i]) == "")
                {
                        return true;
                }
        }

        return false;
}

The CheckWin method is more complicated because it has to check every single row, column and region to see if the game has won. If at any time a blank or duplicate answer is encounted it stops checking.

public static function CheckWin():void
{
        // Don't check if there are any empty cells
        if(AvailableCells() == true)
        return;

        // Check the rows and columns
        var rowcell:Cell;
        var rowanswer:String;
        var rowanswers:String;
        var colcell:Cell;
        var colanswer:String;
        var colanswers:String;

        for(var i:int=0; i<9; i++)
        {
                rowanswers = "";
                colanswers = "";

                for(var j=0; j<9; j++)
                {
                        rowcell = Grid[i][j];
                        colcell = Grid[j][i];

                        // Check the row answer, if it's blank or already been
                        // entered we can return
                        rowanswer = Cell.GetAnswer(rowcell);

                        if(rowanswer == "" || rowanswers.indexOf(rowanswer) > -1)
                        return;

                        // Check the column answer
                        colanswer = Cell.GetAnswer(colcell);

                        if(colanswer == "" || colanswers.indexOf(colanswer) > -1)
                        return;

                        // Append these answers to our check lists
                        rowanswers += rowanswer;
                        colanswers += colanswer;
                }
        }

        // Check the regions
        var regionanswers:String;
        var regionanswer:String;
        var rowstart:int = 0;
        var colstart:int = 0;

        for(var k:int=1; k<10; k++)
        {
                // Work out what the x/y coordinates of the region we're in begin at
                switch(k)
                {
                        case 1:
                        rowstart = 0;
                        colstart = 0;
                        break;

                        case 2:
                        rowstart = 3;
                        colstart = 0;
                        break;

                        case 3:
                        rowstart = 6;
                        colstart = 0;
                        break;

                        case 4:
                        rowstart = 0;
                        colstart = 3;
                        break;

                        case 5:
                        rowstart = 3;
                        colstart = 3;
                        break;

                        case 6:
                        rowstart = 6;
                        colstart = 3;
                        break;

                        case 7:
                        rowstart = 0;
                        colstart = 6;
                        break;

                        case 8:
                        rowstart = 3;
                        colstart = 6;
                        break;

                        case 9:
                        rowstart = 6;
                        colstart = 6;
                        break;
                }

                // Check each cell in this region
                regionanswers = "";

                for(var x:int=rowstart; x<rowstart+3; x++)
                {
                        for(var y:int=colstart; y<colstart+3; y++)
                        {
                                rowcell = Grid[x][y];

                                regionanswer = Cell.GetAnswer(rowcell);

                                // If there's no answer
                                if(regionanswer == "" || regionanswers.indexOf(regionanswer) > -1)
                                return;

                                // Append the answer to our check list
                                regionanswers += regionanswer;
                        }
                }
        }

        Playing = false;
        WinDialogue.visible = true;
        STAGE.setChildIndex(WinDialogue, STAGE.numChildren - 1);
}

The solve button reveals all of the answers:

private static function Solve(e:MouseEvent):void
{
        if(!Playing)
        return;

        // We're not playing any more
        Playing = false;

        // Go through each cell and show the answer
        for(var i:int=0; i<81; i++)
        {
                if(GridCells[i].Revealed == false)
                {
                        Cell.ShowAnswer(GridCells[i]);
                }
        }
}

Finally we have a method that is commonly used throughout the game, Random. It generates a random number between two values inclusive:

private static function Random(min:int, max:int):int
{
        var number:int = min - 1;

        while(number < min || number > max)
        {
                number = min + Math.round(Math.random() * (max - min));
        }

        return number;
}

This is a lot of code to digest and it might be easier and make more sense by checking the source file in it's entirety.

Tutorial parts