Programming in C++ CW2: 2048
1. Introduction
This project is a terminal-based implementation of the game 2048.
2. Implementation Details
-
Game Class (
Game.hpp
,Game.cpp
)-
How: All game states (board, score, size) and logic (moves, display, game over detection) are encapsulated within the
Game
class. Public methods provide the interface for interaction (e.g.,processInput
,displayBoard
), while private members and methods handle the internal state and helper logic. -
Why: Using a class follows object-oriented principles by grouping related data and functions together. This makes the code modular, easier to understand & maintain, and prevents polluting the global namespace. It clearly separates the core game engine from the main execution driver in
main.cpp
.
-
How: All game states (board, score, size) and logic (moves, display, game over detection) are encapsulated within the
-
Board Representation (
Game.hpp
)-
How: A
std::vector<std::vector<int>> board
is used as a private member variable within theGame
class to represent the game grid. Integer values store the tile numbers, with0
indicating an empty cell. -
Why:
std::vector
provides a standard, dynamic C++ container. A 2D vector is a natural fit for representing a grid, allowing intuitive access via row and column indices (board[r][c]
). Storing integers is sufficient for the tile values in 2048.
-
How: A
-
Initialization (
Game::Game
,initializeBoard
,spawnInitialTiles
inGame.cpp
)-
How: The
Game
constructor initializes the boardsize
andscore
. It then callsinitializeBoard()
to resize theboard
vector to the correct dimensions and fill it with zeros. Finally, it callsspawnInitialTiles()
, which in turn callsaddRandomTile()
twice to place the starting tiles. -
Why: This ensures every game starts with a clean, correctly sized empty board and the standard two initial random tiles. Breaking initialization into specific helper functions (
initializeBoard
,spawnInitialTiles
,addRandomTile
) makes the constructor cleaner and the initialization steps explicit and reusable.
-
How: The
-
Random Tile Generation (
addRandomTile
inGame.cpp
)-
How: This function first scans the board to find all cells containing
0
and stores their coordinates ({row, col}
) in astd::vector<std::pair<int, int>> emptyCells
. If no empty cells are found, it returns immediately. Otherwise, it uses C++'s<random>
library (std::random_device
,std::mt19937
,std::uniform_int_distribution
) to randomly select one of the empty cells. Another random distribution determines whether to place a2
(90% chance) or a4
(10% chance) in the selected cell. -
Why: This function implements a core game mechanic of 2048. Finding empty cells first is necessary to know where a tile can be placed. Using
<random>
ensures less predictable tile placement than older methods likerand()
. The 90/10 probability for 2s and 4s matches the behaviour of the original game. Returning early if no cells are empty prevents errors.
-
How: This function first scans the board to find all cells containing
-
Core Tile Merging Logic (
mergeRow
inGame.cpp
)-
How: This private helper function processes a single row (
std::vector<int>
) as if moving left.- It creates a temporary vector (
temp
) containing only the non-zero tile values from the input row, effectively sliding them left. - It iterates through
temp
. If two adjacent tiles are identical, they are merged into a single tile (value doubled) in the finalmergedRow
vector, thescore
is updated, and the second tile is skipped. Non-matching tiles are copied directly. - The
mergedRow
is padded with zeros on the right until it reaches the originalsize
.
- It creates a temporary vector (
- Why: This function encapsulates the fundamental slide-and-merge action. By creating this generic "merge left" function for a single row, it can be reused for all four move directions by manipulating the board orientation beforehand, significantly reducing code duplication. Updating the score here ensures points are awarded correctly at the moment of merging.
-
How: This private helper function processes a single row (
-
Directional Move Implementation (
moveLeft
,moveRight
,moveUp
,moveDown
inGame.cpp
)-
How:
-
moveLeft
: Iterates through each row of theboard
and replaces it with the result of callingmergeRow
on it. -
moveRight
: CallsreverseRows()
to reverse each row, then callsmoveLeft()
(which usesmergeRow
), then callsreverseRows()
again to restore the original orientation. -
moveUp
: CallstransposeBoard()
(swaps rows and columns), then callsmoveLeft()
, then callstransposeBoard()
again. -
moveDown
: CallstransposeBoard()
, then callsmoveRight()
(which itself uses reversing andmoveLeft
), then callstransposeBoard()
again.
-
-
Why: This strategy cleverly reuses the
mergeRow
logic for all directions. Instead of writing separate merge logic for up, down, and right, the board is temporarily transformed (reverseRows
,transposeBoard
) so that themoveLeft
logic achieves the desired effect. This is efficient and less error-prone than implementing four distinct merge algorithms.
-
How:
-
Board Transformation Helpers (
transposeBoard
,reverseRows
inGame.cpp
)-
How:
-
transposeBoard
: Creates a new temporary 2D vector. It copiesboard[r][c]
totransposed[c][r]
and then replaces the originalboard
with thetransposed
version. -
reverseRows
: Iterates through each row vector in theboard
and usesstd::reverse
(from<algorithm>
) to reverse the elements within that row in-place.
-
-
Why: These helper functions provide the necessary board manipulations (swapping rows/columns and reversing row contents) to allow the single
mergeRow
function (withinmoveLeft
) to handle all four movement directions.
-
How:
-
Input Processing (
processInput
inGame.cpp
)-
How: Takes the user's input character (
move
). It creates a copy of theboard
before processing the move. It converts the input to lowercase (tolower
) and uses aswitch
statement to call the corresponding directional move function (moveUp
,moveDown
, etc.). After the move function executes, it compares the currentboard
with the copy made earlier. If they are different (meaning the move changed the board state), it callsaddRandomTile()
. -
Why: This function acts as the bridge between user commands and game actions. Using
tolower
makes the controls case-insensitive for better usability. The crucial step is comparing the board state before and after the move; this prevents adding a new random tile if the player attempts a move that doesn't change anything (e.g., pushing tiles against a wall), which is essential to correct 2048 gameplay.
-
How: Takes the user's input character (
-
Display (
displayBoard
inGame.cpp
)-
How: Uses
system("clear")
(for Linux/macOS) to clear the terminal screen. Prints the currentscore
. Then, it iterates through the 2Dboard
vector, printing a simple text-based grid structure.std::setw(4)
(from<iomanip>
) is used to ensure numbers are aligned within cells. Cells containing 0 are printed as blank spaces. Finally, it prints the control key instructions. -
Why: To provide the player with a constantly updated visual representation of the game state (board layout and score). Clearing the screen avoids clutter from previous turns.
setw
creates a neater, more readable grid. Displaying controls reminds the user how to play.
-
How: Uses
-
Game Over Detection (
isGameOver
inGame.cpp
)-
How: First, it iterates through the entire board to check if any cell contains
0
. If an empty cell is found, the game cannot be over, so it immediately returnsfalse
. If the loop completes without finding empty cells (meaning the board is full), it proceeds to check for possible merges. It does this by checking adjacent pairs:board[r][c]
vsboard[r][c+1]
(horizontal) andboard[r][c]
vsboard[r+1][c]
(vertical). If any adjacent identical pair is found, a merge is possible, so the game is not over, and it returnsfalse
. If the board is full and no merges are possible, it returnstrue
. - Why: This correctly implements the win/loss condition for 2048. The game only ends when the player can no longer make any moves, which occurs precisely when the board is full and no adjacent tiles can be combined. Checking for empty cells first is a quick optimization.
-
How: First, it iterates through the entire board to check if any cell contains
-
Score Management (
score
member andgetScore
method)-
How: A private integer
score
is part of theGame
class, initialized to 0. It's incremented withinmergeRow
by the value of the newly formed tile whenever a merge occurs. A publicconst
methodgetScore()
provides read-only access to the score. -
Why: To track player performance, which is essential feedback in a game. Incrementing the score during the merge operation ensures it's updated accurately. Providing a
getScore
accessor method allows other parts of the program (likemain
ordisplayBoard
) to retrieve the score without being able to modify it directly, adhering to encapsulation principles.
-
How: A private integer
-
Main Game Loop (
main.cpp
)-
How: The
main
function creates aGame
object. It then enters awhile
loop that continues until the input character is 'q'. Inside the loop, it callsgame.displayBoard()
, checksgame.isGameOver()
, prompts for and reads user input usingstd::cin
, checks if the input is 'q' (to break the loop), validates if the input is one of the move keys ('w', 'a', 's', 'd'), and if valid, callsgame.processInput()
. If the game is over, it prints a "Game Over" message and the final score before breaking the loop. -
Why: This structure drives the gameplay. The loop ensures the cycle of display -> input -> update -> check state repeats until the game ends (quit or game over). Input validation prevents errors from unexpected characters. Separating this driver logic from the
Game
class keeps concerns separated.
-
How: The
-
File Structure (
.hpp
,.cpp
)-
How: The
Game
class declaration (member variables, method signatures) is placed ingame.hpp
. TheGame
class definitions (method implementations) are ingame.cpp
. The main execution code is inmain.cpp
. Bothgame.cpp
andmain.cpp
includegame.hpp
to access the class declaration. Header guards (#ifndef GAME_HPP
,#define GAME_HPP
,#endif
) are used ingame.hpp
. -
Why: This is the standard C++ approach for organizing code. It separates the interface (
.hpp
) from the implementation (.cpp
), improving modularity and readability. It also speeds up compilation, as changes to the implementation (.cpp
) don't require recompiling files that only include the header (.hpp
) if the header itself hasn't changed. Header guards prevent issues if the header is accidentally included multiple times in the same compilation unit.
-
How: The
3. How to Build and Run
This project was designed using - and designed to be built and run on - the csctcloud.uwe.ac.uk development environment.
Dependencies
- A C++ compiler supporting C++11 or later (like g++)
- Standard C++ libraries (
<iostream>
,<vector>
,<random>
,<iomanip>
,<algorithm>
,<string>
)
Building
-
Navigate to the directory containing the source and header files:
-
Build the game, including the source files, the name for the executable, and the version of C++ to use (C++11 and above contains the necessary libraries)
g++ main.cpp game.cpp -o 2048game -std=c++11
Running
-
Execute the compiled program:
./2048game
- Play the game: Use the W, A, S, D keys (followed by the Return key) to move the tiles. The game will handle incorrect inputs by ignoring them and clearing the input area. (Note: due to an issue I have been unable to fix, the game can take multiple key inputs in one go i.e. A, W, A, W, Return).
-
Quit: Enter
Q
to exit the game at any time.
4. Author
Kit Lloyd 23074402