├── .formatter.exs ├── .gitignore ├── README.md ├── commands.txt ├── config └── config.exs ├── lib ├── toy_robot.ex └── toy_robot │ ├── application.ex │ ├── cli.ex │ ├── command_interpreter.ex │ ├── command_runner.ex │ ├── game │ ├── game.ex │ ├── player.ex │ ├── player_supervisor.ex │ ├── players.ex │ └── server.ex │ ├── robot.ex │ ├── simulation.ex │ └── table.ex ├── mix.exs ├── mix.lock ├── test ├── fixtures │ └── commands.txt ├── test_helper.exs └── toy_robot │ ├── cli_test.exs │ ├── command_interpreter_test.exs │ ├── command_runner_test.exs │ ├── game │ ├── game_test.exs │ ├── player_test.exs │ └── players_test.exs │ ├── robot_test.exs │ ├── simulation_test.exs │ └── table_test.exs └── toy_robot /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.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 third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | toy_robot-*.tar 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ToyRobot 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `toy_robot` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:toy_robot, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at [https://hexdocs.pm/toy_robot](https://hexdocs.pm/toy_robot). 21 | 22 | -------------------------------------------------------------------------------- /commands.txt: -------------------------------------------------------------------------------- 1 | PLACE 1,2,NORTH 2 | MOVE 3 | LEFT 4 | MOVE 5 | RIGHT 6 | MOVE 7 | REPORT 8 | -------------------------------------------------------------------------------- /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 | # third-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :toy_robot, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:toy_robot, :key) 18 | # 19 | # You can also configure a third-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 | -------------------------------------------------------------------------------- /lib/toy_robot.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot do 2 | @moduledoc """ 3 | Documentation for ToyRobot. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> ToyRobot.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/toy_robot/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.Application do 2 | use Application 3 | 4 | def start(_type, _args) do 5 | children = [ 6 | ToyRobot.Game.PlayerSupervisor 7 | ] 8 | 9 | # See https://hexdocs.pm/elixir/Supervisor.html 10 | # for other strategies and supported options 11 | opts = [strategy: :one_for_one, name: ToyRobot.Supervisor] 12 | Supervisor.start_link(children, opts) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/toy_robot/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.CLI do 2 | def main([file_name]) do 3 | if File.exists?(file_name) do 4 | File.stream!(file_name) 5 | |> Enum.map(&String.trim/1) 6 | |> ToyRobot.CommandInterpreter.interpret 7 | |> ToyRobot.CommandRunner.run 8 | else 9 | IO.puts "The file #{file_name} does not exist" 10 | end 11 | end 12 | 13 | def main(_) do 14 | IO.puts "Usage: toy_robot commands.txt" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/toy_robot/command_interpreter.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.CommandInterpreter do 2 | @doc """ 3 | Interprets commands from a commands list, in preparation for running them. 4 | 5 | ## Examples 6 | 7 | iex> alias ToyRobot.CommandInterpreter 8 | ToyRobot.CommandInterpreter 9 | iex> commands = ["PLACE 1,2,NORTH", "MOVE", "LEFT", "RIGHT", "REPORT"] 10 | ["PLACE 1,2,NORTH", "MOVE", "LEFT", "RIGHT", "REPORT"] 11 | iex> commands |> CommandInterpreter.interpret() 12 | [ 13 | {:place, %{north: 2, east: 1, facing: :north}}, 14 | :move, 15 | :turn_left, 16 | :turn_right, 17 | :report, 18 | ] 19 | """ 20 | def interpret(commands) do 21 | commands |> Enum.map(&do_interpret/1) 22 | end 23 | 24 | defp do_interpret(("PLACE" <> _rest) = command) do 25 | format = ~r/\APLACE (\d+),(\d+),(NORTH|EAST|SOUTH|WEST)\z/ 26 | case Regex.run(format, command) do 27 | [_command, east, north, facing] -> 28 | to_integer = &String.to_integer/1 29 | 30 | {:place, %{ 31 | east: to_integer.(east), 32 | north: to_integer.(north), 33 | facing: facing |> String.downcase |> String.to_atom 34 | }} 35 | nil -> {:invalid, command} 36 | end 37 | end 38 | 39 | defp do_interpret("MOVE"), do: :move 40 | defp do_interpret("LEFT"), do: :turn_left 41 | defp do_interpret("RIGHT"), do: :turn_right 42 | defp do_interpret("REPORT"), do: :report 43 | defp do_interpret(command), do: {:invalid, command} 44 | end 45 | -------------------------------------------------------------------------------- /lib/toy_robot/command_runner.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.CommandRunner do 2 | alias ToyRobot.{Simulation, Table} 3 | 4 | def run([{:place, placement} | rest]) do 5 | table = %Table{north_boundary: 4, east_boundary: 4} 6 | case Simulation.place(table, placement) do 7 | {:ok, simulation} -> run(rest, simulation) 8 | {:error, :invalid_placement} -> run(rest) 9 | end 10 | end 11 | 12 | def run([_command | rest]), do: run(rest) 13 | def run([]), do: nil 14 | 15 | def run([{:invalid, _command} | rest], simulation), do: run(rest, simulation) 16 | def run([:move | rest], simulation) do 17 | new_simulation = simulation 18 | |> Simulation.move 19 | |> case do 20 | {:ok, simulation} -> simulation 21 | {:error, :at_table_boundary} -> simulation 22 | end 23 | run(rest, new_simulation) 24 | end 25 | 26 | def run([:turn_left | rest], simulation) do 27 | {:ok, simulation} = simulation |> Simulation.turn_left 28 | run(rest, simulation) 29 | end 30 | 31 | def run([:turn_right | rest], simulation) do 32 | {:ok, simulation} = simulation |> Simulation.turn_right 33 | run(rest, simulation) 34 | end 35 | 36 | def run( 37 | [:report | rest], 38 | simulation 39 | ) do 40 | %{ 41 | east: east, 42 | north: north, 43 | facing: facing 44 | } = Simulation.report(simulation) 45 | 46 | facing = facing |> Atom.to_string |> String.upcase 47 | IO.puts "The robot is at (#{east}, #{north}) and is facing #{facing}" 48 | 49 | run(rest, simulation) 50 | end 51 | 52 | def run([], simulation), do: simulation 53 | end 54 | -------------------------------------------------------------------------------- /lib/toy_robot/game/game.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.Game do 2 | alias ToyRobot.Game.Server 3 | 4 | def start([north_boundary: north_boundary, east_boundary: east_boundary]) do 5 | Server.start_link([ 6 | north_boundary: north_boundary, 7 | east_boundary: east_boundary 8 | ]) 9 | end 10 | 11 | def place(game, position, name) do 12 | with :ok <- game |> valid_position(position), 13 | :ok <- game |> position_available(position) do 14 | GenServer.call(game, {:place, position, name}) 15 | else 16 | error -> error 17 | end 18 | end 19 | 20 | def move(game, name) do 21 | next_position = game |> next_position(name) 22 | 23 | game 24 | |> position_available(next_position) 25 | |> case do 26 | :ok -> GenServer.call(game, {:move, name}) 27 | error -> error 28 | end 29 | end 30 | 31 | def report(game, name) do 32 | GenServer.call(game, {:report, name}) 33 | end 34 | 35 | def player_count(game) do 36 | GenServer.call(game, :player_count) 37 | end 38 | 39 | defp valid_position(game, position) do 40 | GenServer.call(game, {:valid_position, position}) 41 | end 42 | 43 | defp position_available(game, position) do 44 | GenServer.call(game, {:position_available, position}) 45 | end 46 | 47 | defp next_position(game, name) do 48 | GenServer.call(game, {:next_position, name}) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/toy_robot/game/player.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.Game.Player do 2 | use GenServer 3 | 4 | alias ToyRobot.{Robot, Simulation} 5 | alias ToyRobot.Game.Players 6 | 7 | def start(table, position) do 8 | GenServer.start(__MODULE__, [table: table, position: position]) 9 | end 10 | 11 | def start_link(registry_id: registry_id, table: table, position: position, name: name) do 12 | name = process_name(registry_id, name) 13 | GenServer.start_link( 14 | __MODULE__, 15 | [ 16 | registry_id: registry_id, 17 | table: table, 18 | position: position, 19 | name: name 20 | ], 21 | name: name 22 | ) 23 | end 24 | 25 | def init([table: table, position: position]) do 26 | simulation = %Simulation{ 27 | table: table, 28 | robot: struct(Robot, position) 29 | } 30 | 31 | {:ok, simulation} 32 | end 33 | 34 | def init([registry_id: registry_id, table: table, position: position, name: name]) do 35 | position = 36 | registry_id 37 | |> Players.all 38 | |> Players.except(name) 39 | |> Players.positions 40 | |> Players.change_position_if_occupied(table, position) 41 | 42 | simulation = %Simulation{ 43 | table: table, 44 | robot: struct(Robot, position) 45 | } 46 | 47 | {:ok, simulation} 48 | end 49 | 50 | def process_name(registry_id, name) do 51 | {:via, Registry, {registry_id, name}} 52 | end 53 | 54 | def report(player) do 55 | GenServer.call(player, :report) 56 | end 57 | 58 | def move(player) do 59 | GenServer.cast(player, :move) 60 | end 61 | 62 | def next_position(player) do 63 | GenServer.call(player, :next_position) 64 | end 65 | 66 | def handle_call(:report, _from, simulation) do 67 | {:reply, simulation |> Simulation.report, simulation} 68 | end 69 | 70 | def handle_cast(:move, simulation) do 71 | {:ok, new_simulation} = simulation |> Simulation.move() 72 | {:noreply, new_simulation} 73 | end 74 | 75 | def handle_call(:next_position, _from, simulation) do 76 | next_position = simulation |> Simulation.next_position() 77 | {:reply, next_position, simulation} 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/toy_robot/game/player_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.Game.PlayerSupervisor do 2 | use DynamicSupervisor 3 | 4 | alias ToyRobot.Game.Player 5 | 6 | def start_link(args) do 7 | DynamicSupervisor.start_link(__MODULE__, args, name: __MODULE__) 8 | end 9 | 10 | def init(_args) do 11 | DynamicSupervisor.init(strategy: :one_for_one) 12 | end 13 | 14 | def start_child(registry_id, table, position, name) do 15 | DynamicSupervisor.start_child( 16 | __MODULE__, 17 | {Player, 18 | [ 19 | registry_id: registry_id, 20 | table: table, 21 | position: position, 22 | name: name 23 | ]} 24 | ) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/toy_robot/game/players.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.Game.Players do 2 | alias ToyRobot.Game.Player 3 | alias ToyRobot.Table 4 | 5 | def all(registry_id) do 6 | registry_id 7 | |> Registry.select([{{:"$1", :_, :_}, [], [:"$1"]}]) 8 | |> Enum.map(fn (player_name) -> 9 | Player.process_name(registry_id, player_name) 10 | end) 11 | end 12 | 13 | def except(players, name) do 14 | players |> Enum.reject(&(&1 == name)) 15 | end 16 | 17 | def positions(players) do 18 | players |> Enum.map(&(&1 |> Player.report |> coordinates)) 19 | end 20 | 21 | def position_available?(occupied_positions, position) do 22 | (position |> coordinates()) not in occupied_positions 23 | end 24 | 25 | def next_position(registry_id, name) do 26 | registry_id |> find(name) |> Player.next_position 27 | end 28 | 29 | def move(registry_id, name) do 30 | registry_id |> find(name) |> Player.move 31 | end 32 | 33 | def report(registry_id, name) do 34 | registry_id |> find(name) |> Player.report 35 | end 36 | 37 | def available_positions(occupied_positions, table) do 38 | Table.valid_positions(table) -- occupied_positions 39 | end 40 | 41 | def change_position_if_occupied(occupied_positions, table, position) do 42 | if occupied_positions |> position_available?(position) do 43 | position 44 | else 45 | new_position = 46 | occupied_positions 47 | |> available_positions(table) 48 | |> Enum.random 49 | 50 | new_position |> Map.put(:facing, position.facing) 51 | end 52 | end 53 | 54 | defp find(registry_id, name) do 55 | registry_id |> Player.process_name(name) 56 | end 57 | 58 | defp coordinates(position) do 59 | position |> Map.take([:north, :east]) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/toy_robot/game/server.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.Game.Server do 2 | use GenServer 3 | 4 | alias ToyRobot.Game.{Players, PlayerSupervisor} 5 | alias ToyRobot.Table 6 | 7 | def start_link(args) do 8 | GenServer.start_link(__MODULE__, args) 9 | end 10 | 11 | def init(north_boundary: north_boundary, east_boundary: east_boundary) do 12 | registry_id = "game-#{UUID.uuid4()}" |> String.to_atom() 13 | Registry.start_link(keys: :unique, name: registry_id) 14 | 15 | {:ok, 16 | %{ 17 | registry_id: registry_id, 18 | table: %Table{ 19 | north_boundary: north_boundary, 20 | east_boundary: east_boundary 21 | } 22 | }} 23 | end 24 | 25 | def handle_call( 26 | {:place, position, name}, 27 | _from, 28 | %{registry_id: registry_id, table: table} = state 29 | ) do 30 | {:ok, _player} = PlayerSupervisor.start_child(registry_id, table, position, name) 31 | {:reply, :ok, state} 32 | end 33 | 34 | def handle_call(:player_count, _from, %{registry_id: registry_id} = state) do 35 | {:reply, Registry.count(registry_id), state} 36 | end 37 | 38 | def handle_call({:valid_position, position}, _from, %{table: table} = state) do 39 | reply = 40 | if table |> Table.valid_position?(position) do 41 | :ok 42 | else 43 | {:error, :out_of_bounds} 44 | end 45 | 46 | {:reply, reply, state} 47 | end 48 | 49 | def handle_call({:position_available, position}, _from, %{registry_id: registry_id} = state) do 50 | position_available = 51 | registry_id 52 | |> Players.all 53 | |> Players.positions 54 | |> Players.position_available?(position) 55 | 56 | reply = if position_available, do: :ok, else: {:error, :occupied} 57 | {:reply, reply, state} 58 | end 59 | 60 | def handle_call({:next_position, name}, _from, %{registry_id: registry_id} = state) do 61 | {:reply, registry_id |> Players.next_position(name), state} 62 | end 63 | 64 | def handle_call({:move, name}, _from, %{registry_id: registry_id} = state) do 65 | Players.move(registry_id, name) 66 | 67 | {:reply, :ok, state} 68 | end 69 | 70 | def handle_call({:report, name}, _from, %{registry_id: registry_id} = state) do 71 | {:reply, Players.report(registry_id, name), state} 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/toy_robot/robot.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.Robot do 2 | alias ToyRobot.Robot 3 | defstruct [north: 0, east: 0, facing: :north] 4 | 5 | @doc """ 6 | Moves the robot forward one space in the direction it is facing. 7 | 8 | ## Examples 9 | 10 | iex> alias ToyRobot.Robot 11 | ToyRobot.Robot 12 | iex> robot = %Robot{north: 0, facing: :north} 13 | %Robot{north: 0, facing: :north} 14 | iex> robot |> Robot.move 15 | %Robot{north: 1} 16 | """ 17 | def move(%Robot{facing: facing} = robot) do 18 | case facing do 19 | :north -> robot |> move_north 20 | :east -> robot |> move_east 21 | :south -> robot |> move_south 22 | :west -> robot |> move_west 23 | end 24 | end 25 | 26 | @doc """ 27 | Turns the robot left. 28 | 29 | ## Examples 30 | 31 | iex> alias ToyRobot.Robot 32 | ToyRobot.Robot 33 | iex> robot = %Robot{facing: :north} 34 | %Robot{facing: :north} 35 | iex> robot |> Robot.turn_left 36 | %Robot{facing: :west} 37 | """ 38 | def turn_left(%Robot{facing: facing} = robot) do 39 | new_facing = case facing do 40 | :north -> :west 41 | :east -> :north 42 | :south -> :east 43 | :west -> :south 44 | end 45 | 46 | %Robot{robot | facing: new_facing} 47 | end 48 | 49 | @doc """ 50 | Turns the robot right. 51 | 52 | ## Examples 53 | 54 | iex> alias ToyRobot.Robot 55 | ToyRobot.Robot 56 | iex> robot = %Robot{facing: :north} 57 | %Robot{facing: :north} 58 | iex> robot |> Robot.turn_right 59 | %Robot{facing: :east} 60 | """ 61 | def turn_right(%Robot{facing: facing} = robot) do 62 | new_facing = case facing do 63 | :north -> :east 64 | :east -> :south 65 | :south -> :west 66 | :west -> :north 67 | end 68 | 69 | %Robot{robot | facing: new_facing} 70 | end 71 | 72 | defp move_east(%Robot{} = robot) do 73 | %Robot{robot | east: robot.east + 1} 74 | end 75 | 76 | defp move_west(%Robot{} = robot) do 77 | %Robot{robot | east: robot.east - 1} 78 | end 79 | 80 | defp move_north(%Robot{} = robot) do 81 | %Robot{robot | north: robot.north + 1} 82 | end 83 | 84 | defp move_south(%Robot{} = robot) do 85 | %Robot{robot | north: robot.north - 1} 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/toy_robot/simulation.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.Simulation do 2 | alias ToyRobot.{Robot, Simulation, Table} 3 | 4 | defstruct [:table, :robot] 5 | 6 | @doc """ 7 | Simulates placing a robot on a table. 8 | 9 | ## Examples 10 | 11 | When the robot is placed in a valid position: 12 | 13 | iex> alias ToyRobot.{Robot, Simulation, Table} 14 | [ToyRobot.Robot, ToyRobot.Simulation, ToyRobot.Table] 15 | iex> table = %ToyRobot.Table{north_boundary: 4, east_boundary: 4} 16 | %Table{north_boundary: 4, east_boundary: 4} 17 | iex> Simulation.place(table, %{north: 0, east: 0, facing: :north}) 18 | { 19 | :ok, 20 | %Simulation{ 21 | table: table, 22 | robot: %Robot{north: 0, east: 0, facing: :north} 23 | } 24 | } 25 | 26 | When the robot is placed in an invalid position: 27 | 28 | iex> alias ToyRobot.{Table, Simulation} 29 | [ToyRobot.Table, ToyRobot.Simulation] 30 | iex> table = %Table{north_boundary: 4, east_boundary: 4} 31 | %Table{north_boundary: 4, east_boundary: 4} 32 | iex> Simulation.place(table, %{north: 6, east: 0, facing: :north}) 33 | {:error, :invalid_placement} 34 | """ 35 | def place(table, placement) do 36 | if table |> Table.valid_position?(placement) do 37 | { 38 | :ok, 39 | %Simulation{ 40 | table: table, 41 | robot: struct(Robot, placement), 42 | } 43 | } 44 | else 45 | {:error, :invalid_placement} 46 | end 47 | end 48 | 49 | @doc """ 50 | Moves the robot forward one space in the direction that it is facing, unless that position is past the boundaries of the table. 51 | 52 | ## Examples 53 | 54 | ### A valid movement 55 | 56 | iex> alias ToyRobot.{Robot, Table, Simulation} 57 | [ToyRobot.Robot, ToyRobot.Table, ToyRobot.Simulation] 58 | iex> table = %Table{north_boundary: 4, east_boundary: 4} 59 | %Table{north_boundary: 4, east_boundary: 4} 60 | iex> simulation = %Simulation{ 61 | ...> table: table, 62 | ...> robot: %Robot{north: 0, east: 0, facing: :north} 63 | ...> } 64 | iex> simulation |> Simulation.move 65 | {:ok, %Simulation{ 66 | table: table, 67 | robot: %Robot{north: 1, east: 0, facing: :north} 68 | }} 69 | 70 | ### An invalid movement: 71 | 72 | iex> alias ToyRobot.{Robot, Table, Simulation} 73 | [ToyRobot.Robot, ToyRobot.Table, ToyRobot.Simulation] 74 | iex> table = %Table{north_boundary: 4, east_boundary: 4} 75 | %Table{north_boundary: 4, east_boundary: 4} 76 | iex> simulation = %Simulation{ 77 | ...> table: table, 78 | ...> robot: %Robot{north: 4, east: 0, facing: :north} 79 | ...> } 80 | iex> simulation |> Simulation.move 81 | {:error, :at_table_boundary} 82 | """ 83 | def move(%{robot: robot, table: table} = simulation) do 84 | moved_robot = robot |> Robot.move 85 | 86 | table 87 | |> Table.valid_position?(moved_robot) 88 | |> if do 89 | {:ok, %{simulation | robot: moved_robot}} 90 | else 91 | {:error, :at_table_boundary} 92 | end 93 | end 94 | 95 | @doc """ 96 | Turns the robot left. 97 | 98 | ## Examples 99 | 100 | iex> alias ToyRobot.{Robot, Table, Simulation} 101 | [ToyRobot.Robot, ToyRobot.Table, ToyRobot.Simulation] 102 | iex> table = %Table{north_boundary: 4, east_boundary: 4} 103 | %Table{north_boundary: 4, east_boundary: 4} 104 | iex> simulation = %Simulation{ 105 | ...> table: table, 106 | ...> robot: %Robot{north: 0, east: 0, facing: :north} 107 | ...> } 108 | iex> simulation |> Simulation.turn_left 109 | {:ok, %Simulation{ 110 | table: table, 111 | robot: %Robot{north: 0, east: 0, facing: :west} 112 | }} 113 | """ 114 | def turn_left(%{robot: robot} = simulation) do 115 | {:ok, %{simulation | robot: robot |> Robot.turn_left}} 116 | end 117 | 118 | @doc """ 119 | Turns the robot right. 120 | 121 | ## Examples 122 | 123 | iex> alias ToyRobot.{Robot, Table, Simulation} 124 | [ToyRobot.Robot, ToyRobot.Table, ToyRobot.Simulation] 125 | iex> table = %Table{north_boundary: 4, east_boundary: 4} 126 | %Table{north_boundary: 4, east_boundary: 4} 127 | iex> simulation = %Simulation{ 128 | ...> table: table, 129 | ...> robot: %Robot{north: 0, east: 0, facing: :north} 130 | ...> } 131 | iex> simulation |> Simulation.turn_right 132 | {:ok, %Simulation{ 133 | table: table, 134 | robot: %Robot{north: 0, east: 0, facing: :east} 135 | }} 136 | """ 137 | def turn_right(%{robot: robot} = simulation) do 138 | {:ok, %{simulation | robot: robot |> Robot.turn_right}} 139 | end 140 | 141 | @doc """ 142 | Returns the robot's current position. 143 | 144 | ## Examples 145 | 146 | iex> alias ToyRobot.{Robot, Table, Simulation} 147 | [ToyRobot.Robot, ToyRobot.Table, ToyRobot.Simulation] 148 | iex> table = %Table{north_boundary: 5, east_boundary: 5} 149 | %Table{north_boundary: 5, east_boundary: 5} 150 | iex> simulation = %Simulation{ 151 | ...> table: table, 152 | ...> robot: %Robot{north: 0, east: 0, facing: :north} 153 | ...> } 154 | iex> simulation |> Simulation.report 155 | %Robot{north: 0, east: 0, facing: :north} 156 | """ 157 | def report(%Simulation{robot: robot}), do: robot 158 | 159 | @doc """ 160 | Shows where the robot would move next. 161 | 162 | ## Examples 163 | 164 | iex> alias ToyRobot.{Robot, Table, Simulation} 165 | [ToyRobot.Robot, ToyRobot.Table, ToyRobot.Simulation] 166 | iex> table = %Table{north_boundary: 5, east_boundary: 5} 167 | %Table{north_boundary: 5, east_boundary: 5} 168 | iex> simulation = %Simulation{ 169 | ...> table: table, 170 | ...> robot: %Robot{north: 0, east: 0, facing: :north} 171 | ...> } 172 | iex> simulation |> Simulation.next_position 173 | %Robot{north: 1, east: 0, facing: :north} 174 | """ 175 | def next_position(%{robot: robot} = _simulation) do 176 | robot |> Robot.move 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /lib/toy_robot/table.ex: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.Table do 2 | defstruct [:north_boundary, :east_boundary] 3 | 4 | alias ToyRobot.Table 5 | 6 | @doc """ 7 | Determines if a position would be within the table's boundaries. 8 | 9 | ## Examples 10 | 11 | iex> alias ToyRobot.Table 12 | ToyRobot.Table 13 | iex> table = %Table{north_boundary: 4, east_boundary: 4} 14 | %Table{north_boundary: 4, east_boundary: 4} 15 | iex> table |> Table.valid_position?(%{north: 4, east: 4}) 16 | true 17 | iex> table |> Table.valid_position?(%{north: 0, east: 0}) 18 | true 19 | iex> table |> Table.valid_position?(%{north: 6, east: 0}) 20 | false 21 | iex> table |> Table.valid_position?(%{north: 6, east: 6}) 22 | false 23 | """ 24 | def valid_position?( 25 | %Table{north_boundary: north_boundary, east_boundary: east_boundary}, 26 | %{north: north, east: east} 27 | ) do 28 | north >= 0 && north <= north_boundary && 29 | east >= 0 && east <= east_boundary 30 | end 31 | 32 | @doc """ 33 | Returns all valid positions that are within a table's boundaries. 34 | 35 | ## Examples 36 | 37 | iex> alias ToyRobot.Table 38 | ToyRobot.Table 39 | iex> table = %Table{north_boundary: 1, east_boundary: 1} 40 | %Table{north_boundary: 1, east_boundary: 1} 41 | iex> table |> Table.valid_positions 42 | [ 43 | %{north: 0, east: 0}, 44 | %{north: 0, east: 1}, 45 | %{north: 1, east: 0}, 46 | %{north: 1, east: 1} 47 | ] 48 | """ 49 | def valid_positions( 50 | %Table{north_boundary: north_boundary, east_boundary: east_boundary} 51 | ) do 52 | for north <- 0..north_boundary, east <- 0..east_boundary do 53 | %{north: north, east: east} 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :toy_robot, 7 | version: "0.1.0", 8 | elixir: "~> 1.8", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | escript: escript(), 12 | ] 13 | end 14 | 15 | # Run "mix help compile.app" to learn about applications. 16 | def application do 17 | [ 18 | extra_applications: [:logger], 19 | mod: {ToyRobot.Application, []} 20 | ] 21 | end 22 | 23 | # Run "mix help deps" to learn about dependencies. 24 | defp deps do 25 | [ 26 | {:uuid, "~> 1.1.8"}, 27 | ] 28 | end 29 | 30 | defp escript do 31 | [main_module: ToyRobot.CLI] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/commands.txt: -------------------------------------------------------------------------------- 1 | PLACE 1,2,NORTH 2 | MOVE 3 | LEFT 4 | MOVE 5 | RIGHT 6 | MOVE 7 | REPORT 8 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/toy_robot/cli_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.CLITest do 2 | use ExUnit.Case, async: true 3 | 4 | import ExUnit.CaptureIO 5 | 6 | test "provides usage instructions if no arguments specified" do 7 | output = capture_io fn -> 8 | ToyRobot.CLI.main([]) 9 | end 10 | 11 | assert output |> String.trim == "Usage: toy_robot commands.txt" 12 | end 13 | 14 | test "provides usage instructions if too many arguments specified" do 15 | output = capture_io fn -> 16 | ToyRobot.CLI.main(["commands.txt", "commands2.txt"]) 17 | end 18 | 19 | assert output |> String.trim == "Usage: toy_robot commands.txt" 20 | end 21 | 22 | test "shows an error message if the file does not exist" do 23 | output = capture_io fn -> 24 | ToyRobot.CLI.main(["i-do-not-exist.txt"]) 25 | end 26 | 27 | assert output |> String.trim == "The file i-do-not-exist.txt does not exist" 28 | end 29 | 30 | test "handles commands and reports successfully" do 31 | commands_path = Path.expand("test/fixtures/commands.txt", File.cwd!) 32 | 33 | output = capture_io fn -> 34 | ToyRobot.CLI.main([commands_path]) 35 | end 36 | 37 | expected_output = """ 38 | The robot is at (0, 4) and is facing NORTH 39 | """ 40 | 41 | assert output |> String.trim == expected_output |> String.trim 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/toy_robot/command_interpreter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.CommandInterpreterTest do 2 | use ExUnit.Case, async: true 3 | doctest ToyRobot.CommandInterpreter 4 | 5 | alias ToyRobot.CommandInterpreter 6 | 7 | test "handles all commands" do 8 | commands = ["PLACE 1,2,NORTH", "MOVE", "LEFT", "RIGHT", "REPORT"] 9 | commands |> CommandInterpreter.interpret() 10 | end 11 | 12 | test "marks invalid commands as invalid" do 13 | commands = [ 14 | "SPIN", 15 | "TWIRL", 16 | "EXTERMINATE", 17 | "PLACE 1, 2, NORTH", 18 | "move", 19 | "MoVe", 20 | ] 21 | output = commands |> CommandInterpreter.interpret() 22 | assert output == [ 23 | {:invalid, "SPIN"}, 24 | {:invalid, "TWIRL"}, 25 | {:invalid, "EXTERMINATE"}, 26 | {:invalid, "PLACE 1, 2, NORTH"}, 27 | {:invalid, "move"}, 28 | {:invalid, "MoVe"} 29 | ] 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/toy_robot/command_runner_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Toyrobot.CommandRunnerTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias ToyRobot.{CommandRunner, Simulation} 5 | 6 | import ExUnit.CaptureIO 7 | 8 | test "handles a valid place command" do 9 | %Simulation{robot: robot} = CommandRunner.run([{:place, %{east: 1, north: 2, facing: :north}}]) 10 | 11 | assert robot.east == 1 12 | assert robot.north == 2 13 | assert robot.facing == :north 14 | end 15 | 16 | test "handles an invalid place command" do 17 | simulation = CommandRunner.run([{:place, %{east: 10, north: 10, facing: :north}}]) 18 | assert simulation == nil 19 | end 20 | 21 | test "ignores commands until a valid placement" do 22 | %Simulation{robot: robot} = [ 23 | :move, 24 | {:place, %{east: 1, north: 2, facing: :north}}, 25 | ] 26 | |> CommandRunner.run() 27 | 28 | assert robot.east == 1 29 | assert robot.north == 2 30 | assert robot.facing == :north 31 | end 32 | 33 | test "handles a place + move command" do 34 | %Simulation{robot: robot} = [ 35 | {:place, %{east: 1, north: 2, facing: :north}}, 36 | :move 37 | ] 38 | |> CommandRunner.run() 39 | 40 | assert robot.east == 1 41 | assert robot.north == 3 42 | assert robot.facing == :north 43 | end 44 | 45 | test "handles a place + invalid move command" do 46 | %Simulation{robot: robot} = [ 47 | {:place, %{east: 1, north: 4, facing: :north}}, 48 | :move 49 | ] 50 | |> CommandRunner.run() 51 | 52 | assert robot.east == 1 53 | assert robot.north == 4 54 | assert robot.facing == :north 55 | end 56 | 57 | test "handles a place + turn_left command" do 58 | %Simulation{robot: robot} = [ 59 | {:place, %{east: 1, north: 2, facing: :north}}, 60 | :turn_left 61 | ] 62 | |> CommandRunner.run() 63 | 64 | assert robot.east == 1 65 | assert robot.north == 2 66 | assert robot.facing == :west 67 | end 68 | 69 | test "handles a place + turn_right command" do 70 | %Simulation{robot: robot} = [ 71 | {:place, %{east: 1, north: 2, facing: :north}}, 72 | :turn_right 73 | ] 74 | |> CommandRunner.run() 75 | 76 | assert robot.east == 1 77 | assert robot.north == 2 78 | assert robot.facing == :east 79 | end 80 | 81 | test "handles a place + report command" do 82 | commands = [ 83 | {:place, %{east: 1, north: 2, facing: :north}}, 84 | :report 85 | ] 86 | 87 | output = capture_io fn -> 88 | CommandRunner.run(commands) 89 | end 90 | 91 | assert output |> String.trim == "The robot is at (1, 2) and is facing NORTH" 92 | end 93 | 94 | test "handles a place + invalid command" do 95 | %Simulation{robot: robot} = [ 96 | {:place, %{east: 1, north: 2, facing: :north}}, 97 | {:invalid, "EXTERMINATE"} 98 | ] 99 | |> CommandRunner.run() 100 | 101 | assert robot.east == 1 102 | assert robot.north == 2 103 | assert robot.facing == :north 104 | end 105 | 106 | test "robot cannot move past the north boundary" do 107 | %Simulation{robot: robot} = [ 108 | {:place, %{east: 0, north: 4, facing: :north}}, 109 | :move 110 | ] 111 | |> CommandRunner.run() 112 | 113 | assert robot.north == 4 114 | end 115 | 116 | test "robot cannot move past the east boundary" do 117 | %Simulation{robot: robot} = [ 118 | {:place, %{east: 4, north: 0, facing: :east}}, 119 | :move 120 | ] 121 | |> CommandRunner.run() 122 | 123 | assert robot.east == 4 124 | end 125 | 126 | test "robot cannot move past the south boundary" do 127 | %Simulation{robot: robot} = [ 128 | {:place, %{east: 0, north: 0, facing: :south}}, 129 | :move 130 | ] 131 | |> CommandRunner.run() 132 | 133 | assert robot.north == 0 134 | end 135 | 136 | test "robot cannot move past the west boundary" do 137 | %Simulation{robot: robot} = [ 138 | {:place, %{east: 0, north: 0, facing: :west}}, 139 | :move 140 | ] 141 | |> CommandRunner.run() 142 | 143 | assert robot.east == 0 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /test/toy_robot/game/game_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.GameTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias ToyRobot.Game 5 | 6 | setup do 7 | {:ok, game} = Game.start(north_boundary: 4, east_boundary: 4) 8 | {:ok, %{game: game}} 9 | end 10 | 11 | test "can place a robot", %{game: game} do 12 | :ok = Game.place(game, %{north: 0, east: 0, facing: :north}, "Rosie") 13 | assert Game.player_count(game) == 1 14 | end 15 | 16 | test "can not place a robot out of bounds", %{game: game} do 17 | assert Game.place( 18 | game, %{north: 10, east: 10, facing: :north}, 19 | "Eve" 20 | ) == {:error, :out_of_bounds} 21 | end 22 | 23 | test "can not place a robot in the same space as another robot", %{game: game} do 24 | :ok = Game.place( 25 | game, 26 | %{north: 0, east: 0, facing: :north}, 27 | "Wall-E" 28 | ) 29 | assert Game.place( 30 | game, 31 | %{north: 0, east: 0, facing: :north}, 32 | "Robby" 33 | ) == {:error, :occupied} 34 | end 35 | 36 | describe "move" do 37 | test "cannot move a robot onto another robot's square", %{game: game} do 38 | :ok = Game.place( 39 | game, 40 | %{north: 0, east: 0, facing: :north}, 41 | "Marvin" 42 | ) 43 | 44 | :ok = Game.place( 45 | game, 46 | %{north: 1, east: 0, facing: :south}, 47 | "Chappie" 48 | ) 49 | 50 | assert Game.move(game, "Chappie") == {:error, :occupied} 51 | end 52 | 53 | test "can move onto an unoccupied square", %{game: game} do 54 | :ok = Game.place( 55 | game, 56 | %{north: 0, east: 0, facing: :east}, 57 | "Mr. Roboto" 58 | ) 59 | 60 | :ok = Game.place( 61 | game, 62 | %{north: 1, east: 0, facing: :south}, 63 | "Kit" 64 | ) 65 | 66 | assert Game.move(game, "Mr. Roboto") == :ok 67 | end 68 | end 69 | 70 | describe "respawning" do 71 | test "davros does not respawn on (1, 1)", %{game: game} do 72 | izzy_origin = %{east: 1, north: 0, facing: :north} 73 | :ok = Game.place(game, izzy_origin, "Izzy") 74 | 75 | davros_origin = %{east: 1, north: 1, facing: :west} 76 | :ok = Game.place(game, davros_origin, "Davros") 77 | :ok = Game.move(game, "Davros") 78 | :ok = Game.move(game, "Izzy") 79 | :ok = Game.move(game, "Davros") 80 | :timer.sleep(100) 81 | 82 | refute match?(%{north: 1, east: 1}, Game.report(game, "Davros")) 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/toy_robot/game/player_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.Game.PlayerTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias ToyRobot.Game.Player 5 | alias ToyRobot.{Robot, Table} 6 | 7 | def build_table do 8 | %Table{ 9 | north_boundary: 4, 10 | east_boundary: 4 11 | } 12 | end 13 | 14 | describe "init" do 15 | setup do 16 | registry_id = :player_init_test 17 | Registry.start_link(keys: :unique, name: registry_id) 18 | 19 | {:ok, registry_id: registry_id} 20 | end 21 | 22 | test "maintains the original position", %{registry_id: registry_id} do 23 | position = %{north: 0, east: 0, facing: :north} 24 | 25 | {:ok, %{robot: robot}} = 26 | Player.init([ 27 | registry_id: registry_id, 28 | table: build_table(), 29 | position: position, 30 | name: Player.process_name(registry_id, "Joanne") 31 | ]) 32 | 33 | assert robot.north == 0 34 | assert robot.east == 0 35 | assert robot.facing == :north 36 | end 37 | end 38 | 39 | describe "init, with another player registered" do 40 | setup do 41 | registry_id = :player_init_test 42 | Registry.start_link(keys: :unique, name: registry_id) 43 | 44 | table = build_table() 45 | 46 | Player.start_link( 47 | registry_id: registry_id, 48 | table: table, 49 | position: %{north: 0, east: 0, facing: :west}, 50 | name: "Joanna" 51 | ) 52 | 53 | {:ok, registry_id: registry_id, table: table} 54 | end 55 | 56 | test "picks a random position on the board", %{registry_id: registry_id, table: table} do 57 | position = %{north: 0, east: 0, facing: :north} 58 | 59 | {:ok, %{robot: robot}} = 60 | Player.init([ 61 | registry_id: registry_id, 62 | table: table, 63 | position: position, 64 | name: Player.process_name(registry_id, "Bobbie") 65 | ]) 66 | 67 | refute match?(%{north: 0, east: 0}, robot) 68 | assert robot.facing == :north 69 | end 70 | end 71 | 72 | 73 | describe "report" do 74 | setup do 75 | starting_position = %{north: 0, east: 0, facing: :north} 76 | {:ok, player} = Player.start(build_table(), starting_position) 77 | %{player: player} 78 | end 79 | 80 | test "shows the current position of the robot", %{player: player} do 81 | assert Player.report(player) == %Robot{ 82 | north: 0, 83 | east: 0, 84 | facing: :north 85 | } 86 | end 87 | end 88 | 89 | describe "move" do 90 | setup do 91 | starting_position = %{north: 0, east: 0, facing: :north} 92 | {:ok, player} = Player.start(build_table(), starting_position) 93 | %{player: player} 94 | end 95 | 96 | test "moves the robot forward one space", %{player: player} do 97 | :ok = Player.move(player) 98 | robot = Player.report(player) 99 | 100 | assert robot == %Robot{ 101 | north: 1, 102 | east: 0, 103 | facing: :north 104 | } 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /test/toy_robot/game/players_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.Game.PlayersTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias ToyRobot.Table 5 | alias ToyRobot.Game.Players 6 | 7 | describe "available positions" do 8 | setup do 9 | table = %Table{ 10 | north_boundary: 1, 11 | east_boundary: 1, 12 | } 13 | 14 | {:ok, table: table} 15 | end 16 | 17 | test "does not include the occupied positions", %{table: table} do 18 | occupied_positions = [%{north: 0, east: 0}] 19 | 20 | available_positions = Players.available_positions( 21 | occupied_positions, 22 | table 23 | ) 24 | 25 | assert occupied_positions not in available_positions 26 | end 27 | end 28 | 29 | describe "change_position_if_occupied" do 30 | setup do 31 | table = %Table{ 32 | north_boundary: 1, 33 | east_boundary: 1, 34 | } 35 | 36 | {:ok, table: table} 37 | end 38 | 39 | test "changes position if it is occupied", %{table: table} do 40 | occupied_positions = [%{north: 0, east: 0}] 41 | original_position = %{north: 0, east: 0, facing: :north} 42 | 43 | new_position = Players.change_position_if_occupied( 44 | occupied_positions, 45 | table, 46 | original_position 47 | ) 48 | 49 | assert new_position != original_position 50 | assert new_position.facing == original_position.facing 51 | end 52 | 53 | test "does not change position if it is not occupied", %{table: table} do 54 | occupied_positions = [] 55 | original_position = %{north: 0, east: 0, facing: :north} 56 | 57 | assert Players.change_position_if_occupied( 58 | occupied_positions, 59 | table, 60 | original_position 61 | ) == original_position 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/toy_robot/robot_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.RobotTest do 2 | use ExUnit.Case 3 | doctest ToyRobot.Robot 4 | alias ToyRobot.Robot 5 | 6 | describe "when the robot is facing north" do 7 | setup do 8 | {:ok, %{robot: %Robot{north: 0, facing: :north}}} 9 | end 10 | 11 | test "it moves one space north", %{robot: robot} do 12 | robot = robot |> Robot.move 13 | assert robot.north == 1 14 | end 15 | 16 | test "turns left to face west", %{robot: robot} do 17 | robot = robot |> Robot.turn_left 18 | assert robot.facing == :west 19 | end 20 | 21 | test "turns right to face east", %{robot: robot} do 22 | robot = robot |> Robot.turn_right 23 | assert robot.facing == :east 24 | end 25 | end 26 | 27 | describe "when the robot is facing north, and it has moved forward a space" do 28 | setup do 29 | {:ok, %{robot: %Robot{north: 1, facing: :north}}} 30 | end 31 | 32 | test "turns right to face east", %{robot: robot} do 33 | robot = robot |> Robot.turn_right() 34 | assert robot.facing == :east 35 | assert robot.north == 1 36 | end 37 | 38 | test "turns left to face west", %{robot: robot} do 39 | robot = robot |> Robot.turn_left() 40 | assert robot.facing == :west 41 | assert robot.north == 1 42 | end 43 | end 44 | 45 | describe "when the robot is facing north, and it has moved east once" do 46 | setup do 47 | {:ok, %{robot: %Robot{east: 1, facing: :north}}} 48 | end 49 | 50 | test "moves north one space", %{robot: robot} do 51 | robot = robot |> Robot.move() 52 | assert robot.north == 1 53 | assert robot.east == 1 54 | assert robot.facing == :north 55 | end 56 | end 57 | 58 | describe "when the robot is facing east" do 59 | setup do 60 | {:ok, %{robot: %Robot{east: 0, facing: :east}}} 61 | end 62 | 63 | test "it moves one space east", %{robot: robot} do 64 | robot = robot |> Robot.move 65 | assert robot.east == 1 66 | end 67 | 68 | test "turns left to face north", %{robot: robot} do 69 | robot = robot |> Robot.turn_left 70 | assert robot.facing == :north 71 | end 72 | 73 | test "turns right to face south", %{robot: robot} do 74 | robot = robot |> Robot.turn_right 75 | assert robot.facing == :south 76 | end 77 | end 78 | 79 | describe "when the robot is facing east, and it has moved north once" do 80 | setup do 81 | {:ok, %{robot: %Robot{north: 1, facing: :east}}} 82 | end 83 | 84 | test "moves east one space", %{robot: robot} do 85 | robot = robot |> Robot.move() 86 | assert robot.north == 1 87 | assert robot.east == 1 88 | assert robot.facing == :east 89 | end 90 | end 91 | 92 | describe "when the robot is facing south" do 93 | setup do 94 | {:ok, %{robot: %Robot{north: 0, facing: :south}}} 95 | end 96 | 97 | test "it moves one space south", %{robot: robot} do 98 | robot = robot |> Robot.move 99 | assert robot.north == -1 100 | end 101 | 102 | test "turns left to face east", %{robot: robot} do 103 | robot = robot |> Robot.turn_left 104 | assert robot.facing == :east 105 | end 106 | 107 | test "turns right to face west", %{robot: robot} do 108 | robot = robot |> Robot.turn_right 109 | assert robot.facing == :west 110 | end 111 | end 112 | 113 | describe "when the robot is facing south, and it has moved east once" do 114 | setup do 115 | {:ok, %{robot: %Robot{east: 1, facing: :south}}} 116 | end 117 | 118 | test "moves south one space", %{robot: robot} do 119 | robot = robot |> Robot.move() 120 | assert robot.north == -1 121 | assert robot.east == 1 122 | assert robot.facing == :south 123 | end 124 | end 125 | 126 | describe "when the robot is facing west" do 127 | setup do 128 | {:ok, %{robot: %Robot{east: 0, facing: :west}}} 129 | end 130 | 131 | test "it moves one space west", %{robot: robot} do 132 | robot = robot |> Robot.move 133 | assert robot.east == -1 134 | end 135 | 136 | test "turns left to face south", %{robot: robot} do 137 | robot = robot |> Robot.turn_left 138 | assert robot.facing == :south 139 | end 140 | 141 | test "turns right to face north", %{robot: robot} do 142 | robot = robot |> Robot.turn_right 143 | assert robot.facing == :north 144 | end 145 | end 146 | 147 | describe "when the robot is facing west, and it has moved north once" do 148 | setup do 149 | {:ok, %{robot: %Robot{north: 1, facing: :west}}} 150 | end 151 | 152 | test "moves west one space", %{robot: robot} do 153 | robot = robot |> Robot.move() 154 | assert robot.north == 1 155 | assert robot.east == -1 156 | assert robot.facing == :west 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /test/toy_robot/simulation_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.SimulationTest do 2 | use ExUnit.Case 3 | 4 | doctest ToyRobot.Simulation 5 | end 6 | -------------------------------------------------------------------------------- /test/toy_robot/table_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ToyRobot.TableTest do 2 | use ExUnit.Case 3 | doctest ToyRobot.Table 4 | end 5 | -------------------------------------------------------------------------------- /toy_robot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radar/toy_robot_elixir/cd3ef72eabb338042994605c2dd2359d5fd5e439/toy_robot --------------------------------------------------------------------------------