Introduction
2048 is an extremely popular and easy-to-learn game that has taken the world by storm. If you haven't played it yet, you can download it on your mobile phone to give it a try. This project will guide you in using HTML, CSS, JavaScript, and jQuery to create a web version of the 2048 game.
- Learn the development process of a web application
- Explore how to make the application responsive on mobile devices to accommodate screens of various sizes and handle layout and initialization
- Utilize JavaScript and jQuery to write the game's logic, implement block movement, and determine game outcomes.
👀 Preview

🎯 Tasks
In this project, you will learn:
- How to create the page layout of a 2048 web game using HTML and CSS
- How to implement the game logic in JavaScript and jQuery
- How to handle block movement and merging of blocks
- How to test and run the web game in a web browser
🏆 Achievements
After completing this project, you will be able to:
- Develop a responsive web application for the 2048 game
- Utilize JavaScript and jQuery to write the game's logic and functionality
- Implement block movement and merging in the game
- Test and run a web game in a web browser
Development Preparation
Setting aside the page style, the 2048 game can be abstracted as a two-dimensional array. In the initial state, two numbers need to be randomly generated. The randomly generated numbers can only be 2 or 4. In the following implementation, we will make the occurrence probability of 2 and 4 equal. Of course, you can also make the probability of 4 appearing smaller.
0 0 0 0
2 0 0 0
0 0 2 0
Next, when I press a directional key, the numbers will move in that direction and adjacent cells with the same number will be merged. Then, another number will be randomly generated. For example, if we press Up → Left → Up, the interface may become:
2 0 2 0
0 0 0 0
0 0 0 0
4 0 0 0
4 0 0 0
0 0 0 0
0 0 2 0
4 0 0 0
8 0 2 0
0 0 0 0
0 4 0 0
In this way, we continuously move and merge numbers until numbers can no longer be merged or until 2048 appears, ending the game. As you can see, the core of the game is manipulating this two-dimensional array.
Project File Structure
First, we need to create the following file structure under the ~/project path:
~/project
|__ index.html
|__ main.js
|__ support.js
|__ showanimation.js
|__ style.css
jquery.min.js is already placed under the ~/project path, so you can use it directly.
Now let's officially start our coding tasks! We will begin with the page layout and gradually complete modules such as initializing the chessboard and moving the number blocks.
Page Layout
As you may have observed, the 2048 game is built on 16 squares. Below, we will use div + css to draw these 4X4 cells. Please add the following code to the index.html file:
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>2048 Game</title>
<link rel="stylesheet" href="style.css" />
<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="main.js"></script>
<script type="text/javascript" src="showanimation.js"></script>
<script type="text/javascript" src="support.js"></script>
</head>
<body>
<header>
<h1>2048 Game</h1>
<a href="javascript:new_game();" id="new_game_button">New Game</a>
<p>score: <span id="score">0</span></p>
</header>
<div id="grid_container">
<div class="grid_cell" id="grid_cell_0_0"></div>
<div class="grid_cell" id="grid_cell_0_1"></div>
<div class="grid_cell" id="grid_cell_0_2"></div>
<div class="grid_cell" id="grid_cell_0_3"></div>
<div class="grid_cell" id="grid_cell_1_0"></div>
<div class="grid_cell" id="grid_cell_1_1"></div>
<div class="grid_cell" id="grid_cell_1_2"></div>
<div class="grid_cell" id="grid_cell_1_3"></div>
<div class="grid_cell" id="grid_cell_2_0"></div>
<div class="grid_cell" id="grid_cell_2_1"></div>
<div class="grid_cell" id="grid_cell_2_2"></div>
<div class="grid_cell" id="grid_cell_2_3"></div>
<div class="grid_cell" id="grid_cell_3_0"></div>
<div class="grid_cell" id="grid_cell_3_1"></div>
<div class="grid_cell" id="grid_cell_3_2"></div>
<div class="grid_cell" id="grid_cell_3_3"></div>
</div>
</body>
</html>
Next, we need to add styles to the page and each cell. Please add the following code to the style.css file:
body {
padding: 50px 0px;
}
header {
display: block;
margin: 0 auto;
width: 100%;
text-align: center;
}
header h1 {
font-family: Arial;
font-size: 40px;
font-weight: bold;
margin: 0 auto;
color: #776e65;
padding: 20px 0px;
}
header #new_game_button {
display: block;
margin: 0px auto;
width: 100px;
padding: 10px 10px;
background-color: #8f7a66;
font-family: Arial;
color: white;
border-radius: 10px;
text-decoration: none;
}
header #new_game_button:hover {
background-color: #9f8b77;
}
header p {
font-family: Arial;
font-size: 25px;
margin: 5px auto;
}
#grid_container {
width: 460px;
height: 460px;
padding: 20px;
margin: 0px auto;
background-color: #bbada0;
border-radius: 10px;
position: relative;
}
.grid_cell {
width: 100px;
height: 100px;
border-radius: 6px;
background-color: #ccc0b3;
position: absolute;
}
.number_cell {
border-radius: 6px;
font-family: Arial;
font-weight: bold;
font-size: 60px;
line-height: 100px;
text-align: center;
position: absolute;
}
After completing this step, open the index.html file using Preview,

and we should see the following result:

Initializing the Chessboard
With the increasing popularity of mobile devices, we need to handle adaptability to various screen sizes.
At the beginning of the game, we need to generate two random numbers on the chessboard. This is achieved through JavaScript code.
In main.js, add the following:
var board = new Array(); // The numbers in each cell
var score = 0; // The score
var has_conflicted = new Array(); // Flag for resolving consecutive eliminations
var startx = 0; // x-coordinate of the starting point when touching the mobile screen
var starty = 0; // y-coordinate of the starting point when touching the mobile screen
var endx = 0; // x-coordinate of the ending point when touching the mobile screen
var endy = 0; // y-coordinate of the ending point when touching the mobile screen
var success_string = "Success";
var gameover_string = "GameOver";
// Initialize the chessboard after the HTML document has loaded
$(document).ready(function () {
// Handle adaptability
prepare_for_mobile();
new_game();
});
// Start a new game
function new_game() {
// Initialize the chessboard
init();
// Generate numbers in two random cells
generate_one_number();
}
// Initialize
function init() {
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
var grid_cell = $("#grid_cell_" + i + "_" + j);
grid_cell.css("top", get_pos_top(i, j));
grid_cell.css("left", get_pos_left(i, j));
}
}
for (var i = 0; i < 4; i++) {
board[i] = new Array();
has_conflicted[i] = new Array();
for (var j = 0; j < 4; j++) {
board[i][j] = 0;
has_conflicted[i][j] = false;
}
}
update_board_view();
score = 0;
update_score(score);
}
// Update the chessboard view
function update_board_view() {
$(".number_cell").remove();
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
$("#grid_container").append(
'<div class="number_cell" id="number_cell_' + i + "_" + j + '"></div>'
);
var number_cell = $("#number_cell_" + i + "_" + j);
if (board[i][j] == 0) {
number_cell.css("width", "0px");
number_cell.css("height", "0px");
number_cell.css("top", get_pos_top(i, j) + cell_side_length / 2);
number_cell.css("left", get_pos_left(i, j) + cell_side_length / 2);
} else {
number_cell.css("width", cell_side_length);
number_cell.css("height", cell_side_length);
number_cell.css("top", get_pos_top(i, j));
number_cell.css("left", get_pos_left(i, j));
number_cell.css(
"background-color",
get_number_background_color(board[i][j])
);
number_cell.css("color", get_number_color(board[i][j]));
number_cell.text(board[i][j]);
}
has_conflicted[i][j] = false;
}
}
$(".number_cell").css("line-height", cell_side_length + "px");
$(".number_cell").css("font-size", 0.6 * cell_side_length + "px");
}
// Generate a number in a random cell
function generate_one_number() {
if (nospace(board)) {
return false;
}
// Random position
var randx = parseInt(Math.floor(Math.random() * 4));
var randy = parseInt(Math.floor(Math.random() * 4));
var time = 0;
while (time < 50) {
if (board[randx][randy] == 0) {
break;
}
randx = parseInt(Math.floor(Math.random() * 4));
randy = parseInt(Math.floor(Math.random() * 4));
time++;
}
if (time == 50) {
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
if (board[i][j] == 0) {
randx = i;
randy = j;
}
}
}
}
// Random number
var rand_number = Math.random() < 0.5 ? 2 : 4;
// Display the random number in the random position
board[randx][randy] = rand_number;
show_number_with_animation(randx, randy, rand_number);
return true;
}
// Handle adaptability
function prepare_for_mobile() {
if (document_width > 500) {
grid_container_width = 500;
cell_side_length = 100;
cell_space = 20;
}
$("#grid_container").css("width", grid_container_width - 2 * cell_space);
$("#grid_container").css("height", grid_container_width - 2 * cell_space);
$("#grid_container").css("padding", cell_space);
$("#grid_container").css("border-radius", 0.02 * grid_container_width);
$(".grid_cell").css("width", cell_side_length);
$(".grid_cell").css("height", cell_side_length);
$(".grid_cell").css("border-radius", 0.02 * grid_container_width);
}
Improve Game Logic
Next, we need to improve the logic of the game, including moving number tiles and checking if the game is over. Complete the following code in support.js:
document_width = window.screen.availWidth; // The width of the screen
grid_container_width = 0.92 * document_width; // The width of the game board
cell_side_length = 0.18 * document_width; // The size of each grid cell
cell_space = 0.04 * document_width; // The space between each grid cell
// Get the distance of the corresponding grid cell from the top of the game board
function get_pos_top(i, j) {
return cell_space + i * (cell_space + cell_side_length);
}
// Get the distance of the corresponding grid cell from the left side of the game board
function get_pos_left(i, j) {
return cell_space + j * (cell_space + cell_side_length);
}
// Get the background color of the corresponding number
function get_number_background_color(number) {
switch (number) {
case 2:
return "#eee4da";
break;
case 4:
return "#ede0c8";
break;
case 8:
return "#f2b179";
break;
case 16:
return "#f59563";
break;
case 32:
return "#f67c5f";
break;
case 64:
return "#f65e3b";
break;
case 128:
return "#edcf72";
break;
case 256:
return "#edcc61";
break;
case 512:
return "#9c0";
break;
case 1024:
return "#33b5e5";
break;
case 2048:
return "#09c";
break;
case 4096:
return "#a6c";
break;
case 8192:
return "#93c";
break;
}
return "black";
}
// Get the color of the corresponding number
function get_number_color(number) {
if (number <= 4) return "#776e65";
return "white";
}
// Check if there are any empty grid cells on the game board
function nospace(board) {
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
if (board[i][j] == 0) {
return false;
}
}
}
return true;
}
Enhancing Animation Effects
Next, we need to enhance the animation effects, including displaying number tiles and updating the score. This code is all located in showanimation.js:
// Animation for displaying number tiles
function show_number_with_animation(i, j, rand_number) {
var number_cell = $("#number_cell_" + i + "_" + j);
number_cell.css("background-color", get_number_background_color(rand_number));
number_cell.css("color", get_number_color(rand_number));
number_cell.text(rand_number);
number_cell.animate(
{
width: cell_side_length,
height: cell_side_length,
top: get_pos_top(i, j),
left: get_pos_left(i, j)
},
50
);
}
// Updating the score
function update_score(score) {
$("#score").text(score);
}
Move Number Tiles
After completing the layout and initialization, we will now implement the functionality to move and eliminate number tiles until the game succeeds or fails.
Add the following code to main.js:
// Listen for keyboard arrow key movements
$(document).keydown(function (event) {
if ($("#score").text() == success_string) {
new_game();
return;
}
switch (event.keyCode) {
case 37: // Left
event.preventDefault();
if (move_left()) {
setTimeout("generate_one_number()", 210);
setTimeout("is_gameover()", 300);
}
break;
case 38: // Up
event.preventDefault();
if (move_up()) {
setTimeout("generate_one_number()", 210);
setTimeout("is_gameover()", 300);
}
break;
case 39: // Right
event.preventDefault();
if (move_right()) {
setTimeout("generate_one_number()", 210);
setTimeout("is_gameover()", 300);
}
break;
case 40: // Down
event.preventDefault();
if (move_down()) {
setTimeout("generate_one_number()", 210);
setTimeout("is_gameover()", 300);
}
break;
default:
break;
}
});
// Listen for touchstart event on mobile devices
document.addEventListener("touchstart", function (event) {
startx = event.touches[0].pageX;
starty = event.touches[0].pageY;
});
// Listen for touchmove event on mobile devices
document.addEventListener("touchmove", function (event) {
event.preventDefault();
});
// Listen for touchend event on mobile devices
document.addEventListener("touchend", function (event) {
endx = event.changedTouches[0].pageX;
endy = event.changedTouches[0].pageY;
var deltax = endx - startx;
var deltay = endy - starty;
if (
Math.abs(deltax) < 0.3 * document_width &&
Math.abs(deltay) < 0.3 * document_width
) {
return;
}
if ($("#score").text() == success_string) {
new_game();
return;
}
// x-axis movement
if (Math.abs(deltax) >= Math.abs(deltay)) {
if (deltax > 0) {
// Move right
if (move_right()) {
setTimeout("generate_one_number()", 210);
setTimeout("is_gameover()", 300);
}
} else {
// Move left
if (move_left()) {
setTimeout("generate_one_number()", 210);
setTimeout("is_gameover()", 300);
}
}
} else {
// y-axis movement
if (deltay > 0) {
// Move down
if (move_down()) {
setTimeout("generate_one_number()", 210);
setTimeout("is_gameover()", 300);
}
} else {
// Move up
if (move_up()) {
setTimeout("generate_one_number()", 210);
setTimeout("is_gameover()", 300);
}
}
}
});
// Move left
function move_left() {
if (!can_move_left(board)) {
return false;
}
// Move left
for (var i = 0; i < 4; i++) {
for (var j = 1; j < 4; j++) {
if (board[i][j] != 0) {
for (var k = 0; k < j; k++) {
if (board[i][k] == 0 && no_block_horizontal(i, k, j, board)) {
show_move_animation(i, j, i, k);
board[i][k] = board[i][j];
board[i][j] = 0;
break;
} else if (
board[i][k] == board[i][j] &&
no_block_horizontal(i, k, j, board) &&
!has_conflicted[i][k]
) {
show_move_animation(i, j, i, k);
board[i][k] += board[i][j];
board[i][j] = 0;
// Add score
score += board[i][k];
update_score(score);
has_conflicted[i][k] = true;
break;
}
}
}
}
}
setTimeout("update_board_view()", 200);
return true;
}
// Move right
function move_right() {
if (!can_move_right(board)) {
return false;
}
// Move right
for (var i = 0; i < 4; i++) {
for (var j = 2; j >= 0; j--) {
if (board[i][j] != 0) {
for (var k = 3; k > j; k--) {
if (board[i][k] == 0 && no_block_horizontal(i, j, k, board)) {
show_move_animation(i, j, i, k);
board[i][k] = board[i][j];
board[i][j] = 0;
break;
} else if (
board[i][k] == board[i][j] &&
no_block_horizontal(i, j, k, board) &&
!has_conflicted[i][k]
) {
show_move_animation(i, j, i, k);
board[i][k] += board[i][j];
board[i][j] = 0;
// Add score
score += board[i][k];
update_score(score);
has_conflicted[i][k] = true;
break;
}
}
}
}
}
setTimeout("update_board_view()", 200);
return true;
}
// Move up
function move_up() {
if (!can_move_up(board)) {
return false;
}
// Move up
for (var j = 0; j < 4; j++) {
for (var i = 1; i < 4; i++) {
if (board[i][j] != 0) {
for (var k = 0; k < i; k++) {
if (board[k][j] == 0 && no_block_vertical(j, k, i, board)) {
show_move_animation(i, j, k, j);
board[k][j] = board[i][j];
board[i][j] = 0;
break;
} else if (
board[k][j] == board[i][j] &&
no_block_vertical(j, k, i, board) &&
!has_conflicted[k][j]
) {
show_move_animation(i, j, k, j);
board[k][j] += board[i][j];
board[i][j] = 0;
// Add score
score += board[k][j];
update_score(score);
has_conflicted[k][j] = true;
break;
}
}
}
}
}
setTimeout("update_board_view()", 200);
return true;
}
// Move down
function move_down() {
if (!can_move_down(board)) {
return false;
}
// Move down
for (var j = 0; j < 4; j++) {
for (var i = 2; i >= 0; i--) {
if (board[i][j] != 0) {
for (var k = 3; k > i; k--) {
if (board[k][j] == 0 && no_block_vertical(j, i, k, board)) {
show_move_animation(i, j, k, j);
board[k][j] = board[i][j];
board[i][j] = 0;
break;
} else if (
board[k][j] == board[i][j] &&
no_block_vertical(j, i, k, board) &&
!has_conflicted[k][j]
) {
show_move_animation(i, j, k, j);
board[k][j] += board[i][j];
board[i][j] = 0;
// Add score
score += board[k][j];
update_score(score);
has_conflicted[k][j] = true;
break;
}
}
}
}
}
setTimeout("update_board_view()", 200);
return true;
}
// Check if the game is successful or failed
function is_gameover() {
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
if (board[i][j] == 2048) {
update_score(success_string);
return;
}
}
}
if (nospace(board) && nomove(board)) {
gameover();
}
}
// Update the game over text when the game ends
function gameover() {
update_score(gameover_string);
}
Improve support.js
Next, we need to improve the code in support.js, including checking whether it can move or still can move.
// Check if can move left
function can_move_left(board) {
for (var i = 0; i < 4; i++) {
for (var j = 1; j < 4; j++) {
if (board[i][j] != 0) {
if (board[i][j - 1] == 0 || board[i][j] == board[i][j - 1]) {
return true;
}
}
}
}
return false;
}
// Check if can move right
function can_move_right(board) {
for (var i = 0; i < 4; i++) {
for (var j = 2; j >= 0; j--) {
if (board[i][j] != 0) {
if (board[i][j + 1] == 0 || board[i][j] == board[i][j + 1]) {
return true;
}
}
}
}
return false;
}
// Check if can move up
function can_move_up(board) {
for (var j = 0; j < 4; j++) {
for (var i = 1; i < 4; i++) {
if (board[i][j] != 0) {
if (board[i - 1][j] == 0 || board[i - 1][j] == board[i][j]) {
return true;
}
}
}
}
return false;
}
// Check if can move down
function can_move_down(board) {
for (var j = 0; j < 4; j++) {
for (var i = 2; i >= 0; i--) {
if (board[i][j] != 0) {
if (board[i + 1][j] == 0 || board[i + 1][j] == board[i][j]) {
return true;
}
}
}
}
return false;
}
// Check if there are no blocks in the horizontal direction
function no_block_horizontal(row, col1, col2, board) {
for (var i = col1 + 1; i < col2; i++) {
if (board[row][i] != 0) {
return false;
}
}
return true;
}
// Check if there are no blocks in the vertical direction
function no_block_vertical(col, row1, row2, board) {
for (var i = row1 + 1; i < row2; i++) {
if (board[i][col] != 0) {
return false;
}
}
return true;
}
// Check if it can still move
function nomove(board) {
if (
can_move_down(board) ||
can_move_up(board) ||
can_move_right(board) ||
can_move_left(board)
) {
return false;
}
return true;
}
Complete the Game
Finally, we need to complete the code in showanimation.js, including displaying the move animation and merge animation.
// Animation effect when a grid moves
function show_move_animation(fromx, fromy, tox, toy) {
var number_cell = $("#number_cell_" + fromx + "_" + fromy);
number_cell.animate(
{
top: get_pos_top(tox, toy),
left: get_pos_left(tox, toy)
},
200
);
}
With this, our web version of 2048 is complete.
Running and Testing
Open index.html in a web browser.

To see the following effects, click on Go Live button in the bottom right corner of WebIDE, and switch to the "Web 8080" tab.

Summary
In this project, we have implemented a web version of the 2048 game using HTML, CSS, JavaScript, and jQuery. We have also learned how to make it compatible with mobile devices. It is believed that through this project, you can deepen your understanding of front-end technology and improve your comprehensive application skills.



