<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 }));
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.