生命游戏的程序实现

生命游戏(Game of Life)是由英国数学家约翰·康威(John Conway)在1970年发明的一款零玩家游戏。游戏的规则简单明了,但由此可以产生复杂的行为模式。

关于SDL.NET

为了构建这个项目,需要使用SDL.NET。SDL.NET为SDL游戏库提供了用C#编写的.NET绑定。要创建这个游戏,从下载并安装了版本为sdl_dotnet-6.1.1beta-sdk-setup.exe的SDL.NET。

如何运行生命游戏

默认情况下,这个程序使用Conway的生命游戏规则:一个活着的细胞如果有两个或三个活着的邻居就会在下一代中存活,一个死亡的细胞如果恰好有三个活着的邻居就会变成活着。其他规则也是可能的,可以通过从命令行加载.lif文件来设置。

鼠标命令:

  • 左键点击 - 使细胞变为活着
  • 右键点击 - 使细胞变为死亡

键盘命令:

  • 回车 - 开始游戏
  • 空格 - 暂停游戏
  • 退格 - 清除网格

这个模块可以读取两种格式的生命游戏文件:Life 1.05和1.06。程序中包含了示例文件,位于Data文件夹中。要将数据加载到生命游戏网格中,可以使用文件路径作为命令行参数运行程序,例如:

GOL.exe C:\Life\Data\highlife.lif

生命游戏实现

GOL项目包含4个模块:

  • GOL - 程序的主入口点,处理图形显示、键盘和鼠标输入以及命令行参数
  • GOLGrid - 保存网格中细胞的生死状态,并处理每一代的变化
  • ReadLife - 从.lif文件加载数据到游戏网格
  • General - 包含项目中使用的一般功能和活跃模式的集合

当应用程序运行时,会调用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()

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()

生命游戏中,一个细胞可以是活着的或死亡的。

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的值设置下一代的细胞值,然后更新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模块可以从两种格式中加载数据到GOL程序中 - Life 1.05和1.06。有关这些格式的更多信息可以在Cellular Automata文件格式中找到。

readFromLifeFile函数读取文件到一个字符串数组中,如果文件未找到则抛出异常。然后它使用两个函数中的一个加载数据,具体取决于格式。

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..] |> ignore | _ -> 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

General模块包含一般功能和主动模式。主动模式用于对函数调用结果进行模式匹配。

StartsWith检查一个字符串是否以子字符串开头,并在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函数使用管道操作符|>来返回一个整数列表,当给定一个数字字符串时。这在createLife1_05中用于获取设置出生和生存规则的整数列表。

let stringToIntList (str : string) = str.ToCharArray() |> Array.map (fun x -> x.ToString()) |> Array.map (fun x -> Int32.Parse(x)) |> Array.toList
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485