|
| 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 |
0 commit comments