elixir 规格
by Artur Trzop
通过Artur Trzop
如何在Elixir中构建分布式的生活游戏 (How to build a distributed Game of Life in Elixir)
I wrote my first game in Elixir. It is a popular thing — Conway’s Game of Life — but it gets quite interesting when you solve it in a functional language, especially when you can see how the actor model works and how actors are distributed across servers in the network.
我用Elixir写了我的第一场比赛。 这是很流行的事情-Conway的《人生游戏》,但是当您用一种功能语言解决它时,它就会变得非常有趣,尤其是当您看到参与者模型如何工作以及参与者如何在网络中的服务器之间分布时。
In this blog post I am going to show:
在这篇博客中,我将展示:
- how to write rules for the game of life with tests in Elixir, 如何通过Elixir中的测试编写生活游戏规则,
- parallel tasks across lightweight processes (actors) in order to utilize all CPU cores, 为了利用所有CPU内核,跨轻量级进程(actor)执行并行任务,
- how to distribute work across nodes so the game can be executed by many servers in the cluster, 如何在节点之间分配工作,以便游戏可以由集群中的许多服务器执行,
- how to use GenServer behaviour, TaskSupervisor and Agents in Elixir. 如何在Elixir中使用GenServer行为,TaskSupervisor和代理。
This project and the full source code can be found here.
该项目和完整的源代码可以在这里找到 。
演示版 (Demo)
Let’s start with watching quick demo of how the game works.
让我们从观看游戏运行方式的快速演示开始。
GameOfLife Recorded by ArturTasciinema.org
ArturT asciinema.org 记录的 GameOfLife
As you can see, node1 represents running game and board on the screen. The second node was also started and connected to the first one. From the second node, we added new cells to the board. Both nodes are responsible for processing the game, but only the first node is a primary with information about the current state of the game. We can connect more nodes to the cluster so game processing can happen on all of the nodes. You are going to learn in this article how to make it happen.
如您所见,node1代表屏幕上正在运行的游戏和棋盘。 第二个节点也已启动并连接到第一个节点。 从第二个节点开始,我们向板上添加了新单元。 两个节点都负责处理游戏,但是只有第一个节点才是具有有关游戏当前状态信息的主要节点。 我们可以将更多节点连接到群集,以便可以在所有节点上进行游戏处理。 您将在本文中学习如何实现它。
生活游戏规则 (Game of Life rules)
If you already know about the game of life problem just jump to the next header. If not, in this section you can learn the basic concept.
如果您已经了解生活游戏问题 ,请跳到下一个标题 。 如果没有,您可以在本节中学习基本概念。
The universe of the Game of Life is an infinite two-dimensional orthogonal grid of square cells, each of which is in one of two possible states, alive or dead. Every cell interacts with its eight neighbours, which are the cells that are horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur:
生命游戏的宇宙是一个无限的二维正交方格,每个方格处于两种可能的状态,即活着的或死亡的。 每个单元都与其八个邻居进行交互,这八个邻居是水平,垂直或对角线相邻的。 在每个时间步上,都会发生以下转换:
- Any live cell with fewer than two live neighbours dies as if caused by under-population. 任何具有少于两个活邻居的活细胞都会死亡,就好像是人口不足造成的。
- Any live cell with two or three live neighbours lives on to the next generation. 任何有两个或三个活邻居的活细胞都可以存活到下一代。
- Any live cell with more than three live neighbours dies, as if by over-population. 任何具有三个以上活邻居的活细胞都将死亡,就好像人口过多一样。
- Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction. 具有正好三个活邻居的任何死细胞都将变成活细胞,就像通过繁殖一样。
The initial pattern constitutes the seed of the system. The first generation is created by applying the above rules simultaneously to every cell in the seed — births and deaths occur simultaneously, and the discrete moment at which this happens is sometimes called a tick (in other words, each generation is a pure function of the preceding one). The rules continue to be applied repeatedly to create further generations.
初始模式构成了系统的种子。 第一代是通过将上述规则同时应用于种子中的每个细胞而创建的-生与死同时发生,有时发生这种情况的离散时刻称为滴答声(换句话说,每一代都是纯种的功能)前一个)。 该规则将继续反复应用以创造更多的后代。
在Elixir中创建新的应用程序 (Create new application in Elixir)
First things first, so we are going to create a new Elixir OTP application with supervision tree. We will use supervisor for our game server, you will learn more about it a bit later.
首先,我们将使用监督树创建一个新的Elixir OTP应用程序。 我们将使用主管作为游戏服务器,稍后您将进一步了解。
$ mix new --sup game_of_life
A --sup
option is given to generate an OTP application skeleton including a supervision tree. Normally an app is generated without a supervisor and without the app callback.
提供了--sup
选项以生成包括监督树的OTP应用程序框架。 通常,在没有主管和没有应用程序回调的情况下生成应用程序。
In lib/game_of_life.ex
file you will find an example of how to add child worker to supervisor.
在lib/game_of_life.ex
文件中,您将找到一个示例,该示例介绍了如何将童工添加到主管。
# lib/game_of_life.exdefmodule GameOfLife do use Application # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do import Supervisor.Spec, warn: false children = [ # Define workers and child supervisors to be supervised # worker(GameOfLife.Worker, [arg1, arg2, arg3]), ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: GameOfLife.Supervisor] Supervisor.start_link(children, opts) endend
代表《人生游戏》中的董事会 (Represent the board in Game of Life)
We need to represent the alive cells on the board in our game. A single cell can be a tuple {x, y}
with coordinates in the 2-dimensional board.
我们需要在游戏中代表板上的活细胞。 单个单元格可以是在二维板上具有坐标的元组{x, y}
。
All alive cells on the board will be on the list alive_cells
.
板上所有活动的单元格将在alive_cells
列表中。
alive_cells = [{0, 0}, {1, 0}, {2, 0}, {1, 1}, {-1,-2}]
Here is an example of how this board with alive cells looks like:
这是一个带有活细胞的电路板的示例:
and here are proper x & y coordinates:
这是正确的x&y坐标:
Now when we have the idea of how we are going to store our alive cells we can jump to write some code.
现在,当我们有了关于如何存储活动细胞的想法时,我们可以跳转编写一些代码。
测试人生游戏规则 (Game of Life rules with tests)
We can create GameOfLife.Cell
module with function keep_alive?/2
responsible for determining if a particular alive cell {x, y}
should be still alive on the next generation or not.
我们可以创建带有函数keep_alive?/2
GameOfLife.Cell
模块, GameOfLife.Cell
模块负责确定特定的活细胞{x, y}
在下一代中是否仍然活着。
Here is the function with expected arguments:
这是带有预期参数的函数:
# lib/game_of_life/cell.exdefmodule GameOfLife.Cell do def keep_alive?(alive_cells, {x, y} = _alive_cell) do # TODO endend
Let’s write some tests to cover the first of the requirements of the Game of Life:
让我们编写一些测试来满足《生命游戏》的第一个要求:
Any live cell with fewer than two live neighbours dies, as if caused by under-population.
任何具有少于两个活邻居的活细胞都会死亡,好像是由人口不足造成的。
We wrote tests to ensure GameOfLife.Cell.keep_alive?/2
function returns false in a case when the alive cell has no neighbours or has just one.
我们编写了测试以确保GameOfLife.Cell.keep_alive?/2
函数在活细胞没有邻居或只有一个邻居的情况下返回false。
# test/game_of_life/cell_test.exsdefmodule GameOfLife.CellTest do use ExUnit.Case, async: true test "alive cell with no neighbours dies" do alive_cell = {1, 1} alive_cells = [alive_cell] refute GameOfLife.Cell.keep_alive?(alive_cells, alive_cell) end test "alive cell with 1 neighbour dies" do alive_cell = {1, 1} alive_cells = [alive_cell, {0, 0}] refute GameOfLife.Cell.keep_alive?(alive_cells, alive_cell) endend
GameOfLife.Cell.keep_alive?/2
function needs to return false just to pass our tests so let’s add more tests to cover other requirements.
GameOfLife.Cell.keep_alive?/2
函数需要返回false才能通过测试,因此让我们添加更多测试来满足其他要求。
Any live cell with more than three live neighbours dies, as if by over-population.
任何具有三个以上活邻居的活细胞都会死亡,就好像人口过多一样。
# test/game_of_life/cell_test.exstest "alive cell with more than 3 neighbours dies" do alive_cell = {1, 1} alive_cells = [alive_cell, {0, 0}, {1, 0}, {2, 0}, {2, 1}] refute GameOfLife.Cell.keep_alive?(alive_cells, alive_cell)end
Any live cell with two or three live neighbours lives on to the next generation.
任何有两个或三个活邻居的活细胞都可以存活到下一代。
# test/game_of_life/cell_test.exstest "alive cell with 2 neighbours lives" do alive_cell = {1, 1} alive_cells = [alive_cell, {0, 0}, {1, 0}] assert GameOfLife.Cell.keep_alive?(alive_cells, alive_cell)endtest "alive cell with 3 neighbours lives" do alive_cell = {1, 1} alive_cells = [alive_cell, {0, 0}, {1, 0}, {2, 1}] assert GameOfLife.Cell.keep_alive?(alive_cells, alive_cell)end
Now, we can implement our GameOfLife.Cell.keep_alive?/2
function.
现在,我们可以实现GameOfLife.Cell.keep_alive?/2
函数。
# lib/game_of_life/cell.exdefmodule GameOfLife.Cell do def keep_alive?(alive_cells, {x, y} = _alive_cell) do case count_neighbours(alive_cells, x, y, 0) do 2 -> true 3 -> true _ -> false end end defp count_neighbours([head_cell | tail_cells], x, y, count) do increment = case head_cell do {hx, hy} when hx == x - 1 and hy == y - 1 -> 1 {hx, hy} when hx == x and hy == y - 1 -> 1 {hx, hy} when hx == x + 1 and hy == y - 1 -> 1 {hx, hy} when hx == x - 1 and hy == y -> 1 {hx, hy} when hx == x + 1 and hy == y ->; 1 {hx, hy} when hx == x - 1 and hy == y + 1 ->; 1 {hx, hy} when hx == x and hy == y + 1 -> 1 {hx, hy} when hx == x + 1 and hy == y + 1 -> 1 _not_neighbour -> 0 end count_neighbours(tail_cells, x, y, count + increment) end defp count_neighbours([], _x, _y, count), do: countend
As you can see, we implemented the private function count_neighbours/4
responsible for counting neighbours. It will be helpful to meet the next rule.
如您所见,我们实现了私有函数count_neighbours/4
来对邻居进行计数。 满足下一条规则将很有帮助。
There is one more requirement we forgot about:
我们忘记了另一个要求:
Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
具有正好三个活邻居的任何死细胞都将变成活细胞,就像通过繁殖一样。
We are going to write a new function GameOfLife.Cell.become_alive?/2
expecting the coordinates of the dead cell and returning if the dead cell should become alive or not.
我们将编写一个新函数GameOfLife.Cell.become_alive?/2
期望死细胞的坐标,并返回死细胞是否应该存活。
# lib/game_of_life/cell.exdefmodule GameOfLife.Cell do def become_alive?(alive_cells, {x, y} = _dead_cell) do 3 == count_neighbours(alive_cells, x, y, 0) endend
And here is the test for that:
这是对此的测试:
# test/game_of_life/cell_test.exstest "dead cell with three live neighbours becomes a live cell" do alive_cells = [{2, 2}, {1, 0}, {2, 1}] dead_cell = {1, 1} assert GameOfLife.Cell.become_alive?(alive_cells, dead_cell)endtest "dead cell with two live neighbours stays dead" do alive_cells = [{2, 2}, {1, 0}] dead_cell = {1, 1} refute GameOfLife.Cell.become_alive?(alive_cells, dead_cell)end
There is one more thing which might be helpful for us. We have the list of alive cells but we don’t know much about the dead cells. The number of dead cells is infinite so we need to cut down the number of dead cells for which we want to check if they should become alive. The simple way would be to check only dead cells with alive neighbours. Hence the GameOfLife.Cell.dead_neighbours/1
function.
还有另一件事可能对我们有帮助。 我们有一个活细胞的列表,但是我们对死细胞的了解不多。 死细胞的数量是无限的,因此我们需要减少要检查的死细胞是否应该存活的数量。 一种简单的方法是只检查与存活邻居的死细胞。 因此, GameOfLife.Cell.dead_neighbours/1
函数。
Let’s write some tests first:
让我们先编写一些测试:
# test/game_of_life/cell_test.exstest "find dead cells (neighbours of alive cell)" do alive_cells = [{1, 1}] dead_neighbours = GameOfLife.Cell.dead_neighbours(alive_cells) |> Enum.sort expected_dead_neighbours = [ {0, 0}, {1, 0}, {2, 0}, {0, 1}, {2, 1}, {0, 2}, {1, 2}, {2, 2} ] |>; Enum.sort assert dead_neighbours == expected_dead_neighboursendtest "find dead cells (neighbours of alive cells)" do alive_cells = [{1, 1}, {2, 1}] dead_neighbours = GameOfLife.Cell.dead_neighbours(alive_cells) |> Enum.sort expected_dead_neighbours = [ {0, 0}, {1, 0}, {2, 0}, {3, 0}, {0, 1}, {3, 1}, {0, 2}, {1, 2}, {2, 2}, {3, 2} ] |> Enum.sort assert dead_neighbours == expected_dead_neighboursend
and here is the implemented function:
这是实现的功能:
# lib/game_of_life/cell.exdef dead_neighbours(alive_cells) do neighbours = neighbours(alive_cells, []) (neighbours |> Enum.uniq) -- alive_cellsenddefp neighbours([{x, y} | cells], neighbours) do neighbours(cells, neighbours ++ [ {x - 1, y - 1}, {x , y - 1}, {x + 1, y - 1}, {x - 1, y }, {x + 1, y }, {x - 1, y + 1}, {x , y + 1}, {x + 1, y + 1} ])enddefp neighbours([], neighbours), do: neighbours
Basically, these are all rules implemented in the single module GameOfLife.Cell
. You can see the whole module file with tests on GitHub.
基本上,这些都是在单个模块GameOfLife.Cell
实现的所有规则。 您可以在GitHub上 查看带有测试的整个模块文件 。
分布式生活游戏的架构 (The architecture of distributed Game of Life)
Our main supervisor is GameOfLife.Supervisor
which I mentioned at the beginning of the article. Below you can see how we defined its children like Task.Supervisor
, workers for BoardServer
and GamePrinter
.
我们的主要主管是GameOfLife.Supervisor
,我在本文开头提到。 在下面,您可以看到我们如何定义其子Task.Supervisor
,如Task.Supervisor
, BoardServer
和GamePrinter
。
# lib/game_of_life.exdefmodule GameOfLife do use Application # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do import Supervisor.Spec, warn: false init_alive_cells = [] children = [ # Define workers and child supervisors to be supervised # worker(GameOfLife.Worker, [arg1, arg2, arg3]), supervisor(Task.Supervisor, [[name: GameOfLife.TaskSupervisor]]), worker(GameOfLife.BoardServer, [init_alive_cells]), # We will uncomment this line later # worker(GameOfLife.GamePrinter, []), ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: GameOfLife.Supervisor] Supervisor.start_link(children, opts) endend
Let me describe what each component on the image is responsible for.
让我描述一下图像上的每个组件所负责的事情。
Task.Supervisor
is an Elixir module defining a new supervisor which can be used to dynamically supervise tasks. We are going to use it to spin off tasks like determining if the particular cell should live or die. Those tasks can be run across nodes connected into the cluster.
Task.Supervisor
是一个Elixir模块,它定义了一个新的Supervisor,可用于动态监督任务。 我们将使用它来剥离诸如确定特定细胞是否应该存活或死亡的任务。 这些任务可以在连接到集群的节点上运行。
In the above code, we gave the name GameOfLife.TaskSupervisor
for our supervisor. We will use this name to tell the Task.Supervisor.async
function which Task Supervisor should handle our task. You can read more about Task.Supervisor here.
在上面的代码中,我们为主管指定了名称GameOfLife.TaskSupervisor
。 我们将使用此名称告诉Task.Supervisor.async
函数哪个Task Supervisor应该处理我们的任务。 您可以在此处阅读有关Task.Supervisor的更多信息。
GameOfLife.BoardServer
is our module implemented as GenServer behaviour. It is responsible for holding the state of the game. By that I mean it keeps the list of alive cells on the board along with generation counter and TRef. TRef is a timer reference returned by the Erlang timer module and the apply_interval function.
GameOfLife.BoardServer
是我们作为GenServer行为实现的模块。 它负责保持游戏状态。 就是说,我的意思是它将活细胞列表以及生成计数器和TRef保留在板上。 TRef是由Erlang计时器模块和apply_interval函数返回的计时器参考。
We want to start the game and generate a new list of alive cells for the next generation with a specified time interval. With each new generation, we will update the generation counter. The other interesting thing is that GameOfLife.BoardServer
is running only on a single node. Once another node is connected to the cluster where is already running GameOfLife.BoardServer
then GameOfLife.BoardServer
won’t be started just like that on the newly connected node.
我们要开始游戏,并在指定的时间间隔内为下一代生成一个新的活细胞列表。 对于每个新世代,我们将更新世代计数器。 另一个有趣的事情是GameOfLife.BoardServer
仅在单个节点上运行。 一旦另一个节点连接到已经在运行GameOfLife.BoardServer
的群集,则GameOfLife.BoardServer
将不会像新连接的节点上那样启动。
Instead on the new node GameOfLife.BoardServer
will keep the only reference to the PID of the process existing on the first node. We want to have the single source of truth about the state of our game in one primary GameOfLife.BoardServer
process existing on the first node started in the cluster.
相反,在新节点上, GameOfLife.BoardServer
将保留对第一个节点上现有进程的PID的唯一引用。 我们希望在群集中启动的第一个节点上存在的一个主要GameOfLife.BoardServer
进程中,拥有有关游戏状态的唯一真实信息。
GameOfLife.GamePrinter
is a simple module using Agent in order to keep TRef (time reference) so we can print the board to STDOUT with the specified interval. We will use Erlang timer module to print the board on the screen every second.
GameOfLife.GamePrinter
是一个使用Agent的简单模块,用于保留TRef(时间参考),因此我们可以按指定的时间间隔将电路板打印到STDOUT。 我们将使用Erlang计时器模块每秒在屏幕上打印该板。
You may wonder what’s the difference between GenServer and Agent.
您可能想知道GenServer和Agent之间有什么区别。
A GenServer is a process like any other Elixir process and it can be used to keep state, execute code asynchronously, and so on. The advantage of using a generic server process (GenServer) is that it will have a standard set of interface functions and include functionality for tracing and error reporting. It also fits into a supervision tree as this is what we did in the GameOfLife
module.
GenServer是一个与任何其他Elixir进程一样的进程,可用于保持状态,异步执行代码等。 使用通用服务器进程(GenServer)的优点是它将具有一组标准的接口功能,并包括用于跟踪和错误报告的功能。 它也适合于监督树,因为这是我们在GameOfLife
模块中GameOfLife
。
On the other hand, Agent is a much simpler solution than GenServer. Agents are a simple abstraction around state. Often in Elixir there is a need to share or store state that must be accessed from different processes or by the same process at different points in time. The Agent module provides a basic server implementation that allows state to be retrieved and updated via a simple API. This is what we are going to do in GameOfLife.GamePrinter
as we only need to keep time reference to our timer interval.
另一方面,Agent是比GenServer更简单的解决方案。 代理是围绕状态的简单抽象。 在Elixir中,经常需要共享或存储必须从不同进程或同一进程在不同时间点访问的状态。 代理模块提供了一种基本的服务器实现,该实现允许通过简单的API检索和更新状态。 这就是我们在GameOfLife.GamePrinter
中要做的事情,因为我们只需要保持对计时器间隔的时间参考即可。
为任务主管创建节点管理器 (Create node manager for task supervisor)
Let’s start with something simple just to see if we can distribute work across nodes in the cluster. We assume each new process created by task supervisor will be assigned randomly to one of the connected nodes. Each node should be equally overloaded with the assumption that each task is pretty similar and all nodes are machines with the same configuration and overload.
让我们从简单的事情开始,只是看看我们是否可以在集群中的各个节点之间分配工作。 我们假设任务管理器创建的每个新流程将被随机分配给所连接的节点之一。 假设每个任务都非常相似,并且所有节点都是具有相同配置和过载的机器,则每个节点应同样地过载。
# lib/game_of_life/node_manager.exdefmodule GameOfLife.NodeManager do def all_nodes do [Node.self | Node.list] end def random_node do all_nodes |> Enum.random endend
Our node manager has random_node/0
function which returns the name of a random node connected to the cluster. Basically, that’s it. A simple solution should be enough for now.
我们的节点管理器具有random_node/0
函数,该函数返回连接到集群的随机节点的名称。 基本上就是这样。 现在,一个简单的解决方案就足够了。
创建板辅助功能 (Create board helper functions)
We need some helper functions for operations we can do on the board like adding and removing cells. Let’s start with tests for the module GameOfLife.Board
and function add_cells/2
.
我们需要一些辅助功能来完成板上的操作,例如添加和删除单元格。 让我们从对GameOfLife.Board
模块和add_cells/2
函数的测试开始。
# test/game_of_life/board_test.exsdefmodule GameOfLife.BoardTest do use ExUnit.Case, async: true test "add new cells to alive cells without duplicates" do alive_cells = [{1, 1}, {2, 2}] new_cells = [{0, 0}, {1, 1}] actual_alive_cells = GameOfLife.Board.add_cells(alive_cells, new_cells) |> Enum.sort expected_alive_cells = [{0, 0}, {1, 1}, {2, 2}] assert actual_alive_cells == expected_alive_cells endend
We need to ensure we won’t allow adding the same cell twice to the board so we test that there are no duplicates. Here is the implementation for add_cells/2
function:
我们需要确保不允许将相同的单元两次添加到板上,以便我们测试是否有重复。 这是add_cells/2
函数的实现:
# lib/game_of_life/board.exdefmodule GameOfLife.Board do def add_cells(alive_cells, new_cells) do alive_cells ++ new_cells |> Enum.uniq endend
Another thing is removing cells from the list of alive cells:
另一件事是从活细胞列表中删除细胞:
# test/game_of_life/board_test.exstest "remove cells which must be killed from alive cells" do alive_cells = [{1, 1}, {4, -2}, {2, 2}, {2, 1}] kill_cells = [{1, 1}, {2, 2}] actual_alive_cells = GameOfLife.Board.remove_cells(alive_cells, kill_cells) expected_alive_cells = [{4, -2}, {2, 1}] assert actual_alive_cells == expected_alive_cellsend
Implementation is super simple:
实现非常简单:
# lib/game_of_life/board.exdef remove_cells(alive_cells, kill_cells) do alive_cells -- kill_cellsend
Let’s create something more advanced. We should determine which cells should still live in the next generation after the tick. Here is a test for the GameOfLife.Board.keep_alive_tick/1
function:
让我们创建一些更高级的东西。 我们应该确定滴答之后哪些细胞仍应存在于下一代中。 这是GameOfLife.Board.keep_alive_tick/1
函数的测试:
# test/game_of_life/board_test.exstest "alive cell with 2 neighbours lives on to the next generation" do alive_cells = [{0, 0}, {1, 0}, {2, 0}] expected_alive_cells = [{1, 0}] assert GameOfLife.Board.keep_alive_tick(alive_cells) == expected_alive_cellsend
The function keep_alive_tick
does a few things like creating a new task with Task.Supervisor
for each alive cell. Tasks will be created across available nodes in the cluster. We calculate if alive cells should stay alive or be removed. The keep_alive_or_nilify/2
function returns if the cell should live or nil
otherwise.
函数keep_alive_tick
可以执行一些操作,例如使用Task.Supervisor
为每个活动单元格创建一个新任务。 将跨集群中的可用节点创建任务。 我们计算存活细胞是否应保持存活或被去除。 keep_alive_or_nilify/2
函数返回该单元格是否应存活或nil
。
We wait with Task.await/1
until all tasks across all nodes finished their work. Tasks are working in parallel but we need to wait for results from each task. We remove from the list the nil
values so at the end we end up with only alive cells for the next generation.
我们等待Task.await/1
直到所有节点上的所有任务完成工作。 任务并行运行,但是我们需要等待每个任务的结果。 我们从列表中删除了nil
值,因此最后我们只剩下了下一代的活细胞。
# lib/game_of_life/board.ex@doc "Returns cells that should still live on the next generation"def keep_alive_tick(alive_cells) do alive_cells |> Enum.map(&(Task.Supervisor.async( {GameOfLife.TaskSupervisor, GameOfLife.NodeManager.random_node}, GameOfLife.Board, :keep_alive_or_nilify, [alive_cells, &1]))) |>; Enum.map(&Task.await/1) |> remove_nil_cellsenddef keep_alive_or_nilify(alive_cells, cell) do if GameOfLife.Cell.keep_alive?(alive_cells, cell), do: cell, else: nilenddefp remove_nil_cells(cells) do cells |> Enum.filter(fn cell -> cell != nil end)end
There is one more case we should handle which is a situation when dead cells should become alive. GameOfLife.Board.become_alive_tick/1
function will be responsible for that.
我们还应该处理另外一种情况,即死细胞应该活着。 GameOfLife.Board.become_alive_tick/1
函数将对此负责。
# test/game_of_life/board_test.exstest "dead cell with three live neighbours becomes a live cell" do alive_cells = [{0, 0}, {1, 0}, {2, 0}, {1, 1}] born_cells = GameOfLife.Board.become_alive_tick(alive_cells) expected_born_cells = [{1, -1}, {0, 1}, {2, 1}] assert born_cells == expected_born_cellsend
This is how our function looks like:
这是我们的函数的样子:
# lib/game_of_life/board.ex@doc "Returns new born cells on the next generation"def become_alive_tick(alive_cells) do GameOfLife.Cell.dead_neighbours(alive_cells) |> Enum.map(&(Task.Supervisor.async( {GameOfLife.TaskSupervisor, GameOfLife.NodeManager.random_node}, GameOfLife.Board, :become_alive_or_nilify, [alive_cells, &1]))) |>; Enum.map(&Task.await/1) |> remove_nil_cellsenddef become_alive_or_nilify(alive_cells, dead_cell) do if GameOfLife.Cell.become_alive?(alive_cells, dead_cell), do: dead_cell, else: nilend
It works similarly to GameOfLife.Board.keep_alive_tick/1
. First, we are looking for the dead neighbours of alive cells. Then for each dead cell we create a new process across the nodes in the cluster to determine if the dead cell should become alive in the next generation.
它的工作原理与GameOfLife.Board.keep_alive_tick/1
类似。 首先,我们正在寻找活细胞的死邻居。 然后,对于每个死单元,我们将在集群中的各个节点上创建一个新进程,以确定死单元是否应在下一代中恢复活动。
You can see the full source code of GameOfLife.Board module and tests on github.
您可以在github上查看GameOfLife.Board模块的完整源代码和测试 。
创建BoardServer (Create BoardServer)
Let’s create GameOfLife.BoardServer
generic server behaviour. We define a public interface for the server.
让我们创建GameOfLife.BoardServer
通用服务器行为。 我们为服务器定义一个公共接口。
# lib/game_of_life/board_server.exdefmodule GameOfLife.BoardServer do use GenServer require Logger @name {:global, __MODULE__} @game_speed 1000 # miliseconds # Client def start_link(alive_cells) do case GenServer.start_link(__MODULE__, {alive_cells, nil, 0}, name: @name) do {:ok, pid} -> Logger.info "Started #{__MODULE__} master" {:ok, pid} {:error, {:already_started, pid}} -> Logger.info "Started #{__MODULE__} slave" {:ok, pid} end end def alive_cells do GenServer.call(@name, :alive_cells) end def generation_counter do GenServer.call(@name, :generation_counter) end def state do GenServer.call(@name, :state) end @doc """ Clears board and adds only new cells. Generation counter is reset. """ def set_alive_cells(cells) do GenServer.call(@name, {:set_alive_cells, cells}) end def add_cells(cells) do GenServer.call(@name, {:add_cells, cells}) end def tick do GenServer.cast(@name, :tick) end def start_game(speed \\ @game_speed) do GenServer.call(@name, {:start_game, speed}) end def stop_game do GenServer.call(@name, :stop_game) end def change_speed(speed) do stop_game start_game(speed) endend
As you can see, we use GenServer
behaviour in our module. The module requires also Logger as we would like to print some info to the STDOUT.
如您所见,我们在模块中使用GenServer
行为。 该模块还需要Logger,因为我们希望将一些信息打印到STDOUT。
In the start_link/1
function we start a new GenServer
. When our generic server starts, it is as a first process in the cluster. Then it becomes the primary process. In the case when there is already a running process with a globally registered name {:global,__MODULE__}
, we log info that our process will be a replica process with a reference to the existing PID on another node in the cluster.
在start_link/1
函数中,我们启动一个新的GenServer
。 当我们的通用服务器启动时,它是集群中的第一个进程。 然后,它成为主要过程。 如果已经有一个全局注册名称为{:global,__MODULE__}
的正在运行的进程,我们将记录信息,该进程将是副本进程,并引用集群中另一个节点上的现有PID。
We store the global name for our server in the attribute @name
. We use another attribute @game_speed
for default game speed which is 1000 milliseconds.
我们将服务器的全局名称存储在属性@name
。 我们使用另一个属性@game_speed
作为默认游戏速度,即1000毫秒。
In our public interface, we have the alive_cells/1
function which returns the list of alive cells. Basically, it is the current state of the game (alive cells on the board). This function calls GenServer
with the registered @name
and requests :alive_cells
. We need to implement the handle_call/3
function for this type of request (:alive_cells
).
在我们的公共接口中,我们具有alive_cells/1
函数,该函数返回活动单元格的列表。 基本上,这是游戏的当前状态(棋盘上的活动单元)。 该函数使用已注册的@name
调用GenServer
并请求:alive_cells
。 我们需要为这种类型的请求( :alive_cells
)实现handle_call/3
函数。
There is another public function generation_counter/1
which returns how many generations were already processed by the board server.
还有另一个公共函数generation_counter/1
,它返回板服务器已经处理了多少代。
The state/1
function returns state that is held by our generic server. The state is represented as the tuple with 3 values like alive cells, TRef (time reference - we want to regenerate board every second) and generation counter. TRef is a very internal thing for the board server so we won’t return this to the outside world. That’s why we will return just alive cells and the generation counter. You will see it later in the implementation for handle_call(:state, _from, state)
.
state/1
函数返回由我们的通用服务器保留的状态。 状态表示为具有3个值的元组,例如活动单元,TRef(时间参考-我们想每秒重新生成板)和生成计数器。 对于板服务器而言,TRef是非常内部的东西,因此我们不会将其返回给外界。 这就是为什么我们只返回存活的细胞和世代计数器的原因。 您稍后会在handle_call(:state, _from, state)
的实现中看到它。
You can use the set_alive_cells/1
function in the case when you want to override the current list of alive cells with a new list.
如果要用新列表覆盖当前的活动单元列表,可以使用set_alive_cells/1
函数。
The add_cells/1
function will be very useful as we want to be able to add new cells or figures to the board. For instance, we may want to add a blinker pattern to the existing game. You will learn more about patterns later.
add_cells/1
函数将非常有用,因为我们希望能够向板上添加新的单元格或图形。 例如,我们可能想在现有游戏中添加闪光模式。 稍后您将了解有关模式的更多信息。
We can manually force the game to calculate the next generation of cells with the tick/1
function.
我们可以使用tick/1
函数手动强制游戏计算下一代单元。
The start_game/1
function is responsible for starting a new timer which calls every second a tick/1
function. Thanks to that our game will update the list of alive cells within a specified interval which is @game_speed
.
start_game/1
函数负责启动一个新计时器,该计时器每秒调用一次tick/1
函数。 因此,我们的游戏将在指定的时间间隔@game_speed
更新存活细胞的列表。
The last 2 functions are stop_game/1
and change_speed/1
which just restart the game and start a new one with the provided speed.
最后两个功能是stop_game/1
和change_speed/1
,它们只是重新启动游戏并以提供的速度开始一个新的功能。
Now you can see how the above functions are working. They are calling server callbacks.
现在您可以看到上述功能的工作方式。 他们正在调用服务器回调。
# lib/game_of_life/board_server.exdefmodule GameOfLife.BoardServer do use GenServer # ... # Server (callbacks) def handle_call(:alive_cells, _from, {alive_cells, _tref, _generation_counter} = state) do {:reply, alive_cells, state} end def handle_call(:generation_counter, _from, {_alive_cells, _tref, generation_counter} = state) do {:reply, generation_counter, state} end def handle_call(:state, _from, {alive_cells, _tref, generation_counter} = state) do {:reply, {alive_cells, generation_counter}, state} end def handle_call({:set_alive_cells, cells}, _from, {_alive_cells, tref, _generation_counter}) do {:reply, cells, {cells, tref, 0}} end def handle_call({:add_cells, cells}, _from, {alive_cells, tref, generation_counter}) do alive_cells = GameOfLife.Board.add_cells(alive_cells, cells) {:reply, alive_cells, {alive_cells, tref, generation_counter}} end def handle_call({:start_game, speed}, _from, {alive_cells, nil = _tref, generation_counter}) do {:ok, tref} = :timer.apply_interval(speed, __MODULE__, :tick, []) {:reply, :game_started, {alive_cells, tref, generation_counter}} end def handle_call({:start_game, _speed}, _from, {_alive_cells, _tref, _generation_counter} = state) do {:reply, :game_already_running, state} end def handle_call(:stop_game, _from, {_alive_cells, nil = _tref, _generation_counter} = state) do {:reply, :game_not_running, state} end def handle_call(:stop_game, _from, {alive_cells, tref, generation_counter}) do {:ok, :cancel} = :timer.cancel(tref) {:reply, :game_stoped, {alive_cells, nil, generation_counter}} end def handle_cast(:tick, {alive_cells, tref, generation_counter}) do keep_alive_task = Task.Supervisor.async( {GameOfLife.TaskSupervisor, GameOfLife.NodeManager.random_node}, GameOfLife.Board, :keep_alive_tick, [alive_cells]) become_alive_task = Task.Supervisor.async( {GameOfLife.TaskSupervisor, GameOfLife.NodeManager.random_node}, GameOfLife.Board, :become_alive_tick, [alive_cells]) keep_alive_cells = Task.await(keep_alive_task) born_cells = Task.await(become_alive_task) alive_cells = keep_alive_cells ++ born_cells {:noreply, {alive_cells, tref, generation_counter + 1}} endend
Oh, we forgot about tests. In this case, we can use DocTest. It allows us to generate tests from the code examples existing in a module/function/macro’s documentation.
哦,我们忘记了测试。 在这种情况下,我们可以使用DocTest 。 它允许我们从模块/功能/宏的文档中存在的代码示例生成测试。
Our test file is super short:
我们的测试文件非常简短:
# test/game_of_life/board_server_test.exsdefmodule GameOfLife.BoardServerTest do use ExUnit.Case doctest GameOfLife.BoardServerend
Let’s add @moduledoc
to GameOfLife.BoardServer
.
让我们将@moduledoc
添加到GameOfLife.BoardServer
。
# lib/game_of_life/board_server.exdefmodule GameOfLife.BoardServer do use GenServer require Logger @moduledoc """ ## Example iex> GameOfLife.BoardServer.start_game :game_started iex> GameOfLife.BoardServer.start_game :game_already_running iex> GameOfLife.BoardServer.stop_game :game_stoped iex> GameOfLife.BoardServer.stop_game :game_not_running iex> GameOfLife.BoardServer.change_speed(500) :game_started iex> GameOfLife.BoardServer.stop_game :game_stoped iex> GameOfLife.BoardServer.set_alive_cells([{0, 0}]) [{0, 0}] iex> GameOfLife.BoardServer.alive_cells [{0, 0}] iex> GameOfLife.BoardServer.add_cells([{0, 1}]) [{0, 0}, {0, 1}] iex> GameOfLife.BoardServer.alive_cells [{0, 0}, {0, 1}] iex> GameOfLife.BoardServer.state {[{0, 0}, {0, 1}], 0} iex> GameOfLife.BoardServer.generation_counter 0 iex> GameOfLife.BoardServer.tick :ok iex> GameOfLife.BoardServer.generation_counter 1 iex> GameOfLife.BoardServer.state {[], 1} """end
As you can see we have grouped 3 examples in the @moduledoc
attribute and they are separated by a new line. When you run tests you will see 3 separate tests.
如您所见,我们在@moduledoc
属性中将3个示例分组,并用新行将其分隔开。 运行测试时,您将看到3个独立的测试。
$ mix test test/game_of_life/board_server_test.exsCompiled lib/game_of_life/board_server.ex20:54:30.637 [info] Started Elixir.GameOfLife.BoardServer master...Finished in 0.1 seconds (0.1s on load, 0.00s on tests)3 tests, 0 failuresRandomized with seed 791637
In GameOfLife.BoardServer
you probably noticed 2 interesting things. First is GameOfLife.Board
which is called in:
在GameOfLife.BoardServer
您可能注意到了2件有趣的事情。 首先是GameOfLife.Board
,它被调用:
# lib/game_of_life/board_server.exdef handle_call({:add_cells, cells}, _from, {alive_cells, tref, generation_counter}) do alive_cells = GameOfLife.Board.add_cells(alive_cells, cells) {:reply, alive_cells, {alive_cells, tref, generation_counter}}end
As you saw before we added some useful functions to GameOfLife.Board
module which helps us to do operations on the list of alive cells.
如您所见,我们在GameOfLife.Board
模块中添加了一些有用的功能,该功能可帮助我们对活细胞列表进行操作。
Another interesting thing is how we use Task.Supervisor
in:
另一个有趣的事情是我们如何在以下方面使用Task.Supervisor
:
# lib/game_of_life/board_server.exdef handle_cast(:tick, {alive_cells, tref, generation_counter}) do keep_alive_task = Task.Supervisor.async( {GameOfLife.TaskSupervisor, GameOfLife.NodeManager.random_node}, GameOfLife.Board, :keep_alive_tick, [alive_cells]) become_alive_task = Task.Supervisor.async( {GameOfLife.TaskSupervisor, GameOfLife.NodeManager.random_node}, GameOfLife.Board, :become_alive_tick, [alive_cells]) keep_alive_cells = Task.await(keep_alive_task) born_cells = Task.await(become_alive_task) alive_cells = keep_alive_cells ++ born_cells {:noreply, {alive_cells, tref, generation_counter + 1}} end
What we are doing here is spinning off a new async process to run the GameOfLife.keep_alive_tick/1
function with the argument alive_cells
.
我们在这里正在做的是分离出一个新的异步过程,以使用参数alive_cells
运行GameOfLife.keep_alive_tick/1
函数。
# lib/game_of_life/board_server.exkeep_alive_task = Task.Supervisor.async( {GameOfLife.TaskSupervisor, GameOfLife.NodeManager.random_node}, GameOfLife.Board, :keep_alive_tick, [alive_cells])
The tuple {GameOfLife.TaskSupervisor, GameOfLife.NodeManager.random_node}
tells Task.Supervisor
that we want to use task supervisor with the name GameOfLife.TaskSupervisor
and we want to run the process on the node returned by the GameOfLife.NodeManager.random_node
function.
元组{GameOfLife.TaskSupervisor, GameOfLife.NodeManager.random_node}
告诉Task.Supervisor
,我们要使用名称为GameOfLife.TaskSupervisor
任务管理器,并且希望在GameOfLife.NodeManager.random_node
函数返回的节点上运行该进程。
创建游戏打印机和控制台演示者 (Create game printer and console presenter)
GameOfLife.GamePrinter
module is running as a worker under the supervision of the GameOfLife
supervisor. GameOfLife.GamePrinter
is using Agent
to store TRef
for timer reference as we want to print the board to the STDOUT with the specified interval.
GameOfLife.GamePrinter
模块在GameOfLife
主管的监督下作为工作人员运行。 GameOfLife.GamePrinter
使用Agent
存储TRef
作为计时器参考,因为我们希望以指定的间隔将电路板打印到STDOUT。
You have already seen the example of using Agent
so this shouldn’t be new for you. Basically, we wrote the public interface to start and stop printing the board to the screen. For tests we used DocTest.
您已经看到了使用Agent
的示例,因此这对您来说不是新事物。 基本上,我们编写了公共界面来开始和停止将板子打印到屏幕上。 对于测试,我们使用DocTest 。
# lib/game_of_life/game_printer.exdefmodule GameOfLife.GamePrinter do @moduledoc """ ## Example iex> GameOfLife.GamePrinter.start_printing_board :printing_started iex> GameOfLife.GamePrinter.start_printing_board :already_printing iex> GameOfLife.GamePrinter.stop_printing_board :printing_stopped iex> GameOfLife.GamePrinter.stop_printing_board :already_stopped """ @print_speed 1000 def start_link do Agent.start_link(fn -> nil end, name: __MODULE__) end def start_printing_board do Agent.get_and_update(__MODULE__, __MODULE__, :do_start_printing_board, []) end def do_start_printing_board(nil = _tref) do {:ok, tref} = :timer.apply_interval(@print_speed, __MODULE__, :print_board, []) {:printing_started, tref} end def do_start_printing_board(tref), do: {:already_printing, tref} def print_board do {alive_cells, generation_counter} = GameOfLife.BoardServer.state alive_counter = alive_cells |> Enum.count GameOfLife.Presenters.Console.print(alive_cells, generation_counter, alive_counter) end def stop_printing_board do Agent.get_and_update(__MODULE__, __MODULE__, :do_stop_printing_board, []) end def do_stop_printing_board(nil = _tref), do: {:already_stopped, nil} def do_stop_printing_board(tref) do {:ok, :cancel} = :timer.cancel(tref) {:printing_stopped, nil} endend
GameOfLife.Presenters.Console
is responsible for printing the board nicely with X & Y axes, the number of alive cells, and the generation counter. Let’s start with tests. We are going to capture STDOUT and compare if data printed to the screen are looking as we expect.
GameOfLife.Presenters.Console
负责用X&Y轴,活细胞数和世代计数器很好地打印电路板。 让我们从测试开始。 我们将捕获STDOUT并比较打印到屏幕上的数据是否符合我们的预期。
# test/game_of_life/presenters/console_test.exsdefmodule GameOfLife.Presenters.ConsoleTest do use ExUnit.Case # allows to capture stuff sent to stdout import ExUnit.CaptureIO test "print cells on the console output" do cell_outside_of_board = {-1, -1} cells = [{0, 0}, {1, 0}, {2, 0}, {1, 1}, {0, 2}, cell_outside_of_board] result = capture_io fn -> GameOfLife.Presenters.Console.print(cells, 123, 6, 0, 2, 2, 2) end assert result == ( " 2| O,,\n" <> " 1| ,O,\n" <> " 0| OOO\n" <> " | _ _ \n" <;> " / 0 \n" <> "Generation: 123\n" <> "Alive cells: 6\n" ) endend
Here is the implementation of our print function:
这是我们打印功能的实现:
# lib/game_of_life/presenters/console.exdefmodule GameOfLife.Presenters.Console do @doc """ Print cells to the console output. Board is visible only for specified size for x and y. Start x and y are in top left corner of the board. `x_padding` Must be a prime number. Every x divided by the prime number will be visible on x axis. `y_padding` Any number. Padding for numbers on y axis. """ def print(cells, generation_counter, alive_counter, start_x \\ -10, start_y \\ 15, x_size \\ 60, y_size \\ 20, x_padding \\ 5, y_padding \\ 5) do end_x = start_x + x_size end_y = start_y - y_size x_range = start_x..end_x y_range = start_y..end_y for y <- y_range, x <- x_range do # draw y axis if x == start_x do (y |>; Integer.to_string |> String.rjust(y_padding)) <> "| " |> IO.write end IO.write(if Enum.member?(cells, {x, y}), do: "O", else: ",") if x == end_x, do: IO.puts "" end # draw x axis IO.write String.rjust("| ", y_padding + 2) x_length = (round((end_x-start_x)/2)) for x <- 0..x_length, do: IO.write "_ " IO.puts "" IO.write String.rjust("/ ", y_padding + 2) for x <- x_range do if rem(x, x_padding) == 0 do x |> Integer.to_string |> String.ljust(x_padding) |> IO.write end end IO.puts "" IO.puts "Generation: #{generation_counter}" IO.puts "Alive cells: #{alive_counter}" endend
The board with the bigger visible part looks like this:
可见部分较大的板看起来像这样:
15| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 14| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 13| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 12| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 11| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 10| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 9| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 8| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 7| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 6| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 5| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 4| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 3| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 2| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 1| ,,,,,,,,,,OO,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 0| ,,,,,,,,,,OO,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -1| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -2| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -3| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -4| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -5| ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ / -10 -5 0 5 10 15 20 25 30 35 40 45 50Generation: 18Alive cells: 4
Last step is to uncomment the GameOfLife.GamePrinter
worker in:
最后一步是在以下位置取消注释GameOfLife.GamePrinter
worker:
# lib/game_of_life.exdefmodule GameOfLife do use Application # See http://elixir-lang.org/docs/stable/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do import Supervisor.Spec, warn: false init_alive_cells = [] children = [ # Define workers and child supervisors to be supervised # worker(GameOfLife.Worker, [arg1, arg2, arg3]), supervisor(Task.Supervisor, [[name: GameOfLife.TaskSupervisor]]), worker(GameOfLife.BoardServer, [init_alive_cells]), # This line is uncommented now worker(GameOfLife.GamePrinter, []), ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: GameOfLife.Supervisor] Supervisor.start_link(children, opts) endend
添加图形图案并将其放置在板上 (Add figure patterns and place them on the board)
To play our game of life, it would be great to have an easy way to add figures to the board. There are many commonly known patterns like still lifes, oscillators, and spaceships. You can learn more about them here.
要玩我们的生活游戏,最好有一种简单的方法来增加人物形象。 有许多众所周知的模式,例如静物,振荡器和宇宙飞船。 您可以在此处了解有关它们的更多信息 。
One interesting pattern is the gun. The Gosper Glider Gun is a very popular pattern. Here it is how it looks:
一种有趣的模式是枪。 s翔滑翔机枪是一种非常受欢迎的模式。 这是它的外观:
When you run game the pattern behaves as you see. The gun is shooting.
当您运行游戏时,模式的行为与您看到的一样。 枪射击。
Let’s write this pattern down. Imagine you want to put the pattern in a rectangle. Left bottom corner of the rectangle is at {0,0}
position.
让我们写下这个模式。 想象一下,您想将图案放置在一个矩形中。 矩形的左下角在{0,0}
位置。
# lib/game_of_life/patterns/guns.exdefmodule GameOfLife.Patterns.Guns do @moduledoc """ https://en.wikipedia.org/wiki/Gun_(cellular_automaton) """ @doc """ https://en.wikipedia.org/wiki/File:Game_of_life_glider_gun.svg """ def gosper_glider do [ {24, 8}, {22, 7}, {24, 7}, {12, 6}, {13, 6}, {20, 6}, {21, 6}, {34, 6}, {35, 6}, {11, 5}, {15, 5}, {20, 5}, {21, 5}, {34, 5}, {35, 5}, {0, 4}, {1, 4}, {10, 4}, {16, 4}, {20, 4}, {21, 4}, {0, 3}, {1, 3}, {10, 3}, {14, 3}, {16, 3}, {17, 3}, {22, 3}, {24, 3}, {10, 2}, {16, 2}, {24, 2}, {11, 1}, {15, 1}, {12, 0}, {13, 0}, ] endend
It would be also useful if we could place the pattern on the board in the position specified by us. Let’s write a pattern converter.
如果我们可以将图案放在板上指定的位置上,这也将很有用。 让我们写一个模式转换器。
# lib/game_of_life/pattern_converter.exdefmodule GameOfLife.PatternConverter do @doc """ ## Example iex> GameOfLife.PatternConverter.transit([{0, 0}, {1, 3}], -1, 2) [{-1, 2}, {0, 5}] """ def transit([{x, y} | cells], x_padding, y_padding) do [{x + x_padding, y + y_padding} | transit(cells, x_padding, y_padding)] end def transit([], _x_padding, _y_padding), do: []end
This is the way you can add the Gosper glider pattern to the board with the specified position.
这样可以将Gosper滑行器模式添加到具有指定位置的板上。
GameOfLife.Patterns.Guns.gosper_glider|> GameOfLife.PatternConverter.transit(-2, -3)|> GameOfLife.BoardServer.add_cells
You can find more patterns in modules here.
跨多个节点运行游戏 (Run game across multiple nodes)
Now it is time to run our game. The full source code can be found here.
现在该运行我们的游戏了。 完整的源代码可以在这里找到 。
Let’s run the first node where the GameOfLife.BoardServer
will be running.
让我们运行GameOfLife.BoardServer
将运行的第一个节点。
$ iex --sname node1 -S mixErlang/OTP 18 [erts-7.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]Interactive Elixir (1.2.4) - press Ctrl+C to exit (type h() ENTER for help)16:54:08.554 [info] Started Elixir.GameOfLife.BoardServer masteriex(node1@Artur)1> GameOfLife.BoardServer.start_game:game_startediex(node1@Artur)2> GameOfLife.GamePrinter.start_printing_board:printing_started
In another terminal window, you can start the second node. We will connect it with the first node.
在另一个终端窗口中,您可以启动第二个节点。 我们将其与第一个节点连接。
$ iex --sname node2 -S mixErlang/OTP 18 [erts-7.3] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]Interactive Elixir (1.2.4) - press Ctrl+C to exit (type h() ENTER for help)16:55:17.395 [info] Started Elixir.GameOfLife.BoardServer masteriex(node2@Artur)1> Node.connect :node1@Arturtrue16:55:17.691 [info] Started Elixir.GameOfLife.BoardServer slaveiex(node2@Artur)2> Node.list[:node1@Artur]iex(node2@Artur)3> Node.self:node2@Arturiex(node2@Artur)4> GameOfLife.Patterns.Guns.gosper_glider |> GameOfLife.BoardServer.add_cells[{24, 8}, {22, 7}, {24, 7}, {12, 6}, {13, 6}, {20, 6}, {21, 6}, {34, 6}, {35, 6}, {11, 5}, {15, 5}, {20, 5}, {21, 5}, {34, 5}, {35, 5}, {0, 4}, {1, 4}, {10, 4}, {16, 4}, {20, 4}, {21, 4}, {0, 3}, {1, 3}, {10, 3}, {14, 3}, {16, 3}, {17, 3}, {22, 3}, {24, 3}, {10, 2}, {16, 2}, {24, 2}, {11, 1}, {15, 1}, {12, 0}, {13, 0}]
Both nodes are executing a calculation to determine a new state for living cells. You can run the game also across different servers in the network like this:
两个节点都在执行计算以确定活细胞的新状态。 您也可以像这样在网络中的不同服务器上运行游戏:
# start node1$ iex --name node1@192.168.0.101 --cookie "token_for_cluster" -S mix# start node2 on another server$ iex --name node2@192.168.0.102 --cookie "token_for_cluster" -S mixiex> Node.connect :"node1@192.168.0.101"true
You already saw how the game works in the demo at the beginning of the article. You can try it on your own machine, just clone the repository.
在本文的开头,您已经在演示中看到了游戏的工作方式。 您可以在自己的计算机上尝试,只需克隆存储库即可 。
摘要 (Summary)
Finally, we managed to get to the end. It was a pretty long road but we have a working game, distributed across nodes. We learned how to write GenServer, use Agents, split processes across nodes with TaskSupervisor and connect nodes into the cluster. You also saw examples of tests in Elixir and how to use DocTest.
最后,我们设法结束了。 这是一条漫长的道路,但是我们有一个工作游戏,分布在各个节点上。 我们学习了如何编写GenServer,使用代理,如何使用TaskSupervisor在节点之间拆分进程以及如何将节点连接到集群。 您还看到了Elixir中的测试示例以及如何使用DocTest。
Hope you found something interesting in the article. Please share your thoughts in the comments.
希望您在本文中发现了一些有趣的东西。 请在评论中分享您的想法。
Nowadays I work on CI parallelisation problem to run tests fast on CI servers. I built Knapsack Pro which splits your Ruby and JavaScript tests on parallel CI nodes to save time on testing. You can check one of my articles about Cypress in JavaScript test suite parallelisation or Heroku CI parallelisation. Let me know how slow or fast your tests are in your Elixir projects — just leave a comment. :) I’d like to add support for CI parallelisation testing into Elixir as well.
如今,我致力于CI并行化问题,以在CI服务器上快速运行测试。 我构建了Knapsack Pro,可以在并行CI节点上拆分Ruby和JavaScript测试,以节省测试时间 。 您可以查看我有关JavaScript测试套件并行化或Heroku CI并行化中有关赛普拉斯的文章之一。 让我知道您的Elixir项目中的测试有多慢或多快-请发表评论。 :)我也想在Elixir中添加对CI并行化测试的支持。
翻译自: https://www.freecodecamp.org/news/how-to-build-a-distributed-game-of-life-in-elixir-9152588100cd/
elixir 规格