├── assets ├── favicon.ico ├── battlesquare.png ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png └── android-chrome-512x512.png ├── .gitignore ├── util.js ├── site.webmanifest ├── init.js ├── setup.js ├── index.html ├── README.md ├── bot.js ├── explosion.js ├── style.css └── game.js /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagokroger/battlesquare/HEAD/assets/favicon.ico -------------------------------------------------------------------------------- /assets/battlesquare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagokroger/battlesquare/HEAD/assets/battlesquare.png -------------------------------------------------------------------------------- /assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagokroger/battlesquare/HEAD/assets/favicon-16x16.png -------------------------------------------------------------------------------- /assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagokroger/battlesquare/HEAD/assets/favicon-32x32.png -------------------------------------------------------------------------------- /assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagokroger/battlesquare/HEAD/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagokroger/battlesquare/HEAD/assets/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thiagokroger/battlesquare/HEAD/assets/android-chrome-512x512.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | backend 26 | keys.json 27 | 28 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | function getElement(selector) { 2 | return document.querySelector(selector); 3 | } 4 | function setElementText(element, text) { 5 | element.innerHTML = text; 6 | } 7 | function setElementTextColor(element, color) { 8 | element.style.color = color; 9 | } 10 | function hideElement(element) { 11 | element.style.display = "none"; 12 | } 13 | function showElement(element) { 14 | element.style.display = "flex"; 15 | } 16 | function sleep(ms) { 17 | return new Promise((resolve) => setTimeout(resolve, ms)); 18 | } 19 | function fadeIn(element) { 20 | element.style.display = "flex"; 21 | element.style.opacity = 1; 22 | } 23 | function fadeOut(element) { 24 | element.style.display = "none"; 25 | element.style.opacity = 0; 26 | } 27 | -------------------------------------------------------------------------------- /site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BattleSquare.io", 3 | "short_name": "BattleSquare", 4 | "description": "A turn-based strategy vanilla javascript game.", 5 | "icons": [ 6 | { 7 | "src": "/assets/apple-touch-icon.png", 8 | "sizes": "180x180", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "/assets/favicon-32x32.png", 13 | "sizes": "32x32", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "/assets/favicon-16x16.png", 18 | "sizes": "16x16", 19 | "type": "image/png" 20 | } 21 | ], 22 | "start_url": "/index.html", 23 | "display": "standalone", 24 | "background_color": "#000000", 25 | "theme_color": "#000000", 26 | "orientation": "portrait-primary" 27 | } 28 | -------------------------------------------------------------------------------- /init.js: -------------------------------------------------------------------------------- 1 | let elements; 2 | document.addEventListener("animationstart", async function (e) { 3 | if (e.animationName === "fade-in") { 4 | e.target.classList.add("did-fade-in"); 5 | 6 | await sleep(2000); 7 | 8 | e.target.classList.add("fade-out"); 9 | await sleep(1000); 10 | e.target.classList.remove("did-fade-in"); 11 | e.target.classList.remove("fade-out"); 12 | } 13 | }); 14 | 15 | document.addEventListener("DOMContentLoaded", function () { 16 | elements = { 17 | app: { container: getElement("#app") }, 18 | start: { 19 | container: getElement("#start"), 20 | options: getElement("#startOptions"), 21 | levels: getElement("#startLevels"), 22 | settings: getElement("#startSettings"), 23 | about: getElement("#startAbout"), 24 | }, 25 | game: { 26 | container: getElement("#game"), 27 | board: getElement("#board"), 28 | toolbar: getElement("#toolbar"), 29 | bottombar: getElement("#bottombar"), 30 | frostMines: getElement("#frostMines"), 31 | timer: getElement("#timer"), 32 | turn: getElement("#turn"), 33 | finishTurn: getElement("#finishTurn"), 34 | moves: getElement("#moves"), 35 | }, 36 | result: { 37 | container: getElement("#result"), 38 | winner: getElement("#resultWinner"), 39 | board: getElement("#resultBoard"), 40 | level: getElement("#resultLevel"), 41 | info: getElement("#resultInfo"), 42 | }, 43 | util: { 44 | glass: getElement("#glass"), 45 | }, 46 | turnTransition: { 47 | container: getElement("#turnTransition"), 48 | turn: getElement("#turnTransitionTurn"), 49 | player: getElement("#turnTransitionPlayer"), 50 | moves: getElement("#turnTransitionMoves"), 51 | }, 52 | settings: { 53 | sound: getElement("#settingsSound"), 54 | transitions: getElement("#settingsTransitions"), 55 | speed: getElement("#settingsSpeed"), 56 | }, 57 | }; 58 | loadSettings(); 59 | }); 60 | -------------------------------------------------------------------------------- /setup.js: -------------------------------------------------------------------------------- 1 | function getInitialState(level = "hard") { 2 | const initialState = { 3 | players: { 4 | 1: { 5 | color: "blue", 6 | focusColor: "darkBlue", 7 | frostMines: 3, 8 | }, 9 | 2: { 10 | color: "red", 11 | focusColor: "darkRed", 12 | bot: true, 13 | botLevel: level, 14 | frostMines: 3, 15 | }, 16 | }, 17 | board: { 18 | squares: [], 19 | superSquares: [], 20 | numRows: 8, 21 | numCols: 6, 22 | numPlayers: 2, 23 | squareSize: 128, 24 | }, 25 | currentAction: { 26 | player: 0, 27 | turn: 0, 28 | turnMoves: 0, 29 | remainingMoves: 3, 30 | }, 31 | level, 32 | }; 33 | initialState.board.squares = getCheckersStyle( 34 | initialState.board.numRows, 35 | initialState.board.numCols 36 | ); 37 | 38 | return initialState; 39 | } 40 | 41 | function setup(level) { 42 | const initialState = getInitialState(level); 43 | const boardConfig = initialState.board; 44 | const boardDiv = elements.game.board; 45 | const toolbarDiv = elements.game.toolbar; 46 | const bottombarDiv = elements.game.bottombar; 47 | const gameDiv = elements.game.container; 48 | 49 | boardDiv.innerHTML = ""; 50 | boardDiv.style.width = "100%"; 51 | 52 | const viewportHeight = 53 | gameDiv.clientHeight - 54 | toolbarDiv.clientHeight - 55 | bottombarDiv.clientHeight - 56 | 128; 57 | const viewportWidth = boardDiv.clientWidth; 58 | 59 | const squareSizeBasedOnWidth = Math.floor( 60 | viewportWidth / boardConfig.numCols 61 | ); 62 | const squareSizeBasedOnHeight = Math.floor( 63 | viewportHeight / boardConfig.numRows 64 | ); 65 | let squareSize = squareSizeBasedOnWidth; 66 | 67 | if (squareSizeBasedOnHeight < squareSizeBasedOnWidth) { 68 | squareSize = squareSizeBasedOnHeight; 69 | } 70 | 71 | boardDiv.style.width = squareSize * boardConfig.numCols + 4 + "px"; 72 | toolbarDiv.style.width = boardDiv.style.width; 73 | bottombarDiv.style.width = boardDiv.style.width; 74 | 75 | const defaultParentSquare = document.createElement("div"); 76 | const defaultSquare = document.createElement("div"); 77 | defaultParentSquare.style.width = squareSize + "px"; 78 | defaultParentSquare.style.height = squareSize + "px"; 79 | defaultSquare.classList.add("square"); 80 | 81 | let clone; 82 | for (let x = 1; x <= boardConfig.numRows; x++) { 83 | for (let y = 1; y <= boardConfig.numCols; y++) { 84 | clone = defaultSquare.cloneNode(true); 85 | clone.id = `square-${x}-${y}`; 86 | boardDiv 87 | .appendChild(defaultParentSquare.cloneNode(true)) 88 | .appendChild(clone); 89 | } 90 | } 91 | const board = { 92 | squares: [], 93 | }; 94 | let row = 1, 95 | col = 1; 96 | 97 | boardConfig.squares.forEach((num) => { 98 | const el = document.querySelector(`#square-${row}-${col}`); 99 | 100 | const color = getColor(initialState.players[num].color); 101 | el.style.backgroundColor = color; 102 | el.parentNode.style.backgroundColor = color; 103 | 104 | if (!board[row]) { 105 | board[row] = {}; 106 | } 107 | board[row][col] = { player: num, el, x: row, y: col }; 108 | board.squares.push(board[row][col]); 109 | col++; 110 | if (col > boardConfig.numCols) { 111 | col = 1; 112 | row++; 113 | } 114 | }); 115 | 116 | boardConfig.superSquares.forEach(([player, [x, y], [finalX, finalY]]) => { 117 | const size = finalY - y + 1; 118 | const superSquare = { 119 | player, 120 | size, 121 | x, 122 | y, 123 | }; 124 | const squareList = getSquaresByRange(board, x, y, finalX, finalY); 125 | squareList.forEach((square) => { 126 | square.superSquare = superSquare; 127 | }); 128 | }); 129 | 130 | initialState.board = { 131 | ...boardConfig, 132 | ...board, 133 | }; 134 | 135 | return initialState; 136 | } 137 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 9 | 15 | 21 | 22 | 23 | 24 | 28 |
43 | 29 | Welcome to BattleSquare, a turn-based, vanilla javascript, mobile friendly strategy game that pits you against the computer in a battle of strategy on an 8x6 chess-like board. Each player starts with half of the board in a mixed setup to prevent any initial 2x2 squares. The goal is simple: conquer the entire board to win. 30 |
31 | 32 |  33 | 34 | 35 |38 | The board is your battlefield. With turn-based gameplay, expand your influence by forming larger squares and employ frost mines to thwart your opponent's advances. Challenge yourself with one of the three difficulty levels against the computer. 39 |
40 | 41 |54 | Begin by choosing your difficulty level and then make your moves to conquer the board. Use frost mines after your moves to create obstacles for the opponent. Victory is achieved by capturing all squares on the board. 55 |
56 | 57 |70 | Play BattleSquare online at BattleSquare.io. Test your strategy against our AI with no setup required. 71 |
72 | 73 |  74 | 75 | 76 |
79 | To run BattleSquare locally, clone the repository to your machine, navigate to the project directory, and run the game with the command npx vite.
80 |
$ git clone [repository-link]
83 | $ cd [project-directory]
84 | $ npx vite
85 |
86 |
87 | 88 | Open your web browser and visit http://localhost:5173 to start playing! 89 |
90 | 91 |  92 | 93 | 94 |97 | BattleSquare was inspired by the classic strategic and tactical games of checkers and risk. May the best strategist win! 98 |
99 | 100 | 101 | 102 |  103 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | const getBot = () => { 2 | const bot = (() => { 3 | const levels = { 4 | easy: [randomMove], 5 | medium: [ 6 | possiblyDestroyOpponentSquare, 7 | possiblyCreateSuperSquare, 8 | randomMove, 9 | ], 10 | hard: [ 11 | possiblyDestroyOpponentCreatingSquareAvoidingOpponentCreation, 12 | possiblyDestroyOpponentSquareAvoidingOpponentCreation, 13 | possiblyDestroyOpponentSquare, 14 | possiblyCreateSuperSquare, 15 | randomMove, 16 | ], 17 | }; 18 | 19 | const lastDestinationByPlayer = {}; 20 | 21 | function getVirtualBoard(board) { 22 | const virtualBoard = JSON.parse(JSON.stringify(board)); 23 | virtualBoard.squares = []; 24 | 25 | for (let x = 1; x <= virtualBoard.numRows; x++) { 26 | for (let y = 1; y <= virtualBoard.numCols; y++) { 27 | virtualBoard.squares.push(virtualBoard[x][y]); 28 | } 29 | } 30 | 31 | return virtualBoard; 32 | } 33 | 34 | function getRandomNumber(min, max) { 35 | min = Math.ceil(min); 36 | max = Math.floor(max); 37 | return Math.floor(Math.random() * (max - min + 1)) + min; 38 | } 39 | 40 | const getNextMove = (board, player, level = "easy") => { 41 | const functions = levels[level]; 42 | let move; 43 | 44 | functions.some((fn) => { 45 | move = fn(board, player); 46 | if (move) { 47 | lastDestinationByPlayer[player] = move[0]; 48 | return true; 49 | } 50 | }); 51 | 52 | const [destination, origin] = randomMove(board, player); 53 | plantFrostMine(origin); 54 | 55 | return move; 56 | }; 57 | 58 | function getShuffleMove(list, player) { 59 | const randomSquareIndex = getRandomNumber(0, list.length - 1); 60 | const randomSquare = list[randomSquareIndex]; 61 | 62 | if (randomSquare.player !== player) { 63 | return getShuffleMove(list, player); 64 | } 65 | const possibleMoves = getAdjacentPossibleMoves( 66 | randomSquare.x, 67 | randomSquare.y 68 | ); 69 | 70 | if (possibleMoves.length) { 71 | const move = getRandomNumber(0, possibleMoves.length - 1); 72 | return possibleMoves[move]; 73 | } 74 | 75 | return getShuffleMove(list, player); 76 | } 77 | 78 | function randomMove(board, player) { 79 | const possibleMoves = getPlayerPossibleMoves(board, player); 80 | 81 | if (!possibleMoves.length) { 82 | return; 83 | } 84 | const nextMoveIndex = getRandomNumber(0, possibleMoves.length - 1); 85 | 86 | return possibleMoves[nextMoveIndex]; 87 | } 88 | 89 | function getPlayerPossibleMoves(board, player) { 90 | let possibleMoves = []; 91 | 92 | for (let x = 1; x <= board.numRows; x++) { 93 | for (let y = 1; y <= board.numCols; y++) { 94 | if (board[x][y].player === player) { 95 | possibleMoves = possibleMoves.concat( 96 | getAdjacentPossibleMoves(x, y) 97 | ); 98 | } 99 | } 100 | } 101 | return possibleMoves; 102 | } 103 | 104 | function possiblyDestroyOpponentSquare(board, player) { 105 | const possibleMoves = getPlayerPossibleMoves(board, player); 106 | 107 | let moves = []; 108 | 109 | possibleMoves.forEach(([destinationSquare, originSquare]) => { 110 | if (destinationSquare.superSquare) { 111 | moves.push([destinationSquare, originSquare]); 112 | } 113 | }); 114 | const nextMoveIndex = getRandomNumber(0, moves.length - 1); 115 | 116 | return moves[nextMoveIndex]; 117 | } 118 | 119 | function possiblyDestroyOpponentSquareAvoidingOpponentCreation( 120 | board, 121 | player 122 | ) { 123 | let virtualBoard = getVirtualBoard(board); 124 | const possibleMoves = getPlayerPossibleMoves(virtualBoard, player); 125 | 126 | let moves = []; 127 | 128 | possibleMoves.forEach(([destinationSquare, originSquare]) => { 129 | virtualBoard = getVirtualBoard(board); 130 | destinationSquare = 131 | virtualBoard[destinationSquare.x][destinationSquare.y]; 132 | originSquare = virtualBoard[originSquare.x][originSquare.y]; 133 | if (destinationSquare.superSquare) { 134 | const tempPlayer = destinationSquare.player; 135 | 136 | const previousMoves = getNumberOfMoves(virtualBoard, tempPlayer); 137 | 138 | destinationSquare.player = originSquare.player; 139 | 140 | // check if the prior move will result in a square creation for the opponent 141 | decreaseSuperSquare(destinationSquare, tempPlayer, virtualBoard); 142 | 143 | const newMoves = getNumberOfMoves(virtualBoard, tempPlayer); 144 | 145 | if (newMoves >= previousMoves) { 146 | destinationSquare.player = tempPlayer; 147 | return; 148 | } 149 | 150 | moves.push([destinationSquare, originSquare]); 151 | destinationSquare.player = tempPlayer; 152 | } 153 | }); 154 | const nextMoveIndex = getRandomNumber(0, moves.length - 1); 155 | 156 | return moves[nextMoveIndex]; 157 | } 158 | 159 | function possiblyCreateSuperSquare(board, player) { 160 | const possibleMoves = getPlayerPossibleMoves(board, player); 161 | 162 | let moves = []; 163 | 164 | possibleMoves.forEach(([destinationSquare, originSquare]) => { 165 | const tempPlayer = destinationSquare.player; 166 | destinationSquare.player = originSquare.player; 167 | if (checkSuperSquareCreationPossibly(destinationSquare, originSquare)) { 168 | moves.push([destinationSquare, originSquare]); 169 | } 170 | destinationSquare.player = tempPlayer; 171 | }); 172 | const nextMoveIndex = getRandomNumber(0, moves.length - 1); 173 | 174 | return moves[nextMoveIndex]; 175 | } 176 | 177 | function possiblyDestroyOpponentCreatingSquareAvoidingOpponentCreation( 178 | board, 179 | player 180 | ) { 181 | let virtualBoard = getVirtualBoard(board); 182 | const possibleMoves = getPlayerPossibleMoves(virtualBoard, player); 183 | 184 | let moves = []; 185 | 186 | possibleMoves.forEach(([destinationSquare, originSquare]) => { 187 | virtualBoard = getVirtualBoard(board); 188 | destinationSquare = 189 | virtualBoard[destinationSquare.x][destinationSquare.y]; 190 | originSquare = virtualBoard[originSquare.x][originSquare.y]; 191 | 192 | const tempPlayer = destinationSquare.player; 193 | 194 | const previousMoves = getNumberOfMoves(virtualBoard, tempPlayer); 195 | 196 | destinationSquare.player = originSquare.player; 197 | 198 | if (!destinationSquare.superSquare) { 199 | destinationSquare.player = tempPlayer; 200 | return; 201 | } 202 | 203 | // check if the prior move will result in a square creation for the opponent 204 | decreaseSuperSquare(destinationSquare, tempPlayer, virtualBoard); 205 | 206 | const newMoves = getNumberOfMoves(virtualBoard, tempPlayer); 207 | 208 | if (newMoves >= previousMoves) { 209 | destinationSquare.player = tempPlayer; 210 | return; 211 | } 212 | 213 | if (checkSuperSquareCreationPossibly(destinationSquare, originSquare)) { 214 | moves.push([destinationSquare, originSquare]); 215 | } 216 | destinationSquare.player = tempPlayer; 217 | }); 218 | const nextMoveIndex = getRandomNumber(0, moves.length - 1); 219 | 220 | return moves[nextMoveIndex]; 221 | } 222 | 223 | return { 224 | getNextMove, 225 | getShuffleMove, 226 | }; 227 | })(); 228 | return bot; 229 | }; 230 | -------------------------------------------------------------------------------- /explosion.js: -------------------------------------------------------------------------------- 1 | const explosionAudioEffect = new Audio(); 2 | explosionAudioEffect.src = 3 | "data:audio/mpeg;base64,//uQBAAAAn48TI0YYABVKflgoaAADTEThbk5oBGwIq/3JSICAPnkyabEEHAARD3d7zgQAShxcAwN//R3d4EABFAAAAQq5x3f//4if/1z4n/6IlCIVd39zy67wDfhbnAMWYgBCGOU8MLB+Xf/EYfUCDgQdlHOIVc0AwN9AAATQxK3eyA7F3hET/0r//5d33e/l75d/RKkUMrf/////r3vd79E5ROX/0RE////0d70ROURNxcXsgBcG58ECgYKBjg+fghW8QHAQ6vZQMBgMBgMBgMBgKBAAACbQCx+B5eBtL4DQAAr+CZgLE8DgwHDD2vwCmCzQxMBEP+CgQuDBsgQuDYv/ysKQAeEUcRyLf/+JTDIIWnBwA+xkBb//8ZogI6A9YWEnh1HB2f//opMQ8rJmtRs2A4HA4HA4HA4HA4FAgAnwUv4AQAvW0DTguL4WEgS/wMgBHZR/FliABJBqj/A7ELgxaAvcDbD/xSgZEC0AZkOuI3//LhEw2QPcEJBySl//ipl0UGTJOCU0jxFP//0U1GhPsmiUBWh7/+sGIeiyYgg//uSBAAAAvoyWB8kwAJbibtp4YwBjAFLcyGEcnGAK220MIvVAAJc1pYYBpiatamh8srdk9vsPVt/DZQaZaaQbHM7UtItHN/PLSech+8U+JZeV9fxuvf7Q/y3nKjuie6l9UuBzDxbKKdcq65rGDogHwT9rXf/u/bt/bzO/hwPt69r9XWSWloBAGjBBxAupFUcIYRzZ7SCZKc3JHhohUir8JWDeqxB6GMQRu9wZOtKqu1Y7tMs39lLIjo+X3k80y89jY6cI0IuT8iO9yz7n7ghCPrULGW2cLj2NPEEsDp0USf/5lVv1WozRpVWkwvRygZTXBFTYAGrFHK6aQfkhvOnWyIk0GSqRhjySqouEZmcuRarBEgqIcic0YpozmOSldWKFGtFrC0cykEM5tAHOS+Zhww5E/KoU6CKnBht6wWJqApryrTRTbm+q0AFMzNoJXbNAohJMjubAgawgiMw2bQlwvb3FYYi/88EgpWBdPLlRxsArilK4YGZVkwbFRm292U5NBAYUSHMfYzzoLmr50O91eEdwXiLJv/HUQ+uD2MvETEFNP/7kgQAAALfTd1IYR2cXom7bQwjcwvROV1EmGMJhBmtNGCOhQ6//pmb9dA4cS1YgaETotjKbGVi7ZIzqOBwmmeE1wZorKW7pMp5Q1PrEiCrK8VxJeRVapKnoRie/+nidVds35c8nJs1nWM1IqwpwsoTiKKEABeIWzh4tfW49iUYBA7G3ImpJN8LBAMOUcUJB1JSgc2mNxwx7WqylrdFflWdZmVKRrV9GOie0vYQ5Em6I0pgqy/SdpEYyUhX9+rOfDL98z9EzOTODGCv9CQQ1dLuWGGEhpQeJ54q9yK1AyADigAJTmvKKBcKWnMsluYZK76s9aIk1mRDq5M8jxyILsqpTXMvN3L03fHNYWWiz5YEz1M+ZEyW1Ix/6T4jKxlvn8qymCMvPIjvUyUNJWp0MnXcY1/OOXH2/3/a4qwGwzXZZG023LuIY8KXbkaa5zHva702H9ErM1q0F2KptqJCO5MbqLkNFqUmyetGEEQVgxGT0vuRXJ9WZ0DELczypqfNIjdkYfFXWDzvs7/qFJ6DRE//4vjvhsPjex32Kd3A9piCmor/+5IEAAAi9khW0SEaIF7pusoNIwYLyTtbpJhjAXanq3RkjBBAlGAAQlJaWXBBqmMmXMbRN3aoCIlOmbqjVpSMTOK1KcgsWYVTXjPuUtyL05HPY1U0NdFXqrmSlTbRlm6L+fzLNM0bM1J8Wua47Qri0rzbqigsbIWIFxUJRwFRYD4seAHQQAAlOOBi4UP9S1V4tX0rZMIU1arnUFKhGp2BCzMJGjDiEqkbnq+qEPjEy03X2v4NmU2Cp6/NiaItinr/5SXOFCMn/n1ElNcpTMsgWLFgMjGRRZxOk6ocscU2zqRgCAMLAAAScsURh8NCGSQmE2qTVmaotqPhMEoLaCe2Tckoi5x80kJhJUzY6rwxOFc69ech7SYU5vfQtseJJR6ZaH+11jSbU3R33uT2eormdvYL1UX3gCs0gMXwfZyjGAIExEhN2CAsHxUUW1hdoE0QmIiOxXLMl26Alc1rCIZsqMYvIERDXpfM2iBeWEyNouRSLwLqZGhst2/O/bqTc/dXyPpmVMyVl7k2Q0zOKLgfPLqWuP7hoAMcHWObYUTEFNRQ//uSBAAAIudK1lEmGBBeyuqqIGKeC/EpXUGMVcl6pypIkw14BBmREEpOyQyMLlKrCB5DJeDiEhPnt3COizhEg7tjXBpysYznjqzp8DD0tZ7qhkzBTSzIzTZ+QsyL56Ll7322hEp2/9/bj/nwMFDiN6jDBVSgOxs/etamvDTGTowaEEUAAABMZQHhoCJY8xVIF1tIPa0d77qikIVLkNIPGCVoRsTm+rHh2gdAdEFFqtMe9OL8qzJPnG9WX4TGiVvLupnL//npBn3MyIpqnKOR6qjM6JXM8epJ4DTb2pW7VKgK9SSCm7sHIAJIAorFQ7mu7DFSjFpq2bOXyq88isgLywYJvhlocCy5wT5L9FthDhoWlKs0WmZmZdYvteHfZwokyl+i/Q6DzSX4Zx1Hnj9ruvJIGcm7119rjHOlSPFDI5XDcgsWDyTFIHTuraWd+nlU/VrEcKO/nJvEFo2Hg9zzOlV0VDCxgiDpNFotYDIhfYZTa1AcZFeIeSnHek1acmb7Lw2X+PMLcmNwa155Lim/IKBEFzqfg8Zape1HLW5lMQU1FP/7kgQAAALzPlZowRwiXinK/BgjMQvg91TmBGYJfSusNDGKfQBCUUSCAU3ZIBUARcIFVrQbiqaLSbHNuIAuzBKdOGHh1/htoT5EWuPmnf6pr2nHEtdEMWyP5s5GesODFeX37fEnn8jjQIsreYIDduhmfELkYkrRM+dzsXZBTt7++3ygAnpxOSVvdRYGWpKrTh1GGoy0zXKuqPYGjhsyyHgTjRlXdxdBNk6xiI7eFp5Zk3qU0IclaIhndpyxTTI9yv0pNWacyyNARM+5maVjLigJpCx+UYFhcmTe7UX70ZtAwYgAAKTjCoaF5nuOJ1mR8XyBpHUYu7va8nwqII3w61Qoz4ISUFssinvFNc/pnDPYns9MOdNSa+UR+MGcpmsDzmC5zuDSLemIDt/w7yb6RABCPkZkpfOo//tMa37HV82AGnNa0wUkm5Bwih8YhtRBCQ5ygyZo7iXpEUBBTh/5ZM8CkSqvZTi9nMHSaAzs9Wa87FGyzLM7kXkTPYd9ofkxLqxHpd70bdDX+hktBHvnISTXowFgLnTMKdf8zBn39+kxBTT/+5IEAAAC7khWUMIaglxJ6r0gI9IMFaFRJgxRwYWzqmTDDFkF8eEQUnLChgKoQ66koYm8WZohusKUpnYENFmLPIieNOzL8pvS9bMGxA8rFNM91I7sZ5z81danmRDNlPyI7mLU+/CLiWmvConZZnFHFSBOUoqINSEyGzd//f//ra/MAAEA0AAFtxwCkTBgPPakM5e6etXtEyPWprpPuqhxl3Uot6sswpNhCXrbuVDmTLWIyNmBtW0MjKkxQmizI/3m2XmsT9JUP9dn5++4nH2TAqdDMycARHfZn2octZIAAGQBa6wfKywcxrHFk2epe81l5m+RTyiCQx51CHjNh9aqjbCqjvwmchy0N4bIt3DOUbEtYsJvckMzCEOQ3eUr2dmaDewC6lq9WxW8+ujEYl6oytaupH6enVX11/9uuogoAAEILf0k4XHJFMRUIwkxuqI1yaBR/Iz2eAhJ/UNoDbyDLygjiw8YoTkqMfqTFqda/DeFGKnxTElunvvxCWk/rcx1fPq5sUVhyWEZl5ZnFlPL38vM24e3fr/y3/+dy+/g2UxB//uSBAAAAupn1EmDFHBebOqJMGVuC901UUWkYEl1LOmkwZY4AAGQARuKY2XBchRqTtvqUtk0rXXbxYPMl8KdGLoOk5uRb4lqORnHOL+qpGpMWGPtbvLITUoc/m1kc/mHo1ydMEZ+t2RldBRyFrPctDoqG6Llz6FBN2b/t17ZvVoNwBJQAGqkNEmBwvFdE3aGVEb95kUDW7rjEZvQ1NIua4LIPLS2kYHXdSygIg2R82bV2Ye4gZyspqozdnOY6FIQham+8nRlo/ZVKeVrHuufQQNp+flRnR7em9X/XZvsN0AgtKAAilGVip4nxJVDOpZh2PqsrMeRJmzHsxhbDKbN0K5m+UBZSzJiBgnGzQe7S6+dzsI5lD1ItD85U4RbNO+xv8M+/nJoWvXvKfh1eSHMxCzhnb7fF0ktli50n/vpIAIDAAEohhoWx6NI1NbUWPv2f3+nddMwzkdyaQUY8FqpLBEBZiPVT47CaUqqhaq3j5WTJyJ5Shp8p6Sp33oxNW6U/su1Sy+lEq5MIm07ZFduURmdxfZcDZQmLGtITTEFNRQAAP/7kgQAAALyZ1VQYReCXqxKqgxirkvlQ0hEmGvBc6uqJIGKeAUK4IKKclE4wUOBgmrmYtkzVkqVvK+SzaT/NpwicvcTWNxtCGR6kRBv/2KMiegCOEweU2rveGA3RlrKycrtZbnWVkR1d33TnojqXGyZGYZrWZ1Qxrm1ZWctEe+/oDnAGvlkItyUKEFgI7o1ICMVWMdhtjMiy9a8ZNS50oUO3zXohTHUkzf8kRr/tSgJTUUvCK3q4JmbKnTg/yEf56x0/P6s7GIknajulT7At42mjRtDVZUMY23Y3qMaytFurgAKw0ThQLFRorVEzBYbqJV7F/XZ8SPb2klpsK+Zh3hsi7UxFHEgLzK0JXpMTt80KTtWC39ZSL8pEOwqM3RWxfcz9OH62rn/crPK9nR/h69E7bdouLAi8RZ9SCy5WSGoAEjFR//GBcg92tZcaii4+HRrq+Lo4x3IjlEsbWHGTLhET0/VbfeEYSqQSZaK7tHVUmwljL4vvCYakrrEnk2e9BVnnMptiEVO99jj8BV8TPovTcS0UM0OFk49VsxcmIKaigD/+5IEAAAC30VSsSMscF6ourwMQwFLwU9TIYR2KYSp6iRhin0BAAJVoZDApQGdghpLLlKqhsb7mnhG2VTPBtjbE9hcTN5iX2haETIhIbdaOeTmbe5uQKSUszb7GoorVVRMbkVZp3ETIahYWlBolKiooZZygPsjknYG8HLyWNB4MgMNLRqW5vgkJBArRnoyWDlTHM6+dszql42UZOZ5kcHMMcyQ2csGbR0AlpmZytWrKNwyJUKyF/Tbb97Xv9cq9NM3MBme3AMlv6UwvYKaXPheBhnTi0WX7X6f3+t7dz2gO+aBSG6GAGct2gYZAzNTY4Rdcbl2yZnTCS1aR5j2adPZFfO0vc8yk6HIO4sNyBWMN9zlKmdX0XpdnfV2M6blT/2IQ5fOR+Wc1Y8GR/+CiB05PY9X42Wevr7L96QXAAJ6pEQooUhz5a3xKHhttTbGTVtVi0FUJqxowvStY6mczkss4xnPQ4T/w3qh6LDDgBL0QzkcPpW5Fo78cmLzEVFfZlf3QIaz7tdlmhg8dQbbZgURyd89arcW7+qc9nt+mqJiCmoo//uSBAAAAvFj02jDFXBbyzqJDGWfCzmPRsQMUcF/MOjkYZawAIEKRBBIabnBhhJZVKmllVFcrJ/x9Z9xP5UQiEJ8XKCyNsg0TWGepdrFD5Chz+6LSAxNI5v8yzaJNjsM/5l+tZMy5RZWQzOBi/smV+9mrpUsi39zhF6q0vnU5MLry4DPfMJI0oBCgrtOjOgpiJwhtEIQMrzu5dQpwdPzajxoXcerqeRGvlyQtYoUDDB10d4pFkxWJARs9Jj0bYofMjbVfOR51XpfQploDMVqvRq2zGUduMXaSveXvXrXCAAFbGsC0e7KNZmHVdjmdnvirhboRbYZyOVuMqSjRpjd6SGIQtgpHhSuKKMcVKSgzOjzuk1tY9bzSub5lg2/dulGR/bWq1eio2mkzbJ3cU+6dlqREcjjsm5AIBIgIK9gptzwmWxbdcoS3PpsbKNWmdXgh4xq1EZ5lKqCuoCDpKUuZbln3jZ46ecBXNza//NWWox9IYj2PJDKy386NTUUKN9E9lqPvtpkmJowI7XEt4k9FbRFGA3UakHJiCmooGABIQAAgP/7kgQAAALeTdVoYR6KW4xKrQwi4QxBZ0dDDFHJhDDoGGGV+GAE2k4i2UXMDVttHRkdyOc4pKDLKCSIuH3b05kFD9hLBFuVJoxQvBN/z4QRTYkEl3M6fP/+TXhq8vpuAC5JD5IUJHuvlOZrMsd/OeprFG16VAmEeKBtWClqq/30MlSVKpJhJSVQxmSC8cn1mFv2ZqUCiAkhL3MkI2DHEPh65TKDGR1jOsFPLEgSV0EjNZ6pq500ujiaG3fuyvVdfpFoh6udEazJUMOxWS7tPqp8ZFo122LT8TbZSkAG0USgVJDSaBh2plBcXZKqVCdL9aujEjPEDDKVM0uHYiM+F15BQVMkhornkDLLKThjAOowdDtKfZVLipkeCY135nuCs9HOd6dnUv7btdF5zvqS7rYlag29LF1Ne+HTFMNIgBCAjsAYIAbMtAHIonfpl0cTq6arkSAywmhwmmWei0GhLQ5XCzw3DrlcPpNYcUaoFFBoKrMd5D1Q3d6UMHJ21Wgw8S/LOz2YEcQP0UxtUiYdORpzmsrFeeyDmS/19K8WhlMQU0D/+5IEAAAC9V9QySMsQF7MShkkZY4LmY1FQwy1CX4xqXhijvyAAUQAG+SFCSJhGyYghakZxbVVZbxSbJpwmzoatJsWbRm0XrKd26NPBGxYhrzT1IZ6GVWlVtOY9BF6BtCWpUmz6JU1mzDD/uQhUtjhVXrMjdbI15KLXSiMJpixYNQ9SAJAAITciVKniMWVPtCtliT47W37lqiEEs+GiFTJ/WEs5BOGNm17UIjMY/PzhN/aYMnjrFVSJSNmdyUhIxvmu5zOzX1szSlMImfdy/ZY4rPle3PuUg1XdGrf6p46RbJJEfsSSiVJG0hG0HZZvMK5yy7tKc+wZGma9IVZtctfbnRzM7nFdYebA5TEVhUp7A1VyszTCfs3uobIpqTdGy1czU9vU6p+bW6KIv6V39jCJX0WUl1YSRiu/qIG2P2gEMAimScqg4YhGF+g5hswbro9rvYx6bc6Cbmzbw8hkRLFot2NpYtke1WU56D27jWV5NHE3tXLmKGpUznZHdqotq07ztRGh1V/MWvcTt9df4QDM7AsL8uZLTWD1xFjUXJiCmoo//uSBAAAAvNjUWkjFHBd7EoJGGWOTAVnQyMFDOF6sOdYgZaoAAMRiKZSSkpZO0N0rHSolQw2EttrPppDhfHYIFiGZLZC/zrLjkew7NYpRlQmpyxT/33IEWEqy39sVMyIPQ/pRVLdb2dkNrmb0r9URdN25HrorsEC+2AFDDWq6sFDMuABLMjNeKPPUlzZMOIokJfcvxDxGzqJ+Ehepdj+17t5owJmNrC9puBVmgKlSNqZwF4S3s88yCQ9jFYgaZB9faQhnvzb8UVX7uvVUOLb3zdibK4sc9Po6EUzWoK8bcVCGgJaErAy5pIpJwNPUJZnTBoBonQtTrn7adErbU7vrQfSpE+9KrW/rvV6atwYx6yez3/lXHn1PsFu6+Lrg+xF/gfK33+t1PxC1VVP+czfTc98O0dyTPTD3jZVU+UvtPXmQFRXgGrZiECwntUGPCHxRST4zyjL4yZQN0hLQXqGZR3+3NActRLFM87FPg5IDcyxBuJrNT3LK2g9rVBt6Pad2dk/+4uiP9GZUxMPTd3Kj0KlXER72Qf6UVjb4uJd70xBTf/7kgQAAALhV1BowyrIX4xJqSUCbkvheTtBhRqJcivmXGGXGABRQSyQiQEoBavekunWpM2lhaPESFFE+zsN73ZGY4Kxxa7mQQfR1rWYzujI5szZ2uzCdvdFK+R9lOLtVn8p5zSrnzOjnGuulpjES1d//GbSjnPdKOecKKkmsDCkgEAAAIFAsmF104KrZtqkviXCM7GVJljFanaFSVqY+1pQhW4W21ZvibW+qCSlNMg72ZoPFsQOrOd02TKEFwbT6LXo6sXyt/IJdHXtMi0VxNP+gapYcQh9uss7J6iKJ9hAABQEElufOGwYSagxer1kklUhRwyNfDVvprPe+2bUokW6DEcztohnu4LCFCCxDRXrzJAjXEY1qfle0jVuq/mXu9Ogkuf/hFNu6RzRxvc+yfit16P7axPxXQy3Cp1vXKsIAAABi6RAeKOkWoC1RXMxp0lOVu1Hmm+JJ6Ws75Z/MIUnbREjZn5eDY3/Vmp5mfxqvKcp6Po9aA2iKX691bfqp+lwMdn6oQq3QosKMzPo6KishcoZWjVGRUBqyyYgpqKAAAD/+5IEAAAC8ldOaGUfOFupuboMZY5LARcgxgy4wXUm49iRlyAFgQ0lEhkhyAoKIBlY6JaCtYKNtFNroUjKVRUOGyhSMvPLis4QeyPSBXXYzm9GQPbOrvL76sh6yjQzpUu4qgOUW2iIe1lYeyq1ap8bLQGs6X/KQZZFgEq4qo8KRI56hEVeKCjjtBBh3UaimJwa2NOVYoqVvn6lAqUS5gIDZ/92VXjrD22saqKJKdVU1DAUYzU7WpMpF0R+RQR7uj1pa+j1N9RFTeyfeYBajXf/pf4ItxtmTp1zKlu4v/iAABVweQ9al3WmWFteSnus1dnegjrLtp63rZ6F0qll1LFa1fqtehByDBjZlVvVZmpFVRm8Kk4dqgMXhz1KtkFsxREFzUX8v4kvo4i3DqnqESVOue3OyX8kBAABYGRw0hwiPikhabZihxXPeSWaTyt95+rh0wQilm8uRXKuRmCcgwEeZK3rqAr/SY2hhUnMlUBSOpGuXlw28gqGsPvSRSqVvDy8plHS/7ZngM+dCtYx62MJdZ31piCmooGABIQAAgAAAAAA//uSBAAP8qYfwDEmSvBSRPgRJCYAAAABpAAAACAAADSAAAAEACBAAELlTwWbQ5uqwqtNRZi0TktIslIpNGpKtEtEleWiSLuZZKc5yIiHkLlnkLg0BTwaBksBQEDpKBQEC4LEh8qW4iypJnWAjUsBf+0OhMtyst//v2AQmZEtyhIUWE5W+ZeZwkmROvZytahISajRea1EXRw7SMJc0wlZsJG59mKNz+cOsiyRLHgIDAVnU+NGSwFAth2Kgs9olBJ/yRIjZ8KnS3yv1ZVMQU1FAwAJCAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="; 4 | 5 | const transform = ( 6 | e, 7 | x = 0, 8 | y = 0, 9 | scale = 1, 10 | rotation = 0, 11 | percent = false 12 | ) => { 13 | const unit = percent ? "%" : "px"; 14 | e.style.transform = `translate(${x}${unit}, ${y}${unit}) scale(${scale}) rotate(${rotation}deg)`; 15 | }; 16 | 17 | const createParticle = (x, y, scale) => { 18 | const particle = document.createElement("i"); 19 | const sparcle = document.createElement("i"); 20 | 21 | particle.className = "explosion-particle"; 22 | sparcle.className = "explosion-sparcle"; 23 | transform(particle, x, y, scale); 24 | particle.appendChild(sparcle); 25 | return particle; 26 | }; 27 | 28 | const explode = (container) => { 29 | const particleCoords = [ 30 | [0, 0, 1], 31 | [20, -15, 0.4], 32 | [-8, -40, 0.8], 33 | [-7, 40, 0.4], 34 | [-26, -40, 0.2], 35 | [-26, -15, 0.75], 36 | [56, -15, 0.1], 37 | ]; 38 | particleCoords.forEach(([x, y, scale]) => { 39 | const particle = createParticle(x, y, scale); 40 | container.appendChild(particle); 41 | }); 42 | }; 43 | 44 | const explodeGroup = (x, y, trans) => { 45 | const container = document.createElement("div"); 46 | container.className = "explosion-container"; 47 | container.style.top = `${y}px`; 48 | container.style.left = `${x}px`; 49 | transform(container, trans.x, trans.y, trans.scale, trans.r, true); 50 | explode(container); 51 | 52 | return container; 53 | }; 54 | 55 | const detonateFrostMine = (square) => { 56 | const { left, top } = square.el.getBoundingClientRect(); 57 | const x = left + square.el.offsetWidth / 2; 58 | const y = top + square.el.offsetHeight / 2; 59 | const explosions = [ 60 | explodeGroup(x, y, { scale: 1, x: -50, y: -50, r: 0 }), 61 | explodeGroup(x, y, { 62 | scale: 0.1, 63 | x: -30, 64 | y: -50, 65 | r: 180, 66 | }), 67 | explodeGroup(x, y, { 68 | scale: 0.1, 69 | x: -50, 70 | y: -20, 71 | r: -90, 72 | }), 73 | ]; 74 | 75 | requestAnimationFrame(() => { 76 | const { sound } = getSettings(); 77 | if (sound) { 78 | explosionAudioEffect.play(); 79 | } 80 | explosions.forEach((boum, i) => { 81 | setTimeout(() => { 82 | document.body.appendChild(boum); 83 | setTimeout(() => { 84 | boum.parentNode.removeChild(boum); 85 | }, 500); 86 | }, i * 50); 87 | }); 88 | }); 89 | }; 90 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Roboto Mono", monospace; 3 | width: 100%; 4 | height: 100%; 5 | margin: 0; 6 | display: flex; 7 | justify-content: center; 8 | align-items: center; 9 | background-color: black; 10 | overflow: hidden; 11 | font-weight: bold; 12 | text-shadow: 1px 1px 0 #fff, -1px 1px 0 #fff, 1px -1px 0 #fff, 13 | -1px -1px 0 #fff, 0 0.5px 0 #fff, 0 -0.5px 0 #fff, -0.5px 0.5px 0 #fff, 14 | 0.5px -0.5px 0 #fff, 0 1px 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff, 15 | 1px 0 0 #fff, 0.5px 1px 0 #fff, -0.5px 1px 0 #fff, 0.5px -1px 0 #fff, 16 | -0.5px -1px 0 #fff, 1px 0.5px 0 #fff, -1px 0.5px 0 #fff, 1px -0.5px 0 #fff, 17 | -1px -0.5px 0 #fff; 18 | } 19 | #app { 20 | width: 100%; 21 | height: 100%; 22 | padding: 1rem; 23 | display: flex; 24 | justify-content: center; 25 | align-items: flex-start; 26 | flex-direction: row; 27 | flex-wrap: wrap; 28 | overflow-y: auto; 29 | font-size: 0.8rem; 30 | position: relative; 31 | } 32 | @media screen and (min-height: 768px) { 33 | #app { 34 | align-items: center; 35 | } 36 | } 37 | @media screen and (min-width: 641px) { 38 | #app { 39 | width: 641px; 40 | } 41 | } 42 | @media screen and (min-width: 360px) { 43 | #app { 44 | font-size: 1.2rem; 45 | } 46 | } 47 | @media screen and (min-width: 640px) { 48 | #app { 49 | font-size: 1.5rem; 50 | } 51 | } 52 | @media screen and (min-width: 1024px) { 53 | #app { 54 | font-size: 1.75rem; 55 | } 56 | } 57 | #game { 58 | display: none; 59 | width: 100%; 60 | height: 100%; 61 | justify-content: center; 62 | align-items: center; 63 | flex-direction: column; 64 | } 65 | #startTitle div { 66 | display: flex; 67 | flex-direction: row; 68 | justify-content: center; 69 | align-items: center; 70 | } 71 | .logoText { 72 | display: flex; 73 | flex-direction: row; 74 | letter-spacing: 2px; 75 | } 76 | .logoText h1 { 77 | font-size: 2rem; 78 | } 79 | .logoText h1.blue { 80 | color: #1e88e5; 81 | } 82 | .logoText h1.red { 83 | color: #9c27b0; 84 | } 85 | #starTitle img { 86 | width: 96px; 87 | height: 96px; 88 | } 89 | #startOptions, 90 | #startLevels, 91 | #startSettings, 92 | #startAbout { 93 | display: none; 94 | justify-content: center; 95 | align-items: center; 96 | flex-direction: column; 97 | width: 100%; 98 | } 99 | #startOptions { 100 | display: flex; 101 | } 102 | #start button, 103 | #start .button { 104 | font-size: 1.5rem; 105 | margin: 0.75rem 0; 106 | } 107 | #start { 108 | display: flex; 109 | justify-content: center; 110 | align-items: center; 111 | flex-direction: column; 112 | height: 100%; 113 | width: 100%; 114 | } 115 | 116 | button, 117 | .button { 118 | padding: 16px 0; 119 | width: 100%; 120 | max-width: 320px; 121 | background-color: black; 122 | font-size: 2rem; 123 | border: 4px solid white; 124 | cursor: pointer; 125 | letter-spacing: 4px; 126 | color: #000; 127 | border-radius: 12px; 128 | text-align: center; 129 | text-decoration: none; 130 | text-shadow: 1px 1px 0 #fff, -1px 1px 0 #fff, 1px -1px 0 #fff, 131 | -1px -1px 0 #fff, 0 0.5px 0 #fff, 0 -0.5px 0 #fff, -0.5px 0.5px 0 #fff, 132 | 0.5px -0.5px 0 #fff, 0 1px 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff, 133 | 1px 0 0 #fff, 0.5px 1px 0 #fff, -0.5px 1px 0 #fff, 0.5px -1px 0 #fff, 134 | -0.5px -1px 0 #fff, 1px 0.5px 0 #fff, -1px 0.5px 0 #fff, 1px -0.5px 0 #fff, 135 | -1px -0.5px 0 #fff; 136 | } 137 | #toolbar { 138 | display: flex; 139 | flex-direction: row; 140 | flex-wrap: wrap; 141 | width: 100%; 142 | font-weight: bold; 143 | margin-bottom: 16px; 144 | height: 56px; 145 | } 146 | #bottombar { 147 | display: flex; 148 | flex-direction: row; 149 | margin-top: 8px; 150 | width: 100%; 151 | } 152 | #turn { 153 | display: flex; 154 | flex-grow: 1; 155 | flex-direction: row; 156 | } 157 | #moves { 158 | display: flex; 159 | flex-direction: row; 160 | width: 100%; 161 | justify-content: center; 162 | align-items: flex-end; 163 | } 164 | #frostMines { 165 | display: flex; 166 | flex-direction: row; 167 | width: 50%; 168 | justify-content: flex-start; 169 | } 170 | 171 | #turnPlayer { 172 | display: flex; 173 | width: 100%; 174 | justify-content: center; 175 | margin-bottom: 1rem; 176 | } 177 | #board { 178 | display: flex; 179 | flex-direction: row; 180 | flex-wrap: wrap; 181 | overflow: hidden; 182 | border: 2px solid white; 183 | box-sizing: border-box; 184 | position: relative; 185 | } 186 | #glass { 187 | display: none; 188 | position: fixed; 189 | top: 0; 190 | left: 0; 191 | height: 100%; 192 | width: 100%; 193 | z-index: 10; 194 | cursor: not-allowed; 195 | } 196 | #fade { 197 | display: none; 198 | position: fixed; 199 | top: 0; 200 | left: 0; 201 | height: 100%; 202 | width: 100%; 203 | z-index: 10; 204 | cursor: progress; 205 | background-color: black; 206 | opacity: 0.6; 207 | } 208 | 209 | #turnTransition { 210 | display: none; 211 | cursor: progress; 212 | flex: 1; 213 | flex-direction: column; 214 | animation: fade-in 1s; 215 | position: absolute; 216 | height: 100%; 217 | width: 100%; 218 | z-index: 10; 219 | align-items: center; 220 | justify-content: center; 221 | opacity: 1; 222 | font-size: 1.5rem; 223 | letter-spacing: 0.2rem; 224 | } 225 | #turnTransitionTurn { 226 | position: absolute; 227 | top: 0; 228 | margin-top: 40px; 229 | } 230 | #turnTransitionMoves { 231 | position: absolute; 232 | bottom: 0; 233 | margin-bottom: 60px; 234 | } 235 | #turnTransitionPlayer { 236 | font-size: 3rem; 237 | } 238 | #turnTransition.fade-out { 239 | animation: fade-out 1s; 240 | } 241 | .square { 242 | display: flex; 243 | justify-content: center; 244 | align-items: center; 245 | width: 100%; 246 | height: 100%; 247 | padding: 20%; 248 | font-weight: bold; 249 | background-color: #616161; 250 | box-sizing: border-box; 251 | touch-action: none; 252 | } 253 | .superSquareLabel { 254 | position: absolute; 255 | display: flex; 256 | align-items: center; 257 | justify-content: center; 258 | z-index: 9; 259 | } 260 | .square-vibration { 261 | animation: vibrate 0.1s; 262 | animation-iteration-count: infinite; 263 | } 264 | @keyframes vibrate { 265 | 0%, 266 | 30%, 267 | 60%, 268 | 90% { 269 | transform: rotate(0deg); 270 | } 271 | 10%, 272 | 50%, 273 | 70% { 274 | transform: rotate(-2deg); 275 | } 276 | 20%, 277 | 40%, 278 | 80% { 279 | transform: rotate(2deg); 280 | } 281 | 100% { 282 | transform: rotate(-2deg); 283 | } 284 | } 285 | 286 | .dotted-border { 287 | background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' stroke='white' stroke-width='6' stroke-dasharray='6%2c 14' stroke-dashoffset='63' stroke-linecap='square'/%3e%3c/svg%3e"); 288 | } 289 | 290 | @media only screen and (hover: none) { 291 | .dotted-border { 292 | width: 110%; 293 | height: 110%; 294 | margin-top: -5%; 295 | margin-left: -5%; 296 | border: 4px solid white !important; 297 | } 298 | } 299 | .dotted-border:hover { 300 | width: 110%; 301 | height: 110%; 302 | margin-top: -5%; 303 | margin-left: -5%; 304 | border: 4px solid white !important; 305 | } 306 | #finishTurn { 307 | display: none; 308 | flex-direction: row; 309 | justify-content: center; 310 | width: 100%; 311 | } 312 | #finishTurn button { 313 | font-size: 1rem; 314 | max-width: 256px; 315 | padding: 12px 0; 316 | width: 100%; 317 | background-color: #1e88e5; 318 | border: 4px solid white; 319 | cursor: pointer; 320 | letter-spacing: 4px; 321 | color: #ffffff; 322 | text-shadow: none; 323 | font-weight: bold; 324 | text-transform: uppercase; 325 | box-shadow: 0px 4px 8px 0px rgba(255, 255, 255, 0.4); 326 | } 327 | #turn { 328 | display: flex; 329 | width: 25%; 330 | } 331 | #level { 332 | display: flex; 333 | justify-content: center; 334 | width: 50%; 335 | } 336 | #timer { 337 | display: flex; 338 | width: 50%; 339 | justify-content: flex-end; 340 | } 341 | #result { 342 | display: none; 343 | flex-direction: row; 344 | justify-content: center; 345 | flex-wrap: wrap; 346 | width: 100%; 347 | font-size: 1.5rem; 348 | max-width: 400px; 349 | } 350 | #result button { 351 | margin-top: 1rem; 352 | } 353 | #resultLevel { 354 | display: flex; 355 | } 356 | #resultInfo { 357 | display: flex; 358 | width: 100%; 359 | justify-content: center; 360 | } 361 | #resultTitle { 362 | display: flex; 363 | flex-direction: row; 364 | } 365 | #resultWinner { 366 | width: 100%; 367 | display: flex; 368 | justify-content: center; 369 | margin-bottom: 1rem; 370 | } 371 | #resultBoard #board { 372 | margin: -15% 0; 373 | transform: scale(0.7); 374 | } 375 | #imageContainer { 376 | height: 100%; 377 | width: 100%; 378 | margin-bottom: 1rem; 379 | } 380 | #resultImage { 381 | width: 100%; 382 | } 383 | #frostBombs { 384 | display: flex; 385 | width: 50%; 386 | justify-content: flex-end; 387 | flex-direction: row; 388 | } 389 | .bomb { 390 | display: flex; 391 | align-items: center; 392 | justify-content: center; 393 | } 394 | .frozen { 395 | width: 100%; 396 | height: 100%; 397 | position: absolute; 398 | backdrop-filter: blur(4.2px); 399 | -webkit-backdrop-filter: blur(10.2px); 400 | top: 0; 401 | } 402 | 403 | .explosion-container { 404 | position: absolute; 405 | width: 5rem; 406 | height: 5rem; 407 | transform: translate(-50%, -50%); 408 | pointer-events: none; 409 | } 410 | .explosion-container:before { 411 | content: ""; 412 | position: absolute; 413 | top: 50%; 414 | left: 50%; 415 | display: block; 416 | width: 2rem; 417 | height: 2rem; 418 | background: rgba(0, 0, 0, 0.25); 419 | transform: rotate(45deg); 420 | outline: 10px solid rgba(0, 0, 0, 0.1); 421 | } 422 | .explosion-container:nth-child(4n):before, 423 | .explosion-container:nth-child(7n):before { 424 | display: none; 425 | } 426 | .explosion-particle { 427 | position: absolute; 428 | display: block; 429 | top: 50%; 430 | left: 50%; 431 | width: 0; 432 | height: 0; 433 | } 434 | .explosion-particle:nth-child(1) .sparcle { 435 | animation-delay: 0ms; 436 | } 437 | 438 | .explosion-particle:nth-child(20) .sparcle { 439 | animation-delay: 950ms; 440 | } 441 | .explosion-sparcle { 442 | border-radius: 50%; 443 | position: absolute; 444 | display: block; 445 | top: 0; 446 | left: 0; 447 | width: 1rem; 448 | height: 1em; 449 | background: rgba(228, 249, 250, 0); 450 | will-change: transform, box-shadow, background-color; 451 | transform: rotate(45deg) scale(0.1) translateZ(0); 452 | animation: explode 333ms; 453 | box-shadow: 0 0 0 0 #b3e5fc; 454 | } 455 | @keyframes explode { 456 | 0% { 457 | background-color: #e1f5fe; 458 | transform: rotate(45deg) scale(1.2) translateZ(0); 459 | box-shadow: 0 0 0 0 #e1f5fe; 460 | } 461 | 40% { 462 | background-color: rgba(228, 249, 250, 0.1); 463 | } 464 | 50% { 465 | transform: rotate(45deg) scale(1) translateZ(0); 466 | box-shadow: 0 0 0 10px #e1f5fe; 467 | background-color: rgba(228, 249, 250, 0); 468 | } 469 | 60% { 470 | box-shadow: 0 0 0 50px #b3e5fc; 471 | transform: rotate(45deg) scale(0.5) translateZ(0); 472 | } 473 | 70% { 474 | background-color: rgba(228, 249, 250, 0); 475 | box-shadow: 0 0 0 30px #b2ebf2; 476 | } 477 | 100% { 478 | background-color: #80deea; 479 | box-shadow: 0 0 0 0 #80deea; 480 | transform: rotate(45deg) scale(0.25) translateZ(0); 481 | } 482 | } 483 | 484 | @keyframes fade-in { 485 | from { 486 | opacity: 0; 487 | } 488 | to { 489 | opacity: 1; 490 | } 491 | } 492 | 493 | @keyframes fade-out { 494 | from { 495 | opacity: 1; 496 | } 497 | to { 498 | opacity: 0; 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /game.js: -------------------------------------------------------------------------------- 1 | const global = { 2 | settings: { 3 | sound: true, 4 | transitions: true, 5 | speed: "NORMAL", 6 | }, 7 | timer: 0, 8 | colors: { 9 | red: "#9C27B0", 10 | darkRed: "#7B1FA2", 11 | blue: "#1E88E5", 12 | darkBlue: "#1976D2", 13 | }, 14 | touchExpiration: null, 15 | timerInterval: null, 16 | dragSquare: null, 17 | dropSquare: null, 18 | frozenSvg: ``, 19 | }; 20 | let state = {}; 21 | 22 | const bot = getBot(); 23 | const initialState = getInitialState(); 24 | 25 | function getCheckersStyle(numRows, numCols) { 26 | let switchCondition = true; 27 | let list = Array(numRows * numCols) 28 | .fill() 29 | .map((item, index) => { 30 | switchCondition = 31 | index % numRows === 0 ? !switchCondition : switchCondition; 32 | const player = 33 | index % 2 === 0 ? (switchCondition ? 1 : 2) : switchCondition ? 2 : 1; 34 | return player; 35 | }); 36 | 37 | return list; 38 | } 39 | 40 | function getNextPlayer() { 41 | const currentPlayer = state.currentAction.player; 42 | return state.players[currentPlayer + 1] ? currentPlayer + 1 : 1; 43 | } 44 | 45 | function start(chosenLevel) { 46 | hideElement(elements.start.container); 47 | showElement(elements.game.container); 48 | 49 | state = setup(chosenLevel); 50 | 51 | setupEvents(); 52 | shuffle(); 53 | discoverAllNewSquares(); 54 | updateBoard(); 55 | startTurn(); 56 | startTimer(); 57 | } 58 | 59 | function startTimer() { 60 | global.timerInterval = setInterval(() => { 61 | global.timer += 1; 62 | if (global.timer <= 3600) { 63 | let seconds = global.timer; 64 | 65 | if (global.timer < 60) { 66 | return setElementText(elements.game.timer, `Timer: 00:${pad(seconds)}`); 67 | } 68 | const minutes = 69 | seconds % 60 === 0 ? seconds / 60 : Math.floor(seconds / 60); 70 | seconds = seconds % 60; 71 | 72 | return setElementText( 73 | elements.game.timer, 74 | `Timer ${pad(minutes)}:${pad(seconds)}` 75 | ); 76 | } else { 77 | global.timer = 3600; 78 | } 79 | }, 1000); 80 | } 81 | 82 | function showResult() { 83 | const winner = state.board.squares[0].player; 84 | const color = getColorByPlayer(winner); 85 | elements.game.board.style.backgroundColor = color; 86 | elements.game.board.style.fontSize = "2rem"; 87 | state.board.squares.forEach((square) => { 88 | square.el.style.backgroundColor = color; 89 | }); 90 | clearInterval(global.timerInterval); 91 | hideElement(elements.util.glass); 92 | hideElement(elements.game.container); 93 | 94 | setElementText(elements.result.level, "Level: " + state.level.toUpperCase()); 95 | setElementText(elements.result.winner, `Player ${winner} is the winner!`); 96 | setElementTextColor(elements.result.winner, getColorByPlayer(winner)); 97 | elements.game.container.removeChild(elements.game.board); 98 | elements.result.board.appendChild(elements.game.board); 99 | 100 | const timer = elements.game.timer.innerText.replace("Timer: ", ""); 101 | const turns = state.currentAction.turn + " turns"; 102 | setElementText(elements.result.info, timer + " / " + turns); 103 | 104 | showElement(elements.result.container); 105 | } 106 | 107 | function pad(number) { 108 | return number < 10 ? "0" + number : number; 109 | } 110 | 111 | function plantFrostMine(square) { 112 | if (square.player === state.currentAction.player) { 113 | if (square.el.innerHTML === global.frozenSvg) { 114 | square.el.innerHTML = ""; 115 | square.hasFrostMine = false; 116 | state.players[square.player].frostMines++; 117 | } else if (state.players[square.player].frostMines > 0) { 118 | if (!state.players[state.currentAction.player].bot) { 119 | square.el.innerHTML = global.frozenSvg; 120 | } 121 | square.hasFrostMine = true; 122 | state.players[square.player].frostMines--; 123 | } 124 | setElementText( 125 | elements.game.frostMines, 126 | `Frost Mines: ${state.players[square.player].frostMines}` 127 | ); 128 | } 129 | } 130 | 131 | function setupEvents() { 132 | state.board.squares.forEach((square) => { 133 | const el = square.el; 134 | el.addEventListener("dblclick", () => { 135 | if (!state.currentAction.remainingMoves) { 136 | //plantFrostMine(square); 137 | } 138 | }); 139 | el.addEventListener("mouseover", (e) => { 140 | e.preventDefault(); 141 | onMouseOver(square); 142 | }); 143 | el.addEventListener("mousedown", (e) => { 144 | e.preventDefault(); 145 | if (state.currentAction.remainingMoves) { 146 | onMouseDown(square); 147 | } else { 148 | plantFrostMine(square); 149 | } 150 | }); 151 | el.addEventListener("mouseup", (e) => { 152 | e.preventDefault(); 153 | if (state.currentAction.remainingMoves) { 154 | onMouseUp(square); 155 | } 156 | }); 157 | el.addEventListener("touchmove", (e) => { 158 | e.preventDefault(); 159 | e.stopPropagation(); 160 | if (state.currentAction.remainingMoves) { 161 | const changedTouch = e.changedTouches[0]; 162 | const elem = document.elementFromPoint( 163 | changedTouch.clientX, 164 | changedTouch.clientY 165 | ); 166 | if (!elem || !elem.id) { 167 | return clearEvents(); 168 | } 169 | onMouseOver(getSquareByElementId(elem.id)); 170 | } 171 | }); 172 | el.addEventListener("touchstart2", (e) => { 173 | e.preventDefault(); 174 | e.stopPropagation(); 175 | 176 | if (e.touches.length === 1) { 177 | if (global.touchExpiration === 0) { 178 | global.touchExpiration = e.timeStamp + 400; 179 | } else if (e.timeStamp <= global.touchExpiration) { 180 | if (!state.currentAction.remainingMoves) { 181 | plantFrostMine(square); 182 | } 183 | global.touchExpiration = 0; 184 | } else { 185 | global.touchExpiration = e.timeStamp + 400; 186 | if (state.currentAction.remainingMoves) { 187 | onMouseDown(square); 188 | } 189 | } 190 | } 191 | }); 192 | el.addEventListener("touchstart", (e) => { 193 | e.preventDefault(); 194 | e.stopPropagation(); 195 | 196 | if (state.currentAction.remainingMoves) { 197 | onMouseDown(square); 198 | } else { 199 | plantFrostMine(square); 200 | } 201 | }); 202 | el.addEventListener("touchend", (e) => { 203 | e.preventDefault(); 204 | e.stopPropagation(); 205 | if (state.currentAction.remainingMoves) { 206 | const changedTouch = e.changedTouches[0]; 207 | 208 | const elem = document.elementFromPoint( 209 | changedTouch.clientX, 210 | changedTouch.clientY 211 | ); 212 | if (!elem || !elem.id) { 213 | return clearEvents(); 214 | } 215 | onMouseUp(getSquareByElementId(elem.id)); 216 | return clearEvents(); 217 | } 218 | }); 219 | }); 220 | } 221 | 222 | function getColor(color) { 223 | return global.colors[color]; 224 | } 225 | function getColorByPlayer(player, colorType = "color") { 226 | const playerColorName = state.players[player][colorType]; 227 | return global.colors[playerColorName]; 228 | } 229 | 230 | function getSquareByElementId(id) { 231 | const splittedString = id.split("-"); 232 | const row = splittedString[1]; 233 | const col = splittedString[2]; 234 | if (!state.board[row]) { 235 | return clearEvents(); 236 | } 237 | return state.board[row][col]; 238 | } 239 | 240 | function getSquaresByRange(board, initialX, initialY, finalX, finalY) { 241 | const list = []; 242 | for (let x = initialX; x <= finalX; x++) { 243 | for (let y = initialY; y <= finalY; y++) { 244 | list.push(board[x][y]); 245 | } 246 | } 247 | return list; 248 | } 249 | 250 | function shuffle() { 251 | const { board } = state; 252 | 253 | let player = 1; 254 | Array(board.numRows * board.numCols * board.numCols) 255 | .fill() 256 | .forEach(() => { 257 | const [destinationSquare, originSquare] = bot.getShuffleMove( 258 | board.squares, 259 | player 260 | ); 261 | 262 | const tempPlayer = destinationSquare.player; 263 | destinationSquare.player = originSquare.player; 264 | 265 | const doesItResultInASuperSquare = checkSuperSquareCreationPossibly( 266 | destinationSquare, 267 | originSquare 268 | ); 269 | if (doesItResultInASuperSquare) { 270 | destinationSquare.player = tempPlayer; 271 | return; 272 | } 273 | player = player === 1 ? 2 : 1; 274 | setSquareColor(destinationSquare); 275 | }); 276 | } 277 | 278 | function setPropForAllSquares(prop, value) { 279 | for (let x = 1; x <= state.board.numRows; x++) { 280 | for (let y = 1; y <= state.board.numCols; y++) { 281 | state.board[x][y][prop] = value; 282 | } 283 | } 284 | } 285 | function checkIfPlayerIsOnSquare(player, x, y, board = state.board) { 286 | if (!board[x] || !board[x][y]) { 287 | return false; 288 | } 289 | if (board[x][y].player !== player) { 290 | return false; 291 | } 292 | return true; 293 | } 294 | 295 | function getNumberOfMoves(board, player) { 296 | const superSquareList = getSuperSquareList(board); 297 | const moves = superSquareList.reduce((count, superSquare) => { 298 | if (superSquare.player === player) { 299 | return count + superSquare.size; 300 | } 301 | return count; 302 | }, 0); 303 | return moves; 304 | } 305 | 306 | function finishTurn() { 307 | startTurn(); 308 | } 309 | 310 | async function showTurnTransition() { 311 | elements.app.container.style.overflowY = "hidden"; 312 | hideElement(elements.game.container); 313 | const player = state.currentAction.player; 314 | 315 | showElement(elements.turnTransition.container); 316 | 317 | elements.turnTransition.container.style.backgroundColor = 318 | getColorByPlayer(player); 319 | setElementTextColor( 320 | elements.turnTransition.container, 321 | getColorByPlayer(player, "focusColor") 322 | ); 323 | setElementText( 324 | elements.turnTransition.turn, 325 | `Round ${state.currentAction.turn}` 326 | ); 327 | setElementText(elements.turnTransition.player, `Player ${player}`); 328 | setElementText( 329 | elements.turnTransition.moves, 330 | `Total Moves: ${state.currentAction.remainingMoves}` 331 | ); 332 | 333 | await sleep(2500); 334 | showElement(elements.game.container); 335 | hideElement(elements.turnTransition.container); 336 | elements.app.container.style.overflowY = "auto"; 337 | } 338 | 339 | function getNumberOfSuperSquaresByPlayer(player) { 340 | const superSquareList = getSuperSquareList(state.board); 341 | const superSquares = superSquareList.filter( 342 | (superSquare) => superSquare.player === player 343 | ); 344 | return superSquares.length; 345 | } 346 | 347 | async function startTurn() { 348 | const extraFrostMines = getNumberOfSuperSquaresByPlayer( 349 | state.currentAction.player 350 | ); 351 | const player = getNextPlayer(); 352 | const turn = state.currentAction.turn + 1; 353 | const defaultMoves = state.currentAction.turn === 1 ? 2 : 1; 354 | const moves = defaultMoves + getNumberOfMoves(state.board, player); 355 | 356 | state.currentAction = { 357 | remainingMoves: moves, 358 | player, 359 | turnMoves: 0, 360 | turn, 361 | }; 362 | 363 | /* 364 | make it in a way that the maximumbombs deployed is 3 365 | let totalFrostMines = state.players[player].frostMines + extraFrostMines; 366 | if (totalFrostMines > 3) { 367 | totalFrostMines = 3 368 | } 369 | const frostMines = totalFrostMines; 370 | */ 371 | 372 | const { transitions } = getSettings(); 373 | if (transitions) { 374 | await showTurnTransition(); 375 | } 376 | 377 | setElementText( 378 | elements.game.moves, 379 | `Remaining moves: ${state.currentAction.remainingMoves}` 380 | ); 381 | setElementTextColor(elements.game.moves, getColorByPlayer(player)); 382 | if (state.players[player].bot) { 383 | setElementText(elements.game.frostMines, `Frost Mines: **`); 384 | } else { 385 | setElementText( 386 | elements.game.frostMines, 387 | `Frost Mines: ${state.players[player].frostMines}` 388 | ); 389 | } 390 | 391 | setElementTextColor(elements.game.frostMines, getColorByPlayer(player)); 392 | 393 | hideElement(elements.game.finishTurn); 394 | showElement(elements.game.moves); 395 | 396 | removeDetonatedBombs(); 397 | updateBoard(); 398 | if (state.players[player].bot) { 399 | playAsBot(player); 400 | } else { 401 | hideElement(elements.util.glass); 402 | } 403 | } 404 | 405 | function removeDetonatedBombs() { 406 | state.board.squares.forEach((square) => { 407 | if (square.frozen) { 408 | square.frozen = false; 409 | square.el.innerHTML = ""; 410 | } 411 | }); 412 | } 413 | 414 | function getAdjacentPossibleMoves(x, y) { 415 | const { board } = state; 416 | const possibleMoves = []; 417 | 418 | const square = board[x][y]; 419 | const player = square.player; 420 | 421 | const top = (board[x - 1] && board[x - 1][y]) || null; 422 | const right = (board[x] && board[x][y + 1]) || null; 423 | const bottom = (board[x + 1] && board[x + 1][y]) || null; 424 | const left = (board[x] && board[x][y - 1]) || null; 425 | 426 | if (top && top.player !== player && !top.frozen) { 427 | possibleMoves.push([top, square]); 428 | } 429 | 430 | if (right && right.player !== player && !right.frozen) { 431 | possibleMoves.push([right, square]); 432 | } 433 | 434 | if (bottom && bottom.player !== player && !bottom.frozen) { 435 | possibleMoves.push([bottom, square]); 436 | } 437 | 438 | if (left && left.player !== player && !left.frozen) { 439 | possibleMoves.push([left, square]); 440 | } 441 | 442 | return possibleMoves; 443 | } 444 | async function playAsBot(player) { 445 | if (!state.currentAction.remainingMoves) { 446 | startTurn(); 447 | return; 448 | } 449 | 450 | showElement(elements.util.glass); 451 | 452 | const nextMove = bot.getNextMove( 453 | state.board, 454 | player, 455 | state.players[player].botLevel 456 | ); 457 | if (!nextMove) { 458 | showResult(); 459 | return; 460 | } 461 | let [destinationSquare, originSquare] = nextMove; 462 | destinationSquare = state.board[destinationSquare.x][destinationSquare.y]; 463 | originSquare = state.board[originSquare.x][originSquare.y]; 464 | 465 | const { speed } = getSettings(); 466 | 467 | let relativeTime; 468 | if (speed === "NORMAL") { 469 | relativeTime = state.players[player].botLevel === "easy" ? 3 : 2; 470 | } else { 471 | relativeTime = speed === "FAST" ? 1 : 3; 472 | } 473 | 474 | await sleep(relativeTime * 500); 475 | onMouseOver(originSquare); 476 | await sleep(relativeTime * 300); 477 | onMouseDown(originSquare); 478 | await sleep(relativeTime * 200); 479 | onMouseOut(originSquare); 480 | onMouseOver(destinationSquare); 481 | await sleep(relativeTime * 300); 482 | onMouseUp(destinationSquare); 483 | } 484 | 485 | function discoverAllNewSquares(board = state.board) { 486 | for (let x = 1; x <= board.numRows; x++) { 487 | for (let y = 1; y <= board.numCols; y++) { 488 | if (board[x][y].superSquare) { 489 | continue; 490 | } 491 | discoverNewSquares(board, x, y); 492 | } 493 | } 494 | } 495 | 496 | function discoverNewSquares(board, x, y, size = 1, superSquare) { 497 | const player = board[x][y].player; 498 | if (!checkIfPlayerIsOnSquare(player, x + size, y + size, board)) { 499 | return size; 500 | } 501 | 502 | if (board[x + size][y + size].superSquare) { 503 | return size; 504 | } 505 | 506 | for (let depth = 0; depth < size; depth++) { 507 | if (!checkIfPlayerIsOnSquare(player, x + size, y + depth, board)) { 508 | return size; 509 | } 510 | if (board[x + size][y + depth].superSquare) { 511 | return size; 512 | } 513 | if (!checkIfPlayerIsOnSquare(player, x + depth, y + size, board)) { 514 | return size; 515 | } 516 | if (board[x + depth][y + size].superSquare) { 517 | return size; 518 | } 519 | } 520 | if (!superSquare) { 521 | superSquare = { 522 | player, 523 | size: size + 1, 524 | x, 525 | y, 526 | }; 527 | } 528 | 529 | board[x][y].superSquare = superSquare; 530 | board[x + size][y + size].superSquare = superSquare; 531 | for (let depth = 0; depth < size; depth++) { 532 | board[x + size][y + depth].superSquare = superSquare; 533 | board[x + depth][y + size].superSquare = superSquare; 534 | } 535 | 536 | return discoverNewSquares(board, x, y, size + 1, superSquare); 537 | } 538 | 539 | function getSuperSquareList(board) { 540 | const superSquareMap = board.squares.reduce((map, square) => { 541 | if (square.superSquare) { 542 | const name = square.superSquare.x + "-" + square.superSquare.y; 543 | if (!map[name]) { 544 | map[name] = square.superSquare; 545 | } 546 | } 547 | return map; 548 | }, {}); 549 | 550 | return Object.values(superSquareMap); 551 | } 552 | 553 | function addBorder(el, orientation, player) { 554 | el.style[`border${orientation}`] = `2px solid white`; 555 | } 556 | 557 | function removeBorders(el) { 558 | if (!el.style) { 559 | return; 560 | } 561 | el.style.borderTop = "0px solid white"; 562 | el.style.borderRight = "0px solid white"; 563 | el.style.borderBottom = "0px solid white"; 564 | el.style.borderLeft = "0px solid white"; 565 | } 566 | 567 | function updateBoard() { 568 | const { board, players } = state; 569 | const superSquareList = getSuperSquareList(board); 570 | 571 | const boardEl = document.querySelector("#board"); 572 | board.squares.forEach((currentSquare) => { 573 | removeBorders(currentSquare.el); 574 | const color = global.colors[players[currentSquare.player].color]; 575 | currentSquare.el.style.backgroundColor = color; 576 | currentSquare.el.parentNode.style.backgroundColor = color; 577 | currentSquare.el.style.color = color; 578 | 579 | if ( 580 | !currentSquare.superSquare || 581 | currentSquare.x === currentSquare.superSquare.x 582 | ) { 583 | addBorder(currentSquare.el, "Top", currentSquare.player); 584 | } 585 | if ( 586 | !currentSquare.superSquare || 587 | currentSquare.y === currentSquare.superSquare.y 588 | ) { 589 | addBorder(currentSquare.el, "Left", currentSquare.player); 590 | } 591 | if ( 592 | !currentSquare.superSquare || 593 | currentSquare.superSquare.x + currentSquare.superSquare.size - 1 === 594 | currentSquare.x 595 | ) { 596 | addBorder(currentSquare.el, "Bottom", currentSquare.player); 597 | } 598 | 599 | if ( 600 | !currentSquare.superSquare || 601 | currentSquare.superSquare.y + currentSquare.superSquare.size - 1 === 602 | currentSquare.y 603 | ) { 604 | addBorder(currentSquare.el, "Right", currentSquare.player); 605 | } 606 | if ( 607 | !state.players[currentSquare.player].bot && 608 | currentSquare.hasFrostMine 609 | ) { 610 | currentSquare.el.innerHTML = global.frozenSvg; 611 | } 612 | }); 613 | if (board.superSquareLabelElements) { 614 | board.superSquareLabelElements.forEach((label) => { 615 | boardEl.removeChild(label); 616 | }); 617 | } 618 | board.superSquareLabelElements = []; 619 | 620 | superSquareList.forEach((superSquare) => { 621 | let currentSquare; 622 | const squareValue = superSquare.size; 623 | const squareSize = board.squares[0].el.offsetWidth; 624 | const superSquareLabelSize = squareSize * superSquare.size; 625 | const superSquareLabelEl = document.createElement("div"); 626 | const superSquareLabelSpanEl = document.createElement("span"); 627 | superSquareLabelSpanEl.innerHTML = "+" + squareValue; 628 | superSquareLabelEl.classList.add("superSquareLabel"); 629 | superSquareLabelEl.appendChild(superSquareLabelSpanEl); 630 | superSquareLabelEl.style.height = superSquareLabelSize + "px"; 631 | superSquareLabelEl.style.width = superSquareLabelSize + "px"; 632 | const superSquareLabelElTop = (superSquare.x - 1) * squareSize; 633 | const superSquareLabelElLeft = (superSquare.y - 1) * squareSize; 634 | superSquareLabelEl.style.top = superSquareLabelElTop + "px"; 635 | superSquareLabelEl.style.left = superSquareLabelElLeft + "px"; 636 | superSquareLabelEl.style.color = getColorByPlayer(superSquare.player); 637 | 638 | board.superSquareLabelElements.push(superSquareLabelEl); 639 | 640 | boardEl.appendChild(superSquareLabelEl); 641 | 642 | superSquareLabelEl.style.top = 643 | superSquareLabelElTop + superSquareLabelSpanEl.offsetTop + "px"; 644 | superSquareLabelEl.style.left = 645 | superSquareLabelElLeft + superSquareLabelSpanEl.offsetLeft + "px"; 646 | superSquareLabelEl.style.height = superSquareLabelSpanEl.style.height; 647 | superSquareLabelEl.style.width = superSquareLabelSpanEl.style.width; 648 | 649 | for (let x = superSquare.x; x < superSquare.x + superSquare.size; x++) { 650 | for (let y = superSquare.y; y < superSquare.y + superSquare.size; y++) { 651 | currentSquare = board[x][y]; 652 | 653 | if (x === superSquare.x) { 654 | addBorder(currentSquare.el, "Top", players[superSquare.player]); 655 | } 656 | if (x === superSquare.x + superSquare.size - 1) { 657 | addBorder(currentSquare.el, "Bottom", players[superSquare.player]); 658 | } 659 | if (y === superSquare.y) { 660 | addBorder(currentSquare.el, "Left", players[superSquare.player]); 661 | } 662 | if (y === superSquare.y + superSquare.size - 1) { 663 | addBorder(currentSquare.el, "Right", players[superSquare.player]); 664 | } 665 | } 666 | } 667 | }); 668 | } 669 | 670 | function onMouseOver(square) { 671 | if (!square) { 672 | return clearEvents(); 673 | } else if (square.frozen && square.player !== state.currentAction.player) { 674 | square.el.style.cursor = "not-allowed"; 675 | } 676 | const player = square.player; 677 | 678 | if (global.dropSquare && global.dropSquare !== square) { 679 | onMouseOut(global.dropSquare); 680 | } 681 | 682 | if (!global.dropSquare) { 683 | global.dropSquare = square; 684 | } 685 | 686 | if ( 687 | global.dragSquare && 688 | global.dragSquare.player !== square.player && 689 | square.el.classList.contains("dotted-border") 690 | ) { 691 | global.dragSquare.el.classList.remove("square-vibration"); 692 | const color = getColorByPlayer(global.dragSquare.player, "focusColor"); 693 | 694 | square.el.style.backgroundColor = color; 695 | square.el.style.cursor = "grab"; 696 | square.el.classList.add("square-vibration"); 697 | return; 698 | } else if (global.dragSquare === square) { 699 | square.el.style.cursor = "grabbing"; 700 | } 701 | 702 | if ( 703 | player === state.currentAction.player && 704 | !state.currentAction.remainingMoves 705 | ) { 706 | square.el.style.backgroundColor = getColorByPlayer(player, "focusColor"); 707 | square.el.style.cursor = "pointer"; 708 | } else if (player === state.currentAction.player && !global.dragSquare) { 709 | const adjacentSquares = getAdjacentPossibleMoves(square.x, square.y).map( 710 | (move) => move[0] 711 | ); 712 | if (adjacentSquares.length > 0) { 713 | square.el.style.backgroundColor = getColorByPlayer(player, "focusColor"); 714 | square.el.style.cursor = "grab"; 715 | } else { 716 | square.el.style.cursor = "not-allowed"; 717 | } 718 | } 719 | } 720 | function onMouseOut(square) { 721 | global.dropSquare = null; 722 | square.el.classList.remove("square-vibration"); 723 | const player = square.player; 724 | const color = getColorByPlayer(player); 725 | if (!global.dragSquare || global.dragSquare !== square) { 726 | square.el.style.backgroundColor = color; 727 | } 728 | square.el.style.cursor = "default"; 729 | } 730 | function clearEvents() { 731 | global.dropSquare = null; 732 | global.dragSquare = null; 733 | state.board.squares.forEach((square) => { 734 | square.el.classList.remove("dotted-border"); 735 | square.el.classList.remove("square-vibration"); 736 | const color = getColorByPlayer(square.player); 737 | square.el.style.backgroundColor = color; 738 | square.el.style.cursor = "default"; 739 | }); 740 | } 741 | function onMouseDown(square) { 742 | clearEvents(); 743 | if (square.player !== state.currentAction.player) { 744 | return; 745 | } 746 | 747 | const adjacentSquares = getAdjacentPossibleMoves(square.x, square.y).map( 748 | (move) => move[0] 749 | ); 750 | 751 | if (adjacentSquares.length === 0) { 752 | square.el.style.cursor = "not-allowed"; 753 | return; 754 | } 755 | global.dragSquare = square; 756 | if (adjacentSquares.length) { 757 | square.el.style.cursor = "grabbing"; 758 | } 759 | adjacentSquares.forEach((adjacentSquare) => { 760 | adjacentSquare.el.classList.add("dotted-border"); 761 | }); 762 | } 763 | function onMouseUp(square) { 764 | if (!global.dragSquare) { 765 | return; 766 | } 767 | 768 | square.el.classList.remove("square-vibration"); 769 | const adjacentSquares = getAdjacentPossibleMoves( 770 | global.dragSquare.x, 771 | global.dragSquare.y 772 | ).map((move) => move[0]); 773 | adjacentSquares.forEach((adjacentSquare) => { 774 | adjacentSquare.el.classList.remove("dotted-border"); 775 | }); 776 | 777 | if (square.frozen) { 778 | return clearEvents(); 779 | } 780 | 781 | if (square.player === state.currentAction.player) { 782 | square.el.style.cursor = "grab"; 783 | global.dragSquare = null; 784 | return; 785 | } 786 | 787 | const isHorizontalMove = 788 | global.dragSquare.x === square.x && 789 | Math.abs(global.dragSquare.y - square.y) === 1; 790 | const isVerticalMove = 791 | global.dragSquare.y === square.y && 792 | Math.abs(global.dragSquare.x - square.x) === 1; 793 | 794 | if (!isHorizontalMove && !isVerticalMove) { 795 | global.dragSquare = null; 796 | return; 797 | } 798 | 799 | swapSquare(square, global.dragSquare); 800 | 801 | global.dragSquare = null; 802 | 803 | if (!state.players[state.currentAction.player].bot) { 804 | onMouseOver(square); 805 | } 806 | 807 | if (isTheGameOver()) { 808 | showResult(); 809 | } 810 | } 811 | 812 | function isTheGameOver() { 813 | const player = state.board.squares[0].player; 814 | const findSquareFromAnotherPlayer = state.board.squares.find((square) => { 815 | return square.player !== player; 816 | }); 817 | return !findSquareFromAnotherPlayer; 818 | } 819 | 820 | function decreaseSuperSquare( 821 | destinationSquare, 822 | previousPlayer, 823 | board = state.board 824 | ) { 825 | const { superSquare } = destinationSquare; 826 | const { 827 | x: superSquareX, 828 | y: superSquareY, 829 | size: superSquareSize, 830 | } = superSquare; 831 | 832 | // if the square size is 2, it means that the whole square can be deleted 833 | if (superSquare.size === 2) { 834 | for (let x = superSquareX; x < superSquareX + superSquareSize; x++) { 835 | for (let y = superSquareY; y < superSquareY + superSquareSize; y++) { 836 | removeSquareFromSuperSquare(board[x][y], superSquare); 837 | } 838 | } 839 | } else { 840 | let removingRow; 841 | if ( 842 | destinationSquare.x === superSquareX || 843 | destinationSquare.x === superSquareX + superSquareSize - 1 844 | ) { 845 | removingRow = destinationSquare.x; 846 | } 847 | if (!removingRow) { 848 | const superSquareInitialRow = superSquareX; 849 | const superSquareFinalRow = superSquareX + superSquareSize - 1; 850 | const superSquareInitialRowDistance = Math.abs( 851 | superSquareInitialRow - destinationSquare.x 852 | ); 853 | const superSquareFinalRowDistance = Math.abs( 854 | superSquareFinalRow - destinationSquare.x 855 | ); 856 | removingRow = 857 | superSquareFinalRowDistance > superSquareInitialRowDistance 858 | ? superSquareInitialRow 859 | : superSquareFinalRow; 860 | } 861 | let removingCol; 862 | if ( 863 | destinationSquare.y === superSquareY || 864 | destinationSquare.y === superSquareY + superSquareSize - 1 865 | ) { 866 | removingCol = destinationSquare.y; 867 | } 868 | if (!removingCol) { 869 | const superSquareInitialCol = superSquareY; 870 | const superSquareFinalCol = superSquareY + superSquareSize - 1; 871 | const superSquareInitialColDistance = Math.abs( 872 | superSquareInitialCol - destinationSquare.y 873 | ); 874 | const superSquareFinalColDistance = Math.abs( 875 | superSquareFinalCol - destinationSquare.y 876 | ); 877 | removingCol = 878 | superSquareFinalColDistance > superSquareInitialColDistance 879 | ? superSquareInitialCol 880 | : superSquareFinalCol; 881 | } 882 | 883 | // remove line 884 | for (let y = superSquareY; y < superSquareY + superSquareSize; y++) { 885 | removeSquareFromSuperSquare(board[removingRow][y], superSquare); 886 | } 887 | 888 | // remove col 889 | for (let x = superSquareX; x < superSquareX + superSquareSize; x++) { 890 | removeSquareFromSuperSquare(board[x][removingCol], superSquare); 891 | } 892 | 893 | if (superSquare.x === removingRow) { 894 | superSquare.x++; 895 | } 896 | 897 | if (superSquare.y === removingCol) { 898 | superSquare.y++; 899 | } 900 | 901 | superSquare.size--; 902 | } 903 | 904 | for (let x = 1; x <= board.numRows; x++) { 905 | for (let y = 1; y <= board.numCols; y++) { 906 | if ( 907 | board[x][y].superSquare || 908 | !checkIfPlayerIsOnSquare(previousPlayer, x, y, board) 909 | ) { 910 | continue; 911 | } 912 | discoverNewSquares(board, x, y); 913 | } 914 | } 915 | } 916 | function removeSquareFromSuperSquare(square, superSquare) { 917 | removeBorders(square.el); 918 | square.el.innerHTML = ""; 919 | delete square.superSquare; 920 | } 921 | 922 | function createSuperSquare(superSquareX, superSquareY, size = 2) { 923 | const player = state.board[superSquareX][superSquareY].player; 924 | const superSquare = { 925 | player, 926 | size, 927 | x: superSquareX, 928 | y: superSquareY, 929 | }; 930 | 931 | for (let x = superSquareX; x < superSquareX + size; x++) { 932 | for (let y = superSquareY; y < superSquareY + size; y++) { 933 | state.board[x][y].superSquare = superSquare; 934 | } 935 | } 936 | 937 | return superSquare; 938 | } 939 | function checkSuperSquareCreationPossibly(destinationSquare, originSquare) { 940 | const board = state.board; 941 | 942 | const { x, y, player } = destinationSquare; 943 | 944 | const isHorizontalMove = 945 | originSquare.x === x && Math.abs(originSquare.y - y) === 1; 946 | const isVerticalMove = 947 | originSquare.y === y && Math.abs(originSquare.x - x) === 1; 948 | 949 | const checkSquarePossibility = (possibleX, possibleY) => { 950 | return ( 951 | checkIfPlayerIsOnSquare(player, possibleX, possibleY) && 952 | !board[possibleX][possibleY].superSquare 953 | ); 954 | }; 955 | 956 | if (!originSquare.superSquare) { 957 | if (isHorizontalMove) { 958 | //prioritize if origin and destination are within the new super square 959 | if ( 960 | checkSquarePossibility(x - 1, y) && 961 | checkSquarePossibility(x - 1, y) && 962 | checkSquarePossibility(x - 1, originSquare.y) 963 | ) { 964 | return { x: x - 1, y: y < originSquare.y ? y : originSquare.y }; 965 | } 966 | if ( 967 | checkSquarePossibility(x + 1, y) && 968 | checkSquarePossibility(x + 1, originSquare.y) 969 | ) { 970 | return { x, y: y < originSquare.y ? y : originSquare.y }; 971 | } 972 | } 973 | 974 | if (isVerticalMove) { 975 | //prioritize if origin and destination are within the new super square 976 | if ( 977 | checkSquarePossibility(x, y - 1) && 978 | checkSquarePossibility(originSquare.x, y - 1) 979 | ) { 980 | return { x: x < originSquare.x ? x : originSquare.x, y: y - 1 }; 981 | } 982 | if ( 983 | checkSquarePossibility(x, y + 1) && 984 | checkSquarePossibility(originSquare.x, y + 1) 985 | ) { 986 | return { x: x < originSquare.x ? x : originSquare.x, y }; 987 | } 988 | } 989 | } 990 | 991 | //now check if only the destination square generated a super square 992 | if ( 993 | isHorizontalMove && 994 | checkSquarePossibility(x, y > originSquare.y ? y + 1 : y - 1) 995 | ) { 996 | return checkSuperSquareCreationPossibly( 997 | board[x][y > originSquare.y ? y + 1 : y - 1], 998 | destinationSquare 999 | ); 1000 | } 1001 | if ( 1002 | isVerticalMove && 1003 | checkSquarePossibility(x > originSquare.x ? x + 1 : x - 1, y) 1004 | ) { 1005 | return checkSuperSquareCreationPossibly( 1006 | board[x > originSquare.x ? x + 1 : x - 1][y], 1007 | destinationSquare 1008 | ); 1009 | } 1010 | } 1011 | 1012 | function possiblyIncreaseSuperSquare(superSquare, destinationSquare) { 1013 | const { x: superSquareX, y: superSquareY, size } = superSquare; 1014 | const { x: destinationX, y: destinationY, player } = destinationSquare; 1015 | 1016 | const checkSquarePossibility = (possibleX, possibleY) => { 1017 | return ( 1018 | checkIfPlayerIsOnSquare(player, possibleX, possibleY) && 1019 | !state.board[possibleX][possibleY].superSquare 1020 | ); 1021 | }; 1022 | 1023 | const rowsToCheck = []; 1024 | const colsToCheck = []; 1025 | if ( 1026 | destinationX === superSquareX - 1 || 1027 | destinationX === superSquareX + size 1028 | ) { 1029 | rowsToCheck.push(destinationX); 1030 | colsToCheck.push(superSquareY - 1); 1031 | colsToCheck.push(superSquareY + size); 1032 | } else { 1033 | colsToCheck.push(destinationY); 1034 | rowsToCheck.push(superSquareX - 1); 1035 | rowsToCheck.push(superSquareX + size); 1036 | } 1037 | rowsToCheck.forEach((row) => { 1038 | colsToCheck.forEach((col) => { 1039 | const initialCol = col < superSquareY ? col : superSquareY; 1040 | const finalCol = 1041 | col < superSquareY + size ? superSquareY + size - 1 : col; 1042 | const initialRow = row < superSquareX ? row : superSquareX; 1043 | const finalRow = 1044 | row < superSquareX + size ? superSquareX + size - 1 : row; 1045 | 1046 | for (let x = initialRow; x <= finalRow; x++) { 1047 | if (!checkSquarePossibility(x, col)) { 1048 | return; 1049 | } 1050 | } 1051 | for (let y = initialCol; y <= finalCol; y++) { 1052 | if (!checkSquarePossibility(row, y)) { 1053 | return; 1054 | } 1055 | } 1056 | 1057 | createSuperSquare( 1058 | initialRow < superSquareX ? initialRow : superSquareX, 1059 | initialCol < superSquareY ? initialCol : superSquareY, 1060 | superSquare.size + 1 1061 | ); 1062 | }); 1063 | }); 1064 | } 1065 | 1066 | function possiblyIncreaseNearbySuperSquares(destinationSquare, originSquare) { 1067 | const { board } = state; 1068 | const { x: destinationX, y: destinationY, player } = destinationSquare; 1069 | 1070 | const checkIfSuperSquareAndSamePlayer = (possibleX, possibleY) => { 1071 | return ( 1072 | checkIfPlayerIsOnSquare(player, possibleX, possibleY) && 1073 | board[possibleX][possibleY].superSquare 1074 | ); 1075 | }; 1076 | 1077 | for (let x = destinationX - 1; x <= destinationX + 1; x++) { 1078 | for (let y = destinationY - 1; y <= destinationY + 1; y++) { 1079 | if (checkIfSuperSquareAndSamePlayer(x, y)) { 1080 | possiblyIncreaseSuperSquare(board[x][y].superSquare, destinationSquare); 1081 | } 1082 | } 1083 | } 1084 | } 1085 | function showStartPage(page) { 1086 | const pages = ["options", "levels", "settings", "about"]; 1087 | 1088 | pages.forEach((page) => { 1089 | hideElement(elements.start[page]); 1090 | }); 1091 | 1092 | showElement(elements.start[page]); 1093 | } 1094 | 1095 | function setSquareColor(square) { 1096 | const color = getColorByPlayer(square.player); 1097 | square.el.style.backgroundColor = color; 1098 | square.el.parentNode.style.backgroundColor = color; 1099 | } 1100 | 1101 | async function swapSquare(destinationSquare, originSquare) { 1102 | if (destinationSquare.hasFrostMine) { 1103 | destinationSquare.frozen = true; 1104 | destinationSquare.hasFrostMine = false; 1105 | detonateFrostMine(destinationSquare); 1106 | destinationSquare.el.innerHTML = global.frozenSvg; 1107 | } else { 1108 | const previousPlayer = destinationSquare.player; 1109 | 1110 | destinationSquare.player = originSquare.player; 1111 | setSquareColor(destinationSquare); 1112 | 1113 | //work on the disassembly of the opponent square 1114 | if (destinationSquare.superSquare) { 1115 | decreaseSuperSquare(destinationSquare, previousPlayer); 1116 | } 1117 | 1118 | if (originSquare.superSquare) { 1119 | possiblyIncreaseSuperSquare(originSquare.superSquare, destinationSquare); 1120 | } 1121 | 1122 | const possibleSquare = checkSuperSquareCreationPossibly( 1123 | destinationSquare, 1124 | originSquare 1125 | ); 1126 | if (possibleSquare) { 1127 | createSuperSquare(possibleSquare.x, possibleSquare.y); 1128 | } 1129 | 1130 | possiblyIncreaseNearbySuperSquares(destinationSquare, originSquare); 1131 | 1132 | discoverAllNewSquares(); 1133 | } 1134 | 1135 | state.currentAction.remainingMoves--; 1136 | const currentPlayer = state.players[state.currentAction.player]; 1137 | 1138 | document.querySelector( 1139 | "#moves" 1140 | ).innerHTML = `Remaining moves: ${state.currentAction.remainingMoves}`; 1141 | state.currentAction.turnMoves++; 1142 | 1143 | if (state.currentAction.remainingMoves > 0) { 1144 | if (currentPlayer.bot) { 1145 | playAsBot(state.currentAction.player); 1146 | } 1147 | } else if (currentPlayer.bot) { 1148 | updateBoard(); 1149 | await sleep(1000); 1150 | startTurn(); 1151 | } else { 1152 | clearEvents(); 1153 | hideElement(elements.game.moves); 1154 | showElement(elements.game.finishTurn); 1155 | } 1156 | 1157 | updateBoard(); 1158 | } 1159 | function setSettings(settings) { 1160 | const currentSettings = getSettings(); 1161 | localStorage.setItem( 1162 | "battlesquare_settings", 1163 | JSON.stringify({ 1164 | ...currentSettings, 1165 | ...settings, 1166 | }) 1167 | ); 1168 | loadSettings(); 1169 | } 1170 | 1171 | function toggleSettings(prop) { 1172 | const currentSettings = getSettings(); 1173 | if (prop === "speed") { 1174 | currentSettings[prop] = 1175 | currentSettings[prop] === "FAST" 1176 | ? "SLOW" 1177 | : currentSettings[prop] === "NORMAL" 1178 | ? "FAST" 1179 | : "NORMAL"; 1180 | } else { 1181 | currentSettings[prop] = !currentSettings[prop]; 1182 | } 1183 | 1184 | setSettings(currentSettings); 1185 | } 1186 | 1187 | function getSettings() { 1188 | const localSettings = JSON.parse( 1189 | localStorage.getItem("battlesquare_settings") || "{}" 1190 | ); 1191 | return { ...global.settings, ...localSettings }; 1192 | } 1193 | function loadSettings() { 1194 | const settings = getSettings(); 1195 | Object.keys(settings).forEach((key) => { 1196 | if (elements.settings[key]) { 1197 | elements.settings[key].innerHTML = 1198 | settings[key] === true 1199 | ? "ON" 1200 | : settings[key] === false 1201 | ? "OFF" 1202 | : settings[key]; 1203 | } 1204 | }); 1205 | } 1206 | --------------------------------------------------------------------------------