Roomba

Prompt
The goal is to create a Roomba board (like the vacuum), where you'll have 2 buttons: Turn Right and Move Forward. Your board will be an 6 x 6 matrix. You'll have a pointer on the board that moves as you press move forward and points in whatever direction you're going. Don't forget to add styling to the board and the buttons.
- Each click on Move Forward will move the Roomba one block
- When hitting an edge (where there's no more space to go), turn right and keep moving as you click
- Each click on Turn Right will rotate the Roomba 90 degrees to the right while staying in the same position
If you can complete the above requirements in less than 1 hour, try to add the following:
- Create some random barriers on the board. When hitting a barrier, turn right
- Allow controlling the pointer using the arrow keys on your keyboard
- Display the number of movements
Hi there! Just so you know, these bonus/extras/step ups are typically included in interviews for candidates who finish the main challenge early. They help interviewers gather more information about your skills and determine if you might be a good fit for a higher-level role. Happy coding!
Example
Playground
Solution
Explanation
Let's break down how our solution works, exploring component structure, state management, and the logic behind our virtual vacuum cleaner.
Component Structure
I've organized the code into several focused components.
- App.js: The main container that manages our application state and behavior
- Grid.js: Responsible for rendering the board matrix
- GridItem.js: Represents each cell in our grid
- Roomba.js: Renders our directional pointer which is the Roomba vaccum cleaner
- constants.js: Contains configuration constants and mapping objects
This component structure follows the single responsibility principle, with each component handling a specific concern. It makes our code more maintainable and easier to understand.
State Management
In App.js, we manage two key pieces of state:
const [direction, setDirection] = useState('right');
const [position, setPosition] = useState({ x: 0, y: 0 });
- direction: Tracks which way the Roomba is facing ('up', 'right', 'down', or 'left')
- position: Stores the current coordinates of the Roomba as an object with x and y properties
These two pieces of state are sufficient to determine everything about our Roomba's current status - where it is and which direction it's facing.
Core Logic
The heart of our application is in two main functions: handleTurnRight
and handleMoveForward
.
Turning the Roomba
const handleTurnRight = useCallback(() => {
setDirection(nextDirection[direction]);
}, [direction]);
The handleTurnRight
function is beautifully simple. Instead of using a series of if/else statements, we use a lookup object from our constants file.
This makes our direction changes clear and concise - when the Roomba is facing 'right' and turns right, it will now face 'down', and so on in a clockwise pattern.
export const nextDirection = {
right: 'down',
down: 'left',
left: 'up',
up: 'right',
};
Moving the Roomba
The handleMoveForward
function has a bit more logic we are doing few things here.
- Takes the current position and calculates a new position based on the direction
- Checks if the new position would be outside the grid boundaries
- If it would go out of bounds, the Roomba turns right instead of moving
- If it's a valid move, updates the position state
const handleMoveForward = useCallback(() => {
let { x, y } = position;
if (direction === 'right') x++;
if (direction === 'down') y++;
if (direction === 'left') x--;
if (direction === 'up') y--;
if (
x < 0 ||
x > matrixSize - 1 ||
y < 0 ||
y > matrixSize - 1
) {
handleTurnRight();
} else {
setPosition({ x, y });
}
}, [direction, position, handleTurnRight]);
Using useCallback
for both functions ensures they don't get recreated on every render, which is a small but meaningful optimization.
Rendering the Grid
The Grid component is responsible for creating our 6x6 matrix. We are passing 6 as the number of rows and columns to the Grid component. However, if we want to change the size of the grid, we can easily do so by changing the matrixSize
constant. This shows how reusable our component is.
const Grid = ({
numOfRows,
numOfColumns,
position,
direction,
}) => {
const matrix = useMemo(
() =>
Array(numOfRows).fill(
new Array(numOfColumns).fill(false)
),
[numOfRows, numOfColumns]
);
return (
<div className="grid">
{matrix.map((col, colIndex) => {
return (
<div className="row" key={colIndex}>
{col.map((row, rowIndex) => {
const isRoomba =
colIndex === position.y &&
rowIndex === position.x;
return (
<GridItem
isRoomba={isRoomba}
direction={direction}
key={`${colIndex}-${rowIndex}`}
/>
);
})}
</div>
);
})}
</div>
);
};
For each cell, we determine if it contains the Roomba by comparing its coordinates with the current position. If isRoomba
is true, we want to display the Roomba component in that cell. Which we do in the Roomba
component.
Displaying the Roomba
The Roomba component uses emojis to represent the vacuum's direction:
const Roomba = ({ direction }) => {
if (direction === 'up') return <Emoji symbol="👆" />;
if (direction === 'down') return <Emoji symbol="👇" />;
if (direction === 'right') return <Emoji symbol="👉" />;
if (direction === 'left') return <Emoji symbol="👈" />;
return null;
};
Using emojis makes our Roomba instantly recognizable and adds a fun element to the UI. The directional hand emojis naturally indicate which way the Roomba is facing.
Styling
The CSS is clean and straightforward:
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px;
}
.grid {
display: flex;
flex-direction: column;
gap: 2px;
}
.grid-item {
border: 1px solid black;
width: 32px;
height: 32px;
display: inline-block;
overflow: hidden;
margin: 2px;
text-align: center;
}
We use flexbox to center our content and create a clean layout. Each grid cell is a fixed size with a border to create our board visualization.
Putting It All Together
When a user interacts with our application:
- Clicking "Turn Right" calls
handleTurnRight
, which updates the direction state - Clicking "Move Forward" calls
handleMoveForward
, which either:- Updates the position state to move the Roomba forward
- Turns the Roomba right if it would hit an edge
The Grid component renders the board based on the current position and direction, and the Roomba component displays the appropriate directional emoji.
Roomba Bonus
We are able to solve the Roomba initial requirements in 30 minutes and now we have 30 minutes left to solve the bonus requirements, so let's do that. Playground given below is the solution of the initial requirements. You can use it as a reference to solve the bonus requirements.
Roomba Bonus Solution
Bonus Explanation
Movements Counter
We will start by adding a new state variable to our App component which will keep track of the number of movements.
const [movements, setMovements] = useState(0);
We will update the handleMoveForward
function to increment the movements
state variable only when the Roomba moves forward.
setMovements(movements + 1);
Just above the grid, we will add a new div which will display the number of movements. We will give same actions class to it so it can be styled the same way as the buttons and give same margin bottom.
<div className="actions">
<p>Movements: {movements}</p>
</div>
Keyboard Controls
useEffect(() => {
const handleKeyDown = (event) => {
const pressedDirection = arrowDirection[event.key];
if (pressedDirection === direction) {
handleMoveForward();
} else {
setDirection(pressedDirection);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [direction, position]);
The useEffect hook is setting up an event listener for keyboard inputs that allows users to control the Roomba using arrow keys. Here's what it's doing step by step:
- We define a handleKeyDown function that will be called whenever a key is pressed
- When a key is pressed, we check if it's one of our arrow keys by looking it up in the arrowDirection object
- The arrowDirection object maps keyboard keys to directions:
- ArrowRight: 'right'
- ArrowDown: 'down'
- ArrowLeft: 'left'
- ArrowUp: 'up'
- If the pressed key's direction matches the Roomba's current direction, we call
handleMoveForward()
to move the Roomba forward - If the pressed key's direction is different, we update the Roomba's direction using
setDirection(pressedDirection)
- We add the event listener when the component mounts using
document.addEventListener('keydown', handleKeyDown)
- We remove the event listener when the component unmounts or when dependencies change, using the cleanup function returned from
useEffect
- The dependencies array
[direction, position]
ensures our effect runs again whenever the direction or position changes, so our event handler always has access to the latest state values
This approach gives users two ways to control the Roomba:
- If they press an arrow key that matches the current direction, the Roomba moves forward
- If they press an arrow key for a different direction, the Roomba turns to face that direction
The cleanup function is crucial for preventing memory leaks - it removes the event listener when the component unmounts or before the effect runs again with new dependencies.
Adding Barriers
1. Generating the Barriers
First, we create a function in our constants file that generates random barrier positions:
export const generateBarriers = (numBarriers = 5) => {
const barriers = [];
while (barriers.length < numBarriers) {
const x = Math.floor(Math.random() * matrixSize);
const y = Math.floor(Math.random() * matrixSize);
// Don't place barrier at starting position (0,0)
if (!(x === 0 && y === 0)) {
const barrierKey = `${x},${y}`;
if (!barriers.includes(barrierKey)) {
barriers.push(barrierKey);
}
}
}
return barriers;
};
This function is like setting up furniture in a room:
- It creates 5 random barriers by default
- Each barrier gets a random x,y position on our grid
- We make sure not to put any barriers at (0,0) - that's where our Roomba starts!
- We store barrier positions as strings like "2,3" to make them easy to check later
2. Using the Barriers in Our App
In our App component, we use useMemo
to generate the barriers once when the app starts:
const barriers = useMemo(() => generateBarriers(), []);
We use useMemo
because:
- We only want to generate barriers once when the game starts
- The barriers should stay in the same place throughout the game
- We don't want to recalculate barrier positions on every render
3. Checking for Barriers During Movement
In our handleMoveForward
function, we check if the Roomba would hit a barrier:
if (
x < 0 ||
x > matrixSize - 1 ||
y < 0 ||
y > matrixSize - 1 ||
barriers.includes(`${x},${y}`)
) {
handleTurnRight();
} else {
setPosition({ x, y });
setMovements(movements + 1);
}
Just like with walls, if our Roomba would hit a barrier:
- It doesn't move to that position
- Instead, it turns right to try a different direction
4. Displaying the Barriers
In our Grid component, we check each cell to see if it contains a barrier:
const isBarrier = barriers.includes(
`${rowIndex},${colIndex}`
);
return (
<GridItem
isRoomba={isRoomba}
isBarrier={isBarrier}
direction={direction}
key={`${colIndex}-${rowIndex}`}
/>
);
And in our GridItem component, we show a barrier emoji if needed:
const GridItem = ({ isRoomba, isBarrier, direction }) => {
return (
<div className="grid-item">
{isRoomba && <Roomba direction={direction} />}
{isBarrier && (
<span role="img" aria-label="barrier">
🚧
</span>
)}
</div>
);
};
This is how we add a barrier emoji to our grid.
Interviewer Criteria
HTML/CSS
Does my layout accurately match the provided image?
Did you style the UI quickly (10 minutes or less)?
Are my CSS class names both self-explanatory and meaningful?
JavaScript
Have I leveraged ES6 features efficiently, such as let, const, arrow functions, and destructuring?
Are my variable and function names descriptive, maintaining a consistent naming convention?
Was I swiftly able to create the logic for the Roomba to move and edge detection?
React
Are props passed down appropriately between components?
Is there clear separation of concerns between components?
Have I used the key prop appropriately on all iterated elements, ensuring efficient and correct re-rendering?
Do I know how event listeners work in React? Was I able to use it to control the Roomba?
Was I able to cleanup the event listener when the component unmounts?
I made sure that I not storing barries in the state because they are not changing.
Did I used useMemo to generate barriers only once?
Component Architecture
Did I create separate components for
<Grid />
,<GridItem />
, and<Roomba />
?Are the names of my classes, functions, handlers, and components clear and understandable?
Time Checkpoints
- 10:00 AM
Interview starts 👥
- 10:03 AM
Candidate reads prompt and asks clarifying questions
- 10:12 AM
Generate grid from matrixSize and add action buttons
- 10:30 AM
Turn right and move forward functionality implemented
- 10:36 AM
Basic styling completed
- 10:38 AM
Movements counter implemented
- 10:45 AM
Keyboard controls implemented
- 10:55 AM
Barriers implemented
- 11:00 AM
Interview ends ✅