├── favicon ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── mstile-150x150.png ├── android-chrome-192x192.png ├── android-chrome-256x256.png └── safari-pinned-tab.svg ├── sharing └── sharing.png ├── sound └── korobeyniki.mp3 ├── browserconfig.xml ├── site.webmanifest ├── index.html ├── styles.css └── script.js /favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sacret/game/HEAD/favicon/favicon.ico -------------------------------------------------------------------------------- /sharing/sharing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sacret/game/HEAD/sharing/sharing.png -------------------------------------------------------------------------------- /sound/korobeyniki.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sacret/game/HEAD/sound/korobeyniki.mp3 -------------------------------------------------------------------------------- /favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sacret/game/HEAD/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sacret/game/HEAD/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sacret/game/HEAD/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sacret/game/HEAD/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sacret/game/HEAD/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /favicon/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sacret/game/HEAD/favicon/android-chrome-256x256.png -------------------------------------------------------------------------------- /browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "favicon/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sacret Game 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | 73 | 74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
ON / OFF
113 |
S / P
114 |
SOUND
115 |
RESET
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | 124 | 125 | 126 | 127 | 128 | 129 |
130 |
131 |
132 |
133 |
134 |
135 | 136 |
137 | 138 | R 139 | O 140 | T 141 | A 142 | T 143 | E 144 | 145 |
146 |
147 |
148 |
149 |
150 |
SACRET     
GAME
151 | 152 |
153 |
154 |
155 | 156 | 157 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat+Alternates:wght@400;700&family=Orbitron&display=swap'); 2 | 3 | @keyframes blinkingAnimation { 4 | 0% { opacity:1; } 5 | 50% { opacity:0; } 6 | 100% { opacity:1; } 7 | } 8 | 9 | body { 10 | min-height: 100vh; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | margin: 0; 15 | } 16 | 17 | .brick { 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | flex-direction: column; 22 | user-select: none; 23 | } 24 | 25 | .top { 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | width: 270px; 30 | height: 300px; 31 | padding-bottom: 15px; 32 | background-color: #b5b0ac; 33 | border: 1px solid #76716d; 34 | border-radius: 20px 20px 0 0; 35 | box-shadow: 36 | inset 0px 0px 15px 15px #9b9590, 37 | inset 0px 0px 65px 10px rgba(255, 255, 255, 0.5), 38 | 0px 0px 15px -5px #9b9590; 39 | } 40 | 41 | .center { 42 | z-index: 10; 43 | width: 272px; 44 | height: 70px; 45 | margin: -15px 0; 46 | background-color: #b5b0ac; 47 | border-right: 1px solid #76716d; 48 | border-left: 1px solid #76716d; 49 | border-radius: 20px; 50 | box-shadow: 51 | inset 0px 0px 15px 15px #9b9590, 52 | inset 0px 0px 65px 10px rgba(255, 255, 255, 0.5), 53 | 0px -8px 15px -5px #9b9590, 54 | 0px 8px 15px -5px #9b9590; 55 | } 56 | 57 | .bottom { 58 | display: flex; 59 | flex-direction: column; 60 | align-items: center; 61 | width: 270px; 62 | height: 340px; 63 | background-color: #b5b0ac; 64 | border: 1px solid #76716d; 65 | border-radius: 0 0 20px 20px; 66 | box-shadow: 67 | inset 0px 0px 15px 15px #9b9590, 68 | inset 0px 0px 65px 10px rgba(255, 255, 255, 0.5), 69 | 0px 0px 15px -5px #9b9590; 70 | } 71 | 72 | .top-left, 73 | .top-right { 74 | display: flex; 75 | justify-content: center; 76 | flex-wrap: wrap; 77 | width: 32px; 78 | margin: 0 5px; 79 | } 80 | 81 | .bg-cell { 82 | display: flex; 83 | margin-top: 3px; 84 | width: 11px; 85 | height: 11px; 86 | font-size: 14px; 87 | line-height: 10px; 88 | } 89 | 90 | .frame { 91 | border: 5px solid transparent; 92 | outline: 4px solid #dea901; 93 | } 94 | 95 | .screen { 96 | display: flex; 97 | width: 156px; 98 | background-color: #989f7e; 99 | border: 1px solid #000; 100 | outline: 1px solid #000; 101 | box-shadow: 102 | inset 0px 0px 10px 3px #898f70, 103 | 0px 0px 5px 5px #9b9590; 104 | } 105 | 106 | #game { 107 | display: flex; 108 | flex-wrap: wrap; 109 | width: 110px; 110 | height: 220px; 111 | outline: 2px solid #000; 112 | } 113 | 114 | #game-info { 115 | width: 46px; 116 | font-family: 'Orbitron', sans-serif; 117 | font-size: 10px; 118 | text-align: right; 119 | word-break: break-all; 120 | } 121 | 122 | .invisible { 123 | visibility: hidden; 124 | } 125 | 126 | .blinking { 127 | padding: 5px 3px 0 5px; 128 | font-size: 7px; 129 | font-weight: 900; 130 | animation: blinkingAnimation .5s infinite; 131 | } 132 | 133 | .inactive { 134 | pointer-events: none; 135 | } 136 | 137 | .score-text { 138 | padding: 5px 3px 0 5px; 139 | font-size: 5px; 140 | font-weight: 900; 141 | } 142 | 143 | #score, 144 | #hi-score, 145 | #speed { 146 | padding: 0 3px; 147 | font-size: 10px; 148 | } 149 | 150 | #next-figure { 151 | display: flex; 152 | flex-wrap: wrap; 153 | width: 44px; 154 | height: 44px; 155 | padding: 5px 2px 0; 156 | line-height: 11px; 157 | } 158 | 159 | .cell { 160 | display: flex; 161 | height: 11px; 162 | width: 11px; 163 | font-size: 14px; 164 | line-height: 10px; 165 | } 166 | 167 | .filled { 168 | opacity: 1; 169 | } 170 | 171 | .empty { 172 | opacity: 0.1; 173 | } 174 | 175 | .horizontal-buttons { 176 | display: flex; 177 | justify-content: space-around; 178 | width: 200px; 179 | margin-top: 40px; 180 | } 181 | 182 | .horizontal-texts { 183 | display: flex; 184 | justify-content: space-between; 185 | width: 200px; 186 | margin-top: 5px; 187 | } 188 | 189 | .control-buttons { 190 | display: flex; 191 | justify-content: space-between; 192 | width: 220px; 193 | margin-top: 15px; 194 | } 195 | 196 | .arrow-buttons { 197 | display: flex; 198 | flex-direction: column; 199 | align-items: center; 200 | width: 110px; 201 | } 202 | 203 | .arrow-symbols { 204 | display: flex; 205 | flex-direction: column; 206 | align-items: center; 207 | height: 28px; 208 | font-size: 12px; 209 | line-height: 10px; 210 | } 211 | 212 | .arrow-symbol { 213 | display: flex; 214 | justify-content: space-between; 215 | width: 32px; 216 | } 217 | 218 | .arrow-symbol:first-child, 219 | .arrow-symbol:last-child { 220 | justify-content: center; 221 | } 222 | 223 | .arrow-symbol:nth-child(2) > span:first-child { 224 | transform: rotate(-90deg); 225 | } 226 | 227 | .arrow-symbol:nth-child(2) > span:last-child { 228 | transform: rotate(90deg); 229 | } 230 | 231 | .arrow-symbol:last-child { 232 | transform: rotate(180deg); 233 | } 234 | 235 | .line-buttons { 236 | display: flex; 237 | justify-content: space-between; 238 | align-items: center; 239 | width: 110px; 240 | margin: 5px 0; 241 | } 242 | 243 | .rotate-button-container { 244 | display: flex; 245 | align-items: center; 246 | } 247 | 248 | .rotate-symbol { 249 | font-size: 40px; 250 | transform: scale(-1); 251 | padding: 5px; 252 | } 253 | 254 | .rotate-button { 255 | display: flex; 256 | flex-direction: column; 257 | align-items: center; 258 | width: 60px; 259 | } 260 | 261 | .rotate-text { 262 | position: relative; 263 | width: 60px; 264 | height: 20px; 265 | } 266 | 267 | .rotate-text-letter { 268 | position: absolute; 269 | } 270 | 271 | .rotate-text-letter:nth-child(1) { 272 | left: 0; 273 | bottom: 0; 274 | transform: rotate(320deg); 275 | } 276 | 277 | .rotate-text-letter:nth-child(2) { 278 | left: 10px; 279 | bottom: 7px; 280 | transform: rotate(335deg); 281 | } 282 | 283 | .rotate-text-letter:nth-child(3) { 284 | left: 20px; 285 | bottom: 10px; 286 | transform: rotate(350deg); 287 | } 288 | 289 | .rotate-text-letter:nth-child(4) { 290 | left: 30px; 291 | bottom: 10px; 292 | transform: rotate(365deg); 293 | } 294 | 295 | .rotate-text-letter:nth-child(5) { 296 | left: 41px; 297 | bottom: 7px; 298 | transform: rotate(390deg); 299 | } 300 | 301 | .rotate-text-letter:nth-child(6) { 302 | left: 50px; 303 | bottom: 0; 304 | transform: rotate(405deg); 305 | } 306 | 307 | .info { 308 | display: flex; 309 | justify-content: space-between; 310 | width: 220px; 311 | margin-top: 20px; 312 | padding: 0 0 10px; 313 | border-bottom: 4px solid #dea901; 314 | } 315 | 316 | .title { 317 | font-family: 'Montserrat Alternates', sans-serif; 318 | font-size: 26px; 319 | font-weight: 700; 320 | transform: rotate(330deg); 321 | text-align: end; 322 | } 323 | 324 | .logo { 325 | color: #e11511; 326 | font-size: 60px; 327 | } 328 | 329 | .button { 330 | border-radius: 50%; 331 | border: 1px solid rgba(232, 196, 62, 0.9); 332 | background-color: #dea901; 333 | box-shadow: 334 | 0px 6px 5px 0px #9b9590, 335 | 0px 1px 3px 0px #262422, 336 | inset 5px 5px 10px 0px rgba(232, 196, 62, 0.9); 337 | cursor: pointer; 338 | -webkit-tap-highlight-color: transparent; 339 | } 340 | 341 | .button:hover, 342 | .button:focus { 343 | box-shadow: 344 | 0px 6px 5px 0px #9b9590, 345 | 0px 1px 3px 0px #262422, 346 | inset 5px 5px 10px 1px rgb(232, 196, 62); 347 | } 348 | 349 | .button:active { 350 | box-shadow: 351 | 0px 3px 5px 0px #9b9590, 352 | 0px 1px 3px 0px #262422, 353 | 0px 0px 5px 0px #7f7a75, 354 | 0px 0px 3px 0px #161514, 355 | inset 5px 5px 10px 1px rgb(248, 210, 70); 356 | } 357 | 358 | .button.small { 359 | width: 20px; 360 | height: 20px; 361 | } 362 | 363 | .button.medium { 364 | width: 30px; 365 | height: 30px; 366 | } 367 | 368 | .button.big { 369 | width: 50px; 370 | height: 50px; 371 | } 372 | 373 | .text { 374 | font-family: 'Orbitron', sans-serif; 375 | font-size: 9px; 376 | font-weight: 900; 377 | text-align: center; 378 | white-space: nowrap; 379 | } 380 | 381 | .text.small { 382 | width: 45px; 383 | } 384 | 385 | .text.big { 386 | font-size: 12px; 387 | font-weight: 900; 388 | } 389 | 390 | :focus { 391 | outline: none !important; 392 | } 393 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | class Game { 2 | static #DEBUG = window.location.search.includes('debug'); 3 | static #ROW_LENGTH = 20; 4 | static #COLUMN_LENGTH = 10; 5 | static #START_COLUMN = 3; 6 | 7 | static #FIGURES = { 8 | // I 9 | 0: [ 10 | [true, true, true, true] 11 | ], 12 | // L 13 | 1: [ 14 | [true, false, false], 15 | [true, true, true] 16 | ], 17 | // J 18 | 2: [ 19 | [false, false, true], 20 | [true, true, true] 21 | ], 22 | // O 23 | 3: [ 24 | [true, true], 25 | [true, true] 26 | ], 27 | // S 28 | 4: [ 29 | [false, true, true], 30 | [true, true, false] 31 | ], 32 | // T 33 | 5: [ 34 | [false, true, false], 35 | [true, true, true] 36 | ], 37 | // Z 38 | 6: [ 39 | [true, true, false], 40 | [false, true, true] 41 | ], 42 | }; 43 | static #FIGURES_ROTATION = { 44 | 0: [{ 45 | row: -2, 46 | column: 1, 47 | }, { 48 | row: 1, 49 | column: -1, 50 | }, { 51 | row: -1, 52 | column: 2, 53 | }, { 54 | row: 2, 55 | column: -2, 56 | }], 57 | 1: [{ 58 | row: -1, 59 | column: 0, 60 | }, { 61 | row: 0, 62 | column: 0, 63 | }, { 64 | row: 0, 65 | column: 1, 66 | }, { 67 | row: 1, 68 | column: -1, 69 | }], 70 | 2: [{ 71 | row: -1, 72 | column: 0, 73 | }, { 74 | row: 0, 75 | column: 0, 76 | }, { 77 | row: 0, 78 | column: 1, 79 | }, { 80 | row: 1, 81 | column: -1, 82 | }], 83 | 3: [{ 84 | row: 0, 85 | column: 0, 86 | }, { 87 | row: 0, 88 | column: 0, 89 | }, { 90 | row: 0, 91 | column: 0, 92 | }, { 93 | row: 0, 94 | column: 0, 95 | }], 96 | 4: [{ 97 | row: -1, 98 | column: 0, 99 | }, { 100 | row: 0, 101 | column: 0, 102 | }, { 103 | row: 0, 104 | column: 1, 105 | }, { 106 | row: 1, 107 | column: -1, 108 | }], 109 | 5: [{ 110 | row: -1, 111 | column: 0, 112 | }, { 113 | row: 0, 114 | column: 0, 115 | }, { 116 | row: 0, 117 | column: 1, 118 | }, { 119 | row: 1, 120 | column: -1, 121 | }], 122 | 6: [{ 123 | row: -1, 124 | column: 0, 125 | }, { 126 | row: 0, 127 | column: 0, 128 | }, { 129 | row: 0, 130 | column: 1, 131 | }, { 132 | row: 1, 133 | column: -1, 134 | }], 135 | }; 136 | static #FIGURES_COUNT = 7; 137 | static #FIGURE_MAX_HEIGHT = 4; 138 | static #CHANGE_DELAY_INTERVAl = 10; 139 | 140 | static #START_DELAY = Game.#DEBUG ? 5000 : 1000; 141 | static #DIFF_DELAY = 50; 142 | static #SPEED_DELAY = 100; 143 | static #MIN_DELAY = 0; 144 | static #SPLASH_DELAY = 20; 145 | 146 | static #CELL = '▣'; 147 | static #HI_SCORE_LABEL = 'hi-score'; 148 | static #HI_SCORE = localStorage.getItem(Game.#HI_SCORE_LABEL); 149 | 150 | static #AUDIO = new Audio('./sound/korobeyniki.mp3'); 151 | 152 | static #SCORES_LEVEL = { 153 | 0: 0, 154 | 1: 100, 155 | 2: 300, 156 | 3: 700, 157 | 4: 1500, 158 | }; 159 | 160 | static #EMPTY_FIELD = [...Array(Game.#ROW_LENGTH)].map( 161 | row => [...Array(Game.#COLUMN_LENGTH)].map(column => false), 162 | ); 163 | 164 | static #SPLASH_SCREEN = [ 165 | [true, true, true, false, true, true, true, false, false, false], 166 | [false, true, false, false, true, false, false, false, false, false], 167 | [false, true, false, false, true, true, true, false, false, false], 168 | [false, true, false, false, true, false, false, false, false, false], 169 | [false, true, false, false, true, true, true, false, false, false], 170 | [false, false, false, false, false, false, false, false, false, false], 171 | [false, false, false, true, true, true, false, true, true, true], 172 | [false, false, false, false, true, false, false, true, false, true], 173 | [false, false, false, false, true, false, false, true, true, true], 174 | [false, false, false, false, true, false, false, true, true, false], 175 | [false, false, false, false, true, false, false, true, false, true], 176 | [false, false, false, false, false, false, false, false, false, false], 177 | [true, true, true, false, true, true, true, false, false, false], 178 | [false, true, false, false, true, false, false, false, false, false], 179 | [false, true, false, false, true, true, true, false, false, false], 180 | [false, true, false, false, false, false, true, false, false, false], 181 | [true, true, true, false, true, true, true, false, false, false], 182 | [false, false, false, false, false, false, false, false, false, false], 183 | [false, false, false, false, false, false, false, false, false, false], 184 | [false, false, false, false, false, false, false, false, false, false], 185 | ]; 186 | 187 | #scores = 0; 188 | #figuresCount = 0; 189 | #currentRow = 0; 190 | #currentColumn = Game.#START_COLUMN; 191 | #rotations = 0; 192 | 193 | #currentTimeoutId = 0; 194 | #delay = Game.#START_DELAY; 195 | #currentDelay = this.#delay; 196 | 197 | #figure = null; 198 | #figureIndex = null; 199 | #nextFigure = null; 200 | #nextFigureIndex = null; 201 | #figuresHistory = [4, 6, 4, 6]; // S Z S Z 202 | #field = Game.#EMPTY_FIELD; 203 | #figureField = Game.#EMPTY_FIELD; 204 | 205 | #isGameInProgress = false; 206 | #isGameOn = false; 207 | #isSoundOn = false; 208 | 209 | #moveLeftButtonInterval = 0; 210 | #moveRightButtonInterval = 0; 211 | 212 | #randomizer = null; 213 | 214 | constructor(gameElem, gameInfoElem) { 215 | this.gameElem = gameElem; 216 | this.gameInfoElem = gameInfoElem; 217 | 218 | this.gameInfoElem.innerHTML = ` 219 |
Hi-Score
220 |
0
221 |
Score
222 |
0
223 |
224 |
Speed
225 |
1
226 | 227 | `; 228 | 229 | this.scoresElem = document.getElementById('score'); 230 | this.hiScoresElem = document.getElementById('hi-score'); 231 | this.nextFigureElem = document.getElementById('next-figure'); 232 | this.speedElem = document.getElementById('speed'); 233 | this.pausedElem = document.getElementById('paused'); 234 | 235 | this.#setHiScore(); 236 | 237 | this.#randomizer = this.#getRandomFigure(); 238 | 239 | Game.#AUDIO.loop = true; 240 | Game.#AUDIO.volume = 0.5; 241 | }; 242 | 243 | #setHiScore = () => { 244 | this.hiScoresElem.innerText = Game.#HI_SCORE || 0; 245 | }; 246 | 247 | #clearCurrentTimeout = () => { 248 | clearTimeout(this.#currentTimeoutId); 249 | }; 250 | 251 | #changeDelay = () => { 252 | if (this.#figuresCount % Game.#CHANGE_DELAY_INTERVAl === 0) { 253 | this.#delay = this.#delay - Game.#DIFF_DELAY > Game.#SPEED_DELAY 254 | ? this.#delay - Game.#DIFF_DELAY 255 | : Game.#SPEED_DELAY; 256 | this.speedElem.innerText = Math.round(this.#figuresCount / Game.#CHANGE_DELAY_INTERVAl); 257 | } 258 | }; 259 | 260 | * #getRandomFigure() { 261 | let rand = [0, 1, 2, 5][Math.floor(Math.random() * 4)]; // I L J T 262 | yield Game.#FIGURES[rand]; 263 | 264 | this.#figuresHistory = [4, 6, 4, rand]; // S Z S ... 265 | 266 | while(true) { 267 | for (let roll = 0; roll < 4; ++roll) { 268 | rand = Math.floor(Math.random() * Game.#FIGURES_COUNT); 269 | if (!this.#figuresHistory.includes(rand)) break; 270 | } 271 | 272 | this.#nextFigureIndex = rand; 273 | this.#figuresHistory.shift(); 274 | this.#figuresHistory.push(rand); 275 | 276 | yield Game.#FIGURES[rand]; 277 | } 278 | }; 279 | 280 | #getCell = (val) => `${Game.#CELL}`; 281 | 282 | #getStrField = (field1, field2) => { 283 | let strField = ''; 284 | 285 | for (let i = 0; i < Game.#ROW_LENGTH; i++) { 286 | for (let j = 0; j < Game.#COLUMN_LENGTH; j++) { 287 | strField += this.#getCell(field1[i][j] || field2[i][j]); 288 | } 289 | } 290 | 291 | return strField; 292 | }; 293 | 294 | #getAnimation = (splashStep, defaultSplashCell) => { 295 | const ranges = [19, 28, 47, 55, 73, 80, 97, 103, 119, 124, 139, 143, 157, 160, 173, 175, 187, 188, 199]; 296 | const currentRange = ranges.find(range => splashStep <= range); 297 | const currentRangeIndex = ranges.findIndex(range => range === currentRange); 298 | 299 | const setStaticCell = (right, up, left, down, i, j) => 300 | (i < up || i > Game.#ROW_LENGTH - 1 - down || j < left || j > Game.#COLUMN_LENGTH - 1 - right) 301 | ? !defaultSplashCell 302 | : defaultSplashCell; 303 | 304 | return [...Array(Game.#ROW_LENGTH)].map( 305 | (row, i) => [...Array(Game.#COLUMN_LENGTH)].map((column, j) => { 306 | let splashCell = defaultSplashCell; 307 | 308 | switch (currentRangeIndex) { 309 | case 0: case 4: case 8: case 12: case 16: { // up 310 | const rightLinesCount = (currentRangeIndex + 0) / 4; 311 | const leftLinesCount = rightLinesCount; 312 | const upLinesCount = leftLinesCount; 313 | const downLinesCount = upLinesCount; 314 | 315 | splashCell = setStaticCell(rightLinesCount, upLinesCount, leftLinesCount, downLinesCount, i, j); 316 | 317 | if (currentRange - splashStep + downLinesCount <= i && j === Game.#COLUMN_LENGTH - 1 - rightLinesCount) { 318 | splashCell = !defaultSplashCell; 319 | } 320 | 321 | break; 322 | } 323 | case 1: case 5: case 9: case 13: case 17: { // left 324 | const rightLinesCount = (currentRangeIndex + 3) / 4; 325 | const leftLinesCount = rightLinesCount - 1; 326 | const upLinesCount = leftLinesCount; 327 | const downLinesCount = upLinesCount; 328 | 329 | splashCell = setStaticCell(rightLinesCount, upLinesCount, leftLinesCount, downLinesCount, i, j); 330 | 331 | if (currentRange - splashStep + leftLinesCount <= j && i === upLinesCount) { 332 | splashCell = !defaultSplashCell; 333 | } 334 | 335 | break; 336 | } 337 | case 2: case 6: case 10: case 14: case 18: { // down 338 | const rightLinesCount = (currentRangeIndex + 2) / 4; 339 | const leftLinesCount = rightLinesCount - 1; 340 | const upLinesCount = rightLinesCount; 341 | const downLinesCount = upLinesCount - 1; 342 | 343 | splashCell = setStaticCell(rightLinesCount, upLinesCount, leftLinesCount, downLinesCount, i, j); 344 | 345 | if (splashStep - ranges[currentRangeIndex - 1] + downLinesCount >= i && j === leftLinesCount) { 346 | splashCell = !defaultSplashCell; 347 | } 348 | 349 | break; 350 | } 351 | case 3: case 7: case 11: case 15: { // right 352 | const rightLinesCount = (currentRangeIndex + 1) / 4; 353 | const leftLinesCount = rightLinesCount; 354 | const upLinesCount = rightLinesCount; 355 | const downLinesCount = upLinesCount - 1; 356 | 357 | splashCell = setStaticCell(rightLinesCount, upLinesCount, leftLinesCount, downLinesCount, i, j); 358 | 359 | if (splashStep - ranges[currentRangeIndex - 1] + downLinesCount >= j && i === Game.#ROW_LENGTH - 1 - downLinesCount) { 360 | splashCell = !defaultSplashCell; 361 | } 362 | 363 | break; 364 | } 365 | 366 | default: { 367 | break; 368 | } 369 | } 370 | 371 | return splashCell; 372 | }), 373 | ); 374 | }; 375 | 376 | #printSplash = (callback) => { 377 | const allSplashSteps = Game.#ROW_LENGTH * Game.#COLUMN_LENGTH; 378 | let splashStep = 0; 379 | 380 | const printCallback = (defaultSplashCell) => { 381 | const currentSplashStep = splashStep > allSplashSteps - 1 382 | ? splashStep - allSplashSteps 383 | : splashStep; 384 | const splashAnimation = this.#getAnimation(currentSplashStep, defaultSplashCell); 385 | 386 | this.gameElem.innerHTML = this.#getStrField(Game.#SPLASH_SCREEN, splashAnimation); 387 | } 388 | 389 | const intervalId = setInterval(() => { 390 | printCallback(splashStep > allSplashSteps); 391 | splashStep++; 392 | 393 | if (!this.#isGameOn) { 394 | clearInterval(intervalId); 395 | } 396 | 397 | if (splashStep === allSplashSteps * 2 - 1) { 398 | clearInterval(intervalId); 399 | callback(); 400 | } 401 | }, Game.#SPLASH_DELAY); 402 | } 403 | 404 | #printField = () => { 405 | this.gameElem.innerHTML = this.#getStrField(this.#field, this.#figureField); 406 | }; 407 | 408 | #printNextFigure = () => { 409 | const nextFigure = this.#nextFigure; 410 | 411 | let strField = ''; 412 | 413 | for (let i = 0; i < Game.#FIGURE_MAX_HEIGHT; i++) { 414 | for (let j = 0; j < Game.#FIGURE_MAX_HEIGHT; j++) { 415 | strField += this.#getCell(nextFigure && nextFigure[i] && nextFigure[i][j]); 416 | } 417 | } 418 | 419 | this.nextFigureElem.innerHTML = strField; 420 | }; 421 | 422 | #checkEndGame = () => { 423 | const field = this.#field; 424 | let rowFilledCount = 0; 425 | 426 | for (let i = 0; i < Game.#ROW_LENGTH; i++) { 427 | if (field[i].some(elem => elem)) { 428 | rowFilledCount++; 429 | } 430 | } 431 | 432 | return rowFilledCount === Game.#ROW_LENGTH; 433 | }; 434 | 435 | #mergeFields = () => { 436 | const newField = []; 437 | const field = this.#field; 438 | const figureField = this.#figureField; 439 | 440 | let rowCount = 0; 441 | 442 | for (let i = 0; i < Game.#ROW_LENGTH; i++) { 443 | let row = []; 444 | for (let j = 0; j < Game.#COLUMN_LENGTH; j++) { 445 | row[j] = field[i][j] || figureField[i][j]; 446 | } 447 | if (!row.every(elem => elem)) { 448 | newField[rowCount] = row; 449 | rowCount++; 450 | } 451 | } 452 | 453 | const rowDiff = field.length - newField.length; 454 | let emptyField = []; 455 | 456 | if (rowDiff) { 457 | emptyField = [...Array(rowDiff)].map( 458 | row => [...Array(Game.#COLUMN_LENGTH)].map(column => false) 459 | ); 460 | this.#scores += Game.#SCORES_LEVEL[rowDiff]; 461 | } 462 | 463 | this.#field = [...emptyField, ...newField]; 464 | }; 465 | 466 | #getMaxColumn = ( 467 | currentFigure, 468 | currentRow = this.#currentRow, 469 | currentColumn = this.#currentColumn, 470 | ) => { 471 | const figureRows = currentFigure.length; 472 | const figureColumns = currentFigure[0].length; 473 | 474 | let maxColumn = Game.#COLUMN_LENGTH - figureColumns; 475 | 476 | for (let i = 0; i < figureRows; i++) { 477 | let prevFilledColumn = -1; 478 | 479 | for (let j = figureColumns - 1; j >= 0; j--) { 480 | if (!currentFigure[i][j] || j < prevFilledColumn) continue; 481 | 482 | prevFilledColumn = j; 483 | 484 | const fieldColumnIndex = currentRow + i >= 0 485 | ? this.#field[currentRow + i] 486 | .findIndex((elem, index) => { 487 | return (index > currentColumn) && elem; 488 | }) 489 | : -1; 490 | 491 | if (fieldColumnIndex !== -1 && fieldColumnIndex - j - 1 < maxColumn) { 492 | maxColumn = fieldColumnIndex - j - 1; 493 | } 494 | } 495 | } 496 | 497 | return maxColumn; 498 | }; 499 | 500 | #getMinColumn = ( 501 | currentFigure, 502 | currentRow = this.#currentRow, 503 | currentColumn = this.#currentColumn, 504 | ) => { 505 | const figureRows = currentFigure.length; 506 | const figureColumns = currentFigure[0].length; 507 | 508 | let minColumn = 0; 509 | 510 | for (let i = 0; i < figureRows; i++) { 511 | let prevFilledColumn = figureColumns; 512 | 513 | for (let j = 0; j < figureColumns; j++) { 514 | if (!currentFigure[i][j] || j > prevFilledColumn) continue; 515 | 516 | prevFilledColumn = j; 517 | 518 | const fieldColumnIndex = currentRow + i >= 0 519 | ? this.#field[currentRow + i] 520 | .findLastIndex((elem, index) => { 521 | return (index < currentColumn + figureColumns) && elem; 522 | }) 523 | : -1; 524 | 525 | if (fieldColumnIndex !== -1 && fieldColumnIndex - j + 1 > minColumn) { 526 | minColumn = fieldColumnIndex - j + 1; 527 | } 528 | } 529 | } 530 | 531 | return minColumn; 532 | }; 533 | 534 | #getMaxRow = ( 535 | currentFigure, 536 | currentRow = this.#currentRow, 537 | currentColumn = this.#currentColumn, 538 | ) => { 539 | const figureRows = currentFigure.length; 540 | const figureColumns = currentFigure[0].length; 541 | 542 | let maxRow = Game.#ROW_LENGTH - figureRows; 543 | 544 | for (let j = 0; j < figureColumns; j++) { 545 | let prevFilledRow = -1; 546 | 547 | for (let i = figureRows - 1; i >= 0; i--) { 548 | if (!currentFigure[i][j] || i < prevFilledRow) continue; 549 | 550 | prevFilledRow = i; 551 | 552 | const fieldColumn = this.#field.map(row => row[currentColumn + j]); 553 | const fieldRowIndex = fieldColumn 554 | .findIndex((elem, index) => { 555 | return (index > currentRow) && elem; 556 | }); 557 | 558 | if (fieldRowIndex !== -1 && fieldRowIndex - i - 1 < maxRow) { 559 | maxRow = fieldRowIndex - i - 1; 560 | } 561 | } 562 | } 563 | 564 | return maxRow; 565 | }; 566 | 567 | #getMinRow = ( 568 | currentFigure, 569 | currentRow = this.#currentRow, 570 | currentColumn = this.#currentColumn, 571 | ) => { 572 | const figureRows = currentFigure.length; 573 | const figureColumns = currentFigure[0].length; 574 | 575 | let minRow = 0; 576 | 577 | for (let i = 0; i < figureRows; i++) { 578 | let prevFilledRow = figureRows; 579 | 580 | for (let j = 0; j < figureColumns; j++) { 581 | if (!currentFigure[i][j] || i > prevFilledRow) continue; 582 | 583 | prevFilledRow = i; 584 | 585 | const fieldColumn = this.#field.map(row => row[currentColumn + j]); 586 | const fieldRowIndex = fieldColumn 587 | .findLastIndex((elem, index) => { 588 | return (index < currentRow + figureRows) && elem; 589 | }) 590 | 591 | if (fieldRowIndex !== -1 && fieldRowIndex + i + 1 > minRow) { 592 | minRow = fieldRowIndex + i + 1; 593 | } 594 | } 595 | } 596 | 597 | return minRow; 598 | }; 599 | 600 | #getNextPosition = (currentFigure) => { 601 | const { row, column } = Game.#FIGURES_ROTATION[this.#figureIndex][this.#rotations % 4]; 602 | 603 | let nextRow = this.#currentRow + row < 0 ? 0 : this.#currentRow + row; 604 | let nextColumn = this.#currentColumn + column < 0 ? 0 : this.#currentColumn + column; 605 | 606 | const maxRow = this.#getMaxRow(currentFigure, nextRow, nextColumn); 607 | const minRow = this.#getMinRow(currentFigure, nextRow, nextColumn); 608 | 609 | const maxColumn = this.#getMaxColumn(currentFigure, nextRow, nextColumn); 610 | const minColumn = this.#getMinColumn(currentFigure, nextRow, nextColumn); 611 | 612 | if (minRow > maxRow) { 613 | nextRow = -1; 614 | } else if (nextRow >= maxRow) { 615 | nextRow = maxRow; 616 | } else if (nextRow <= minRow) { 617 | nextRow = minRow; 618 | } 619 | 620 | if (minColumn > maxColumn) { 621 | nextColumn = -1; 622 | } else if (nextColumn >= maxColumn) { 623 | nextColumn = maxColumn; 624 | } else if (nextColumn <= minColumn) { 625 | nextColumn = minColumn; 626 | } 627 | 628 | return { 629 | nextRow, 630 | nextColumn, 631 | }; 632 | }; 633 | 634 | #setFigureField = () => { 635 | this.#figureField = [...Array(Game.#ROW_LENGTH)].map( 636 | (_, row) => [...Array(Game.#COLUMN_LENGTH)].map((_, column) => { 637 | const figureRows = this.#figure.length; 638 | const figureColumns = this.#figure[0].length; 639 | 640 | if (row < this.#currentRow 641 | || row >= this.#currentRow + figureRows 642 | || column < this.#currentColumn 643 | || column >= this.#currentColumn + figureColumns 644 | ) { 645 | return false; 646 | } 647 | 648 | return this.#figure[row - this.#currentRow][column - this.#currentColumn]; 649 | }), 650 | ); 651 | 652 | this.#printField(); 653 | }; 654 | 655 | moveFigureLeft = () => { 656 | if (!this.#isGameInProgress) return; 657 | 658 | const minColumn = this.#getMinColumn(this.#figure); 659 | this.#currentColumn = this.#currentColumn <= minColumn 660 | ? minColumn 661 | : this.#currentColumn - 1; 662 | 663 | this.#setFigureField(); 664 | }; 665 | 666 | moveFigureRight = () => { 667 | if (!this.#isGameInProgress) return; 668 | 669 | const maxColumn = this.#getMaxColumn(this.#figure); 670 | this.#currentColumn = this.#currentColumn >= maxColumn 671 | ? maxColumn 672 | : this.#currentColumn + 1; 673 | 674 | this.#setFigureField(); 675 | }; 676 | 677 | moveFigureDown = (e) => { 678 | if (!this.#isGameInProgress) return; 679 | 680 | this.#currentDelay = Game.#SPEED_DELAY; 681 | this.#clearCurrentTimeout(); 682 | this.continue(); 683 | }; 684 | 685 | stopMovingFigureDown = () => { 686 | if (!this.#isGameInProgress) return; 687 | 688 | this.#currentDelay = this.#delay; 689 | }; 690 | 691 | moveFigureDownQuickly = () => { 692 | if (!this.#isGameInProgress) return; 693 | 694 | this.#currentDelay = Game.#MIN_DELAY; 695 | this.#clearCurrentTimeout(); 696 | this.continue(); 697 | }; 698 | 699 | onMoveLeftButtonMouseDown = (e) => { 700 | this.#moveLeftButtonInterval = setInterval(() => { 701 | this.moveFigureLeft(); 702 | }, 50); 703 | e.preventDefault(); 704 | }; 705 | 706 | onMoveLeftButtonMouseUp = () => { 707 | clearInterval(this.#moveLeftButtonInterval); 708 | }; 709 | 710 | onMoveRightButtonMouseDown = (e) => { 711 | this.#moveRightButtonInterval = setInterval(() => { 712 | this.moveFigureRight(); 713 | }, 50); 714 | e.preventDefault(); 715 | }; 716 | 717 | onMoveRightButtonMouseUp = () => { 718 | clearInterval(this.#moveRightButtonInterval); 719 | }; 720 | 721 | rotateFigure = () => { 722 | if (!this.#isGameInProgress) return; 723 | 724 | const newFigure = []; 725 | const figureRows = this.#figure.length; 726 | const figureColumns = this.#figure[0].length; 727 | 728 | for (let i = 0; i < figureRows; i++) { 729 | for (let j = 0; j < figureColumns; j++) { 730 | if (!newFigure[j]) { 731 | newFigure[j] = []; 732 | } 733 | newFigure[j][figureRows - 1 - i] = this.#figure[i][j]; 734 | } 735 | } 736 | 737 | const { nextRow, nextColumn } = this.#getNextPosition(newFigure); 738 | 739 | if (nextRow !== -1 && nextColumn !== -1) { 740 | this.#figure = newFigure; 741 | this.#currentRow = nextRow; 742 | this.#currentColumn = nextColumn; 743 | this.#rotations++; 744 | this.#setFigureField(); 745 | } 746 | }; 747 | 748 | continue = () => { 749 | if (!this.#isGameInProgress) return; 750 | 751 | let isGameEnd = false; 752 | 753 | const maxRow = this.#getMaxRow(this.#figure); 754 | this.#setFigureField(); 755 | 756 | if (this.#currentRow >= maxRow) { 757 | this.#mergeFields(); 758 | isGameEnd = this.#checkEndGame(); 759 | 760 | if (isGameEnd) { 761 | if (this.#scores > Game.#HI_SCORE) { 762 | localStorage.setItem(Game.#HI_SCORE_LABEL, this.#scores); 763 | this.#setHiScore(); 764 | } 765 | this.#clearCurrentTimeout(); 766 | this.#isGameInProgress = false; 767 | alert(`Game over, your scores: ${this.#scores}`) 768 | } else { 769 | this.scoresElem.innerText = this.#scores; 770 | 771 | if (this.#currentDelay === Game.#MIN_DELAY) { 772 | this.#currentDelay = this.#delay; 773 | } 774 | 775 | this.#currentRow = 0; 776 | this.#currentColumn = Game.#START_COLUMN; 777 | 778 | this.#figure = this.#nextFigure; 779 | this.#figureIndex = this.#nextFigureIndex; 780 | this.#nextFigure = this.#randomizer.next().value; 781 | 782 | this.#rotations = 0; 783 | this.#figuresCount++; 784 | this.#figureField = Game.#EMPTY_FIELD; 785 | 786 | this.#changeDelay(); 787 | this.#printNextFigure(); 788 | } 789 | } else { 790 | this.#currentRow++; 791 | } 792 | 793 | if (!isGameEnd) { 794 | this.#currentTimeoutId = setTimeout(this.continue, this.#currentDelay); 795 | } 796 | }; 797 | 798 | start = () => { 799 | this.continue(); 800 | }; 801 | 802 | reset = () => { 803 | this.#clearCurrentTimeout(); 804 | this.#randomizer = this.#getRandomFigure(); 805 | 806 | this.#scores = 0; 807 | this.#figuresCount = 0; 808 | this.#currentRow = 0; 809 | this.#currentColumn = Game.#START_COLUMN; 810 | 811 | this.#currentTimeoutId = 0; 812 | this.#delay = Game.#START_DELAY; 813 | this.#currentDelay = this.#delay; 814 | 815 | this.#field = Game.#EMPTY_FIELD; 816 | this.#figure = this.#randomizer.next().value; 817 | this.#figureIndex = this.#nextFigureIndex; 818 | this.#nextFigure = this.#randomizer.next().value; 819 | this.#figureField = Game.#EMPTY_FIELD; 820 | 821 | this.scoresElem.innerText = this.#scores; 822 | 823 | this.#isGameInProgress = true; 824 | document.getElementById('button-game-pause').classList.remove('inactive'); 825 | this.pausedElem.classList.add('invisible'); 826 | 827 | this.#printNextFigure(); 828 | this.start(); 829 | }; 830 | 831 | togglePause = () => { 832 | if (!this.#isGameOn) return; 833 | 834 | this.#isGameInProgress = !this.#isGameInProgress; 835 | 836 | if (this.#isGameInProgress) { 837 | this.continue(); 838 | this.pausedElem.classList.add('invisible'); 839 | } else { 840 | this.pausedElem.classList.remove('invisible'); 841 | } 842 | }; 843 | 844 | toggleOn = () => { 845 | this.gameElem.classList.toggle('invisible'); 846 | this.gameInfoElem.classList.toggle('invisible'); 847 | const pauseButton = document.getElementById('button-game-pause'); 848 | 849 | this.#isGameOn = !this.#isGameOn; 850 | 851 | if (this.#isGameOn) { 852 | if (Game.#DEBUG) { 853 | this.#isGameInProgress = true; 854 | this.reset(); 855 | } else { 856 | this.#printSplash(() => { 857 | this.#isGameInProgress = true; 858 | this.reset(); 859 | }); 860 | } 861 | } else { 862 | this.#isGameInProgress = false; 863 | pauseButton.classList.add('inactive'); 864 | this.pausedElem.classList.add('invisible'); 865 | this.#isSoundOn = false; 866 | Game.#AUDIO.pause(); 867 | Game.#AUDIO.currentTime = 0; 868 | } 869 | }; 870 | 871 | toggleSound = () => { 872 | if (!this.#isGameOn) return; 873 | 874 | if (!this.#isSoundOn) { 875 | Game.#AUDIO.play(); 876 | } else { 877 | Game.#AUDIO.pause(); 878 | } 879 | 880 | this.#isSoundOn = !this.#isSoundOn; 881 | }; 882 | }; 883 | 884 | const gameElem = document.getElementById('game'); 885 | const gameInfoElem = document.getElementById('game-info'); 886 | 887 | const game = new Game(gameElem, gameInfoElem); 888 | 889 | const KEY_LEFT_ARROW = 'ArrowLeft'; 890 | const KEY_UP_ARROW = 'ArrowUp'; 891 | const KEY_RIGHT_ARROW = 'ArrowRight'; 892 | const KEY_DOWN_ARROW = 'ArrowDown'; 893 | const KEY_SPACE = 'Space'; 894 | 895 | document.addEventListener('keydown', (e) => { 896 | const event = e || window.event; 897 | const code = event.code; 898 | 899 | if (code === KEY_LEFT_ARROW) { 900 | game.moveFigureLeft(); 901 | } else if (code == KEY_RIGHT_ARROW) { 902 | game.moveFigureRight(); 903 | } else if (code === KEY_DOWN_ARROW) { 904 | game.moveFigureDown(); 905 | } else if (code === KEY_UP_ARROW) { 906 | game.rotateFigure(); 907 | } else if (code === KEY_SPACE) { 908 | game.moveFigureDownQuickly(); 909 | } 910 | }); 911 | 912 | document.addEventListener('keyup', (e) => { 913 | const event = e || window.event; 914 | const code = event.code; 915 | 916 | if (code === KEY_DOWN_ARROW) { 917 | game.stopMovingFigureDown(); 918 | } 919 | }); 920 | 921 | document.getElementById('button-game-on').addEventListener('click', game.toggleOn); 922 | document.getElementById('button-game-pause').addEventListener('click', game.togglePause); 923 | document.getElementById('button-game-reset').addEventListener('click', game.reset); 924 | document.getElementById('button-game-sound').addEventListener('click', game.toggleSound); 925 | 926 | document.getElementById('button-left').addEventListener('mousedown', game.onMoveLeftButtonMouseDown); 927 | document.getElementById('button-left').addEventListener('mouseup', game.onMoveLeftButtonMouseUp); 928 | document.getElementById('button-left').addEventListener('touchstart', game.onMoveLeftButtonMouseDown); 929 | document.getElementById('button-left').addEventListener('touchend', game.onMoveLeftButtonMouseUp); 930 | 931 | document.getElementById('button-right').addEventListener('mousedown', game.onMoveRightButtonMouseDown); 932 | document.getElementById('button-right').addEventListener('mouseup', game.onMoveRightButtonMouseUp); 933 | document.getElementById('button-right').addEventListener('touchstart', game.onMoveRightButtonMouseDown); 934 | document.getElementById('button-right').addEventListener('touchend', game.onMoveRightButtonMouseUp); 935 | 936 | document.getElementById('button-up').addEventListener('click', game.moveFigureDownQuickly); 937 | document.getElementById('button-rotate').addEventListener('click', game.rotateFigure); 938 | 939 | document.getElementById('button-down').addEventListener('mousedown', game.moveFigureDown); 940 | document.getElementById('button-down').addEventListener('mouseup', game.stopMovingFigureDown); 941 | document.getElementById('button-down').addEventListener('touchstart', game.moveFigureDown); 942 | document.getElementById('button-down').addEventListener('touchend', game.stopMovingFigureDown); 943 | --------------------------------------------------------------------------------