Avatar of paulfioravanti

paulfioravanti's solution

to Robot Simulator in the Elm Track

Published at Jul 29 2019 · 0 comments
Instructions
Test suite
Solution

Write a robot simulator.

A robot factory's test facility needs a program to verify robot movements.

The robots have three possible movements:

  • turn right
  • turn left
  • advance

Robots are placed on a hypothetical infinite grid, facing a particular direction (north, east, south, or west) at a set of {x,y} coordinates, e.g., {3,8}, with coordinates increasing to the north and east.

The robot then receives a number of instructions, at which point the testing facility verifies the robot's new position, and in which direction it is pointing.

  • The letter-string "RAALAL" means:
    • Turn right
    • Advance twice
    • Turn left
    • Advance once
    • Turn left yet again
  • Say a robot starts at {7, 3} facing north. Then running this stream of instructions should leave it at {9, 4} facing west.

Elm Installation

Refer to the Installing Elm page for information about installing elm.

Writing the Code

The first time you start an exercise, you'll need to ensure you have the appropriate dependencies installed. Thankfully, Elm makes that easy for you and will install dependencies when you try to run tests or build the code.

Execute the tests with:

$ elm-test

Automatically run tests again when you save changes:

$ elm-test --watch

As you work your way through the test suite, be sure to remove the skip <| calls from each test until you get them all passing!

Source

Inspired by an interview question at a famous company.

Submitting Incomplete Solutions

It is possible to submit an incomplete solution so you can see how others have completed the exercise.

Tests.elm

module Tests exposing (assertionList, tests)

import Expect
import RobotSimulator exposing (Bearing(..), Robot, advance, defaultRobot, simulate, turnLeft, turnRight)
import Test exposing (..)


tests : Test
tests =
    describe "RobotSimulator"
        [ describe "init"
            (let
                robot =
                    defaultRobot
             in
             [ test "coordinates" <|
                \() -> Expect.equal { x = 0, y = 0 } robot.coordinates
             , skip <|
                test "bearing" <|
                    \() -> Expect.equal North robot.bearing
             ]
            )
        , describe "setup"
            (let
                robot =
                    Robot South { x = -1, y = 1 }
             in
             [ skip <|
                test "coordinates" <|
                    \() -> Expect.equal { x = -1, y = 1 } robot.coordinates
             , skip <|
                test "bearing" <|
                    \() -> Expect.equal South robot.bearing
             ]
            )
        , skip <|
            describe "turn right"
                (List.range 1 3
                    |> scanl (\_ r -> turnRight r) defaultRobot
                    |> List.map .bearing
                    |> assertionList [ North, East, South, West ]
                    |> List.indexedMap (\i e -> test ("step " ++ String.fromInt i) (\() -> e))
                )
        , skip <|
            describe
                "turn left"
                (List.range 1 3
                    |> scanl (\_ r -> turnLeft r) defaultRobot
                    |> List.map .bearing
                    |> assertionList [ North, West, South, East ]
                    |> List.indexedMap (\i e -> test ("step " ++ String.fromInt i) (\() -> e))
                )
        , describe "advance positive north"
            (let
                robot =
                    Robot North { x = 0, y = 0 }
                        |> advance
             in
             [ skip <|
                test "coordinates" <|
                    \() -> Expect.equal { x = 0, y = 1 } robot.coordinates
             , skip <|
                test "bearing" <|
                    \() -> Expect.equal North robot.bearing
             ]
            )
        , describe "advance positive east"
            (let
                robot =
                    Robot East { x = 0, y = 0 }
                        |> advance
             in
             [ skip <|
                test "coordinates" <|
                    \() -> Expect.equal { x = 1, y = 0 } robot.coordinates
             , skip <|
                test "bearing" <|
                    \() -> Expect.equal East robot.bearing
             ]
            )
        , describe "advance negative south"
            (let
                robot =
                    Robot South { x = 0, y = 0 }
                        |> advance
             in
             [ skip <|
                test "coordinates" <|
                    \() -> Expect.equal { x = 0, y = -1 } robot.coordinates
             , skip <|
                test "bearing" <|
                    \() -> Expect.equal South robot.bearing
             ]
            )
        , describe "advance positive west"
            (let
                robot =
                    Robot West { x = 0, y = 0 }
                        |> advance
             in
             [ skip <|
                test "coordinates" <|
                    \() -> Expect.equal { x = -1, y = 0 } robot.coordinates
             , skip <|
                test "bearing" <|
                    \() -> Expect.equal West robot.bearing
             ]
            )
        , describe "simulate prog 1"
            (let
                robot =
                    Robot North { x = 0, y = 0 }
                        |> simulate "LAAARALA"
             in
             [ skip <|
                test "coordinates" <|
                    \() -> Expect.equal { x = -4, y = 1 } robot.coordinates
             , skip <|
                test "bearing" <|
                    \() -> Expect.equal West robot.bearing
             ]
            )
        , describe "simulate prog 2"
            (let
                robot =
                    Robot East { x = 2, y = -7 }
                        |> simulate "RRAAAAALA"
             in
             [ skip <|
                test "coordinates" <|
                    \() -> Expect.equal { x = -3, y = -8 } robot.coordinates
             , test "bearing" <|
                \() -> Expect.equal South robot.bearing
             ]
            )
        , describe "simulate prog 3"
            (let
                robot =
                    Robot South { x = 8, y = 4 }
                        |> simulate "LAAARRRALLLL"
             in
             [ skip <|
                test "coordinates" <|
                    \() -> Expect.equal { x = 11, y = 5 } robot.coordinates
             , skip <|
                test "bearing" <|
                    \() -> Expect.equal North robot.bearing
             ]
            )
        ]


{-| Given a list of values and another list of expected values,
generate a list of Assert Equal assertions.
-}
assertionList : List a -> List a -> List Expect.Expectation
assertionList xs ys =
    List.map2 Expect.equal xs ys


{-| Taken from 0.18's List.scanl for easier upgrade to 0.19
<https://github.com/elm-lang/core/blob/5.1.1/src/List.elm#L171>
-}
scanl : (a -> b -> b) -> b -> List a -> List b
scanl f b xs =
    let
        scan1 x accAcc =
            case accAcc of
                acc :: _ ->
                    f x acc :: accAcc

                [] ->
                    []

        -- impossible
    in
    List.reverse (List.foldl scan1 [ b ] xs)
module RobotSimulator exposing
    ( Bearing(..)
    , Robot
    , advance
    , defaultRobot
    , simulate
    , turnLeft
    , turnRight
    )


type Bearing
    = North
    | East
    | South
    | West


type alias Robot =
    { bearing : Bearing
    , coordinates :
        { x : Int
        , y : Int
        }
    }


defaultRobot : Robot
defaultRobot =
    { bearing = North
    , coordinates = { x = 0, y = 0 }
    }


turnRight : Robot -> Robot
turnRight robot =
    bearingsList
        |> rotate 1
        |> turn robot


turnLeft : Robot -> Robot
turnLeft robot =
    bearingsList
        |> rotate -1
        |> turn robot


advance : Robot -> Robot
advance robot =
    let
        { x, y } =
            robot.coordinates

        newCoordinates =
            case robot.bearing of
                North ->
                    { x = x, y = y + 1 }

                East ->
                    { x = x + 1, y = y }

                South ->
                    { x = x, y = y - 1 }

                West ->
                    { x = x - 1, y = y }
    in
    { robot | coordinates = newCoordinates }


simulate : String -> Robot -> Robot
simulate directions robot =
    directions
        |> String.toList
        |> List.foldl performDirection robot



-- PRIVATE


bearingsList : List Bearing
bearingsList =
    [ North, East, South, West ]


rotate : Int -> List Bearing -> List Bearing
rotate count bearings =
    case ( compare count 0, bearings ) of
        ( GT, [] ) ->
            []

        ( GT, head :: tail ) ->
            rotate (count - 1) (tail ++ [ head ])

        ( LT, _ ) ->
            bearings
                |> List.reverse
                |> rotate (abs count)
                |> List.reverse

        ( EQ, _ ) ->
            bearings


turn : Robot -> List Bearing -> Robot
turn robot newBearings =
    let
        index =
            bearingsList
                |> List.indexedMap Tuple.pair
                |> List.filter (\( _, bearing ) -> bearing == robot.bearing)
                |> List.head
                |> Maybe.withDefault ( 0, robot.bearing )
                |> Tuple.first

        newBearing =
            newBearings
                |> List.drop index
                |> List.head
                |> Maybe.withDefault robot.bearing
    in
    { robot | bearing = newBearing }


performDirection : Char -> Robot -> Robot
performDirection direction robot =
    case direction of
        'L' ->
            turnLeft robot

        'R' ->
            turnRight robot

        'A' ->
            advance robot

        _ ->
            robot

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?