diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template new file mode 100644 index 000000000..44ac4e963 --- /dev/null +++ b/.github/workflows/test.yml-template @@ -0,0 +1,29 @@ +name: Test + +on: + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm start & sleep 5 && npm test + - name: Upload tests report(cypress mochaawesome merged HTML report) + if: ${{ always() }} + uses: actions/upload-artifact@v2 + with: + name: report + path: reports diff --git a/README.md b/README.md index 5aab92544..b9a3a5618 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ You can change the HTML/CSS layout if you need it. ## Deploy and Pull Request 1. Replace `` with your Github username in the link - - [DEMO LINK](https://.github.io/js_2048_game/) + - [DEMO LINK](https://nester7an.github.io/js_2048_game/) 2. Follow [this instructions](https://mate-academy.github.io/layout_task-guideline/) - Run `npm run test` command to test your code; - Run `npm run test:only -- -n` to run fast test ignoring linter; diff --git a/package-lock.json b/package-lock.json index f209cb6e0..ff37dc85b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@mate-academy/eslint-config": "latest", "@mate-academy/jest-mochawesome-reporter": "^1.0.0", "@mate-academy/linthtml-config": "latest", - "@mate-academy/scripts": "^1.8.5", + "@mate-academy/scripts": "^1.9.12", "@mate-academy/stylelint-config": "latest", "@parcel/transformer-sass": "^2.12.0", "cypress": "^13.13.0", @@ -1467,10 +1467,11 @@ "dev": true }, "node_modules/@mate-academy/scripts": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.5.tgz", - "integrity": "sha512-mHRY2FkuoYCf5U0ahIukkaRo5LSZsxrTSgMJheFoyf3VXsTvfM9OfWcZIDIDB521kdPrScHHnRp+JRNjCfUO5A==", + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.9.12.tgz", + "integrity": "sha512-/OcmxMa34lYLFlGx7Ig926W1U1qjrnXbjFJ2TzUcDaLmED+A5se652NcWwGOidXRuMAOYLPU2jNYBEkKyXrFJA==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", diff --git a/package.json b/package.json index 0335978ca..05abe81e0 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@mate-academy/eslint-config": "latest", "@mate-academy/jest-mochawesome-reporter": "^1.0.0", "@mate-academy/linthtml-config": "latest", - "@mate-academy/scripts": "^1.8.5", + "@mate-academy/scripts": "^1.9.12", "@mate-academy/stylelint-config": "latest", "@parcel/transformer-sass": "^2.12.0", "cypress": "^13.13.0", diff --git a/src/index.html b/src/index.html index aff3d1a98..1288441ad 100644 --- a/src/index.html +++ b/src/index.html @@ -65,6 +65,6 @@

2048

- + diff --git a/src/modules/Game.class.js b/src/modules/Game.class.js index 65cd219c9..4b1a0cf00 100644 --- a/src/modules/Game.class.js +++ b/src/modules/Game.class.js @@ -1,14 +1,7 @@ '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 @@ -16,29 +9,375 @@ class Game { * [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); + + constructor( + initialState = [ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + ], + ) { + this.state = initialState; + this.initialState = initialState; + this.status = 'idle'; + this.score = 0; + } + + static transposeMatrix(matrix) { + return matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex])); + } + + static ifEqualSeeEach(elements) { + let ifEqual = false; + const indexesOfRight = []; + const allEqual = elements.every((item) => { + return item === elements[0] && elements[0] !== 0; + }); + + for (let i = 0; i < elements.length - 1; i += 1) { + let j = i + 1; + + if (elements[j] === 0) { + j++; + + if (elements[j] === 0) { + j++; + } + } + + if (elements[i] === elements[j] && elements[i] !== 0) { + ifEqual = true; + + indexesOfRight.push(i); + indexesOfRight.push(j); + + if ( + !allEqual && + !(elements[0] === elements[1] && elements[2] === elements[3]) + ) { + break; + } + + i++; + } + } + + return [ifEqual, indexesOfRight]; } - moveLeft() {} - moveRight() {} - moveUp() {} - moveDown() {} + static convertTo2DArray(arr) { + const result = []; + + for (let i = 0; i < 4; i++) { + result.push(arr.slice(i * 4, i * 4 + 4)); + } + + return result; + } + + static cellsMoveValues(rowOrColValues) { + let insertPosition = 0; + const valuesCopy = [...rowOrColValues]; + + valuesCopy.forEach((cell, index, row) => { + if (cell !== 0) { + if (insertPosition !== index) { + valuesCopy[insertPosition] = cell; + row[index] = 0; + } + insertPosition++; + } + }); + + return valuesCopy; + } + + static are2DArraysEqual(arr1, arr2) { + if (arr1.length !== arr2.length) { + return false; + } + + return arr1.every((subArr1, index) => { + const subArr2 = arr2[index]; + + if (subArr1.length !== subArr2.length) { + return false; + } + + return subArr1.every((value, subIndex) => value === subArr2[subIndex]); + }); + } + + canCellsMove(grid) { + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + if (grid[row][col] === 0) { + return true; + } + } + } + + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + const current = grid[row][col]; + + if ( + (col < 3 && current === grid[row][col + 1]) || + (row < 3 && current === grid[row + 1][col]) + ) { + return true; + } + } + } + + return false; + } + + cellsMergeValues(target, second) { + const newTarget = target + second; + const newSecond = 0; + + this.score += newTarget; + + return [newTarget, newSecond]; + } + + moveLeft() { + if (this.status === 'playing') { + const firstState = this.getState(); + + this.state = this.state.map((row) => { + const rowCopy = [...row]; + const ifEquals = Game.ifEqualSeeEach(rowCopy)[0]; + const indexesOfRights = Game.ifEqualSeeEach(rowCopy)[1]; + + if (ifEquals) { + const newCells = this.cellsMergeValues( + rowCopy[indexesOfRights[0]], + rowCopy[indexesOfRights[1]], + ); + + rowCopy[indexesOfRights[0]] = newCells[0]; + rowCopy[indexesOfRights[1]] = newCells[1]; + + if (indexesOfRights.length === 4) { + const newCells2 = this.cellsMergeValues( + rowCopy[indexesOfRights[2]], + rowCopy[indexesOfRights[3]], + ); + + rowCopy[indexesOfRights[2]] = newCells2[0]; + rowCopy[indexesOfRights[3]] = newCells2[1]; + } + } + + const rowAfterMove = Game.cellsMoveValues(rowCopy); + + return rowAfterMove; + }); + + const secondState = this.getState(); + + if (!Game.are2DArraysEqual(firstState, secondState)) { + this.addRandomCell(); + } + } + + const gameField = this.getState(); + const flatField = gameField.flat(); + + if (flatField.includes(2048)) { + this.status = 'win'; + } + + if (!this.canCellsMove(this.getState())) { + this.status = 'lose'; + } + } + + moveRight() { + if (this.status === 'playing') { + const firstState = this.getState(); + + this.state = this.state.map((row) => { + const reverseRow = [...row].reverse(); + const ifEquals = Game.ifEqualSeeEach(reverseRow)[0]; + const indexesOfRights = Game.ifEqualSeeEach(reverseRow)[1]; + + if (ifEquals) { + const newCells = this.cellsMergeValues( + reverseRow[indexesOfRights[0]], + reverseRow[indexesOfRights[1]], + ); + + reverseRow[indexesOfRights[0]] = newCells[0]; + reverseRow[indexesOfRights[1]] = newCells[1]; + + if (indexesOfRights.length === 4) { + const newCells2 = this.cellsMergeValues( + reverseRow[indexesOfRights[2]], + reverseRow[indexesOfRights[3]], + ); + + reverseRow[indexesOfRights[2]] = newCells2[0]; + reverseRow[indexesOfRights[3]] = newCells2[1]; + } + } + + const rowAfterMove = Game.cellsMoveValues(reverseRow); + + return rowAfterMove.reverse(); + }); + + const secondState = this.getState(); + + if (!Game.are2DArraysEqual(firstState, secondState)) { + this.addRandomCell(); + } + } + + const gameField = this.getState(); + const flatField = gameField.flat(); + + if (flatField.includes(2048)) { + this.status = 'win'; + } + + if (!this.canCellsMove(this.getState())) { + this.status = 'lose'; + } + } + + moveUp() { + if (this.status === 'playing') { + const firstState = this.getState(); + + const currentState = this.state; + const transState = Game.transposeMatrix(currentState); + + const transMovedState = transState.map((row) => { + const ifEquals = Game.ifEqualSeeEach(row)[0]; + const indexesOfRights = Game.ifEqualSeeEach(row)[1]; + + if (ifEquals) { + const newCells = this.cellsMergeValues( + row[indexesOfRights[0]], + row[indexesOfRights[1]], + ); + + row[indexesOfRights[0]] = newCells[0]; + row[indexesOfRights[1]] = newCells[1]; + + if (indexesOfRights.length === 4) { + const newCells2 = this.cellsMergeValues( + row[indexesOfRights[2]], + row[indexesOfRights[3]], + ); + + row[indexesOfRights[2]] = newCells2[0]; + row[indexesOfRights[3]] = newCells2[1]; + } + } + + const rowAfterMove = Game.cellsMoveValues(row); + + return rowAfterMove; + }); + + this.state = Game.transposeMatrix(transMovedState); + + const secondState = this.getState(); + + if (!Game.are2DArraysEqual(firstState, secondState)) { + this.addRandomCell(); + } + } + + const gameField = this.getState(); + const flatField = gameField.flat(); + + if (flatField.includes(2048)) { + this.status = 'win'; + } + + if (!this.canCellsMove(this.getState())) { + this.status = 'lose'; + } + } + + moveDown() { + const firstState = this.getState(); + + if (this.status === 'playing') { + const currentState = this.state; + const transState = Game.transposeMatrix(currentState); + + const transMovedState = transState.map((row) => { + const reverseRow = [...row].reverse(); + const ifEquals = Game.ifEqualSeeEach(reverseRow)[0]; + const indexesOfRights = Game.ifEqualSeeEach(reverseRow)[1]; + + if (ifEquals) { + const newCells = this.cellsMergeValues( + reverseRow[indexesOfRights[0]], + reverseRow[indexesOfRights[1]], + ); + + reverseRow[indexesOfRights[0]] = newCells[0]; + reverseRow[indexesOfRights[1]] = newCells[1]; + + if (indexesOfRights.length === 4) { + const newCells2 = this.cellsMergeValues( + reverseRow[indexesOfRights[2]], + reverseRow[indexesOfRights[3]], + ); + + reverseRow[indexesOfRights[2]] = newCells2[0]; + reverseRow[indexesOfRights[3]] = newCells2[1]; + } + } + + const rowAfterMove = Game.cellsMoveValues(reverseRow); + + return rowAfterMove.reverse(); + }); + + this.state = Game.transposeMatrix(transMovedState); + + const secondState = this.getState(); + + if (!Game.are2DArraysEqual(firstState, secondState)) { + this.addRandomCell(); + } + } + + const gameField = this.getState(); + const flatField = gameField.flat(); + + if (flatField.includes(2048)) { + this.status = 'win'; + } + + if (!this.canCellsMove(this.getState())) { + this.status = 'lose'; + } + } /** * @returns {number} */ - getScore() {} + getScore() { + return this.score; + } /** * @returns {number[][]} */ - getState() {} + getState() { + return this.state; + } /** * Returns the current game status. @@ -50,19 +389,99 @@ class Game { * `win` - the game is won; * `lose` - the game is lost */ - getStatus() {} + getStatus() { + return this.status; + } /** * Starts the game. */ - start() {} + start() { + this.status = 'playing'; + this.addRandomStartCells(); + } /** * Resets the game. */ - restart() {} + restart() { + this.status = 'idle'; + this.state = this.initialState; + this.score = 0; + } + + addRandomCell() { + const currentState = this.getState(); + const linearState = currentState.flat(); + + const iOfNulls = []; + + linearState.forEach((cell, index) => { + if (cell === 0) { + iOfNulls.push(index); + } + }); + + if (iOfNulls.length > 0) { + const randomIndex = Math.floor(Math.random() * iOfNulls.length); + const randomIOfNull = iOfNulls[randomIndex]; + + let value = 0; - // Add your own methods here + if (Math.random() <= 0.1) { + value = 4; + } else { + value = 2; + } + + linearState[randomIOfNull] = value; + + this.state = Game.convertTo2DArray(linearState); + } + } + + addRandomStartCells() { + const currentState = this.getState(); + const linearState = currentState.flat(); + + const iOfNulls = []; + + linearState.forEach((cell, index) => { + if (cell === 0) { + iOfNulls.push(index); + } + }); + + if (iOfNulls.length > 0) { + const randomIndex1 = Math.floor(Math.random() * iOfNulls.length); + const randomIOfNull1 = iOfNulls[randomIndex1]; + + iOfNulls.splice(randomIndex1, 1); + + const randomIndex2 = Math.floor(Math.random() * iOfNulls.length); + const randomIOfNull2 = iOfNulls[randomIndex2]; + + let value1 = 0; + let value2 = 0; + + if (Math.random() <= 0.5) { + value1 = 4; + } else { + value1 = 2; + } + + if (Math.random() <= 0.5) { + value2 = 4; + } else { + value2 = 2; + } + + linearState[randomIOfNull1] = value1; + linearState[randomIOfNull2] = value2; + + this.state = Game.convertTo2DArray(linearState); + } + } } module.exports = Game; diff --git a/src/scripts/main.js b/src/scripts/main.js index dc7f045a3..c80cbdf96 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -1,7 +1,209 @@ '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'); -// Write your code here +const game = new Game([ + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 0], +]); + +const gameBody = document.querySelector('.game-field tbody'); +const gameHeader = document.querySelector('.game-header'); +const button = gameHeader.querySelector('.button'); +const gameScore = document.querySelector('.game-score'); + +function updateBoard() { + gameBody.innerHTML = ''; + + game.getState().forEach((row) => { + const rowElement = document.createElement('tr'); + + rowElement.classList.add('field-row'); + + row.forEach((cell) => { + const cellElement = document.createElement('td'); + + cellElement.classList.add('field-cell'); + + if (cell > 0) { + cellElement.classList.add(`field-cell--${cell}`); + } + + cellElement.textContent = cell === 0 ? '' : cell; + rowElement.appendChild(cellElement); + }); + gameBody.appendChild(rowElement); + }); + gameScore.textContent = `${game.getScore()}`; +} + +function ifMaxScore() { + if (game.getStatus() === 'win') { + removeEventListeners(); + document.querySelector('.message-win').classList.remove('hidden'); + } +} + +function ifStateBlocked() { + document.querySelector('.message-lose').classList.remove('hidden'); +} + +function are2DArraysEqual(arr1, arr2) { + if (arr1.length !== arr2.length) { + return false; + } + + return arr1.every((subArr1, index) => { + const subArr2 = arr2[index]; + + if (subArr1.length !== subArr2.length) { + return false; + } + + return subArr1.every((value, subIndex) => value === subArr2[subIndex]); + }); +} + +function clickOnButton() { + if (button.classList.contains('start')) { + const messageStart = document.querySelector('.message-start'); + + button.classList.remove('start'); + button.classList.add('restart'); + button.textContent = 'restart'; + messageStart.classList.add('hidden'); + + const gameField = document.querySelector('.game-field'); + const cells = gameField.querySelectorAll('.field-cell'); + const regularArray = game.initialState.flat(); + + document.querySelector('.game-score').textContent = 0; + + cells.forEach((cell, index) => { + if (regularArray[index] > 0) { + cell.textContent = regularArray[index]; + + cell.classList.add(`field-cell--${regularArray[index]}`); + } + }); + + game.start(); + updateBoard(); + addEventListeners(); + } else { + game.restart(); + + document.querySelector('.message-lose').classList.add('hidden'); + document.querySelector('.message-win').classList.add('hidden'); + button.classList.remove('restart'); + button.classList.add('start'); + button.textContent = 'start'; + + updateBoard(); + + addEventListeners(); + } +} + +button.addEventListener('click', clickOnButton); + +function moveLeft() { + if (event.key === 'ArrowLeft') { + game.moveLeft(); + + updateBoard(); + + ifMaxScore(); + + if (game.getStatus === 'lose') { + ifStateBlocked(); + } + + updateBoard(); + } +} + +function moveRight() { + if (event.key === 'ArrowRight') { + const firstState = game.getState(); + + game.moveRight(); + + const secondState = game.getState(); + + updateBoard(); + + ifMaxScore(); + + if (!game.canCellsMove(game.getState())) { + ifStateBlocked(); + } + + if (!are2DArraysEqual(firstState, secondState)) { + game.addRandomCell(); + updateBoard(); + } + } +} + +function moveUp() { + if (event.key === 'ArrowUp') { + const firstState = game.getState(); + + game.moveUp(); + + const secondState = game.getState(); + + updateBoard(); + + ifMaxScore(); + + if (!game.canCellsMove(game.getState())) { + ifStateBlocked(); + } + + if (!are2DArraysEqual(firstState, secondState)) { + game.addRandomCell(); + updateBoard(); + } + } +} + +function moveDown() { + if (event.key === 'ArrowDown') { + const firstState = game.getState(); + + game.moveDown(); + + const secondState = game.getState(); + + updateBoard(); + + ifMaxScore(); + + if (!game.canCellsMove(game.getState())) { + ifStateBlocked(); + } + + if (!are2DArraysEqual(firstState, secondState)) { + game.addRandomCell(); + updateBoard(); + } + } +} + +function removeEventListeners() { + document.removeEventListener('keydown', moveLeft); + document.removeEventListener('keydown', moveRight); + document.removeEventListener('keydown', moveUp); + document.removeEventListener('keydown', moveDown); +} + +function addEventListeners() { + document.addEventListener('keydown', moveLeft); + document.addEventListener('keydown', moveRight); + document.addEventListener('keydown', moveUp); + document.addEventListener('keydown', moveDown); +}