Haskell实现GIF流游戏编程:贪吃蛇实战

本文详细介绍了如何使用Haskell语言和GIF流框架开发贪吃蛇游戏,涵盖游戏状态管理、图形渲染、输入处理和网络传输等核心技术,通过具体代码示例展示函数式编程在游戏开发中的实际应用。

Haskell:使用GIF流进行游戏编程

注意:本文译自德语原文,曾作为卡尔斯鲁厄理工学院"编程范式"课程的作业,当时我负责辅导实践课程。

游戏介绍

贪吃蛇是一款电脑游戏,玩家需要操控蛇在游戏场地中移动。吃到食物会增加蛇的长度。当蛇撞到墙壁或自身时游戏结束。

任务目标

在这份作业中,你将使用Haskell实现贪吃蛇游戏。为此你需要使用gifstream仓库中的框架。游戏输出通过动态GIF流实现,可以在浏览器中观看。

技术基础

支持64种颜色,可表示为从(0,0,0)到(3,3,3)的Int元组:

1
type RGB = (Int,Int,Int)

GIF的单个帧定义为行列表,每行是RGB值列表:

1
type Frame = [[RGB]]

框架提供server函数,在指定端口运行HTTP服务器:

1
server :: PortNumber -> Int -> Logic -> IO ()

编译与运行

Snake.hs文件包含编写贪吃蛇游戏的基础代码。编译并运行游戏(需要安装network和random Haskell包):

1
2
3
4
5
6
$ ghc -O3 -threaded Snake.hs
[1 of 2] Compiling GifStream        ( GifStream.hs, GifStream.o )
[2 of 2] Compiling Main             ( Snake.hs, Snake.o )
Linking Snake ...
$ ./Snake
Listening on http://127.0.0.1:5002/

在浏览器中打开提供的地址。通过在终端中按下WASD键可以影响浏览器中的GIF。网络中的其他参与者也可以通过使用你的网络IP地址而不是127.0.0.1来观看GIF流。

还可以录制GIF流并稍后观看:

1
wget -O game.gif http://127.0.0.1:5002/

核心逻辑函数

Snake.hs中最重要的函数是logic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
logic wait getInput sendFrame = initialState >>= go
  where
    go (State oldAction snake food) = do
      input <- getInput

      -- 生成新状态
      let action = charToAction input oldAction
      let newSnake = snake
      let newFood = food

      let frame = case action of
            MoveUp    -> replicate height (replicate width (3,0,0))
            MoveDown  -> replicate height (replicate width (0,3,0))
            MoveLeft  -> replicate height (replicate width (0,0,3))
            MoveRight -> replicate height (replicate width (3,3,3))

      sendFrame (scale zoom frame)

      wait
      go (State action newSnake newFood)

logic函数为贪吃蛇游戏创建初始状态并将其传递给go函数。该函数使用getInput读取最后按下的键,然后生成新的游戏状态。显示的帧基于按下的键选择。最后sendFrame将新帧发送给所有连接的客户端。在此函数中,每个帧都通过scale进行缩放。对wait的调用会导致等待设置的延迟时间,默认为100毫秒。在函数末尾,它使用新生成的状态尾递归调用自身。

任务步骤

1. 打印游戏场地

使用当前状态生成图像并打印,而不是简单的单色图像。

编写fieldPositions列表,保存游戏场地在其对应位置的坐标:

1
fieldPositions :: [[Position]]

场地大小保存在width和height中。对于3x4大小的场地,fieldPositions如下所示:

1
2
3
4
fieldPositions = [[(0,0),(1,0),(2,0)]
                 ,[(0,1),(1,1),(2,1)]
                 ,[(0,2),(1,2),(2,2)]
                 ,[(0,3),(1,3),(2,3)]]

实现colorize函数,将图像的单个位置映射到颜色,以便通过let frame = map (map (colorize newSnake newFood)) fieldPositions创建新帧。根据该位置是蛇的一部分、食物还是背景,场地应着色不同:

1
colorize :: [Position] -> Position -> Position -> RGB

2. 蛇的行为

接下来实现状态变化,以便游戏逻辑可以写为let newSnake = moveSnake snake food action

1
moveSnake :: [Position] -> Position -> Action -> Position

蛇被定义为位置列表。根据传递的动作,新蛇获得新的头部。Action定义如下:

1
data Action = MoveLeft | MoveRight | MoveUp | MoveDown deriving Eq

在尾部,最后一个元素被切断,除非蛇刚刚吃到食物。需要确保用户选择的动作是可能的。

编写validateAction函数,以便游戏逻辑可以通过let action = validateAction oldAction (charToAction input oldAction)进行扩展。为此,validateAction应仅在可能时返回新动作,否则返回旧动作。

3. 食物行为

接下来实现食物的状态变化,以便游戏逻辑可以通过newFood <- moveFood newSnake food进行扩展:

1
moveFood :: [Position] -> Position -> IO Position

当蛇当前没有吃到食物时,可以直接返回食物的旧位置。否则应选择食物的新位置。避免食物出现在蛇的身体内部。

可以使用do语法生成x和y之间(包括)的随机数:r <- randomRIO (x,y)。因此需要导入System.Random模块。

4. 游戏结束

调整logic的结尾,以便使用newSnake检查新状态的有效性。在无效状态下,应通过调用initialstate >>= go重新启动游戏:

1
checkGameOver :: [Position] -> Bool

完整解决方案

完整的解决方案可在GitHub仓库中找到。

附加任务

使用GIF流输出编程另一个游戏,例如Pong、俄罗斯方块或康威生命游戏。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计