How to Create a Sudoku Puzzle game with Html

When We first started playing with writing a Sudoku game, I took the long way to create Sudoku templates. Using a recursive backtracking algorithm, I would slowly fill up the grid with random numbers – checking the validity of the puzzle at each step. If I hit a roadblock where the puzzle cannot be completed, the algorithm would backtrack until it could move forward, and then move forward again. how to make a sudoku game in javascript,
how to make a sudoku puzzle in html

There is a more elegant solution however: create a solved sudoku and then shuffle it.

Generate and Solved Sudoku

1.Step : Creating the solved Sudoku is easy: just shift the row above to the left by 3 unless its vertical index is equally divisible by 3 (starting with index 0) in which case shift the row above by 4.

1 2 3 4 5 6 7 8 9
4 5 6 7 8 9 1 2 3
7 8 9 1 2 3 4 5 6
2 3 4 5 6 7 8 9 1
5 6 7 8 9 1 2 3 4
8 9 1 2 3 4 5 6 7
3 4 5 6 7 8 9 1 2
6 7 8 9 1 2 3 4 5
9 1 2 3 4 5 6 7 8


2.Step : Shuffle the rows and columns. Of course, to do so and ensure the Sudoku rules maintain integrity, you can only shuffle rows and columns of the same group: groups being 1-3, 4-6, and 7-9. Demonstrated below, the columns 1 and 3 are shuffled

Sudoku Puzzle Game Source Code

index.html

<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>DheerajHitech - 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>

Sudoku puzzle in html

style.css

\**********************************/
@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;
}

script.js

// 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: ['&raquo;', '&raquo;', '&gt;', '&gt;'],
      classes: 'fr',
      mods: [
      'neutral',
      'compound',
      'compound-last',
      `${!historyStash.length ?
      'disabled' :
      ''
      }`] })

    }
          ${buttonComponent({
      id: 'undo-button',
      text: ['&laquo;', '&laquo;', '&lt;', '&lt;'],
      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();

Additional Reading

Leave a Reply

Your email address will not be published. Required fields are marked *