<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.