In this post, we’ll walk through the implementation of a lightweight terminal-based tile game inspired by 2048. We'll explore the logic, algorithms, color display in the terminal, and movement mechanics—all crafted in pure Python.
🧩 Overview of What We're Building
- A grid-based game where tiles with the same number can be merged.
- Navigation using WASD keys.
- Auto-generated new tiles after each move.
- A colorful terminal UI for better visibility.
- Game-ending logic upon reaching 2048 or running out of moves.
🚀 Features at a Glance
- Pure Python, no external libraries required.
- Uses ANSI color codes for terminal coloring.
- Compatible with both Windows and Unix-based systems.
- Modular code design using classes, enums, and functions.
- Supports extensibility and customizations.
🧠 2048 Summary
- Each round, the player can move tiles in one of four directions.
- If two adjacent tiles with the same value meet, they merge.
- A new tile is randomly generated in an empty space after each move.
- The game ends when there are no valid moves or a tile reaches 2048.
Code Structure and Explanations
📦 1. Imports
import enum
import random
We use enum
for cleaner state and action management, and random
to add new tiles.
🔡 2. Cross-platform Character Input
def getch() -> str:
"""Gets a single character"""
try:
import msvcrt
return str(msvcrt.getch().decode("utf-8")) # type: ignore
except ImportError:
import sys
import termios
import tty
fd = sys.stdin.fileno()
oldsettings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, oldsettings)
return ch
- On Windows, it uses
msvcrt
. - On Unix-based systems, it uses
termios
andtty
.
This allows us to read single-character inputs like w
, a
, etc., in real-time.
🚦 3. Game State and Actions
class State(enum.Enum):
BLOCK = enum.auto()
WALL = enum.auto()
CONTINUE = enum.auto()
END = enum.auto()
class Action(enum.Enum):
MOVE_DOWN = "down"
MOVE_LEFT = "left"
MOVE_RIGHT = "right"
MOVE_UP = "up"
EXIT = "exit"
NOTHING = "nothing"
We define possible cell states (like wall or end) and player actions (directional movement).
🎨 4. Terminal Coloring
def rgb_bg(r: int, g: int, b: int) -> str:
return f"\033[48;2;{r};{g};{b}m"
def rgb(r: int, g: int, b: int) -> str:
return f"\033[38;2;{r};{g};{b}m"
def reset() -> str:
return "\033[0m"
These helper functions generate ANSI escape codes for colored text and backgrounds.
🌈 5. Tile Colors by Value
value_color = {
2: f"{rgb(0, 0, 0)}{rgb_bg(255, 255, 255)}",
4: f"{rgb(0, 0, 0)}{rgb_bg(150, 150, 255)}",
8: f"{rgb(0, 0, 0)}{rgb_bg(100, 255, 100)}",
16: f"{rgb(0, 0, 0)}{rgb_bg(255, 100, 100)}",
32: f"{rgb(0, 0, 0)}{rgb_bg(150, 150, 150)}",
64: f"{rgb(0, 0, 0)}{rgb_bg(200, 150, 100)}",
128: f"{rgb(0, 0, 0)}{rgb_bg(255,182,193)}",
256: f"{rgb(0, 0, 0)}{rgb_bg(255, 255, 0)}",
512: f"{rgb(0, 0, 0)}{rgb_bg(100, 250, 250)}",
1024: f"{rgb(0, 0, 0)}{rgb_bg(255, 0, 255)}",
2048: f"{rgb(0, 0, 0)}{rgb_bg(255, 99, 71)}",
}
Each tile value has a color combo, improving visual feedback in the terminal. This maps values (like 2, 4, 8...) to specific foreground/background color combinations using ANSI escape sequences.
🧱 6. The Cell Class
class Cell:
def __init__(self, state: State = State.BLOCK, value: int = 0) -> None:
self.state: State = state
self.value: int = value
self.down: "Cell"
self.up: "Cell"
self.right: "Cell"
self.left: "Cell"
def __str__(self) -> str:
match self:
case self if self.state == State.WALL:
return "🔹"
case self if self.value:
value = str(self.value).center(6)[2:4]
return f"{value_color[self.value]}{value}{reset()}"
case self if self.right.value:
value = str(self.right.value).center(6)[:2]
return f"{value_color[self.right.value]}{value}{reset()}"
case self if self.left.value:
value = str(self.left.value).center(6)[4:]
return f"{value_color[self.left.value]}{value}{reset()}"
margin_value = (
self.down.value
or self.up.value
or (getattr(self.down.left, "value") if hasattr(self.down, "left") else 0)
or (getattr(self.down.right, "value") if hasattr(self.down, "right") else 0)
or (getattr(self.up.right, "value") if hasattr(self.up, "right") else 0)
or int(getattr(self.up.left, "value") if hasattr(self.up, "left") else 0)
)
if margin_value:
return f"{value_color[int(margin_value)]} {reset()}"
return f"{rgb_bg(0, 0, 0)} {reset()}"
Each cell knows:
- Its
state
(block
,wall
, etc.) - Its numeric value (used in merging)
- References to its four neighbors (
left
,right
,up
,down
) -
__str__
is overridden to print the cell with colors and context-aware visuals.
🗺️ 7. The Board Class
class Board:
def __init__(self, size: int) -> None:
self.size = size * 3 + 2
self.player_state: State = State.CONTINUE
self.cells: list[list[Cell]]
self.valuable_cells: list[Cell]
self.set_initial()
def set_initial(self) -> None:
self.set_cells()
self.set_walls()
self.set_cells_neighboring()
self.set_valuable_cells()
self.set_init_value_cell()
def set_cells(self) -> None:
self.cells = [[Cell() for _ in range(self.size)] for _ in range(self.size)]
def set_walls(self) -> None:
for i in range(self.size):
for j in [0, self.size - 1]:
self.cells[i][j].state = State.WALL
self.cells[j][i].state = State.WALL
def set_cells_neighboring(self) -> None:
for i in range(1, self.size - 1):
for j in range(1, self.size - 1):
self.cells[i][j].left = self.cells[i][j - 1]
self.cells[i][j].right = self.cells[i][j + 1]
self.cells[i][j].up = self.cells[i - 1][j]
self.cells[i][j].down = self.cells[i + 1][j]
def set_valuable_cells(self) -> None:
self.valuable_cells = [
self.cells[i][j]
for i in range(2, self.size, 3)
for j in range(2, self.size, 3)
]
def set_init_value_cell(self) -> None:
self.valuable_cells[0].value = 2
self.valuable_cells[-3].value = 2
def take(self, ch: str) -> None:
for _ in range(int(pow(len(self.valuable_cells), 0.5))):
match ch:
case " ":
self.player_state = State.END
case "w":
self.move(Action.MOVE_UP.value)
case "s":
self.move(Action.MOVE_DOWN.value)
case "a":
self.move(Action.MOVE_LEFT.value)
case "d":
self.move(Action.MOVE_RIGHT.value)
new_valuable_cell = list(filter(lambda c: not c.value, self.valuable_cells))
if new_valuable_cell:
random.choice(new_valuable_cell).value = 2
else:
self.player_state = State.END
def move(self, direction: str) -> None:
for c in self.valuable_cells:
if getattr(getattr(c, direction), direction).state == State.BLOCK:
d_cell = getattr(getattr(getattr(c, direction), direction), direction)
if d_cell.value == c.value:
d_cell.value += c.value
c.value = 0
elif c.value and not d_cell.value:
d_cell.value = c.value
c.value = 0
if d_cell.value == 2048:
self.player_state = State.END
break
def __str__(self) -> str:
return "\n".join(["".join(map(str, rows)) for rows in self.cells])
This handles the entire game board logic. The board is constructed as a 2D grid with padded walls.
Methods:
-
set_initial
: Sets up the board by calling the other setup methods. -
set_cells
: Creates all cells. -
set_walls
: Adds walls to edges. -
set_cells_neighboring
: Links each cell to its 4 neighbors. -
set_valuable_cells
: Marks specific cells on the grid as "valuable" cells — meaning the player can place or merge tile values (like 2, 4, 8, etc.) only in these cells. -
set_init_value_cell
: Adds the first two cells with value 2. -
take(ch)
:- Takes user input (
w
,a
,s
,d
, orspace
). - Applies movement based on input.
- Spawns a new random 2 tile if there's space.
- Ends the game if no space.
- Takes user input (
-
move
:- Moves all cells in the given direction by checking and merging cells. Merging is allowed only if the destination cell is the same value.
- If 2048 is achieved, the game ends.
🎮 8. The Game Loop
def run() -> None:
board = Board(4)
print(f"\033[H\033[J{board}")
while board.player_state != State.END:
board.take(getch())
print(f"\033[H\033[J{board}")
The run()
function drives the game:
- Initializes the board.
- Clears and redraws the screen on each move.
- Takes input using
getch()
.
🧪 Try it Yourself!
This program is built for the Python terminal environment using only Python’s standard libraries. It does not depend on any external libraries and is compatible with Python 3.10 and above.
For the complete code, please visit the “oyna” project and then go to the “twenty_forty_eight_2048” section to view the code.
You can run the script by saving it as grid_base.py or download grid_base.py file and executing:
python grid_base.py
Use w
, a
, s
, d
to move the tiles. Press SPACE
to exit.
🔧 Want to Contribute?
The Oyna project is a fun starting point! You can improve or expand it by:
- Adding undo functionality.
- Implementing scoring.
- Building a GUI version (Tkinter, Pygame, etc.).
- Creating an AI to play it.
- Supporting custom board sizes or patterns.
Feel free to fork it and submit a pull request. Let’s make it awesome together! 🚀