Command-Line Robot Simulator in C++
Build a terminal robot simulator using OOP, inheritance, and 2D vectors.
Introduction
30 Second Summary
Robots, games, and simulations all share a common code pattern: objects with clear responsibilities interacting inside a world. Whether it's a warehouse robot deciding where to move next or a game character responding to player commands, the underlying structure looks surprisingly similar.
In this project, you will build a command-line robot simulator in C++ where you pilot a robot through a grid world, scan your surroundings with sensors, and collect energy cells to win.
What You'll Build
A text-based robot simulator that uses a class hierarchy, a 2D vector grid, and a command-parsing game loop, all compiled and run from Git Bash.
By the end of this project, you'll have:
- A Grid class using a 2D std::vector that represents and displays a tile-based world with energy cells.
- A Robot class that manages position, movement, and energy collection across the grid.
- A Sensor inheritance hierarchy with a virtual base class and two specialized derived classes (ProximitySensor and EnergyScanner) that read the grid in different ways.
- Secret Mission: Add obstacles and a health mechanic where the robot takes damage if it crashes into walls.
Are there any prerequisites?
This project assumes you already know basic C++ concepts: cout, variables, functions, structs, classes, and derived classes. You should also have VS Code and Git Bash with g++ installed.
If you have never written a class or used inheritance before, start with a beginner C++ project first.
Before We Start
Before We Start...
Set Up the Project and Grid Class
Every simulation starts with a world. In this project, that world is a 2D grid of cells that can hold a robot, energy pickups, or nothing at all.
Before you can move a robot or scan its surroundings, you need the grid structure in place. In this step, you will create your project file, build a Grid class powered by a 2D vector, and randomly scatter energy cells across it.
In this step, get ready to:
- Create your project file and verify it compiles.
- Build the Grid class with a 2D vector and a display method.
- Add random energy cell placement to the grid.
Create your file and verify compilation
You need a project folder and a single C++ source file. Once the file compiles and runs, you know your toolchain is working.
- Open Git Bash by pressing the Windows key, typing Git Bash, and pressing Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir robot-simulator
cd robot-simulator
- Create your source file by running:
touch robot_sim.cpp
- Confirm the file exists by running:
ls
You should see robot_sim.cpp in the output.
- Open the file in VS Code by running:
code robot_sim.cpp
VS Code opens with robot_sim.cpp ready to edit.
- Paste the following starter code into robot_sim.cpp:
#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
#include <ctime>
int main() {
std::cout << "Robot Simulator starting...\n";
return 0;
}
What does this code do?
- The five #include lines pull in the standard libraries you will use throughout the project: console output, vectors, strings, random numbers, and time.
- The main() function is the entry point. Right now it just prints a test message so you can confirm compilation works.
- Save the file in VS Code with Ctrl+S.
- Switch back to Git Bash and compile by running:
g++ robot_sim.cpp -o robot_sim
What does this command do?
This tells the g++ compiler to read robot_sim.cpp and produce an executable called robot_sim. The -o flag sets the output filename.
If compilation succeeds, g++ prints nothing. Silence means success.
- Run your program:
./robot_sim
You should see Robot Simulator starting... printed in Git Bash.
Seeing 'command not found' or compile errors?
- If g++ is not recognized, make sure you are running from Git Bash (not PowerShell or CMD) and that MinGW is on your PATH.
- If you see a syntax error, double-check that your code has a semicolon after the std::cout line and matching braces around main().
Still stuck? Help me fix my g++ compile error in Git Bash.
Design and implement the Grid class
A grid is a 2D collection of cells. Each cell holds a value representing what is there: empty space, the robot, or an energy pickup. An enum is perfect for representing these fixed categories because it gives each option a readable name.
The Grid class owns a 2D std::vector of these cell values. It knows its dimensions and can print itself to the terminal.
- In robot_sim.cpp, select everything between the last #include line and the end of the file.
- Replace it with the following code:
enum CellType {
EMPTY,
ROBOT,
ENERGY
};
class Grid {
public:
int rows;
int cols;
std::vector<std::vector<CellType>> cells;
// Constructor: creates a rows-by-cols grid filled with EMPTY
Grid(int r, int c) : rows(r), cols(c), cells(r, std::vector<CellType>(c, EMPTY)) {}
// Prints the grid using characters for each cell type
void display() {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
switch (cells[i][j]) {
case EMPTY: std::cout << ". "; break;
case ROBOT: std::cout << "R "; break;
case ENERGY: std::cout << "E "; break;
}
}
std::cout << "\n";
}
}
};
int main() {
// Test the Grid class
Grid grid(7, 7);
grid.display();
return 0;
}
What does this code do?
- The enum CellType defines three possible states for any grid cell: EMPTY, ROBOT, or ENERGY.
- The constructor uses an initializer list to create the 2D vector. cells(r, std::vector<CellType>(c, EMPTY)) means: create r rows, each containing c cells initialized to EMPTY.
- The display() method loops through every cell and prints a character: . for empty, R for robot, E for energy.
- The updated main() constructs a 7x7 grid and displays it so you can verify the class works.
- Save robot_sim.cpp with Ctrl+S.
- Compile and run in Git Bash:
g++ robot_sim.cpp -o robot_sim
./robot_sim
You should see a 7x7 grid of dots printed in your terminal.
Seeing errors about vector or undeclared type?
- Make sure #include <vector> is still at the top of your file. The enum CellType definition must appear BEFORE the Grid class since the class uses it.
- Check for mismatched braces. The class needs a closing }; (semicolon after the brace).
Need more help? Help me fix my Grid class compile error.
Add energy cell placement
A grid of empty dots is not much of a world. You need energy cells scattered at random positions for the robot to collect. You also need helper methods that check whether a position is inside the grid boundaries.
- In robot_sim.cpp, find the closing brace of the display() method inside the Grid class.
- Add the following three methods below display(), still inside the Grid class (before the final };):
// Places energy cells at random empty positions
void placeEnergy(int count) {
int placed = 0;
while (placed < count) {
int r = rand() % rows;
int c = rand() % cols;
if (cells[r][c] == EMPTY) {
cells[r][c] = ENERGY;
placed++;
}
}
}
// Checks if a row/column position is within grid boundaries
bool inBounds(int r, int c) {
return r >= 0 && r < rows && c >= 0 && c < cols;
}
// Returns the cell type at a position (EMPTY if out of bounds)
CellType getCell(int r, int c) {
if (!inBounds(r, c)) return EMPTY;
return cells[r][c];
}
What does this code do?
- The placeEnergy() method picks random row/column pairs and places energy cells there. It skips cells that are already occupied, so placements never overlap.
- The inBounds() method returns true if a position is inside the grid. You will use this later to prevent the robot from walking off the edge.
- The getCell() method safely reads a cell value. If the position is out of bounds, it returns EMPTY instead of crashing.
Now update main() to seed the random number generator and use your new method.
- Replace your current main() function with this version:
int main() {
// Seed the random number generator with current time
srand(static_cast<unsigned>(time(nullptr)));
const int GRID_ROWS = 7;
const int GRID_COLS = 7;
const int TOTAL_ENERGY = 5;
// Create the grid and scatter energy cells
Grid grid(GRID_ROWS, GRID_COLS);
grid.placeEnergy(TOTAL_ENERGY);
grid.display();
return 0;
}
Why seed the random number generator?
Without srand(), the rand() function produces the same sequence every time you run the program. By seeding it with the current time, you get different energy cell positions on each run.
The static_cast<unsigned>(time(nullptr)) converts the current time into an unsigned integer that srand() expects.
- Save robot_sim.cpp with Ctrl+S.
- Compile and run in Git Bash:
g++ robot_sim.cpp -o robot_sim
./robot_sim
You should see a 7x7 grid of dots with 5 E characters scattered at random positions.
- Run ./robot_sim a second time. The E cells should appear in different positions.
Energy cells always in the same spot?
- Make sure srand(static_cast<unsigned>(time(nullptr))) appears at the very top of main(), before any call to rand(). If the seed is missing, rand() uses the same default seed every run.
- Verify you included #include <cstdlib> and #include <ctime> at the top of your file.
Still stuck? Help me debug why my energy cells always appear in the same place.
✔️ Awesome, I've got everything!
Great. Double check you have saved robot_sim.cpp and the grid displays correctly with random energy cells on each run.
ⓧ I'd like to double check the full code
Here is the complete robot_sim.cpp at the end of this step:
#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
#include <ctime>
enum CellType {
EMPTY,
ROBOT,
ENERGY
};
class Grid {
public:
int rows;
int cols;
std::vector<std::vector<CellType>> cells;
Grid(int r, int c) : rows(r), cols(c), cells(r, std::vector<CellType>(c, EMPTY)) {}
void display() {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
switch (cells[i][j]) {
case EMPTY: std::cout << ". "; break;
case ROBOT: std::cout << "R "; break;
case ENERGY: std::cout << "E "; break;
}
}
std::cout << "\n";
}
}
void placeEnergy(int count) {
int placed = 0;
while (placed < count) {
int r = rand() % rows;
int c = rand() % cols;
if (cells[r][c] == EMPTY) {
cells[r][c] = ENERGY;
placed++;
}
}
}
bool inBounds(int r, int c) {
return r >= 0 && r < rows && c >= 0 && c < cols;
}
CellType getCell(int r, int c) {
if (!inBounds(r, c)) return EMPTY;
return cells[r][c];
}
};
int main() {
srand(static_cast<unsigned>(time(nullptr)));
const int GRID_ROWS = 7;
const int GRID_COLS = 7;
const int TOTAL_ENERGY = 5;
Grid grid(GRID_ROWS, GRID_COLS);
grid.placeEnergy(TOTAL_ENERGY);
grid.display();
return 0;
}
Your grid world is alive. Each run scatters energy cells in new locations, giving you a different puzzle every time. Next up, you will build the Robot class and wire it to move across this grid.
Build the Robot Class and Movement
Your grid world is alive with energy cells scattered across it. Now it needs an inhabitant.
In this step, you will build a Robot class that knows its position and can move through the grid. The robot does not own the grid. It has a position and interacts with the grid through method calls. This separation of concerns is the same principle robotics engineers use: the world manages its state, and the agent manages its movement logic.
In this step, get ready to:
- Design and implement the Robot class with position tracking and movement.
- Connect the Robot to the Grid so movement updates the display.
- Build an interactive command loop that reads directions and moves the robot.
Design the Robot class
A robot needs to know where it is (row and column) and how much energy it has collected. It needs to do one key thing: move in a direction. The move logic checks boundaries, collects energy if present, and updates the grid cells.
- In robot_sim.cpp, find the closing }; of the Grid class.
- Add the following Robot class below it, above int main():
class Robot {
public:
int row;
int col;
int energyCollected;
// Start the robot at a given grid position with zero energy
Robot(int r, int c) : row(r), col(c), energyCollected(0) {}
// Attempt to move in a direction, updating the grid accordingly
bool move(const std::string& direction, Grid& grid) {
int newRow = row;
int newCol = col;
// Convert direction string to coordinate change
if (direction == "north") newRow--;
else if (direction == "south") newRow++;
else if (direction == "east") newCol++;
else if (direction == "west") newCol--;
else return false;
// Boundary check: stop if the target is off the grid
if (!grid.inBounds(newRow, newCol)) {
std::cout << "Blocked! Wall ahead.\n";
return false;
}
// Collect energy if the target cell has it
if (grid.cells[newRow][newCol] == ENERGY) {
energyCollected++;
std::cout << "Energy cell collected!\n";
}
// Update grid: clear old position, mark new position
grid.cells[row][col] = EMPTY;
row = newRow;
col = newCol;
grid.cells[row][col] = ROBOT;
return true;
}
};
What does this code do?
The Robot class manages the robot's position and collection score. It does not store a copy of the grid.
- move() takes a direction string and a reference to the Grid. It calculates the target cell, checks if it is in bounds, and handles energy collection.
- The method returns false if the move is invalid (bad direction or wall). This gives calling code a way to know whether the move succeeded.
- Grid updates happen in two steps: the old cell becomes EMPTY, then the new cell becomes ROBOT.
- Save robot_sim.cpp.
- Compile by running:
g++ robot_sim.cpp -o robot_sim
You should see no errors. The Robot class compiles even though nothing uses it yet.
Seeing a compile error?
- Make sure the Robot class is placed after the Grid class. Robot references Grid in its move() method, so Grid must be defined first.
- Check that you have the closing }; at the end of the class. Missing semicolons after class definitions cause confusing errors.
- Verify that const std::string& direction uses & for the reference. Without it, the code still compiles but copies the string unnecessarily.
Why won't my Robot class compile?
Connect Robot to Grid
The Robot class exists but nothing creates an instance of it yet. You need to place the robot at the centre of the grid so it appears when the game starts.
- In main(), find this line:
grid.display();
- Delete that line and replace it with the following robot placement code:
// Place the robot in the centre of the grid
int startRow = GRID_ROWS / 2;
int startCol = GRID_COLS / 2;
Robot robot(startRow, startCol);
grid.cells[startRow][startCol] = ROBOT;
What does this code do?
- startRow and startCol calculate the middle of the 7x7 grid (row 3, column 3).
- Robot robot(startRow, startCol) creates a Robot object that tracks its position internally.
- grid.cells[startRow][startCol] = ROBOT marks the grid cell so the display method knows to print R there.
- Save robot_sim.cpp and compile again:
g++ robot_sim.cpp -o robot_sim
The program compiles with no errors. If you run it now, it exits immediately because you removed the display call. The next substep adds the interactive loop that will display the grid and accept commands.
Getting an undeclared identifier error?
- If you see an error about Robot being undeclared, confirm your Robot class appears above main() in the file.
- Make sure you deleted the old grid.display(); line and did not accidentally remove other lines like grid.placeEnergy(TOTAL_ENERGY);.
Help me fix my robot placement code in main().
Add the command input loop
A game loop reads input, processes it, updates state, and displays the result. This is the same pattern as a robot's control loop: read sensors, decide action, execute, repeat.
- In main(), add the following game loop code below the robot placement lines you just added, and above return 0;:
std::string input;
std::cout << "=== ROBOT SIMULATOR ===\n";
std::cout << "Collect all " << TOTAL_ENERGY << " energy cells to win!\n";
std::cout << "Type 'help' for commands.\n\n";
while (true) {
// Show current score and grid each turn
std::cout << "[Energy: " << robot.energyCollected << "/" << TOTAL_ENERGY << "]\n";
grid.display();
std::cout << "\nCommand: ";
std::getline(std::cin, input);
// Handle each command
if (input == "quit") {
std::cout << "Shutting down robot. Goodbye!\n";
break;
} else if (input == "north" || input == "south" || input == "east" || input == "west") {
robot.move(input, grid);
} else {
std::cout << "Unknown command. Type 'help' for options.\n";
}
std::cout << "\n";
}
What does this game loop do?
- Each iteration displays the energy score, renders the grid, and waits for the player to type a command.
- std::getline(std::cin, input) reads a full line of text, which is why the learner can type multi-character words like north.
- The quit command breaks out of the infinite loop. Direction commands call robot.move(). Anything else prints an error message.
- Save robot_sim.cpp.
- Compile and run the program:
g++ robot_sim.cpp -o robot_sim
./robot_sim
You should see the welcome banner, then a 7x7 grid with an R in the centre and E cells scattered around. Type north, south, east, or west and watch the robot move. Type quit to exit.
Program hangs or doesn't accept input?
- If the program prints the grid but does not show a Command: prompt, check that you have std::getline(std::cin, input) inside the loop.
- If you see an infinite loop of grids printing, make sure the break statement is inside the if (input == "quit") block.
- If the robot does not move, confirm that the direction check uses == (comparison) not = (assignment).
My robot simulator loop isn't working correctly. Help me debug.
✔️ Awesome, I've got everything!
Great. Double check you have saved robot_sim.cpp and the robot moves when you type directions.
ⓧ I'd like to double check the full code
Here is the full robot_sim.cpp at this point:
#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
#include <ctime>
enum CellType {
EMPTY,
ROBOT,
ENERGY
};
class Grid {
public:
int rows;
int cols;
std::vector<std::vector<CellType>> cells;
Grid(int r, int c) : rows(r), cols(c), cells(r, std::vector<CellType>(c, EMPTY)) {}
void display() {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
switch (cells[i][j]) {
case EMPTY: std::cout << ". "; break;
case ROBOT: std::cout << "R "; break;
case ENERGY: std::cout << "E "; break;
}
}
std::cout << "\n";
}
}
void placeEnergy(int count) {
int placed = 0;
while (placed < count) {
int r = rand() % rows;
int c = rand() % cols;
if (cells[r][c] == EMPTY) {
cells[r][c] = ENERGY;
placed++;
}
}
}
bool inBounds(int r, int c) {
return r >= 0 && r < rows && c >= 0 && c < cols;
}
CellType getCell(int r, int c) {
if (!inBounds(r, c)) return EMPTY;
return cells[r][c];
}
};
class Robot {
public:
int row;
int col;
int energyCollected;
Robot(int r, int c) : row(r), col(c), energyCollected(0) {}
bool move(const std::string& direction, Grid& grid) {
int newRow = row;
int newCol = col;
if (direction == "north") newRow--;
else if (direction == "south") newRow++;
else if (direction == "east") newCol++;
else if (direction == "west") newCol--;
else return false;
if (!grid.inBounds(newRow, newCol)) {
std::cout << "Blocked! Wall ahead.\n";
return false;
}
if (grid.cells[newRow][newCol] == ENERGY) {
energyCollected++;
std::cout << "Energy cell collected!\n";
}
grid.cells[row][col] = EMPTY;
row = newRow;
col = newCol;
grid.cells[row][col] = ROBOT;
return true;
}
};
int main() {
srand(static_cast<unsigned>(time(nullptr)));
const int GRID_ROWS = 7;
const int GRID_COLS = 7;
const int TOTAL_ENERGY = 5;
Grid grid(GRID_ROWS, GRID_COLS);
grid.placeEnergy(TOTAL_ENERGY);
int startRow = GRID_ROWS / 2;
int startCol = GRID_COLS / 2;
Robot robot(startRow, startCol);
grid.cells[startRow][startCol] = ROBOT;
std::string input;
std::cout << "=== ROBOT SIMULATOR ===\n";
std::cout << "Collect all " << TOTAL_ENERGY << " energy cells to win!\n";
std::cout << "Type 'help' for commands.\n\n";
while (true) {
std::cout << "[Energy: " << robot.energyCollected << "/" << TOTAL_ENERGY << "]\n";
grid.display();
std::cout << "\nCommand: ";
std::getline(std::cin, input);
if (input == "quit") {
std::cout << "Shutting down robot. Goodbye!\n";
break;
} else if (input == "north" || input == "south" || input == "east" || input == "west") {
robot.move(input, grid);
} else {
std::cout << "Unknown command. Type 'help' for options.\n";
}
std::cout << "\n";
}
return 0;
}
Your robot moves, collects energy, and respects boundaries. Next up, you will give it sensors that can scan the world around it using inheritance.
Build the Sensor Inheritance System
Your robot can move around the grid and collect energy, but it is blind. It has no way to perceive what surrounds it before moving.
In this step, you will build a sensor system using inheritance. You will create a base Sensor class that defines what all sensors can do, then build two specialized sensors that each detect different things. This is the same architectural pattern used in real robotics firmware, where LiDAR, infrared, and ultrasonic sensors all share a common interface but behave differently.
In this step, get ready to:
- Design an abstract base Sensor class with a pure virtual method.
- Implement a ProximitySensor that checks adjacent cells.
- Implement an EnergyScanner that counts nearby energy cells and wire both sensors into the game loop.
Design the base Sensor class
All sensors share something in common: they have a name, and they can scan the grid to produce a report. The difference is HOW each sensor scans.
This is where inheritance shines. A base class defines the interface (what a sensor IS and what it CAN do). Derived classes define the specific behaviour (HOW each sensor does it).
Why use a virtual method here?
Marking scan() as virtual and setting it to = 0 makes the base class abstract. You cannot create a plain Sensor object. You can only create objects of derived classes that provide their own implementation of scan().
This forces every sensor type to define how it scans. The compiler will reject any derived class that forgets to implement it.
- In robot_sim.cpp, find the closing }; of the Robot class. Add the following Sensor class below it:
class Sensor {
public:
std::string name;
Sensor(const std::string& n) : name(n) {}
// Pure virtual: every derived sensor MUST implement scan()
virtual std::string scan(const Grid& grid, int robotRow, int robotCol) = 0;
// Virtual destructor for safe cleanup through base pointers
virtual ~Sensor() {}
};
What does this code do?
The name member stores what kind of sensor this is (for display purposes).
The = 0 after scan() makes it a pure virtual function. This means the Sensor class itself is incomplete. It defines the contract (scan takes a grid and a position, returns a string report) but leaves the actual logic to its children.
The virtual destructor ensures that if you ever delete a sensor through a base-class pointer, the derived class's destructor runs properly.
- Save robot_sim.cpp.
- Compile to check for syntax errors by running:
g++ robot_sim.cpp -o robot_sim
You should see no errors. The abstract class compiles fine on its own since you are not trying to instantiate it.
Seeing a compilation error?
Make sure the Sensor class is placed AFTER the Robot class and BEFORE main(). The Sensor class references Grid in its scan() signature, so Grid must be defined above it.
Check that you have the semicolon after the closing brace (};). Missing class-closing semicolons are a common C++ mistake.
Help me fix my Sensor class compilation error
Implement the ProximitySensor
A proximity sensor checks what is immediately around the robot: the four adjacent cells (north, south, east, west). It reports whether each direction has a wall (grid boundary), energy, or is clear.
- Add the ProximitySensor class directly below your Sensor class:
class ProximitySensor : public Sensor {
public:
ProximitySensor() : Sensor("Proximity Sensor") {}
std::string scan(const Grid& grid, int robotRow, int robotCol) override {
std::string report = "--- Proximity Report ---\n";
// Direction offsets: north, south, east, west
int dr[] = {-1, 1, 0, 0};
int dc[] = {0, 0, 1, -1};
std::string dirs[] = {"North", "South", "East", "West"};
for (int i = 0; i < 4; i++) {
int nr = robotRow + dr[i];
int nc = robotCol + dc[i];
report += " " + dirs[i] + ": ";
if (!grid.inBounds(nr, nc)) {
report += "WALL";
} else if (grid.cells[nr][nc] == ENERGY) {
report += "ENERGY";
} else {
report += "clear";
}
report += "\n";
}
return report;
}
};
What does this code do?
The : public Sensor means ProximitySensor inherits from Sensor. Its constructor passes "Proximity Sensor" up to the base class.
The dr[] and dc[] arrays encode direction offsets. For example, north is row-1, col+0. The loop checks each neighbour using inBounds() and builds a human-readable report string.
The override keyword tells the compiler this method is fulfilling the base class contract. If you accidentally misspell the method name, the compiler catches it.
- Save robot_sim.cpp and compile again:
g++ robot_sim.cpp -o robot_sim
No errors means your derived class successfully implements the pure virtual method from Sensor.
Getting an 'override' error?
Make sure the scan() signature in ProximitySensor matches the base class exactly: std::string scan(const Grid& grid, int robotRow, int robotCol). Even a small difference (missing const, different parameter names) will cause the compiler to reject the override.
Help me fix my ProximitySensor override error
Implement EnergyScanner and integrate sensors into the game
The energy scanner has a longer detection range. Instead of checking just the four adjacent cells, it searches a square area around the robot (2 cells in each direction) and counts how many energy cells are within that radius.
- Add the EnergyScanner class directly below your ProximitySensor class:
class EnergyScanner : public Sensor {
public:
EnergyScanner() : Sensor("Energy Scanner") {}
std::string scan(const Grid& grid, int robotRow, int robotCol) override {
int count = 0;
int range = 2;
// Search a square area around the robot
for (int r = robotRow - range; r <= robotRow + range; r++) {
for (int c = robotCol - range; c <= robotCol + range; c++) {
if (grid.inBounds(r, c) && grid.cells[r][c] == ENERGY) {
count++;
}
}
}
return "--- Energy Scanner ---\n Energy cells within range: " + std::to_string(count) + "\n";
}
};
What does this code do?
The nested for loops sweep a 5x5 square (range 2 in each direction) centred on the robot. For each cell in that area, it checks if it is in bounds AND contains energy.
Notice both sensors implement the same scan() method but produce completely different results. That is polymorphism in action: same interface, different behaviour.
- Save robot_sim.cpp and compile:
g++ robot_sim.cpp -o robot_sim
No errors means both derived classes properly implement the base class contract.
Seeing an error about std::to_string?
Make sure you have #include <string> at the top of your file. The std::to_string() function is declared in that header.
Help me fix my EnergyScanner compilation error
Now you need to create sensor objects and wire them into the game loop so the player can use them.
- In main(), find the line that says std::string input;. Add these two lines directly above it:
ProximitySensor proxSensor;
EnergyScanner energySensor;
- Now find your if-else chain inside the while (true) loop. It currently looks like this:
if (input == "quit") {
std::cout << "Shutting down robot. Goodbye!\n";
break;
} else if (input == "north" || input == "south" || input == "east" || input == "west") {
robot.move(input, grid);
} else {
std::cout << "Unknown command. Type 'help' for options.\n";
}
- Replace that entire if-else chain with this updated version that adds help, status, and scan commands:
if (input == "quit") {
std::cout << "Shutting down robot. Goodbye!\n";
break;
} else if (input == "help") {
std::cout << "Commands: north, south, east, west, scan, status, quit\n";
} else if (input == "status") {
std::cout << "Energy collected: " << robot.energyCollected << "/" << TOTAL_ENERGY << "\n";
std::cout << "Position: (" << robot.row << ", " << robot.col << ")\n";
} else if (input == "scan") {
std::cout << proxSensor.scan(grid, robot.row, robot.col);
std::cout << energySensor.scan(grid, robot.row, robot.col);
} else if (input == "north" || input == "south" || input == "east" || input == "west") {
robot.move(input, grid);
} else {
std::cout << "Unknown command. Type 'help' for options.\n";
}
What changed in the game loop?
Three new command branches sit between quit and the movement check. The order matters. help lists all available commands. status shows the robot's energy progress and grid coordinates. scan calls both sensors and prints their reports.
The movement branch moved lower in the chain so that the single-word commands (help, status, scan) are checked first. This keeps the logic clean and avoids accidentally treating them as invalid directions.
- Save robot_sim.cpp.
- Compile and run the program:
g++ robot_sim.cpp -o robot_sim
./robot_sim
- Type scan and press Enter.
- Type help and press Enter.
- Type status and press Enter.
You should see the proximity report showing WALL or clear for each direction, the energy scanner showing how many energy cells are within range, the help listing all commands, and the status showing your position and collection progress.
Scan shows 0 energy cells nearby?
That is normal if the random placement put all energy cells more than 2 cells away from the centre. Move the robot closer to an E on the grid and scan again. The energy count should increase.
If the program does not compile, check that proxSensor and energySensor are declared AFTER grid.cells[startRow][startCol] = ROBOT; and BEFORE the std::string input; line.
Help me debug my scan command
✔️ Awesome, I've got everything!
Great. Double check you have saved robot_sim.cpp and all three commands (scan, help, status) work as expected.
ⓧ I'd like to double check the full code
Here is the complete robot_sim.cpp file at the end of this step:
#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
#include <ctime>
enum CellType {
EMPTY,
ROBOT,
ENERGY
};
class Grid {
public:
int rows;
int cols;
std::vector<std::vector<CellType>> cells;
Grid(int r, int c) : rows(r), cols(c), cells(r, std::vector<CellType>(c, EMPTY)) {}
void display() {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
switch (cells[i][j]) {
case EMPTY: std::cout << ". "; break;
case ROBOT: std::cout << "R "; break;
case ENERGY: std::cout << "E "; break;
}
}
std::cout << "\n";
}
}
void placeEnergy(int count) {
int placed = 0;
while (placed < count) {
int r = rand() % rows;
int c = rand() % cols;
if (cells[r][c] == EMPTY) {
cells[r][c] = ENERGY;
placed++;
}
}
}
bool inBounds(int r, int c) {
return r >= 0 && r < rows && c >= 0 && c < cols;
}
CellType getCell(int r, int c) {
if (!inBounds(r, c)) return EMPTY;
return cells[r][c];
}
};
class Robot {
public:
int row;
int col;
int energyCollected;
Robot(int r, int c) : row(r), col(c), energyCollected(0) {}
bool move(const std::string& direction, Grid& grid) {
int newRow = row;
int newCol = col;
if (direction == "north") newRow--;
else if (direction == "south") newRow++;
else if (direction == "east") newCol++;
else if (direction == "west") newCol--;
else return false;
if (!grid.inBounds(newRow, newCol)) {
std::cout << "Blocked! Wall ahead.\n";
return false;
}
if (grid.cells[newRow][newCol] == ENERGY) {
energyCollected++;
std::cout << "Energy cell collected!\n";
}
grid.cells[row][col] = EMPTY;
row = newRow;
col = newCol;
grid.cells[row][col] = ROBOT;
return true;
}
};
class Sensor {
public:
std::string name;
Sensor(const std::string& n) : name(n) {}
virtual std::string scan(const Grid& grid, int robotRow, int robotCol) = 0;
virtual ~Sensor() {}
};
class ProximitySensor : public Sensor {
public:
ProximitySensor() : Sensor("Proximity Sensor") {}
std::string scan(const Grid& grid, int robotRow, int robotCol) override {
std::string report = "--- Proximity Report ---\n";
int dr[] = {-1, 1, 0, 0};
int dc[] = {0, 0, 1, -1};
std::string dirs[] = {"North", "South", "East", "West"};
for (int i = 0; i < 4; i++) {
int nr = robotRow + dr[i];
int nc = robotCol + dc[i];
report += " " + dirs[i] + ": ";
if (!grid.inBounds(nr, nc)) {
report += "WALL";
} else if (grid.cells[nr][nc] == ENERGY) {
report += "ENERGY";
} else {
report += "clear";
}
report += "\n";
}
return report;
}
};
class EnergyScanner : public Sensor {
public:
EnergyScanner() : Sensor("Energy Scanner") {}
std::string scan(const Grid& grid, int robotRow, int robotCol) override {
int count = 0;
int range = 2;
for (int r = robotRow - range; r <= robotRow + range; r++) {
for (int c = robotCol - range; c <= robotCol + range; c++) {
if (grid.inBounds(r, c) && grid.cells[r][c] == ENERGY) {
count++;
}
}
}
return "--- Energy Scanner ---\n Energy cells within range: " + std::to_string(count) + "\n";
}
};
int main() {
srand(static_cast<unsigned>(time(nullptr)));
const int GRID_ROWS = 7;
const int GRID_COLS = 7;
const int TOTAL_ENERGY = 5;
Grid grid(GRID_ROWS, GRID_COLS);
grid.placeEnergy(TOTAL_ENERGY);
int startRow = GRID_ROWS / 2;
int startCol = GRID_COLS / 2;
Robot robot(startRow, startCol);
grid.cells[startRow][startCol] = ROBOT;
ProximitySensor proxSensor;
EnergyScanner energySensor;
std::string input;
std::cout << "=== ROBOT SIMULATOR ===\n";
std::cout << "Collect all " << TOTAL_ENERGY << " energy cells to win!\n";
std::cout << "Type 'help' for commands.\n\n";
while (true) {
std::cout << "[Energy: " << robot.energyCollected << "/" << TOTAL_ENERGY << "]\n";
grid.display();
std::cout << "\nCommand: ";
std::getline(std::cin, input);
if (input == "quit") {
std::cout << "Shutting down robot. Goodbye!\n";
break;
} else if (input == "help") {
std::cout << "Commands: north, south, east, west, scan, status, quit\n";
} else if (input == "status") {
std::cout << "Energy collected: " << robot.energyCollected << "/" << TOTAL_ENERGY << "\n";
std::cout << "Position: (" << robot.row << ", " << robot.col << ")\n";
} else if (input == "scan") {
std::cout << proxSensor.scan(grid, robot.row, robot.col);
std::cout << energySensor.scan(grid, robot.row, robot.col);
} else if (input == "north" || input == "south" || input == "east" || input == "west") {
robot.move(input, grid);
} else {
std::cout << "Unknown command. Type 'help' for options.\n";
}
std::cout << "\n";
}
return 0;
}
Your robot now has eyes. It can sense walls, detect energy nearby, and report its status. Next up, you will add a win condition and polish the game into a complete, playable experience.
Add Win Condition and Game Polish
Your robot can move, collect energy, and scan its environment. You have a working sensor hierarchy with polymorphism in action.
A game without an ending is just a sandbox. In this step, you will add a win condition that detects when the robot has collected all energy cells and ends the game with a victory message. Then you will test every command and edge case to make sure the simulator handles everything gracefully.
In this step, get ready to:
- Add a win condition that ends the game when all energy cells are collected.
- Verify the status display and help commands work correctly.
- Test edge cases to confirm robust error handling.
Add a win condition to end the game
The game should end when the robot has collected every energy cell on the grid. Your program already tracks energyCollected on the Robot and stores the total in TOTAL_ENERGY. All you need is a comparison after each command.
- In robot_sim.cpp, scroll to the bottom of your while (true) game loop inside main().
- Find this line near the end of the loop:
std::cout << "\n";
- Add the following win condition check directly ABOVE that line, after the closing } of the else block for unknown commands:
if (robot.energyCollected == TOTAL_ENERGY) {
std::cout << "\n*** MISSION COMPLETE! All energy cells collected! ***\n";
grid.display();
break;
}
What does this code do?
After every command the player types, this check runs. If the robot's energyCollected count equals TOTAL_ENERGY, the game is won.
It prints a victory message, displays the final grid state one last time, and uses break to exit the while (true) loop. Placing this check after ALL commands (not just movement) means the game responds immediately when the last energy cell is collected.
- Save robot_sim.cpp.
- Compile your program by running this command in Git Bash:
g++ robot_sim.cpp -o robot_sim
- Run the game by running:
./robot_sim
- Navigate to each energy cell (E) on the grid using north, south, east, and west commands to collect all 5 energy cells.
- You should see the message *** MISSION COMPLETE! All energy cells collected! *** and the game should exit.
Game not ending after collecting all cells?
Make sure the win condition check is placed AFTER the command-handling chain (after the closing } of the last else) but BEFORE std::cout << "\n";. If it is inside one of the else if branches, it will only trigger for that one command type.
Also confirm you are comparing with == (equality) not = (assignment).
Why isn't my win condition triggering after I collect all energy cells?
Verify status display and help commands
A good interface tells the user what they can do and what state they are in. Your game already includes a header showing the energy count, a help command listing available commands, and a status command showing progress. Let's confirm they all work together.
- Run the game again with ./robot_sim.
- Type help and press Enter.
- You should see: Commands: north, south, east, west, scan, status, quit.
- Type status and press Enter.
- You should see the energy collected vs. total and your robot's current position.
- Move the robot by typing north or another direction, then look at the top of the display.
- Confirm the [Energy: x/5] header updates when you collect an energy cell.
Why test commands individually?
In professional software development, testing each feature in isolation helps you catch bugs early. If the score header never updates, you know the issue is specifically in the display line, not in the collection logic. This systematic approach saves time compared to playing the whole game and hoping you notice problems.
Test edge cases for robustness
Edge cases are inputs that sit at the boundaries of what your program expects. A robust program handles them without crashing.
- Move the robot to any edge of the grid, then try to move further in that direction.
- You should see Blocked! Wall ahead. and the robot should stay in place.
- Type a nonsense command like fly or jump and press Enter.
- You should see Unknown command. Type 'help' for options. instead of a crash.
- Try moving into all four walls (north wall, south wall, east wall, west wall) to confirm boundary checks work in every direction.
- Type quit to exit cleanly.
What makes this robust?
Your inBounds() check in the Robot's move() method prevents out-of-range vector access, which would crash the program. The else clause at the end of the command chain catches anything that does not match a known command.
This is defensive programming. In robotics firmware, an unhandled input could mean a physical collision. In games, it means a crash and a frustrated player. Both scenarios are solved the same way: check first, act second.
Program crashing on an edge case?
If you get a segfault or crash when moving to the edge, double check that your inBounds() method returns false before the robot accesses the grid cell. The boundary check must come before any cell access in move().
My robot simulator crashes when I try to move off the edge of the grid. Help me debug my boundary checking.
✔️ Awesome, I've got everything!
Great. Double check you have saved robot_sim.cpp and your game compiles and runs without errors.
ⓧ I'd like to double check the full code
Here is the complete robot_sim.cpp file as it should look at the end of this step.
#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
#include <ctime>
enum CellType {
EMPTY,
ROBOT,
ENERGY
};
class Grid {
public:
int rows;
int cols;
std::vector<std::vector<CellType>> cells;
Grid(int r, int c) : rows(r), cols(c), cells(r, std::vector<CellType>(c, EMPTY)) {}
void display() {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
switch (cells[i][j]) {
case EMPTY: std::cout << ". "; break;
case ROBOT: std::cout << "R "; break;
case ENERGY: std::cout << "E "; break;
}
}
std::cout << "\n";
}
}
void placeEnergy(int count) {
int placed = 0;
while (placed < count) {
int r = rand() % rows;
int c = rand() % cols;
if (cells[r][c] == EMPTY) {
cells[r][c] = ENERGY;
placed++;
}
}
}
bool inBounds(int r, int c) {
return r >= 0 && r < rows && c >= 0 && c < cols;
}
CellType getCell(int r, int c) {
if (!inBounds(r, c)) return EMPTY;
return cells[r][c];
}
};
class Robot {
public:
int row;
int col;
int energyCollected;
Robot(int r, int c) : row(r), col(c), energyCollected(0) {}
bool move(const std::string& direction, Grid& grid) {
int newRow = row;
int newCol = col;
if (direction == "north") newRow--;
else if (direction == "south") newRow++;
else if (direction == "east") newCol++;
else if (direction == "west") newCol--;
else return false;
if (!grid.inBounds(newRow, newCol)) {
std::cout << "Blocked! Wall ahead.\n";
return false;
}
if (grid.cells[newRow][newCol] == ENERGY) {
energyCollected++;
std::cout << "Energy cell collected!\n";
}
grid.cells[row][col] = EMPTY;
row = newRow;
col = newCol;
grid.cells[row][col] = ROBOT;
return true;
}
};
class Sensor {
public:
std::string name;
Sensor(const std::string& n) : name(n) {}
virtual std::string scan(const Grid& grid, int robotRow, int robotCol) = 0;
virtual ~Sensor() {}
};
class ProximitySensor : public Sensor {
public:
ProximitySensor() : Sensor("Proximity Sensor") {}
std::string scan(const Grid& grid, int robotRow, int robotCol) override {
std::string report = "--- Proximity Report ---\n";
int dr[] = {-1, 1, 0, 0};
int dc[] = {0, 0, 1, -1};
std::string dirs[] = {"North", "South", "East", "West"};
for (int i = 0; i < 4; i++) {
int nr = robotRow + dr[i];
int nc = robotCol + dc[i];
report += " " + dirs[i] + ": ";
if (!grid.inBounds(nr, nc)) {
report += "WALL";
} else if (grid.cells[nr][nc] == ENERGY) {
report += "ENERGY";
} else {
report += "clear";
}
report += "\n";
}
return report;
}
};
class EnergyScanner : public Sensor {
public:
EnergyScanner() : Sensor("Energy Scanner") {}
std::string scan(const Grid& grid, int robotRow, int robotCol) override {
int count = 0;
int range = 2;
for (int r = robotRow - range; r <= robotRow + range; r++) {
for (int c = robotCol - range; c <= robotCol + range; c++) {
if (grid.inBounds(r, c) && grid.cells[r][c] == ENERGY) {
count++;
}
}
}
return "--- Energy Scanner ---\n Energy cells within range: " + std::to_string(count) + "\n";
}
};
int main() {
srand(static_cast<unsigned>(time(nullptr)));
const int GRID_ROWS = 7;
const int GRID_COLS = 7;
const int TOTAL_ENERGY = 5;
Grid grid(GRID_ROWS, GRID_COLS);
grid.placeEnergy(TOTAL_ENERGY);
int startRow = GRID_ROWS / 2;
int startCol = GRID_COLS / 2;
Robot robot(startRow, startCol);
grid.cells[startRow][startCol] = ROBOT;
ProximitySensor proxSensor;
EnergyScanner energySensor;
std::string input;
std::cout << "=== ROBOT SIMULATOR ===\n";
std::cout << "Collect all " << TOTAL_ENERGY << " energy cells to win!\n";
std::cout << "Type 'help' for commands.\n\n";
while (true) {
std::cout << "[Energy: " << robot.energyCollected << "/" << TOTAL_ENERGY << "]\n";
grid.display();
std::cout << "\nCommand: ";
std::getline(std::cin, input);
if (input == "quit") {
std::cout << "Shutting down robot. Goodbye!\n";
break;
} else if (input == "help") {
std::cout << "Commands: north, south, east, west, scan, status, quit\n";
} else if (input == "status") {
std::cout << "Energy collected: " << robot.energyCollected << "/" << TOTAL_ENERGY << "\n";
std::cout << "Position: (" << robot.row << ", " << robot.col << ")\n";
} else if (input == "scan") {
std::cout << proxSensor.scan(grid, robot.row, robot.col);
std::cout << energySensor.scan(grid, robot.row, robot.col);
} else if (input == "north" || input == "south" || input == "east" || input == "west") {
robot.move(input, grid);
} else {
std::cout << "Unknown command. Type 'help' for options.\n";
}
if (robot.energyCollected == TOTAL_ENERGY) {
std::cout << "\n*** MISSION COMPLETE! All energy cells collected! ***\n";
grid.display();
break;
}
std::cout << "\n";
}
return 0;
}
Your robot simulator is now a complete, playable game. You have classes collaborating through method calls, a sensor inheritance hierarchy with polymorphism, and a game loop that processes commands until a win condition triggers. Next up, the Secret Mission will challenge you to extend this architecture with obstacles and a damage mechanic.
Secret mission
Add Obstacles and a Danger Mechanic
Your robot world is too safe. Real robots face hazards. Add obstacles to the grid that damage the robot's hull when it crashes into them. If health drops to zero, game over.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for future reference or delete them entirely.
Resources you used:
- Project folder containing robot_sim.cpp and the compiled robot_sim binary.
Keep everything running
No action needed. Choose this if you want to keep experimenting with the simulator, adding new features, or referencing the code later.
- Your source file and compiled binary stay on your Desktop.
- You can recompile any time with g++ robot_sim.cpp -o robot_sim and run with ./robot_sim.
Pause - I'll come back to this later
Since this is a local project with no running processes, there is nothing to pause. Your files sit idle on disk until you return.
- No background services or servers are running.
- When you come back, open the folder in VS Code and recompile to pick up where you left off.
Delete - I don't want to use this again
Remove the project folder and all its contents to start fresh.
- In Git Bash, navigate to your Desktop and delete the project folder by running:
cd ~/Desktop
rm -rf robot_sim.cpp robot_sim
If you placed your files inside a named project folder, delete the entire folder instead:
cd ~/Desktop
rm -rf your-project-folder-name
- Confirm the files are gone by running ls in the same directory. You should no longer see robot_sim.cpp or robot_sim in the output.
Nice Work!
Nice Work!
You just built a fully playable command-line robot simulator in C++. From a blank file to a class hierarchy with polymorphic sensors, a 2D grid world, and an interactive game loop, this is serious architectural thinking.
You've learned how to:
- Build a Grid class using a 2D std::vector to represent a tile world, placing and tracking objects at specific positions.
- Design a Sensor inheritance hierarchy with a pure virtual base class and two specialized derived classes (ProximitySensor and EnergyScanner) that override the same interface to produce different behaviour.
- Implement a command-parsing game loop that reads input, updates state, and renders output. This is the same control-loop pattern used in robotics firmware and game engines.
- Secret Mission: Added obstacles and a health/damage system, extending the enum and class hierarchy to introduce a danger mechanic.
Ready to quiz yourself?