Minesweeper in T-SQL

Whatever happened to the idea that programming in TSQL can be fun? A Simple-Talk reader contributes an article to remind us all that there is more to TSQL than wrestling with DMVs and pumelling recalcitrant correlated subqueries.

Whatever happened to the idea that programming in TSQL can be fun? A Simple-Talk reader contributes an article to remind us all that there is more to TSQL than wrestling with DMVs and pummelling recalcitrant correlated subqueries.

/* As a fun exercise I decided to program Minesweeper in T-SQL. I think it worked out quite nicely, so I’d like to share it with the world. But first, for anyone who doesn’t know what minesweeper is, a description is available here.

1132-img20.gif

I’ve used SQL Server Management Studio 2008 Express, but other versions should work as well (although the layout of the resultgrid might require some work).

I wanted a minefield that was variable in length and width, so I created three tables: */

CREATE TABLE Grid

    (

     X INT NOT NULL,          — X coordinate

     Y INT NOT NULL,          — Y coordinate

     S INT NULL,              — Status, see the user defined function udf_S

     CONSTRAINT PK_Grid PRIMARY KEY (X ASC, Y ASC)

    )

CREATE TABLE Mine

    (

     X INT NOT NULL,          — X coordinate

     Y INT NOT NULL,          — Y coordinate

     CONSTRAINT PK_Mine PRIMARY KEY (X ASC, Y ASC)

    )

/* The setting table will only ever hold one record */

CREATE TABLE Settings

    (

     MaxX INT NOT NULL,       — Number of rows

     MaxY INT NOT NULL,       — Number of columns

     NumberOfMines INT NOT NULL

    )

/* Inserting some default values */

INSERT  INTO Settings

VALUES  (10, 10, 10)

GO

CREATE FUNCTION udf_S (@S INT)

RETURNS CHAR(1)

/* A user defined function for the visual representation of the status */AS

    BEGIN

        RETURN (CASE

                  WHEN @S = -100 THEN ‘F’       –Flagged

                  WHEN @S = -10 THEN ‘S’        –Safe

                  WHEN @S = -1 THEN ‘?’         –Unknown

                  WHEN @S = 0 THEN ”           –No mines in the vicinity

                  WHEN @S = 9 THEN ‘M’          –Exploded mine

                  ELSE CONVERT(CHAR(1),@S)     

–Number of mines in the vicinity

            END

                  )

    END

GO   

CREATE PROCEDURE p_Initialize

    (

     @MaxX INT = NULL,

     @MaxY INT = NULL,

     @NumberOfMines INT = NULL,

     @Seed INT = NULL

    )

/* Initializing the game by creating the grid and randomly dropping the mines. I included a @seed parameter to be able to create the same map multiple times. If you call the procedure without any parameters he’ll use the same as last game, except the seed naturally */

AS

    BEGIN

        UPDATE  Settings

        SET     MaxX = ISNULL(@MaxX, MaxX),

                MaxY = ISNULL(@MaxY, MaxY),

                NumberOfMines = ISNULL(@NumberOfMines, NumberOfMines)

           

        SELECT  @MaxX = MaxX,

                @MaxY = MaxY,

                @NumberOfMines = NumberOfMines

        FROM    Settings

        TRUNCATE TABLE Mine

        TRUNCATE TABLE Grid

 

        DECLARE @x INT

        DECLARE @y INT

/* Creating records to represent the squares of the grid */       

        SET @x = 1

        WHILE @x <= @MaxX

            BEGIN

                SET @y = 1

                WHILE @y <= @MaxY

                    BEGIN

                        INSERT  INTO grid

                        VALUES  (@x, @y, -1)

                        SET @y = @y + 1

                    END

                SET @x = @x + 1

            END

/* Setting the seed if necessary */
        IF @seed IS NOT NULL

            SET @x = RAND(@seed)

/* Dropping mines, but never on the same square */

        WHILE @NumberOfMines > 0

            BEGIN

                SET @x = FLOOR(RAND() * @MaxX) + 1

                SET @y = FLOOR(RAND() * @MaxY) + 1

           

                IF (SELECT  COUNT(*)

                    FROM    mine

                    WHERE   x = @x

                            AND y = @y

                   ) = 0

                    BEGIN

                        INSERT  INTO Mine

                        VALUES  (@x, @y)

                        SET @NumberOfMines = @NumberOfMines – 1

                    END

            END  

    END

   

 GO

 

CREATE PROCEDURE p_Draw

/* Using a bit of dynamic SQL to draw the grid */

AS

    BEGIN

        DECLARE @sql VARCHAR(MAX)

        SELECT  @sql = ISNULL(@sql + ‘, ‘ + CHAR(13) + CHAR(9), ‘SELECT ‘)

                + ‘MAX(CASE WHEN Y = ‘ + CONVERT(VARCHAR(3), y)

                + ‘ THEN dbo.udf_S(S) END) as [‘ + CONVERT(VARCHAR(3), y)

                + ‘]’

        FROM    grid

        GROUP BY y

        ORDER BY y

        SET @sql = @sql + CHAR(13) + ‘FROM GRID

      GROUP BY X

      ORDER BY X’

     

        –PRINT @sql  –Uncomment this to see the SQL statement generated

        EXEC (@sql)

    END

GO

CREATE PROCEDURE p_MarkSafeSquares

/* Mark all unexplored squares next to a square with no mines in the vicinity (a blank square) See procedure p_Explore */

AS

    UPDATE  grid

    SET     S = -10

    FROM    (SELECT g2.x,

                    g2.y

             FROM   grid g1

                    INNER JOIN grid g2 ON g1.x – g2.x BETWEEN -1 AND 1

                                          AND g1.y – g2.y BETWEEN -1 AND 1

             WHERE  g1.S = 0

                    AND g2.S = -1

            ) subset

    WHERE   subset.x = grid.X

            AND subset.y = grid.y

GO

CREATE PROCEDURE p_ExploreSafeSquares (@count INT OUTPUT)

/* Explore all squares marked as safe. See procedure p_Explore */

AS –First update all safe squares with mines in the vicinity

    UPDATE  Grid

    SET     S = MinesDetected

    FROM    (SELECT g.x,

                    g.y,

                    COUNT(*) AS MinesDetected

             FROM   Grid g

                    INNER JOIN Mine m ON g.x – m.x BETWEEN -1 AND 1

                                         AND g.y – m.y BETWEEN -1 AND 1

             WHERE  S = -10

             GROUP BY g.x,

                    g.y

            ) s1

    WHERE   s1.x = grid.x

            AND s1.y = grid.y

     

      –Determine if the procedure should be run again

    SELECT  @count = COUNT(*)

    FROM    grid

    WHERE   s = -10

   

      –Second, update all other safe squares to blank squares   

    UPDATE  Grid

    SET     S = 0

    WHERE   S = -10

     

GO

CREATE PROCEDURE p_Explore (@x INT, @y INT)

/* Expore an unknown square */

AS

    BEGIN

        IF (SELECT  COUNT(*)

            FROM    Grid

            WHERE   @x = x

                    AND @y = y

                    AND S = -100

           ) = 0 –Check if a square is flagged

            IF (SELECT  COUNT(*)

                FROM    Mine

                WHERE   @x = x

                        AND @y = y

               ) = 0 –Check if there’s a mine

                BEGIN

                    DECLARE @count INT

                    SET @count = (SELECT    COUNT(*)

                                  FROM      (SELECT x,

                                                    y

                                             FROM   Mine

                                             WHERE  x – @x BETWEEN -1 AND 1

                                                    AND y – @y BETWEEN -1 AND 1

                                             GROUP BY x,

                                                    y

                                            ) subset

                                 )

–Count the number of mines in the vicinity

–Update the square to the number of mines in the vicinity

                    UPDATE  Grid

                    SET     S = @count

                    WHERE   x = @x

                            AND y = @y

                       

/* Here’s where the fun starts! In another language you would probably use recursion to solve this square by square, but with a single update you can explore multiple squares that you know are safe. It’s probably possible to do this in a single update instead of the 3 sequential updates, but I’ll leave that up to someone else. */

                    DECLARE @BlankSquares INT

                    IF @count = 0

                        SET @BlankSquares = 1

                             

                    WHILE @BlankSquares > 0

                        BEGIN

                            EXEC p_MarkSafeSquares

                            EXEC p_ExploreSafeSquares @BlankSquares OUTPUT

                        END

                END

            ELSE

                        –A mine was hit!

                UPDATE  Grid

                SET     S = 9

                WHERE   x = @x

                        AND y = @y

    END

GO

CREATE PROCEDURE p_GameState

/* Showing the gamestate */

AS

    BEGIN

        DECLARE @NumberOfMines INT

        DECLARE @MaxX INT

        DECLARE @MaxY INT

     

        SELECT  @NumberOfMines = NumberOfMines,

                @MaxX = MaxX,

                @MaxY = MaxY

        FROM    Settings

     

        DECLARE @SquaresExplored INT,

            @MinesExploded INT,

            @SquaresFlagged INT

     

        SELECT  @SquaresExplored = SUM(CASE WHEN S BETWEEN 0 AND 8 THEN 1

                                            ELSE 0

                                       END),

                @MinesExploded = SUM(CASE WHEN S = 9 THEN 1

                                          ELSE 0

                                     END),

                @SquaresFlagged = SUM(CASE WHEN S = -100 THEN 1

                                           ELSE 0

                                      END)

        FROM    Grid

       

        SELECT  @SquaresExplored AS SquaresExplored,

                @MinesExploded AS MinesExploded,

                @SquaresFlagged AS SquaresFlagged,

                @NumberOfMines AS NumberOfMines,

                @MaxX * @MaxY AS TotalSquares,

                CASE WHEN @MinesExploded > 0 THEN ‘You lost!’

                     WHEN @SquaresExplored + @NumberOfMines = @MaxX * @MaxY

                     THEN ‘You won!’

                     ELSE ‘Keep on playing!’

                END AS GameState     

    END

GO

CREATE PROCEDURE p_flag (@X INT, @Y INT)

/* A procedure to flag/unflag an unknown square as suspect */

AS

    BEGIN

        UPDATE  Grid

        SET     S = CASE WHEN S = -1 THEN -100  –Flag when unknown

                         WHEN S = -100 THEN -1  –Unflag when flagged

                         ELSE S                             –Otherwise no change

                    END

        WHERE   @x = X

                AND @y = y

                 

    END

GO

CREATE PROCEDURE E (@X INT, @Y INT)

/* A procedure with a very short name to play the game. E for Explore */

AS

    BEGIN

        EXEC p_Explore @X, @Y

        EXEC p_Draw

        EXEC p_GameState

    END

GO

CREATE PROCEDURE F (@X INT, @Y INT)

/* A procedure with a very short name to play the game. F for Flag */

AS

    BEGIN

        EXEC p_Flag @X, @Y

        EXEC p_Draw

        EXEC p_GameState

    END

GO

CREATE PROCEDURE p_Solve1

/* It’s quite easy to write SQL statements to help clear the minefield. This one flags the only non-explored square next to a 1. */

AS

    UPDATE  Grid

    SET     S = -100

    FROM    (SELECT

      DISTINCT      x2,

                    y2

             FROM   (SELECT x1,

                            y1,

                            MAX(x2) x2,

                            MAX(y2) y2

                     FROM   (SELECT g1.x AS x1,

                                    g1.y AS y1,

                                    g2.x AS x2,

                                    g2.y AS y2

                             FROM   grid g1

                                    INNER JOIN grid g2 ON g1.x – g2.X BETWEEN -1 AND 1

                                                          AND g1.y – g2.y BETWEEN -1 AND 1

                             WHERE  g1.S = 1

                                    AND g2.S IN (-1, -100)

                            ) s1

                     GROUP BY x1,

                            y1

                     HAVING COUNT(*) = 1

                    ) s2

            ) s3

    WHERE   grid.x = s3.x2

            AND grid.y = s3.y2

            AND grid.s = -1

GO

EXEC p_Initialize 10, 10, 10, 10

EXEC e 1, 1

1132-Minesweeper1.jpg

–the rest of the solution to ‘p_Initialize 10, 10, 10, 10’

/*
EXEC e 7, 6

EXEC f 8, 3

EXEC e 8, 4

EXEC e 9, 3

1132-img1C.gif
                    /* “You lost” */

EXEC f 10, 1

EXEC e 10, 2

EXEC e 10, 3

EXEC f 7, 5

EXEC e 8, 5

EXEC f 9, 5

EXEC e 5, 6

EXEC e 6, 6

EXEC f 4, 6

EXEC e 3, 6

EXEC f 2, 6

EXEC e 1, 6

EXEC e 6, 7

EXEC e 5, 7

EXEC e 4, 7

EXEC e 3, 7

EXEC e 3, 8

EXEC e 4, 8

EXEC f 2, 7

EXEC e 1, 7

EXEC f 10, 8

EXEC e 8, 7

EXEC e 9, 7

EXEC e 10, 7

EXEC f 7, 7

EXEC f 8, 6

EXEC e 10, 5

EXEC e 10, 6

EXEC e 9, 6

*/

–A bigger one

/*

EXEC p_Initialize 30, 30, 60, 10

exec e 1,1

*/

–A really big one, with only a few mines, to see how long it takes the ‘auto explore’ to clear the field. (about 32 sec on my PC)

/*

EXEC p_Initialize 50, 50, 10, 10

EXEC e 1,1

*/

/* Although there are situations where the field can’t be cleared without guessing a lot can probably solved with procedures like p_Solve1. It might even be possible to have a procedure make an educated guess.

Competition:

Create a series of procedures that can solve a multitude of grids. Submit them as comments to this article. (forget the pretty formatting, this happens later)  They will be judged on effectiveness, creativity and style!  The winner will get a license for SQL Prompt Pro, or SQL Data Generator, whichever they prefer */