Microsoft SQL Server 2008 introduces two new 'spatial' datatypes:
GEOMETRY. They can be used to store, retrieve, and query two-dimensional data including latitude/longitude coordinates (using the
GEOGRAPHY datatype) and any flat-plane coordinate space (using
The rules for Battleship are on Wikipedia - go and read them there, this article will still be here when you're done...
...welcome back. As you can see, Battleship is generally played on a 10x10 grid (like the example to the right) - just the sort of thing that the
GEOMETRY datatype can be used for. If you were going to build this in a procedural language, you'd probably start with a two-dimensional array or similar data-structure.
In SQL Server, we're not going to 'create' a specific data-structure at all. Instead, we'll just create LINEs representing the ships and then create POINTs for each shot, and check whether they Intersect (i.e., a hit) or not (i.e., a miss). For simplicity, this is just a single-player version of the game - the ships are automatically placed for you, you just have to sink them in the fewest number of shots.
The database schema is very simple: the two
GEOMETRY columns are highlighted with green. I also used a
TIME column - unnecessarily - just because they are also new features of SQL Server 2008.
You'll also notice the
PlayOrder column is 'hidden' - in future, the code could be extended to allow two players to 'compete' (creating their own boards, and shooting at someone else's on separate SQL connections) - but that is left as an exercise for the reader...
About the 'code'
The code really consists of the table definitions, static lookup data, and a handful of Stored Procedures:
|Requires your name to create Player and Game records, then generates a new/random board of ships. The GameId and PlayerId that it returns are required to play the game. It uses another Stored Procedure - AddShip - which you could also use to create playing boards directly.
|The main way you interact with the game - EXEC it with the supplied GameId and PlayerId to take a 'shot' at the board, and see whether you hit or missed a ship.
|Shows you the board (which is what you are trying to 'guess'). Useful for testing and understanding the code, but don't be tempted if you are playing!
|Clears your shots so you can play the same board again from scratch.
"Well Known Text" not currently 'well known'
I hadn't heard of Well Known Text (WKT) before - but again, Wikipedia has a useful definition. You only need to know about two types of WKT (and how to use it in T-SQL) for now:
DECLARE @line VARCHAR(500);
DECLARE @lingG GEOMETRY
SET @line = 'LINESTRING('+CAST(@StartX AS VARCHAR(2))+' '+CAST(@StartY AS VARCHAR(2))+
','+CAST(@EndX AS VARCHAR(2))+' '+CAST(@EndY AS VARCHAR(2))+')'
SET @lineG = geometry::STGeomFromText(@line, 4326))
DECLARE @point VARCHAR(500);
DECLARE @pointG GEOMETRY
SET @point = 'POINT('+CAST(@X AS VARCHAR(2))+' '+CAST(@Y AS VARCHAR(2))')'
SET @pointG = geometry::STGeomFromText(@point, 4326))
And while there are dozens of
GEOMETRY-specific functions (
STArea, etc.), we only really need
STIntersects() for 'hit testing' - both when randomly generating the ships (to ensure they don't share locations):
DECLARE ShipCursor CURSOR FOR
SELECT Id, Length FROM LU_Ship ORDER BY Length DESC
DECLARE @ShipId TINYINT; DECLARE @ShipLength TINYINT
FETCH NEXT FROM ShipCursor INTO @ShipId, @ShipLength
WHILE (@@FETCH_STATUS <> -1)
DECLARE @placed BIT; SET @placed = 0
WHILE (@placed = 0)
PRINT '--- Attempting to place ship '+CAST (@ShipId AS VARCHAR(2))
SET @Orientation = ROUND(RAND(),0) IF @Orientation = 0
BEGIN SET @StartX = ROUND(RAND()*(@BoardSize-1-@ShipLength)+1,0)
SET @StartY = ROUND(RAND()*(@BoardSize-1)+1,0)
SET @EndX = @StartX + @ShipLength - 1
SET @EndY = @StartY
BEGIN SET @StartX = ROUND(RAND()*(@BoardSize-1)+1,0)
SET @StartY = ROUND(RAND()*(@BoardSize-1-@ShipLength)+1,0)
SET @EndX = @StartX
SET @EndY = @StartY + @ShipLength - 1
EXEC AddShip @GameId, @PlayerId, @ShipId, @StartX, @StartY, @EndX, @EndY
SELECT @placed = COUNT(*)
WHERE GameId = @GameId AND PlayerId = @PlayerId AND ShipId = @ShipId
END SET @placed = 0
FETCH NEXT FROM ShipCursor INTO @ShipId, @ShipLength
CLOSE ShipCursor; DEALLOCATE ShipCursor
and when playing the game itself.
SELECT @GamePlayerShipId = Id
, @ShipId = ShipId
WHERE [Line].STIntersects(@point) = 1 AND GameId = @GameId
AND PlayerId = @PlayerId
Installing the 'code'
Simply create a new database in SQL Server 2008 - I called mine Battleship then executed the supplied SQL scripts 01_Battleship.sql, 02_Looksup.sql, 03_Diagram.sql. It (should be) that simple, and the resulting objects should look like this:
Running the 'code'
Here is an example "game" (OK, you can see the first thing I did was cheat to find out where to shoot):
Note that I ran all these queries 'at once' to get the screenshot. You would normally
Shoot at a time.
The idea is you keep
Shooting - getting a
Sunk result - until you have sunk all five ships, at which point the code will return
There's literally only 100 lines of T-SQL or so (excluding the table definitions), so there isn't much more to discuss.
StDistance() could be used in the random-board-generation to ensure ships are at least a certain number of squares away from each other (and you could respond to
Shoot with "Miss, but close!" if desired).
- Make a two-player game, but adding some sort of 'state' (game being built, boards submitted, whose turn is next) so that two DBAs can play from different Management Studio instances.
- Allow the 'computer' to be the second player, randomly Shooting until it Hits, then using the spatial functions to 'decide' the best guess for subsequent shots.
- Configurable board sizes, ships, Shooting rules.