Skip to content

Commit c52ecb4

Browse files
committed
Adding class simulations.
1 parent d992288 commit c52ecb4

30 files changed

+1654
-0
lines changed

forest_fire_sim/.DS_Store

6 KB
Binary file not shown.

forest_fire_sim/.gitignore

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps
9+
10+
# Where 3rd-party dependencies like ExDoc output generated docs.
11+
/doc
12+
13+
# If the VM crashes, it generates a dump, let's ignore it too.
14+
erl_crash.dump
15+
16+
# Also ignore archive artifacts (built via "mix archive.build").
17+
*.ez

forest_fire_sim/README.md

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Forest Fire Simulation
2+
3+
The goal of this exercise is to build a simulation of a forest fire spreading through some trees.
4+
5+
A `ForestFireSim.Forest` data structure is already provided, but modules for the individual processes involved in the simulation had been left out. You can look through the documentation of `ForestFireSim.Forest` to learn which operations are provided. (You won't need to add anymore.)
6+
7+
A `ForestFireSim.Fire` process will be spawned as each fire ignites. It should use the arrival of `:advance` messages to notify the `world` of the change, until its `intensity` runs out.
8+
9+
There also will be one `ForestFireSim.World` process that will track the current state of the `forest` and `:render` it to the screen on demand.
10+
11+
You can use the tests to recreate these processes. Start with `ForestFireSim.Fire`. It has no dependencies. You can move to `ForestFireSim.World` next, which makes use of the `ForestFireSim.Forest` data structure.
12+
13+
## Instructions
14+
15+
1. Run `mix test --exclude todo`
16+
2. Fix the failing test
17+
3. Remove the highest `@tag todo: true` line found in `test/fire_test.exs` or
18+
`test/world_test.exs`
19+
4. If a line was removed in step 3, go back to step 1
20+
5. Run `mix` to view the final simulation

forest_fire_sim/config/config.exs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file is responsible for configuring your application
2+
# and its dependencies with the aid of the Mix.Config module.
3+
use Mix.Config
4+
5+
# This configuration is loaded before any dependency and is restricted
6+
# to this project. If another project depends on this project, this
7+
# file won't be loaded nor affect the parent project. For this reason,
8+
# if you want to provide default values for your application for
9+
# 3rd-party users, it should be done in your "mix.exs" file.
10+
11+
# You can configure for your application as:
12+
#
13+
# config :forest_fire_sim, key: :value
14+
#
15+
# And access this configuration in your application as:
16+
#
17+
# Application.get_env(:forest_fire_sim, :key)
18+
#
19+
# Or configure a 3rd-party app:
20+
#
21+
# config :logger, level: :info
22+
#
23+
24+
# It is also possible to import configuration files, relative to this
25+
# directory. For example, you can emulate configuration per environment
26+
# by uncommenting the line below and defining dev.exs, test.exs and such.
27+
# Configuration from the imported file will override the ones defined
28+
# here (which is why it is important to import them last).
29+
#
30+
# import_config "#{Mix.env}.exs"
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
defmodule ForestFireSim do
2+
alias ForestFireSim.{Fire, Forest, World}
3+
4+
def start do
5+
forest = Forest.generate(%{width: 80, height: 24, percent: 66})
6+
fire_starter = fn {xy, intensity} ->
7+
fire = Fire.ignite(self, xy, intensity)
8+
:timer.send_interval(1_000, fire, :advance)
9+
end
10+
world = World.create(forest, fire_starter)
11+
:timer.send_interval(1_000, world, :render)
12+
end
13+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
defmodule ForestFireSim.Fire do
2+
def ignite(world, xy, intensity) do
3+
spawn_link(__MODULE__, :burn, [world, xy, intensity])
4+
end
5+
6+
def burn(_world, _xy, 0), do: :ok
7+
def burn(world, xy, intensity) do
8+
receive do
9+
:advance ->
10+
advance(world, xy)
11+
burn(world, xy, intensity - 1)
12+
end
13+
end
14+
15+
defp advance(world, xy) do
16+
send(world, {:advance_fire, xy})
17+
end
18+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
defmodule ForestFireSim.Forest do
2+
@moduledoc ~S"""
3+
This model is the data representation of the simulation. It keeps track of:
4+
5+
* The width and height of the simulation space
6+
* The current locations of all trees and fires
7+
"""
8+
9+
defstruct width: nil, height: nil, locations: %{ }
10+
11+
@max_intensity 4
12+
13+
@doc ~S"""
14+
Returns the location an intensity of all current fires.
15+
16+
iex> forest = ForestFireSim.Forest.from_string("&")
17+
iex> ForestFireSim.Forest.get_fires(forest)
18+
[{{0, 0}, 4}]
19+
"""
20+
def get_fires(%__MODULE__{locations: locations}) do
21+
Stream.filter(locations, fn {_xy, location} ->
22+
is_tuple(location) and elem(location, 0) == :fire
23+
end)
24+
|> Enum.map(fn {xy, {:fire, intensity}} -> {xy, intensity} end)
25+
end
26+
27+
@doc ~S"""
28+
This utility function returns what is currently at the location `xy`. This is
29+
used in the tests. The three possible return values are:
30+
31+
* `:tree`
32+
* `{:fire, current_intensity}`
33+
* `nil`
34+
35+
iex> forest = ForestFireSim.Forest.from_string("&* ")
36+
iex> ForestFireSim.Forest.get_location(forest, {0, 0})
37+
{:fire, 4}
38+
iex> ForestFireSim.Forest.get_location(forest, {1, 0})
39+
:tree
40+
iex> ForestFireSim.Forest.get_location(forest, {2, 0})
41+
nil
42+
"""
43+
def get_location(%__MODULE__{locations: locations}, xy) do
44+
Map.get(locations, xy)
45+
end
46+
47+
@doc ~S"""
48+
Reduces the intensity of a fire in the forest at `xy` until it cycles to
49+
extinction.
50+
51+
iex> forest = ForestFireSim.Forest.from_string("&")
52+
iex> ForestFireSim.Forest.get_location(forest, {0, 0})
53+
{:fire, 4}
54+
iex> forest = ForestFireSim.Forest.reduce_fire(forest, {0, 0})
55+
iex> ForestFireSim.Forest.get_location(forest, {0, 0})
56+
{:fire, 3}
57+
iex> forest = ForestFireSim.Forest.reduce_fire(forest, {0, 0})
58+
iex> ForestFireSim.Forest.get_location(forest, {0, 0})
59+
{:fire, 2}
60+
iex> forest = ForestFireSim.Forest.reduce_fire(forest, {0, 0})
61+
iex> ForestFireSim.Forest.get_location(forest, {0, 0})
62+
{:fire, 1}
63+
iex> forest = ForestFireSim.Forest.reduce_fire(forest, {0, 0})
64+
iex> ForestFireSim.Forest.get_location(forest, {0, 0})
65+
nil
66+
"""
67+
def reduce_fire(forest = %__MODULE__{locations: locations}, xy) do
68+
updated_locations =
69+
if Map.fetch!(locations, xy) == {:fire, 1} do
70+
Map.delete(locations, xy)
71+
else
72+
Map.update!(locations, xy, fn {:fire, intensity} ->
73+
{:fire, intensity - 1}
74+
end)
75+
end
76+
%__MODULE__{forest | locations: updated_locations}
77+
end
78+
79+
@doc ~S"""
80+
This function spreads a fire to trees that are north, south, east, and west
81+
of `xy`. It returns the new forest, and a list of new fires created.
82+
83+
iex> forest = ForestFireSim.Forest.from_string("&* ")
84+
iex> ForestFireSim.Forest.get_location(forest, {1, 0})
85+
:tree
86+
iex> {forest, new_fires} = ForestFireSim.Forest.spread_fire(forest, {0, 0})
87+
iex> new_fires
88+
[{{1, 0}, 4}]
89+
iex> ForestFireSim.Forest.get_location(forest, {1, 0})
90+
{:fire, 4}
91+
"""
92+
def spread_fire(forest = %__MODULE__{locations: locations}, xy) do
93+
{updated_locations, new_fires} =
94+
case Map.get(locations, xy) do
95+
{:fire, _intensity} ->
96+
neighbors(xy)
97+
|> Enum.reduce(
98+
{locations, [ ]},
99+
fn neighbor_xy, {with_new_fires, new_fires} ->
100+
if Map.get(with_new_fires, neighbor_xy) == :tree do
101+
{
102+
Map.put(with_new_fires, neighbor_xy, {:fire, @max_intensity}),
103+
[{neighbor_xy, @max_intensity} | new_fires]
104+
}
105+
else
106+
{with_new_fires, new_fires}
107+
end
108+
end
109+
)
110+
_ ->
111+
{locations, [ ]}
112+
end
113+
{%__MODULE__{forest | locations: updated_locations}, new_fires}
114+
end
115+
116+
@doc ~S"""
117+
Returns a string representation of passed forest. By default ANSI coloring
118+
is included, but passing `false` as the second argument disables this behavior.
119+
120+
iex> "&* " |> ForestFireSim.Forest.from_string |> ForestFireSim.Forest.to_string(false)
121+
"&* "
122+
"""
123+
def to_string(
124+
%__MODULE__{width: width, height: height, locations: locations},
125+
ansi? \\ true
126+
) do
127+
string =
128+
Enum.map(0..(height - 1), fn y ->
129+
Enum.map(0..(width - 1), fn x ->
130+
Map.get(locations, {x, y})
131+
|> to_location_string
132+
|> IO.ANSI.format(ansi?)
133+
end)
134+
|> Enum.join
135+
end)
136+
|> Enum.join("\n")
137+
IO.ANSI.format_fragment([:clear, :home, string], ansi?)
138+
|> IO.chardata_to_string
139+
end
140+
141+
@doc """
142+
This is a utility function that builds known forest layouts. It's used in the
143+
test. For example:
144+
145+
iex> ForestFireSim.Forest.from_string(
146+
iex> \"""
147+
iex> &**
148+
iex> **
149+
iex> \"""
150+
iex> )
151+
%ForestFireSim.Forest{width: 3, height: 2, locations: %{{0, 0} => {:fire, 4}, {1, 0} => :tree, {1, 1} => :tree, {2, 0} => :tree, {2, 1} => :tree}}
152+
"""
153+
def from_string(string) do
154+
rows = String.split(string, "\n", trim: true)
155+
width = rows |> hd |> String.length
156+
height = length(rows)
157+
158+
unless Enum.all?(rows, fn row -> String.length(row) == width end) do
159+
raise "All rows needs the same width"
160+
end
161+
162+
locations =
163+
rows
164+
|> Enum.with_index
165+
|> Enum.reduce(%{ }, fn {row, y}, map ->
166+
row
167+
|> String.graphemes
168+
|> Enum.with_index
169+
|> Enum.reduce(map, fn {char, x}, row_map ->
170+
location =
171+
case char do
172+
"*" -> :tree
173+
"&" -> {:fire, @max_intensity}
174+
_ -> nil
175+
end
176+
if location do
177+
Map.put(row_map, {x, y}, location)
178+
else
179+
row_map
180+
end
181+
end)
182+
end)
183+
%__MODULE__{width: width, height: height, locations: locations}
184+
end
185+
186+
@doc ~S"""
187+
This function generates a new forest with the passed `:width` and `:height`.
188+
Roughly `:percent` of the locations in the generated forest will be filled.
189+
A filled location on the far left side will be a fire, but all other filled
190+
locations will be trees.
191+
192+
ForestFireSim.Forest.generate(%{width: 80, height: 24, percent: 66})
193+
"""
194+
def generate(%{width: width, height: height, percent: percent}) do
195+
locations =
196+
for x <- 0..(width - 1),
197+
y <- 0..(height - 1),
198+
:rand.uniform(100) <= percent,
199+
into: %{ } do
200+
location = if x == 0, do: {:fire, @max_intensity}, else: :tree
201+
{{x, y}, location}
202+
end
203+
%__MODULE__{width: width, height: height, locations: locations}
204+
end
205+
206+
defp neighbors({x, y}) do
207+
[ {x, y - 1},
208+
{x - 1, y}, {x + 1, y},
209+
{x, y + 1} ]
210+
end
211+
212+
defp to_location_string(:tree), do: [:green, "*"]
213+
defp to_location_string({:fire, 4}), do: [:bright, :red, "&"]
214+
defp to_location_string({:fire, 3}), do: [:red, "&"]
215+
defp to_location_string({:fire, 2}), do: [:bright, :yellow, "&"]
216+
defp to_location_string({:fire, 1}), do: [:yellow, "&"]
217+
defp to_location_string(nil), do: " "
218+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
defmodule ForestFireSim.World do
2+
alias ForestFireSim.Forest
3+
4+
def create(forest, fire_starter) do
5+
spawn_link(__MODULE__, :build, [forest, fire_starter])
6+
end
7+
8+
def build(forest, fire_starter) do
9+
Forest.get_fires(forest)
10+
|> Enum.each(fire_starter)
11+
run(forest, fire_starter)
12+
end
13+
14+
def run(forest, fire_starter) do
15+
receive do
16+
{:advance_fire, xy} ->
17+
{new_forest, new_fires} = Forest.spread_fire(forest, xy)
18+
Enum.each(new_fires, fire_starter)
19+
Forest.reduce_fire(new_forest, xy)
20+
|> run(fire_starter)
21+
{:debug_location, xy, from} ->
22+
location = Forest.get_location(forest, xy)
23+
send(from, {:debug_location_response, location})
24+
run(forest, fire_starter)
25+
:render ->
26+
Forest.to_string(forest)
27+
|> IO.puts
28+
run(forest, fire_starter)
29+
end
30+
end
31+
end

forest_fire_sim/mix.exs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
defmodule ForestFireSim.Mixfile do
2+
use Mix.Project
3+
4+
def project do
5+
[app: :forest_fire_sim,
6+
version: "0.1.0",
7+
elixir: "~> 1.3",
8+
build_embedded: Mix.env == :prod,
9+
start_permanent: Mix.env == :prod,
10+
deps: deps,
11+
aliases: aliases,
12+
default_task: "simulate"]
13+
end
14+
15+
# Configuration for the OTP application
16+
#
17+
# Type "mix help compile.app" for more information
18+
def application do
19+
[applications: [:logger]]
20+
end
21+
22+
def aliases do
23+
[simulate: "run -e 'ForestFireSim.start' --no-halt"]
24+
end
25+
26+
# Dependencies can be Hex packages:
27+
#
28+
# {:mydep, "~> 0.3.0"}
29+
#
30+
# Or git/path repositories:
31+
#
32+
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
33+
#
34+
# Type "mix help deps" for more examples and options
35+
defp deps do
36+
[]
37+
end
38+
end

0 commit comments

Comments
 (0)