用于生命游戏的 OCaml 二维数组

OCaml 2D array for Game of Life

我正在尝试做 Conway 的生命游戏来练习,因为我以前用 C++ 做过这个,但我想知道如何在 Ocaml 中生成一个给定高度和宽度的二维数组,并随机分配生成基于给定的人口密度,我正在尝试使用make_matrix。我在网上找到的所有教程都使用图形或其他一些硬编码方式(即 rosetta 代码),但我想尝试避免这种情况,以便我可以有一些变化。谢谢。

这是我当前的代码,它返回未绑定的值。

print_string "Input width ";
let num = read_int () in
 print_string "Input height";
 let num2 = read_int () in
  myArray = Array.make_matrix num num2 0;

error: Unbound value myArray;

这里有多项选择:

  1. 大数组
  2. 数组
  3. 地图

双数组的值只能是数字,但它们可以是任意大小和维数。它们还有一个用于访问多维数据的漂亮界面。数组,可以有任何类型的值,你可以创建数组的数组来模拟多维空间。数组和双数组都是命令式数据结构,您的代码也是。这就是为什么我建议您使用持久映射,使您的代码纯函数化(如果您想在 OCaml 中练习,最好在其功能子空间中练习)。

地图示例

那么,让我们为状态和坐标定义一个类型:

type state = Dead | Live

type coord = {x : int; y : int}

由于我们将使用地图,因此不需要未填充状态。无人居住的州只是未映射。

现在,我们可以定义棋盘数据结构的实现了:

module Board = Map.Make(struct 
    type t = coord
    let compare = compare
  end)

作为用法示例,让我们定义一个名为 fold_neighbors 的高阶函数,它将把用户提供的函数应用于每个已填充的相邻单元格。

let neighbors {x;y} = [
  x,  y+1;
  x+1,y+1;
  x+1,y;
  x+1,y-1;
  x,  y-1;
  x-1,y-1;
  x-1,y;
  x-1,y+1;
] |> List.map (fun (x,y) -> {x;y})

let fold_neighbors board cell ~f ~init =
  neighbors cell |>
  List.fold_left (fun acc n -> 
      try f acc (Board.find n board) 
      with Not_found -> acc)
    init

使用这个通用迭代器函数,我们可以定义专门的函数,例如 count_live_neighbors:

let count_live_neighbors =
  fold_neighbors ~init:0 ~f:(fun count nb -> match nb with
      | Live -> count + 1
      | Dead -> count)

实现假设一个无限的棋盘,如果你想让它有界,那么你需要调整fold_neighbors函数来排除那些躺在棋盘外面的人。

数组示例

另一种选择是使用普通数组。我们可以使用一个方便的 make_matrix 函数来创建一个给定大小的二维数组并用提供的值填充它,例如

 make_matrix 300 400 0 

将创建一个 300 行和 400 列的零填充矩阵。

在我们的例子中,我们不想用数字填充矩阵,而是用状态填充矩阵。我们将需要一个状态类型,它能够表示 3 个状态,我们将重用前面示例中的 state 类型,但我们还将其包装到 option 类型中,以便一个单元格是死的还是活的将表示为 Some DeadSome Live,而未填充的将只是 None,因此我们可以创建一个空板

Array.make_matrix width height None

要初始化我们的电路板,我们可以先创建它,然后根据所需的人口密度为随机选择的单元格赋予生命。我们将用 0 到 1 之间的浮点数表示密度,例如,密度 0.1 表示大约 10% 的细胞是活的。为了保持更多的功能,我们将使用 Array.map 来转换我们的数组。带有就地修改的显式循环和迭代当然会更快、更惯用,但为了实验起见,让我们使用更函数式的方法:

let create_board width height density =
  Array.make_matrix width height None |>
  Array.map (Array.map (fun cell ->
      if Random.float 1.0 > density then Some Live else None))

但是,如果我们不首先创建一个空板,而是从初始化的板开始,我们可以做得更好。为此,我们可以使用 Array.init 函数,它允许为每个单元格赋予不同的值,这里是更好的 create_board 函数的示例:

let create_board width height density =
  Array.init height (fun _ -> 
      Array.init width (fun _ ->
          if Random.float 1.0 > density then Some Live else None))

Array.init 函数使用元素的索引调用用户提供的函数。在我们的例子中,生命的概率不依赖于坐标,所以我们可以忽略位置,这就是为什么我们使用 _ 来指定,我们不会使用参数。