<div id="main"></div>
:root{
  --main-purple: #001d3d;
}

.main-grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  height: 500px;
  max-width: 500px;
  margin: auto;
  margin-top: 80px;
  background-color: ghostwhite;
}

html, body {
  height: calc(100% - 70px);
  font-family: 'Cutive Mono', monospace;
}
body {
  background: #000814
}

.big-block{
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(3, 1fr);
  border: 2px solid #666666;
}

[class^="block-"] {
  display: grid;
  place-content: center;
  border: 1px solid #9b5de5;
  transition: all .3s ease-in-out;
  user-select: none;
  position: relative;
}

.selected {
  background-color: #00bbf9;
  color: #000000 !important;
}

.selected > .notes {
  color: #000000 !important;
}

[class^="block-"]:not(.empty) {
  background-color: var(--main-purple);
  color: #FFFFFF;
}

.empty {
  color: #00a6ff;;
}

.header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 70px;
  padding: 14px 10px;
  background-color: var(--main-purple);
  color: #fff;
  box-sizing: border-box;
  display: grid;
  grid-template-columns: .8fr 1fr;
}

.header .options > div {
  display: grid;
  grid-auto-flow: column;
  column-gap: .5rem;
}

.header > h3 {
  margin: 0;
  place-self: center start;
  padding-left: 10px;
}

.note-selected {
  background-color: #fee440;
  color: #000 !important;
}

.notes {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 50%;
  display: grid;
  place-content: center;
  color: #666666;
  font-size: 14px;
}

.mobile-controls {
  display: none;
}

.options {
  display: grid;
  grid-auto-flow: column;
  place-items: center;
  place-self: center end;
  column-gap: 10px;
}

.options > * {
  cursor: pointer;
  transition: transform .2s ease-in-out;
  position: relative;
}

.options > *::after {
  content: '';
  position: absolute;
  height: .2rem;
  width: 100%;
  bottom: -5px;
  background-color: #ffd60a;
  transform-origin: right;
  transition: transform .2s ease-in-out;
  transform: scaleX(0)
}

.options > *:hover {
  transform: scale(1.06);
}

.options > *:hover::after {
  transform-origin: left;
  transform: scaleX(1);
}

.hidden {
  display: none !important;
}

@media only screen and (min-device-width: 300px) 
  and (max-device-width: 800px) 
  {
    .main-grid {
      height: calc(100vh - 180px);
    }
    .mobile-controls {
      display: grid;
      grid-auto-flow: column;
      margin: 10px 5px;
      padding: 5px;
      place-items: center;
      border: 1px solid #d3d3d3;
      background-color: ghostwhite;
    }
    [class^="slector-"] {
      padding-top: 5px;
      width: 100%;
      height: 100%;
      text-align: center;
      border: 1px solid #d3d3d3;
      transition: all .3s ease-in-out;
    }
    [class^="slector-"]:hover {
      background-color: var(--main-purple);
      color: #ffffff;
    }
  }

.hint-n {
  position: absolute;
  bottom: -1.5rem;
  left: calc(50% - 0.6rem);
  width: 1.2rem;
  height: 1.2rem;
  background-color: #ffd60a;
  border-radius: 50%;
  display: grid;
  place-content: center;
}
/*-------METAFLUX STORAGE-------*/
// empty sudoku array
const _empty = new Array(
  0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0
);
window.storage = new Store(
  {
    Main: {
      sudoku: _empty,
      userSolve: _empty,
      hintN: 5
    }
  },
  {
    SUDOKU_SOLUTION: (action, state) => {
      const { sudoku, positions } = action;
      state.Main.sudoku = sudoku;
      state.Main.positions = positions;
      return { newState: state }
    },
    SET_VALUE: (action, state) => {
      const { pos, value } = action;
      state.Main.userSolve[pos] = value;
      if(isSolvedSudoku(state.Main.userSolve)) alert('Wiiii you won');
      return { newState: state }
    },
    LOAD_SAVED_GAME: (action, state) => {
      const { savedGame } = action;
      state.Main = savedGame;
      return { newState: state }
    },
    USE_HINT: (action, state) => {
      state.Main.hintN = state.Main.hintN - 1;
      return { newState: state }
    }
  }
)

/*
* On Load start game or load in local storage
*/
document.addEventListener('DOMContentLoaded', () => {
  const main = document.querySelector('#main')
  main.appendChild(headerComponent)
  main.appendChild(mainContainer);
  main.appendChild(mobileControlls);
  keyboardListener();
  let savedGame = localStorage.getItem('saved_sudoku');
  if(!savedGame) {
    newSudoku();
  } else {
    savedGame = JSON.parse(savedGame);
    load(savedGame);
  }
});
/*
* UI ELEMENTS
*/
/*
* Main container
*/
const mainContainer = Div({ className: 'main-grid' })
/*
* header component
*/
const headerComponent = 
      Div({ className: 'header' },[
        H3(false, 'Metaflux example'),
        Div({ className: 'options' },[
          Div({onclick: () => { save() }},[
            Span({}, 'Save'),
            I({className: 'fas fa-save'})
          ]),
          Div({onclick: () => { hint() }},[
            Span({}, 'Hint'),
            I({className: 'fas fa-question'}),
            Div({className: 'hint-n'},5)
            .onStoreEvent('USE_HINT', (state, that) => {
              that.innerHTML = state.Main.hintN;
            })
          ]),
          Div({onclick: () => { reroll() }},[
            Span({}, 'New Game'),
            I({className: 'fas fa-sync-alt'})
          ]),
        ])]
      );
/*
 * get solved blocks
*/
function getBlock(n, i, j) {
  return Div({ className: `block-${i}-${j}`},
       Div({ className:'solution' }, n)
  );
}
/*
* get empty blocks
*/
function getEmptyBlock(i,j, n) {
  return Div({ className: `block-${i}-${j} empty`, onclick: function () {
    // on click select block
    selectLine(this);
  }, oncontextmenu: function (ev) {
    // on right click select note
    ev.preventDefault();
    selectNoteLine(this);
  }},[
    Div({ className: 'solution' }),
    Div({ className: 'notes' })
 ]);
}
/*
* mobile controlls
*/
const mobileControlls = Div({ className: 'mobile-controls' }, function () {
  const arr = [];
  for(let i=1; i < 10; i++) {
    arr.push(Div({ className: `slector-${i}`, onclick: () => {
      setValue(i)
    } }, i));
  }
  arr.push(Div({ className: 'slector-0', onclick: () => {
      setValue(0)
  }}, I({ className: 'fas fa-trash' })));
  return arr;
});


/*---------GAME LOGIC------------------------ */
/*
* IN THIS SECTION WE WILL GENERATE A NEW SUDOKU
*/
function newSudoku() {
  mainContainer.innerHTML = "";
  solve(_empty);
}
/*
* load game from localstorage
*/
function load(savedGame) {
  document.querySelector('.main-grid').innerHTML = "";
  showSudoku(savedGame.sudoku, savedGame.positions);
  loadedGame(savedGame.userSolve);
  window.storage.dispatch({ type: 'LOAD_SAVED_GAME', savedGame})
}

/**
 * load game from array
 * @param {Array} userSolve block solved by user
 */
function loadedGame(userSolve) {
  for(let i=0; i < 9; i++) {
    for(let j=0; j < 9; j++) {
      const block = document.querySelector(`.block-${i}-${j}`)
      const key = userSolve[i*9 + j];
      if (key !== 0){
        window.storage.dispatch({ type: 'SET_VALUE', pos: i*9+j, value: userSolve[i*9+j]})
        block.querySelector('.solution').innerHTML = key;
      }
      
    }
  }
}
/*
* Select a block as a note and deselect old.
*/
function selectNoteLine(block) {
  const selectedBefore = document.querySelectorAll('.selected');
  const selectedNoteBefore = document.querySelectorAll('.note-selected');
  selectedNoteBefore.forEach(old => { old.classList.remove('note-selected') });
  selectedBefore.forEach(old => { old.classList.remove('selected') });
  block.classList.add('note-selected')
}

/**
 * select a block and deselect the old one;
 * @param {HTMLElement} block 
 */
function selectLine(block) {
  const selectedBefore = document.querySelectorAll('.selected');
  const selectedNoteBefore = document.querySelectorAll('.note-selected');
  selectedNoteBefore.forEach(old => { old.classList.remove('note-selected') });
  selectedBefore.forEach(old => { old.classList.remove('selected') });
  block.classList.add('selected')
}
/**
 * Listen to the keyboard, if a number is press and some block is selected dispatch value
 */
function keyboardListener() {
  document.addEventListener('keydown', (ev) => {
    const key = parseInt(ev.key);
    if(!isNaN(key)) {
      setValue(key);
    }
  })
}
/**
 * Set value to the selected or note-selecte block
 * @param {Number} key Integer from 0 to 9
 */
function setValue(key) {
  const selected = document.querySelector('.selected');
  const noteSelected = document.querySelector('.note-selected');
  if (selected !== null) {
    window.storage.dispatch({
      type: 'SET_VALUE',
      pos: getLinearPosition(selected),
      value: key
    });
    selected.querySelector('.solution').innerHTML = key !== 0 ? key : "";
    selected.querySelector('.notes').innerHTML = '';
  } else if(noteSelected !== null) {
    window.storage.dispatch({
      type: 'SET_VALUE',
      pos: getLinearPosition(noteSelected),
      value: 0
    });
    noteSelected.querySelector('.solution').innerHTML = '';
    noteSelected.querySelector('.notes').innerHTML += `|${key}|`;
    if(key === 0) noteSelected.querySelector('.notes').innerHTML = '';
  }
}
/**
 * Given a position y and x convert it to a linear postion 
 * (y*9) + x
 * @param {HTMLElement} block 
 */
function getLinearPosition(block) {
  let fc = block.classList[0];
  let pos = fc.slice(6);
  pos = pos.split('-');
  return parseInt(pos[0]) * 9 + parseInt(pos[1]);
}


function save() {
  const main = window.storage.getState().Main;
  localStorage.setItem('saved_sudoku', JSON.stringify(main));
}
/*
* display hint in selected block
*/
function hint() {
  const { hintN, sudoku } = window.storage.getState().Main;
  const selected = document.querySelector('.selected');
  if(selected !== null) {
    if (hintN !== 0) {
      const linear = getLinearPosition(selected);
      selected.querySelector('.solution').innerHTML = sudoku[linear];
      window.storage.dispatch({ type: 'SET_VALUE', pos: linear, value: sudoku[linear] });
      window.storage.dispatch({ type: 'USE_HINT' });
    }
  } else {
    alert(`You have to select a block for a hint, you have: ${hintN} hints left`);
  }
}
/**
 * Reroll, the game
 */
function reroll() {
  newSudoku();
}

// given a sudoku, solves it
function solve(sudoku) {
  var saved = new Array();
  var savedSudoku = new Array();
  var i=0;
  var nextMove;
  var whatToTry;
  var attempt;
  while (!isSolvedSudoku(sudoku)) {
    i++;
    nextMove = scanSudokuForUnique(sudoku);
    if (nextMove == false) {
      nextMove = saved.pop();
      sudoku = savedSudoku.pop();
    }
    whatToTry = nextRandom(nextMove);
    attempt = determineRandomPossibleValue(nextMove,whatToTry);
    if (nextMove[whatToTry].length>1) {
      nextMove[whatToTry] = removeAttempt(nextMove[whatToTry],attempt);
      saved.push(nextMove.slice());
      savedSudoku.push(sudoku.slice());
    }
    sudoku[whatToTry] = attempt;
  }
  const pos = getRandomPositions();
  showSudoku(sudoku,pos);
}


/*
* Display sudoku
*/
function showSudoku(sudoku, pos) {
  let temp = Div();
  window.storage.dispatch({ type: 'SUDOKU_SOLUTION', sudoku, positions: pos });
  for(let x=0; x < 9; x++) {
    for(let y=0; y < 9; y++) {
      if(pos.match(new RegExp(`\/${x}-${y}\/`)) !== null) {
        temp.appendChild(getBlock(sudoku[x*9+y], x, y));
        window.storage.dispatch({
          type: 'SET_VALUE', pos: x*9+y, value: sudoku[x*9+y]
        });
      } else {
        temp.appendChild(getEmptyBlock(x,y));
      }
    }
  }
  // create the Big blocks(3 x 3)
  for(let i=0; i < 9; i = i + 3) {
    for(let j=0; j < 9; j = j + 3 ) {
      const BB =  rec(j, i, temp);
      mainContainer.appendChild(BB);
    }
  }
  console.log('Finish');
}
/*
* Create 3 X 3 rectangles 
*/
function rec(n, x, el) {
  const BB = Div({className: 'big-block'});
  for(let i=x; i < x + 3; i++) {
    for(let j=n; j < n +3; j++) {
      BB.appendChild(el.querySelector(`.block-${i}-${j}`))
    }
  }
  return BB;
}

/**
 * create 30 random positions non reaping to display initial values
 */
function getRandomPositions() {
  let longStr = '';
  for(let i = 0; i < 30; i++) {
    const x = getRandomInt(); const y = getRandomInt();
    if(longStr.match(new RegExp(`\/${y}-${x}\/`)) !== null) {
      i = i - 1;
    } else {
      longStr += `/${y}-${x}/`
    }
  }
  return longStr;
}

// given a sudoku cell, returns the row
function returnRow(cell) {
  return Math.floor(cell / 9);
}

// given a sudoku cell, returns the column
function returnCol(cell) {
  return cell % 9;
}

// given a sudoku cell, returns the 3x3 block
function returnBlock(cell) {
  return Math.floor(returnRow(cell) / 3) * 3 + Math.floor(returnCol(cell) / 3);
}

// given a number, a row and a sudoku, returns true if the number can be placed in the row
function isPossibleRow(number,row,sudoku) {
  for (var i=0; i<=8; i++) {
    if (sudoku[row*9+i] == number) {
      return false;
    }
  }
  return true;
}

// given a number, a column and a sudoku, returns true if the number can be placed in the column
function isPossibleCol(number,col,sudoku) {
  for (var i=0; i<=8; i++) {
    if (sudoku[col+9*i] == number) {
      return false;
    }
  }
  return true;
}

// given a number, a 3x3 block and a sudoku, returns true if the number can be placed in the block
function isPossibleBlock(number,block,sudoku) {
  for (var i=0; i<=8; i++) {
    if (sudoku[Math.floor(block/3)*27+i%3+9*Math.floor(i/3)+3*(block%3)] == number) {
      return false;
    }
  }
  return true;
}

// given a cell, a number and a sudoku, returns true if the number can be placed in the cell
function isPossibleNumber(cell,number,sudoku) {
  var row = returnRow(cell);
  var col = returnCol(cell);
  var block = returnBlock(cell);
  return isPossibleRow(number,row,sudoku) && isPossibleCol(number,col,sudoku) && isPossibleBlock(number,block,sudoku);
}

// given a row and a sudoku, returns true if it's a legal row
function isCorrectRow(row,sudoku) {
  var rightSequence = new Array(1,2,3,4,5,6,7,8,9);
  var rowTemp= new Array();
  for (var i=0; i<=8; i++) {
    rowTemp[i] = sudoku[row*9+i];
  }
  rowTemp.sort();
  return rowTemp.join() == rightSequence.join();
}

// given a column and a sudoku, returns true if it's a legal column
function isCorrectCol(col,sudoku) {
  var rightSequence = new Array(1,2,3,4,5,6,7,8,9);
  var colTemp= new Array();
  for (var i=0; i<=8; i++) {
    colTemp[i] = sudoku[col+i*9];
  }
  colTemp.sort();
  return colTemp.join() == rightSequence.join();
}

// given a 3x3 block and a sudoku, returns true if it's a legal block 
function isCorrectBlock(block,sudoku) {
  var rightSequence = new Array(1,2,3,4,5,6,7,8,9);
  var blockTemp= new Array();
  for (var i=0; i<=8; i++) {
    blockTemp[i] = sudoku[Math.floor(block/3)*27+i%3+9*Math.floor(i/3)+3*(block%3)];
  }
  blockTemp.sort();
  return blockTemp.join() == rightSequence.join();
}

// given a sudoku, returns true if the sudoku is solved
function isSolvedSudoku(sudoku) {
  for (var i=0; i<=8; i++) {
    if (!isCorrectBlock(i,sudoku) || !isCorrectRow(i,sudoku) || !isCorrectCol(i,sudoku)) {
      return false;
    }
  }
  return true;
}

// given a cell and a sudoku, returns an array with all possible values we can write in the cell
function determinePossibleValues(cell,sudoku) {
  var possible = new Array();
  for (var i=1; i<=9; i++) {
    if (isPossibleNumber(cell,i,sudoku)) {
      possible.unshift(i);
    }
  }
  return possible;
}

// given an array of possible values assignable to a cell, returns a random value picked from the array
function determineRandomPossibleValue(possible,cell) {
  var randomPicked = Math.floor(Math.random() * possible[cell].length);
  return possible[cell][randomPicked];
}

// given a sudoku, returns a two dimension array with all possible values 
function scanSudokuForUnique(sudoku) {
  var possible = new Array();
  for (var i=0; i<=80; i++) {
    if (sudoku[i] == 0) {
      possible[i] = new Array();
      possible[i] = determinePossibleValues(i,sudoku);
      if (possible[i].length==0) {
        return false;
      }
    }
  }
  return possible;
}

// given an array and a number, removes the number from the array
function removeAttempt(attemptArray,number) {
  var newArray = new Array();
  for (var i=0; i<attemptArray.length; i++) {
    if (attemptArray[i] != number) {
      newArray.unshift(attemptArray[i]);
    }
  }
  return newArray;
}

// given a two dimension array of possible values, returns the index of a cell where there are the less possible numbers to choose from
function nextRandom(possible) {
  var max = 9;
  var minChoices = 0;
  for (var i=0; i<=80; i++) {
    if (possible[i]!=undefined) {
      if ((possible[i].length<=max) && (possible[i].length>0)) {
        max = possible[i].length;
        minChoices = i;
      }
    }
  }
  return minChoices;
}

/*------------UTILS-----------------*/
function getRandomInt() {
  return Math.floor(Math.random() * (8 - 0)) + 0;
}

function I(props, content = '') {
   return HTMLElementCreator('i',Object.assign({}, props, { content }));
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.