├── .gitattributes ├── README.md ├── app.css ├── index.html └── static ├── images └── icon.png └── js ├── app.js ├── constant.js └── sudoku.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # javascript-sudoku 2 | 3 | Make Sudoku Game With HTML CSS JavaScript 4 | 5 | # Video tutorial 6 | 7 | https://youtu.be/xpsm3tOLTVE 8 | 9 | # Resource 10 | 11 | Google font: https://fonts.google.com/ 12 | 13 | Boxicons: https://boxicons.com/ 14 | 15 | Images: https://unsplash.com/ 16 | 17 | # Preview 18 | 19 | !["Make Sudoku Game With HTML CSS JavaScript"](https://user-images.githubusercontent.com/67447840/135793517-57b1d971-67c5-4561-bd70-43f26998a108.jpg "Make Sudoku Game With HTML CSS JavaScript") 20 | -------------------------------------------------------------------------------- /app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg-main: #f0f2f5; 3 | --bg-body: #fff; 4 | --color-txt: #000; 5 | --filled-color: #000; 6 | --filled-bg: #caf0f8; 7 | 8 | --white: #fff; 9 | --blue: #00aeef; 10 | --red: #e91e63; 11 | --black: #000; 12 | 13 | --nav-size: 70px; 14 | --sudoku-cell-size: 50px; 15 | 16 | --border-radius: 10px; 17 | 18 | --space-y: 20px; 19 | 20 | --gap: 5px; 21 | 22 | --font-size: 1.5rem; 23 | --font-size-lg: 2rem; 24 | --font-size-xl: 3rem; 25 | } 26 | 27 | .dark { 28 | --bg-main: #2a2a38; 29 | --bg-body: #1a1a2e; 30 | --color-txt: #6a6a6a; 31 | --filled-color: #4f4f63; 32 | --filled-bg: #000; 33 | } 34 | 35 | * { 36 | padding: 0; 37 | margin: 0; 38 | box-sizing: border-box; 39 | -webkit-tap-highlight-color: transparent; 40 | } 41 | 42 | body { 43 | font-family: "Potta One", cursive; 44 | /* height: 100vh; */ 45 | background-color: var(--bg-body); 46 | overflow-x: hidden; 47 | user-select: none; 48 | } 49 | 50 | input { 51 | font-family: "Potta One", cursive; 52 | border: 2px solid var(--bg-main); 53 | color: var(--color-txt); 54 | } 55 | 56 | input:hover, 57 | input:focus { 58 | border-color: var(--blue); 59 | } 60 | 61 | a { 62 | text-decoration: none; 63 | color: unset; 64 | } 65 | 66 | ul { 67 | list-style-type: none; 68 | } 69 | 70 | nav { 71 | background-color: var(--bg-body); 72 | color: var(--color-txt); 73 | position: fixed; 74 | top: 0; 75 | width: 100%; 76 | box-shadow: 5px 2px var(--bg-main); 77 | z-index: 99; 78 | } 79 | 80 | .nav-container { 81 | max-width: 1280px; 82 | margin: auto; 83 | display: flex; 84 | align-items: center; 85 | justify-content: space-between; 86 | padding: 0 40px; 87 | height: var(--nav-size); 88 | } 89 | 90 | .nav-logo { 91 | font-size: var(--font-size-lg); 92 | color: var(--blue); 93 | } 94 | 95 | .dark-mode-toggle { 96 | color: var(--blue); 97 | font-size: var(--font-size-lg); 98 | cursor: pointer; 99 | } 100 | 101 | .bxs-sun { 102 | display: none; 103 | } 104 | 105 | .bxs-moon { 106 | display: inline-block; 107 | } 108 | 109 | .dark .bxs-sun { 110 | display: inline-block; 111 | } 112 | 113 | .dark .bxs-moon { 114 | display: none; 115 | } 116 | 117 | .main { 118 | /* height: 100vh; */ 119 | padding-top: var(--nav-size); 120 | display: grid; 121 | place-items: center; 122 | } 123 | 124 | .screen { 125 | position: relative; 126 | overflow: hidden; 127 | height: 100%; 128 | min-width: 400px; 129 | } 130 | 131 | .start-screen { 132 | position: absolute; 133 | top: 0; 134 | left: 0; 135 | width: 100%; 136 | height: 100%; 137 | transform: translateX(-100%); 138 | transition: transform 0.3s ease-in-out; 139 | display: flex; 140 | flex-direction: column; 141 | align-items: center; 142 | justify-content: center; 143 | } 144 | 145 | .start-screen.active { 146 | transform: translateX(0); 147 | } 148 | 149 | .start-screen > * + * { 150 | margin-top: 20px; 151 | } 152 | 153 | .input-name { 154 | height: 80px; 155 | width: 280px; 156 | border-radius: var(--border-radius); 157 | outline: 0; 158 | background-color: var(--bg-main); 159 | padding: 20px; 160 | font-size: var(--font-size-lg); 161 | text-align: center; 162 | } 163 | 164 | .btn { 165 | height: 80px; 166 | width: 280px; 167 | background-color: var(--bg-main); 168 | color: var(--color-txt); 169 | border-radius: var(--border-radius); 170 | display: grid; 171 | place-items: center; 172 | transition: width 0.3s ease-in-out; 173 | overflow: hidden; 174 | font-size: var(--font-size-lg); 175 | cursor: pointer; 176 | } 177 | 178 | .btn-blue { 179 | background-color: var(--blue); 180 | color: var(--white); 181 | } 182 | 183 | .input-err { 184 | border-color: var(--red); 185 | animation: bounce 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); 186 | } 187 | 188 | @keyframes bounce { 189 | 0% { 190 | transform: translateX(0); 191 | } 192 | 25% { 193 | transform: translateX(20px); 194 | } 195 | 50% { 196 | transform: translateX(-20px); 197 | } 198 | 100% { 199 | transform: translateX(0); 200 | } 201 | } 202 | 203 | .main-game { 204 | display: flex; 205 | height: 100%; 206 | flex-direction: column; 207 | justify-content: space-between; 208 | padding: 30px 0; 209 | transform: translateX(100%); 210 | transition: transform 0.3s ease-in-out; 211 | } 212 | 213 | .main-game.active { 214 | transform: translateX(0); 215 | } 216 | 217 | .main-sudoku-grid { 218 | display: grid; 219 | gap: var(--gap); 220 | grid-template-columns: repeat(9, auto); 221 | } 222 | 223 | .main-grid-cell { 224 | height: var(--sudoku-cell-size); 225 | width: var(--sudoku-cell-size); 226 | border-radius: var(--border-radius); 227 | background-color: var(--bg-main); 228 | color: var(--blue); 229 | display: grid; 230 | place-items: center; 231 | font-size: var(--font-size); 232 | cursor: pointer; 233 | } 234 | 235 | .main-grid-cell.filled { 236 | background-color: var(--filled-bg); 237 | color: var(--filled-color); 238 | } 239 | 240 | .main-grid-cell.selected { 241 | background-color: var(--blue); 242 | color: var(--white); 243 | } 244 | 245 | .main-grid-cell:hover { 246 | border: 2px solid var(--blue); 247 | } 248 | 249 | .main-grid-cell.hover { 250 | border: 3px solid var(--blue); 251 | } 252 | 253 | .dark .main-grid-cell.hover { 254 | border: 1px solid var(--blue); 255 | } 256 | 257 | .main-grid-cell.err { 258 | background-color: var(--red); 259 | color: var(--white); 260 | } 261 | 262 | .main-game-info { 263 | margin-top: var(--space-y); 264 | margin-bottom: 10px; 265 | display: grid; 266 | grid-template-columns: 1fr 1fr; 267 | gap: 10px; 268 | } 269 | 270 | .main-game-info-box { 271 | height: 45px; 272 | background-color: var(--bg-main); 273 | color: var(--color-txt); 274 | border-radius: var(--border-radius); 275 | display: grid; 276 | place-items: center; 277 | padding: 0 20px; 278 | font-size: var(--font-size); 279 | } 280 | 281 | .main-game-info-time { 282 | position: relative; 283 | align-items: center; 284 | justify-content: center; 285 | padding-left: 2rem; 286 | margin-bottom: auto; 287 | } 288 | 289 | .pause-btn { 290 | position: absolute; 291 | right: 10px; 292 | height: 30px; 293 | width: 30px; 294 | border-radius: var(--border-radius); 295 | background-color: var(--blue); 296 | color: var(--white); 297 | font-size: var(--font-size); 298 | display: grid; 299 | place-items: center; 300 | cursor: pointer; 301 | } 302 | 303 | .numbers { 304 | margin-top: var(--space-y); 305 | display: grid; 306 | grid-template-columns: repeat(5, 1fr); 307 | gap: 5px; 308 | } 309 | 310 | .number { 311 | height: var(--sudoku-cell-size); 312 | border-radius: var(--border-radius); 313 | background-color: var(--bg-main); 314 | color: var(--color-txt); 315 | display: grid; 316 | place-items: center; 317 | font-size: var(--font-size); 318 | cursor: pointer; 319 | } 320 | 321 | .delete { 322 | background-color: var(--red); 323 | color: var(--white); 324 | height: var(--sudoku-cell-size); 325 | border-radius: var(--border-radius); 326 | display: grid; 327 | place-items: center; 328 | font-size: var(--font-size); 329 | cursor: pointer; 330 | } 331 | 332 | .pause-screen, 333 | .result-screen { 334 | position: absolute; 335 | top: 0; 336 | left: 0; 337 | width: 100%; 338 | height: 100%; 339 | background-color: var(--bg-body); 340 | align-items: center; 341 | justify-content: center; 342 | flex-direction: column; 343 | display: none; 344 | } 345 | 346 | .pause-screen.active, 347 | .result-screen.active { 348 | display: flex; 349 | } 350 | 351 | .pause-screen > * + *, 352 | .result-screen > * + * { 353 | margin-top: 20px; 354 | } 355 | 356 | .result-screen.active div { 357 | animation: zoom-in 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); 358 | } 359 | 360 | .pause-screen.active .btn { 361 | animation: zoom-in 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); 362 | } 363 | 364 | .result-screen .congrate { 365 | font-size: var(--font-size-xl); 366 | color: var(--blue); 367 | } 368 | 369 | .result-screen .info { 370 | color: var(--color-txt); 371 | font-size: var(--font-size); 372 | } 373 | 374 | #result-time { 375 | color: var(--blue); 376 | font-size: var(--font-size-xl); 377 | } 378 | 379 | .zoom-in { 380 | animation: zoom-in 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); 381 | } 382 | 383 | @keyframes zoom-in { 384 | 0% { 385 | transform: scale(3); 386 | } 387 | 100% { 388 | transform: scale(1); 389 | } 390 | } 391 | 392 | .cell-err { 393 | animation: zoom-out-shake 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); 394 | } 395 | 396 | @keyframes zoom-out-shake { 397 | 0% { 398 | transform: scale(2); 399 | } 400 | 25% { 401 | transform: scale(2) rotate(30deg); 402 | } 403 | 50% { 404 | transform: scale(2) rotate(-30deg); 405 | } 406 | 100% { 407 | transform: scale(1); 408 | } 409 | } 410 | 411 | @media only screen and (max-width: 800px) { 412 | :root { 413 | --nav-size: 50px; 414 | 415 | --sudoku-cell-size: 30px; 416 | 417 | --border-radius: 5px; 418 | 419 | --space-y: 10px; 420 | 421 | --gap: 2px; 422 | 423 | --font-size: 1rem; 424 | --font-size-lg: 1.5rem; 425 | --font-size-xl: 2rem; 426 | } 427 | 428 | .input-name, 429 | .btn { 430 | height: 50px; 431 | } 432 | 433 | .main-grid-cell.hover { 434 | border-width: 2px; 435 | } 436 | 437 | .screen { 438 | min-width: unset; 439 | } 440 | 441 | .main { 442 | height: 100vh; 443 | } 444 | } 445 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Sudoku 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 37 | 38 | 39 | 40 |
41 |
42 | 43 |
44 | 45 |
46 | Easy 47 |
48 |
Continue
49 |
New game
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 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | 140 |
141 |
142 | tuat 143 |
144 |
145 | Easy 146 |
147 |
148 | 149 |
150 | 10:20 151 |
152 | 153 |
154 |
155 | 156 |
157 |
1
158 |
2
159 |
3
160 |
4
161 |
5
162 |
6
163 |
7
164 |
8
165 |
9
166 |
X
167 |
168 |
169 | 170 | 171 | 172 |
173 |
Resume
174 |
New game
175 |
176 | 177 | 178 | 179 |
180 |
Competed
181 |
Time
182 |
183 |
New game
184 |
185 | 186 |
187 |
188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /static/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trananhtuat/javascript-sudoku/cd5385f6fe32da8fe825be391bd6976a469ff693/static/images/icon.png -------------------------------------------------------------------------------- /static/js/app.js: -------------------------------------------------------------------------------- 1 | document.querySelector('#dark-mode-toggle').addEventListener('click', () => { 2 | document.body.classList.toggle('dark'); 3 | const isDarkMode = document.body.classList.contains('dark'); 4 | localStorage.setItem('darkmode', isDarkMode); 5 | // chang mobile status bar color 6 | document.querySelector('meta[name="theme-color"').setAttribute('content', isDarkMode ? '#1a1a2e' : '#fff'); 7 | }); 8 | 9 | // initial value 10 | 11 | // screens 12 | const start_screen = document.querySelector('#start-screen'); 13 | const game_screen = document.querySelector('#game-screen'); 14 | const pause_screen = document.querySelector('#pause-screen'); 15 | const result_screen = document.querySelector('#result-screen'); 16 | // ---------- 17 | const cells = document.querySelectorAll('.main-grid-cell'); 18 | 19 | const name_input = document.querySelector('#input-name'); 20 | 21 | const number_inputs = document.querySelectorAll('.number'); 22 | 23 | const player_name = document.querySelector('#player-name'); 24 | const game_level = document.querySelector('#game-level'); 25 | const game_time = document.querySelector('#game-time'); 26 | 27 | const result_time = document.querySelector('#result-time'); 28 | 29 | let level_index = 0; 30 | let level = CONSTANT.LEVEL[level_index]; 31 | 32 | let timer = null; 33 | let pause = false; 34 | let seconds = 0; 35 | 36 | let su = undefined; 37 | let su_answer = undefined; 38 | 39 | let selected_cell = -1; 40 | 41 | // -------- 42 | 43 | const getGameInfo = () => JSON.parse(localStorage.getItem('game')); 44 | 45 | // add space for each 9 cells 46 | const initGameGrid = () => { 47 | let index = 0; 48 | 49 | for (let i = 0; i < Math.pow(CONSTANT.GRID_SIZE,2); i++) { 50 | let row = Math.floor(i/CONSTANT.GRID_SIZE); 51 | let col = i % CONSTANT.GRID_SIZE; 52 | if (row === 2 || row === 5) cells[index].style.marginBottom = '10px'; 53 | if (col === 2 || col === 5) cells[index].style.marginRight = '10px'; 54 | 55 | index++; 56 | } 57 | } 58 | // ---------------- 59 | 60 | const setPlayerName = (name) => localStorage.setItem('player_name', name); 61 | const getPlayerName = () => localStorage.getItem('player_name'); 62 | 63 | const showTime = (seconds) => new Date(seconds * 1000).toISOString().substr(11, 8); 64 | 65 | const clearSudoku = () => { 66 | for (let i = 0; i < Math.pow(CONSTANT.GRID_SIZE, 2); i++) { 67 | cells[i].innerHTML = ''; 68 | cells[i].classList.remove('filled'); 69 | cells[i].classList.remove('selected'); 70 | } 71 | } 72 | 73 | const initSudoku = () => { 74 | // clear old sudoku 75 | clearSudoku(); 76 | resetBg(); 77 | // generate sudoku puzzle here 78 | su = sudokuGen(level); 79 | su_answer = [...su.question]; 80 | 81 | seconds = 0; 82 | 83 | saveGameInfo(); 84 | 85 | // show sudoku to div 86 | for (let i = 0; i < Math.pow(CONSTANT.GRID_SIZE, 2); i++) { 87 | let row = Math.floor(i / CONSTANT.GRID_SIZE); 88 | let col = i % CONSTANT.GRID_SIZE; 89 | 90 | cells[i].setAttribute('data-value', su.question[row][col]); 91 | 92 | if (su.question[row][col] !== 0) { 93 | cells[i].classList.add('filled'); 94 | cells[i].innerHTML = su.question[row][col]; 95 | } 96 | } 97 | } 98 | 99 | const loadSudoku = () => { 100 | let game = getGameInfo(); 101 | 102 | game_level.innerHTML = CONSTANT.LEVEL_NAME[game.level]; 103 | 104 | su = game.su; 105 | 106 | su_answer = su.answer; 107 | 108 | seconds = game.seconds; 109 | game_time.innerHTML = showTime(seconds); 110 | 111 | level_index = game.level; 112 | 113 | // show sudoku to div 114 | for (let i = 0; i < Math.pow(CONSTANT.GRID_SIZE, 2); i++) { 115 | let row = Math.floor(i / CONSTANT.GRID_SIZE); 116 | let col = i % CONSTANT.GRID_SIZE; 117 | 118 | cells[i].setAttribute('data-value', su_answer[row][col]); 119 | cells[i].innerHTML = su_answer[row][col] !== 0 ? su_answer[row][col] : ''; 120 | if (su.question[row][col] !== 0) { 121 | cells[i].classList.add('filled'); 122 | } 123 | } 124 | } 125 | 126 | const hoverBg = (index) => { 127 | let row = Math.floor(index / CONSTANT.GRID_SIZE); 128 | let col = index % CONSTANT.GRID_SIZE; 129 | 130 | let box_start_row = row - row % 3; 131 | let box_start_col = col - col % 3; 132 | 133 | for (let i = 0; i < CONSTANT.BOX_SIZE; i++) { 134 | for (let j = 0; j < CONSTANT.BOX_SIZE; j++) { 135 | let cell = cells[9 * (box_start_row + i) + (box_start_col + j)]; 136 | cell.classList.add('hover'); 137 | } 138 | } 139 | 140 | let step = 9; 141 | while (index - step >= 0) { 142 | cells[index - step].classList.add('hover'); 143 | step += 9; 144 | } 145 | 146 | step = 9; 147 | while (index + step < 81) { 148 | cells[index + step].classList.add('hover'); 149 | step += 9; 150 | } 151 | 152 | step = 1; 153 | while (index - step >= 9*row) { 154 | cells[index - step].classList.add('hover'); 155 | step += 1; 156 | } 157 | 158 | step = 1; 159 | while (index + step < 9*row + 9) { 160 | cells[index + step].classList.add('hover'); 161 | step += 1; 162 | } 163 | } 164 | 165 | const resetBg = () => { 166 | cells.forEach(e => e.classList.remove('hover')); 167 | } 168 | 169 | const checkErr = (value) => { 170 | const addErr = (cell) => { 171 | if (parseInt(cell.getAttribute('data-value')) === value) { 172 | cell.classList.add('err'); 173 | cell.classList.add('cell-err'); 174 | setTimeout(() => { 175 | cell.classList.remove('cell-err'); 176 | }, 500); 177 | } 178 | } 179 | 180 | let index = selected_cell; 181 | 182 | let row = Math.floor(index / CONSTANT.GRID_SIZE); 183 | let col = index % CONSTANT.GRID_SIZE; 184 | 185 | let box_start_row = row - row % 3; 186 | let box_start_col = col - col % 3; 187 | 188 | for (let i = 0; i < CONSTANT.BOX_SIZE; i++) { 189 | for (let j = 0; j < CONSTANT.BOX_SIZE; j++) { 190 | let cell = cells[9 * (box_start_row + i) + (box_start_col + j)]; 191 | if (!cell.classList.contains('selected')) addErr(cell); 192 | } 193 | } 194 | 195 | let step = 9; 196 | while (index - step >= 0) { 197 | addErr(cells[index - step]); 198 | step += 9; 199 | } 200 | 201 | step = 9; 202 | while (index + step < 81) { 203 | addErr(cells[index + step]); 204 | step += 9; 205 | } 206 | 207 | step = 1; 208 | while (index - step >= 9*row) { 209 | addErr(cells[index - step]); 210 | step += 1; 211 | } 212 | 213 | step = 1; 214 | while (index + step < 9*row + 9) { 215 | addErr(cells[index + step]); 216 | step += 1; 217 | } 218 | } 219 | 220 | const removeErr = () => cells.forEach(e => e.classList.remove('err')); 221 | 222 | const saveGameInfo = () => { 223 | let game = { 224 | level: level_index, 225 | seconds: seconds, 226 | su: { 227 | original: su.original, 228 | question: su.question, 229 | answer: su_answer 230 | } 231 | } 232 | localStorage.setItem('game', JSON.stringify(game)); 233 | } 234 | 235 | const removeGameInfo = () => { 236 | localStorage.removeItem('game'); 237 | document.querySelector('#btn-continue').style.display = 'none'; 238 | } 239 | 240 | const isGameWin = () => sudokuCheck(su_answer); 241 | 242 | const showResult = () => { 243 | clearInterval(timer); 244 | result_screen.classList.add('active'); 245 | result_time.innerHTML = showTime(seconds); 246 | } 247 | 248 | const initNumberInputEvent = () => { 249 | number_inputs.forEach((e, index) => { 250 | e.addEventListener('click', () => { 251 | if (!cells[selected_cell].classList.contains('filled')) { 252 | cells[selected_cell].innerHTML = index + 1; 253 | cells[selected_cell].setAttribute('data-value', index + 1); 254 | // add to answer 255 | let row = Math.floor(selected_cell / CONSTANT.GRID_SIZE); 256 | let col = selected_cell % CONSTANT.GRID_SIZE; 257 | su_answer[row][col] = index + 1; 258 | // save game 259 | saveGameInfo() 260 | // ----- 261 | removeErr(); 262 | checkErr(index + 1); 263 | cells[selected_cell].classList.add('zoom-in'); 264 | setTimeout(() => { 265 | cells[selected_cell].classList.remove('zoom-in'); 266 | }, 500); 267 | 268 | // check game win 269 | if (isGameWin()) { 270 | removeGameInfo(); 271 | showResult(); 272 | } 273 | // ---- 274 | } 275 | }) 276 | }) 277 | } 278 | 279 | const initCellsEvent = () => { 280 | cells.forEach((e, index) => { 281 | e.addEventListener('click', () => { 282 | if (!e.classList.contains('filled')) { 283 | cells.forEach(e => e.classList.remove('selected')); 284 | 285 | selected_cell = index; 286 | e.classList.remove('err'); 287 | e.classList.add('selected'); 288 | resetBg(); 289 | hoverBg(index); 290 | } 291 | }) 292 | }) 293 | } 294 | 295 | const startGame = () => { 296 | start_screen.classList.remove('active'); 297 | game_screen.classList.add('active'); 298 | 299 | player_name.innerHTML = name_input.value.trim(); 300 | setPlayerName(name_input.value.trim()); 301 | 302 | game_level.innerHTML = CONSTANT.LEVEL_NAME[level_index]; 303 | 304 | showTime(seconds); 305 | 306 | timer = setInterval(() => { 307 | if (!pause) { 308 | seconds = seconds + 1; 309 | game_time.innerHTML = showTime(seconds); 310 | } 311 | }, 1000); 312 | } 313 | 314 | const returnStartScreen = () => { 315 | clearInterval(timer); 316 | pause = false; 317 | seconds = 0; 318 | start_screen.classList.add('active'); 319 | game_screen.classList.remove('active'); 320 | pause_screen.classList.remove('active'); 321 | result_screen.classList.remove('active'); 322 | } 323 | 324 | // add button event 325 | document.querySelector('#btn-level').addEventListener('click', (e) => { 326 | level_index = level_index + 1 > CONSTANT.LEVEL.length - 1 ? 0 : level_index + 1; 327 | level = CONSTANT.LEVEL[level_index]; 328 | e.target.innerHTML = CONSTANT.LEVEL_NAME[level_index]; 329 | }); 330 | 331 | document.querySelector('#btn-play').addEventListener('click', () => { 332 | if (name_input.value.trim().length > 0) { 333 | initSudoku(); 334 | startGame(); 335 | } else { 336 | name_input.classList.add('input-err'); 337 | setTimeout(() => { 338 | name_input.classList.remove('input-err'); 339 | name_input.focus(); 340 | }, 500); 341 | } 342 | }); 343 | 344 | document.querySelector('#btn-continue').addEventListener('click', () => { 345 | if (name_input.value.trim().length > 0) { 346 | loadSudoku(); 347 | startGame(); 348 | } else { 349 | name_input.classList.add('input-err'); 350 | setTimeout(() => { 351 | name_input.classList.remove('input-err'); 352 | name_input.focus(); 353 | }, 500); 354 | } 355 | }); 356 | 357 | document.querySelector('#btn-pause').addEventListener('click', () => { 358 | pause_screen.classList.add('active'); 359 | pause = true; 360 | }); 361 | 362 | document.querySelector('#btn-resume').addEventListener('click', () => { 363 | pause_screen.classList.remove('active'); 364 | pause = false; 365 | }); 366 | 367 | document.querySelector('#btn-new-game').addEventListener('click', () => { 368 | returnStartScreen(); 369 | }); 370 | 371 | document.querySelector('#btn-new-game-2').addEventListener('click', () => { 372 | console.log('object') 373 | returnStartScreen(); 374 | }); 375 | 376 | document.querySelector('#btn-delete').addEventListener('click', () => { 377 | cells[selected_cell].innerHTML = ''; 378 | cells[selected_cell].setAttribute('data-value', 0); 379 | 380 | let row = Math.floor(selected_cell / CONSTANT.GRID_SIZE); 381 | let col = selected_cell % CONSTANT.GRID_SIZE; 382 | 383 | su_answer[row][col] = 0; 384 | 385 | removeErr(); 386 | }) 387 | // ------------- 388 | 389 | const init = () => { 390 | const darkmode = JSON.parse(localStorage.getItem('darkmode')); 391 | document.body.classList.add(darkmode ? 'dark' : 'light'); 392 | document.querySelector('meta[name="theme-color"').setAttribute('content', darkmode ? '#1a1a2e' : '#fff'); 393 | 394 | const game = getGameInfo(); 395 | 396 | document.querySelector('#btn-continue').style.display = game ? 'grid' : 'none'; 397 | 398 | initGameGrid(); 399 | initCellsEvent(); 400 | initNumberInputEvent(); 401 | 402 | if (getPlayerName()) { 403 | name_input.value = getPlayerName(); 404 | } else { 405 | name_input.focus(); 406 | } 407 | } 408 | 409 | init(); -------------------------------------------------------------------------------- /static/js/constant.js: -------------------------------------------------------------------------------- 1 | const CONSTANT = { 2 | UNASSIGNED: 0, 3 | GRID_SIZE: 9, 4 | BOX_SIZE: 3, 5 | NUMBERS: [1,2,3,4,5,6,7,8,9], 6 | LEVEL_NAME: [ 7 | 'Easy', 8 | 'Medium', 9 | 'Hard', 10 | 'Very hard', 11 | 'Insane', 12 | 'Inhuman' 13 | ], 14 | LEVEL: [29, 38, 47, 56, 65, 74] 15 | } -------------------------------------------------------------------------------- /static/js/sudoku.js: -------------------------------------------------------------------------------- 1 | const newGrid = (size) => { 2 | let arr = new Array(size); 3 | 4 | for (let i = 0; i < size; i++) { 5 | arr[i] = new Array(size); 6 | } 7 | 8 | for (let i = 0; i < Math.pow(size, 2); i++) { 9 | arr[Math.floor(i/size)][i%size] = CONSTANT.UNASSIGNED; 10 | } 11 | 12 | return arr; 13 | } 14 | 15 | // check duplicate number in col 16 | const isColSafe = (grid, col, value) => { 17 | for (let row = 0; row < CONSTANT.GRID_SIZE; row++) { 18 | if (grid[row][col] === value) return false; 19 | } 20 | return true; 21 | } 22 | 23 | // check duplicate number in row 24 | const isRowSafe = (grid, row, value) => { 25 | for (let col = 0; col < CONSTANT.GRID_SIZE; col++) { 26 | if (grid[row][col] === value) return false; 27 | } 28 | return true; 29 | } 30 | 31 | // check duplicate number in 3x3 box 32 | const isBoxSafe = (grid, box_row, box_col, value) => { 33 | for (let row = 0; row < CONSTANT.BOX_SIZE; row++) { 34 | for (let col = 0; col < CONSTANT.BOX_SIZE; col++) { 35 | if (grid[row + box_row][col + box_col] === value) return false; 36 | } 37 | } 38 | return true; 39 | } 40 | 41 | // check in row, col and 3x3 box 42 | const isSafe = (grid, row, col, value) => { 43 | return isColSafe(grid, col, value) && isRowSafe(grid, row, value) && isBoxSafe(grid, row - row%3, col - col%3, value) && value !== CONSTANT.UNASSIGNED; 44 | } 45 | 46 | // find unassigned cell 47 | const findUnassignedPos = (grid, pos) => { 48 | for (let row = 0; row < CONSTANT.GRID_SIZE; row++) { 49 | for (let col = 0; col < CONSTANT.GRID_SIZE; col++) { 50 | if (grid[row][col] === CONSTANT.UNASSIGNED) { 51 | pos.row = row; 52 | pos.col = col; 53 | return true; 54 | } 55 | } 56 | } 57 | return false; 58 | } 59 | 60 | // shuffle arr 61 | const shuffleArray = (arr) => { 62 | let curr_index = arr.length; 63 | 64 | while (curr_index !== 0) { 65 | let rand_index = Math.floor(Math.random() * curr_index); 66 | curr_index -= 1; 67 | 68 | let temp = arr[curr_index]; 69 | arr[curr_index] = arr[rand_index]; 70 | arr[rand_index] = temp; 71 | } 72 | 73 | return arr; 74 | } 75 | 76 | // check puzzle is complete 77 | const isFullGrid = (grid) => { 78 | return grid.every((row, i) => { 79 | return row.every((value, j) => { 80 | return value !== CONSTANT.UNASSIGNED; 81 | }); 82 | }); 83 | } 84 | 85 | const sudokuCreate = (grid) => { 86 | let unassigned_pos = { 87 | row: -1, 88 | col: -1 89 | } 90 | 91 | if (!findUnassignedPos(grid, unassigned_pos)) return true; 92 | 93 | let number_list = shuffleArray([...CONSTANT.NUMBERS]); 94 | 95 | let row = unassigned_pos.row; 96 | let col = unassigned_pos.col; 97 | 98 | number_list.forEach((num, i) => { 99 | if (isSafe(grid, row, col, num)) { 100 | grid[row][col] = num; 101 | 102 | if (isFullGrid(grid)) { 103 | return true; 104 | } else { 105 | if (sudokuCreate(grid)) { 106 | return true; 107 | } 108 | } 109 | 110 | grid[row][col] = CONSTANT.UNASSIGNED; 111 | } 112 | }); 113 | 114 | return isFullGrid(grid); 115 | } 116 | 117 | const sudokuCheck = (grid) => { 118 | let unassigned_pos = { 119 | row: -1, 120 | col: -1 121 | } 122 | 123 | if (!findUnassignedPos(grid, unassigned_pos)) return true; 124 | 125 | grid.forEach((row, i) => { 126 | row.forEach((num, j) => { 127 | if (isSafe(grid, i, j, num)) { 128 | if (isFullGrid(grid)) { 129 | return true; 130 | } else { 131 | if (sudokuCreate(grid)) { 132 | return true; 133 | } 134 | } 135 | } 136 | }) 137 | }) 138 | 139 | return isFullGrid(grid); 140 | } 141 | 142 | const rand = () => Math.floor(Math.random() * CONSTANT.GRID_SIZE); 143 | 144 | const removeCells = (grid, level) => { 145 | let res = [...grid]; 146 | let attemps = level; 147 | while (attemps > 0) { 148 | let row = rand(); 149 | let col = rand(); 150 | while (res[row][col] === 0) { 151 | row = rand(); 152 | col = rand(); 153 | } 154 | res[row][col] = CONSTANT.UNASSIGNED; 155 | attemps--; 156 | } 157 | return res; 158 | } 159 | 160 | // generate sudoku base on level 161 | const sudokuGen = (level) => { 162 | let sudoku = newGrid(CONSTANT.GRID_SIZE); 163 | let check = sudokuCreate(sudoku); 164 | if (check) { 165 | let question = removeCells(sudoku, level); 166 | return { 167 | original: sudoku, 168 | question: question 169 | } 170 | } 171 | return undefined; 172 | } --------------------------------------------------------------------------------