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..e40cec244 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://pasha28091997.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..755cd3c2e 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,9 +1467,9 @@ "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, "dependencies": { "@octokit/rest": "^17.11.2", 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/images/41NgOgTVblL.png b/src/images/41NgOgTVblL.png new file mode 100644 index 000000000..8e692c1b0 Binary files /dev/null and b/src/images/41NgOgTVblL.png differ diff --git a/src/index.html b/src/index.html index aff3d1a98..cb8eaab14 100644 --- a/src/index.html +++ b/src/index.html @@ -11,6 +11,11 @@ rel="stylesheet" href="./styles/main.scss" /> +
@@ -65,6 +70,9 @@

2048

- + diff --git a/src/modules/Game.class.js b/src/modules/Game.class.js index 65cd219c9..4d57cc23a 100644 --- a/src/modules/Game.class.js +++ b/src/modules/Game.class.js @@ -1,3 +1,4 @@ +/* eslint-disable function-paren-newline */ 'use strict'; /** @@ -21,24 +22,147 @@ class Game { * initial state. */ constructor(initialState) { - // eslint-disable-next-line no-console - console.log(initialState); + this.board = initialState || this.createEmptyBoard(); + this.score = 0; + this.status = 'idle'; } - moveLeft() {} - moveRight() {} - moveUp() {} - moveDown() {} + createEmptyBoard() { + return Array(4) + .fill() + .map(() => Array(4).fill(0)); + } + + moveLeft() { + let moved = false; + + for (const row of this.board) { + const newRow = this.merge(this.slide(row)); + + if (!this.arraysEqual(row, newRow)) { + moved = true; + } + Object.assign(row, newRow); + } + + if (moved) { + this.addRandomTile(); + } + } + + moveRight() { + let moved = false; + + for (const row of this.board) { + const reversedRow = row.slice().reverse(); + const newRow = this.merge(this.slide(reversedRow)).reverse(); + + if (!this.arraysEqual(row, newRow)) { + moved = true; + + Object.assign(row, newRow); + } + } + + if (moved) { + this.addRandomTile(); + } + } + + moveUp() { + let moved = false; + + for (let col = 0; col < 4; col++) { + const column = this.getColumn(col); + const newColumn = this.merge(this.slide(column)); + + if (!this.arraysEqual(column, newColumn)) { + moved = true; + this.setColumn(col, newColumn); + } + } + + if (moved) { + this.addRandomTile(); + } + } + + moveDown() { + let moved = false; + + for (let col = 0; col < 4; col++) { + const column = this.getColumn(col).reverse(); + const newColumn = this.merge(this.slide(column)).reverse(); + + if (!this.arraysEqual(column, newColumn)) { + moved = true; + this.setColumn(col, newColumn); + } + } + + if (moved) { + this.addRandomTile(); + } + } + + getColumn(index) { + const column = []; + + for (let row = 0; row < 4; row++) { + column.push(this.board[row][index]); + } + + return column; + } + + setColumn(index, newColumn) { + for (let row = 0; row < 4; row++) { + this.board[row][index] = newColumn[row]; + } + } + + slide(row) { + const newRow = row.filter((value) => value !== 0); + + while (newRow.length < 4) { + newRow.push(0); + } + + return newRow; + } + + merge(row) { + for (let i = 0; i < row.length - 1; i++) { + if (row[i] !== 0 && row[i] === row[i + 1]) { + row[i] *= 2; + row[i + 1] = 0; + this.score += row[i]; + } + } + + return this.slide(row); + } + + arraysEqual(arr1, arr2) { + return ( + arr1.length === arr2.length && + arr1.every((value, index) => value === arr2[index]) + ); + } /** * @returns {number} */ - getScore() {} + getScore() { + return this.score; + } /** * @returns {number[][]} */ - getState() {} + getState() { + return this.board; + } /** * Returns the current game status. @@ -50,19 +174,74 @@ class Game { * `win` - the game is won; * `lose` - the game is lost */ - getStatus() {} + getStatus() { + if (this.board.some((row) => row.includes(2048))) { + return 'win'; + } + + const hasMoves = this.board.some((row, rowIndex) => + row.some((value, colIndex) => { + if (value === 0) { + return true; + } + + if (colIndex < 3 && value === row[colIndex + 1]) { + return true; + } + + if (rowIndex < 3 && value === this.board[rowIndex + 1][colIndex]) { + return true; + } + + return false; + }), + ); + + if (!hasMoves) { + return 'lose'; + } + + return 'playing'; + } /** * Starts the game. */ - start() {} + start() { + if (this.status === 'idle') { + this.status = 'playing'; + this.addRandomTile(); + this.addRandomTile(); + } + } /** * Resets the game. */ - restart() {} + restart() { + this.board = this.createEmptyBoard(); + this.score = 0; + this.status = 'idle'; + this.start(); + } - // Add your own methods here -} + addRandomTile() { + const emptyCells = []; + + for (let row = 0; row < this.board.length; row++) { + for (let col = 0; col < this.board[row].length; col++) { + if (this.board[row][col] === 0) { + emptyCells.push({ row, col }); + } + } + } + if (emptyCells.length > 0) { + const { row: newRow, col: newCol } = + emptyCells[Math.floor(Math.random() * emptyCells.length)]; + + this.board[newRow][newCol] = Math.random() < 0.9 ? 2 : 4; + } + } +} module.exports = Game; diff --git a/src/scripts/main.js b/src/scripts/main.js index dc7f045a3..c8dfe576a 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -4,4 +4,99 @@ // const Game = require('../modules/Game.class'); // const game = new Game(); -// Write your code here +import Game from '../modules/Game.class.js'; + +const game = new Game(); +let isGameStarted = false; + +// eslint-disable-next-line no-shadow +document.addEventListener('keydown', (event) => { + if (!isGameStarted) { + return; + } + + // eslint-disable-next-line no-shadow + const status = game.getStatus(); + + if (status !== 'playing') { + return; + } + + switch (event.key) { + case 'ArrowLeft': + game.moveLeft(); + break; + case 'ArrowRight': + game.moveRight(); + break; + case 'ArrowUp': + game.moveUp(); + break; + case 'ArrowDown': + game.moveDown(); + break; + default: + return; + } + + updateUI(); +}); + +function updateUI() { + const board = game.getState(); + const score = game.getScore(); + // eslint-disable-next-line no-shadow + const status = game.getStatus(); + + document.querySelector('.game-score').textContent = score; + + const cells = document.querySelectorAll('.field-cell'); + let index = 0; + + for (const row of board) { + for (const value of row) { + const cell = cells[index]; + + cell.className = 'field-cell'; + + if (value !== 0) { + cell.classList.add(`field-cell--${value}`); + cell.textContent = value; + } else { + cell.textContent = ''; + } + index++; + } + } + + document.querySelector('.message-start').classList.add('hidden'); + + document + .querySelector('.message-lose') + .classList.toggle('hidden', status !== 'lose'); + + document + .querySelector('.message-win') + .classList.toggle('hidden', status !== 'win'); + + const startButton = document.querySelector('.button.start'); + + if (status === 'idle') { + startButton.textContent = 'Start'; + startButton.classList.remove('restart'); + } else { + startButton.textContent = 'Restart'; + startButton.classList.add('restart'); + } +} + +document.querySelector('.button.start').addEventListener('click', () => { + if (game.getStatus() === 'idle') { + game.start(); + isGameStarted = true; + } else { + game.restart(); + isGameStarted = true; + } + updateUI(); +}); diff --git a/src/styles/main.scss b/src/styles/main.scss index c43f37dcf..f206fdbc2 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -139,7 +139,7 @@ h1 { .start { background: #1dae28; - font-size: 20px; + font-size: 18px; &:hover { background: #179921; @@ -148,6 +148,7 @@ h1 { .restart { background: #f1b2b2; + font-size: 17px; &:hover { background: #f87474;