├── .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 | 
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 | }
--------------------------------------------------------------------------------