Introduction
Microsoft SQL Server 2008 introduces two new 'spatial' datatypes: GEOGRAPHY
and 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 GEOMETRY
).
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 schema
The database schema is very simple: the two GEOMETRY
columns are highlighted with green. I also used a DATE
and 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:
NewGame | 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. |
---|
Shoot | 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. |
---|
Cheat | 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! |
---|
Reset | 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))
STIntersects
And while there are dozens of GEOMETRY
-specific functions (STLength
, 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
OPEN ShipCursor
DECLARE @ShipId TINYINT; DECLARE @ShipLength TINYINT
FETCH NEXT FROM ShipCursor INTO @ShipId, @ShipLength
WHILE (@@FETCH_STATUS <> -1)
BEGIN
DECLARE @placed BIT; SET @placed = 0
WHILE (@placed = 0)
BEGIN
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
END
ELSE
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
END
EXEC AddShip @GameId, @PlayerId, @ShipId, @StartX, @StartY, @EndX, @EndY
SELECT @placed = COUNT(*)
FROM GamePlayerShip
WHERE GameId = @GameId AND PlayerId = @PlayerId AND ShipId = @ShipId
END
SET @placed = 0
FETCH NEXT FROM ShipCursor INTO @ShipId, @ShipLength
END
CLOSE ShipCursor; DEALLOCATE ShipCursor
and when playing the game itself.
SELECT @GamePlayerShipId = Id
, @ShipId = ShipId
FROM GamePlayerShip
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 EXEC
one Shoot
at a time.
The idea is you keep Shoot
ing - getting a Hit
, Miss
, or Sunk
result - until you have sunk all five ships, at which point the code will return Won
.
Conclusion
There's literally only 100 lines of T-SQL or so (excluding the table definitions), so there isn't much more to discuss.
Possible extensions:
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.
More Reading