SQL 战舰 2008
使用 GEOMETRY 数据类型和 SQL Server 中的空间函数实现的单人游戏。
引言
Microsoft SQL Server 2008 引入了两个新的“空间”数据类型:
GEOGRAPHY
和 GEOMETRY
。它们可用于存储、检索和查询二维数据,包括纬度/经度坐标(使用 GEOGRAPHY
数据类型)和任何平面坐标空间(使用 GEOMETRY
)。
战舰的规则在维基百科上 - 去那里阅读,完成后本文仍然在这里...
...欢迎回来。正如您所见,战舰通常在一个 10x10 的网格上进行(例如右侧的示例)- 这正是 GEOMETRY
数据类型可以用于的。如果您要在过程式语言中构建此游戏,您可能会从二维数组或类似的数据结构开始。
在 SQL Server 中,我们根本不会“创建”特定的数据结构。相反,我们将仅创建表示船只的 LINE(线段)并为每次射击创建 POINT(点),然后检查它们是否相交(即命中)或不相交(即未命中)。为简单起见,这只是一个单人游戏版本 - 船只自动放置,您只需在最少的射击次数内将其击沉。
模式
数据库模式非常简单:两个 GEOMETRY
列用绿色突出显示。我还使用了 DATE
和 TIME
列 - 并非必需 - 只是因为它们也是 SQL Server 2008 的新功能。
您还会注意到 PlayOrder
列是“隐藏”的 - 将来,代码可以扩展以允许两人“竞争”(创建自己的棋盘,并在独立的 SQL 连接上射击别人的棋盘)- 但这留给读者作为练习...
关于“代码”
代码实际上包括表定义、静态查找数据以及少量存储过程。
NewGame |
需要您的姓名来创建 Player 和 Game 记录,然后生成新的/随机的船只棋盘。它返回的 GameId 和 PlayerId 是玩游戏所必需的。它使用了另一个存储过程 - AddShip - 您也可以直接使用它来创建游戏棋盘。 |
---|---|
Shoot |
您与游戏互动的主要方式 - 使用提供的 GameId 和 PlayerId 执行它来在棋盘上“射击”,然后查看您是否击中了船只或未击中。 |
Cheat |
显示棋盘(您正在尝试“猜测”的位置)。这对于测试和理解代码很有用,但如果您正在玩游戏,请不要诱惑! |
Reset |
清除您的射击,以便您可以从头开始玩同一个棋盘。 |
“Well Known Text”目前不“为人所知”。
我以前从未听说过 Well Known Text (WKT) - 但同样,维基百科有一个有用的定义。目前您只需要了解两种 WKT 类型(以及如何在 T-SQL 中使用它)。
LINESTRING
DECLARE @line VARCHAR(500);
DECLARE @lingG GEOMETRY
-- LINESTRING(1 1,4 5)
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))
POINT
DECLARE @point VARCHAR(500);
DECLARE @pointG GEOMETRY
-- POINT(2 2)
SET @point = 'POINT('+CAST(@X AS VARCHAR(2))+' '+CAST(@Y AS VARCHAR(2))')'
SET @pointG = geometry::STGeomFromText(@point, 4326))
STIntersects
虽然有数十种 GEOMETRY
特定的函数(STLength
、STArea
、等等),但对于“命中测试”,我们实际上只需要 STIntersects()
- 无论是在随机生成船只时(以确保它们不共享位置)
-- Loop through ships (biggest to smallest) placing them
-- where they don't intersect with other ships
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) -- Choose north/south or east/west
IF @Orientation = 0
BEGIN -- 'Horizontal'
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 -- 'Vertical'
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
-- Attempt to 'save' ship to board: STIntersects is called in AddShip
EXEC AddShip @GameId, @PlayerId, @ShipId, @StartX, @StartY, @EndX, @EndY
-- Check the result, if it wasn't saved it must have overlapped,
-- so we'll try again in the WHILE loop
SELECT @placed = COUNT(*)
FROM GamePlayerShip
WHERE GameId = @GameId AND PlayerId = @PlayerId AND ShipId = @ShipId
END -- WHILE
SET @placed = 0
FETCH NEXT FROM ShipCursor INTO @ShipId, @ShipLength
END
CLOSE ShipCursor; DEALLOCATE ShipCursor
还是在游戏本身进行时。
-- Hit test: does the point intersect a ship 'line'?
SELECT @GamePlayerShipId = Id
, @ShipId = ShipId
FROM GamePlayerShip
WHERE [Line].STIntersects(@point) = 1 -- Means point intersects line
AND GameId = @GameId
AND PlayerId = @PlayerId
安装“代码”
只需在 SQL Server 2008 中创建一个新数据库 - 我将其命名为 **Battleship**,然后执行提供的 SQL 脚本 01_Battleship.sql、02_Looksup.sql、03_Diagram.sql。它(应该)就是这么简单,生成的对象应该看起来像这样。
运行“代码”
这是一个示例“游戏”(好的,您可以看到我做的第一件事是作弊以找出射击位置)。
请注意,我“同时”运行了所有这些查询来获取屏幕截图。您通常会一次执行一个 Shoot
。
游戏的思路是您继续 Shoot
- 收到 Hit
(命中)、Miss
(未命中)或 Sunk
(击沉)的结果 - 直到您击沉所有五艘船,此时代码将返回 Won
(获胜)。
结论
T-SQL 代码(不包括表定义)字面意义上只有大约 100 行,所以没有太多可讨论的了。
可能的扩展
StDistance()
可用于随机棋盘生成,以确保船只彼此之间至少有一定数量的格子距离(如果您愿意,还可以用“未命中,但很近!”来响应Shoot
)。- 制作一个双人游戏,但添加某种“状态”(游戏正在构建、棋盘已提交、轮到谁了),以便两名 DBA 可以从不同的 Management Studio 实例进行游戏。
- 允许“计算机”成为第二个玩家,随机射击直到命中,然后使用空间函数“决定”后续射击的最佳猜测。
- 可配置的棋盘大小、船只、射击规则。