F# 和 SDL.NET 实现康威生命游戏





5.00/5 (1投票)
这是使用 F# 和 SDL.NET 实现康威生命游戏。

引言
概述
这是使用 F# 和 SDL.NET 实现的康威生命游戏。生命游戏是一个零玩家游戏,由英国数学家约翰·康威于 1970 年发明。生命游戏的规则很简单,但这些规则可能产生复杂的行为。
关于 SDL.NET
要构建此项目,需要 SDL.NET。SDL.NET 提供 C# 编写的 SDL 游戏库的 .NET 绑定。为了创建这个游戏,我从 http://cs-sdl.sourceforge.net/ 安装了 sdldotnet-6.1.1beta-sdk-setup.exe 版本。
如何运行生命游戏
规则
默认情况下,此程序使用康威生命游戏的规则:一个存活的细胞,如果邻居数量为两个或三个,则在下一代继续存活;一个死亡的细胞,如果邻居数量恰好为三个,则变为存活。也可以设置其他规则,可以通过命令行加载 .lif 文件到程序中来设置。
鼠标和键盘命令
鼠标命令
- 左键单击 - 使细胞存活
- 右键单击 - 使细胞死亡
键盘命令
- 回车键 - 开始游戏
- 暂停键 - 暂停游戏
- 退格键 - 清除网格
从 .lif 文件加载数据
此模块以两种格式读取生命游戏文件:Life 1.05 和 1.06。程序中包含示例文件,位于 Data 文件夹中。要将数据加载到生命游戏网格中,请使用文件路径作为命令行参数运行程序,例如:GOL.exe C:\Life\Data\highlife.lif。
生命游戏实现
项目结构
GOL 项目包含 4 个模块
- GOL - 程序的入口点,负责图形显示、键盘和鼠标输入以及命令行参数
- GOLGrid - 存储存活和死亡细胞网格的状态,并处理其在每一代的变化
- ReadLife - 从 .lif 文件加载数据到游戏网格
- General - 一系列通用的函数和项目中使用的活动模式
GOL
应用程序运行时会调用 go
函数。它获取命令行参数(如果有),并添加鼠标和键盘事件的处理程序。
let go() =
let args = System.Environment.GetCommandLineArgs()
if args.Length > 1 then readFromLifeFile args.[1]
clearScreen()
Events.KeyboardDown.Add(HandleInputDown)
Events.MouseButtonDown.Add(HandleMouseClick)
Events.Quit.Add(quit)
Events.Tick.Add(update)
Events.Run()
go()
HandleInputDown
函数使用模式匹配来选择按下特定键时要调用的函数。
let HandleInputDown(args : KeyboardEventArgs) =
match args.Key with
| Key.Escape ->Events.QuitApplication()
| Key.Backspace -> clearGrid()
| Key.Return -> runGame()
| Key.Pause -> pauseGame()
| _ -> None |>ignore
HandleMouseClick
函数在鼠标主按钮(通常是左键)单击时设置细胞为存活状态,在游戏暂停或尚未开始时,使用其他鼠标按钮设置细胞为死亡状态。
let HandleMouseClick(args : MouseButtonEventArgs) =
if isRunning = false then
let x = args.X
let y = args.Y
match args.Button with
| MouseButton.PrimaryButton -> setCell (int y) (int x) true
drawCell x y cellColour
| _ -> setCell (int y) (int x) false
drawCell x y Color.White
drawGrid()
GOLGrid
生命游戏中的细胞可以是存活或死亡的。
type private State =
| DEAD = 0
| ALIVE = 1
默认情况下,一个细胞在其有 3 个存活邻居时出生,并在有 2 或 3 个存活邻居时继续存活。这些规则可以通过使用 ReadLife
模块加载 .lif 文件来更改。
let mutable private born = [3]
let mutable private survive = [2;3]
let setRules birthList survivalList =
born <- birthList
survive <- survivalList
newValue
函数根据细胞的总存活邻居数量,返回网格中细胞的 ALIVE
或 DEAD
值。它使用活动模式 IsMember
来检查总数是否在 born
或 survive
列表中。
let private newValue row col =
let total = countNeighbours row col in
match total with
| IsMember born when isDead row col -> State.ALIVE
| IsMember survive when isAlive row col -> State.ALIVE
| _ -> State.DEAD
next
函数根据当前 grid
的值设置下一代 nextGrid
中细胞的值,然后更新 grid
。
let next() =
for row = 0 to (height-1) do
for col = 0 to (width - 1) do
nextGrid.[row,col] <- newValue row col
for row = 0 to (height-1) do
for col = 0 to (width - 1) do
grid.[row,col] <- nextGrid.[row,col]
ReadLife
ReadLife
模块可以从两种格式加载数据到 GOL 程序中 - Life 1.05 和 1.06。有关这些格式的更多信息可以在 细胞自动机文件格式 中找到。
readFromLifeFile
函数将文件读取为 string
数组,如果文件未找到则抛出异常。然后它使用其中一个函数来加载数据,具体取决于格式。
let readFromLifeFile lifeFile =
try
let lifeData = File.ReadAllLines(lifeFile)
match lifeData.[0] with
| "#Life 1.05" -> createLife1_05 lifeData |> ignore
| "#Life 1.06" -> createLife1_06 lifeData.[1..]
| _ -> None |> ignore
with
| :? System.IO.FileNotFoundException
-> printfn "System.IO.FileNotFoundException:
The file %s was not found" lifeFile
| _ -> printfn
"Exception occurred when trying to read %s" lifeFile
在读取文件的每一行时,createLife1_05
会为以 #N
或 #R
开头的行设置游戏的生存和出生规则,为以 #P
开头的行设置中心偏移量,或者将该行传递给内部函数 setCellsLife
来设置网格的细胞死亡或存活。其他行,如以 #D
开头的描述行或空行,则被忽略。变量 X
和 Y
在 createLife1_05
和 setCellsLife
外部未使用,因此它们是使用可变引用单元的闭包。
let private createLife1_05 (lifeData : string[]) =
let X = ref 0
let Y = ref 0
let setCellsLife (line : string) =
let startX = !X
for ch in line do
match ch with
| '.' -> setDead !Y !X
| '*' -> setAlive !Y !X
| _ -> None |> ignore
X := !X + 1
Y := !Y + 1
X := startX
for line in lifeData do
match line with
| StartsWith "#N"-> setRules [3] [2;3]
| StartsWith "#R"-> let [| _ ; survival ; birth |]
= line.Trim().Split([|' ';'/' |])
setRules (stringToIntList birth)(stringToIntList survival)
| StartsWith "#P"-> let [|_; x ; y|] = line.Trim().Split(' ')
X := Int32.Parse( x) + centreCol
Y := Int32.Parse (y) + centreRow
| StartsWith "."
| StartsWith "*" -> setCellsLife line
| _ -> None |> ignore
左键单击 gadget 并拖动以移动它。左键单击 gadget 的右下角并拖动以调整其大小。右键单击 gadget 以访问其属性。
General 模块包含通用的函数和活动模式。活动模式用于对函数调用的结果进行模式匹配。StartsWith
检查一个 string
是否以子字符串开头,并在 ReadLife
模块的 createLife1_05
中使用。
let (|StartsWith|_|) substr (str : string) =
if str.StartsWith substr then Some() else None
IsMember
检查一个项是否在一个列表中,并在 GOLGrid
模块的 newValue
函数中使用。
let (|IsMember|_|) list item =
if (List.tryFind (fun x -> x = item) list)
= Some(item) then Some() else None
stringToIntList
函数使用管道 |>
来在给定数字 string
时返回整数列表。这在 createLife1_05
中用于获取整数列表以设置出生和生存规则。
let stringToIntList (str : string) =
str.ToCharArray() |> Array.map (fun x -> x.ToString())
|> Array.map (fun x -> Int32.Parse(x))
|> Array.toList
示例
这些示例模式可以在 Data 文件夹中找到。
滑翔机 - glider.lif
滑翔机是一种对角移动的模式,需要四个世代才能恢复其原始形状。生命游戏的规则并未提及移动,但移动模式是这些规则的衍生物。
振荡器 - oscillators.lif
振荡器是停留在原地周期的模式。oscillators.lif 中的模式从左到右依次是闪烁器、脉冲器和十五边形。
R-五格马 - r-pentomino.lif
这最初是一个简单的五细胞模式,但会生成许多模式,最终稳定下来。滑翔机最早是在 R-五格马中发现的。
滑翔机枪 - 13glidercollision.lif
滑翔机枪生成源源不断的滑翔机,它是由 13 个滑翔机的碰撞产生的。
Highlife 复制器 - highlife.lif
HighLife 的规则与康威生命游戏略有不同:一个细胞如果有 2 或 3 个邻居则存活,如果有 3 或 6 个邻居则出生。HighLife
中的复制器模式会复制自身。
历史
- 2011 年 2 月 4 日:初始帖子