├── .gitignore ├── turtles ├── test │ ├── test_helper.exs │ ├── turtles_test.exs │ ├── turtle_test.exs │ ├── world_test.exs │ └── clock_test.exs ├── mix.lock ├── .gitignore ├── mix.exs ├── README.md ├── README_sup.md ├── config │ └── config.exs ├── README_fix.md └── lib │ ├── turtles │ ├── painter.ex │ ├── clock.ex │ ├── world.ex │ └── turtle.ex │ └── turtles.ex ├── slime_mold ├── test │ ├── test_helper.exs │ └── slime_mold_test.exs ├── .gitignore ├── mix.lock ├── README.md ├── lib │ ├── slime_mold │ │ ├── environment.ex │ │ ├── cell.ex │ │ ├── renderer.ex │ │ └── board.ex │ └── slime_mold.ex ├── mix.exs └── config │ └── config.exs ├── traffic_jam ├── test │ ├── test_helper.exs │ └── traffic_jam_test.exs ├── .gitignore ├── mix.lock ├── lib │ ├── traffic_jam │ │ ├── car_supervisor.ex │ │ ├── road.ex │ │ ├── window.ex │ │ ├── car.ex │ │ └── canvas.ex │ └── traffic_jam.ex ├── README.md ├── mix.exs └── config │ └── config.exs ├── forest_fire_sim ├── test │ ├── test_helper.exs │ ├── fire_test.exs │ ├── world_test.exs │ └── forest_test.exs ├── .DS_Store ├── lib │ ├── forest_fire_sim.ex │ └── forest_fire_sim │ │ ├── fire.ex │ │ ├── world.ex │ │ └── forest.ex ├── .gitignore ├── mix.exs ├── config │ └── config.exs └── README.md └── turtles_and_frogs ├── test ├── test_helper.exs └── turtles_and_frogs_test.exs ├── .gitignore ├── mix.lock ├── README.md ├── mix.exs ├── config └── config.exs └── lib ├── turtles_and_frogs.ex └── turtles_and_frogs ├── painter.ex ├── critter.ex └── pond.ex /.gitignore: -------------------------------------------------------------------------------- 1 | canvas 2 | smooth_drawing 3 | -------------------------------------------------------------------------------- /turtles/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /slime_mold/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /traffic_jam/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /forest_fire_sim/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /turtles_and_frogs/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /slime_mold/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /traffic_jam/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /turtles_and_frogs/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /forest_fire_sim/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JEG2/simulations/HEAD/forest_fire_sim/.DS_Store -------------------------------------------------------------------------------- /slime_mold/mix.lock: -------------------------------------------------------------------------------- 1 | %{"wx_utils": {:git, "https://github.com/JEG2/wx_utils.git", "59ccf1007a44c0eefbd3779b77670f4207381263", []}} 2 | -------------------------------------------------------------------------------- /traffic_jam/mix.lock: -------------------------------------------------------------------------------- 1 | %{"wx_utils": {:hex, :wx_utils, "0.0.2", "6b3ee8376cf122b0fdbf3f8f511b8083097488aff6d0eb626b97624765ffdf66", [:mix], []}} 2 | -------------------------------------------------------------------------------- /turtles/test/turtles_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TurtlesTest do 2 | use ExUnit.Case 3 | doctest Turtles 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /slime_mold/test/slime_mold_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SlimeMoldTest do 2 | use ExUnit.Case 3 | doctest SlimeMold 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /traffic_jam/test/traffic_jam_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TrafficJamTest do 2 | use ExUnit.Case 3 | doctest TrafficJam 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /turtles_and_frogs/test/turtles_and_frogs_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TurtlesAndFrogsTest do 2 | use ExUnit.Case 3 | doctest TurtlesAndFrogs 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /turtles/mix.lock: -------------------------------------------------------------------------------- 1 | %{"canvas": {:hex, :canvas, "0.0.1", "4ec7e9f009081e05a08bdba59893f0a5706c7fd8edfef0ddc81d50a5a10293d2", [:mix], [{:wx_utils, "~> 0.0.2", [hex: :wx_utils, optional: false]}]}, 2 | "wx_utils": {:hex, :wx_utils, "0.0.2", "6b3ee8376cf122b0fdbf3f8f511b8083097488aff6d0eb626b97624765ffdf66", [:mix], []}} 3 | -------------------------------------------------------------------------------- /turtles_and_frogs/mix.lock: -------------------------------------------------------------------------------- 1 | %{"canvas": {:hex, :canvas, "0.0.1", "4ec7e9f009081e05a08bdba59893f0a5706c7fd8edfef0ddc81d50a5a10293d2", [:mix], [{:wx_utils, "~> 0.0.2", [hex: :wx_utils, optional: false]}]}, 2 | "wx_utils": {:hex, :wx_utils, "0.0.2", "6b3ee8376cf122b0fdbf3f8f511b8083097488aff6d0eb626b97624765ffdf66", [:mix], []}} 3 | -------------------------------------------------------------------------------- /traffic_jam/lib/traffic_jam/car_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule TrafficJam.CarSupervisor do 2 | def start_link do 3 | import Supervisor.Spec 4 | 5 | children = [ 6 | worker(TrafficJam.Car, [ ], restart: :transient) 7 | ] 8 | 9 | Supervisor.start_link( 10 | children, 11 | strategy: :simple_one_for_one, 12 | name: __MODULE__ 13 | ) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /forest_fire_sim/lib/forest_fire_sim.ex: -------------------------------------------------------------------------------- 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 14 | -------------------------------------------------------------------------------- /turtles/.gitignore: -------------------------------------------------------------------------------- 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 18 | -------------------------------------------------------------------------------- /forest_fire_sim/.gitignore: -------------------------------------------------------------------------------- 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 18 | -------------------------------------------------------------------------------- /forest_fire_sim/lib/forest_fire_sim/fire.ex: -------------------------------------------------------------------------------- 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 19 | -------------------------------------------------------------------------------- /slime_mold/README.md: -------------------------------------------------------------------------------- 1 | # SlimeMold 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed as: 8 | 9 | 1. Add slime_mold to your list of dependencies in `mix.exs`: 10 | 11 | def deps do 12 | [{:slime_mold, "~> 0.0.1"}] 13 | end 14 | 15 | 2. Ensure slime_mold is started before your application: 16 | 17 | def application do 18 | [applications: [:slime_mold]] 19 | end 20 | 21 | -------------------------------------------------------------------------------- /traffic_jam/README.md: -------------------------------------------------------------------------------- 1 | # TrafficJam 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed as: 8 | 9 | 1. Add traffic_jam to your list of dependencies in `mix.exs`: 10 | 11 | def deps do 12 | [{:traffic_jam, "~> 0.0.1"}] 13 | end 14 | 15 | 2. Ensure traffic_jam is started before your application: 16 | 17 | def application do 18 | [applications: [:traffic_jam]] 19 | end 20 | 21 | -------------------------------------------------------------------------------- /slime_mold/lib/slime_mold/environment.ex: -------------------------------------------------------------------------------- 1 | defmodule SlimeMold.Environment do 2 | # Client API 3 | 4 | def start do 5 | delay = Application.fetch_env!(:slime_mold, :environment_delay) 6 | 7 | run(delay) 8 | end 9 | 10 | # Server API 11 | 12 | defp run(delay) do 13 | :timer.sleep(delay) 14 | 15 | evaporate 16 | diffuse 17 | 18 | run(delay) 19 | end 20 | 21 | defp evaporate do 22 | SlimeMold.Board.evaporate_all 23 | end 24 | 25 | defp diffuse do 26 | SlimeMold.Board.diffuse_all 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /turtles_and_frogs/README.md: -------------------------------------------------------------------------------- 1 | # TurtlesAndFrogs 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed as: 8 | 9 | 1. Add turtles_and_frogs to your list of dependencies in `mix.exs`: 10 | 11 | def deps do 12 | [{:turtles_and_frogs, "~> 0.0.1"}] 13 | end 14 | 15 | 2. Ensure turtles_and_frogs is started before your application: 16 | 17 | def application do 18 | [applications: [:turtles_and_frogs]] 19 | end 20 | 21 | -------------------------------------------------------------------------------- /turtles/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Turtles.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :turtles, 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 | end 12 | 13 | # Configuration for the OTP application 14 | # 15 | # Type "mix help compile.app" for more information 16 | def application do 17 | [applications: [:logger], 18 | mod: {Turtles, []}] 19 | end 20 | 21 | # Dependencies can be Hex packages: 22 | # 23 | # {:mydep, "~> 0.3.0"} 24 | # 25 | # Or git/path repositories: 26 | # 27 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 28 | # 29 | # Type "mix help deps" for more examples and options 30 | defp deps do 31 | [{:canvas, "~> 0.0.1"}] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /traffic_jam/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TrafficJam.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :traffic_jam, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | # Configuration for the OTP application 14 | # 15 | # Type "mix help compile.app" for more information 16 | def application do 17 | [applications: [:logger], 18 | mod: {TrafficJam, []}] 19 | end 20 | 21 | # Dependencies can be Hex packages: 22 | # 23 | # {:mydep, "~> 0.3.0"} 24 | # 25 | # Or git/path repositories: 26 | # 27 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 28 | # 29 | # Type "mix help deps" for more examples and options 30 | defp deps do 31 | [{:wx_utils, "~> 0.0.2"}] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /slime_mold/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule SlimeMold.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :slime_mold, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | # Configuration for the OTP application 14 | # 15 | # Type "mix help compile.app" for more information 16 | def application do 17 | [applications: [:logger], 18 | mod: {SlimeMold, []}] 19 | end 20 | 21 | # Dependencies can be Hex packages: 22 | # 23 | # {:mydep, "~> 0.3.0"} 24 | # 25 | # Or git/path repositories: 26 | # 27 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 28 | # 29 | # Type "mix help deps" for more examples and options 30 | defp deps do 31 | [{:wx_utils, github: "JEG2/wx_utils"}] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /turtles_and_frogs/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TurtlesAndFrogs.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :turtles_and_frogs, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | # Configuration for the OTP application 14 | # 15 | # Type "mix help compile.app" for more information 16 | def application do 17 | [applications: [:logger], 18 | mod: {TurtlesAndFrogs, []}] 19 | end 20 | 21 | # Dependencies can be Hex packages: 22 | # 23 | # {:mydep, "~> 0.3.0"} 24 | # 25 | # Or git/path repositories: 26 | # 27 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 28 | # 29 | # Type "mix help deps" for more examples and options 30 | defp deps do 31 | [{:canvas, "~> 0.0.1"}] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /slime_mold/lib/slime_mold.ex: -------------------------------------------------------------------------------- 1 | defmodule SlimeMold do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | width = Application.fetch_env!(:slime_mold, :board_width) 10 | height = Application.fetch_env!(:slime_mold, :board_height) 11 | 12 | children = [ 13 | # Define workers and child supervisors to be supervised 14 | supervisor(Task.Supervisor, [[name: SlimeMold.Actives]]), 15 | worker(SlimeMold.Board, [{width, height}]), 16 | worker(SlimeMold.Renderer, [ ]) 17 | ] 18 | 19 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 20 | # for other strategies and supported options 21 | opts = [strategy: :rest_for_one, name: SlimeMold.Supervisor] 22 | Supervisor.start_link(children, opts) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /turtles/README.md: -------------------------------------------------------------------------------- 1 | # Turtle Ecology Simulation 2 | 3 | The goal of this exercise is to build a simulation of turtles that walk around, eat food, reproduce, and die. 4 | 5 | The simulation is built, minus a `Turtles.Clock` process that you need to add. 6 | 7 | The `Turtles.Clock` is responsible for managing time in the simulation. When the clock is started, it places plants and turtles in the `Turtles.World`. After that, as time advances, the clock tells each live turtle to act. New turtles may be born as a result of these actions. When they are, the clock must make them act in future actions. Finally, the clock should note when a turtle dies, so they are no longer told to act. 8 | 9 | ## Instructions 10 | 11 | 1. Run `mix test --exclude todo` 12 | 2. Fix the failing test 13 | 3. Remove the highest `@tag todo: true` line found in `test/clock_test.exs` 14 | 4. If a line was removed in step 3, go back to step 1 15 | 5. Run `iex -S mix` to view the final simulation 16 | -------------------------------------------------------------------------------- /turtles/README_sup.md: -------------------------------------------------------------------------------- 1 | # Turtle Ecology Supervisors Simulation 2 | 3 | This is the same simulation as before. All processes are complete this time, but the supervision tree has been removed. The goal is to reconstruct the tree so the simulation runs again. 4 | 5 | ## Instructions 6 | 7 | 1. Run `mix deps.get` 8 | 2. Build a dynamic `Supervisor` that has one template to launch `Turtles.Turtle` 9 | * It needs to be named: see `Turtles.Turtle.start_supervised/5` 10 | * `Turtles.Turtle` is a `:transient` process 11 | 3. A top-level `Supervisor` is also needed 12 | * It should supervise the `Supervisor` created in step 2 13 | * It should supervise `Turtles.World` which needs a matching name 14 | * It should supervise `Turtles.Clock` which needs a matching name 15 | * It should supervise a `Canvas.GUI` (`start_link/1` takes `canvas_options`) 16 | * If any of these processes fails, all should be relaunched 17 | 4. Run `iex -S mix` to view the final simulation 18 | -------------------------------------------------------------------------------- /forest_fire_sim/mix.exs: -------------------------------------------------------------------------------- 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 39 | -------------------------------------------------------------------------------- /forest_fire_sim/lib/forest_fire_sim/world.ex: -------------------------------------------------------------------------------- 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 32 | -------------------------------------------------------------------------------- /forest_fire_sim/test/fire_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FireTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias ForestFireSim.Fire 5 | 6 | test "advance themselves in the `world` when they receive messages" do 7 | world = self 8 | xy = {4, 2} 9 | intensity = 2 10 | fire = Fire.ignite(world, xy, intensity) 11 | send(fire, :advance) 12 | assert_receive {:advance_fire, ^xy} 13 | end 14 | 15 | @tag todo: true 16 | test "burn for `intensity` number of `:advance` messages" do 17 | # ask to receive a message when `fire` exits: 18 | Process.flag(:trap_exit, true) 19 | 20 | world = self 21 | xy = {0, 0} 22 | intensity = 1 23 | fire = Fire.ignite(world, xy, intensity) 24 | send(fire, :advance) 25 | assert_receive {:advance_fire, ^xy} 26 | assert_receive {:EXIT, ^fire, :normal} # `fire` exited 27 | 28 | intensity = 4 29 | bigger_fire = Fire.ignite(world, xy, intensity) 30 | Stream.repeatedly(fn -> 31 | send(bigger_fire, :advance) 32 | assert_receive {:advance_fire, ^xy} 33 | end) 34 | |> Enum.take(intensity) 35 | assert_receive {:EXIT, ^bigger_fire, :normal} # `bigger_fire` exited 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /traffic_jam/lib/traffic_jam.ex: -------------------------------------------------------------------------------- 1 | defmodule TrafficJam do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | road_width = Application.fetch_env!(:traffic_jam, :road_width) 10 | road_height = Application.fetch_env!(:traffic_jam, :road_height) 11 | car_width = Application.fetch_env!(:traffic_jam, :car_width) 12 | paint_delay = Application.fetch_env!(:traffic_jam, :paint_delay) 13 | cars = Application.fetch_env!(:traffic_jam, :cars) 14 | 15 | children = [ 16 | # Define workers and child supervisors to be supervised 17 | supervisor(TrafficJam.CarSupervisor, [ ]), 18 | worker(TrafficJam.Road, [road_width, car_width, cars]), 19 | worker( 20 | TrafficJam.Window, 21 | [paint_delay, {road_width, road_height}, car_width] 22 | ) 23 | ] 24 | 25 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 26 | # for other strategies and supported options 27 | opts = [strategy: :one_for_all, name: TrafficJam.Supervisor] 28 | Supervisor.start_link(children, opts) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /turtles/config/config.exs: -------------------------------------------------------------------------------- 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 :turtles, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:turtles, :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" 31 | -------------------------------------------------------------------------------- /forest_fire_sim/config/config.exs: -------------------------------------------------------------------------------- 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" 31 | -------------------------------------------------------------------------------- /turtles_and_frogs/config/config.exs: -------------------------------------------------------------------------------- 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 :turtles_and_frogs, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:turtles_and_frogs, :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" 31 | -------------------------------------------------------------------------------- /turtles_and_frogs/lib/turtles_and_frogs.ex: -------------------------------------------------------------------------------- 1 | defmodule TurtlesAndFrogs do 2 | use Application 3 | 4 | @width 64 5 | @height 128 6 | @scale 5 7 | 8 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 9 | # for more information on OTP Applications 10 | def start(_type, _args) do 11 | import Supervisor.Spec, warn: false 12 | 13 | canvas_options = [ 14 | width: @width * @scale, 15 | height: @height * @scale, 16 | paint_interval: 100, 17 | painter_module: TurtlesAndFrogs.Painter, 18 | painter_state: @scale, 19 | brushes: %{ 20 | blue: {0, 0, 50, 255}, 21 | red: {150, 0, 0, 255}, 22 | green: {0, 150, 0, 255} 23 | } 24 | ] 25 | 26 | children = [ 27 | # Define workers and child supervisors to be supervised 28 | supervisor(Task.Supervisor, [[name: TurtlesAndFrogs.Critters]]), 29 | worker(TurtlesAndFrogs.Pond, [{@width, @height}, 3_000, 3_000]), 30 | worker(Canvas.GUI, [canvas_options]) 31 | ] 32 | 33 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 34 | # for other strategies and supported options 35 | opts = [strategy: :one_for_all, name: TurtlesAndFrogs.Supervisor] 36 | Supervisor.start_link(children, opts) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /forest_fire_sim/README.md: -------------------------------------------------------------------------------- 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 21 | -------------------------------------------------------------------------------- /traffic_jam/config/config.exs: -------------------------------------------------------------------------------- 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 :traffic_jam, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:traffic_jam, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | config :traffic_jam, road_width: 1_000 25 | config :traffic_jam, road_height: 100 26 | config :traffic_jam, car_width: 5 27 | 28 | config :traffic_jam, paint_delay: 100 29 | config :traffic_jam, car_delay: 250 30 | 31 | config :traffic_jam, cars: 50 32 | 33 | # It is also possible to import configuration files, relative to this 34 | # directory. For example, you can emulate configuration per environment 35 | # by uncommenting the line below and defining dev.exs, test.exs and such. 36 | # Configuration from the imported file will override the ones defined 37 | # here (which is why it is important to import them last). 38 | # 39 | # import_config "#{Mix.env}.exs" 40 | -------------------------------------------------------------------------------- /turtles/README_fix.md: -------------------------------------------------------------------------------- 1 | # Turtle Ecology Fix Simulation 2 | 3 | This is the same simulation as before. It is complete and runs. 4 | 5 | The current code has a design flaw though. The `Turtles.Clock` is coupled to `Turtles.Painter`. The goal of this exercise is to break that coupling. 6 | 7 | Fixing the design will expose drawing bugs. Fixing those bugs requires simplifying how changes are managed in `Turtles.World`. 8 | 9 | ## Instructions 10 | 11 | 1. Run `mix deps.get` 12 | 2. Remove `Clock.advance(Clock)` in `Turtles.Painter` 13 | 3. Make `Turtles.Clock` handle its own ticking by replacing `start_link/4` with: 14 | 15 | ```elixir 16 | def start_link(world, size, turtle_starter, options \\ [ ]) do 17 | clock = %__MODULE__{world: world, size: size, turtle_starter: turtle_starter} 18 | with {:ok, pid} = GenServer.start_link(__MODULE__, clock, options), 19 | :timer.send_interval(300, pid, :tick), 20 | do: {:ok, pid} 21 | end 22 | ``` 23 | 24 | 4. Switch `Turtles.Clock`'s `handle_call(:tick, _from, clock = …)` to 25 | `handle_info(:tick, clock = …)` 26 | 5. Set `:paint_interval` to `100` in `lib/turtles.ex` 27 | 6. Run `iex -S mix` to view the drawing bugs 28 | 7. Replace `Turtles.World`'s empty changes with an empty list: `[ ]` 29 | 8. Fix the server callbacks in `Turtles.World` to build up changes of the form 30 | `{:clear | :plant | :turtle, {x, y}}` 31 | 9. Adjust `Turtles.Painter.paint/4` to draw the new change format 32 | 10. Run `iex -S mix` to view the final simulation 33 | -------------------------------------------------------------------------------- /slime_mold/config/config.exs: -------------------------------------------------------------------------------- 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 :slime_mold, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:slime_mold, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | config :slime_mold, board_width: 400 25 | config :slime_mold, board_height: 400 26 | config :slime_mold, scale: 2 27 | 28 | config :slime_mold, paint_delay: 1_500 29 | config :slime_mold, cell_delay: 3_000 30 | config :slime_mold, environment_delay: 3_000 31 | 32 | config :slime_mold, cells: 1_000 33 | 34 | # It is also possible to import configuration files, relative to this 35 | # directory. For example, you can emulate configuration per environment 36 | # by uncommenting the line below and defining dev.exs, test.exs and such. 37 | # Configuration from the imported file will override the ones defined 38 | # here (which is why it is important to import them last). 39 | # 40 | # import_config "#{Mix.env}.exs" 41 | -------------------------------------------------------------------------------- /turtles/lib/turtles/painter.ex: -------------------------------------------------------------------------------- 1 | defmodule Turtles.Painter do 2 | alias Canvas.GUI.Brush 3 | alias Turtles.{World, Clock} 4 | 5 | def paint(canvas, _width, _height, scale) do 6 | %{clears: clears, plants: plants, turtles: turtles} = 7 | World.changes(World) 8 | 9 | Enum.each(clears, fn location -> clear(canvas, scale, location) end) 10 | Enum.each(plants, fn location -> paint_plant(canvas, scale, location) end) 11 | Enum.each(turtles, fn location -> paint_turtle(canvas, scale, location) end) 12 | 13 | Clock.advance(Clock) 14 | end 15 | 16 | defp clear(canvas, scale, {x, y}) do 17 | Brush.draw_rectangle(canvas, {x * scale, y * scale}, {scale, scale}, :black) 18 | end 19 | 20 | defp paint_plant(canvas, scale, {x, y}) do 21 | plant_size = round(scale / 2) 22 | offset = round((scale - plant_size) / 2) 23 | Brush.draw_rectangle( 24 | canvas, 25 | {x * scale + offset, y * scale + offset}, 26 | {plant_size, plant_size}, 27 | :green 28 | ) 29 | end 30 | 31 | defp paint_turtle(canvas, scale, {x, y}) do 32 | turtle_size = round(scale / 2) 33 | turtle_radius = round(turtle_size / 2) 34 | offset = round((scale - turtle_size) / 2) 35 | Brush.draw_circle( 36 | canvas, 37 | {x * scale + offset + turtle_radius, y * scale + offset + turtle_radius}, 38 | turtle_radius, 39 | :red 40 | ) 41 | end 42 | 43 | def handle_key_down( 44 | %{key: ?Q, shift: false, alt: false, control: false, meta: false}, 45 | _scale 46 | ) do 47 | System.halt(0) 48 | end 49 | def handle_key_down(_key_combo, scale), do: scale 50 | end 51 | -------------------------------------------------------------------------------- /turtles/lib/turtles.ex: -------------------------------------------------------------------------------- 1 | defmodule Turtles do 2 | use Application 3 | 4 | @width 128 5 | @height 128 6 | @scale 4 7 | 8 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 9 | # for more information on OTP Applications 10 | def start(_type, _args) do 11 | import Supervisor.Spec, warn: false 12 | 13 | canvas_options = [ 14 | width: @width * @scale, 15 | height: @height * @scale, 16 | paint_interval: 500, 17 | painter_module: Turtles.Painter, 18 | painter_state: @scale, 19 | brushes: %{ 20 | black: {0, 0, 0, 255}, 21 | red: {150, 0, 0, 255}, 22 | green: {0, 150, 0, 255} 23 | } 24 | ] 25 | turtle_starter = fn args -> 26 | apply(Turtles.Turtle, :start_supervised, args) 27 | end 28 | 29 | # Define workers and child supervisors to be supervised 30 | children = 31 | if Mix.env != :test do 32 | [ 33 | # Starts a worker by calling: Turtles.Worker.start_link(arg1, arg2, arg3) 34 | supervisor( 35 | Supervisor, 36 | [ [worker(Turtles.Turtle, [ ], restart: :transient)], 37 | [name: Turtles.TurtleSupervisor, strategy: :simple_one_for_one] ] 38 | ), 39 | worker(Turtles.World, [{@width, @height}, [name: Turtles.World]]), 40 | worker( 41 | Turtles.Clock, 42 | [ Turtles.World, 43 | {@width, @height}, 44 | turtle_starter, 45 | [name: Turtles.Clock] ] 46 | ), 47 | worker(Canvas.GUI, [canvas_options]) 48 | ] 49 | else 50 | [ ] 51 | end 52 | 53 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 54 | # for other strategies and supported options 55 | opts = [strategy: :one_for_all, name: Turtles.Supervisor] 56 | Supervisor.start_link(children, opts) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /turtles_and_frogs/lib/turtles_and_frogs/painter.ex: -------------------------------------------------------------------------------- 1 | defmodule TurtlesAndFrogs.Painter do 2 | def paint(canvas, width, height, scale) do 3 | TurtlesAndFrogs.Pond.get_changes 4 | |> Enum.each(fn change -> 5 | paint_change(change, canvas, width, height, scale) 6 | end) 7 | end 8 | 9 | defp paint_change({:init, grid}, canvas, width, height, scale) do 10 | paint_pond(canvas, width, height) 11 | paint_grid(grid, canvas, scale) 12 | end 13 | defp paint_change({:remove, xy}, canvas, _width, _height, scale) do 14 | paint_critter(canvas, scale, xy, :blue) 15 | end 16 | defp paint_change({:add_turtle, xy}, canvas, _width, _height, scale) do 17 | paint_turtle(canvas, scale, xy) 18 | end 19 | defp paint_change({:add_frog, xy}, canvas, _width, _height, scale) do 20 | paint_frog(canvas, scale, xy) 21 | end 22 | 23 | defp paint_pond(canvas, width, height) do 24 | Canvas.GUI.Brush.draw_rectangle(canvas, {0, 0}, {width, height}, :blue) 25 | end 26 | 27 | defp paint_grid(grid, canvas, scale) do 28 | grid 29 | |> Enum.with_index 30 | |> Enum.each(fn {row, y} -> 31 | row 32 | |> Enum.with_index 33 | |> Enum.each(fn 34 | {0, _x} -> :ok 35 | {1, x} -> paint_turtle(canvas, scale, {x, y}) 36 | {2, x} -> paint_frog(canvas, scale, {x, y}) 37 | end) 38 | end) 39 | end 40 | 41 | defp paint_turtle(canvas, scale, xy) do 42 | paint_critter(canvas, scale, xy, :green) 43 | end 44 | 45 | defp paint_frog(canvas, scale, xy) do 46 | paint_critter(canvas, scale, xy, :red) 47 | end 48 | 49 | defp paint_critter(canvas, scale, {x, y}, color) do 50 | Canvas.GUI.Brush.draw_circle( 51 | canvas, 52 | {x * scale + 2, y * scale + 2}, 53 | 2, 54 | color 55 | ) 56 | end 57 | 58 | def handle_key_down( 59 | %{key: ?Q, shift: false, alt: false, control: false, meta: false}, 60 | _scale 61 | ) do 62 | System.halt(0) 63 | end 64 | def handle_key_down(_key_combo, scale), do: scale 65 | end 66 | -------------------------------------------------------------------------------- /turtles_and_frogs/lib/turtles_and_frogs/critter.ex: -------------------------------------------------------------------------------- 1 | defmodule TurtlesAndFrogs.Critter do 2 | # Client API 3 | 4 | def start(xy, type, size) do 5 | loop(xy, neighbors_for(xy, size), type, size) 6 | end 7 | 8 | # Helpers 9 | 10 | defp loop(xy, neighbor_xys, type, size) do 11 | :timer.sleep(333) 12 | 13 | if happy?(neighbor_xys, type) do 14 | loop(xy, neighbor_xys, type, size) 15 | else 16 | move_away_from(xy, type, size) |> loop(neighbor_xys, type, size) 17 | end 18 | end 19 | 20 | defp neighbors_for({x, y}, size = {width, height}) do 21 | [ {-1, -1}, {0, -1}, {1, -1}, 22 | {-1, 0}, {1, 0}, 23 | {-1, 1}, {0, 1}, {1, 1} ] 24 | |> Enum.map(fn {x_offset, y_offset} -> 25 | {rem(width + (x + x_offset), width), rem(height + (y + y_offset), height)} 26 | end) 27 | |> Enum.filter(fn new_xy -> valid?(new_xy, size) end) 28 | end 29 | 30 | defp valid?({new_x, new_y}, {width, height}) do 31 | new_x >= 0 and new_x < width and new_y >= 0 and new_y < height 32 | end 33 | 34 | defp happy?(neighbor_xys, type) do 35 | neighbors = TurtlesAndFrogs.Pond.look_around(neighbor_xys, type) 36 | Enum.sum(neighbors) >= length(neighbors) * 0.3 37 | end 38 | 39 | defp move_away_from(xy, type, size) do 40 | possible_xy = 41 | :rand.uniform(8) 42 | |> heading_to_direction 43 | |> advance(xy, :rand.uniform(5)) 44 | if valid?(possible_xy, size) do 45 | TurtlesAndFrogs.Pond.try_move(xy, possible_xy, type) 46 | else 47 | xy 48 | end 49 | end 50 | 51 | defp heading_to_direction(1), do: {0, -1} 52 | defp heading_to_direction(2), do: {1, -1} 53 | defp heading_to_direction(3), do: {1, 0} 54 | defp heading_to_direction(4), do: {1, 1} 55 | defp heading_to_direction(5), do: {0, 1} 56 | defp heading_to_direction(6), do: {-1, 1} 57 | defp heading_to_direction(7), do: {-1, 0} 58 | defp heading_to_direction(8), do: {-1, -1} 59 | 60 | defp advance({x_offset, y_offset}, {x, y}, distance) do 61 | {x + x_offset * distance, y + y_offset * distance} 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /traffic_jam/lib/traffic_jam/road.ex: -------------------------------------------------------------------------------- 1 | defmodule TrafficJam.Road do 2 | # Client API 3 | 4 | def start_link(road_width, car_width, cars) 5 | when is_integer(road_width) and road_width > 0 6 | and is_integer(cars) and cars > 0 and cars <= road_width do 7 | Agent.start_link( 8 | fn -> init(road_width, car_width, cars) end, 9 | name: __MODULE__ 10 | ) 11 | end 12 | 13 | def place_car(position) when is_integer(position) and position >= 0 do 14 | Agent.update(__MODULE__, fn set -> handle_place_car(set, position) end) 15 | end 16 | 17 | def move_car(position, new_position) 18 | when is_integer(position) and position >= 0 19 | and is_integer(new_position) and new_position >= 0 do 20 | Agent.update(__MODULE__, fn set -> 21 | handle_move_car(set, position, new_position) 22 | end) 23 | end 24 | 25 | def get_cars do 26 | Agent.get(__MODULE__, &handle_get_cars/1) 27 | end 28 | 29 | # Server API 30 | 31 | defp init(road_width, car_width, cars) do 32 | init_cars(road_width, car_width, cars) 33 | MapSet.new 34 | end 35 | 36 | defp handle_place_car(cars, position), do: MapSet.put(cars, position) 37 | 38 | defp handle_move_car(cars, position, new_position) do 39 | MapSet.delete(cars, position) |> MapSet.put(new_position) 40 | end 41 | 42 | defp handle_get_cars(cars), do: cars 43 | 44 | # Helpers 45 | 46 | defp init_cars(road_width, car_width, cars) do 47 | positions = div(road_width, car_width) 48 | 0..(positions - 1) 49 | |> Enum.shuffle 50 | |> Enum.take(cars) 51 | |> Enum.map(fn position -> position * car_width end) 52 | |> Enum.sort(fn a, b -> b < a end) 53 | |> start_cars(car_width, road_width, nil, nil) 54 | end 55 | 56 | defp start_cars([position | positions], car_width, road_width, first, last) do 57 | {:ok, car} = 58 | Supervisor.start_child( 59 | TrafficJam.CarSupervisor, 60 | [position, car_width, road_width, last] 61 | ) 62 | start_cars(positions, car_width, road_width, first || car, car) 63 | end 64 | defp start_cars([ ], _car_width, _road_width, first, last) do 65 | TrafficJam.Car.assign_leader(first, last) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /slime_mold/lib/slime_mold/cell.ex: -------------------------------------------------------------------------------- 1 | defmodule SlimeMold.Cell do 2 | defstruct board_size: nil, xy: nil, angle: 0, rand_state: nil 3 | 4 | # Client API 5 | 6 | def start do 7 | delay = Application.fetch_env!(:slime_mold, :cell_delay) 8 | 9 | init 10 | |> run(delay) 11 | end 12 | 13 | # Server API 14 | 15 | defp init do 16 | <> = :crypto.rand_bytes(12) 17 | rand_state = :rand.seed_s(:exs1024, {a, b, c}) 18 | 19 | board_size = {width, height} = SlimeMold.Board.get_size 20 | {x, rand_state} = :rand.uniform_s(width, rand_state) 21 | {y, rand_state} = :rand.uniform_s(height, rand_state) 22 | 23 | xy = {x - 1, y - 1} 24 | SlimeMold.Board.place_cell(xy) 25 | 26 | %__MODULE__{board_size: board_size, xy: xy, rand_state: rand_state} 27 | end 28 | 29 | defp run(cell, delay) do 30 | :timer.sleep(delay) 31 | 32 | cell 33 | |> wander 34 | |> drop_pheromone 35 | |> run(delay) 36 | end 37 | 38 | defp wander( 39 | cell = %__MODULE__{ 40 | board_size: {width, height}, 41 | xy: xy = {x, y}, 42 | angle: angle 43 | } 44 | ) do 45 | possible_xys = 46 | [wrap(angle - 1, 8), angle, wrap(angle + 1, 8)] 47 | |> Enum.map(fn possible_angle -> 48 | {x_offset, y_offset} = offsets(possible_angle) 49 | {{wrap(x + x_offset, width), wrap(y + y_offset, height)}, possible_angle} 50 | end) 51 | |> Enum.into(Map.new) 52 | possible_moves = 53 | SlimeMold.Board.sniff(Map.keys(possible_xys)) 54 | |> Enum.map(fn {possible_xy, pheromone} -> 55 | {possible_xy, Float.floor(pheromone + 0.0) |> trunc} 56 | end) 57 | best = 58 | Enum.max_by(possible_moves, fn {_xy, pheromone} -> pheromone end) 59 | |> elem(1) 60 | move = 61 | Enum.filter(possible_moves, fn {_xy, pheromone} -> pheromone == best end) 62 | |> Enum.random 63 | |> elem(0) 64 | SlimeMold.Board.move_cell(xy, move) 65 | %__MODULE__{cell | xy: move, angle: Map.fetch!(possible_xys, move)} 66 | end 67 | 68 | defp offsets(0), do: {0, -1} 69 | defp offsets(1), do: {1, -1} 70 | defp offsets(2), do: {1, 0} 71 | defp offsets(3), do: {1, 1} 72 | defp offsets(4), do: {0, 1} 73 | defp offsets(5), do: {-1, 1} 74 | defp offsets(6), do: {-1, 0} 75 | defp offsets(7), do: {-1, -1} 76 | 77 | defp wrap(value, limit), do: rem(limit + value, limit) 78 | 79 | defp drop_pheromone(cell = %__MODULE__{xy: xy}) do 80 | SlimeMold.Board.increase_pheromone(xy) 81 | cell 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /traffic_jam/lib/traffic_jam/window.ex: -------------------------------------------------------------------------------- 1 | defmodule TrafficJam.Window do 2 | @behaviour :wx_object 3 | 4 | defstruct ~w[frame panel]a 5 | 6 | require Logger 7 | require Record 8 | Record.defrecordp( 9 | :wx, Record.extract(:wx, from_lib: "wx/include/wx.hrl") 10 | ) 11 | Record.defrecordp( 12 | :wxClose, Record.extract(:wxClose, from_lib: "wx/include/wx.hrl") 13 | ) 14 | 15 | # Client API 16 | 17 | def start_link(paint_delay, size, car_width) do 18 | window = 19 | :wx_object.start_link(__MODULE__, [paint_delay, size, car_width], [ ]) 20 | {:ok, :wx_object.get_pid(window)} 21 | end 22 | 23 | # Server API 24 | 25 | def init([paint_delay | args]) do 26 | Process.flag(:trap_exit, true) 27 | 28 | :timer.send_interval(paint_delay, :tick) 29 | 30 | :wx.new 31 | 32 | :wx.batch(fn -> do_init(args) end) 33 | end 34 | 35 | def handle_call(message, _from, state) do 36 | Logger.debug "Unhandled call: #{inspect message}" 37 | {:reply, :ok, state} 38 | end 39 | 40 | def handle_cast(message, state) do 41 | Logger.debug "Unhandled cast: #{inspect message}" 42 | {:noreply, state} 43 | end 44 | 45 | def handle_event(wx(event: wxClose()), state) do 46 | System.halt(0) 47 | {:noreply, state} 48 | end 49 | def handle_event(wx, state) do 50 | Logger.debug "Unhandled event: #{inspect wx}" 51 | {:noreply, state} 52 | end 53 | 54 | def handle_info(:tick, state = %__MODULE__{frame: frame}) do 55 | :wxFrame.refresh(frame, eraseBackground: false) 56 | {:noreply, state} 57 | end 58 | def handle_info(info, state) do 59 | Logger.debug "Unhandled info: #{inspect info}" 60 | {:noreply, state} 61 | end 62 | 63 | def code_change(_old_vsn, _state, _extra) do 64 | {:error, :not_implemented} 65 | end 66 | 67 | def terminate(_reason, %__MODULE__{frame: frame, panel: panel}) do 68 | :wx_object.call(panel, :shutdown) 69 | :wxFrame.destroy(frame) 70 | :wx.destroy 71 | end 72 | 73 | # Helpers 74 | 75 | defp do_init([{width, height}, car_width]) do 76 | frame = :wxFrame.new(:wx.null, :wx_const.wxID_ANY, 'Traffic Jam') 77 | panel = TrafficJam.Canvas.start_link(frame, car_width) 78 | 79 | :wxFrame.setClientSize(frame, {width, height}) 80 | sizer = :wxBoxSizer.new(:wx_const.wxVERTICAL) 81 | :wxSizer.add(sizer, panel, flag: :wx_const.wxEXPAND, proportion: 1) 82 | :wxPanel.setSizer(frame, sizer) 83 | :wxSizer.layout(sizer) 84 | frame_size = :wxFrame.getSize(frame) 85 | :wxFrame.setMinSize(frame, frame_size) 86 | :wxFrame.setMaxSize(frame, frame_size) 87 | 88 | :wxFrame.connect(frame, :close_window) 89 | 90 | :wxFrame.center(frame) 91 | :wxFrame.show(frame) 92 | 93 | {frame, %__MODULE__{frame: frame, panel: panel}} 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /turtles/lib/turtles/clock.ex: -------------------------------------------------------------------------------- 1 | defmodule Turtles.Clock do 2 | use GenServer 3 | 4 | require Logger 5 | 6 | alias Turtles.{World, Turtle} 7 | 8 | defstruct world: nil, size: nil, turtle_starter: nil, turtles: MapSet.new 9 | 10 | # Client API 11 | 12 | def start_link(world, size, turtle_starter, options \\ [ ]) do 13 | clock = %__MODULE__{world: world, size: size, turtle_starter: turtle_starter} 14 | GenServer.start_link(__MODULE__, clock, options) 15 | end 16 | 17 | def advance(clock) do 18 | GenServer.call(clock, :tick) 19 | end 20 | 21 | # Server API 22 | 23 | def init( 24 | clock = %__MODULE__{ 25 | world: world, 26 | size: size = {width, height}, 27 | turtle_starter: turtle_starter 28 | } 29 | ) do 30 | plants = place(round(width * height * 0.2), size, MapSet.new) 31 | World.place_plants(world, plants) 32 | 33 | turtles = place(300, size, MapSet.new) 34 | World.place_turtles(world, turtles) 35 | turtle_pids = 36 | Enum.reduce(turtles, MapSet.new, fn location, set -> 37 | {:ok, pid} = turtle_starter.([world, size, turtle_starter, location]) 38 | Process.monitor(pid) 39 | MapSet.put(set, pid) 40 | end) 41 | 42 | new_clock = 43 | %__MODULE__{clock | turtles: turtle_pids} 44 | {:ok, new_clock} 45 | end 46 | 47 | def handle_call( 48 | :tick, 49 | _from, 50 | clock = %__MODULE__{ 51 | world: world, 52 | size: size = {width, height}, 53 | turtles: turtles 54 | } 55 | ) do 56 | plants = place(round(width * height / 1_000), size, MapSet.new) 57 | World.place_plants(world, plants) 58 | 59 | new_turtles = 60 | Enum.reduce(turtles, turtles, fn turtle, set -> 61 | case Turtle.act(turtle) do 62 | pid when is_pid(pid) -> 63 | # Logger.info "Turtles: #{MapSet.size(set) + 1}" 64 | Process.monitor(pid) 65 | MapSet.put(set, pid) 66 | :ok -> 67 | set 68 | end 69 | end) 70 | 71 | {:reply, :ok, %__MODULE__{clock | turtles: new_turtles}} 72 | end 73 | 74 | def handle_info( 75 | {:DOWN, _reference, :process, pid, _reason}, 76 | clock = %__MODULE__{turtles: turtles} 77 | ) do 78 | # Logger.info "Turtles: #{MapSet.size(turtles) - 1}" 79 | {:noreply, %__MODULE__{clock | turtles: MapSet.delete(turtles, pid)}} 80 | end 81 | def handle_info(message, clock) do 82 | Logger.debug "Unexpected message: #{message}" 83 | {:noreply, clock} 84 | end 85 | 86 | # Helpers 87 | 88 | defp place(0, _size, locations), do: Enum.to_list(locations) 89 | defp place(count, size = {width, height}, locations) do 90 | location = {:rand.uniform(width) - 1, :rand.uniform(height) - 1} 91 | new_count = count - (if MapSet.member?(locations, location), do: 0, else: 1) 92 | place(new_count, size, MapSet.put(locations, location)) 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /forest_fire_sim/test/world_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WorldTest do 2 | use ExUnit.Case, async: true 3 | import ExUnit.CaptureIO 4 | 5 | alias ForestFireSim.{Forest, World} 6 | 7 | @tag todo: true 8 | test "starts fires for all initial fires" do 9 | forest = Forest.from_string("&*") 10 | test_process = self 11 | fire_starter = fn {xy, intensity} -> 12 | send(test_process, {:fire_started, xy, intensity}) 13 | end 14 | World.create(forest, fire_starter) 15 | 16 | xy = {0, 0} 17 | intensity = 4 18 | assert_receive {:fire_started, ^xy, ^intensity} 19 | end 20 | 21 | @tag todo: true 22 | test "allows another process to peek at locations" do 23 | forest = Forest.from_string("&*") 24 | fire_starter = fn _fire -> :ok end 25 | world = World.create(forest, fire_starter) 26 | 27 | xy = {0, 0} 28 | reply_to = self 29 | send(world, {:debug_location, xy, reply_to}) 30 | intensity = 4 31 | assert_receive {:debug_location_response, {:fire, ^intensity}} 32 | 33 | other_xy = {1, 0} 34 | send(world, {:debug_location, other_xy, reply_to}) 35 | assert_receive {:debug_location_response, :tree} 36 | end 37 | 38 | @tag todo: true 39 | test "can advance a fire" do 40 | forest = Forest.from_string("&*") 41 | fire_starter = fn _fire -> :ok end 42 | world = World.create(forest, fire_starter) 43 | 44 | xy = {0, 0} 45 | send(world, {:advance_fire, xy}) 46 | reply_to = self 47 | send(world, {:debug_location, xy, reply_to}) 48 | intensity = 3 49 | assert_receive {:debug_location_response, {:fire, ^intensity}} 50 | neighbor_xy = {1, 0} 51 | send(world, {:debug_location, neighbor_xy, reply_to}) 52 | fresh_fire_intensity = 4 53 | assert_receive {:debug_location_response, {:fire, ^fresh_fire_intensity}} 54 | end 55 | 56 | @tag todo: true 57 | test "starts new fires during an advance" do 58 | forest = Forest.from_string("&*") 59 | test_process = self 60 | fire_starter = fn {xy, intensity} -> 61 | send(test_process, {:fire_started, xy, intensity}) 62 | end 63 | world = World.create(forest, fire_starter) 64 | xy = {0, 0} 65 | assert_receive {:fire_started, ^xy, 4} 66 | 67 | send(world, {:advance_fire, xy}) 68 | neighbor_xy = {1, 0} 69 | intensity = 4 70 | assert_receive {:fire_started, ^neighbor_xy, ^intensity} 71 | end 72 | 73 | @tag todo: true 74 | test "can render the current state of the world" do 75 | forest = Forest.from_string("&*") 76 | fire_starter = fn _fire -> :ok end 77 | 78 | assert capture_io(fn -> 79 | world = World.create(forest, fire_starter) 80 | 81 | send(world, :render) 82 | # to ensure `world` is past the `:render` message 83 | xy = {0, 0} 84 | reply_to = self 85 | send(world, {:debug_location, xy, reply_to}) 86 | assert_receive {:debug_location_response, _location} 87 | end) |> String.trim == Forest.to_string(forest) 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /turtles/test/turtle_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TurtleTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Turtles.{World, Turtle} 5 | 6 | @size {10, 10} 7 | 8 | setup do 9 | {:ok, world} = World.start_link(@size) 10 | fake_turtle_starter = fn _args -> {:ok, self} end 11 | [world: world, fake_turtle_starter: fake_turtle_starter] 12 | end 13 | 14 | test "turtles know their location", 15 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 16 | {:ok, turtle} = 17 | Turtle.start_link(world, @size, fake_turtle_starter, {1, 2}, :east, 1) 18 | assert Turtle.get_location(turtle) == {1, 2} 19 | end 20 | 21 | test "turtles know their heading", 22 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 23 | {:ok, turtle} = 24 | Turtle.start_link(world, @size, fake_turtle_starter, {0, 0}, :south, 1) 25 | assert Turtle.get_heading(turtle) == :south 26 | end 27 | 28 | test "turtles know their energy", 29 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 30 | {:ok, turtle} = 31 | Turtle.start_link(world, @size, fake_turtle_starter, {0, 0}, :east, 42) 32 | assert Turtle.get_energy(turtle) == 42 33 | end 34 | 35 | test "turtles will always try to eat first", 36 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 37 | World.place_plants(world, [{0, 0}]) 38 | {:ok, turtle} = 39 | Turtle.start_link(world, @size, fake_turtle_starter, {0, 0}, :east, 1) 40 | Turtle.act(turtle) 41 | assert Turtle.get_energy(turtle) > 1 42 | end 43 | 44 | test "turtles with energy and no food will move", 45 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 46 | {:ok, turtle} = 47 | Turtle.start_link(world, @size, fake_turtle_starter, {0, 0}, :north, 1) 48 | Turtle.act(turtle) 49 | assert Turtle.get_location(turtle) in [{9, 9}, {0, 9}, {1, 9}] 50 | assert Turtle.get_heading(turtle) in ~w[northwest north northeast]a 51 | assert Turtle.get_energy(turtle) == 0.9 52 | end 53 | 54 | test "turtles will pass if they can't eat or move", 55 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 56 | World.place_turtles(world, [{1, 9}, {1, 0}, {1, 1}]) 57 | {:ok, turtle} = 58 | Turtle.start_link(world, @size, fake_turtle_starter, {0, 0}, :east, 1) 59 | Turtle.act(turtle) 60 | assert Turtle.get_location(turtle) == {0, 0} 61 | assert Turtle.get_heading(turtle) == :east 62 | assert Turtle.get_energy(turtle) == 1 63 | end 64 | 65 | test "turtles without energy and no food will die", 66 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 67 | Process.flag(:trap_exit, true) 68 | {:ok, turtle} = 69 | Turtle.start_link(world, @size, fake_turtle_starter, {0, 0}, :east, 0.9) 70 | Turtle.act(turtle) 71 | assert_receive {:EXIT, ^turtle, :normal} 72 | end 73 | 74 | test "turtles with surplus energy will try to give birth", 75 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 76 | {:ok, turtle} = 77 | Turtle.start_link(world, @size, fake_turtle_starter, {0, 0}, :east, 15) 78 | Turtle.act(turtle) 79 | assert hd(World.changes(world).turtles) in [{1, 9}, {1, 0}, {1, 1}] 80 | assert Turtle.get_energy(turtle) == 7.5 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /traffic_jam/lib/traffic_jam/car.ex: -------------------------------------------------------------------------------- 1 | defmodule TrafficJam.Car do 2 | defstruct ~w[position car_width road_width speed leader]a 3 | 4 | @max_speed 3.0 5 | 6 | use GenServer 7 | require Logger 8 | 9 | # Client API 10 | 11 | def start_link(position, car_width, road_width, leader) do 12 | GenServer.start_link(__MODULE__, [position, car_width, road_width, leader]) 13 | end 14 | 15 | def assign_leader(car, leader) do 16 | GenServer.cast(car, {:assign_leader, leader}) 17 | end 18 | 19 | def get_lead(pid, position) do 20 | GenServer.call(pid, {:get_lead, position}) 21 | end 22 | 23 | # Server API 24 | 25 | def init([position, car_width, road_width, leader]) do 26 | GenServer.cast(self, :finish_init) 27 | initial_state = %__MODULE__{ 28 | position: position, 29 | car_width: car_width, 30 | road_width: road_width, 31 | speed: :rand.uniform * @max_speed, 32 | leader: leader 33 | } 34 | {:ok, initial_state} 35 | end 36 | 37 | def handle_call( 38 | {:get_lead, follower_position}, 39 | _from, 40 | state = %__MODULE__{position: position, road_width: road_width} 41 | ) do 42 | adjusted_position = 43 | if position < follower_position, do: position + road_width, else: position 44 | {:reply, adjusted_position - follower_position, state} 45 | end 46 | def handle_call(message, _from, state) do 47 | Logger.debug "Unhandled call: #{inspect message}" 48 | {:reply, :ok, state} 49 | end 50 | 51 | def handle_cast(:finish_init, state = %__MODULE__{position: position}) do 52 | TrafficJam.Road.place_car(position) 53 | :timer.send_interval(333, :move) 54 | {:noreply, state} 55 | end 56 | def handle_cast({:assign_leader, leader}, state) do 57 | {:noreply, %__MODULE__{state | leader: leader}} 58 | end 59 | def handle_cast(message, state) do 60 | Logger.debug "Unhandled cast: #{inspect message}" 61 | {:noreply, state} 62 | end 63 | 64 | def handle_info( 65 | :move, 66 | state = %__MODULE__{ 67 | position: position, 68 | car_width: car_width, 69 | road_width: road_width, 70 | speed: speed, 71 | leader: leader 72 | } 73 | ) do 74 | new_speed = adjust_speed(position, car_width, speed, leader) 75 | new_position = move(position, road_width, new_speed) 76 | update_position(position, new_position) 77 | {:noreply, %__MODULE__{state | position: new_position}} 78 | end 79 | def handle_info(message, state) do 80 | Logger.debug "Unhandled info: #{inspect message}" 81 | {:noreply, state} 82 | end 83 | 84 | # Helpers 85 | 86 | defp adjust_speed(position, car_width, speed, leader) do 87 | bumped = speed + 0.5 88 | current_max = 89 | case TrafficJam.Car.get_lead(leader, position) do 90 | distance when distance < car_width + 1 + @max_speed -> 91 | Enum.max([0, distance - (car_width + 1)]) 92 | _distance -> 93 | @max_speed 94 | end 95 | if bumped > current_max, do: current_max, else: bumped 96 | end 97 | 98 | defp move(position, road_width, speed) do 99 | case position + speed do 100 | new_position when new_position >= road_width -> new_position 101 | new_position -> new_position 102 | end 103 | end 104 | 105 | defp update_position(old_position, new_position) do 106 | trunced_old_position = trunc(old_position) 107 | trunced_new_position = trunc(new_position) 108 | if trunced_old_position != trunced_new_position do 109 | TrafficJam.Road.move_car(trunced_old_position, trunced_new_position) 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /traffic_jam/lib/traffic_jam/canvas.ex: -------------------------------------------------------------------------------- 1 | defmodule TrafficJam.Canvas do 2 | @behaviour :wx_object 3 | 4 | @black {0, 0, 0, 255} 5 | @white {255, 255, 255, 255} 6 | 7 | defstruct ~w[panel car_width]a 8 | 9 | require Logger 10 | require Record 11 | Record.defrecordp( 12 | :wx, Record.extract(:wx, from_lib: "wx/include/wx.hrl") 13 | ) 14 | Record.defrecordp( 15 | :wxPaint, Record.extract(:wxPaint, from_lib: "wx/include/wx.hrl") 16 | ) 17 | Record.defrecordp( 18 | :wxKey, Record.extract(:wxKey, from_lib: "wx/include/wx.hrl") 19 | ) 20 | 21 | # Client API 22 | 23 | def start_link(parent, car_width) do 24 | :wx_object.start_link(__MODULE__, [parent, car_width], [ ]) 25 | end 26 | 27 | # Server API 28 | 29 | def init(args) do 30 | :wx.batch(fn -> do_init(args) end) 31 | end 32 | 33 | def handle_call(:shutdown, _from, state = %__MODULE__{panel: panel}) do 34 | :wxPanel.destroy(panel) 35 | {:reply, :ok, state} 36 | end 37 | def handle_call(message, _from, state) do 38 | Logger.debug "Unhandled call: #{inspect message}" 39 | {:reply, :ok, state} 40 | end 41 | 42 | def handle_cast(message, state) do 43 | Logger.debug "Unhandled cast: #{inspect message}" 44 | {:noreply, state} 45 | end 46 | 47 | def handle_sync_event(wx(event: wxPaint()), _paint_event, state) do 48 | paint(state) 49 | :ok 50 | end 51 | 52 | def handle_event( 53 | wx( 54 | event: wxKey( 55 | keyCode: key, 56 | controlDown: control, 57 | shiftDown: shift, 58 | metaDown: meta, 59 | altDown: alt 60 | ) 61 | ), 62 | state 63 | ) do 64 | new_state = do_key_down(key, shift, alt, control, meta, state) 65 | {:noreply, new_state} 66 | end 67 | def handle_event(wx, state) do 68 | Logger.debug "Unhandled event: #{inspect wx}" 69 | {:noreply, state} 70 | end 71 | 72 | def handle_info(info, state) do 73 | Logger.debug "Unhandled info: #{inspect info}" 74 | {:noreply, state} 75 | end 76 | 77 | def code_change(_old_vsn, _state, _extra) do 78 | {:error, :not_implemented} 79 | end 80 | 81 | def terminate(_reason, _state) do 82 | :ok 83 | end 84 | 85 | # Helpers 86 | 87 | defp do_init([parent, car_width]) do 88 | panel = :wxPanel.new(parent) 89 | 90 | :wxFrame.connect(panel, :paint, [:callback]) 91 | :wxPanel.connect(panel, :key_down) 92 | 93 | {panel, %__MODULE__{panel: panel, car_width: car_width}} 94 | end 95 | 96 | defp do_key_down(key, shift, alt, control, meta, state) do 97 | case key do 98 | ?Q when not (shift or alt or control or meta) -> System.halt(0) 99 | _ -> :ok 100 | end 101 | state 102 | end 103 | 104 | defp paint(%__MODULE__{panel: panel, car_width: car_width}) do 105 | {width, height} = :wxPanel.getClientSize(panel) 106 | drawing_context = :wxPaintDC.new(panel) 107 | 108 | paint_road(drawing_context, width, height) 109 | paint_cars(drawing_context, car_width) 110 | 111 | :wxPaintDC.destroy(drawing_context) 112 | end 113 | 114 | defp paint_road(context, width, height) do 115 | :wxDC.setPen(context, :wxPen.new(@black)) 116 | :wxDC.setBrush(context, :wxBrush.new(@black)) 117 | :wxDC.drawRectangle(context, {0, 0}, {width, height}) 118 | end 119 | 120 | defp paint_cars(context, car_width) do 121 | TrafficJam.Road.get_cars 122 | |> Enum.each(fn car -> 123 | :wxDC.setPen(context, :wxPen.new(@white)) 124 | :wxDC.setBrush(context, :wxBrush.new(@white)) 125 | :wxDC.drawRectangle(context, {car, 49}, {car_width, 2}) 126 | end) 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /turtles_and_frogs/lib/turtles_and_frogs/pond.ex: -------------------------------------------------------------------------------- 1 | defmodule TurtlesAndFrogs.Pond do 2 | defstruct turtles: %{ }, frogs: %{ }, changes: [ ] 3 | 4 | # Client API 5 | 6 | def start_link(size, turtle_count, frog_count) do 7 | Agent.start_link( 8 | fn -> init(size, turtle_count, frog_count) end, 9 | name: __MODULE__ 10 | ) 11 | end 12 | 13 | def look_around(neighbors, type) do 14 | Agent.get( 15 | __MODULE__, 16 | fn struct -> handle_look_around(struct, neighbors, type) end 17 | ) 18 | end 19 | 20 | def try_move(old_xy, new_xy, type) do 21 | Agent.get_and_update( 22 | __MODULE__, 23 | fn struct -> handle_try_move(struct, old_xy, new_xy, type) end 24 | ) 25 | end 26 | 27 | def get_changes do 28 | Agent.get_and_update(__MODULE__, fn struct -> handle_get_changes(struct) end) 29 | end 30 | 31 | # Server API 32 | 33 | defp init(size = {width, height}, turtle_count, frog_count) do 34 | xys = 35 | for x <- 0..(width - 1), y <- 0..(height - 1) do {x, y} end 36 | |> Enum.shuffle 37 | |> Enum.take(turtle_count + frog_count) 38 | turtles = 39 | xys 40 | |> Enum.take(turtle_count) 41 | |> build_critter_list(:turtle, size) 42 | frogs = 43 | xys 44 | |> Enum.drop(turtle_count) 45 | |> build_critter_list(:frog, size) 46 | grid = 47 | for y <- 0..(height - 1) do 48 | for x <- 0..(width - 1) do 49 | cond do 50 | Map.has_key?(frogs, {x, y}) -> 2 51 | Map.has_key?(turtles, {x, y}) -> 1 52 | true -> 0 53 | end 54 | end 55 | end 56 | %__MODULE__{turtles: turtles, frogs: frogs, changes: [{:init, grid}]} 57 | end 58 | 59 | defp handle_look_around(pond, neighbors, type) do 60 | Enum.map(neighbors, fn xy -> 61 | [Map.get(same_kind(pond, type), xy, 0), 1] |> Enum.min 62 | end) 63 | end 64 | 65 | defp handle_try_move( 66 | pond = %__MODULE__{changes: changes}, 67 | old_xy, 68 | new_xy, 69 | type 70 | ) do 71 | if count_critters_at(new_xy, pond) == 0 do 72 | critters = same_kind(pond, type) 73 | removed = 74 | if Map.fetch!(critters, old_xy) == 1 do 75 | Map.delete(critters, old_xy) 76 | else 77 | Map.update!(critters, old_xy, fn count -> count - 1 end) 78 | end 79 | added = Map.update(removed, new_xy, 1, fn count -> count + 1 end) 80 | left = [{:remove, old_xy} | changes] 81 | new_pond = 82 | if type == :turtle do 83 | %__MODULE__{ 84 | pond | 85 | turtles: added, 86 | changes: [{:add_turtle, new_xy} | left] 87 | } 88 | else 89 | %__MODULE__{ 90 | pond | 91 | frogs: added, 92 | changes: [{:add_frog, new_xy} | left] 93 | } 94 | end 95 | {new_xy, new_pond} 96 | else 97 | {old_xy, pond} 98 | end 99 | end 100 | 101 | defp handle_get_changes(pond = %__MODULE__{changes: changes}) do 102 | {Enum.reverse(changes), %__MODULE__{pond | changes: [ ]}} 103 | end 104 | 105 | # Helpers 106 | 107 | defp build_critter_list(xys, type, size) do 108 | Enum.reduce(xys, %{ }, fn xy, counts -> 109 | Task.Supervisor.start_child( 110 | TurtlesAndFrogs.Critters, 111 | TurtlesAndFrogs.Critter, 112 | :start, 113 | [xy, type, size] 114 | ) 115 | Map.update(counts, xy, 1, fn count -> count + 1 end) 116 | end) 117 | end 118 | 119 | defp same_kind(%__MODULE__{turtles: turtles}, :turtle), do: turtles 120 | defp same_kind(%__MODULE__{frogs: frogs}, :frog), do: frogs 121 | 122 | defp count_critters_at(xy, %__MODULE__{turtles: turtles, frogs: frogs}) do 123 | Map.get(turtles, xy, 0) + Map.get(frogs, xy, 0) 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /forest_fire_sim/test/forest_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ForestTest do 2 | alias ForestFireSim.Forest 3 | 4 | use ExUnit.Case, async: true 5 | doctest Forest 6 | 7 | test "can be constructed from strings" do 8 | forest = Forest.from_string("&* ") 9 | assert Forest.get_location(forest, {0, 0}) == {:fire, 4} 10 | assert Forest.get_location(forest, {1, 0}) == :tree 11 | assert Forest.get_location(forest, {2, 0}) == nil 12 | end 13 | 14 | test "construction errors out if widths are inconsistent" do 15 | assert_raise RuntimeError, fn -> 16 | Forest.from_string( 17 | """ 18 | *** 19 | ** 20 | """ 21 | ) 22 | end 23 | end 24 | 25 | test "can be stringified" do 26 | string = "&* " 27 | assert string |> Forest.from_string |> Forest.to_string(false) == string 28 | end 29 | 30 | test "is stringified with ANSI colors by default" do 31 | stringified = "&* " |> Forest.from_string |> Forest.to_string 32 | colored = IO.ANSI.format_fragment( 33 | [:clear, :home, 34 | IO.ANSI.format([:bright, :red, "&"]), 35 | IO.ANSI.format([:green, "*"]), 36 | " "] 37 | ) |> IO.chardata_to_string 38 | assert stringified == colored 39 | end 40 | 41 | test "can be generated from statistics" do 42 | empty = 43 | Forest.generate(%{width: 2, height: 1, percent: 0}) 44 | |> Forest.to_string(false) 45 | assert empty == " " 46 | 47 | full = 48 | Forest.generate(%{width: 2, height: 1, percent: 100}) 49 | |> Forest.to_string(false) 50 | assert full == "&*" 51 | end 52 | 53 | test "can return all fires" do 54 | actual = Forest.from_string( 55 | """ 56 | & * 57 | * & 58 | """ 59 | ) 60 | |> Forest.get_fires 61 | |> Enum.sort_by(fn {{x, y}, _location} -> [x, y] end) 62 | expected = [{{0, 0}, 4}, {{2, 1}, 4}] 63 | assert actual == expected 64 | end 65 | 66 | test "can reduce a fires intensity" do 67 | forest = 68 | [4, 3, 2, 1] 69 | |> Enum.reduce(Forest.from_string("&"), fn intensity, reduced_forest -> 70 | assert Forest.get_location(reduced_forest, {0, 0}) == {:fire, intensity} 71 | Forest.reduce_fire(reduced_forest, {0, 0}) 72 | end) 73 | assert Forest.get_location(forest, {0, 0}) == nil 74 | end 75 | 76 | test "can spread fires" do 77 | spread = fn forest, xy -> 78 | {forest, fires} = Forest.spread_fire(forest, xy) 79 | sorted_fires = Enum.sort_by(fires, fn {{x, y}, _location} -> [x, y] end) 80 | {forest, sorted_fires} 81 | end 82 | forest = Forest.from_string( 83 | """ 84 | *** 85 | *&* 86 | *** 87 | """ 88 | ) 89 | 90 | {forest, new_fires} = spread.(forest, {1, 1}) 91 | expected_forest = 92 | String.trim( 93 | """ 94 | *&* 95 | &&& 96 | *&* 97 | """ 98 | ) 99 | expected_fires = [{{0, 1}, 4}, {{1, 0}, 4}, {{1, 2}, 4}, {{2, 1}, 4}] 100 | assert Forest.to_string(forest, false) == expected_forest 101 | assert new_fires == expected_fires 102 | 103 | {forest, new_fires} = spread.(forest, {1, 0}) 104 | expected_forest = 105 | String.trim( 106 | """ 107 | &&& 108 | &&& 109 | *&* 110 | """ 111 | ) 112 | expected_fires = [{{0, 0}, 4}, {{2, 0}, 4}] 113 | assert Forest.to_string(forest, false) == expected_forest 114 | assert new_fires == expected_fires 115 | 116 | {forest, new_fires} = spread.(forest, {2, 1}) 117 | expected_forest = 118 | String.trim( 119 | """ 120 | &&& 121 | &&& 122 | *&& 123 | """ 124 | ) 125 | expected_fires = [{{2, 2}, 4}] 126 | assert Forest.to_string(forest, false) == expected_forest 127 | assert new_fires == expected_fires 128 | 129 | {forest, new_fires} = spread.(forest, {2, 2}) 130 | assert Forest.to_string(forest, false) == expected_forest 131 | assert new_fires == [ ] 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /turtles/test/world_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WorldTest do 2 | use ExUnit.Case, async: true 3 | 4 | @size {100, 100} 5 | 6 | setup do 7 | {:ok, world} = Turtles.World.start_link(@size) 8 | [world: world] 9 | end 10 | 11 | test "begins with a full world clear", %{world: world} do 12 | all_locations = 13 | for x <- 0..(elem(@size, 0) - 1), y <- 0..(elem(@size, 1) - 1) do 14 | {x, y} 15 | end 16 | assert Turtles.World.changes(world).clears == all_locations 17 | end 18 | 19 | test "begins with no plant changes", %{world: world} do 20 | assert Turtles.World.changes(world).plants == [ ] 21 | end 22 | 23 | test "begins with no turtle changes", %{world: world} do 24 | assert Turtles.World.changes(world).turtles == [ ] 25 | end 26 | 27 | test "records plant placements", %{world: world} do 28 | Turtles.World.place_plants(world, [{0, 0}, {50, 50}]) 29 | assert Turtles.World.changes(world).plants == [{50, 50}, {0, 0}] 30 | end 31 | 32 | test "records turtle placements", %{world: world} do 33 | Turtles.World.place_turtles(world, [{0, 0}, {50, 50}]) 34 | assert Turtles.World.changes(world).turtles == [{50, 50}, {0, 0}] 35 | end 36 | 37 | test "turtles will eat if food is available", %{world: world} do 38 | Turtles.World.place_plants(world, [{0, 0}]) 39 | Turtles.World.place_turtles(world, [{0, 0}]) 40 | Turtles.World.changes(world) 41 | 42 | assert Turtles.World.eat_or_move(world, {0, 0}, {1, 0}) == :ate 43 | changes = Turtles.World.changes(world) 44 | assert changes.clears == [{0, 0}] 45 | assert changes.turtles == [{0, 0}] 46 | end 47 | 48 | test "turtles will move if no food is available and location is open", 49 | %{world: world} do 50 | Turtles.World.place_turtles(world, [{0, 0}]) 51 | Turtles.World.changes(world) 52 | 53 | assert Turtles.World.eat_or_move(world, {0, 0}, {1, 0}) == :moved 54 | changes = Turtles.World.changes(world) 55 | assert changes.clears == [{0, 0}] 56 | assert changes.turtles == [{1, 0}] 57 | end 58 | 59 | test "turtles will pass if they can't eat or move", %{world: world} do 60 | Turtles.World.place_turtles(world, [{0, 0}, {1, 0}]) 61 | Turtles.World.changes(world) 62 | 63 | assert Turtles.World.eat_or_move(world, {0, 0}, {1, 0}) == :pass 64 | assert Turtles.World.changes(world) == Turtles.World.Changes.empty 65 | end 66 | 67 | test "turtles will not die if food is available", %{world: world} do 68 | Turtles.World.place_plants(world, [{0, 0}]) 69 | Turtles.World.place_turtles(world, [{0, 0}]) 70 | Turtles.World.changes(world) 71 | 72 | assert Turtles.World.eat_or_die(world, {0, 0}) == :ate 73 | changes = Turtles.World.changes(world) 74 | assert changes.clears == [{0, 0}] 75 | assert changes.turtles == [{0, 0}] 76 | end 77 | 78 | test "turtles will die if no food is available", %{world: world} do 79 | Turtles.World.place_turtles(world, [{0, 0}]) 80 | Turtles.World.changes(world) 81 | 82 | assert Turtles.World.eat_or_die(world, {0, 0}) == :died 83 | assert Turtles.World.changes(world).clears == [{0, 0}] 84 | end 85 | 86 | test "turtles can give birth on open locations", %{world: world} do 87 | Turtles.World.place_turtles(world, [{0, 0}]) 88 | Turtles.World.changes(world) 89 | 90 | assert Turtles.World.give_birth(world, {1, 0}) == :birthed 91 | assert Turtles.World.changes(world).turtles == [{1, 0}] 92 | end 93 | 94 | test "turtles pass if they can't give birth", %{world: world} do 95 | Turtles.World.place_turtles(world, [{0, 0}, {1, 0}]) 96 | Turtles.World.changes(world) 97 | 98 | assert Turtles.World.give_birth(world, {1, 0}) == :pass 99 | assert Turtles.World.changes(world) == Turtles.World.Changes.empty 100 | end 101 | 102 | test "clears changes as they are fetched", %{world: world} do 103 | assert Turtles.World.changes(world) != Turtles.World.Changes.empty 104 | assert Turtles.World.changes(world) == Turtles.World.Changes.empty 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /turtles/test/clock_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ClockTest do 2 | use ExUnit.Case, async: true 3 | 4 | ############################### 5 | ### Tests start on line 57! ### 6 | ############################### 7 | 8 | alias Turtles.{Clock, World} 9 | 10 | defmodule FakeTurtle do 11 | use GenServer 12 | 13 | def start_link(_world, _size, _turtle_starter, _location, _energy \\ 10) do 14 | GenServer.start_link(__MODULE__, nil) 15 | end 16 | 17 | def init(nil) do 18 | turtle = self 19 | Agent.update(FakeTurtles, fn turtles -> 20 | Map.update!(turtles, :turtles, fn pids -> [turtle | pids] end) 21 | end) 22 | {:ok, nil} 23 | end 24 | 25 | def handle_call(:act, _from, nil) do 26 | Agent.update(FakeTurtles, fn turtles -> 27 | Map.update!(turtles, :act_count, fn count -> count + 1 end) 28 | end) 29 | newborns = Agent.get_and_update(FakeTurtles, fn 30 | turtles = %{births: 0} -> {0, turtles} 31 | turtles = %{births: births} -> {births, %{turtles | births: births - 1}} 32 | end) 33 | reply = if newborns > 0 do 34 | {:ok, pid} = 35 | __MODULE__.start_link(:world, :size, :turtle_starter, :location) 36 | pid 37 | else 38 | :ok 39 | end 40 | {:reply, reply, nil} 41 | end 42 | end 43 | 44 | @size {100, 100} 45 | 46 | setup do 47 | {:ok, world} = World.start_link(@size) 48 | 49 | Agent.start_link(fn -> 50 | %{turtles: [ ], act_count: 0, births: 0} 51 | end, name: FakeTurtles) 52 | fake_turtle_starter = fn args -> apply(FakeTurtle, :start_link, args) end 53 | 54 | [world: world, fake_turtle_starter: fake_turtle_starter] 55 | end 56 | 57 | test "plants are populated (20% of the world) at the dawn of time", 58 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 59 | {:ok, _clock} = Clock.start_link(world, @size, fake_turtle_starter) 60 | expected_count = round(elem(@size, 0) * elem(@size, 1) * 0.2) 61 | assert length(World.changes(world).plants) == expected_count 62 | end 63 | 64 | @tag todo: true 65 | test "300 turtles are born at the dawn of time", 66 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 67 | {:ok, _clock} = Clock.start_link(world, @size, fake_turtle_starter) 68 | assert length(World.changes(world).turtles) == 300 69 | end 70 | 71 | @tag todo: true 72 | test "starts a process for each turtle as it is born", 73 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 74 | {:ok, _clock} = Clock.start_link(world, @size, fake_turtle_starter) 75 | count = Agent.get(FakeTurtles, fn turtles -> 76 | length(Map.get(turtles, :turtles)) 77 | end) 78 | assert count == 300 79 | end 80 | 81 | @tag todo: true 82 | test "instructs each turtle to act as time advances", 83 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 84 | {:ok, clock} = Clock.start_link(world, @size, fake_turtle_starter) 85 | Clock.advance(clock) 86 | count = Agent.get(FakeTurtles, fn turtles -> 87 | Map.get(turtles, :act_count) 88 | end) 89 | assert count == 300 90 | end 91 | 92 | @tag todo: true 93 | test "tracks new turtles born during the actions of others", 94 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 95 | {:ok, clock} = Clock.start_link(world, @size, fake_turtle_starter) 96 | 97 | Agent.update(FakeTurtles, fn turtles -> Map.put(turtles, :births, 1) end) 98 | Clock.advance(clock) 99 | count = Agent.get(FakeTurtles, fn turtles -> 100 | length(Map.get(turtles, :turtles)) 101 | end) 102 | assert count == 301 103 | 104 | Clock.advance(clock) 105 | count = Agent.get(FakeTurtles, fn turtles -> 106 | Map.get(turtles, :act_count) 107 | end) 108 | assert count == 601 109 | end 110 | 111 | @tag todo: true 112 | test "stops tracking turtles when they die", 113 | %{world: world, fake_turtle_starter: fake_turtle_starter} do 114 | {:ok, clock} = Clock.start_link(world, @size, fake_turtle_starter) 115 | turtle = Agent.get_and_update(FakeTurtles, fn 116 | turtles = %{turtles: [death | rest]} -> {death, %{turtles | turtles: rest}} 117 | end) 118 | GenServer.stop(turtle) 119 | Clock.advance(clock) 120 | count = Agent.get(FakeTurtles, fn turtles -> 121 | Map.get(turtles, :act_count) 122 | end) 123 | assert count == 299 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /turtles/lib/turtles/world.ex: -------------------------------------------------------------------------------- 1 | defmodule Turtles.World do 2 | defmodule Changes do 3 | defstruct clears: [ ], plants: [ ], turtles: [ ] 4 | 5 | def empty do 6 | %__MODULE__{ } 7 | end 8 | end 9 | 10 | defstruct size: nil, 11 | plants: MapSet.new, 12 | turtles: MapSet.new, 13 | changes: Changes.empty 14 | 15 | # Client API 16 | 17 | def start_link(size, options \\ [ ]) do 18 | Agent.start_link(fn -> init(size) end, options) 19 | end 20 | 21 | def changes(world) do 22 | Agent.get_and_update(world, &handle_changes/1) 23 | end 24 | 25 | # 26 | # locations is a list of x, y tuples: 27 | # 28 | # [{0, 0}, {0, 1}, …] 29 | # 30 | def place_plants(world, locations) do 31 | Agent.update(world, fn struct -> handle_place_plants(struct, locations) end) 32 | end 33 | 34 | # 35 | # locations is a list of x, y tuples: 36 | # 37 | # [{0, 0}, {0, 1}, …] 38 | # 39 | def place_turtles(world, locations) do 40 | Agent.update(world, fn struct -> handle_place_turtles(struct, locations) end) 41 | end 42 | 43 | def eat_or_move(world, location, move_location) do 44 | Agent.get_and_update(world, fn struct -> 45 | handle_eat_or_move(struct, location, move_location) 46 | end) 47 | end 48 | 49 | def eat_or_die(world, location) do 50 | Agent.get_and_update(world, fn struct -> 51 | handle_eat_or_die(struct, location) 52 | end) 53 | end 54 | 55 | def give_birth(world, new_location) do 56 | Agent.get_and_update(world, fn struct -> 57 | handle_give_birth(struct, new_location) 58 | end) 59 | end 60 | 61 | # Server API 62 | 63 | defp init(size = {width, height}) do 64 | background = for x <- 0..(width - 1), y <- 0..(height - 1) do {x, y} end 65 | %__MODULE__{size: size, changes: %Changes{clears: background}} 66 | end 67 | 68 | defp handle_changes(world = %__MODULE__{changes: changes}) do 69 | {changes, %__MODULE__{world | changes: Changes.empty}} 70 | end 71 | 72 | defp handle_place_plants( 73 | world = %__MODULE__{plants: plants, changes: changes}, 74 | locations 75 | ) do 76 | {new_plants, new_plant_changes} = 77 | add_with_changes(plants, changes.plants, Enum.uniq(locations)) 78 | new_changes = %Changes{changes | plants: new_plant_changes} 79 | %__MODULE__{world | plants: new_plants, changes: new_changes} 80 | end 81 | 82 | defp handle_place_turtles( 83 | world = %__MODULE__{turtles: turtles, changes: changes}, 84 | locations 85 | ) do 86 | {new_turtles, new_turtle_changes} = 87 | add_with_changes(turtles, changes.turtles, Enum.uniq(locations)) 88 | new_changes = %Changes{changes | turtles: new_turtle_changes} 89 | %__MODULE__{world | turtles: new_turtles, changes: new_changes} 90 | end 91 | 92 | defp handle_eat_or_move( 93 | world = %__MODULE__{plants: plants, turtles: turtles, changes: changes}, 94 | location, 95 | move_location 96 | ) do 97 | cond do 98 | MapSet.member?(plants, location) -> 99 | eat(world, location) 100 | not MapSet.member?(turtles, move_location) -> 101 | new_turtles = 102 | MapSet.delete(turtles, location) 103 | |> MapSet.put(move_location) 104 | new_changes = %Changes{ 105 | changes | 106 | clears: [location | changes.clears], 107 | turtles: [move_location | changes.turtles] 108 | } 109 | {:moved, %__MODULE__{world | turtles: new_turtles, changes: new_changes}} 110 | true -> 111 | {:pass, world} 112 | end 113 | end 114 | 115 | defp handle_eat_or_die( 116 | world = %__MODULE__{plants: plants, turtles: turtles, changes: changes}, 117 | location 118 | ) do 119 | cond do 120 | MapSet.member?(plants, location) -> 121 | eat(world, location) 122 | true -> 123 | new_turtles = MapSet.delete(turtles, location) 124 | new_changes = %Changes{changes | clears: [location | changes.clears]} 125 | {:died, %__MODULE__{world | turtles: new_turtles, changes: new_changes}} 126 | end 127 | end 128 | 129 | defp handle_give_birth( 130 | world = %__MODULE__{turtles: turtles, changes: changes}, 131 | new_location 132 | ) do 133 | if not MapSet.member?(turtles, new_location) do 134 | new_turtles = MapSet.put(turtles, new_location) 135 | new_changes = %Changes{changes | turtles: [new_location | changes.turtles]} 136 | {:birthed, %__MODULE__{world | turtles: new_turtles, changes: new_changes}} 137 | else 138 | {:pass, world} 139 | end 140 | end 141 | 142 | # Helpers 143 | 144 | defp add_with_changes(set, changes, [ ]), do: {set, changes} 145 | defp add_with_changes(set, changes, [location | locations]) do 146 | new_set = MapSet.put(set, location) 147 | new_changes = [location | changes] 148 | add_with_changes(new_set, new_changes, locations) 149 | end 150 | 151 | defp eat(world = %__MODULE__{plants: plants, changes: changes}, location) do 152 | new_plants = MapSet.delete(plants, location) 153 | new_changes = %Changes{ 154 | changes | 155 | clears: [location | changes.clears], 156 | turtles: [location | changes.turtles] 157 | } 158 | {:ate, %__MODULE__{world | plants: new_plants, changes: new_changes}} 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /slime_mold/lib/slime_mold/renderer.ex: -------------------------------------------------------------------------------- 1 | defmodule SlimeMold.Renderer do 2 | use GenServer 3 | 4 | defstruct frame: nil, panel: nil 5 | 6 | @black {0, 0, 0, 255} 7 | @white {255, 255, 255, 255} 8 | @orange {255, 127, 0, 255} 9 | 10 | require Record 11 | Record.defrecordp( 12 | :wx, Record.extract(:wx, from_lib: "wx/include/wx.hrl") 13 | ) 14 | Record.defrecordp( 15 | :wxClose, Record.extract(:wxClose, from_lib: "wx/include/wx.hrl") 16 | ) 17 | Record.defrecordp( 18 | :wxPaint, Record.extract(:wxPaint, from_lib: "wx/include/wx.hrl") 19 | ) 20 | Record.defrecordp( 21 | :wxKey, Record.extract(:wxKey, from_lib: "wx/include/wx.hrl") 22 | ) 23 | 24 | # Client API 25 | 26 | def start_link do 27 | GenServer.start_link(__MODULE__, [ ]) 28 | end 29 | 30 | # Server API 31 | 32 | def init([ ]) do 33 | :wx.new 34 | 35 | delay = Application.fetch_env!(:slime_mold, :paint_delay) 36 | scale = Application.fetch_env!(:slime_mold, :scale) 37 | 38 | :timer.send_interval(delay, :tick) 39 | 40 | :wx.batch(fn -> 41 | frame = :wxFrame.new(:wx.null, :wx_const.wxID_ANY, 'Slime Mold') 42 | panel = :wxPanel.new(frame) 43 | 44 | {width, height} = SlimeMold.Board.get_size 45 | :wxFrame.setClientSize(frame, {width * scale, height * scale}) 46 | sizer = :wxBoxSizer.new(:wx_const.wxVERTICAL) 47 | :wxSizer.add(sizer, panel, flag: :wx_const.wxEXPAND, proportion: 1) 48 | :wxPanel.setSizer(frame, sizer) 49 | :wxSizer.layout(sizer) 50 | frame_size = {frame_width, frame_height} = :wxFrame.getSize(frame) 51 | :wxFrame.setMinSize(frame, frame_size) 52 | :wxFrame.setMaxSize(frame, frame_size) 53 | 54 | bitmap = :wxBitmap.new(frame_width, frame_height) 55 | user_data = %{scale: scale, bitmap: bitmap} 56 | :wxPanel.connect(panel, :paint, [callback: &paint/2, userData: user_data]) 57 | :wxFrame.connect(frame, :close_window) 58 | :wxPanel.connect(panel, :key_down) 59 | 60 | :wxFrame.center(frame) 61 | :wxFrame.show(frame) 62 | 63 | {:ok, %__MODULE__{frame: frame, panel: panel}} 64 | end) 65 | end 66 | 67 | def paint( 68 | wx(event: wxPaint(), obj: panel, userData: %{scale: scale, bitmap: bitmap}), 69 | _paint_event 70 | ) do 71 | {width, height} = :wxPanel.getClientSize(panel) 72 | bitmap_context = :wxMemoryDC.new(bitmap) 73 | panel_context = :wxPaintDC.new(panel) 74 | 75 | paint_board(bitmap_context, width, height, scale) 76 | 77 | :wxPaintDC.blit( 78 | panel_context, 79 | {0, 0}, 80 | {width, height}, 81 | bitmap_context, 82 | {0, 0} 83 | ) 84 | 85 | :wxMemoryDC.destroy(bitmap_context) 86 | :wxPaintDC.destroy(panel_context) 87 | end 88 | 89 | defp paint_board(context, width, height, scale) do 90 | changed = SlimeMold.Board.get_changed 91 | 92 | if Map.get(changed, :background) do 93 | paint_background(context, width, height) 94 | end 95 | Enum.each(changed.blanks, fn xy -> paint_blank(context, scale, xy) end) 96 | Enum.each(changed.pheromones, fn {xy, pheromone} -> 97 | paint_pheromone(context, scale, xy, pheromone) 98 | end) 99 | Enum.each(changed.cells, fn xy -> paint_cell(context, scale, xy) end) 100 | end 101 | 102 | defp paint_background(context, width, height) do 103 | :wxDC.setPen(context, :wxPen.new(@black)) 104 | :wxDC.setBrush(context, :wxBrush.new(@black)) 105 | :wxDC.drawRectangle(context, {0, 0}, {width, height}) 106 | end 107 | 108 | defp paint_blank(context, scale, xy) do 109 | # ensure we get stray pixels 110 | paint_scaled(context, scale, xy, @black) 111 | paint_scaled(context, scale, xy, @black) 112 | paint_scaled(context, scale, xy, @black) 113 | end 114 | 115 | defp paint_cell(context, scale, xy) do 116 | paint_scaled(context, scale, xy, @orange) 117 | end 118 | 119 | defp paint_pheromone(context, scale, xy, pheromone) do 120 | color = 121 | case pheromone do 122 | n when n <= 0 -> raise "Illegal color" 123 | n when n <= 1 -> {0, 85, 0, 255} 124 | n when n <= 2 -> {0, 170, 0, 255} 125 | n when n <= 3 -> {0, 255, 0, 255} 126 | n when n > 3 -> @white 127 | end 128 | paint_scaled(context, scale, xy, color) 129 | end 130 | 131 | defp paint_scaled(context, scale, {x, y}, color) do 132 | :wxDC.setPen(context, :wxPen.new(color)) 133 | :wxDC.setBrush(context, :wxBrush.new(color)) 134 | :wxDC.drawRectangle(context, {x * scale, y * scale}, {scale, scale}) 135 | end 136 | 137 | def handle_info(:tick, state) do 138 | :wxFrame.refresh(state.frame, eraseBackground: false) 139 | {:noreply, state} 140 | end 141 | def handle_info(wx(event: wxClose()), state) do 142 | System.halt(0) 143 | {:noreply, state} 144 | end 145 | def handle_info( 146 | wx(event: wxKey( 147 | keyCode: key, 148 | controlDown: control, 149 | shiftDown: shift, 150 | metaDown: meta, 151 | altDown: alt 152 | )), 153 | state 154 | ) do 155 | case key do 156 | ?Q when not (shift or alt or control or meta) -> System.halt(0) 157 | _ -> :ok 158 | end 159 | {:noreply, state} 160 | end 161 | def handle_info(_msg, state) do 162 | {:noreply, state} 163 | end 164 | 165 | def terminate(_reason, _state) do 166 | :wx.destroy 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /turtles/lib/turtles/turtle.ex: -------------------------------------------------------------------------------- 1 | defmodule Turtles.Turtle do 2 | use GenServer 3 | 4 | alias Turtles.{World, TurtleSupervisor} 5 | 6 | @headings ~w[north northeast east southeast south southwest west northwest]a 7 | @starting_energy 10 8 | 9 | defstruct world: nil, 10 | size: nil, 11 | turtle_starter: nil, 12 | location: nil, 13 | heading: nil, 14 | energy: @starting_energy 15 | 16 | # Client API 17 | 18 | def start_link(world, size, turtle_starter, location, heading, energy) do 19 | turtle = %__MODULE__{ 20 | world: world, 21 | size: size, 22 | turtle_starter: turtle_starter, 23 | location: location, 24 | heading: heading, 25 | energy: energy 26 | } 27 | GenServer.start_link(__MODULE__, turtle) 28 | end 29 | 30 | def start_supervised( 31 | world, size, turtle_starter, location, energy \\ @starting_energy 32 | ) do 33 | Supervisor.start_child( 34 | TurtleSupervisor, 35 | [world, size, turtle_starter, location, random_heading, energy] 36 | ) 37 | end 38 | 39 | def get_location(pid) do 40 | GenServer.call(pid, :get_location) 41 | end 42 | 43 | def get_heading(pid) do 44 | GenServer.call(pid, :get_heading) 45 | end 46 | 47 | def get_energy(pid) do 48 | GenServer.call(pid, :get_energy) 49 | end 50 | 51 | # returns :ok or the pid of a newborn turtle 52 | def act(pid) do 53 | GenServer.call(pid, :act) 54 | end 55 | 56 | # Server API 57 | 58 | def handle_call( 59 | :get_location, 60 | _from, 61 | turtle = %__MODULE__{location: location} 62 | ) do 63 | {:reply, location, turtle} 64 | end 65 | 66 | def handle_call(:get_heading, _from, turtle = %__MODULE__{heading: heading}) do 67 | {:reply, heading, turtle} 68 | end 69 | 70 | def handle_call(:get_energy, _from, turtle = %__MODULE__{energy: energy}) do 71 | {:reply, energy, turtle} 72 | end 73 | 74 | def handle_call(:act, _from, turtle) do 75 | {new_turtle, pid} = do_act(turtle) |> record_action 76 | if new_turtle.energy > 0 do 77 | {:reply, pid || :ok, new_turtle} 78 | else 79 | {:stop, :normal, pid || :ok, new_turtle} 80 | end 81 | end 82 | 83 | # Helpers 84 | 85 | defp random_heading, do: Enum.at(@headings, :rand.uniform(8) - 1) 86 | 87 | defp do_act(turtle = %__MODULE__{world: world, energy: energy}) 88 | when energy >= 15 do 89 | {birth_location, moved_turtle} = pick_moved_location(turtle) 90 | {turtle, moved_turtle, World.give_birth(world, birth_location)} 91 | end 92 | defp do_act( 93 | turtle = %__MODULE__{world: world, location: location, energy: energy} 94 | ) 95 | when energy < 1 do 96 | {turtle, turtle, World.eat_or_die(world, location)} 97 | end 98 | defp do_act( 99 | turtle = %__MODULE__{world: world, location: location} 100 | ) do 101 | {moved_location, moved_turtle} = pick_moved_location(turtle) 102 | {turtle, moved_turtle, World.eat_or_move(world, location, moved_location)} 103 | end 104 | 105 | defp pick_moved_location( 106 | turtle = %__MODULE__{size: size, location: location, heading: heading} 107 | ) do 108 | index = 109 | @headings 110 | |> Enum.with_index 111 | |> Enum.find(fn {h, _i} -> h == heading end) 112 | |> elem(1) 113 | new_heading = Enum.at(@headings, drift(index)) 114 | new_location = move(size, location, new_heading) 115 | new_turtle = 116 | %__MODULE__{turtle | location: new_location, heading: new_heading} 117 | {new_location, new_turtle} 118 | end 119 | 120 | defp drift(i) do 121 | rem(i + (:rand.uniform(3) - 2), 8) 122 | end 123 | 124 | defp move(size, {x, y}, :north), do: wrap(size, {x, y - 1}) 125 | defp move(size, {x, y}, :northeast), do: wrap(size, {x + 1, y - 1}) 126 | defp move(size, {x, y}, :east), do: wrap(size, {x + 1, y}) 127 | defp move(size, {x, y}, :southeast), do: wrap(size, {x + 1, y + 1}) 128 | defp move(size, {x, y}, :south), do: wrap(size, {x, y + 1}) 129 | defp move(size, {x, y}, :southwest), do: wrap(size, {x - 1, y + 1}) 130 | defp move(size, {x, y}, :west), do: wrap(size, {x - 1, y}) 131 | defp move(size, {x, y}, :northwest), do: wrap(size, {x - 1, y - 1}) 132 | 133 | defp wrap(size, location), do: wrap_width(size, location) 134 | 135 | defp wrap_width(size = {width, _height}, {-1, y}) do 136 | wrap_height(size, {width - 1, y}) 137 | end 138 | defp wrap_width(size = {width, _height}, {x, y}) do 139 | wrap_height(size, {rem(x, width), y}) 140 | end 141 | 142 | defp wrap_height({_width, height}, {x, -1}) do 143 | {x, height - 1} 144 | end 145 | defp wrap_height({_width, height}, {x, y}) do 146 | {x, rem(y, height)} 147 | end 148 | 149 | defp record_action( 150 | {turtle = %__MODULE__{energy: energy}, _moved_turtle, :ate} 151 | ) do 152 | {%__MODULE__{turtle | energy: energy + 1}, nil} 153 | end 154 | defp record_action({turtle, _moved_turtle, :died}) do 155 | {%__MODULE__{turtle | energy: 0}, nil} 156 | end 157 | defp record_action( 158 | {_turtle, moved_turtle = %__MODULE__{energy: energy}, :moved} 159 | ) do 160 | {%__MODULE__{moved_turtle | energy: energy - 0.1}, nil} 161 | end 162 | defp record_action( 163 | { turtle = %__MODULE__{ 164 | world: world, 165 | size: size, 166 | energy: energy, 167 | turtle_starter: turtle_starter 168 | }, 169 | %__MODULE__{location: birth_location}, 170 | :birthed } 171 | ) do 172 | split_energy = energy / 2 173 | {:ok, pid} = turtle_starter.( 174 | [world, size, turtle_starter, birth_location, split_energy] 175 | ) 176 | {%__MODULE__{turtle | energy: split_energy}, pid} 177 | end 178 | defp record_action({turtle, _moved_turtle, :pass}), do: {turtle, nil} 179 | end 180 | -------------------------------------------------------------------------------- /forest_fire_sim/lib/forest_fire_sim/forest.ex: -------------------------------------------------------------------------------- 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 219 | -------------------------------------------------------------------------------- /slime_mold/lib/slime_mold/board.ex: -------------------------------------------------------------------------------- 1 | defmodule SlimeMold.Board do 2 | @empty_changes %{cells: MapSet.new, pheromones: %{ }, blanks: MapSet.new} 3 | 4 | defstruct size: nil, 5 | cells: %{ }, 6 | pheromones: %{ }, 7 | changes: Map.put(@empty_changes, :background, true) 8 | 9 | # Client API 10 | 11 | def start_link(size = {width, height}) 12 | when is_integer(width) and width > 0 and is_integer(height) and height > 0 do 13 | Agent.start_link(fn -> init(size) end, name: __MODULE__) 14 | end 15 | 16 | def get_size do 17 | Agent.get(__MODULE__, &handle_get_size/1) 18 | end 19 | 20 | def get_changed do 21 | Agent.get_and_update(__MODULE__, &handle_get_changed/1) 22 | end 23 | 24 | def place_cell(xy = {x, y}) 25 | when is_integer(x) and x >= 0 and is_integer(y) and y >= 0 do 26 | Agent.update(__MODULE__, fn struct -> handle_place_cell(struct, xy) end) 27 | end 28 | 29 | def sniff(xys) when is_list(xys) do 30 | Enum.map(xys, fn 31 | xy = {x, y} when is_integer(x) and x >= 0 and is_integer(y) and y >= 0 -> 32 | {xy, Agent.get(__MODULE__, fn struct -> handle_sniff(struct, xy) end)} 33 | end) 34 | end 35 | 36 | def move_cell(from = {fx, fy}, to = {tx, ty}) 37 | when is_integer(fx) and fx >= 0 and is_integer(fy) and fy >= 0 38 | and is_integer(tx) and tx >= 0 and is_integer(ty) and ty >= 0 do 39 | Agent.update(__MODULE__, fn struct -> handle_move_cell(struct, from, to) end) 40 | end 41 | 42 | def increase_pheromone(xy = {x, y}) 43 | when is_integer(x) and x >= 0 and is_integer(y) and y >= 0 do 44 | Agent.update(__MODULE__, fn struct -> 45 | handle_increase_pheromone(struct, xy) 46 | end) 47 | end 48 | 49 | def evaporate_all do 50 | Agent.update(__MODULE__, &handle_evaporate_all/1) 51 | end 52 | 53 | def diffuse_all do 54 | Agent.update(__MODULE__, &handle_diffuse_all/1) 55 | end 56 | 57 | # Server API 58 | 59 | defp init(size) do 60 | cells = Application.fetch_env!(:slime_mold, :cells) 61 | 62 | Stream.repeatedly(fn -> 63 | Task.Supervisor.start_child(SlimeMold.Actives, &SlimeMold.Cell.start/0) 64 | end) |> Enum.take(cells) 65 | Task.Supervisor.start_child( 66 | SlimeMold.Actives, 67 | &SlimeMold.Environment.start/0 68 | ) 69 | 70 | %__MODULE__{size: size} 71 | end 72 | 73 | defp handle_get_size(%__MODULE__{size: size}), do: size 74 | 75 | defp handle_get_changed(board = %__MODULE__{changes: changes}) do 76 | {changes, %__MODULE__{board | changes: @empty_changes}} 77 | end 78 | 79 | defp handle_place_cell( 80 | board = %__MODULE__{cells: cells, changes: changes}, 81 | xy 82 | ) do 83 | %__MODULE__{ 84 | board | 85 | cells: Map.update(cells, xy, 1, &(&1 + 1)), 86 | changes: change_cell(changes, xy) 87 | } 88 | end 89 | 90 | defp handle_sniff(%__MODULE__{pheromones: pheromones}, xy) do 91 | Map.get(pheromones, xy, {0, nil}) |> elem(0) 92 | end 93 | 94 | defp handle_move_cell( 95 | board = %__MODULE__{cells: cells, pheromones: pheromones, changes: changes}, 96 | from, 97 | to 98 | ) do 99 | {from_cells, from_changes} = 100 | case Map.fetch!(cells, from) do 101 | 1 -> 102 | changed = 103 | case Map.get(pheromones, from) do 104 | {value, _category} -> 105 | %{changes | pheromones: Map.put(changes.pheromones, from, value)} 106 | nil -> 107 | %{changes | blanks: MapSet.put(changes.blanks, from)} 108 | end 109 | {Map.delete(cells, from), changed} 110 | n when n > 1 -> 111 | {Map.put(cells, from, n - 1), changes} 112 | _ -> 113 | raise "Cell count too low" 114 | end 115 | {to_changed, new_cells} = Map.get_and_update(from_cells, to, fn current -> 116 | {is_nil(current), (current || 0) + 1} 117 | end) 118 | new_changes = 119 | if to_changed do 120 | change_cell(from_changes, to) 121 | else 122 | from_changes 123 | end 124 | %__MODULE__{board | cells: new_cells, changes: new_changes} 125 | end 126 | 127 | defp handle_increase_pheromone( 128 | board = %__MODULE__{cells: cells, pheromones: pheromones, changes: changes}, 129 | xy 130 | ) do 131 | {value, category} = Map.get(pheromones, xy, {0, nil}) 132 | new_value = value + 1.0 133 | new_category = Float.floor(new_value) |> trunc 134 | new_pheromones = Map.put(pheromones, xy, {new_value, new_category}) 135 | new_changes = 136 | cond do 137 | new_category == category -> changes 138 | Map.has_key?(cells, xy) -> changes 139 | true -> 140 | %{changes | pheromones: Map.put(changes.pheromones, xy, new_value)} 141 | end 142 | %__MODULE__{board | pheromones: new_pheromones, changes: new_changes} 143 | end 144 | 145 | defp handle_evaporate_all( 146 | board = %__MODULE__{cells: cells, pheromones: pheromones, changes: changes} 147 | ) do 148 | {new_pheromones, new_changes} = 149 | Enum.reduce( 150 | pheromones, 151 | {pheromones, changes}, 152 | fn {xy, {value, category}}, {evaporated, changed} -> 153 | new_value = value * 0.9 154 | new_category = Float.floor(new_value) |> trunc 155 | with_new_pheromone = 156 | if new_value < 0.1 do 157 | Map.delete(evaporated, xy) 158 | else 159 | Map.put(evaporated, xy, {new_value, new_category}) 160 | end 161 | with_new_change = 162 | cond do 163 | new_value < 0.1 -> 164 | %{changed | blanks: MapSet.put(changed.blanks, xy)} 165 | new_category == category -> 166 | changed 167 | Map.has_key?(cells, xy) -> 168 | changed 169 | true -> 170 | %{ changed | 171 | pheromones: Map.put(changed.pheromones, xy, new_value) } 172 | end 173 | {with_new_pheromone, with_new_change} 174 | end 175 | ) 176 | %__MODULE__{board | pheromones: new_pheromones, changes: new_changes} 177 | end 178 | 179 | defp handle_diffuse_all( 180 | board = %__MODULE__{ 181 | size: size, 182 | cells: cells, 183 | pheromones: pheromones, 184 | changes: changes 185 | } 186 | ) do 187 | new_pheromones = 188 | reduce_for_diffusion(Map.keys(pheromones), pheromones, %{ }, size) 189 | |> add_for_dissusion 190 | new_changes = 191 | calculate_diffusion_changes(cells, pheromones, changes, new_pheromones) 192 | %__MODULE__{board | pheromones: new_pheromones, changes: new_changes} 193 | end 194 | 195 | # Helpers 196 | 197 | defp change_cell(changes = %{cells: cells}, xy) do 198 | %{changes | cells: MapSet.put(cells, xy)} 199 | end 200 | 201 | defp reduce_for_diffusion([xy | rest], pheromones, additions, size) do 202 | {amount, new_pheromones} = 203 | Map.get_and_update!(pheromones, xy, fn {value, _category} -> 204 | diffusion = value * 0.2 205 | new_value = value - diffusion 206 | new_category = Float.floor(new_value) |> trunc 207 | {diffusion / 8, {new_value, new_category}} 208 | end) 209 | new_additions = 210 | xy 211 | |> neighbors(size) 212 | |> Enum.reduce(additions, fn neighbor_xy, added -> 213 | Map.update(added, neighbor_xy, amount, &(&1 + amount)) 214 | end) 215 | reduce_for_diffusion(rest, new_pheromones, new_additions, size) 216 | end 217 | defp reduce_for_diffusion([ ], pheromones, additions, _size) do 218 | {Map.keys(additions), pheromones, additions} 219 | end 220 | 221 | defp neighbors({x, y}, {width, height}) do 222 | [ {-1, -1}, {0, -1}, {1, -1}, 223 | {-1, 0}, {1, 0}, 224 | {-1, 1}, {0, 1}, {1, 1} ] 225 | |> Enum.map(fn {x_offset, y_offset} -> 226 | {rem(width + (x + x_offset), width), rem(height + (y + y_offset), height)} 227 | end) 228 | end 229 | 230 | defp add_for_dissusion({xys, pheromones, additions}) do 231 | Enum.reduce(xys, pheromones, fn xy, new_pheromones -> 232 | new_value = Map.fetch!(additions, xy) 233 | new_category = Float.floor(new_value) |> trunc 234 | Map.update( 235 | new_pheromones, 236 | xy, 237 | {new_value, new_category}, 238 | fn {value, _category} -> 239 | {value + new_value, new_category} 240 | end 241 | ) 242 | end) 243 | end 244 | 245 | defp calculate_diffusion_changes(cells, pheromones, changes, new_pheromones) do 246 | Enum.reduce( 247 | new_pheromones, 248 | changes, 249 | fn {xy, {new_value, new_category}}, new_changes -> 250 | category = Map.get(pheromones, xy, {0, nil}) |> elem(1) 251 | cond do 252 | new_category == category -> 253 | new_changes 254 | Map.has_key?(cells, xy) -> 255 | new_changes 256 | true -> 257 | %{ new_changes | 258 | pheromones: Map.put(new_changes.pheromones, xy, new_value) } 259 | end 260 | end 261 | ) 262 | end 263 | end 264 | --------------------------------------------------------------------------------