diff --git a/Sudoku Game/.vscode/settings.json b/Sudoku Game/.vscode/settings.json new file mode 100644 index 0000000..6b665aa --- /dev/null +++ b/Sudoku Game/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5501 +} diff --git a/Sudoku Game/bozhin-karaivanov-yqcWOAHH1Rs-unsplash.jpg b/Sudoku Game/bozhin-karaivanov-yqcWOAHH1Rs-unsplash.jpg new file mode 100644 index 0000000..cca934b Binary files /dev/null and b/Sudoku Game/bozhin-karaivanov-yqcWOAHH1Rs-unsplash.jpg differ diff --git a/Sudoku Game/index.html b/Sudoku Game/index.html new file mode 100644 index 0000000..a48b4ec --- /dev/null +++ b/Sudoku Game/index.html @@ -0,0 +1,151 @@ + + + + + + + + + Play Free Sudoku online - solve web sudoku puzzles + + + + + + + +
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ Mistake + 0/3 + Hints: + 3/3 +
+
+ + +
+
+ + + + + + + + + +
+
+ + + + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/Sudoku Game/logo.png b/Sudoku Game/logo.png new file mode 100644 index 0000000..53ff503 Binary files /dev/null and b/Sudoku Game/logo.png differ diff --git a/Sudoku Game/main.js b/Sudoku Game/main.js new file mode 100644 index 0000000..bbd3d69 --- /dev/null +++ b/Sudoku Game/main.js @@ -0,0 +1,171 @@ +const gameBoard = document.getElementById("gameBoard"); +const squares = document.getElementsByClassName("square"); +const numbers = document.getElementsByClassName("number"); +const solveButton = document.getElementById("solveButton"); +const newGameButton = document.getElementById("newGameButton"); +const eraseButton = document.getElementById("eraseButton"); +const hintButton = document.getElementById("hintButton"); +const hints = document.getElementById("hint"); +const mistakeElement = document.getElementById("mistakes"); +let mistake =0; +let hintNumber =3; + +let level = difficulty.value; +let board = sudoku.generate(level,false); +let solvedString = sudoku.solve(board); +let sudokuString = board; +let sudokuBoard = stringToArray(sudokuString); +let sudokuSolved = stringToArray(solvedString); + +setupBoard(); +fillBoard(sudokuBoard); + + +function setupBoard() { + for (let i=0;isquare.classList.remove("active")); + this.classList.add("active"); +} + +function fillBoard(board) { + for (let i=0;isquare.classList.remove("active")); + showAlert(message,color); +} +eraseButton.addEventListener("click",function(){ + let activeSquare = document.querySelector(".active"); + if(activeSquare==null)return; + activeSquare.classList.remove("active"); + if(activeSquare.classList.contains("filled")) return; + activeSquare.innerHTML=""; +}); + +hintButton.addEventListener ("click",function(){ + if(hintNumber==0) { + showAlert("Oops! You've used all your hints."); + return; + } + let activeSquare = document.querySelector(".active"); + activeSquare.classList.remove("active"); + if(activeSquare.classList.contains("filled")) return; + let row = parseInt(activeSquare.id.charAt(0)); + let column = parseInt(activeSquare.id.charAt(1)); + activeSquare.innerHTML = sudokuSolved[row-1][column-1]; + activeSquare.classList.add("filled"); + activeSquare.classList.add("true"); + hintNumber--; + hints.innerHTML = hintNumber+"/3"; +}); +solveButton.addEventListener("click",function(){ + resetBoard(); + fillBoard(sudokuSolved); +}); +newGameButton.addEventListener("click",function(){ + resetBoard(); + level = difficulty.value; + board = sudoku.generate(level,false); + sudokuString = board; + solvedString = sudoku.solve(board); + sudokuBoard = stringToArray(sudokuString); + sudokuSolved = stringToArray(solvedString); + setupBoard(); + fillBoard(sudokuBoard); + mistake = 0; + mistakeElement.innerHTML = "0/3"; + hintNumber =3; + hints.innerHTML = "3/3"; +}); + +function resetBoard() { + for(let i=0;i units map + var SQUARE_PEERS_MAP = null; // Squares -> peers map + + var MIN_GIVENS = 17; // Minimum number of givens + var NR_SQUARES = 81; // Number of squares + + // Define difficulties by how many squares are given to the player in a new + // puzzle. + var DIFFICULTY = { + "easy": 62, + "medium": 53, + "hard": 44, + "very-hard": 35, + "insane": 26, + "inhuman": 17, + }; + + // Blank character and board representation + sudoku.BLANK_CHAR = '.'; + sudoku.BLANK_BOARD = "...................................................."+ + "............................."; + + // Init + // ------------------------------------------------------------------------- + function initialize(){ + /* Initialize the Sudoku library (invoked after library load) + */ + SQUARES = sudoku._cross(ROWS, COLS); + UNITS = sudoku._get_all_units(ROWS, COLS); + SQUARE_UNITS_MAP = sudoku._get_square_units_map(SQUARES, UNITS); + SQUARE_PEERS_MAP = sudoku._get_square_peers_map(SQUARES, + SQUARE_UNITS_MAP); + } + + // Generate + // ------------------------------------------------------------------------- + sudoku.generate = function(difficulty, unique){ + /* Generate a new Sudoku puzzle of a particular `difficulty`, e.g., + + // Generate an "easy" sudoku puzzle + sudoku.generate("easy"); + + + Difficulties are as follows, and represent the number of given squares: + + "easy": 61 + "medium": 52 + "hard": 43 + "very-hard": 34 + "insane": 25 + "inhuman": 17 + + + You may also enter a custom number of squares to be given, e.g., + + // Generate a new Sudoku puzzle with 60 given squares + sudoku.generate(60) + + + `difficulty` must be a number between 17 and 81 inclusive. If it's + outside of that range, `difficulty` will be set to the closest bound, + e.g., 0 -> 17, and 100 -> 81. + + + By default, the puzzles are unique, uless you set `unique` to false. + (Note: Puzzle uniqueness is not yet implemented, so puzzles are *not* + guaranteed to have unique solutions) + + TODO: Implement puzzle uniqueness + */ + + // If `difficulty` is a string or undefined, convert it to a number or + // default it to "easy" if undefined. + if(typeof difficulty === "string" || typeof difficulty === "undefined"){ + difficulty = DIFFICULTY[difficulty] || DIFFICULTY.easy; + } + + // Force difficulty between 17 and 81 inclusive + difficulty = sudoku._force_range(difficulty, NR_SQUARES + 1, + MIN_GIVENS); + + // Default unique to true + unique = unique || true; + + // Get a set of squares and all possible candidates for each square + var blank_board = ""; + for(var i = 0; i < NR_SQUARES; ++i){ + blank_board += '.'; + } + var candidates = sudoku._get_candidates_map(blank_board); + + // For each item in a shuffled list of squares + var shuffled_squares = sudoku._shuffle(SQUARES); + for(var si in shuffled_squares){ + var square = shuffled_squares[si]; + + // If an assignment of a random chioce causes a contradictoin, give + // up and try again + var rand_candidate_idx = + sudoku._rand_range(candidates[square].length); + var rand_candidate = candidates[square][rand_candidate_idx]; + if(!sudoku._assign(candidates, square, rand_candidate)){ + break; + } + + // Make a list of all single candidates + var single_candidates = []; + for(var si in SQUARES){ + var square = SQUARES[si]; + + if(candidates[square].length == 1){ + single_candidates.push(candidates[square]); + } + } + + // If we have at least difficulty, and the unique candidate count is + // at least 8, return the puzzle! + if(single_candidates.length >= difficulty && + sudoku._strip_dups(single_candidates).length >= 8){ + var board = ""; + var givens_idxs = []; + for(var i in SQUARES){ + var square = SQUARES[i]; + if(candidates[square].length == 1){ + board += candidates[square]; + givens_idxs.push(i); + } else { + board += sudoku.BLANK_CHAR; + } + } + + // If we have more than `difficulty` givens, remove some random + // givens until we're down to exactly `difficulty` + var nr_givens = givens_idxs.length; + if(nr_givens > difficulty){ + givens_idxs = sudoku._shuffle(givens_idxs); + for(var i = 0; i < nr_givens - difficulty; ++i){ + var target = parseInt(givens_idxs[i]); + board = board.substr(0, target) + sudoku.BLANK_CHAR + + board.substr(target + 1); + } + } + + // Double check board is solvable + // TODO: Make a standalone board checker. Solve is expensive. + if(sudoku.solve(board)){ + return board; + } + } + } + + // Give up and try a new puzzle + return sudoku.generate(difficulty); + }; + + // Solve + // ------------------------------------------------------------------------- + sudoku.solve = function(board, reverse){ + /* Solve a sudoku puzzle given a sudoku `board`, i.e., an 81-character + string of sudoku.DIGITS, 1-9, and spaces identified by '.', representing the + squares. There must be a minimum of 17 givens. If the given board has no + solutions, return false. + + Optionally set `reverse` to solve "backwards", i.e., rotate through the + possibilities in reverse. Useful for checking if there is more than one + solution. + */ + + // Assure a valid board + var report = sudoku.validate_board(board); + if(report !== true){ + throw report; + } + + // Check number of givens is at least MIN_GIVENS + var nr_givens = 0; + for(var i in board){ + if(board[i] !== sudoku.BLANK_CHAR && sudoku._in(board[i], sudoku.DIGITS)){ + ++nr_givens; + } + } + if(nr_givens < MIN_GIVENS){ + throw "Too few givens. Minimum givens is " + MIN_GIVENS; + } + + // Default reverse to false + reverse = reverse || false; + + var candidates = sudoku._get_candidates_map(board); + var result = sudoku._search(candidates, reverse); + + if(result){ + var solution = ""; + for(var square in result){ + solution += result[square]; + } + return solution; + } + return false; + }; + + sudoku.get_candidates = function(board){ + /* Return all possible candidatees for each square as a grid of + candidates, returnning `false` if a contradiction is encountered. + + Really just a wrapper for sudoku._get_candidates_map for programmer + consumption. + */ + + // Assure a valid board + var report = sudoku.validate_board(board); + if(report !== true){ + throw report; + } + + // Get a candidates map + var candidates_map = sudoku._get_candidates_map(board); + + // If there's an error, return false + if(!candidates_map){ + return false; + } + + // Transform candidates map into grid + var rows = []; + var cur_row = []; + var i = 0; + for(var square in candidates_map){ + var candidates = candidates_map[square]; + cur_row.push(candidates); + if(i % 9 == 8){ + rows.push(cur_row); + cur_row = []; + } + ++i; + } + return rows; + } + + sudoku._get_candidates_map = function(board){ + /* Get all possible candidates for each square as a map in the form + {square: sudoku.DIGITS} using recursive constraint propagation. Return `false` + if a contradiction is encountered + */ + + // Assure a valid board + var report = sudoku.validate_board(board); + if(report !== true){ + throw report; + } + + var candidate_map = {}; + var squares_values_map = sudoku._get_square_vals_map(board); + + // Start by assigning every digit as a candidate to every square + for(var si in SQUARES){ + candidate_map[SQUARES[si]] = sudoku.DIGITS; + } + + // For each non-blank square, assign its value in the candidate map and + // propigate. + for(var square in squares_values_map){ + var val = squares_values_map[square]; + + if(sudoku._in(val, sudoku.DIGITS)){ + var new_candidates = sudoku._assign(candidate_map, square, val); + + // Fail if we can't assign val to square + if(!new_candidates){ + return false; + } + } + } + + return candidate_map; + }; + + sudoku._search = function(candidates, reverse){ + /* Given a map of squares -> candiates, using depth-first search, + recursively try all possible values until a solution is found, or false + if no solution exists. + */ + + // Return if error in previous iteration + if(!candidates){ + return false; + } + + // Default reverse to false + reverse = reverse || false; + + // If only one candidate for every square, we've a solved puzzle! + // Return the candidates map. + var max_nr_candidates = 0; + var max_candidates_square = null; + for(var si in SQUARES){ + var square = SQUARES[si]; + + var nr_candidates = candidates[square].length; + + if(nr_candidates > max_nr_candidates){ + max_nr_candidates = nr_candidates; + max_candidates_square = square; + } + } + if(max_nr_candidates === 1){ + return candidates; + } + + // Choose the blank square with the fewest possibilities > 1 + var min_nr_candidates = 10; + var min_candidates_square = null; + for(si in SQUARES){ + var square = SQUARES[si]; + + var nr_candidates = candidates[square].length; + + if(nr_candidates < min_nr_candidates && nr_candidates > 1){ + min_nr_candidates = nr_candidates; + min_candidates_square = square; + } + } + + // Recursively search through each of the candidates of the square + // starting with the one with fewest candidates. + + // Rotate through the candidates forwards + var min_candidates = candidates[min_candidates_square]; + if(!reverse){ + for(var vi in min_candidates){ + var val = min_candidates[vi]; + + // TODO: Implement a non-rediculous deep copy function + var candidates_copy = JSON.parse(JSON.stringify(candidates)); + var candidates_next = sudoku._search( + sudoku._assign(candidates_copy, min_candidates_square, val) + ); + + if(candidates_next){ + return candidates_next; + } + } + + // Rotate through the candidates backwards + } else { + for(var vi = min_candidates.length - 1; vi >= 0; --vi){ + var val = min_candidates[vi]; + + // TODO: Implement a non-rediculous deep copy function + var candidates_copy = JSON.parse(JSON.stringify(candidates)); + var candidates_next = sudoku._search( + sudoku._assign(candidates_copy, min_candidates_square, val), + reverse + ); + + if(candidates_next){ + return candidates_next; + } + } + } + + // If we get through all combinations of the square with the fewest + // candidates without finding an answer, there isn't one. Return false. + return false; + }; + + sudoku._assign = function(candidates, square, val){ + /* Eliminate all values, *except* for `val`, from `candidates` at + `square` (candidates[square]), and propagate. Return the candidates map + when finished. If a contradiciton is found, return false. + + WARNING: This will modify the contents of `candidates` directly. + */ + + // Grab a list of canidates without 'val' + var other_vals = candidates[square].replace(val, ""); + + // Loop through all other values and eliminate them from the candidates + // at the current square, and propigate. If at any point we get a + // contradiction, return false. + for(var ovi in other_vals){ + var other_val = other_vals[ovi]; + + var candidates_next = + sudoku._eliminate(candidates, square, other_val); + + if(!candidates_next){ + //console.log("Contradiction found by _eliminate."); + return false; + } + } + + return candidates; + }; + + sudoku._eliminate = function(candidates, square, val){ + /* Eliminate `val` from `candidates` at `square`, (candidates[square]), + and propagate when values or places <= 2. Return updated candidates, + unless a contradiction is detected, in which case, return false. + + WARNING: This will modify the contents of `candidates` directly. + */ + + // If `val` has already been eliminated from candidates[square], return + // with candidates. + if(!sudoku._in(val, candidates[square])){ + return candidates; + } + + // Remove `val` from candidates[square] + candidates[square] = candidates[square].replace(val, ''); + + // If the square has only candidate left, eliminate that value from its + // peers + var nr_candidates = candidates[square].length; + if(nr_candidates === 1){ + var target_val = candidates[square]; + + for(var pi in SQUARE_PEERS_MAP[square]){ + var peer = SQUARE_PEERS_MAP[square][pi]; + + var candidates_new = + sudoku._eliminate(candidates, peer, target_val); + + if(!candidates_new){ + return false; + } + } + + // Otherwise, if the square has no candidates, we have a contradiction. + // Return false. + } if(nr_candidates === 0){ + return false; + } + + // If a unit is reduced to only one place for a value, then assign it + for(var ui in SQUARE_UNITS_MAP[square]){ + var unit = SQUARE_UNITS_MAP[square][ui]; + + var val_places = []; + for(var si in unit){ + var unit_square = unit[si]; + if(sudoku._in(val, candidates[unit_square])){ + val_places.push(unit_square); + } + } + + // If there's no place for this value, we have a contradition! + // return false + if(val_places.length === 0){ + return false; + + // Otherwise the value can only be in one place. Assign it there. + } else if(val_places.length === 1){ + var candidates_new = + sudoku._assign(candidates, val_places[0], val); + + if(!candidates_new){ + return false; + } + } + } + + return candidates; + }; + + + // Square relationships + // ------------------------------------------------------------------------- + // Squares, and their relationships with values, units, and peers. + + sudoku._get_square_vals_map = function(board){ + /* Return a map of squares -> values + */ + var squares_vals_map = {}; + + // Make sure `board` is a string of length 81 + if(board.length != SQUARES.length){ + throw "Board/squares length mismatch."; + + } else { + for(var i in SQUARES){ + squares_vals_map[SQUARES[i]] = board[i]; + } + } + + return squares_vals_map; + }; + + sudoku._get_square_units_map = function(squares, units){ + /* Return a map of `squares` and their associated units (row, col, box) + */ + var square_unit_map = {}; + + // For every square... + for(var si in squares){ + var cur_square = squares[si]; + + // Maintain a list of the current square's units + var cur_square_units = []; + + // Look through the units, and see if the current square is in it, + // and if so, add it to the list of of the square's units. + for(var ui in units){ + var cur_unit = units[ui]; + + if(cur_unit.indexOf(cur_square) !== -1){ + cur_square_units.push(cur_unit); + } + } + + // Save the current square and its units to the map + square_unit_map[cur_square] = cur_square_units; + } + + return square_unit_map; + }; + + sudoku._get_square_peers_map = function(squares, units_map){ + /* Return a map of `squares` and their associated peers, i.e., a set of + other squares in the square's unit. + */ + var square_peers_map = {}; + + // For every square... + for(var si in squares){ + var cur_square = squares[si]; + var cur_square_units = units_map[cur_square]; + + // Maintain list of the current square's peers + var cur_square_peers = []; + + // Look through the current square's units map... + for(var sui in cur_square_units){ + var cur_unit = cur_square_units[sui]; + + for(var ui in cur_unit){ + var cur_unit_square = cur_unit[ui]; + + if(cur_square_peers.indexOf(cur_unit_square) === -1 && + cur_unit_square !== cur_square){ + cur_square_peers.push(cur_unit_square); + } + } + } + + // Save the current square an its associated peers to the map + square_peers_map[cur_square] = cur_square_peers; + } + + return square_peers_map; + }; + + sudoku._get_all_units = function(rows, cols){ + /* Return a list of all units (rows, cols, boxes) + */ + var units = []; + + // Rows + for(var ri in rows){ + units.push(sudoku._cross(rows[ri], cols)); + } + + // Columns + for(var ci in cols){ + units.push(sudoku._cross(rows, cols[ci])); + } + + // Boxes + var row_squares = ["ABC", "DEF", "GHI"]; + var col_squares = ["123", "456", "789"]; + for(var rsi in row_squares){ + for(var csi in col_squares){ + units.push(sudoku._cross(row_squares[rsi], col_squares[csi])); + } + } + + return units; + }; + + + // Conversions + // ------------------------------------------------------------------------- + sudoku.board_string_to_grid = function(board_string){ + /* Convert a board string to a two-dimensional array + */ + var rows = []; + var cur_row = []; + for(var i in board_string){ + cur_row.push(board_string[i]); + if(i % 9 == 8){ + rows.push(cur_row); + cur_row = []; + } + } + return rows; + }; + + sudoku.board_grid_to_string = function(board_grid){ + /* Convert a board grid to a string + */ + var board_string = ""; + for(var r = 0; r < 9; ++r){ + for(var c = 0; c < 9; ++c){ + board_string += board_grid[r][c]; + } + } + return board_string; + }; + + + // Utility + // ------------------------------------------------------------------------- + + sudoku.print_board = function(board){ + /* Print a sudoku `board` to the console. + */ + + // Assure a valid board + var report = sudoku.validate_board(board); + if(report !== true){ + throw report; + } + + var V_PADDING = " "; // Insert after each square + var H_PADDING = '\n'; // Insert after each row + + var V_BOX_PADDING = " "; // Box vertical padding + var H_BOX_PADDING = '\n'; // Box horizontal padding + + var display_string = ""; + + for(var i in board){ + var square = board[i]; + + // Add the square and some padding + display_string += square + V_PADDING; + + // Vertical edge of a box, insert v. box padding + if(i % 3 === 2){ + display_string += V_BOX_PADDING; + } + + // End of a line, insert horiz. padding + if(i % 9 === 8){ + display_string += H_PADDING; + } + + // Horizontal edge of a box, insert h. box padding + if(i % 27 === 26){ + display_string += H_BOX_PADDING; + } + } + + console.log(display_string); + }; + + sudoku.validate_board = function(board){ + /* Return if the given `board` is valid or not. If it's valid, return + true. If it's not, return a string of the reason why it's not. + */ + + // Check for empty board + if(!board){ + return "Empty board"; + } + + // Invalid board length + if(board.length !== NR_SQUARES){ + return "Invalid board size. Board must be exactly " + NR_SQUARES + + " squares."; + } + + // Check for invalid characters + for(var i in board){ + if(!sudoku._in(board[i], sudoku.DIGITS) && board[i] !== sudoku.BLANK_CHAR){ + return "Invalid board character encountered at index " + i + + ": " + board[i]; + } + } + + // Otherwise, we're good. Return true. + return true; + }; + + sudoku._cross = function(a, b){ + /* Cross product of all elements in `a` and `b`, e.g., + sudoku._cross("abc", "123") -> + ["a1", "a2", "a3", "b1", "b2", "b3", "c1", "c2", "c3"] + */ + var result = []; + for(var ai in a){ + for(var bi in b){ + result.push(a[ai] + b[bi]); + } + } + return result; + }; + + sudoku._in = function(v, seq){ + /* Return if a value `v` is in sequence `seq`. + */ + return seq.indexOf(v) !== -1; + }; + + sudoku._first_true = function(seq){ + /* Return the first element in `seq` that is true. If no element is + true, return false. + */ + for(var i in seq){ + if(seq[i]){ + return seq[i]; + } + } + return false; + }; + + sudoku._shuffle = function(seq){ + /* Return a shuffled version of `seq` + */ + + // Create an array of the same size as `seq` filled with false + var shuffled = []; + for(var i = 0; i < seq.length; ++i){ + shuffled.push(false); + } + + for(var i in seq){ + var ti = sudoku._rand_range(seq.length); + + while(shuffled[ti]){ + ti = (ti + 1) > (seq.length - 1) ? 0 : (ti + 1); + } + + shuffled[ti] = seq[i]; + } + + return shuffled; + }; + + sudoku._rand_range = function(max, min){ + /* Get a random integer in the range of `min` to `max` (non inclusive). + If `min` not defined, default to 0. If `max` not defined, throw an + error. + */ + min = min || 0; + if(max){ + return Math.floor(Math.random() * (max - min)) + min; + } else { + throw "Range undefined"; + } + }; + + sudoku._strip_dups = function(seq){ + /* Strip duplicate values from `seq` + */ + var seq_set = []; + var dup_map = {}; + for(var i in seq){ + var e = seq[i]; + if(!dup_map[e]){ + seq_set.push(e); + dup_map[e] = true; + } + } + return seq_set; + }; + + sudoku._force_range = function(nr, max, min){ + /* Force `nr` to be within the range from `min` to, but not including, + `max`. `min` is optional, and will default to 0. If `nr` is undefined, + treat it as zero. + */ + min = min || 0 + nr = nr || 0 + if(nr < min){ + return min; + } + if(nr > max){ + return max; + } + return nr + } + + // Initialize library after load + initialize(); + +// Pass whatever the root object is, lsike 'window' in browsers +})(this); \ No newline at end of file diff --git a/script.js b/script.js index 6599335..7fdc7f8 100644 --- a/script.js +++ b/script.js @@ -241,6 +241,13 @@ const projects = [ "Its a basics Javascript snake game using DOM Event.", link: "./Snake_Game/index.html", image: "https://w7.pngwing.com/pngs/543/374/png-transparent-snake-2000-classic-nokia-game-slither-worm-snake-a-classic-snake-game-classic-game-snake-ii-snake-food-animals-grass-thumbnail.png" + }, + { + title: "Sudoku Game", + discription: + "Sudoku Game game made using HTML, CSS and JS", + link: "./Sudoku Game/index.html", + image: "./Sudoku Game/bozhin-karaivanov-yqcWOAHH1Rs-unsplash.jpg" } ];