exercism

Exercism - Robot Simulator

This post shows you how to get Robot Simulator exercise of Exercism.

Stevinator Stevinator
9 min read
SHARE
exercism dart flutter robot-simulator

Preparation

Before we click on our next exercise, let’s see what concepts of DART we need to consider

Robot Simulator Exercise

So we need to use the following concepts.

Classes and Objects

Classes define blueprints for creating objects. Objects are instances of classes that contain data (fields) and behavior (methods).

class Robot {
  Position position;
  Orientation orientation;
  
  Robot(this.position, this.orientation);
}

void main() {
  // Create a robot
  Position startPos = Position(7, 3);
  Robot robot = Robot(startPos, Orientation.north);
  
  print(robot.position.x); // 7
  print(robot.orientation); // Orientation.north
}

Enums

Enums define a set of named constants. They’re perfect for representing a fixed set of values like directions.

enum Orientation {
  north,
  east,
  south,
  west,
}

void main() {
  Orientation dir = Orientation.north;
  print(dir); // Orientation.north
  
  // Use in switch statements
  switch (dir) {
    case Orientation.north:
      print("Facing north");
      break;
    case Orientation.east:
      print("Facing east");
      break;
    // ...
  }
}

Records (Tuples)

Records allow you to group multiple values together. They’re useful for representing coordinate pairs or direction vectors.

void main() {
  // Record syntax: (x, y)
  var point = (0, 1);
  print(point); // (0, 1)
  
  // Destructure records
  var (x, y) = point;
  print(x); // 0
  print(y); // 1
  
  // Use in maps
  Map<Orientation, (int, int)> vectors = {
    Orientation.north: (0, 1),
    Orientation.east: (1, 0),
  };
  
  // Access record values
  var (dx, dy) = vectors[Orientation.north]!;
  print(dx); // 0
  print(dy); // 1
}

Static Const Maps

Static const maps are class-level constants that can be accessed without creating an instance. They’re perfect for lookup tables.

class Robot {
  // Static const map - shared by all instances
  static const _directionVectors = {
    Orientation.north: (0, 1),
    Orientation.east: (1, 0),
    Orientation.south: (0, -1),
    Orientation.west: (-1, 0),
  };
  
  // Access without instance
  void move() {
    var vector = _directionVectors[Orientation.north];
    // Use vector...
  }
}

void main() {
  // Can access static members without instance
  // (though private, so only within class)
}

For Loops

For loops allow you to iterate through a collection. They’re essential for processing each instruction character.

void main() {
  String instructions = "RAALAL";
  
  // Iterate through each character
  for (var instruction in instructions.split('')) {
    print(instruction); // R, A, A, L, A, L
  }
  
  // Process each instruction
  for (var instruction in instructions.split('')) {
    switch (instruction) {
      case 'R':
        print("Turn right");
        break;
      case 'L':
        print("Turn left");
        break;
      case 'A':
        print("Advance");
        break;
    }
  }
}

Switch Statements

Switch statements allow you to check a value against multiple cases and execute different code for each case. They’re perfect for handling different instruction types.

void main() {
  String instruction = 'R';
  
  switch (instruction) {
    case 'R':
      print("Turn right");
      break;
    case 'L':
      print("Turn left");
      break;
    case 'A':
      print("Advance");
      break;
    default:
      print("Unknown instruction");
  }
}

String Split Method

The split() method divides a string into a list of substrings. When called with an empty string '', it splits the string into individual characters.

void main() {
  String instructions = "RAALAL";
  
  // Split into individual characters
  List<String> chars = instructions.split('');
  print(chars); // [R, A, A, L, A, L]
  
  // Iterate through characters
  for (var char in instructions.split('')) {
    print(char); // R, A, A, L, A, L
  }
}

Null Assertion Operator

The null assertion operator (!) tells Dart that you’re certain a value is not null. Use it when you know a map lookup will succeed.

void main() {
  Map<Orientation, (int, int)> vectors = {
    Orientation.north: (0, 1),
  };
  
  // Without null assertion (nullable)
  var vector1 = vectors[Orientation.north]; // (int, int)?
  
  // With null assertion (non-nullable)
  var vector2 = vectors[Orientation.north]!; // (int, int)
  
  // Use when you're certain the key exists
  var (dx, dy) = vectors[Orientation.north]!;
  print(dx); // 0
  print(dy); // 1
}

Map Lookup

Maps allow you to look up values by key. They’re perfect for translating orientations to direction vectors or turn directions.

void main() {
  // Map orientation to direction vector
  Map<Orientation, (int, int)> vectors = {
    Orientation.north: (0, 1),
    Orientation.east: (1, 0),
    Orientation.south: (0, -1),
    Orientation.west: (-1, 0),
  };
  
  // Lookup direction vector
  var vector = vectors[Orientation.north]!;
  var (dx, dy) = vector;
  print(dx); // 0
  print(dy); // 1
  
  // Map current orientation to next orientation after right turn
  Map<Orientation, Orientation> rightTurns = {
    Orientation.north: Orientation.east,
    Orientation.east: Orientation.south,
    Orientation.south: Orientation.west,
    Orientation.west: Orientation.north,
  };
  
  Orientation current = Orientation.north;
  Orientation next = rightTurns[current]!;
  print(next); // Orientation.east
}

Introduction

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.

Example

The letter-string “RAALAL” means:

  • R - Turn right
  • A - Advance twice
  • A - Advance
  • L - Turn left
  • A - Advance once
  • L - 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.

How does the robot move?

To simulate robot movement:

  1. Initialize: Create a robot with a starting position and orientation
  2. Process instructions: For each character in the instruction string:
    • ‘R’: Turn right (north → east → south → west → north)
    • ‘L’: Turn left (north → west → south → east → north)
    • ‘A’: Advance in the current direction
  3. Update position: When advancing, add the direction vector to the current position
  4. Update orientation: When turning, look up the new orientation from a turn map

The key insight is using maps to translate:

  • Orientation → Direction Vector: Maps each direction to (dx, dy) movement
  • Orientation → Next Orientation (Right Turn): Maps current direction to direction after right turn
  • Orientation → Next Orientation (Left Turn): Maps current direction to direction after left turn

For example, with starting position {7, 3} facing north and instructions “RAALAL”:

  • Start: {7, 3}, facing north
  • R: Turn right → facing east
  • A: Advance east → {8, 3}
  • A: Advance east → {9, 3}
  • L: Turn left → facing north
  • A: Advance north → {9, 4}
  • L: Turn left → facing west
  • Final: {9, 4}, facing west

Solution

import 'orientation.dart';
import 'position.dart';

class Robot {
  Position position;
  Orientation orientation;

  Robot(this.position, this.orientation);

  static const _directionVectors = {
    Orientation.north: (0, 1),
    Orientation.east: (1, 0),
    Orientation.south: (0, -1),
    Orientation.west: (-1, 0),
  };

  static const _rightTurns = {
    Orientation.north: Orientation.east,
    Orientation.east: Orientation.south,
    Orientation.south: Orientation.west,
    Orientation.west: Orientation.north,
  };

  static const _leftTurns = {
    Orientation.north: Orientation.west,
    Orientation.west: Orientation.south,
    Orientation.south: Orientation.east,
    Orientation.east: Orientation.north,
  };

  void move(String instructions) {
    for (var instruction in instructions.split('')) {
      switch (instruction) {
        case 'R':
          orientation = _rightTurns[orientation]!;
          break;
        case 'L':
          orientation = _leftTurns[orientation]!;
          break;
        case 'A':
          final (dx, dy) = _directionVectors[orientation]!;
          position = Position(position.x + dx, position.y + dy);
          break;
      }
    }
  }
}

Let’s break down the solution:

  1. class Robot - Robot class with position and orientation:

    • Position position: Current (x, y) coordinates
    • Orientation orientation: Current facing direction (north, east, south, west)
    • Robot(this.position, this.orientation): Constructor that initializes position and orientation
  2. static const _directionVectors - Maps orientation to movement vector:

    • Orientation.north: (0, 1): Moving north increases y by 1
    • Orientation.east: (1, 0): Moving east increases x by 1
    • Orientation.south: (0, -1): Moving south decreases y by 1
    • Orientation.west: (-1, 0): Moving west decreases x by 1
    • Static const means it’s shared by all instances and can’t be modified
  3. static const _rightTurns - Maps current orientation to orientation after right turn:

    • Orientation.north → Orientation.east: North → East
    • Orientation.east → Orientation.south: East → South
    • Orientation.south → Orientation.west: South → West
    • Orientation.west → Orientation.north: West → North
    • Forms a clockwise cycle
  4. static const _leftTurns - Maps current orientation to orientation after left turn:

    • Orientation.north → Orientation.west: North → West
    • Orientation.west → Orientation.south: West → South
    • Orientation.south → Orientation.east: South → East
    • Orientation.east → Orientation.north: East → North
    • Forms a counter-clockwise cycle
  5. void move(String instructions) - Processes instruction string:

    • Takes a string of instructions (e.g., “RAALAL”)
    • Iterates through each character using for (var instruction in instructions.split(''))
    • Uses switch statement to handle each instruction type
  6. case 'R' - Turn right:

    • orientation = _rightTurns[orientation]!: Looks up new orientation after right turn
    • Uses null assertion operator (!) because we know the key exists
    • Updates the robot’s orientation
  7. case 'L' - Turn left:

    • orientation = _leftTurns[orientation]!: Looks up new orientation after left turn
    • Updates the robot’s orientation
  8. case 'A' - Advance:

    • final (dx, dy) = _directionVectors[orientation]!: Gets direction vector for current orientation
    • Destructures the record into dx and dy components
    • position = Position(position.x + dx, position.y + dy): Creates new position by adding vector to current position
    • Updates the robot’s position

The solution efficiently simulates robot movement by using lookup maps for direction vectors and turn directions, making the code clean and maintainable.


A video tutorial for this exercise is coming soon! In the meantime, check out my YouTube channel for more Dart and Flutter tutorials. 😉

Visit My YouTube Channel
Stevinator

Stevinator

Stevinator is a software engineer passionate about clean code and best practices. Loves sharing knowledge with the developer community.