PracHub
QuestionsPremiumCoachesLearningGuidesInterview Prep
|Home/Coding & Algorithms/Shopify

Design a robot movement command system

Last updated: May 13, 2026

Quick Overview

This question evaluates design and implementation skills for an extensible, command-driven robot movement system, covering state representation on a 2D integer grid, directional updates, command dispatching, input/output semantics, and explicit handling policies for invalid commands.

  • easy
  • Shopify
  • Coding & Algorithms
  • Machine Learning Engineer

Design a robot movement command system

Company: Shopify

Role: Machine Learning Engineer

Category: Coding & Algorithms

Difficulty: easy

Interview Round: Technical Screen

## Robot Movement (Pair Programming) You are given an empty starter repository (only a README). Implement a small, testable robot movement module that can: - Represent a robot on a 2D grid. - Receive a sequence of user commands. - Execute those commands to update the robot’s position and direction. - Be easily extensible as new valid commands are added over time. ### Requirements 1. **Robot state** - The robot has a position `(x, y)` on an integer grid. - The robot has a facing direction: one of `{N, E, S, W}`. 2. **Commands (initial set)** Support at least these commands: - `L`: rotate 90° left (N→W→S→E→N) - `R`: rotate 90° right (N→E→S→W→N) - `F`: move forward by 1 step in the direction it is currently facing 3. **Input / Output** - Input: initial state `(x0, y0, dir0)` and a command string like `"FFLFFR"` (or an equivalent list/array of commands). - Output: final state `(x, y, dir)` after executing all commands. 4. **Invalid command handling** - Define and implement a clear policy for unknown commands (e.g., throw an error, ignore, or collect errors). State your choice. 5. **Extensibility constraint (core design requirement)** - Assume **valid commands will keep expanding** (e.g., `B` for backward, `J` for jump, `U` for undo, etc.). - Design the command system so adding a new command does **not** require rewriting large parts of the robot execution logic. - Discuss/implement a command abstraction (e.g., command objects, a registry/dispatcher, etc.). 6. **Testing** - Write small, incremental tests as you implement. - After finishing, explain: - What else you would optimize. - How you would do **systematic testing** (unit tests, property-based tests, edge cases). ### Example - Initial: `(0, 0, N)` - Commands: `"FFRFF"` - Expected final: `(2, 2, E)` ### Constraints (you may assume) - Command string length: up to ~10^5. - Grid is unbounded (no walls/obstacles) unless you explicitly choose to add bounds as an extension. Implement the core classes/functions (e.g., `Robot`, command execution/dispatcher) and demonstrate with tests.

Quick Answer: This question evaluates design and implementation skills for an extensible, command-driven robot movement system, covering state representation on a 2D integer grid, directional updates, command dispatching, input/output semantics, and explicit handling policies for invalid commands.

Solution

### What this problem is really testing The core grid logic (`L`/`R`/`F`) is trivial. The interview signal is in the **design**: how you receive commands, how you make the command set extensible without rewriting the executor, your invalid-command policy, and how you reason about testing. Treat this as a small but real codebase, not a LeetCode one-liner. Below I build it up the way you'd do it live in a pair-programming session: state model first, then a registry-based command dispatcher, then policy, then tests. --- ### 1. Model the state cleanly Two ideas to nail up front: - **Directions are a 4-cycle.** Order them clockwise `N, E, S, W`. Then "turn right" is `+1 mod 4`, "turn left" is `-1 mod 4`. No `if/elif` ladder, no bugs in the rotation table. - **Each direction maps to a unit `(dx, dy)` step.** Forward is just `pos += step[dir]`. Backward (a future command) is `pos -= step[dir]`, for free. ```python from enum import IntEnum from dataclasses import dataclass class Direction(IntEnum): # Ordered clockwise so +1 mod 4 == turn right, -1 mod 4 == turn left. # This ordering is load-bearing: the rotation logic depends on it. N = 0 E = 1 S = 2 W = 3 def turn_right(self) -> "Direction": return Direction((self + 1) % 4) def turn_left(self) -> "Direction": return Direction((self - 1) % 4) # Unit step per direction. Using the standard math convention: # N increases y, E increases x. (Pick a convention and document it.) _STEP = { Direction.N: (0, 1), Direction.E: (1, 0), Direction.S: (0, -1), Direction.W: (-1, 0), } @dataclass class Robot: x: int = 0 y: int = 0 facing: Direction = Direction.N def turn_left(self) -> None: self.facing = self.facing.turn_left() def turn_right(self) -> None: self.facing = self.facing.turn_right() def step(self, n: int = 1) -> None: """Move n units along the current facing (n can be negative).""" dx, dy = _STEP[self.facing] self.x += dx * n self.y += dy * n def state(self) -> tuple[int, int, str]: return (self.x, self.y, self.facing.name) ``` `Robot` exposes a small **vocabulary of physical mutations** (`turn_left`, `turn_right`, `step`). Commands are written against this vocabulary — they never touch `self.facing` arithmetic directly. That separation is what keeps the executor stable as commands grow. > **Convention call-out (say this aloud in the interview):** I'm treating left/right as the robot's own perspective and `N→W→S→E→N` for left, matching the prompt. I'm using the math convention where `N` increases `y`. If the interviewer wants screen coordinates (`N` decreases `y`) it's a one-line change in `_STEP`. Stating the convention is half the point — silent assumptions here are the classic bug source. --- ### 2. The extensible command system (the heart of the question) The requirement: *"adding a new command must not require rewriting the execution logic."* This is the Open/Closed Principle and the **Command pattern + a registry**. Each command is a small object that knows how to mutate a `Robot`. The executor just looks the command up and applies it — it has **no knowledge** of which commands exist. ```python from typing import Protocol, Callable class Command(Protocol): def execute(self, robot: "Robot") -> None: ... class CommandRegistry: """Maps a command token (e.g. 'F') to a Command. Open for extension.""" def __init__(self) -> None: self._commands: dict[str, Command] = {} def register(self, token: str, command: Command) -> None: if token in self._commands: raise ValueError(f"Command {token!r} already registered") self._commands[token] = command def command(self, token: str): """Decorator sugar for registering a function as a command.""" def deco(fn: Callable[["Robot"], None]): self.register(token, _FuncCommand(fn)) return fn return deco def get(self, token: str) -> Command | None: return self._commands.get(token) @dataclass class _FuncCommand: fn: Callable[["Robot"], None] def execute(self, robot: "Robot") -> None: self.fn(robot) ``` Now the **initial command set** is just registration — no `switch`/`if` on the token anywhere: ```python registry = CommandRegistry() @registry.command("L") def _left(robot: Robot): robot.turn_left() @registry.command("R") def _right(robot: Robot): robot.turn_right() @registry.command("F") def _forward(robot: Robot): robot.step(1) ``` **Adding `B` (backward) and `J` (jump-2) later is a pure addition — zero changes to the executor or `Robot`:** ```python @registry.command("B") def _back(robot: Robot): robot.step(-1) @registry.command("J") def _jump(robot: Robot): robot.step(2) ``` That is the answer to requirement #5: new commands are new objects in a lookup table. The execution loop below never changes. > Why a registry over a giant `match token:`? A `match`/`if-elif` block in the executor means every new command edits the executor — you reopen, re-test, and risk regressing existing commands. The registry inverts this: commands register *themselves*, the executor stays closed. It also lets you build commands at runtime, swap command sets per robot, and (below) support stateful commands like `undo` without special-casing. --- ### 3. The executor + invalid-command policy ```python class RobotController: def __init__(self, robot: Robot, registry: CommandRegistry): self.robot = robot self.registry = registry def run(self, program: str, *, on_unknown: str = "raise") -> Robot: """ on_unknown: 'raise' -> stop and raise on first unknown token (default, fail-fast) 'ignore' -> skip unknown tokens silently 'collect' -> skip but return the list of (index, token) errors """ errors: list[tuple[int, str]] = [] for i, token in enumerate(program): if token.isspace(): # tolerate whitespace formatting continue cmd = self.registry.get(token) if cmd is None: if on_unknown == "raise": raise ValueError(f"Unknown command {token!r} at index {i}") if on_unknown == "collect": errors.append((i, token)) continue # 'ignore' and 'collect' both skip cmd.execute(self.robot) if on_unknown == "collect": self.last_errors = errors return self.robot ``` **Invalid-command policy — state the choice explicitly (requirement #4):** I default to **fail-fast `raise`**, because a robot acting on a corrupted/typo'd command stream is a safety issue — you'd rather stop than silently drift to the wrong place. But I make it a parameter rather than hard-coding it, because the right policy is context-dependent: a batch tool ingesting noisy logs may prefer `collect` (validate the whole program, report all bad tokens at once), and an interactive REPL may prefer `ignore`. Reporting the **index** of the bad token matters for debuggability — "unknown command at index 4203" is actionable; "invalid input" is not. **End-to-end usage matching the prompt's example:** ```python robot = Robot(0, 0, Direction.N) RobotController(robot, registry).run("FFRFF") assert robot.state() == (2, 2, "E") # matches the spec's expected output ``` --- ### 4. Worked trace of the example Start `(0, 0, N)`, program `"FFRFF"`: | step | cmd | facing before | action | state after | |------|-----|---------------|--------|-------------| | 1 | F | N | y += 1 | (0, 1, N) | | 2 | F | N | y += 1 | (0, 2, N) | | 3 | R | N | turn right | (0, 2, E) | | 4 | F | E | x += 1 | (1, 2, E) | | 5 | F | E | x += 1 | (2, 2, E) | Final `(2, 2, E)` ✔ — matches the spec. --- ### 5. Complexity - `run` is $O(n)$ in the program length $n$, with $O(1)$ work per command (dict lookup + a couple of integer ops). At $n = 10^5$ that's trivially fast. - State is $O(1)$. The registry is $O(k)$ for $k$ distinct commands ($k$ is tiny). - One micro-optimization if a program is dominated by long forward runs: **run-length collapse** consecutive identical commands (`FFFF…` → one `step(count)`) and apply rotations mod 4. This turns a degenerate $10^5$-`F` string into a single arithmetic update. I'd only add this if profiling showed a hot loop — it's a nice thing to mention, not to over-engineer first. --- ### 6. Handling stateful commands (`U` = undo) The interviewer's list includes `U` (undo), which doesn't fit a pure "mutate the robot" command — it needs history. The clean way to support it **without** breaking the abstraction: make the executor own an **undo stack of inverse operations**, and let commands optionally provide an inverse. ```python class ReversibleCommand(Protocol): def execute(self, robot: "Robot") -> None: ... def undo(self, robot: "Robot") -> None: ... ``` Turns and steps are trivially invertible (`turn_left`↔`turn_right`, `step(n)`↔`step(-n)`). The controller pushes each *reversible* executed command onto a stack; `U` pops and calls `undo`. Two things to call out so the design is actually correct, not just sketched: - **`U` itself must not be pushed onto the stack** — otherwise a second `U` would pop and "undo the undo" (or try to undo `U`, which has no inverse). Concretely, the controller pushes a command only if it's reversible *and* it isn't the undo command, so the stack holds only the real mutations. - A bare `U` on an empty stack is a no-op (or a defined error) — decide and state the policy, same as for unknown commands. This stays open/closed — `U` is just another registered command that happens to read the controller's stack. Bring this up to show you've thought past the happy path; implement it only if asked, to respect the 75-minute clock. --- ### 7. What I'd optimize / harden next - **Parse vs. execute separation.** Right now tokens are single chars. If commands grow to multi-char (`MOVE 3`, `TURN 90`), add a tiny **tokenizer** in front of the registry so the executor stays per-token. Designing the registry around a *token* (not a *char*) already anticipates this. - **Bounded grid extension.** The prompt says unbounded "unless you add bounds." If walls/obstacles appear, the natural home is inside `Robot.step` (clamp or raise on collision) or a `World` object the robot queries — the command objects don't change. Keeping movement policy out of the commands is what makes this drop-in. - **Command parameters & validation** (e.g. `step(n)` with `n` from input) — validate at registration/parse time, not deep in the loop. - **Immutability option.** For concurrency or replay/debugging, make `Robot` immutable and have commands *return* a new `Robot`. Slower but trivially testable and thread-safe; a reasonable trade to discuss. --- ### 8. Systematic testing strategy (requirement #6) I write tests **incrementally** alongside each piece (red/green), not all at the end. Concretely: **Unit tests — direction algebra (the bug-prone part):** ```python def test_rotation_cycles(): assert Direction.N.turn_right() == Direction.E assert Direction.N.turn_left() == Direction.W # four turns return to start, both ways for d in Direction: cur = d for _ in range(4): cur = cur.turn_right() assert cur == d ``` **Unit tests — single commands & the example:** ```python def test_forward_in_each_direction(): for d, (dx, dy) in _STEP.items(): r = Robot(0, 0, d); r.step() assert (r.x, r.y) == (dx, dy) def test_spec_example(): r = Robot(0, 0, Direction.N) RobotController(r, registry).run("FFRFF") assert r.state() == (2, 2, "E") ``` **Edge cases — enumerate deliberately:** - Empty program `""` → state unchanged. - Whitespace / formatting in the program → tolerated. - Unknown command under each policy: `raise` throws with the right index; `ignore` skips; `collect` returns exactly the bad `(index, token)` pairs. - Net-zero programs: `"LR"`, `"FB"`, `"RRRR"` → robot returns to start. - Large input: $10^5$ `F`s runs fast and lands at the expected coordinate. **Property-based tests (e.g. Hypothesis)** — these catch the bugs hand-written cases miss: - *Round-trip / inverse:* for any program, executing it then its reverse-inverse (`F`↔`B`, `L`↔`R`) returns to the start state. Great for catching off-by-one and sign errors. - *Rotation invariance:* the final position is independent of how many full `4×L`/`4×R` cycles are sprinkled in. - *Translation invariance:* running program `P` from `(x0,y0)` lands at `start + delta(P)`, where `delta` is independent of the start position — verifies movement is a pure offset. - *Manhattan bound:* final distance from origin ≤ number of `F`/`B` commands. **Extensibility regression test** — encodes the actual design goal: ```python def test_new_command_needs_no_executor_change(): reg = CommandRegistry() reg.register("L", _FuncCommand(lambda r: r.turn_left())) reg.register("Z", _FuncCommand(lambda r: r.step(5))) # brand-new command r = Robot(0, 0, Direction.N) RobotController(r, reg).run("Z") assert r.state() == (0, 5, "N") # works without touching RobotController ``` This last test is the one I'd point to when asked "how do you know the design is extensible?" — it *proves* a new command needs zero changes to the executor. --- ### Summary of design decisions (the talking points that score) - **Directions as a mod-4 enum** + per-direction unit step → rotation and movement are arithmetic, not branchy tables. - **Command pattern + registry/dispatcher** → Open/Closed: new commands are new registered objects; the executor never changes. This is the explicit answer to the extensibility requirement. - **Invalid-command policy is an explicit, swappable parameter** (default fail-fast `raise`), reporting the offending index. - **Forward/backward/jump fall out for free** because they're all `step(n)`; **undo** is handled by an optional inverse + controller stack without breaking the abstraction. - **Testing is incremental and layered**: unit (direction algebra is the hot spot), edge cases, property-based invariants (round-trip, translation invariance, Manhattan bound), and an extensibility regression test that encodes the design goal itself.

Related Interview Questions

  • Grid Robot Command Simulator - Shopify (medium)
  • Compute Theme Similarity - Shopify (medium)
  • Compute Jaccard Similarity for Lists - Shopify (medium)
  • Implement URL Shortening Codec - Shopify (medium)
  • Simulate a rover fleet - Shopify (medium)
|Home/Coding & Algorithms/Shopify

Design a robot movement command system

Shopify logo
Shopify
Dec 15, 2025, 12:00 AM
easyMachine Learning EngineerTechnical ScreenCoding & Algorithms
39
0

Robot Movement (Pair Programming)

You are given an empty starter repository (only a README). Implement a small, testable robot movement module that can:

  • Represent a robot on a 2D grid.
  • Receive a sequence of user commands.
  • Execute those commands to update the robot’s position and direction.
  • Be easily extensible as new valid commands are added over time.

Requirements

  1. Robot state
    • The robot has a position (x, y) on an integer grid.
    • The robot has a facing direction: one of {N, E, S, W} .
  2. Commands (initial set) Support at least these commands:
    • L : rotate 90° left (N→W→S→E→N)
    • R : rotate 90° right (N→E→S→W→N)
    • F : move forward by 1 step in the direction it is currently facing
  3. Input / Output
    • Input: initial state (x0, y0, dir0) and a command string like "FFLFFR" (or an equivalent list/array of commands).
    • Output: final state (x, y, dir) after executing all commands.
  4. Invalid command handling
    • Define and implement a clear policy for unknown commands (e.g., throw an error, ignore, or collect errors). State your choice.
  5. Extensibility constraint (core design requirement)
    • Assume valid commands will keep expanding (e.g., B for backward, J for jump, U for undo, etc.).
    • Design the command system so adding a new command does not require rewriting large parts of the robot execution logic.
    • Discuss/implement a command abstraction (e.g., command objects, a registry/dispatcher, etc.).
  6. Testing
    • Write small, incremental tests as you implement.
    • After finishing, explain:
      • What else you would optimize.
      • How you would do systematic testing (unit tests, property-based tests, edge cases).

Example

  • Initial: (0, 0, N)
  • Commands: "FFRFF"
  • Expected final: (2, 2, E)

Constraints (you may assume)

  • Command string length: up to ~10^5.
  • Grid is unbounded (no walls/obstacles) unless you explicitly choose to add bounds as an extension.

Implement the core classes/functions (e.g., Robot, command execution/dispatcher) and demonstrate with tests.

Submit Your Answer to Earn 20XP

Sign in to leave a comment

Loading comments...

Browse More Questions

More Coding & Algorithms•More Shopify•More Machine Learning Engineer•Shopify Machine Learning Engineer•Shopify Coding & Algorithms•Machine Learning Engineer Coding & Algorithms
PracHub

Master your tech interviews with 8,000+ real questions from top companies.

Product

  • Questions
  • Learning Tracks
  • Interview Guides
  • Resources
  • Premium
  • For Universities
  • Student Access

Browse

  • By Company
  • By Role
  • By Category
  • Topic Hubs
  • SQL Questions
  • Compare Platforms
  • Discord Community

Support

  • support@prachub.com
  • (916) 541-4762

Legal

  • Privacy Policy
  • Terms of Service
  • About Us

© 2026 PracHub. All rights reserved.