在 haskell 的 gloss 中读取用户鼠标单击位置

Reading user mouseclick position in haskells gloss

编辑: 所以我听从了您的指点并得出了这一点:(遵循游戏规则)

https://hackage.haskell.org/package/gloss-1.9.4.1/docs/Graphics-Gloss-Interface-Pure-Game.html

drawBoard :: IO ()
drawBoard = play (InWindow "Tic Tac Toe" (300,300)(10,10)) yellow 10 board (testBoard) (handleKeys) (iteration)

testBoard :: Board -> Picture
testBoard board = grid
    where
        grid =
            color black (line [ (-100, -300), (-100,  300) ])

iteration :: Float -> Board -> Board
iteration _ board = board 

handleKeys :: Event -> Board -> Board
handleKeys (EventKey (MouseButton LeftButton) Up _ (x, y)) board = board
handleKeys _ _ = board

board = []

它正确地打开了 window 并根据我的 testBoard 功能画了一条黑线。

我现在不确定的是如何在单击按钮时通过新板。我是否创建另一个函数来绘制新板,或者我将如何在单击时继续绘制新板?

您想使用 play 而不是 display

它明确采用类型为 Event -> world -> world 的参数(world 在您的情况下为 Board)描述如何对 Event 做出反应。

编辑:好的,我已经实现了一个玩具示例来向您展示我将如何构建我的代码。您可以通过适当的导入在 self-contained gist 中找到它。

系统的状态是什么?它是如何演变的?

首先要弄清楚的是如何表示棋盘的状态。在 Tic-tac-toe 游戏中,您有一堆 noughts 和一堆 crosses 在给定的 coordinates(一对 Int 等于 -1、0 或 1)在黑板上。您还有当前的 player(在下一步行动后会发生变化)。那么让我们从那个开始:

type Coordinates = (Int, Int)
data Player = Nought | Cross

data Board = Board
  { noughts :: [Coordinates]
  , crosses :: [Coordinates]
  , player  :: Player
  }

然后你可以描述各种简单的事情。当你开始比赛时,empty 棋盘应该是什么样的?玩家的移动(在棋盘上放置一个新的 令牌 )对系统状态有何影响(它将令牌插入当前玩家的列表中,然后将当前玩家更改为对手):

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对应当前状态的问题了。在这里,我假设图片将以 (0, 0) 为中心,这意味着可以通过简单地将所有坐标乘以给定常数来更改其大小。我将使用 Size 参数对我的所有函数进行参数化,这样我就可以毫不费力地调整显示板的大小。

type Size = Float

resize :: Size -> Path -> Path
resize k = fmap (\ (x, y) -> (x * k, y * k))

Noughts 很容易显示:我们可以简单地使用合适大小的 thickCircle,然后将它们转换为它们的坐标!十字有点难处理,因为你必须组合 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 所做的:它将 Float 除以我们为绘图选择的大小,并检查该位置对应的板的哪个细分。

这里我使用 guard, (<$) and (<|>) 来对各种情况进行声明性介绍,但如果您愿意,也可以使用 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 函数创建 window,用 emptyBoard 开始游戏,用 drawBoard 显示它并用 [= 处理用户输入34=].

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)

还有什么事要做?

我没有执行任何游戏逻辑:

  • 玩家可以在已被占据的格子的细分中放置一个令牌,

  • 游戏不检测何时有赢家

  • 没有办法连续玩多局