Open In App

Design a 2048 Game in JavaScript

Last Updated : 26 Apr, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

In this article, you will learn to build the famous 2048 game in JavaScript. This article aims to develop problem-solving and logical thinking abilities and will thus keep its focus on JavaScript. We will see how to access the DOM elements and update multiple times them as the user interacts with the web page, in order to create an interactive application, in this case, the game 2048. If you have not played this game, it is recommended to play it first to get the hang of it. You can easily install it on your smartphone.

How to Play the Game?

  • You will have a 4 X 4 Matrix, in which two randomly selected cells will have ‘2’ as their value.
  • Use the arrow keys to move all cells in the corresponding direction
  • While cells move in a particular direction same values will add up to combine into a single cell and different value cells will simply move in that particular direction if there are empty cells
  • The magnitude of the value assigned will increase as the game moves on. Also, empty cells will randomly be assigned with random values. All values will be of some power of 2.
  • You have to smartly make combinations such that you reach the number 2048. This is the winning condition.

Play 2048 Game Online

Programming Approach

  • First, create a 4 X 4 matrix using a CSS grid. Each cell is an HTML div tag with a p tag inside to hold its text.
  • Define a function in Javascript to randomly pick a cell and assign it a value
  • Use JavaScript event listeners in moveBlocks to acknowledge the key press events like arrow keys and to shit cells in that corresponding direction directed by the arrows.
  • Assign different colors to the cell containing different values and also to the empty cell.
  • Create function shift(direction) and move(direction) to shift the values in case of empty cells and combine the same values respectively
  • Check the max value in the cells and use the gameOver function to show winning dialogue if the max value reaches 2048 and losing dialogue in case of no cells in empty and the max cell value has not reached 2048.

Project Structure

file_structure.PNG

Complete Code

Javascript




// Filename: script.js
// Get the game grid
const gridItems = [
    ...document.querySelectorAll(".grid-item"),
];
const score_val = document.querySelector(".score-value");
const result = document.querySelector(".result");
let score = 0;
let moves = 0;
let moveFactor = 4;
let options = [
    2, 4, 8, 2, 4, 8, 2, 2, 4, 4, 2, 8, 2, 2, 4, 4, 2,
];
let matrix = [];
let prevMatrix;
  
let colors = [
    "#caf0f8",
    "#90e0ef",
    "#00b4d8",
    "#0077b6",
    "#03045e",
    "#023047",
    "#fca311",
    "#14213d",
    "#e63946",
    "#ffc300",
    "#6a040f",
    "#000000",
];
  
// Create the starting game grid.
let row = [];
for (let index = 1; index < gridItems.length + 1; index++) {
    if (index % 4 === 0) {
        let item = gridItems[index - 1];
        item.firstElementChild.innerText = "";
        row.push(item);
        matrix.push(row);
        row = [];
    } else {
        let item = gridItems[index - 1];
        item.firstElementChild.innerText = "";
        row.push(item);
    }
}
  
// Assign any two grid blocks the value of 2
const rowIdx = Math.floor(Math.random() * 4);
const colIdx = Math.floor(Math.random() * 4);
let rowIdx2 = Math.floor(Math.random() * 4);
let colIdx2 = Math.floor(Math.random() * 4);
  
if (rowIdx === rowIdx2 && colIdx === colIdx2) {
    rowIdx2 = Math.floor(Math.random() * 4);
    colIdx2 = Math.floor(Math.random() * 4);
}
  
matrix[rowIdx][colIdx].firstElementChild.textContent = 2;
matrix[rowIdx2][colIdx2].firstElementChild.textContent = 2;
  
let availIndexes = updateAvailIndexes();
  
updateColors();
  
// Make web page able to listen to keydown event
document.addEventListener("keydown", moveBlocks);
  
// Method to extract columns from an 2D array.
const arrayColumn = (arr, n) => arr.map((x) => x[n]);
  
function moveBlocks(e) {
    if (
        e.key !== "ArrowLeft" &&
        e.key !== "ArrowRight" &&
        e.key !== "ArrowUp" &&
        e.key !== "ArrowDown"
    ) {
        return;
    }
  
    moves++;
    matrixVals = getCurrentMatrixValues();
    prevMatrix = matrixVals;
  
    let col1 = arrayColumn(matrix, 0);
    let col2 = arrayColumn(matrix, 1);
    let col3 = arrayColumn(matrix, 2);
    let col4 = arrayColumn(matrix, 3);
    let row1 = matrix[0];
    let row2 = matrix[1];
    let row3 = matrix[2];
    let row4 = matrix[3];
  
    if (e.key === "ArrowLeft") {
        moveLeft(row1);
        moveLeft(row2);
        moveLeft(row3);
        moveLeft(row4);
    }
    if (e.key === "ArrowRight") {
        moveRight(row1);
        moveRight(row2);
        moveRight(row3);
        moveRight(row4);
    }
    if (e.key === "ArrowUp") {
        moveLeft(col1);
        moveLeft(col2);
        moveLeft(col3);
        moveLeft(col4);
    }
    if (e.key === "ArrowDown") {
        moveRight(col1);
        moveRight(col2);
        moveRight(col3);
        moveRight(col4);
    }
  
    matrixVals = getCurrentMatrixValues();
    availIndexes = updateAvailIndexes();
    updateColors();
  
    let check = checkMatrixEquality(prevMatrix, matrixVals);
  
    if (availIndexes.length === 0 && check === true) {
        gameOver("loose");
    }
  
    if (moves % moveFactor === 0) {
        generateNewBlock();
    }
}
  
setInterval(() => {
    availIndexes = updateAvailIndexes();
    generateNewBlock();
}, 8000);
  
setTimeout(() => {
    options.push(16);
    setTimeout(() => {
        options.push(16);
        options.push(32);
        setTimeout(() => {
            options.push(16);
            options.push(32);
            options.push(64);
        }, 40000);
    }, 18000);
}, 120000);
  
function getCurrentMatrixValues() {
    let gridItems = [
        ...document.querySelectorAll(".grid-item"),
    ];
    let matrix_grid = [];
    let row = [];
    for (
        let index = 1;
        index < gridItems.length + 1;
        index++
    ) {
        if (index % 4 === 0) {
            let item = gridItems[index - 1];
            row.push(item.firstElementChild.innerText);
            matrix_grid.push(row);
            row = [];
        } else {
            let item = gridItems[index - 1];
            row.push(item.firstElementChild.innerText);
        }
    }
    return matrix_grid;
}
  
function shiftLeft(arr) {
    for (let i = 0; i < 4; i++) {
        for (let i = 1; i < 4; i++) {
            let currElement = arr[i].firstElementChild;
            let prevElement = arr[i - 1].firstElementChild;
            if (prevElement.innerText == 0) {
                prevElement.innerText =
                    currElement.innerText;
                currElement.innerText = "";
            }
        }
    }
}
  
function shiftRight(arr) {
    for (let i = 0; i < 4; i++) {
        for (let i = 2; i >= 0; i--) {
            let currElement = arr[i].firstElementChild;
            let nextElement = arr[i + 1].firstElementChild;
            if (nextElement.innerText == 0) {
                nextElement.innerText =
                    currElement.innerText;
                currElement.innerText = "";
            }
        }
    }
}
  
function moveRight(row) {
    shiftRight(row);
  
    for (let i = 2; i >= 0; i--) {
        let currElement = row[i].firstElementChild;
        let nextElement = row[i + 1].firstElementChild;
        let val = parseInt(currElement.innerText);
        let nextVal = parseInt(nextElement.innerText);
        if (val === nextVal && val !== 0) {
            let newVal = val + nextVal;
            nextElement.innerText = newVal;
            currElement.innerText = "";
            score = score + 2;
            score_val.innerText = score;
            if (newVal === 2048) {
                gameOver("Win");
            }
        }
    }
  
    shiftRight(row);
}
  
function moveLeft(row) {
    shiftLeft(row);
  
    for (let i = 1; i < 4; i++) {
        let currElement = row[i].firstElementChild;
        let prevElement = row[i - 1].firstElementChild;
        let val = parseInt(currElement.innerText);
        let prevVal = parseInt(prevElement.innerText);
        if (val === prevVal && val !== 0) {
            let newVal = val + prevVal;
            prevElement.innerText = newVal;
            currElement.innerText = "";
            score = score + 2;
            score_val.innerText = score;
            if (newVal === 2048) {
                gameOver("Win");
            }
        }
    }
  
    shiftLeft(row);
}
  
function updateAvailIndexes() {
    matrixValues = getCurrentMatrixValues();
    let grid = [];
    for (let i = 0; i < 4; i++) {
        for (let j = 0; j < 4; j++) {
            if (matrixValues[i][j] == "") {
                grid.push([i, j]);
            }
        }
    }
    return grid;
}
  
function generateNewBlock() {
    if (availIndexes.length !== 0) {
        let randInt = Math.floor(
            Math.random() * availIndexes.length
        );
        let coords = availIndexes[randInt];
        let randInt3 = Math.floor(
            Math.random() * options.length
        );
        let ele =
            matrix[coords[0]][coords[1]].firstElementChild;
        ele.innerText = options[randInt3];
        updateColors();
    }
}
  
function checkMatrixEquality(mat1, mat2) {
    for (let i = 0; i < 4; i++) {
        for (let j = 0; j < 4; j++) {
            if (mat1[i][j] !== mat2[i][j]) {
                return false;
            }
        }
    }
    return true;
}
  
function gameOver(status) {
    if (status === "Win") {
        result.innerText = "You Won!!!";
        result.style.color = "rgb(78, 236, 144)";
    } else {
        result.innerText = "You Loose!!!";
        result.style.color = "rgb(252, 51, 51)";
    }
}
  
function updateColors() {
    for (let i = 0; i < 4; i++) {
        for (let j = 0; j < 4; j++) {
            let elem = matrix[i][j].firstElementChild;
            if (elem.innerText == 0) {
                elem.parentElement.style.backgroundColor =
                    colors[0];
                elem.style.color = "black";
            } else if (elem.innerText == 2) {
                elem.style.color = "black";
                elem.parentElement.style.backgroundColor =
                    colors[1];
            } else if (elem.innerText == 4) {
                elem.style.color = "black";
                elem.parentElement.style.backgroundColor =
                    colors[2];
            } else if (elem.innerText == 8) {
                elem.style.color = "black";
                elem.parentElement.style.backgroundColor =
                    colors[3];
            } else if (elem.innerText == 16) {
                elem.style.color = "white";
                elem.parentElement.style.backgroundColor =
                    colors[4];
            } else if (elem.innerText == 32) {
                elem.style.color = "white";
                elem.parentElement.style.backgroundColor =
                    colors[5];
            } else if (elem.innerText == 64) {
                elem.style.color = "white";
                elem.parentElement.style.backgroundColor =
                    colors[6];
            } else if (elem.innerText == 128) {
                elem.style.color = "white";
                elem.parentElement.style.backgroundColor =
                    colors[7];
            } else if (elem.innerText == 256) {
                elem.style.color = "white";
                elem.parentElement.style.backgroundColor =
                    colors[8];
            } else if (elem.innerText == 512) {
                elem.style.color = "white";
                elem.parentElement.style.backgroundColor =
                    colors[9];
            } else if (elem.innerText == 1024) {
                elem.style.color = "white";
                elem.parentElement.style.backgroundColor =
                    colors[10];
            } else if (elem.innerText == 2048) {
                elem.style.color = "white";
                elem.parentElement.style.backgroundColor =
                    colors[11];
            }
        }
    }
}


HTML




<!-- filename: index.html -->
<!DOCTYPE html>
<html lang="en">
  
<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" 
        content="IE=edge" />
    <meta name="viewport" content=
        "width=device-width, initial-scale=1.0" />
    <!-- Importing Google Fonts API Link -->
    <link rel="preconnect" 
        href="https://fonts.googleapis.com" />
    <link rel="preconnect" 
        href="https://fonts.gstatic.com" 
        crossorigin />
    <link href=
        rel="stylesheet" />
  
    <!-- Importing our CSS File where we 
        write all our styles -->
    <link rel="stylesheet" href="./style.css" />
</head>
  
<body>
    <div class="container">
        <div class="score">
            <p class="score-title">Score:</p>
            <p class="score-value">0</p>
        </div>
  
        <div class="result"></div>
  
        <div class="grid">
            <div class="grid-item">
                <p id="1"></p>
            </div>
            <div class="grid-item">
                <p id="2"></p>
            </div>
            <div class="grid-item">
                <p id="3"></p>
            </div>
            <div class="grid-item">
                <p id="4"></p>
            </div>
            <div class="grid-item">
                <p id="5"></p>
            </div>
            <div class="grid-item">
                <p id="6"></p>
            </div>
            <div class="grid-item">
                <p id="7"></p>
            </div>
            <div class="grid-item">
                <p id="8"></p>
            </div>
            <div class="grid-item">
                <p id="9"></p>
            </div>
            <div class="grid-item">
                <p id="10"></p>
            </div>
            <div class="grid-item">
                <p id="11"></p>
            </div>
            <div class="grid-item">
                <p id="12"></p>
            </div>
            <div class="grid-item">
                <p id="13"></p>
            </div>
            <div class="grid-item">
                <p id="14"></p>
            </div>
            <div class="grid-item">
                <p id="15"></p>
            </div>
            <div class="grid-item">
                <p id="16"></p>
            </div>
        </div>
    </div>
    <script src="script.js"></script>
</body>
  
</html>


CSS




/* filename style.css */
:root {
    --primary-text-color: #27374d;
}
  
* {
    box-sizing: border-box;
    padding: 0%;
    margin: 0%;
    font-family: "Montserrat", sans-serif;
    font-family: "Ubuntu", sans-serif;
}
  
body {
    background-color: #dda15e;
    color: var(--primary-text-color);
    min-height: 100vh;
}
  
.container {
    width: 400px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}
  
.score {
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: space-evenly;
    flex-wrap: wrap;
    font-size: 2rem;
    text-align: center;
    padding: 10px;
}
  
.score-title {
    width: 50%;
}
  
.score-value {
    width: 50%;
}
  
.result {
    font-size: 2rem;
    text-align: center;
    padding: 10px;
}
  
.grid {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 1fr;
    grid-template-rows: 1fr 1fr 1fr 1fr;
    width: 400px;
    height: 400px;
    background-color: rgb(209, 207, 207);
    box-shadow: 5px 5px 20px rgba(0, 0, 0, 0.753);
}
  
.grid-item {
    border: 1px solid var(--primary-text-color);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 2rem;
}
  
.rules {
    border: 2px solid white;
    padding: 20px;
}
  
.rules>h1 {
    width: 100%;
    font-size: 2rem;
    text-align: center;
    padding: 10px;
}
  
.rules-para {
    padding: 20px 10px;
}


Output:

final_code_output.PNG



Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads