Ship It to the Browser: 2048 as a Web App
Explore how to convert your Python 2048 game engine into a browser-based web app by translating core game functions into JavaScript. Learn to implement game logic, handle user input, render the board, and manage game states. Understand the AI-assisted workflow that helps scaffold the frontend code while preserving your engine architecture. By the end, you'll have a playable web version of 2048 and insights into modular design and web development integration.
By the end of this lesson, you will:
Treat your Python 2048 code as a game engine spec.
Use a carefully-designed AI prompt to translate the logic into vanilla JavaScript.
Build a single-file HTML web app that:
Implements the same 4×4 board logic,
Maps
W/A/S/Dand the arrow keys to moves,Looks and feels like the classic 2048 game.
Run and tweak your game in the browser using a sandpack HTML/JS environment.
Reflect on what you handed to AI (architecture, contracts) vs. what AI handled (boilerplate, DOM, CSS).
Step 0: Your starter code
From the previous lesson, you now have a clean, modular Python engine:
Board: 4×4 list of lists of ints, 0 means empty.
Core functions:
create_initial_board()add_random_tile(board)move_board(board, direction) → (new_board, gained_score, moved)check_game_state(board, target) → 'ongoing' | 'won' | 'lost'
Helpers:
compress_row,merge_row,move_left,transpose,reverse_rows
Constants:
TARGET_TILE = 2048KEY_TO_DIRECTION = {"w": "up", "a": "left", "s": "down", "d": "right"}
import random
def add_random_tile(board: list[list[int]]) -> None:
empty_positions = [
(r, c)
for r in range(4)
for c in range(4)
if board[r][c] == 0
]
if not empty_positions:
return
r, c = random.choice(empty_positions)
board[r][c] = 4 if random.random() < 0.1 else 2
def create_initial_board() -> list[list[int]]:
board = [[0] * 4 for _ in range(4)]
add_random_tile(board)
add_random_tile(board)
return board
def render_board(board: list[list[int]]) -> str:
size = len(board)
cell_width = 5
line = "+" + ("-" * cell_width + "+") * size
lines = [line]
for r in range(size):
row_text = "|"
for c in range(size):
val = board[r][c]
text = "" if val == 0 else str(val)
row_text += text.rjust(cell_width) + "|"
lines.append(row_text)
lines.append(line)
return "\n".join(lines)
def compress_row(row: list[int]) -> list[int]:
non_zero = [x for x in row if x != 0]
zeros_needed = len(row) - len(non_zero)
return non_zero + [0] * zeros_needed
def merge_row(row: list[int]) -> tuple[list[int], int]:
result = []
score = 0
i = 0
n = len(row)
while i < n:
if row[i] == 0:
break
if i + 1 < n and row[i] == row[i + 1] and row[i] != 0:
merged = 2 * row[i]
result.append(merged)
score += merged
i += 2
else:
result.append(row[i])
i += 1
while len(result) < n:
result.append(0)
return result, score
def transpose(board: list[list[int]]) -> list[list[int]]:
size = len(board)
return [[board[r][c] for r in range(size)] for c in range(size)]
def reverse_rows(board: list[list[int]]) -> list[list[int]]:
return [list(reversed(row)) for row in board]
def move_left(board: list[list[int]]) -> tuple[list[list[int]], int, bool]:
new_board = []
total_score = 0
for row in board:
c1 = compress_row(row)
m, gained = merge_row(c1)
c2 = compress_row(m)
new_board.append(c2)
total_score += gained
moved = new_board != board
return new_board, total_score, moved
def move_board(board: list[list[int]], direction: str) -> tuple[list[list[int]], int, bool]:
direction = direction.lower()
if direction == "left":
return move_left(board)
if direction == "right":
reversed_board = reverse_rows(board)
moved_board, score, moved = move_left(reversed_board)
return reverse_rows(moved_board), score, moved
if direction == "up":
transposed = transpose(board)
moved_board, score, moved = move_left(transposed)
return transpose(moved_board), score, moved
if direction == "down":
transposed = transpose(board)
reversed_board = reverse_rows(transposed)
moved_board, score, moved = move_left(reversed_board)
final_board = transpose(reverse_rows(moved_board))
return final_board, score, moved
return board, 0, False
def has_won(board: list[list[int]], target: int) -> bool:
for row in board:
for v in row:
if v >= target:
return True
return False
def has_any_moves(board: list[list[int]]) -> bool:
size = len(board)
# Any empty cell?
for row in board:
for v in row:
if v == 0:
return True
# Any equal horizontal neighbors?
for r in range(size):
for c in range(size - 1):
if board[r][c] == board[r][c + 1]:
return True
# Any equal vertical neighbors?
for r in range(size - 1):
for c in range(size):
if board[r][c] == board[r + 1][c]:
return True
return False
def check_game_state(board: list[list[int]], target: int = 2048) -> str:
if has_won(board, target):
return "won"
if has_any_moves(board):
return "ongoing"
return "lost"
def main():
print("=== Lesson 7: Game Over Logic ===")
board = create_initial_board()
score = 0
target = 2048
while True:
print("\nScore:", score)
print(render_board(board))
state = check_game_state(board, target=target)
if state == "won":
print("🎉 You reached", target, "! You win!")
break
if state == "lost":
print("💀 No more moves. Game over.")
break
command = input("Move (w/a/s/d, q to quit): ").strip().lower()
if command == "q":
print("Goodbye!")
break
key_to_direction = {
"w": "up",
"a": "left",
"s": "down",
"d": "right",
}
if command not in key_to_direction:
print("Invalid input. Use w/a/s/d or q.")
continue
direction = key_to_direction[command]
new_board, gained, moved = move_board(board, direction)
if not moved:
print("Board did not change. Try a different move.")
continue
board = new_board
score += gained
add_random_tile(board)
if __name__ == "__main__":
main()
In this lesson, you won’t modify the Python engine. Instead, you’ll ...