您想使用play
而不是display
.
它明确地接受一个类型的参数Event -> world -> world
(world
在Board
你的情况下)描述如何对Event
s 做出反应。
编辑:好的,我已经实现了一个玩具示例,向您展示我将如何构建我的代码。您可以在包含适当导入的独立要点中找到它。
系统的状态是什么?它是如何演变的?
您需要弄清楚的第一件事是如何表示董事会的状态。在井字游戏中,您在棋盘上在给定坐标(一对s 等于 -1、0 或 1)处有一堆nought和一堆cross 。您还有一个当前玩家(在下一步行动后会改变)。所以让我们从那个开始:Int
type Coordinates = (Int, Int)
data Player = Nought | Cross
data Board = Board
{ noughts :: [Coordinates]
, crosses :: [Coordinates]
, player :: Player
}
然后,您可以描述各种简单的事物。开始比赛时,空棋盘应该是什么样子?玩家的移动(在棋盘上放置一个新的标记)对系统的状态有什么影响(它将标记插入到当前玩家的列表中,然后将当前玩家更改为对手):
emptyBoard :: Board
emptyBoard = Board [] [] Nought
pushToken:: Coordinates -> Board -> Board
pushToken c b = case player b of
Nought -> b { noughts = c : noughts b, player = Cross }
Cross -> b { crosses = c : crosses b, player = Nought }
我们如何向用户显示状态?
接下来是绘制Picture
当前状态对应的a的问题。在这里,我假设图片将居中,(0, 0)
这意味着可以通过简单地将所有坐标乘以给定常数来更改其大小。我将使用一个参数对我的所有函数进行Size
参数化,这样我就可以毫不费力地调整显示板的大小。
type Size = Float
resize :: Size -> Path -> Path
resize k = fmap (\ (x, y) -> (x * k, y * k))
Noughts 很容易显示:我们可以简单地使用thickCircle
正确大小的 a,然后将它们转换为它们的坐标!十字架有点难处理,因为你必须结合 2 个矩形来绘制它们。
drawNought :: Size -> Coordinates -> Picture
drawNought k (x, y) =
let x' = k * fromIntegral x
y' = k * fromIntegral y
in color green $ translate x' y' $ thickCircle (0.1 * k) (0.3 * k)
drawCross :: Size -> Coordinates -> Picture
drawCross k (x, y) =
let x' = k * fromIntegral x
y' = k * fromIntegral y
in color red $ translate x' y' $ Pictures
$ fmap (polygon . resize k)
[ [ (-0.35, -0.25), (-0.25, -0.35), (0.35,0.25), (0.25, 0.35) ]
, [ (0.35, -0.25), (0.25, -0.35), (-0.35,0.25), (-0.25, 0.35) ]
]
为了画棋盘,我们画了一个黑色的网格,然后用五角星和十字架填充它:
drawBoard :: Size -> Board -> Picture
drawBoard k b = Pictures $ grid : ns ++ cs where
ns = fmap (drawNought k) $ noughts b
cs = fmap (drawCross k) $ crosses b
grid :: Picture
grid = color black $ Pictures $ fmap (line . resize k)
[ [(-1.5, -0.5), (1.5 , -0.5)]
, [(-1.5, 0.5) , (1.5 , 0.5)]
, [(-0.5, -1.5), (-0.5, 1.5)]
, [(0.5 , -1.5), (0.5 , 1.5)]
]
我们如何对输入做出反应?
现在我们有了一块板并且可以显示它,我们只需要能够获取用户输入并对其做出响应,这样我们就有了一个可以运行的游戏。
鼠标点击被接收为一对与鼠标在绘图中的位置相对应的浮点数。我们需要将此位置转换为适当的坐标。这就是这样checkCoordinate
做的:它将 a 除以Float
我们为绘图选择的大小,并检查该位置对应于电路板的哪个细分。
在这里,我使用guard
,(<$)
和(<|>)来对各种情况进行声明性表示,但if ... then ... else ...
如果您愿意,也可以使用。
checkCoordinate :: Size -> Float -> Maybe Int
checkCoordinate k f' =
let f = f' / k
in (-1) <$ guard (-1.5 < f && f < -0.5)
<|> 0 <$ guard (-0.5 < f && f < 0.5)
<|> 1 <$ guard (0.5 < f && f < 1.5)
最后,handleKeys
可以检测鼠标点击,检查它们是否对应于棋盘中的位置,并通过调用做出适当的反应pushToken
:
handleKeys :: Size -> Event -> Board -> Board
handleKeys k (EventKey (MouseButton LeftButton) Down _ (x', y')) b =
fromMaybe b $ do
x <- checkCoordinate k x'
y <- checkCoordinate k y'
return $ pushToken (x, y) b
handleKeys k _ b = b
把它们放在一起
然后,我们可以声明一个main
创建窗口的函数,使用 开始游戏,emptyBoard
使用 显示它并使用drawBoard
处理用户输入handleKeys
。
main :: IO ()
main =
let window = InWindow "Tic Tac Toe" (300, 300) (10, 10)
size = 100.0
in play window yellow 1 emptyBoard (drawBoard size) (handleKeys size) (flip const)
还有什么可做的?
我没有强制执行任何游戏逻辑:
玩家可以在已经被占用的网格的细分中放置一个标记,
游戏没有检测到何时有赢家
没有办法连续玩多个游戏