You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

利用Monad模拟全局变量解决Haskell贪吃蛇特殊苹果计分问题

解决方案:用State Monad管理游戏状态,实现特殊苹果计分

嘿,这个需求我之前做Haskell贪吃蛇项目时也碰到过!咱们可以通过扩展游戏状态+State Monad封装状态操作的方式,优雅解决分数和蛇长度解绑的问题,同时完美适配Gloss的纯函数式游戏循环。

第一步:重构游戏状态类型

首先,咱们需要把原来的游戏状态(只有蛇和普通苹果)扩展,加入特殊苹果的位置列表,以及专门统计吃掉特殊苹果的计数器——这就是你要的“模拟全局变量”的核心:

import Graphics.Gloss
import Control.Monad.State

-- 坐标类型
type Coord = (Int, Int)

-- 扩展后的游戏状态
data World = World
  { snake           :: [Coord]       -- 蛇的身体段坐标列表
  , regularApples   :: [Coord]       -- 普通苹果位置
  , specialApples   :: [Coord]       -- 特殊苹果位置
  , specialEaten    :: Int           -- 吃掉的特殊苹果次数(核心统计量)
  } deriving (Show)

初始化状态时,把specialEaten设为0即可:

-- 初始蛇长度设为3(你可以根据自己的需求调整)
initialSnake :: [Coord]
initialSnake = [(0,0), (-10,0), (-20,0)]

initialWorld :: World
initialWorld = World
  { snake = initialSnake
  , regularApples = [(100, 50), (-50, 100)]
  , specialApples = []
  , specialEaten = 0
  }

第二步:用State Monad封装状态更新逻辑

State Monad天生适合处理这种需要读写状态的场景,咱们把“吃苹果”的逻辑封装成State操作,让代码更模块化、可读性更强:

-- 处理蛇吃苹果的逻辑:检查蛇头是否碰到普通/特殊苹果,更新状态
handleAppleCollision :: State World ()
handleAppleCollision = do
  world <- get
  let snakeHead = head $ snake world
      -- 分离被吃掉的普通苹果和剩余苹果
      (eatenReg, remainingReg) = partition (== snakeHead) (regularApples world)
      -- 分离被吃掉的特殊苹果和剩余特殊苹果
      (eatenSpec, remainingSpec) = partition (== snakeHead) (specialApples world)

  case (not $ null eatenReg, not $ null eatenSpec) of
    -- 吃到普通苹果:蛇身加1段,移除该苹果
    (True, _) -> put world
      { snake = snake world ++ [last $ snake world]
      , regularApples = remainingReg
      }
    -- 吃到特殊苹果:蛇身加1段,移除该苹果,特殊计数+1
    (_, True) -> put world
      { snake = snake world ++ [last $ snake world]
      , specialApples = remainingSpec
      , specialEaten = specialEaten world + 1
      }
    -- 没吃到苹果,啥也不做
    _ -> return ()

第三步:调整计分逻辑

现在分数不再直接绑定蛇长度,而是通过基础分(蛇长度变化)+ 特殊苹果额外分计算:

calculateScore :: World -> Int
calculateScore world =
  let baseScore = length (snake world) - length initialSnake  -- 普通苹果带来的分数
      specialBonus = specialEaten world                       -- 特殊苹果额外加的分数(每个+1,因为普通给1分,特殊给2分,差值是1)
  in baseScore + specialBonus

举个例子:初始蛇长3,吃2个普通苹果(长度+2,基础分2),吃1个特殊苹果(长度+1,基础分+1,特殊计数+1),总分数=3+1=4,正好对应21 +12=4,完全符合需求!

第四步:适配Gloss的游戏循环

Gloss的play函数是纯函数式的,咱们只需要在update函数里运行State操作即可:

-- Gloss的update函数:每帧更新游戏状态
update :: Float -> World -> World
update _ = execState handleAppleCollision

然后在渲染函数里显示计算后的分数:

-- 渲染函数:绘制蛇、苹果和分数
render :: World -> Picture
render world = pictures
  [ drawSnake $ snake world
  , drawRegularApples $ regularApples world
  , drawSpecialApples $ specialApples world
  , translate (-190) 190 $ scale 0.1 0.1 $ text $ "Score: " ++ show (calculateScore world)
  ]

-- 以下是辅助绘制函数(你可以根据自己的Gloss绘制逻辑调整)
drawSnake :: [Coord] -> Picture
drawSnake = pictures . map (\(x,y) -> translate (fromIntegral x) (fromIntegral y) $ color green $ rectangleSolid 10 10)

drawRegularApples :: [Coord] -> Picture
drawRegularApples = pictures . map (\(x,y) -> translate (fromIntegral x) (fromIntegral y) $ color red $ circleSolid 5)

drawSpecialApples :: [Coord] -> Picture
drawSpecialApples = pictures . map (\(x,y) -> translate (fromIntegral x) (fromIntegral y) $ color purple $ circleSolid 5)

最后,加上输入处理和启动函数:

-- 处理键盘输入(控制蛇的方向,你可以根据自己的逻辑实现)
handleInput :: Event -> World -> World
handleInput (EventKey (SpecialKey KeyUp) Down _ _) world = world { snake = adjustSnakeDirection Up (snake world) }
handleInput (EventKey (SpecialKey KeyDown) Down _ _) world = world { snake = adjustSnakeDirection Down (snake world) }
handleInput (EventKey (SpecialKey KeyLeft) Down _ _) world = world { snake = adjustSnakeDirection Left (snake world) }
handleInput (EventKey (SpecialKey KeyRight) Down _ _) world = world { snake = adjustSnakeDirection Right (snake world) }
handleInput _ world = world

-- 调整蛇方向的辅助函数(示例逻辑,你可以替换成自己的)
adjustSnakeDirection :: Direction -> [Coord] -> [Coord]
adjustSnakeDirection dir snake = newHead : init snake
  where (x,y) = head snake
        newHead = case dir of
          Up -> (x, y+10)
          Down -> (x, y-10)
          Left -> (x-10, y)
          Right -> (x+10, y)

data Direction = Up | Down | Left | Right

-- 启动游戏
main :: IO ()
main = play
  (InWindow "Haskell Snake" (400, 400) (10, 10))
  black
  10
  initialWorld
  render
  handleInput
  update

额外优化建议

  • 如果需要随机生成特殊苹果,可以把随机数种子加入World状态,用State结合Random模块来生成,避免IO污染纯函数循环。
  • 可以给特殊苹果加生命周期,比如一段时间后消失,同样在update函数里用State操作处理。

内容的提问来源于stack exchange,提问作者Bar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 10:14:43