生命游戏(Game of Life)是由英国数学家约翰·康威(John Conway)在1970年发明的一款零玩家游戏。游戏的规则简单明了,但由此可以产生复杂的行为模式。
为了构建这个项目,需要使用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个模块:
当应用程序运行时,会调用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