How to Create a Sudoku Puzzle game using Html,CSS and Javascript
Sudoku is a logic-based, combinatorial number-placement puzzle. In classic sudoku, the objective is to fill a 9×9 grid with digits so that each column, each row, and each of the nine 3×3 subgrids that compose the grid contain all of the digits from 1 to 9.
HTML Code
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>TechWorld4U9 - Sudoku Puzzle Project</title>
<meta name="viewport" content="width=device-width, initial-scale=1"><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id='sudoku-app'></div>
<script type='text/javascript'>
"use strict";!function(a){if("function"==typeof bootstrap)bootstrap("bem",a);else if("object"==typeof exports&&"object"==typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define(a);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeBem=a}else{if("undefined"==typeof window&&"undefined"==typeof self)throw new Error("This environment was not anticipated by bem. Please file a bug.");var b="undefined"!=typeof window?window:self,c=b.bem;b.bem=a(),b.bem.noConflict=function(){return b.bem=c,this}}}(function(){function a(a){"undefined"!=typeof a.modifier&&(c.modifier=a.modifier),"undefined"!=typeof a.element&&(c.element=a.element)}function b(a){if(!d.validate(a))return null;var b=a.block,e=a.element,f=a.modifiers,g=b,h=[];return!!e&&(g+=""+c.element+e),!!f&&Object.keys(f).forEach(function(a){var d=f[a],i="function"==typeof d?d(b,e,f):d;!!i&&h.push(""+g+c.modifier+a+" ")}),(g+" "+h.join("")).slice(0,-1)}var c={element:"__",modifier:"--"},d={messages:{block:"You must specify the name of block.",element:"Element name must be a string.",modifier:"Modifiers must be supplied in the `{name : bool || fn}` style."},blockName:function(a){return"undefined"!=typeof a&&"string"==typeof a&&a.length?!0:(console.warn(this.messages.block),!1)},element:function(a){return"undefined"!=typeof a&&"string"!=typeof a?(console.warn(this.messages.element),!1):!0},modifiers:function(a){return"undefined"==typeof a||"object"==typeof a&&"[object Object]"===toString.call(a)?!0:(console.warn(this.messages.modifier),!1)},validate:function(a){return this.blockName(a.block)&&this.element(a.element)&&this.modifiers(a.modifiers)}};return{setDelimiters:a,makeClassName:b}});
</script>
<!-- Include Babel to transform code in browser -->
<script type='text/javascript'
src='https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.25/browser-polyfill.min.js'>
</script>
<script type='text/javascript'
src='https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.25/browser.min.js'>
</script>
<!-- worker, sudoku api -->
<script type='text/babel' id='worker'>
self.sudoku = null
// Worker Setup
self.addEventListener('message', (event) => {
var options = { method: null }
try {
options = JSON.parse(event.data);
} catch (e) {
console.warn('event.data is misformed', event)
}
switch (options.method) {
case 'generate':
var { hints, limit } = options
self.sudoku = new Sudoku(hints, limit).generate()
self.postMessage({
success: self.sudoku.success,
board: self.sudoku.getBoard(),
solution: self.sudoku.getSolution()
});
break;
case 'validate':
var { map, number, index } = options
self.postMessage({
result: sudoku.validate(map, number, index)
});
break;
}
}, false);
// API
class Sudoku {
constructor(hints, limit) {
this.hints = hints
this.limit = limit || 10000
this._logs = {
raw: [],
incidents: {
limitExceeded: 0,
notValid: 0,
noNumbers: 0
}
}
this.success = null
this.numbers = () =>
new Array(9)
.join(" ")
.split(" ")
.map((num , i) => i + 1)
/*
Will be used in initial map. Each row will be
consisted of randomly ordered numbers
*/ this.randomRow = () => {
var row = []
var numbers = this.numbers()
while (row.length < 9) {
var index = Math.floor(Math.random() * numbers.length)
row.push(numbers[index])
numbers.splice(index, 1)
}
return row
}
/*
This is the dummy placeholder for the
final results. Will be overridden through the
backtracking process, and at the and, this will
be the real results.
*/ this.result = new Array(9 * 9)
.join(" ")
.split(" ")
.map(entry => null)
/*
Will be used as the nodeTree in the
process of backtracking. Each cell has 9 alternative
paths (randomly ordered).
*/ this.map = new Array(9 * 9)
.join(" ")
.split(" ")
.map(path => this.randomRow())
/*
Will be used as history in the backtracking
process for checking if a candidate number is valid.
*/ this.stack = []
return this
}
toRows(arr) {
var row = 0
var asRows = new Array(9)
.join(" ")
.split(" ")
.map(row => [])
for (let [index, entry] of arr.entries()) {
asRows[row].push(entry)
if ( !((index + 1) % 9) ) {
row += 1
}
}
return asRows
}
no(path, index, msg) {
var number = path[path.length - 1]
this._logs.raw.push(`no: @${index} [${number}] ${msg} ${path} `)
}
yes(path, index) {
this._logs.raw.push(`yes: ${index} ${path}`)
}
finalLog() {
console.groupCollapsed('Raw Logs')
console.groupCollapsed(this._logs.raw)
console.groupEnd()
console.groupEnd()
console.groupCollapsed('Incidents')
console.groupCollapsed(this._logs.incidents)
console.groupEnd()
console.groupEnd()
}
getBoard() {
return this.toRows(this.substractCells())
}
getSolution() {
return this.toRows(this.result)
}
substractCells() {
var _getNonEmptyIndex = () => {
var index = Math.floor(Math.random() * _result.length)
return _result[index] ? index : _getNonEmptyIndex()
}
var _result = this.result.filter(() => true)
while (
_result.length - this.hints >
_result.filter(n => !n).length
) {
_result[_getNonEmptyIndex()] = ''
}
return _result
}
validate(map, number, index) {
var rowIndex = Math.floor(index / 9)
var colIndex = index % 9
var row = map.slice(
rowIndex * 9, 9 * (rowIndex + 1)
)
var col = map.filter((e, i) =>
i % 9 === colIndex
)
var boxRow = Math.floor(rowIndex / 3)
var boxCol = Math.floor(colIndex / 3)
var box = map.filter((e, i) =>
Math.floor(Math.floor(i / 9) / 3) === boxRow &&
Math.floor((i % 9) / 3) === boxCol
)
return {
row: {
first: row.indexOf(number),
last: row.lastIndexOf(number)
},
col: {
first: col.indexOf(number),
last: col.lastIndexOf(number)
},
box: {
first: box.indexOf(number),
last: box.lastIndexOf(number)
}
}
}
_validate(map, index) {
if (!map[index].length) {
return false
}
this.stack.splice(index, this.stack.length)
var path = map[index]
var number = path[path.length - 1]
var didFoundNumber = this.validate(this.stack, number, index)
return (
didFoundNumber.col.first === -1 &&
didFoundNumber.row.first === -1 &&
didFoundNumber.box.first === -1
)
}
_generate(map, index) {
if (index === 9 * 9) {
return true
}
if (--this.limit < 0) {
this._logs.incidents.limitExceeded++
this.no(map[index], index, 'limit exceeded')
return false
}
var path = map[index]
if (!path.length) {
map[index] = this.numbers()
map[index - 1].pop()
this._logs.incidents.noNumbers++
this.no(path, index, 'no numbers in it')
return false
}
var currentNumber = path[path.length - 1]
var isValid = this._validate(map, index)
if (!isValid) {
map[index].pop()
map[index + 1] = this.numbers()
this._logs.incidents.notValid++
this.no(path, index, 'is not valid')
return false
} else {
this.stack.push(currentNumber)
}
for (let number of path.entries()) {
if (this._generate(map, index + 1)) {
this.result[index] = currentNumber
this.yes(path, index)
return true
}
}
return false
}
generate() {
if (this._generate(this.map, 0)) {
this.success = true
}
this.finalLog()
return this
}
}
</script>
<!-- partial -->
<script src='https://cdn.rawgit.com/MaxArt2501/object-observe/master/dist/object-observe-lite.min.js'></script><script src="./script.js"></script>
</body>
</html>
CSS Code
\**********************************/@font-face {
src: url("http://enes.in/GillSansTr-LightNr.otf");
font-family: Gill;
font-weight: 100;
}
@font-face {
src: url("http://enes.in/GillSansTr-Normal.otf");
font-family: Gill;
font-weight: 300;
}
@font-face {
src: url("http://enes.in/GillSansTr-Bold.otf");
font-family: Gill;
font-weight: 600;
}
@font-face {
src: url("http://enes.in/GillSansTr-ExtraBold.otf");
font-family: Gill;
font-weight: 700;
}
@font-face {
src: url("http://enes.in/GillSansTr-UltraBold.otf");
font-family: Gill;
font-weight: 900;
}
html, body {
width: 100%;
height: 100%;
}
body {
margin: 0;
background: #f0f0f0;
}
@media (max-width: 260px) {
.show-on-sm {
display: none;
}
.show-on-md {
display: none;
}
.show-on-lg {
display: none;
}
.show-on-xs {
display: block;
}
}
@media (max-width: 420px) {
.show-on-xs {
display: none;
}
.show-on-md {
display: none;
}
.show-on-lg {
display: none;
}
.show-on-sm {
display: block;
}
}
@media (min-width: 421px) and (max-width: 615px) {
.show-on-xs {
display: none;
}
.show-on-sm {
display: none;
}
.show-on-lg {
display: none;
}
.show-on-md {
display: block;
}
}
@media (min-width: 615px) {
.show-on-xs {
display: none;
}
.show-on-sm {
display: none;
}
.show-on-md {
display: none;
}
.show-on-lg {
display: block;
}
}
@-webkit-keyframes progress {
0% {
box-shadow: none;
}
25% {
box-shadow: 2px -2px 0 1px;
}
50% {
box-shadow: 2px -2px 0 1px, 7px -2px 0 1px;
}
100% {
box-shadow: 2px -2px 0 1px, 7px -2px 0 1px, 12px -2px 0 1px;
}
}
@keyframes progress {
0% {
box-shadow: none;
}
25% {
box-shadow: 2px -2px 0 1px;
}
50% {
box-shadow: 2px -2px 0 1px, 7px -2px 0 1px;
}
100% {
box-shadow: 2px -2px 0 1px, 7px -2px 0 1px, 12px -2px 0 1px;
}
}
.fr {
float: right;
}
.fl {
float: left;
}
@media (max-width: 260px) {
.button {
padding: 0.25em 0.5em;
font-size: 0.6em;
}
.button:not(:last-of-type) {
margin-right: 0.15em;
}
.button--loading {
padding-right: 1.5em;
}
}
@media (min-width: 261px) and (max-width: 420px) {
.button {
padding: 0.25em 0.5em 0.15em;
font-size: 0.75em;
}
.button:not(:last-of-type) {
margin-right: 0.25em;
}
.button--loading {
padding-right: 1.5em;
}
}
@media (min-width: 421px) and (max-width: 615px) {
.button {
padding: 0.5em 0.75em 0.4em;
font-size: 0.9em;
}
.button:not(:last-of-type) {
margin-right: 0.5em;
}
.button--loading {
padding-right: 1.5em;
}
}
@media (min-width: 615px) {
.button {
padding: 0.75em 1em 0.6em;
font-size: 1em;
}
.button:not(:last-of-type) {
margin-right: 0.75em;
}
.button--loading {
padding-right: 1.5em;
}
}
.button {
border: 1px solid;
font-weight: normal;
border-radius: 3px;
background: none;
box-shadow: none;
-webkit-transition: all 0.2s;
transition: all 0.2s;
}
.button--primary {
color: #4242d7;
font-weight: 600;
}
.button--primary:hover, .button--primary:focus, .button--primary:active {
border-color: #4242d7;
background: #4242d7;
}
.button--primary:focus {
box-shadow: 0 0 5px #4242d7;
}
.button--secondary {
color: #d74242;
}
.button--secondary:hover, .button--secondary:focus, .button--secondary:active {
border-color: #d74242;
background: #d74242;
}
.button--secondary:focus {
box-shadow: 0 0 5px #d74242;
}
.button--tertiary {
color: #fff;
border-color: #2ECC40;
background: #2ECC40;
}
.button--neutral {
color: #333;
}
.button--neutral:hover, .button--neutral:focus, .button--neutral:active {
border-color: #333;
background: #333;
}
.button--neutral:focus {
box-shadow: 0 0 5px #333;
}
.button--compound {
border-radius: 0;
border-right: none;
}
.button--compound-first {
border-bottom-left-radius: 3px;
border-top-left-radius: 3px;
}
.button--compound-last {
border-bottom-right-radius: 3px;
border-top-right-radius: 3px;
border-right: 1px solid;
}
.button--muted {
pointer-events: none;
}
.button--disabled {
border-color: #bbb;
color: #bbb;
pointer-events: none;
}
.button--loading-text::after {
display: inline-block;
width: 1px;
height: 1px;
content: '';
box-shadow: 2px -2px 1px 0;
-webkit-animation: progress 1s infinite;
animation: progress 1s infinite;
}
.button:hover, .button:focus, .button:active {
color: #fff;
}
.button:focus {
outline: none;
}
.button:active {
box-shadow: inset 0 -2px 10px rgba(0, 0, 0, 0.4);
}
.message {
font-size: .9em;
padding: 2em;
margin: 0;
border-radius: 3px;
color: rgba(0, 0, 0, 0.75);
}
.message--busy {
background: rgba(0, 0, 255, 0.1);
}
.message--fail {
background: rgba(255, 0, 0, 0.1);
}
@media (max-width: 260px) {
.sudoku {
margin: 0 auto;
padding-top: 0.5em;
padding-bottom: 0.5em;
}
.sudoku__header {
padding-bottom: 0.6em;
}
.sudoku__title {
font-size: 1em;
}
.sudoku__table {
font-size: 0.9em;
border-top: 2px solid #444;
border-left: 2px solid #444;
border-collapse: collapse;
}
.sudoku__table-row {
border-bottom: 1px solid #444;
border-right: 2px solid #444;
}
.sudoku__table-row--separator {
border-bottom: 2px solid #444;
}
.sudoku__table-cell {
width: 16px;
height: 16px;
border-right: 1px solid #444;
}
.sudoku__table-cell--separator {
border-right: 2px solid #444;
}
.sudoku {
max-width: calc(260px / 1.5);
min-width: calc(260px / 2);
}
}
@media (min-width: 261px) and (max-width: 420px) {
.sudoku {
margin: 0 auto;
padding-top: 1em;
padding-bottom: 1em;
}
.sudoku__header {
padding-bottom: 0.9em;
}
.sudoku__title {
font-size: 1.2em;
}
.sudoku__table {
font-size: 1.2em;
border-top: 3px solid #444;
border-left: 3px solid #444;
border-collapse: collapse;
}
.sudoku__table-row {
border-bottom: 1px solid #444;
border-right: 3px solid #444;
}
.sudoku__table-row--separator {
border-bottom: 3px solid #444;
}
.sudoku__table-cell {
width: 32px;
height: 32px;
border-right: 1px solid #444;
}
.sudoku__table-cell--separator {
border-right: 3px solid #444;
}
.sudoku {
width: 260px;
}
}
@media (min-width: 421px) and (max-width: 615px) {
.sudoku {
margin: 0 auto;
padding-top: 2em;
padding-bottom: 2em;
}
.sudoku__header {
padding-bottom: 1.3em;
}
.sudoku__title {
font-size: 1.5em;
}
.sudoku__table {
font-size: 1.5em;
border-top: 4px solid #444;
border-left: 4px solid #444;
border-collapse: collapse;
}
.sudoku__table-row {
border-bottom: 1px solid #444;
border-right: 4px solid #444;
}
.sudoku__table-row--separator {
border-bottom: 4px solid #444;
}
.sudoku__table-cell {
width: 48px;
height: 48px;
border-right: 1px solid #444;
}
.sudoku__table-cell--separator {
border-right: 4px solid #444;
}
.sudoku {
width: 420px;
}
}
@media (min-width: 615px) {
.sudoku {
margin: 0 auto;
padding-top: 3em;
padding-bottom: 3em;
}
.sudoku__header {
padding-bottom: 1.618em;
}
.sudoku__title {
font-size: 2em;
}
.sudoku__table {
font-size: 1.75em;
border-top: 6px solid #444;
border-left: 6px solid #444;
border-collapse: collapse;
}
.sudoku__table-row {
border-bottom: 2px solid #444;
border-right: 6px solid #444;
}
.sudoku__table-row--separator {
border-bottom: 6px solid #444;
}
.sudoku__table-cell {
width: 64px;
height: 64px;
border-right: 2px solid #444;
}
.sudoku__table-cell--separator {
border-right: 6px solid #444;
}
.sudoku {
width: 615px;
}
}
.sudoku {
color: #444;
}
.sudoku__header {
font-family: Gill, sans-serif;
}
.sudoku__title {
font-weight: 600;
}
.sudoku__description {
max-width: 640px;
line-height: 1.4;
font-weight: 100;
}
.sudoku__table {
background: #fff;
}
.sudoku__table-cell {
overflow: hidden;
text-align: center;
-webkit-transition: all .25s;
transition: all .25s;
}
.sudoku__table-cell--editable {
color: #2020df;
}
.sudoku__table-cell--editable:focus {
background: rgba(0, 0, 255, 0.1);
outline: none;
}
.sudoku__table-cell--error {
color: red;
background: #fdd;
}
.sudoku__table-cell--editable-error {
text-shadow: 0 0 15px;
}
.sudoku__table-cell--editable-error:focus {
color: #eee;
background: #f45;
}
JavaScript Code
// Utility
var utils = (() => {
function dom(selector) {
if (selector[0] === '#') {
return document.getElementById(selector.slice(1));
}
return document.querySelectorAll(selector);
}
function copyJSON(obj) {
return JSON.parse(JSON.stringify(obj));
}
function isTouchDevice() {
return navigator.userAgent.
match(/(iPhone|iPod|iPad|Android|BlackBerry)/);
}
function getWorkerURLFromElement(selector) {
var element = dom(selector);
var content = babel.transform(element.innerText).code;
var blob = new Blob([content], { type: 'text/javascript' });
return URL.createObjectURL(blob);
}
var cursorManager = function () {
var cursorManager = {};
var voidNodeTags = [
'AREA', 'BASE', 'BR', 'COL', 'EMBED',
'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK',
'MENUITEM', 'META', 'PARAM', 'SOURCE',
'TRACK', 'WBR', 'BASEFONT', 'BGSOUND',
'FRAME', 'ISINDEX'];
Array.prototype.contains = function (obj) {
var i = this.length;
while (i--) {
if (this[i] === obj) {
return true;
}
}
return false;
};
function canContainText(node) {
if (node.nodeType == 1) {
return !voidNodeTags.contains(node.nodeName);
} else {
return false;
}
};
function getLastChildElement(el) {
var lc = el.lastChild;
while (lc && lc.nodeType != 1) {
if (lc.previousSibling)
lc = lc.previousSibling;else
break;
}
return lc;
}
cursorManager.setEndOfContenteditable = function (contentEditableElement) {
while (getLastChildElement(contentEditableElement) &&
canContainText(getLastChildElement(contentEditableElement))) {
contentEditableElement = getLastChildElement(contentEditableElement);
}
var range, selection;
if (document.createRange) {
range = document.createRange();
range.selectNodeContents(contentEditableElement);
range.collapse(false);
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
} else
if (document.selection)
{
range = document.body.createTextRange();
range.moveToElementText(contentEditableElement);
range.collapse(false);
range.select();
}
};
return cursorManager;
}();
return {
copyJSON, cursorManager, dom,
getWorkerURLFromElement, isTouchDevice };
})();
// API Adapter
class SudokuAdapter {
constructor(url) {
this.worker = new Worker(url);
return this;
}
_postMessage(options) {
this.worker.postMessage(JSON.stringify(options));
return new Promise((resolve, reject) => {
this.worker.onmessage = event => {
resolve(event.data);
};
});
}
generate(options) {
options = Object.assign(
{}, options, { method: 'generate' });
return this._postMessage(options);
}
validate(options) {
options = Object.assign(
{}, options, { method: 'validate' });
return this._postMessage(options);
}}
// Client Side Settings
const SUDOKU_APP_CONFIG = {
HINTS: 34,
TRY_LIMIT: 100000,
WORKER_URL: utils.getWorkerURLFromElement('#worker'),
DOM_TARGET: utils.dom('#sudoku-app') };
// Client Side
var SudokuApp = (config => {
const {
HINTS, TRY_LIMIT,
WORKER_URL, DOM_TARGET } =
config;
var sudokuAdapter = new SudokuAdapter(WORKER_URL);
var state = {
success: null,
board: null,
solution: null,
solved: null,
errors: [] };
Object.observe(state, render);
var history = [state];
var historyStash = [];
// Event listeners
var onClickGenerate = initialize;
var onClickSolve = function () {
setState({
board: state.solution,
solved: true,
errors: [] });
};
var onKeyUpCell = function (event) {
var key = event.keyCode;
if ( // a
key === 36 || // r
key === 37 || // r
key === 38 || // o
key === 39 || // w
key === 9 || // tab
// mod key flags are always false in keyup event
// keyIdentifier doesn't seem to be implemented
// in all browsers
key === 17 || // Control
key === 16 || // Shift
key === 91 || // Meta
key === 19 || // Alt
event.keyIdentifier === 'Control' ||
event.keyIdentifier === 'Shift' ||
event.keyIdentifier === 'Meta' ||
event.keyIdentifier === 'Alt')
return;
var cell = event.target;
var value = cell.innerText;
if (value.length > 4) {
cell.innerText = value.slice(0, 4);
return false;
}
var cellIndex = cell.getAttribute('data-cell-index');
cellIndex = parseInt(cellIndex, 10);
var rowIndex = Math.floor(cellIndex / 9);
var cellIndexInRow = cellIndex - rowIndex * 9;
var board = Object.assign([], state.board);
board[rowIndex].splice(cellIndexInRow, 1, value);
validate(board).then(errors => {
historyStash = [];
history.push({});
var solved = null;
if (errors.indexOf(true) === -1) {
solved = true;
board.forEach(row => {
row.forEach(value => {
if (!value || !parseInt(value, 10) || value.length > 1) {
solved = false;
}
});
});
}
if (solved) {
board = Object.assign([], board).map(row => row.map(n => +n));
}
setState({ board, errors, solved }, newState => {
history[history.length - 1] = newState;
restoreCaretPosition(cellIndex);
});
});
};
function keyDown(event) {
var keys = {
ctrlOrCmd: event.ctrlKey || event.metaKey,
shift: event.shiftKey,
z: event.keyCode === 90 };
if (keys.ctrlOrCmd && keys.z) {
if (keys.shift && historyStash.length) {
redo();
} else if (!keys.shift && history.length > 1) {
undo();
}
}
}
function undo() {
historyStash.push(history.pop());
setState(utils.copyJSON(history[history.length - 1]));
}
function redo() {
history.push(historyStash.pop());
setState(utils.copyJSON(history[history.length - 1]));
}
function initialize() {
unbindEvents();
render();
getSudoku().then(sudoku => {
setState({
success: sudoku.success,
board: sudoku.board,
solution: sudoku.solution,
errors: [],
solved: false },
newState => {
history = [newState];
historyStash = [];
});
});
}
function setState(newState, callback) {
requestAnimationFrame(() => {
Object.assign(state, newState);
if (typeof callback === 'function') {
var param = utils.copyJSON(state);
requestAnimationFrame(callback.bind(null, param));
}
});
}
function bindEvents() {
var generateButton = utils.dom('#generate-button');
var solveButton = utils.dom('#solve-button');
var undoButton = utils.dom('#undo-button');
var redoButton = utils.dom('#redo-button');
generateButton &&
generateButton.
addEventListener('click', onClickGenerate);
solveButton &&
solveButton.
addEventListener('click', onClickSolve);
undoButton &&
undoButton.
addEventListener('click', undo);
redoButton &&
redoButton.
addEventListener('click', redo);
var cells = utils.dom('.sudoku__table-cell');
[].forEach.call(cells, cell => {
cell.addEventListener('keyup', onKeyUpCell);
});
window.addEventListener('keydown', keyDown);
}
function unbindEvents() {
var generateButton = utils.dom('#generate-button');
var solveButton = utils.dom('#solve-button');
var undoButton = utils.dom('#undo-button');
var redoButton = utils.dom('#redo-button');
generateButton &&
generateButton.
removeEventListener('click', onClickGenerate);
solveButton &&
solveButton.
removeEventListener('click', onClickSolve);
undoButton &&
undoButton.
removeEventListener('click', undo);
redoButton &&
redoButton.
removeEventListener('click', redo);
var cells = utils.dom('.sudoku__table-cell');
[].forEach.call(cells, cell => {
cell.removeEventListener('keyup', onKeyUpCell);
});
window.removeEventListener('keydown', keyDown);
}
function restoreCaretPosition(cellIndex) {
utils.cursorManager.setEndOfContenteditable(
utils.dom(`[data-cell-index="${cellIndex}"]`)[0]);
}
function getSudoku() {
return sudokuAdapter.generate({
hints: HINTS,
limit: TRY_LIMIT });
}
function validate(board) {
var map = board.reduce((memo, row) => {
for (let num of row) {
memo.push(num);
}
return memo;
}, []).map(num => parseInt(num, 10));
var validations = [];
// Will validate one by one
for (let [index, number] of map.entries()) {
if (!number) {
validations.push(
new Promise(res => {
res({ result: { box: -1, col: -1, row: -1 } });
}));
} else {
let all = Promise.all(validations);
validations.push(all.then(() => {
return sudokuAdapter.validate({ map, number, index });
}));
}
}
return Promise.all(validations).
then(values => {
var errors = [];
for (let [index, validation] of values.entries()) {
let { box, col, row } = validation.result;
let errorInBox = box.first !== box.last;
let errorInCol = col.first !== col.last;
let errorInRow = row.first !== row.last;
let indexOfRow = Math.floor(index / 9);
let indexInRow = index - indexOfRow * 9;
errors[index] = errorInRow || errorInCol || errorInBox;
}
return errors;
});
}
function render() {
unbindEvents();
DOM_TARGET.innerHTML = `
<div class='sudoku'>
${headerComponent()}
${contentComponent()}
</div>
`;
bindEvents();
}
function buttonComponent(props) {
var { id, text, mods, classes } = props;
var blockName = 'button';
var modifiers = {};
var modType = toString.call(mods);
if (modType === '[object String]') {
modifiers[mods] = true;
} else if (modType === '[object Array]') {
for (let modName of mods) {
modifiers[modName] = true;
}
}
var blockClasses = bem.makeClassName({
block: blockName,
modifiers: modifiers });
var buttonTextClass = `${blockName}-text`;
if (Object.keys(modifiers).length) {
buttonTextClass +=
Object.keys(modifiers).reduce((memo, curr) => {
return memo + ` ${blockName}--${curr}-text`;
}, '');
}
var lgText = typeof text === 'string' ?
text : text[0];
var mdText = typeof text === 'string' ?
text : text[1];
var smText = typeof text === 'string' ?
text : text[2];
return `
<button
id='${id}'
class='${blockClasses} ${classes || ""}'>
<span class='show-on-sm ${buttonTextClass}'>
${smText}
</span>
<span class='show-on-md ${buttonTextClass}'>
${mdText}
</span>
<span class='show-on-lg ${buttonTextClass}'>
${lgText}
</span>
</button>
`;
}
function messageComponent(options) {
var { state, content } = options;
var messageClass = bem.makeClassName({
block: 'message',
modifiers: state ? {
[state]: true } :
{} });
return `
<p class='${messageClass}'>
${content}
</p>
`;
}
function descriptionComponent(options) {
var { className, infoLevel } = options;
var technical = `
In this demo,
<a href='https://en.wikipedia.org/wiki/Backtracking'>
backtracking algorithm
</a> is used for <em>making</em>
the sudoku project.`;
var description = `
Difficulty and solvability is
totally random as I randomly left a certain number of hints
from a full-filled board.
`;
if (infoLevel === 'full') {
return `
<p class='${className || ''}'>
${technical} ${description}
</p>
`;
} else if (infoLevel === 'mini') {
return `
<p class='${className || ''}'>
${description}
</p>
`;
}
}
function restoreScrollPosComponent() {
return `<div style='height: 540px'></div>`;
}
function headerComponent() {
return `
<div class='sudoku__header'>
<h1 class='sudoku__title'>
<span class='show-on-sm'>
Sudoku
</span>
<span class='show-on-md'>
Sudoku Puzzle
</span>
<span class='show-on-lg'>
Sudoku Puzzle Project
</span>
</h1>
${descriptionComponent({
infoLevel: 'mini',
className: 'sudoku__description show-on-md' })
}
${descriptionComponent({
infoLevel: 'full',
className: 'sudoku__description show-on-lg' })
}
${
state.success ? `
${buttonComponent({
id: 'generate-button',
text: ['New Board', 'New Board', 'New'],
mods: 'primary' })
}
${state.solved ?
buttonComponent({
id: 'solve-button',
text: 'Solved',
mods: ['tertiary', 'muted'] }) :
buttonComponent({
id: 'solve-button',
text: 'Solve',
mods: 'secondary' })
}
` :
`
${buttonComponent({
id: 'generate-button',
text: ['Generating', '', ''],
mods: ['disabled', 'loading'] })
}
${buttonComponent({
id: 'solve-button',
text: 'Solve',
mods: 'disabled' })
}
`
}
${utils.isTouchDevice() ? `
${buttonComponent({
id: 'redo-button',
text: ['»', '»', '>', '>'],
classes: 'fr',
mods: [
'neutral',
'compound',
'compound-last',
`${!historyStash.length ?
'disabled' :
''
}`] })
}
${buttonComponent({
id: 'undo-button',
text: ['«', '«', '<', '<'],
classes: 'fr',
mods: [
'neutral',
'compound',
'compound-first',
`${history.length > 1 ?
'' :
'disabled'
}`] })
}
` : ''}
</div>
`;
}
function contentComponent() {
var _isSeparator = (index) =>
!!index && !((index + 1) % 3);
var resultReady = !!state.board;
var fail = resultReady && !state.success;
if (!resultReady) {
return `
${messageComponent({
state: 'busy',
content: `Generating new board...` })
}
${restoreScrollPosComponent()}
`;
}
if (fail) {
return `
${messageComponent({
state: 'fail',
content: `Something went wrong with this board, try generating another one.` })
}
${restoreScrollPosComponent()}
`;
}
var rows = state.board;
return `
<table class='sudoku__table'>
${rows.map((row, index) => {
let className = bem.makeClassName({
block: 'sudoku',
element: 'table-row',
modifiers: {
separator: _isSeparator(index) } });
return (
`<tr class='${className}'>
${row.map((num, _index) => {
let cellIndex = index * 9 + _index;
let separator = _isSeparator(_index);
let editable = typeof num !== 'number';
let error = state.errors[cellIndex];
let className = bem.makeClassName({
block: 'sudoku',
element: 'table-cell',
modifiers: {
separator,
editable,
error,
'editable-error': editable && error } });
return (
`\n\t
<td class='${className}'
data-cell-index='${cellIndex}'
${editable ? 'contenteditable' : ''}>
${num}
</td>`);
}).join('')}
\n</tr>\n`);
}).join('')}
</table>
`;
}
return { initialize };
})(SUDOKU_APP_CONFIG).initialize();