Cannot implement shuffle bars feature in sorting algorithm visualizer - setTimeout updates are not rendering - javascript

I'm making a sorting algorithm visualizer project and I'm in the process of implementing a shuffle button. The visualization involves bars of different heights getting moved around. I would like the shuffle button to modify the bar array one step at a time, showcasing each swap at a high speed. I've tried tweaking many different things (some of which do move bars around in a strange manner), but I can't seem to get the desired functionality to work. Here's some of the relevant code:
// Swap does not modify the original array, but the implementation details don't really affect the result.
// It basically swaps two elements in the bar array, and then returns the updated array.
const swapBars = (bars, bar1, bar2) => {
if (!bars || !bar1 || !bar2) {
return;
}
const _bars = bars;
let _bar1 = bar1;
let _bar2 = bar2;
const tempLeft = _bar1.left;
_bar1.left = _bar2.left;
_bar2.left = tempLeft;
const temp = _bar1;
_bar1 = _bar2;
_bar2 = temp;
return _bars;
};
// Init bars is basically synchronous shuffle. It takes the array that is created and shuffles it
// because the array should begin in a shuffled state. This is working properly.
const initBars = (bars) => {
let currentIndex = bars.length - 1;
while (currentIndex > 0) {
// randomIndex will always be different from currentIndex, so each bar will always shuffle
const randomIndex = Math.floor(Math.random() * currentIndex);
swapBars(bars, bars[currentIndex], bars[randomIndex]);
currentIndex--;
}
setBarsToRender(bars);
};
// createBarArray is what is used to actually populate an empty array with bars depending on a number passed
// through by a slider. This is also working properly.
const createBarArray = (quantity) => {
let bars = [];
const width = calcWidthPercentage(quantity);
for (let i = 0; i < quantity; i++) {
const height = calcHeightPercentage(quantity, i + 1);
const left = calcLeftPosPercentage(quantity, i + 1);
bars.push({ correctPos: i, height: height, width: width, left: left });
}
return initBars(bars);
};
// shuffleBars seems to be broken. I've tried many different things, and this is just the latest snapshot of it.
// It is being called when the shuffle button is being clicked using `shuffleBars(barsToRender)` where barsToRender is the stateful value that is being rendered.
const shuffleBars = (bars) => {
let currentIndex = bars.length - 1;
while (currentIndex > 0) {
const randomIndex = Math.floor(Math.random() * currentIndex);
setTimeout(() => {
setBarsToRender((prev) => {
return swapBars(prev, prev[currentIndex], prev[randomIndex]);
});
}, 50 * (bars.length - currentIndex));
currentIndex--;
}
};
If I do something like moving the swapBars call inside setBarsToRender outside of it and then
do setBarsToRender[...bars], I can see some of the bars moving, but not with the intended behavior (the smallest bar is the only one that keeps swapping). I'm not sure if I'm misunderstanding how state updates work inside setTimeout, or if it's something else, so I'd greatly appreciate some help.

I removed the setTimeout and used a transition delay to create the staggered effect.
Working demo below:
const swapBars = (bars, bar1, bar2) => {
if (!bars || !bar1 || !bar2) {
return;
}
const _bars = bars;
let _bar1 = bar1;
let _bar2 = bar2;
const tempLeft = _bar1.left;
_bar1.left = _bar2.left;
_bar2.left = tempLeft;
const temp = _bar1;
_bar1 = _bar2;
_bar2 = temp;
return _bars;
};
const initBars = (bars) => {
let currentIndex = bars.length - 1;
while (currentIndex > 0) {
const randomIndex = Math.floor(Math.random() * currentIndex);
swapBars(bars, bars[currentIndex], bars[randomIndex]);
currentIndex--;
}
return bars;
};
const createBarArray = (quantity) => {
let bars = [];
const width = 100 / quantity;
for (let i = 0; i < quantity; i++) {
const height = width * (i + 1);
const left = width * i;
bars.push({ correctPos: i, height: height, width: width, left: left });
}
return initBars(bars);
};
function Bars({ quantity = 10 }) {
const [barsToRender, setBarsToRender] = React.useState([]);
React.useEffect(() => {
const bars = createBarArray(quantity);
setBarsToRender(bars);
}, [quantity]);
const shuffleBars = () => {
const bars = [...barsToRender];
setBarsToRender(initBars(bars));
};
return (
<div>
<ul
style={{
height: "50vh",
display: "flex",
position: "relative"
}}
>
{barsToRender.map((bar, i) => (
<Bar key={bar.correctPos} bar={bar} index={i} />
))}
</ul>
<button onClick={shuffleBars}>Shuffle</button>
</div>
);
}
function Bar({ bar, index: i }) {
return (
<li
style={{
background: "blue",
height: `${bar.height}%`,
width: `${bar.width}%`,
left: `${bar.left}%`,
position: "absolute",
bottom: 0,
transitionProperty: "left",
transitionTimingFunction: "ease-in-out",
transitionDuration: ".25s",
transitionDelay: `${i*50}ms`
}}
>
<p>{bar.correctPos}</p>
</li>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<Bars />)
p {
font-family: sans-serif;
font-weight: 700;
height: 1.5rem;
width: 1.5rem;
display: grid;
place-content: center;
background: white;
border-radius: 50%;
border: 1px solid;
}
ul {
padding: 0;
list-style-type: none;
}
li {
display: grid;
align-content: end;
justify-items: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
<div id="root"></div>

Related

How to style a singular v-for item in vue, based on a condition?

I am making a multiple choice quiz game and want what the user clicked to change to either red or green depending on if that answer is correct or incorrect. I made a variable called selected which is what the user pressed- this does correctly update. I have also got all of the v-for items to change to the same colour depending on if the answer is correct or not, I only need help separating it so that only one of the v-for things change colour.
Here is my relative HTML:
<button type="button" class="btn" class="answer" v-for="option in options" #click="checkAnswer(option)" #click="selected = option" :style="{backgroundColor: colour}">
{{option}}<br/>
</button>
<button type="button" #click="getQ" #click="shuffle(options)" class="btn button next">Next</button>
Here is the relative JS:
let colour = Vue.ref('');
let selected = Vue.ref('');
let options = Vue.ref([correctAnswer, incorrectAnswerOne, incorrectAnswerTwo, incorrectAnswerThree]);
// Methods
let shuffle = function(options) {
let num = options.length, t, raInt;
//while there are remaining elements to shuffle
while (num) {
//choose random
raInt = Math.floor(Math.random() * num--);
//swap with current element
t = options[num];
options[num] = options[raInt];
options[raInt] = t;
}
return options;
};
let checkAnswer = function(clicked) {
console.log(clicked.value);
console.log(correctAnswer.value);
if (clicked.value == correctAnswer.value) { // checks if the button that was clicked is the same as the answers value
this.result = "Correct!"; //if it does, result changes to Correct!
this.colour = "green";
} else {
this.result = "Incorrect!"; //if the answer is incorrect, result changes to Incorrect!
this.colour = "red";
};
};
And here is some CSS:
.answer {
width: 100%;
background-color: #dbdbdb;
padding: 4% 2%;
margin: 1 0;
}
.answer:hover {
background-color: #c2c2c2
}
I haven’t really tried that much. I’m not sure what to try. In a different project I did style a different div based on what other div was selected, but I am not sure how to change just one part of a v-for, or if it is even possible.
Thanks in advance
You can set condition for showing color style:
const { ref, computed } = Vue
const app = Vue.createApp({
setup() {
let correctAnswer = 3
let incorrectAnswerOne = 1
let incorrectAnswerTwo = 2
let incorrectAnswerThree = 4
let colour = ref('');
let selected = ref('');
let options = ref([correctAnswer, incorrectAnswerOne, incorrectAnswerTwo, incorrectAnswerThree]);
let shuffled = ref([])
let shuffle = (array) => {
selected.value = null
let currentIndex = array.length, randomIndex;
while (currentIndex != 0) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
shuffled.value = array;
};
shuffle(options.value)
let checkAnswer = function(clicked) {
// 👇 set selected
selected.value = clicked
if (clicked == correctAnswer) {
this.result = "Correct!";
this.colour = "green";
} else {
this.result = "Incorrect!";
this.colour = "red";
};
};
return { colour, selected, options, shuffle, checkAnswer, shuffled }
},
})
app.mount('#demo')
.answer {
width: 100%;
background-color: #dbdbdb;
padding: .5em 2em;
margin: 1 0;
}
.answer:hover {
background-color: #c2c2c2
}
.btns {
display: flex;
}
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<div class="btns">
<div v-for="(option, i) in shuffled" :key="i" >
<!-- 👇 condition -->
<button class="answer" #click="checkAnswer(option)" :style="selected == option && {backgroundColor: colour}">
{{option}}<br/>
</button>
</div>
</div>
<button type="button" #click="getQ, shuffle(options)" class="btn button next">Next</button>
</div>

How could I restart this current function

During the use of this function you click a button that will give you a square that will display a colour within a square. However when you click the button 60 times the programme stops working because all the colours have been used. How would I then restart this process so that I can continue clicking ?
<html>
<head>
<script>
var usedColors = [];
function randomColour(){
var colour=[];
colour[0]= '#edf2fb';
colour[1]= '#d7e3fc';
colour[3]= '#c1d3fe';
colour[4]= '#d1d1d1';
colour[5]= '#e1dbd6';
colour[6]= '#e2e2e2';
colour[7]= '#f9f6f2';
colour[8]='#ffc09f';
colour[9]='#ffee93';
colour[10]='#fcf5c7';
colour[11]='#a0ced9';
colour[12]='#adf7b6';
colour[13]='#809bce';
colour[14]='#95b8d1';
colour[15]='#b8e0d2';
colour[16]='#d6eadf';
colour[17]='#eac4d5';
colour[18]='#e8d1c5';
colour[19]='#eddcd2';
colour[20]='#fff1e6';
colour[21]='#f0efeb';
colour[22]='#eeddd3';
colour[23]='#e8dff5';
colour[24]='#fce1e4';
colour[25]='#fcf4dd';
colour[26]='#ddedea';
colour[27]='#daeaf6';
colour[28]='#d3ab9e';
colour[29]='#eac9c1';
colour[30]='#ebd8d0';
colour[31]='#ffe5ec';
colour[32]='#ffc2d1';
colour[33]='#ceb5b7';
colour[35]='#b5d6d6';
colour[36]='#f2f5ff';
colour[37]='#efcfe3';
colour[38]='#eaf2d7';
colour[39]='#b3dee2';
colour[40]='#f8ad9d';
colour[41]='#fbc4ab';
colour[42]='#ffdab9';
colour[43]='#cdb4db';
colour[44]='#ffc8dd';
colour[45]='#ffafcc';
colour[46]='#bde0fe';
colour[47]='#a2d2ff';
colour[48]='#fdffb6';
colour[49]='#caffbf';
colour[50]='#9bf6ff';
colour[51]='#a0c4ff';
colour[52]='#ffc6ff';
colour[53]='#a7bed3';
colour[54]='#c6e2e9';
colour[55]='#f1ffc4';
colour[56]='#ffcaaf';
colour[57]='#dab894';
colour[58]='#fec7bc';
colour[59]='#fcf5ee';
var pick= Math.floor(Math.random()*60);
if(usedColors.includes(pick)){
randomColour();
}
usedColors.push(pick); document.getElementById("colorpad").style.backgroundColor = colour[pick];
console.log(usedColors);
}
</script>
</head>
<body>
<div id="colorpad" style="height: 300px; width: 300px;">
<button onclick="randomColour()">btn</button>
</div>
</body>
</html>
Keep track of the current color:
let curr = 0;
On "Next" button click increment the curr index, and loopback with the help of the Modulo Operator %:
curr += 1;
curr %= colour.length; // On "next" loop back to 0 if we reached the end
Finally there's your color back at 0
console.log(colour[curr]); // '#edf2fb'
Demonstration:
const EL = (sel, EL) => (EL||document).querySelector(sel);
const colors = [
'#edf2fb',
'#d7e3fc',
'#c1d3fe',
'#d1d1d1',
'#e1dbd6',
'#e2e2e2',
'#f9f6f2',
'#ffc09f',
'#ffee93',
'#fcf5c7',
];
const tot = colors.length;
let curr = 0;
const curr_rand = () => curr = Math.floor(Math.random() * tot);
const curr_next = () => (curr += 1, curr %= tot);
const applyColor = () => EL("body").style.background = colors[curr];
EL("#rand").addEventListener("click", () => {
curr_rand();
applyColor();
console.log(curr);
});
EL("#next").addEventListener("click", () => {
curr_next();
applyColor();
console.log(curr);
});
<button type="button" id="rand">Generate</button>
<button type="button" id="next">Next</button>
Here's a snippet using a small factory function to be able to recolor infinitely. The recoloring is done using a randomly shuffled copy of the color array. This way you don't have to check each time if a color is already used.
Furthermore, see comments in the snippet. It uses event delegation for the handler, because it's generally not a good idea to use inline event handlers.
To keep the snippet lean, only 10 colors are used.
const colorize = randomColor(
['#edf2fb', '#d7e3fc', '#c1d3fe', '#d1d1d1', '#e1dbd6',
'#e2e2e2', '#f9f6f2', '#ffc09f', '#ffee93', '#fcf5c7'] );
document.addEventListener(`click`, evt => {
if (evt.target.id === `colorChange`) {
return colorize(document.querySelector(`.color`));
}
});
// randomColor is a factory function, it returns a function.
// The function can use the inner variables as well as the
// [colors] array from the parameter(they are 'closed over').
// The [colors] array is shuffled randomly (Fisher-Yates) and
// on every call (from click) the first color from that shuffled
// array is picked - until it's empty. If [shuffled] is empty
// the original [color] array is reshuffled (into [shuffled]).
// This way the coloring is restarted with a new set of
// random colors.
function randomColor(colors){
const shuffle = array =>
[...Array(array.length)]
.map((el, i) => Math.floor(Math.random() * i))
.reduce( (a, rv, i) =>
([a[i], a[rv]] = [a[rv], a[i]]) && a, array.slice());
// ^ slice copies the original
let shuffled = shuffle(colors);
// return a function
return colorDiv => {
const restarted = !shuffled.length;
shuffled = shuffled.length > 0 ? shuffled : shuffle(colors);
const nwColor = shuffled.shift();
colorDiv.style.backgroundColor = nwColor;
// for demo: color is displayed, and bold/red if [shuffled] is renewed
colorDiv.classList[restarted ? `add` : `remove`](`restart`);
colorDiv.dataset.color = nwColor;
}
}
.color {
width: 100px;
height: 100px;
border: 1px solid #777;
margin: 1rem 0;
text-align: center;
line-height: 100px;
}
.color:before {
content: attr(data-color);
}
.color.restart:before {
color: red;
font-weight: bold;
}
<div class="color"></div>
<button id="colorChange">change</button>

JavaScript Minesweeper - Opening whole mine-free area at once not working properly (flood-fill)

I am trying to build a simple minesweeper game in Javascript. It works propererly apart from the function to open the entire mine-free area when clicking on a mine-free tile. It starts checking the neighbouring tiles, but stops when the first neighbouring tile has a mine.
As you can see on the screenshot below (after clicking on tile 1/5) only the tiles until the first "1" are opened. It should actually open a much larger area:
It seems I am pretty close. THis is my code:
const gridSize = 10
// generate grid
const board = document.querySelector("#minesweeper");
// loop over num for rows
let header = 0;
for(let i = 0; i < gridSize+1; i++) {
const row = document.createElement("tr");
// loop over num for cols
for (let j = 0; j < gridSize+1; j++) {
// add col to row
if ( i === 0 ) {
row.insertAdjacentHTML("beforeend", `<th>${header}</th>`);
header += 1;
} else if (j === 0) {
row.insertAdjacentHTML("beforeend", `<th>${header-10}</th>`);
header += 1;
} else {
row.insertAdjacentHTML("beforeend", `<td class='unopened' dataset-column=${j}></td>`);
};
};
// add row to board
board.append(row);
};
// functions -------------------
function getNeighbour(tile, i, j) {
const column = tile.cellIndex; // so the columns get the cellIndex
const row = tile.parentElement.rowIndex; // row gets the rowIndex(tr)
const offsetY = row + i;
const offsetX = column + j;
return document.querySelector(`[data-row="${offsetY}"][data-column="${offsetX}"]`);
}
// count mines of neighbours
function countMines(tile) {
let mines = 0;
for(i = -1; i <= 1; i++) {
for(j = -1; j <= 1; j++ ) {
// check if neighbour has mine
// get cell values from neighbour in DOM
nb = getNeighbour(tile, i, j);
if (nb && nb.classList.contains('has-mine') || (nb && nb.classList.contains('mine'))) mines += 1; // if nb exists and has a mine increase mines
}
}
// write into DOM
if (mines === 0) {
tile.classList.add(`opened`);
} else {
tile.classList.add(`neighbour-${mines}`);
}
tile.classList.remove(`unopened`);
// if mines are 0, go to neigbours and count mines there
// console.log(tile.classList);
if (mines === 0) {
// alert("mines are zero");
for (i = -1; i <= 1; i+=1) {
for (j = -1; j <= 1; j+=1) {
nb = getNeighbour(tile, i, j);
if (nb && nb.classList.contains("unopened")) {
countMines(nb);
}
}
}
}
return mines;
}
// function open tile on click
function openTile(event) {
const tile = event.currentTarget;
// if there is a mine you lose
if (tile.classList.contains("has-mine")) {
document.querySelectorAll(".has-mine").forEach((cell) => {
cell.classList.remove("has-mine", "unopened");
cell.classList.add("mine", "opened");
});
alert("booooooooom!");
} else {
countMines(tile);
}
}
const tiles = document.querySelectorAll("td");
tiles.forEach((td) => {
td.dataset.column = td.cellIndex; // so the columns get the cellIndex
td.dataset.row = td.parentElement.rowIndex; // row gets the rowIndex(tr)
// add mines randomly
const freq = 0.1;
if (Math.random() < freq) {
td.classList.add("has-mine");
}
// eventlisteners per tile
td.addEventListener("click", openTile);
});
I have been thinking hours about it but could not find a way to work on with this code. Not sure if I am close or if I would need to modify the whole approach?
Many thanks for any ideas!
the principle is simple, for each empty cell, you must add all the adjacent empty cells.
it is also necessary to collect the number of adjacent mines of each cell
a) list the 8 adjacent cells, except for the cells placed at the edge
this is the prxElm() function in my code
b) count the mines present around a cell -> prxMne()
starting from the first cell
1- we count (a) nearby mines
2- it becomes the first element of a stack of cells to be mapped
3- if its number of nearby mines is zero, repeat this operation for all adjacent cells
the particularity of this algorithm is to use only one stack to accumulate the coordinates to be mapped.
it places the elements with adjacent mines at the top of the stack, and those with none at the end of the stack.
as there can be several cells without adjacent mines, we keep an indexiExp of the last empty cell processed.
of course when you add a cell with mines nearby at the start of the stack, this index is shifted.
the algorithm also take care not to add duplicate the same cell by checking before if this cell is not in the stack.
see .filter(x=>!explor.some(e=>e.p===x.p))
this ends when the exploration index iExp reaches the end of the stack.
here is the whole code, it is not completely finalized, but the essentials are there.
const
MinesCount = 17 // adjusted values to fit this snippet display area
, gridSz = { r:7, c:20 } // grid rows x cols
, gridMx = gridSz.r * gridSz.c
, proxim = [ {v:-1,h:-1}, {v:-1,h:0}, {v:-1,h:+1}, {v:0,h:-1}, {v:0,h:+1}, {v:+1,h:-1}, {v:+1,h:0}, {v:+1,h:+1} ]
, prxElm = (r,c) => proxim.reduce((a,{v,h})=>
{
let rv = r+v, ch = c+h;
if (rv>=0 && ch>=0 && rv<gridSz.r && ch<gridSz.c) a.push({p:((rv * gridSz.c) + ch), r:rv, c:ch} )
return a
},[])
, GenNbX = (nb,vMax) => [null].reduce(arr=>
{
while (arr.length < nb)
{
let numGen = Math.floor(Math.random() * vMax)
if (!arr.includes(numGen)) arr.push(numGen);
}
return arr //.sort((a,b)=>a-b)
},[])
, minesP = GenNbX( MinesCount, gridMx )
, prxMne = (r,c) => prxElm(r,c).reduce((a,{p})=>minesP.includes(p)?++a:a,0) // count mines arroub=nd
, td2rcp = td =>
{
let r = td.closest('tr').rowIndex -1 // -1 for thead count of rows
, c = td.cellIndex
, p = (r * gridSz.c) +c
return {r,c,p}
}
, p2rc = p =>({r: Math.floor(p / gridSz.c), c: (p % gridSz.c)})
, { timE, cFlags, minesArea } = drawTable('mines-area', gridSz, MinesCount )
;
const chrono = (function( timeElm )
{
const
one_Sec = 1000
, one_Min = one_Sec * 60
, twoDgts = t => (t<10) ? `0${t}` : t.toString(10)
, chronos =
{ timZero : null
, timDisp : timeElm
, timIntv : null
, running : false
}
, obj =
{ start()
{
if (chronos.running) return
chronos.timDisp.textContent = '00:00'
chronos.running = true
chronos.timZero = new Date().getTime()
chronos.timIntv = setInterval(() =>
{
let tim = (new Date().getTime()) - chronos.timZero
chronos.timDisp.textContent = `${Math.floor(tim/one_Min)}:${twoDgts(Math.floor((tim % one_Min)/one_Sec))}`
}
, 250);
}
, stop()
{
if (!chronos.running) return
chronos.running = false
clearInterval( chronos.timIntv )
}
}
return obj
}(timE))
function drawTable(tName, gSz, mines )
{
let table = document.getElementById(tName)
// table.innerHTML = '' // eraze table
let tHead = table.createTHead()
, tBody = table.createTBody()
, xRow = tHead.insertRow()
, timE = xRow.insertCell()
, cFlags = xRow.insertCell()
;
timE.setAttribute('colspan', gSz.c -4)
timE.className = 'time'
timE.textContent = '0:00'
cFlags.setAttribute('colspan', 4)
cFlags.className = 'flag'
cFlags.textContent = ' 0/' + mines
for (let r=gSz.r;r--;)
{
xRow = tBody.insertRow()
for(let c = gSz.c;c--;) xRow.insertCell()
}
return { timE, cFlags, minesArea: tBody }
}
minesArea.onclick = ({target}) =>
{
if (!target.matches('td')) return
if (target.hasAttribute('class')) return // already done
chrono.start()
let {r,c,p} = td2rcp(target)
if (minesP.includes(p)) // you are dead!
{
chrono.stop()
minesArea.className = 'Boom'
minesP.forEach(p=> // show mines
{
let {r,c} = p2rc(p)
let td = minesArea.rows[r].cells[c]
if (!td.hasAttribute('class')) td.className = 'mineOff'
})
minesArea.rows[r].cells[c].className = 'mineBoom' // this one is for you
minesArea.querySelectorAll('td:not([class]), td.flag') // jusr disable click
.forEach(td=>td.classList.add('off')) // and cursor
}
else
{
let explor = [ {p, r, c, m:prxMne(r,c) } ]
, iExp = 0
;
while (iExp < explor.length && explor[iExp].m === 0) // Open mine-free area
{
prxElm(explor[iExp].r,explor[iExp].c) // look around
.filter(x=>!explor.some(e=>e.p===x.p)) // if not already in
.forEach(x=>
{
M = prxMne(x.r,x.c)
if (M>0 ) { explor.unshift( { p:x.p, r:x.r, c:x.c, m:M} ); iExp++ }
else explor.push( { p:x.p, r:x.r, c:x.c, m:M} ) // mine-free space
})
iExp++
}
explor.forEach(({r,c,m})=>minesArea.rows[r].cells[c].className = 'm'+m )
}
if (checkEnd()) // some kind of victory!?
{
chrono.stop()
minesArea.querySelectorAll('td.flag').forEach(td=>td.classList.add('off'))
minesArea.className = 'win'
}
}
minesArea.oncontextmenu = e => // Yes, there is a right click for flag mines
{
if (!e.target.matches('td')) return
e.preventDefault()
let {r,c,p} = td2rcp( e.target)
, cell_rc = minesArea.rows[r].cells[c]
;
if (!cell_rc.hasAttribute('class')) cell_rc.className = 'flag'
else if (cell_rc.className === 'flag') cell_rc.removeAttribute('class')
let nbFlags = minesArea.querySelectorAll('td.flag').length
cFlags.textContent = ` ${nbFlags} / ${MinesCount}`
}
function checkEnd()
{ // what about us ?
let count = 0
, reject = 0
, tdNotSeen = minesArea.querySelectorAll('td:not([class])')
, flagPos = minesArea.querySelectorAll('td.flag')
;
cFlags.textContent = ` ${flagPos.length} / ${MinesCount}`
if (tdNotSeen.length > MinesCount ) return false
flagPos.forEach(td=>
{
let {r,c,p} = td2rcp(td)
if (minesP.includes(p)) count++ // correct place
else reject++
})
tdNotSeen.forEach(td=>
{
let {r,c,p} = td2rcp(td)
if (minesP.includes(p)) count++
else reject++ // no mines there
})
if (count != MinesCount || reject != 0 ) return false
tdNotSeen.forEach(td=>
{
let {r,c,p} = td2rcp(td)
minesArea.rows[r].cells[c].className = 'mineOff'
})
cFlags.textContent = ` ${MinesCount} / ${MinesCount}`
return true
}
body { background-color: #383947; } /* dark mode ? ;-) */
table {
border-collapse : collapse;
margin : 1em auto;
--szRC : 18px;
font-family : Arial, Helvetica, sans-serif;
}
table td {
border : 1px solid #1a1a1a80;
text-align : center;
}
table thead {
font-size : .8em;
background-color : #c3c5db;
}
table tbody {
background-color : #a39999;
cursor : cell;
}
table tbody td {
width : var(--szRC);
height : var(--szRC);
overflow : hidden;
}
.m0, .m1, .m2, .m3, .m4, .m5, .m6, .m7, .m8 { background-color: whitesmoke; font-size: 12px; font-weight: bold; cursor: default; }
.m1::after { content: '1'; color: #0000ff; }
.m2::after { content: '2'; color: #008000; }
.m3::after { content: '3'; color: #ff0000; }
.m4::after { content: '4'; color: #000080; }
.m5::after { content: '5'; color: #800000; }
.m6::after { content: '6'; color: #008080; }
.m7::after { content: '7'; color: #000000; }
.m8::after { content: '8'; color: #808080; }
.off { cursor: default; }
.Boom { background-color: yellow; cursor: default; }
.mineOff { cursor: default; padding: 0; }
.flag { background-color: lightgray; padding: 0; }
.mineBoom { color: crimson; padding: 0; }
.mineOff::after,
.mineBoom::after { content: '\2738'; }
.flag::before { content: '\2691'; color: crimson; }
.time::before { content: 'Time elapsed : '; color: darkblue; }
.win td { border-color: gold;}
<table id="mines-area"></table>
I don't think a recursive method is suitable for this kind of problem.
It requires having a complex strategy for exploring empty spaces.
For example, spiraling around the starting point.
But this strategy comes up against the problem of the island hindering this progress, and which requires, once crossed, to carry out a new spiral advance to recover the points hidden during the previous spiral, but having to avoid the points already explored during the previous spiral.
You can also move forward by sectors from an initial point, but you will still encounter the same problem of the islet and its backtracking, which also multiply here with each roughness on the explored shores.
This requires complex calculations that are difficult to master and debug, not to mention the fact that recursive methods intensively use call stacks which it adds up for each new branch.
(Without neglecting the risk of going crazy, by striving to develop its recursive algorithms.)
The larger the grid and the more its recursive lists will conflict with each other, and more the computationnal time will be affected.
Of course there are already winning heuristics on this type of strategy, they are of a very high level, and we are here just on a minesweeper game where hundreds of lines of code have nothing to do.
My method only uses a single stack, its algorithm is easy to understand and requires a few lines of code.
What more ?

Set specific gradient from percentages

I have a code that generates color from values ( from 0 to 200% )
Actually the specter is Green-yellow-red.
I would like to change the gradient to Green-Grey-Red : green if percentage < 100% , grey if in 100% field, red if > 100%.
I don't see how to do it using mathematics formula.
window.onload = function() {
let colorShower = document.querySelector('.color-shower')
let colorSlider = document.querySelector('#colorSlider')
let percentage = (200 -colorSlider.value) /2
let buildColor = (capHue) => `hsl(${capHue},90%,60%)`
colorShower.style.backgroundColor = buildColor(percentage)
colorSlider.addEventListener('input', e => {
percentage = (200 -colorSlider.value) /2
colorShower.style.backgroundColor = buildColor(percentage)
})
}
.color-shower {
width: 100%;
height: 50px;
}
.color-slider {
width: 100%;
}
<div class = "color-shower"></div>
<input type = "range" min = "1" max = "200" value = "1" class = "color-slider" id = "colorSlider">
Edit: OK, I updated my response with a dynamic solution. I created wrapper <div> elements around the display/input elements. These wrappers allow you to define both a start-hue and end-hue data attribute. These are used as the gradient start/end for the display.
It's easy to miss (since you have to scroll all the way to the bottom), but with the plugin below; you can call GradientSlider() after your page loads.
The "static" default properties are at the bottom of the function definition. This can easily be re-written into an ES5/6 class.
GradientSlider.defaultOptions = {
selector : '.gradient-slider'
};
Various sliders are created below. I borrowed Infodev's absolute-value trick to adjust the saturation value as the slider approaches and 50%.
function GradientSlider(options) {
let opts = Object.assign({}, GradientSlider.defaultOptions, options);
construct(opts.selector); // Begin...
function construct(selector) {
Array.from(document.querySelectorAll(selector))
.forEach(gradientSlider => initializeSlider(gradientSlider));
}
function initializeSlider(gradientSlider) {
let hueStart = parseInt(gradientSlider.getAttribute('data-start-hue'), 10);
let hueEnd = parseInt(gradientSlider.getAttribute('data-end-hue'), 10);
let display = gradientSlider.querySelector('.gradient-slider-display');
let slider = gradientSlider.querySelector('.gradient-slider-input');
slider.addEventListener('input', onSliderChange);
let percentage = getSliderPercentage(slider);
let hue = percentage < 50 ? hueStart : hueEnd;
display.style.backgroundColor = calculateColor(hue, percentage);
}
function onSliderChange(e) {
let gradientSlider = e.target.parentElement;
let hueStart = parseInt(gradientSlider.getAttribute('data-start-hue'), 10);
let hueEnd = parseInt(gradientSlider.getAttribute('data-end-hue'), 10);
let display = gradientSlider.querySelector('.gradient-slider-display');
let percentage = getSliderPercentage(e.target);
let hue = percentage < 50 ? hueStart : hueEnd;
display.style.backgroundColor = calculateColor(hue, percentage)
}
function calculateColor(hue, percentage) {
return `hsl(${hue}, ${Math.abs(50 - percentage)}%, 50%)`;
}
function getSliderPercentage(slider) {
let value = parseInt(slider.value, 10);
let minValue = parseInt(slider.getAttribute('min'), 10);
let maxValue = parseInt(slider.getAttribute('max'), 10);
return scaleBetween(value, 0, 100, minValue, maxValue);
}
// Source: https://stackoverflow.com/a/60514474/1762224
function scaleBetween(n, tMin, tMax, sMin, sMax) {
return (tMax - tMin) * (n - sMin) / (sMax - sMin) + tMin;
}
}
GradientSlider.defaultOptions = {
selector : '.gradient-slider'
};
GradientSlider(); // Call the plugin...
.gradient-slider,
.gradient-slider > .gradient-slider-display,
.gradient-slider > .gradient-slider-input {
width: 100%;
}
.gradient-slider-display {
height: 50px;
}
<div class="gradient-slider" data-start-hue="120" data-end-hue="0">
<div class="gradient-slider-display"></div>
<input class="gradient-slider-input" type="range" min="1" max="200" value="1" />
</div>
<div class="gradient-slider" data-start-hue="240" data-end-hue="300">
<div class="gradient-slider-display"></div>
<input class="gradient-slider-input" type="range" min="50" max="150" value="75" />
</div>
<div class="gradient-slider" data-start-hue="30" data-end-hue="180">
<div class="gradient-slider-display"></div>
<input class="gradient-slider-input" type="range" min="0" max="10" value="7" />
</div>
I have found the solution but it's not perfect
Using this formulahsl(${hue}, ${Math.abs(100 - perc)}%, 50%);
window.onload = function() {
let colorShower = document.querySelector('.color-shower')
let colorSlider = document.querySelector('#colorSlider')
let percentage = (200 -colorSlider.value) /2
let buildColor = (capHue) => `hsl(${capHue}, ${Math.abs(100 - colorSlider.value)}%, 50%)`
colorShower.style.backgroundColor = buildColor(percentage)
colorSlider.addEventListener('input', e => {
percentage = (200 -colorSlider.value) /2
colorShower.style.backgroundColor = buildColor(percentage)
})
}
.color-shower {
width: 100%;
height: 50px;
}
.color-slider {
width: 100%;
}
<div class = "color-shower"></div>
<input type = "range" min = "1" max = "200" value = "1" class = "color-slider" id = "colorSlider">
But I still have some orange.

Checking function for sliding puzzle javascript

I created a sliding puzzle with different formats like: 3x3, 3x4, 4x3 and 4x4. When you run my code you can see on the right side a selection box where you can choose the 4 formats. The slidingpuzzle is almost done. But I need a function which checks after every move if the puzzle is solved and if that is the case it should give out a line like "Congrantulations you solved it!" or "You won!". Any idea how to make that work?
In the javascript code you can see the first function loadFunc() is to replace every piece with the blank one and the functions after that are to select a format and change the format into it. The function Shiftpuzzlepieces makes it so that you can move each piece into the blank space. Function shuffle randomizes every pieces position. If you have any more question or understanding issues just feel free to ask in the comments. Many thanks in advance.
Since I don't have enough reputation I will post a link to the images here: http://imgur.com/a/2nMlt . These images are just placeholders right now.
Here is the jsfiddle:
http://jsfiddle.net/Cuttingtheaces/vkyxgwo6/19/
As always, there is a "hacky", easy way to do this, and then there is more elegant but one that requires significant changes to your code.
Hacky way
To accomplish this as fast and dirty as possible, I would go with parsing id-s of pieces to check if they are in correct order, because they have this handy pattern "position" + it's expected index or "blank":
function isFinished() {
var puzzleEl = document.getElementById('slidingpuzzleContainer').children[0];
// convert a live list of child elements into regular array
var pieces = [].slice.call(puzzleEl.children);
return pieces
.map(function (piece) {
return piece.id.substr(8); // strip "position" prefix
})
.every(function (id, index, arr) {
if (arr.length - 1 == index) {
// last peace, check if it's blank
return id == "blank";
}
// check that every piece has an index that matches its expected position
return index == parseInt(id);
});
}
Now we need to check it somewhere, and naturally the best place would be after each move, so shiftPuzzlepieces() should be updated to call isFinished() function, and show the finishing message if it returns true:
function shiftPuzzlepieces(el) {
// ...
if (isFinished()) {
alert("You won!");
}
}
And voilà: live version.
How would I implement this game
For me, the proper way of implementing this would be to track current positions of pieces in some data structure and check it in similar way, but without traversing DOM or checking node's id-s. Also, it would allow to implement something like React.js application: onclick handler would mutate current game's state and then just render it into the DOM.
Here how I would implement the game:
/**
* Provides an initial state of the game
* with default size 4x4
*/
function initialState() {
return {
x: 4,
y: 4,
started: false,
finished: false
};
}
/**
* Inits a game
*/
function initGame() {
var gameContainer = document.querySelector("#slidingpuzzleContainer");
var gameState = initialState();
initFormatControl(gameContainer, gameState);
initGameControls(gameContainer, gameState);
// kick-off rendering
render(gameContainer, gameState);
}
/**
* Handles clicks on the container element
*/
function initGameControls(gameContainer, gameState) {
gameContainer.addEventListener("click", function hanldeClick(event) {
if (!gameState.started || gameState.finished) {
// game didn't started yet or already finished, ignore clicks
return;
}
if (event.target.className.indexOf("piece") == -1) {
// click somewhere not on the piece (like, margins between them)
return;
}
// try to move piece somewhere
movePiece(gameState, parseInt(event.target.dataset.index));
// check if we're done here
checkFinish(gameState);
// render the state of game
render(gameContainer, gameState);
event.stopPropagation();
return false;
});
}
/**
* Checks whether game is finished
*/
function checkFinish(gameState) {
gameState.finished = gameState.pieces.every(function(id, index, arr) {
if (arr.length - 1 == index) {
// last peace, check if it's blank
return id == "blank";
}
// check that every piece has an index that matches its expected position
return index == id;
});
}
/**
* Moves target piece around if there's blank somewhere near it
*/
function movePiece(gameState, targetIndex) {
if (isBlank(targetIndex)) {
// ignore clicks on the "blank" piece
return;
}
var blankPiece = findBlankAround();
if (blankPiece == null) {
// nowhere to go :(
return;
}
swap(targetIndex, blankPiece);
function findBlankAround() {
var up = targetIndex - gameState.x;
if (targetIndex >= gameState.x && isBlank(up)) {
return up;
}
var down = targetIndex + gameState.x;
if (targetIndex < ((gameState.y - 1) * gameState.x) && isBlank(down)) {
return down;
}
var left = targetIndex - 1;
if ((targetIndex % gameState.x) > 0 && isBlank(left)) {
return left;
}
var right = targetIndex + 1;
if ((targetIndex % gameState.x) < (gameState.x - 1) && isBlank(right)) {
return right;
}
}
function isBlank(index) {
return gameState.pieces[index] == "blank";
}
function swap(i1, i2) {
var t = gameState.pieces[i1];
gameState.pieces[i1] = gameState.pieces[i2];
gameState.pieces[i2] = t;
}
}
/**
* Handles form for selecting and starting the game
*/
function initFormatControl(gameContainer, state) {
var formatContainer = document.querySelector("#formatContainer");
var formatSelect = formatContainer.querySelector("select");
var formatApply = formatContainer.querySelector("button");
formatSelect.addEventListener("change", function(event) {
formatApply.disabled = false;
});
formatContainer.addEventListener("submit", function(event) {
var rawValue = event.target.format.value;
var value = rawValue.split("x");
// update state
state.x = parseInt(value[0], 10);
state.y = parseInt(value[1], 10);
state.started = true;
state.pieces = generatePuzzle(state.x * state.y);
// render game
render(gameContainer, state);
event.preventDefault();
return false;
});
}
/**
* Renders game's state into container element
*/
function render(container, state) {
var numberOfPieces = state.x * state.y;
updateClass(container, state.x, state.y);
clear(container);
var containerHTML = "";
if (!state.started) {
for (var i = 0; i < numberOfPieces; i++) {
containerHTML += renderPiece("", i) + "\n";
}
} else if (state.finished) {
containerHTML = "<div class='congratulation'><h2 >You won!</h2><p>Press 'Play!' to start again.</p></div>";
} else {
containerHTML = state.pieces.map(renderPiece).join("\n");
}
container.innerHTML = containerHTML;
function renderPiece(id, index) {
return "<div class='piece' data-index='" + index + "'>" + id + "</div>";
}
function updateClass(container, x, y) {
container.className = "slidingpuzzleContainer" + x + "x" + y;
}
function clear(container) {
container.innerHTML = "";
}
}
/**
* Generates a shuffled array of id-s ready to be rendered
*/
function generatePuzzle(n) {
var pieces = ["blank"];
for (var i = 0; i < n - 1; i++) {
pieces.push(i);
}
return shuffleArray(pieces);
function shuffleArray(array) {
for (var i = array.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = array[i];
array[i] = array[j];
array[j] = temp;
}
return array;
}
}
body {
font-family: "Lucida Grande", "Lucida Sans Unicode", Verdana, Helvetica, Arial, sans-serif;
font-size: 12px;
color: #000;
}
#formatContainer {
position: absolute;
top: 50px;
left: 500px;
}
#formatContainer label {
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
}
#formatContainer select {
display: block;
width: 100%;
margin-top: 10px;
margin-bottom: 10px;
}
#formatContainer button {
display: inline-block;
width: 100%;
}
.piece {
width: 96px;
height: 96px;
margin: 1px;
float: left;
border: 1px solid black;
}
.slidingpuzzleContainer3x3,
.slidingpuzzleContainer3x4,
.slidingpuzzleContainer4x3,
.slidingpuzzleContainer4x4 {
position: absolute;
top: 50px;
left: 50px;
border: 10px solid black;
}
.slidingpuzzleContainer3x3 {
width: 300px;
height: 300px;
}
.slidingpuzzleContainer3x4 {
width: 300px;
height: 400px;
}
.slidingpuzzleContainer4x3 {
width: 400px;
height: 300px;
}
.slidingpuzzleContainer4x4 {
width: 400px;
height: 400px;
}
.congratulation {
margin: 10px;
}
}
<body onload="initGame();">
<div id="slidingpuzzleContainer"></div>
<form id="formatContainer">
<label for="format">select format:</label>
<select name="format" id="format" size="1">
<option value="" selected="true" disabled="true"></option>
<option value="3x3">Format 3 x 3</option>
<option value="3x4">Format 3 x 4</option>
<option value="4x3">Format 4 x 3</option>
<option value="4x4">Format 4 x 4</option>
</select>
<button type="submit" disabled="true">Play!</button>
</form>
</body>
Here we have the initGame() function that starts everything. When called it will create an initial state of the game (we have default size and state properties to care about there), add listeners on the controls and call render() function with the current state.
initGameControls() sets up a listener for clicks on the field that will 1) call movePiece() which will try to move clicked piece on the blank spot if the former is somewhere around, 2) check if after move game is finished with checkFinish(), 3) call render() with updated state.
Now render() is a pretty simple function: it just gets the state and updates the DOM on the page accordingly.
Utility function initFormatControl() handles clicks and updates on the form for field size selection, and when the 'Play!' button is pressed will generate initial order of the pieces on the field and call render() with new state.
The main benefit of this approach is that almost all functions are decoupled from one another: you can tweak logic for finding blank space around target piece, to allow, for example, to swap pieces with adjacent ids, and even then functions for rendering, initialization and click handling will stay the same.
$(document).on('click','.puzzlepiece', function(){
var count = 0;
var imgarray = [];
var test =[0,1,2,3,4,5,6,7,8,'blank']
$('#slidingpuzzleContainer img').each(function(i){
var imgalt = $(this).attr('alt');
imgarray[i] = imgalt;
count++;
});
var is_same = (imgarray.length == test.length) && imgarray.every(function(element, index) {
return element === array2[index];
});
console.log(is_same); ///it will true if two array is same
});
try this... this is for only 3*3.. you pass the parameter and makethe array value as dynamically..

Categories