2048 Web Game Using jQuery

CSSCSSBeginner
Practice Now

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,

Alt text

and we should see the following result:

Alt text
âœĻ Check Solution and Practice

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);
}
âœĻ Check Solution and Practice

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;
}
âœĻ Check Solution and Practice

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);
}
âœĻ Check Solution and Practice

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);
}
âœĻ Check Solution and Practice

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;
}
âœĻ Check Solution and Practice

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.

âœĻ Check Solution and Practice

Running and Testing

Open index.html in a web browser.

Alt text

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

âœĻ Check Solution and Practice

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.

Other CSS Tutorials you may like