diff --git a/src/images/2048.png b/src/images/2048.png new file mode 100644 index 000000000..ba4c0d48c Binary files /dev/null and b/src/images/2048.png differ diff --git a/src/index.html b/src/index.html index aff3d1a98..2601f24df 100644 --- a/src/index.html +++ b/src/index.html @@ -7,6 +7,11 @@ content="width=device-width, initial-scale=1.0" /> 2048 + 2048

- + diff --git a/src/modules/Game.class.js b/src/modules/Game.class.js index 65cd219c9..4807aa45c 100644 --- a/src/modules/Game.class.js +++ b/src/modules/Game.class.js @@ -1,68 +1,627 @@ '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], + ], + ) { + if ( + initialState.length === 4 && + initialState.every((row) => row.length === 4) + ) { + this.initialState = initialState; + this.restart(); + } else { + throw new Error('Initial state is not valid!'); + } + } + + moveLeft() { + if (this.status === 'playing') { + const result = this.moveTiles(true, false); + + if (this.isStateDifferent(result.state)) { + this.updateGame(result); + } + } + } + + moveRight() { + if (this.status === 'playing') { + const result = this.moveTiles(true, true); + + if (this.isStateDifferent(result.state)) { + this.updateGame(result); + } + } + } + + moveUp() { + if (this.status === 'playing') { + const result = this.moveTiles(false, false); + + if (this.isStateDifferent(result.state)) { + this.updateGame(result); + } + } + } + + moveDown() { + if (this.status === 'playing') { + const result = this.moveTiles(false, true); + + if (this.isStateDifferent(result.state)) { + this.updateGame(result); + } + } + } + + getScore() { + return this.score; + } + + getState() { + return this.state; + } + + getStatus() { + return this.status; + } + + start() { + this.restart(); + this.status = 'playing'; + + this.putNewNumber(); + this.putNewNumber(); + } + + restart() { + this.state = this.cloneState(this.initialState); + this.score = 0; + this.status = 'idle'; + this.firstMoveMade = false; + } + + cloneState(state) { + const newState = []; + + for (let row = 0; row < state.length; row++) { + newState.push([...state[row]]); + } + + return newState; + } + + isStateDifferent(newState) { + for (let row = 0; row < this.state.length; row++) { + for (let column = 0; column < this.state[row].length; column++) { + if (this.state[row][column] !== newState[row][column]) { + return true; + } + } + } + + return false; + } + + updateGame(result) { + this.state = result.state; + this.score += result.score; + this.firstMoveMade = true; + this.putNewNumber(); + + if (this.isGameOver()) { + this.status = 'lose'; + } else if (this.isGameWon()) { + this.status = 'win'; + } + } + + getFirstMoveMade() { + return this.firstMoveMade; + } + + getAvailableSpace() { + return this.state.reduce((space, row, rowIndex) => { + row.forEach((cell, cellIndex) => { + if (cell === 0) { + space.push({ + x: cellIndex, + y: rowIndex, + }); + } + }); + + return space; + }, []); + } + + getRandomElement(space) { + return space[Math.floor(Math.random() * space.length)]; + } + + generateNumber() { + return Math.floor(Math.random() * 10) === 0 ? 4 : 2; + } + + putNewNumber() { + const availableSpace = this.getAvailableSpace(); + const randomSpace = this.getRandomElement(availableSpace); + + this.state[randomSpace.y][randomSpace.x] = this.generateNumber(); + } + + isGameOver() { + if (this.getAvailableSpace().length === 0) { + if ( + this.isStateDifferent(this.moveTiles(false, false).state) || + this.isStateDifferent(this.moveTiles(true, true).state) || + this.isStateDifferent(this.moveTiles(false, true).state) || + this.isStateDifferent(this.moveTiles(true, false).state) + ) { + return false; + } + + return true; + } + + return false; + } + + isGameWon() { + return this.state.some((row) => row.some((cell) => cell === 2048)); + } + + getCellCoords(horizontal, firstCoord, position) { + if (horizontal) { + return { + row: firstCoord, + column: position, + }; + } + + return { + row: position, + column: firstCoord, + }; + } + + moveTiles(horizontal, forward, addScore) { + const state = this.cloneState(this.state); + let score = 0; + + const rowLength = state[0].length; + const columnLength = state.length; + let firstCoordLength; + let secondCoordLength; + + if (horizontal) { + firstCoordLength = columnLength; + secondCoordLength = rowLength; + } else { + firstCoordLength = rowLength; + secondCoordLength = columnLength; + } + + let initialSecondCoord; + let startOfStripe; + let endOfStirpe; + let step; + + if (forward) { + initialSecondCoord = secondCoordLength - 2; + startOfStripe = -1; + endOfStirpe = secondCoordLength; + step = 1; + } else { + initialSecondCoord = 1; + startOfStripe = secondCoordLength; + endOfStirpe = -1; + step = -1; + } + + for (let firstCoord = 0; firstCoord < firstCoordLength; firstCoord++) { + const mergedInStripe = Array(secondCoordLength).fill(false); + + for ( + let secondCoord = initialSecondCoord; + secondCoord !== startOfStripe; + secondCoord -= step + ) { + let merged = false; + let position = secondCoord; + + let currentCellCoords = this.getCellCoords( + horizontal, + firstCoord, + position, + ); + let nextCellCoords = this.getCellCoords( + horizontal, + firstCoord, + position + step, + ); + + while ( + position + step !== endOfStirpe && + (state[nextCellCoords.row][nextCellCoords.column] === 0 || + (state[nextCellCoords.row][nextCellCoords.column] === + state[currentCellCoords.row][currentCellCoords.column] && + !mergedInStripe[position + step] && + !merged)) + ) { + if (state[nextCellCoords.row][nextCellCoords.column] > 0) { + merged = true; + score += state[nextCellCoords.row][nextCellCoords.column] * 2; + } + + state[nextCellCoords.row][nextCellCoords.column] += + state[currentCellCoords.row][currentCellCoords.column]; + state[currentCellCoords.row][currentCellCoords.column] = 0; + position += step; + + currentCellCoords = this.getCellCoords( + horizontal, + firstCoord, + position, + ); + + nextCellCoords = this.getCellCoords( + horizontal, + firstCoord, + position + step, + ); + } + + if (merged) { + mergedInStripe[position] = true; + } + } + } + + return { + state: state, + score: score, + }; + } } module.exports = Game; + +// '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 = [ +// [0, 0, 0, 0], +// [0, 0, 0, 0], +// [0, 0, 0, 0], +// [0, 0, 0, 0], +// ], +// ) { +// if ( +// initialState.length === 4 && +// initialState.every((row) => row.length === 4) +// ) { +// this.initialState = initialState; +// this.restart(); +// } else { +// throw new Error('Initial state is not valid!'); +// } +// } + +// moveLeft() { +// if (this.status === 'playing') { +// const result = this.moveTiles(true, false); + +// if (this.isStateDifferent(result.state)) { +// this.updateGame(result); +// } +// } +// } + +// moveRight() { +// if (this.status === 'playing') { +// const result = this.moveTiles(true, true); + +// if (this.isStateDifferent(result.state)) { +// this.updateGame(result); +// } +// } +// } + +// moveUp() { +// if (this.status === 'playing') { +// const result = this.moveTiles(false, false); + +// if (this.isStateDifferent(result.state)) { +// this.updateGame(result); +// } +// } +// } + +// moveDown() { +// if (this.status === 'playing') { +// const result = this.moveTiles(false, true); + +// if (this.isStateDifferent(result.state)) { +// this.updateGame(result); +// } +// } +// } + +// /** +// * @returns {number} +// */ +// getScore() { +// return this.score; +// } + +// /** +// * @returns {number[][]} +// */ +// getState() { +// return this.state; +// } + +// /** +// * 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() { +// return this.status; +// } + +// /** +// * Starts the game. +// */ +// start() { +// this.restart(); +// this.status = 'playing'; + +// this.putNewNumber(); +// this.putNewNumber(); +// } + +// /** +// * Resets the game. +// */ +// restart() { +// this.state = this.cloneState(this.initialState); +// this.score = 0; +// this.status = 'idle'; +// this.firstMoveMade = false; +// } + +// // Add your own methods here +// cloneState(state) { +// const newState = []; + +// for (let row = 0; row < state.length; row++) { +// newState.push([...state[row]]); +// } + +// return newState; +// } + +// isStateDifferent(newState) { +// for (let row = 0; row < this.state.length; row++) { +// for (let column = 0; column < this.state[row].length; column++) { +// if (this.state[row][column] !== newState[row][column]) { +// return true; +// } +// } +// } + +// return false; +// } + +// updateGame(result) { +// this.state = result.state; +// this.score += result.score; +// this.firstMoveMade = true; +// this.putNewNumber(); + +// if (this.isGameOver()) { +// this.status = 'lose'; +// } else if (this.isGameWon()) { +// this.status = 'win'; +// } +// } + +// getFirstMoveMade() { +// return this.firstMoveMade; +// } + +// getAvailableCell() { +// const cell = []; + +// this.state.forEach((row, y) => { +// row.forEach((number, x) => { +// if (!number) { +// cell.push({ x, y }); +// } +// }); +// }); + +// return cell; +// } + +// getRandomElement(arr) { +// const randomIndex = Math.floor(Math.random() * arr.length); + +// return arr[randomIndex]; +// } + +// generateNumber() { +// return Math.floor(Math.random() * 10) === 0 ? 4 : 2; +// } + +// putNewNumber() { +// const availableCell = this.getAvailableCell(); +// const randomCell = this.getRandomElement(availableCell); + +// this.state[randomCell.y][randomCell.x] = this.generateNumber(); +// } + +// isGameOver() { +// if (this.getAvailableCell().length === 0) { +// const directions = [ +// { horizontal: false, forward: false }, +// { horizontal: true, forward: true }, +// { horizontal: false, forward: true }, +// { horizontal: true, forward: false }, +// ]; + +// return directions.every(({ horizontal, forward }) => { +// const newState = this.moveTiles(horizontal, forward).state; + +// return this.isStateDifferent(newState); +// }); +// } + +// return false; +// } + +// isGameWon() { +// return this.state.flat().includes(2048); +// } + +// getCellCoords(horizontal, firstCoord, position) { +// return horizontal +// ? { row: firstCoord, column: position } +// : { row: position, column: firstCoord }; +// } + +// moveTiles(horizontal, forward, addScore) { +// const state = this.cloneState(this.state); +// let score = 0; + +// const rowLength = state[0].length; +// const columnLength = state.length; +// let firstCoordLength; +// let secondCoordLength; + +// if (horizontal) { +// firstCoordLength = columnLength; +// secondCoordLength = rowLength; +// } else { +// firstCoordLength = rowLength; +// secondCoordLength = columnLength; +// } + +// let initialSecondCoord; +// let startOfStripe; +// let endOfStirpe; +// let step; + +// if (forward) { +// initialSecondCoord = secondCoordLength - 2; +// startOfStripe = -1; +// endOfStirpe = secondCoordLength; +// step = 1; +// } else { +// initialSecondCoord = 1; +// startOfStripe = secondCoordLength; +// endOfStirpe = -1; +// step = -1; +// } + +// for (let firstCoord = 0; firstCoord < firstCoordLength; firstCoord++) { +// const mergedInStripe = Array(secondCoordLength).fill(false); + +// for ( +// let secondCoord = initialSecondCoord; +// secondCoord !== startOfStripe; +// secondCoord -= step +// ) { +// let merged = false; +// let position = secondCoord; + +// let currentCellCoords = this.getCellCoords( +// horizontal, +// firstCoord, +// position, +// ); +// let nextCellCoords = this.getCellCoords( +// horizontal, +// firstCoord, +// position + step, +// ); + +// while ( +// position + step !== endOfStirpe && +// (state[nextCellCoords.row][nextCellCoords.column] === 0 || +// (state[nextCellCoords.row][nextCellCoords.column] === +// state[currentCellCoords.row][currentCellCoords.column] && +// !mergedInStripe[position + step] && +// !merged)) +// ) { +// if (state[nextCellCoords.row][nextCellCoords.column] > 0) { +// merged = true; +// score += state[nextCellCoords.row][nextCellCoords.column] * 2; +// } + +// state[nextCellCoords.row][nextCellCoords.column] += +// state[currentCellCoords.row][currentCellCoords.column]; +// state[currentCellCoords.row][currentCellCoords.column] = 0; +// position += step; + +// currentCellCoords = this.getCellCoords( +// horizontal, +// firstCoord, +// position, +// ); + +// nextCellCoords = this.getCellCoords( +// horizontal, +// firstCoord, +// position + step, +// ); +// } + +// if (merged) { +// mergedInStripe[position] = true; +// } +// } +// } + +// return { +// state: state, +// score: score, +// }; +// } +// } + +// module.exports = Game; diff --git a/src/scripts/main.js b/src/scripts/main.js index dc7f045a3..d1544379f 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -1,7 +1,96 @@ '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(); -// Write your code here +function updateTable(state) { + const fieldRows = document.querySelectorAll('.field-row'); + + fieldRows.forEach((rowElement, rowIndex) => { + const rowState = state[rowIndex]; + + rowState.forEach((cellState, columnIndex) => { + const cellElement = rowElement.children[columnIndex]; + + cellElement.className = `field-cell field-cell--${cellState}`; + cellElement.innerText = cellState > 0 ? cellState : ''; + + if (cellState > 0) { + cellElement.classList.add('merge'); + + setTimeout(() => cellElement.classList.remove('merge'), 600); + } + }); + }); +} + +function updateScore(score) { + document.querySelector('.game-score').innerText = score; +} + +function updateButton(firstMoveMade) { + if (firstMoveMade) { + button.className = 'button restart'; + button.innerText = 'Restart'; + } else { + button.className = 'button start'; + button.innerText = 'Start'; + } +} + +function updateMessage(gameStatus) { + const messageClasses = { + idle: 'message-start', + win: 'message-win', + lose: 'message-lose', + }; + + document.querySelectorAll('.message').forEach((message) => { + message.classList.add('hidden'); + }); + + const messageClass = messageClasses[gameStatus]; + + if (messageClass) { + document.querySelector(`.${messageClass}`).classList.remove('hidden'); + } +} + +function update() { + updateTable(game.getState()); + updateScore(game.getScore()); + updateButton(game.getFirstMoveMade()); + updateMessage(game.getStatus()); +} + +const button = document.querySelector('.button'); + +button.addEventListener('click', () => { + switch (button.innerText) { + case 'Start': + game.start(); + break; + case 'Restart': + game.restart(); + } + + update(); +}); + +document.addEventListener('keydown', (e) => { + const actions = { + ArrowUp: () => game.moveUp(), + ArrowRight: () => game.moveRight(), + ArrowDown: () => game.moveDown(), + ArrowLeft: () => game.moveLeft(), + }; + + const action = actions[e.key]; + + if (action) { + e.preventDefault(); + action(); + } + + update(); +}); diff --git a/src/styles/main.scss b/src/styles/main.scss index c43f37dcf..4c1f7cd62 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -4,7 +4,7 @@ body { align-items: center; justify-content: center; min-height: 100vh; - background: #fbf8ef; + background: #7b68ee; font-family: sans-serif; font-size: 24px; font-weight: 900; @@ -22,61 +22,63 @@ body { user-select: none; &--2 { - background: #eee4da; + background: #b0c4de; + color: #000; } &--4 { - background: #ede0c8; + background: #b0e0e6; + color: #000; } &--8 { - background: #f2b179; - color: #f9f6f2; + background: #7fffd4; + color: #000; } &--16 { - background: #f59563; - color: #f9f6f2; + background: #add8e6; + color: #000; } &--32 { - background: #f67c5f; - color: #f9f6f2; + background: #40e0d0; + color: #000; } &--64 { - background: #f65e3b; - color: #f9f6f2; + background: #87ceeb; + color: #000; } &--128 { - background: #edcf72; - color: #f9f6f2; + background: #48d1cc; + color: #000; } &--256 { - background: #edcc61; - color: #f9f6f2; + background: #87cefa; + color: #000; } &--512 { - background: #edc850; - color: #f9f6f2; + background: #00ced1; + color: #000; } &--1024 { - background: #edc53f; - color: #f9f6f2; + background: #00bfff; + color: #000; } &--2048 { - background: #edc22e; - color: #f9f6f2; + background: #4682b4; + color: #000; } } .game-field { - background: #bbada0; + background: #00f; border-spacing: 10px; border-radius: 5px; } @@ -147,7 +149,7 @@ h1 { } .restart { - background: #f1b2b2; + background: #f4a460; &:hover { background: #f87474;