diff --git a/src/images/2048-icon.png b/src/images/2048-icon.png new file mode 100644 index 000000000..0a8141c10 Binary files /dev/null and b/src/images/2048-icon.png differ diff --git a/src/index.html b/src/index.html index aff3d1a98..32029141d 100644 --- a/src/index.html +++ b/src/index.html @@ -7,6 +7,10 @@ content="width=device-width, initial-scale=1.0" /> 2048 + 2048 +

Combine the numbers and aim to reach the 2048 tile!

+ @@ -58,13 +64,20 @@

2048

- +

Press "Start" to begin game. Good luck!

+

+ HOW TO PLAY: Use the arrow keys to move the tiles around. When two + tiles with the same number collide, they combine into a single tile! +

- + diff --git a/src/modules/Game.class.js b/src/modules/Game.class.js index 65cd219c9..292871d64 100644 --- a/src/modules/Game.class.js +++ b/src/modules/Game.class.js @@ -1,68 +1,266 @@ 'use strict'; -/** - * This class represents the game. - * Now it has a basic structure, that is needed for testing. - * Feel free to add more props and methods if needed. - */ class Game { - /** - * Creates a new game instance. - * - * @param {number[][]} initialState - * The initial state of the board. - * @default - * [[0, 0, 0, 0], - * [0, 0, 0, 0], - * [0, 0, 0, 0], - * [0, 0, 0, 0]] - * - * If passed, the board will be initialized with the provided - * initial state. - */ - constructor(initialState) { - // eslint-disable-next-line no-console - console.log(initialState); - } - - moveLeft() {} - moveRight() {} - moveUp() {} - moveDown() {} - - /** - * @returns {number} - */ - getScore() {} - - /** - * @returns {number[][]} - */ - getState() {} - - /** - * Returns the current game status. - * - * @returns {string} One of: 'idle', 'playing', 'win', 'lose' - * - * `idle` - the game has not started yet (the initial state); - * `playing` - the game is in progress; - * `win` - the game is won; - * `lose` - the game is lost - */ - getStatus() {} - - /** - * Starts the game. - */ - start() {} - - /** - * Resets the game. - */ - restart() {} - - // Add your own methods here + constructor( + initialState = [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ], + ) { + this.score = 0; + this.status = 'idle'; + this.initialState = initialState; + this.state = this.copyState(this.initialState); + } + + getScore() { + return this.score; + } + + getState() { + return this.state; + } + + getStatus() { + return this.status; + } + + start() { + if (this.status === 'idle') { + this.status = 'playing'; + this.addRandomTile(); + this.addRandomTile(); + } + } + + restart() { + this.state = this.copyState(this.initialState); + this.score = 0; + this.status = 'idle'; + } + + copyState(state) { + return state.map((row) => [...row]); + } + + addRandomTile() { + const emptyTiles = []; + + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + if (this.state[row][col] === 0) { + emptyTiles.push([row, col]); + } + } + } + + if (emptyTiles.length > 0) { + const randomIndex = Math.floor(Math.random() * emptyTiles.length); + + const [row, col] = emptyTiles[randomIndex]; + + this.state[row][col] = Math.random() < 0.9 ? 2 : 4; + } + } + + moveLeft() { + if (this.status !== 'playing') { + return; + } + + const moved = this.move('left'); + + if (moved) { + this.addRandomTile(); + this.checkGameState(); + } + } + + moveRight() { + if (this.status !== 'playing') { + return; + } + + const moved = this.move('right'); + + if (moved) { + this.addRandomTile(); + this.checkGameState(); + } + } + + moveUp() { + if (this.status !== 'playing') { + return; + } + + const moved = this.move('up'); + + if (moved) { + this.addRandomTile(); + this.checkGameState(); + } + } + + moveDown() { + if (this.status !== 'playing') { + return; + } + + const moved = this.move('down'); + + if (moved) { + this.addRandomTile(); + this.checkGameState(); + } + } + + move(direction) { + const originalState = this.copyState(this.state); + + const combineRow = (row) => { + const newRow = row.filter((n) => n !== 0); + + for (let i = 0; i < newRow.length - 1; i++) { + if (newRow[i] === newRow[i + 1]) { + newRow[i] *= 2; + newRow[i + 1] = 0; + this.score += newRow[i]; + } + } + + return newRow.filter((n) => n !== 0); + }; + + const moveRowLeft = (row) => { + const newRow = combineRow(row); + + while (newRow.length < 4) { + newRow.push(0); + } + + return newRow; + }; + + const moveRowRight = (row) => { + const copyRow = [...row]; + + const newRow = combineRow(copyRow.reverse()); + + while (newRow.length < 4) { + newRow.push(0); + } + + return newRow.reverse(); + }; + + const moveStateLeft = (state) => { + return state.map((row) => moveRowLeft(row)); + }; + + const moveStateRight = (state) => { + return state.map((row) => moveRowRight(row)); + }; + + switch (direction) { + case 'left': + this.state = moveStateLeft(this.state); + break; + + case 'right': + this.state = moveStateRight(this.state); + break; + + case 'up': + this.state = this.transposeState( + moveStateLeft(this.transposeState(this.state)), + ); + break; + + case 'down': + this.state = this.transposeState( + moveStateRight(this.transposeState(this.state)), + ); + break; + } + + return !this.areStatesEqual(this.state, originalState); + } + + hasEmptyCells() { + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + if (this.state[row][col] === 0) { + return true; + } + } + } + + return false; + } + + canCombine() { + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + const current = this.state[row][col]; + + if (col < 3 && current === this.state[row][col + 1]) { + return true; + } + + if (row < 3 && current === this.state[row + 1][col]) { + return true; + } + } + } + + return false; + } + + checkGameState() { + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + if (this.state[row][col] === 2048) { + this.status = 'win'; + + return; + } + } + } + + if (this.hasEmptyCells() || this.canCombine()) { + return; + } + this.status = 'lose'; + } + + transposeState(state) { + const result = []; + + for (let col = 0; col < 4; col++) { + result[col] = []; + + for (let row = 0; row < 4; row++) { + result[col].push(state[row][col]); + } + } + + return result; + } + + areStatesEqual(state1, state2) { + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + if (state1[row][col] !== state2[row][col]) { + return false; + } + } + } + + return true; + } } module.exports = Game; diff --git a/src/scripts/main.js b/src/scripts/main.js index dc7f045a3..69559f483 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -1,7 +1,75 @@ 'use strict'; -// Uncomment the next lines to use your game instance in the browser -// const Game = require('../modules/Game.class'); -// const game = new Game(); +const Game = require('../modules/Game.class'); +const game = new Game(); +const cells = document.querySelectorAll('.field-cell'); +const buttonStart = document.querySelector('.button.start'); -// Write your code here +document.addEventListener('keydown', (e) => { + if (game.getStatus() === 'playing') { + switch (e.key) { + case 'ArrowLeft': + game.moveLeft(); + break; + + case 'ArrowRight': + game.moveRight(); + break; + + case 'ArrowUp': + game.moveUp(); + break; + + case 'ArrowDown': + game.moveDown(); + break; + } + + updateView(); + } +}); + +function updateView() { + let i = 0; + const state = game.getState(); + + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + const cell = cells[i]; + + cell.className = 'field-cell'; + + if (state[row][col]) { + cell.textContent = state[row][col]; + cell.classList.add(`field-cell--${state[row][col]}`); + } else { + cell.textContent = ''; + } + i++; + } + } + + document.querySelector('.game-score').textContent = game.getScore(); + + const gameStatus = game.getStatus(); + + if (gameStatus === 'win') { + document.querySelector('.message-win').classList.remove('hidden'); + } else if (gameStatus === 'lose') { + document.querySelector('.message-lose').classList.remove('hidden'); + } +} + +buttonStart.addEventListener('click', () => { + if (buttonStart.className === 'button restart') { + game.restart(); + } + + game.start(); + updateView(); + document.querySelector('.message-win').classList.add('hidden'); + document.querySelector('.message-lose').classList.add('hidden'); + document.querySelector('.message-start').classList.add('hidden'); + buttonStart.textContent = 'Restart'; + buttonStart.className = 'button restart'; +}); diff --git a/src/styles/main.scss b/src/styles/main.scss index c43f37dcf..d614751d4 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -12,8 +12,8 @@ body { .field-cell { background: #d6cdc4; - width: 75px; - height: 75px; + width: 80px; + height: 80px; border-radius: 5px; color: #776e65; box-sizing: border-box; @@ -85,23 +85,26 @@ body { display: flex; width: 100%; justify-content: space-between; - margin-bottom: 24px; + margin-bottom: 10px; padding: 10px; box-sizing: border-box; } h1 { - background: #edc22e; - color: #f9f6f2; - width: 75px; - height: 75px; - font-size: 24px; - border-radius: 5px; - display: flex; - align-items: center; - justify-content: center; - box-sizing: border-box; + font-size: 67px; + font-weight: 700; margin: 0; + display: block; + color: #776e65; +} + +h3 { + margin: 0; + margin-bottom: 10px; + font-family: sans-serif; + font-size: 20px; + color: #edc22e; + padding: 10px; } .info { @@ -170,6 +173,7 @@ h1 { } .container { + margin-top: 0; display: flex; flex-direction: column; align-items: center; @@ -181,6 +185,15 @@ h1 { color: #f9f6f2; } +.message-lose { + color: #776e65; +} + +.message-rule { + margin-bottom: 0; + font-size: 15px; +} + .message-container { width: 100%; height: 150px;