I am currently trying to generate a View with React Typescript where I can show all Appointments of a day (similar to outlook calendar). But I am facing an issue. When Appointments are overlapping how can I determine the with and position? So I guess that I probably need an algorithm accomplish this issue.
Here an example how it could be structured:
Every box stands for an Appointment
The classes of the Object looks like this:
class AppointmentStructured {
appointment: Appointment
width?: number
left?: number
}
class Appointment {
start: Date
end: Date
subject:string
duration: number
}
I am looking for a solution where I can determine the width (max. 1 = 100%) of the specific appointment and additionally I need the position. The easiest would be to how far it is from the left (max. 1). When there are more than 2 appointments starting at the same time, the appointments are ordered by the appointment with the longest duration.
Example the third box from the image would be:
width: 0.333
left: 0.666
Out of this data I can use CSS to design them.
The generated CSS then would look more or less like this:
position: absolute;
width: calc(100% * 0.33333);
top: 20rem; //doesn't matter for the algorithm
left: calc(100% * 0.66666)
--Edit
This is how far i came. It starts with the method generateAppointmentTile([])
export interface AppointmentStructured {
appointment: Appointment
width: string | number;
left: string | number
}
export const generateAppointmentTile = (appointments: Appointment[]): AppointmentStructured[] => {
var appointmentsStructured = initAppointmentStructured(appointments);
var response: AppointmentStructured[] = [];
for (var i = 0; i < appointmentsStructured.length; ++i) {
var width = 1;
var previous = getPreviousOverlapping(appointmentsStructured, i);
var previousWidth = previous
.map((item: AppointmentStructured): number => item.width as number)
.reduce((a: number, b: number): number => a + b, 0)
var forward = getForwardOverlapping(appointmentsStructured, i);
var forwardOverlays = appointmentOverlays(forward);
var previousHasOverlayWithForward = false;
previous.forEach((structured: AppointmentStructured) => checkAppointmentOverlaySingle(structured, forward) !== 0 ? previousHasOverlayWithForward = true : null);
width = (width - previousWidth) / (!previousHasOverlayWithForward && forwardOverlays !== 0 ? forwardOverlays : 1);
appointmentsStructured[i].width = width;
response.push(appointmentsStructured[i]);
}
response.forEach((value: AppointmentStructured): void => {
value.width = `calc((100% - 8rem) * ${value.width})`;
});
return response
}
const appointmentOverlays = (reading: AppointmentStructured[]): number => {
var highestNumber = 0;
reading.forEach((structured: AppointmentStructured): void => {
var start = checkAppointmentOverlaySingle(structured, reading) + 1;
highestNumber = start > highestNumber ? start : highestNumber;
});
return highestNumber;
}
const checkAppointmentOverlaySingle = (structured: AppointmentStructured, reading: AppointmentStructured[]): number => {
var start = 0;
reading.forEach((item: AppointmentStructured): void => {
if (item.appointment.id !== structured.appointment.id) {
if ((structured.appointment.start <= item.appointment.start && structured.appointment.end >= item.appointment.start)
|| (structured.appointment.start >= item.appointment.start && structured.appointment.start <= item.appointment.end)) {
start += 1;
}
}
});
return start;
}
const getPreviousOverlapping = (appointmentsStructured: AppointmentStructured[], index: number): AppointmentStructured[] => {
var response: AppointmentStructured[] = [];
for (var i = index - 1; i >= 0; --i) {
if (appointmentsStructured[index].appointment.start >= appointmentsStructured[i].appointment.start
&& appointmentsStructured[index].appointment.start <= appointmentsStructured[i].appointment.end) {
response.push(appointmentsStructured[i]);
}
}
return response;
}
const getForwardOverlapping = (appointmentsStructured: AppointmentStructured[], index: number): AppointmentStructured[] => {
var response: AppointmentStructured[] = [];
for (var i = index; i < appointmentsStructured.length; ++i) {
if (appointmentsStructured[index].appointment.start >= appointmentsStructured[i].appointment.start
&& appointmentsStructured[index].appointment.start <= appointmentsStructured[i].appointment.end) {
response.push(appointmentsStructured[i]);
}
}
return response;
}
const initAppointmentStructured = (appointments: Appointment[]): AppointmentStructured[] => {
var appointmentsStructured: AppointmentStructured[] = appointments
.sort((a: Appointment, b: Appointment): number => a.start.getTime() - b.start.getTime())
.map((appointment: Appointment): AppointmentStructured => ({ appointment, width: 100, left: 0 }));
var response: AppointmentStructured[] = [];
// sort in a intelligent way
for (var i = 0; i < appointmentsStructured.length; ++i) {
var duration = appointmentsStructured[i].appointment.end.getTime() - appointmentsStructured[i].appointment.start.getTime();
var sameStartAppointments = findAppointmentWithSameStart(appointmentsStructured[i], appointmentsStructured);
var hasLongerAppointment: boolean = false;
sameStartAppointments.forEach((structured: AppointmentStructured) => (structured.appointment.end.getTime() - structured.appointment.start.getTime()) > duration ? hasLongerAppointment = true : null);
if (!hasLongerAppointment) {
response.push(appointmentsStructured[i]);
appointmentsStructured.splice(i, 1);
i = -1;
}
}
return response.sort((a: AppointmentStructured, b: AppointmentStructured): number => a.appointment.start.getTime() - b.appointment.start.getTime());
}
const findAppointmentWithSameStart = (structured: AppointmentStructured, all: AppointmentStructured[]): AppointmentStructured[] => {
var response: AppointmentStructured[] = [];
all.forEach((appointmentStructured: AppointmentStructured) => appointmentStructured.appointment.start === structured.appointment.start
&& appointmentStructured.appointment.id !== structured.appointment.id ? response.push(appointmentStructured) : null)
return response;
}
Even some pseudo code would help me a lot.
This solution is based on this answer: Visualization of calendar events. Algorithm to layout events with maximum width
I've just ported the script from C# to JS and extracted some steps.
Group all your intervals/appointments into distinct groups, where one group does not influence another.
For each group, fill columns from left to right (with each appointment getting the same width). Just use as many columns as you need.
Stretch each appointment maximally.
Make the actual boxes from that and render them.
This procedure is not perfect. Sometimes you will have a thin box on the left and the box to the right got stretched all the way. This happens mainly when the left box has a long "overlap chain" to the bottom, which does not really happen in practice.
/*
interface Interval {
start: number;
end: number;
}
*/
// Just for demonstration purposes.
function generateIntervals({ count, maxStart, maxEnd, minLength, maxLength, segments }) {
return Array.from(Array(count), () => randomInterval())
function randomInterval() {
const start = randomInt(0, maxStart * segments) / segments
const length = randomInt(minLength * segments, maxLength * segments) / segments
return {
start,
end: Math.min(start + length, maxEnd),
text: "Here could be your advertising :)"
}
}
function randomInt(start, end) {
return Math.floor(Math.random() * (end - start) + start)
}
}
function isOverlapping(interval1, interval2) {
const start = Math.max(interval1.start, interval2.start)
const end = Math.min(interval1.end, interval2.end)
return start < end
}
// Sorts by start ascending.
function sortIntervalsByStart(intervals) {
return intervals
.slice()
.sort(({ start: s1 }, { start: s2 }) => s1 - s2)
}
// Split intervals into groups, which are independent of another.
function groupIntervals(intervals) {
const groups = []
let latestIntervalEnd = -Infinity
for (const interval of sortIntervalsByStart(intervals)) {
const { start, end } = interval
// There is no overlap to previous intervals. Create a new group.
if (start >= latestIntervalEnd) {
groups.push([])
}
groups[groups.length - 1].push(interval)
latestIntervalEnd = Math.max(latestIntervalEnd, end)
}
return groups
}
// Fill columns with equal width from left to right.
function putIntervalsIntoColumns(intervals) {
const columns = []
for (const interval of intervals) {
let columnIndex = findFreeColumn(interval)
columns[columnIndex] = (columns[columnIndex] || []).concat([interval])
}
return columns
function findFreeColumn(interval) {
let columnIndex = 0
while (
columns?.[columnIndex]
?.some(otherInterval => isOverlapping(interval, otherInterval))
) {
columnIndex++
}
return columnIndex
}
}
// Expand columns maximally.
function makeBoxes(columns, containingInterval) {
const boxes = []
for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
for (const interval of columns[columnIndex]) {
const columnSpan = findColumnSpan(columnIndex, interval)
const box = {
...interval,
top: (interval.start - containingInterval.start) / (containingInterval.end - containingInterval.start),
height: (interval.end - interval.start) / (containingInterval.end - containingInterval.start),
left: columnIndex / columns.length,
width: columnSpan / columns.length
}
boxes.push(box)
}
}
return boxes
function findColumnSpan(columnIndex, interval) {
let columnSpan = 1
while (
columns
?.[columnIndex + columnSpan]
?.every(otherInterval => !isOverlapping(interval, otherInterval))
) {
columnSpan++
}
return columnSpan
}
}
function computeBoxes(intervals, containingInterval) {
return groupIntervals(intervals)
.map(intervals => putIntervalsIntoColumns(intervals))
.flatMap(columns => makeBoxes(columns, containingInterval))
}
function renderBoxes(boxes) {
const calendar = document.querySelector("#calendar")
for (const { top, left, width, height, text = "" } of boxes) {
const el = document.createElement("div")
el.style.position = "absolute"
el.style.top = `${top * 100}%`
el.style.left = `${left * 100}%`
el.style.width = `${width * 100}%`
el.style.height = `${height * 100}%`
el.textContent = text
calendar.appendChild(el)
}
}
const hours = 24
const intervals = generateIntervals({
count: 20,
maxStart: 22,
maxEnd: hours,
maxLength: 4,
minLength: 1,
segments: 2,
})
const boxes = computeBoxes(intervals, { start: 0, end: hours })
renderBoxes(boxes)
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
#calendar {
height: 100vh;
width: 100vw;
min-height: 800px;
position: relative;
}
#calendar > * {
border: 1px solid black;
background-color: #b89d9d;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
overflow-y: auto;
}
<div id="calendar"></div>
<script src="./index.js"></script>
Related
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 ?
I can use the coloroo function quite well but just for the first time. when I try to do it after the first time, the function cannot compare results properly.
for some reason the chosen right answer does change and beca
help please
function coloroo() {
newColours.addEventListener("click", () => {
let limit = 6;
let check = 0;
while (check < limit) {
let a = random();
let b = random();
let c = random();
square[check].style.backgroundColor = `rgb(${a},${b},${c})`;
check = check + 1;
}
let right = Math.floor((Math.random() * 6));
let answer = square[right];
textColo.innerHTML = `${answer.style.backgroundColor}`;
square.forEach(e => {
e.addEventListener("click", () => {
let choice = e.style.backgroundColor;
let corree = answer.style.backgroundColor;
if (corree == choice) {
messag.innerHTML = "correct";
} else if (corree != choice) {
messag.innerHTML = "try again";
e.style.backgroundColor = "#232323";
}
})
})
})
};
coloroo();
Looking at your code it seems you're building a color quiz is my guess.
I've edit the code and added some. Maybe it will help you for what you are looking for. The compare problem doesn't happen in this example.
const d = document;
const newColours = d.getElementById('new-colours');
const random = () => 0 + Math.floor(Math.random() * (255 - 0 + 1));
const square = [
d.getElementById('item-0'),
d.getElementById('item-1'),
d.getElementById('item-2'),
d.getElementById('item-3'),
d.getElementById('item-4'),
d.getElementById('item-5')
];
const question = d.getElementById('question');
const textColor = d.getElementById('text-color');
const messag = d.getElementById('messag');
const createOptions = () => {
let limit = 6;
let check = 0;
while (check < limit) {
let a = random();
let b = random();
let c = random();
square[check].style.backgroundColor = `rgb(${a},${b},${c})`;
check = check + 1;
}
}
function coloroo() {
let answer, choice, corree;
newColours.addEventListener("click", () => {
question.style.display = 'block';
newColours.innerHTML = 'Refresh';
createOptions();
let right = Math.floor((Math.random() * 6));
answer = square[right];
textColor.innerHTML = `${answer.style.backgroundColor}`;
messag.style.display = 'none';
})
square.forEach(e => {
e.addEventListener("click", () => {
choice = e.style.backgroundColor;
corree = answer.style.backgroundColor;
if (corree == choice) {
messag.innerHTML = "correct";
} else if (corree != choice) {
messag.innerHTML = "try again";
e.style.backgroundColor = "#232323";
}
messag.style.display = 'block';
})
})
// Uncomment the next line to make it start immediately
// newColours.click();
}
coloroo();
.choices {
display: flex;
margin-top: 10px;
}
.choices div {
color: #fff;
cursor: pointer;
height: 40px;
width: 40px;
padding: 10px;
box-sizing: border-box;
margin: 0 10px 10px 0;
}
#question {
display: none;
}
<html>
<body>
<button id="new-colours">Start</button>
<div class="choices">
<div id="item-0"></div>
<div id="item-1"></div>
<div id="item-2"></div>
<div id="item-3"></div>
<div id="item-4"></div>
<div id="item-5"></div>
</div>
<p id="question">Which square has <span id="text-color"></span> as background color value?</p>
<p id="messag"></p>
</body>
</html>
I'm trying to build a 10x10 grid with a for loop in React. But React doesn't let me perform this function because it says that it exceed the maximum of actions performed. Is there a way to generate this 10x10 grid without the use of the loop?
Any help I would really appreciate it. I'm building the battleship project.
import React, {useEffect ,useState} from 'react'
import Ship from '../components/ShipGenerate.js'
import '../style/style.css'
function Grid(props) {
const [grid, setGrid] = useState([])
console.log(grid)
useEffect(() => {
// Here we push the result boxes to random generated arrays in different positions. We put it inside a useEffect so it only changes the array once
for (i = 0; i < props.data[0].size; i++) {
let result = randomNumberArray.map(function(val){return ++val;})
let finalResult = randomNumberArray.push(result[i])
}
for (i = 0; i < props.data[1].size; i++) {
let result2 = randomNumberArray2.map(function(val) {return ++val})
let secondResult = randomNumberArray2.push(result2[i])
}
for (i = 0; i < props.data[2].size; i++) {
let result = randomNumberArray3.map(function(val){return ++val;})
let finalResult = randomNumberArray3.push(result[i])
}
for (i = 0; i < props.data[3].size; i++) {
let result2 = randomNumberArray4.map(function(val) {return ++val})
let secondResult = randomNumberArray4.push(result2[i])
}
for (i = 0; i < props.data[4].size; i++) {
let result2 = randomNumberArray5.map(function(val) {return ++val})
let secondResult = randomNumberArray5.push(result2[i])
}
}, [])
let randomNumberArray = [...Array(1)].map(()=>(Math.random() * 7 | 0) + 11 * (Math.random() * 10 | 0))
let randomNumberArray2 = [...Array(1)].map(()=>(Math.random() * 7 | 0) + 11 * (Math.random() * 10 | 0))
let randomNumberArray3 = [...Array(1)].map(()=>(Math.random() * 7 | 0) + 11 * (Math.random() * 10 | 0))
let randomNumberArray4 = [...Array(1)].map(()=>(Math.random() * 7 | 0) + 11 * (Math.random() * 10 | 0))
let randomNumberArray5 = [...Array(1)].map(()=>(Math.random() * 7 | 0) + 11 * (Math.random() * 10 | 0))
// We generate the 10x10 grid
for (var i = 0; i < 110; i++) {
if (randomNumberArray.includes(i)) { // ---> checking with this condition if the index in the random array to give it a different className
setGrid([...grid,<button className="hello"></button>])
}
else if (randomNumberArray2.includes(i)) {
setGrid([...grid,<button className="hello"></button>])
}
else if (randomNumberArray3.includes(i)) {
setGrid([...grid,<button className="hello"></button>])
}
else if (randomNumberArray4.includes(i)) {
setGrid([...grid,<button className="hello"></button>])
}
else if (randomNumberArray5.includes(i)) {
setGrid([...grid,<button className="hello"></button>])
}
else {
setGrid([...grid,<button className="boxGrid"></button>])
}
}
const onClick = (e) => {
e.preventDefault()
e.target.textContent = "x"
props.setTurn(false)
}
return (
<div>
<div onClick={onClick} className="box">
{grid}
</div>
</div>
)
}
export default Grid
Trying to figure out how your trying to create the game, and correct it.
I thought it might be easier to create a simple example that you could use as a starting point.
Below is a 10x10 grid, I've randomly created ships, I'll leave it to you to update to create proper ships etc, and handle players & game logic.
const {useState} = React;
let board;
function createBoard() {
//create battlefield board
board = new Array(10);
for (let i=0; i<board.length; i+=1)
board[i] = new Array(10).fill(0);
const rnd = r => Math.trunc(Math.random() * r);
//board values
//0 = empty
//1 = ship
//2 = empty missed shot
//3 = ship hit
//fill some random ship
for (let l=0; l<40;l +=1) {
board[rnd(10)][rnd(10)] = 1;
}
}
createBoard();
function Piece(p) {
const [value, setValue] = useState(board[p.y][p.x]);
const cls = ['piece'];
let txt = '.';
if (value === 2) cls.push('empty-shot');
if (value === 3) { cls.push('boom'); txt = '*' }
return <div
onClick={() => {
let c = board[p.y][p.x];
if (c > 1) return;
if (c === 0) c = 2;
else if (c === 1) c = 3;
board[p.y][p.x] = c;
setValue(c);
}}
className={cls.join(' ')}>
{txt}
</div>
}
function Board() {
return <div className="board">{
board.map((b,iy) =>
<div key={iy}>
{b.map((a,ix) => <Piece key={ix} x={ix} y={iy}/>)}
</div>
)}</div>
}
function Game() {
const [gameCount, setGameCount] = useState(1);
return <div>
<div style={{marginBottom: "1rem"}}><button onClick={() => {
createBoard();
setGameCount(c => c + 1);
}}>Clear</button></div>
<Board key={gameCount}/>
</div>
}
ReactDOM.render(<Game/>, document.querySelector('#mount'));
.board {
display: inline-block;
background-color: black;
border: 0.3em solid black;
}
.piece {
display: inline-grid;
width: 30px;
height: 30px;
background-color: cyan;
border: 0.2em solid black;
align-content: center;
justify-items: center;
cursor: pointer;
user-select: none;
}
.piece.empty-shot {
color: white;
background-color: navy;
}
.piece.boom {
background-color: red;
color: white;
}
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="mount"></div>
I have create two dices that are 1 until 11 slots.I want to display the results.If red wins (for example I want to show me a message R wins or if B ends first the B wins).Also, I want to display the results in a new table.I am trying to do that but I can't. Javascript code is this.
// basic game settings
const gameSettings = {
length: 12,
die: {
min: 1,
max: 8,
}
}
// define "actors"
let gameItems = [{
name: "R",
bgColor: "red",
color: "white",
position: 1,
},
{
name: "B",
bgColor: "black",
color: "white",
position: 1,
},
]
// the random function
// source: https://stackoverflow.com/questions/4959975/generate-random-number-between-two-numbers-in-javascript
function randomIntFromInterval(min, max) { // min and max included
return Math.floor(Math.random() * (max - min + 1) + min);
}
// -----------------
// creating a row & cells
const rowHtml = (gameItem, cols) => {
let html = `<div class="row" style="grid-template-columns: repeat(${ cols }, 1fr)">`
for (let i = 0; i < cols; i++) {
let marker = "-"
let background = "white"
let color = "black"
if (gameItem.position - 1 === i) {
marker = gameItem.name
background = gameItem.bgColor
color = gameItem.color
}
html += `<div class="cell" style="background: ${ background }; color: ${ color }">${ marker }</div>`
}
html += `</div>`
return html
}
// updating rows
const updateRows = (container, gameItems, cols) => {
let html = ''
for (let i = 0; i < gameItems.length; i++) {
html += rowHtml(gameItems[i], cols)
}
container.innerHTML = html
}
// setting container
const container = document.getElementById('container')
// set up board for first time
updateRows(container, gameItems, gameSettings.length)
// -----------------
// action buttons
const btnRoll = document.getElementById("rollTheDice")
const btnResetBoard = document.getElementById("resetBoard")
// roll action
btnRoll.addEventListener('click', function() {
const {
length,
die: {
min
},
die: {
max
}
} = gameSettings
gameItems = gameItems.map(item => ({
...item,
position: item.position + randomIntFromInterval(min, max),
}))
updateRows(container, gameItems, length)
})
// reset action
btnResetBoard.addEventListener('click', function() {
const {
length
} = gameSettings
gameItems = gameItems.map(item => ({
...item,
position: 1,
}))
updateRows(container, gameItems, length)
})
and the code of html,css is this
.row {
display: grid;
grid-template-rows: 1fr;
}
.cell {
box-sizing: border-box;
border: 1px solid black;
display: flex;
justify-content: center;
}
<div id="container"></div>
<hr />
<button id="rollTheDice">ROLL THE DICE</button><br />
<button id="resetBoard">RESET BOARD</button>
Its really simple solution but you can work on it and extend to really have what you want.
I would done it like so apply some check of each player score on each click on roll the dice button, of course after all randomizing happens to prevent need clicking another time to get results.
let winnerScore = 0;
let winner = '';
gameItems.forEach(item => {
if (item.position >= 11) {
winner = item.name;
winnerScore = item.position;
}
});
if (winnerScore >= 11) {
console.log('[winner_result]', winner, ' wins game!');
}
Of course instead of using this console.log on detect that someone won you can easily exapnd it to display scores or really what you want on screen. I think the best way to do that is to really get all properties from "actor" object in loop check.
This solution will really decreas number of loops you need to do.
To really show results you'll have to add function or outputing it into html div element like so
if (winnerScore >= 11) {
console.log('[winner_result]', winner, ' wins game!');
document.getElementById('results').innerHTML = `${winner} wins game!`;
}
to get it working really you also need to add somewhere in you html strucure div with id "results" or anything else really but you have to remeber to swap ip too on js script
Hope It answers you're needs.
Below link to codepen which I was working on this solution:
https://codepen.io/snoh666/pen/jOMamMy
I am working on a basic sorting visualizer with using only HTML, CSS, and JS, and I've run into a problem with the animation aspect. To initialize the array, I generate random numbers within some specified range and push them on to the array. Then based on the webpage dimensions, I create divs for each element and give each one height and width dimensions accordingly, and append each to my "bar-container" div currently in the dom.
function renderVisualizer() {
var barContainer = document.getElementById("bar-container");
//Empties bar-container div
while (barContainer.hasChildNodes()) {
barContainer.removeChild(barContainer.lastChild);
}
var heightMult = barContainer.offsetHeight / max_element;
var temp = barContainer.offsetWidth / array.length;
var barWidth = temp * 0.9;
var margin = temp * 0.05;
//Creating array element bars
for (var i = 0; i < array.length; i++) {
var arrayBar = document.createElement("div");
arrayBar.className = "array-bar"
if (barWidth > 30)
arrayBar.textContent = array[i];
//Style
arrayBar.style.textAlign = "center";
arrayBar.style.height = array[i] * heightMult + "px";
arrayBar.style.width = barWidth;
arrayBar.style.margin = margin;
barContainer.appendChild(arrayBar);
}
}
I wrote the following animated selection sort and it works well, but the only "animated" portion is in the outer for-loop, and I am not highlighting bars as I traverse through them.
function selectionSortAnimated() {
var barContainer = document.getElementById("bar-container");
var barArr = barContainer.childNodes;
for (let i = 0; i < barArr.length - 1; i++) {
let min_idx = i;
let minNum = parseInt(barArr[i].textContent);
for (let j = i + 1; j < barArr.length; j++) {
let jNum = parseInt(barArr[j].textContent, 10);
if (jNum < minNum) {
min_idx = j;
minNum = jNum;
}
}
//setTimeout(() => {
barContainer.insertBefore(barArr[i], barArr[min_idx])
barContainer.insertBefore(barArr[min_idx], barArr[i]);
//}, i * 500);
}
}
I am trying to use nested setTimeout calls to highlight each bar as I traverse through it, then swap the bars, but I'm running into an issue. I'm using idxContainer object to store my minimum index, but after each run of innerLoopHelper, it ends up being equal to i and thus there is no swap. I have been stuck here for a few hours and am utterly confused.
function selectionSortTest() {
var barContainer = document.getElementById("bar-container");
var barArr = barContainer.childNodes;
outerLoopHelper(0, barArr, barContainer);
console.log(array);
}
function outerLoopHelper(i, barArr, barContainer) {
if (i < array.length - 1) {
setTimeout(() => {
var idxContainer = {
idx: i
};
innerLoopHelper(i + 1, idxContainer, barArr);
console.log(idxContainer);
let minIdx = idxContainer.idx;
let temp = array[minIdx];
array[minIdx] = array[i];
array[i] = temp;
barContainer.insertBefore(barArr[i], barArr[minIdx])
barContainer.insertBefore(barArr[minIdx], barArr[i]);
//console.log("Swapping indices: " + i + " and " + minIdx);
outerLoopHelper(++i, barArr, barContainer);
}, 100);
}
}
function innerLoopHelper(j, idxContainer, barArr) {
if (j < array.length) {
setTimeout(() => {
if (j - 1 >= 0)
barArr[j - 1].style.backgroundColor = "gray";
barArr[j].style.backgroundColor = "red";
if (array[j] < array[idxContainer.idx])
idxContainer.idx = j;
innerLoopHelper(++j, idxContainer, barArr);
}, 100);
}
}
I know this is a long post, but I just wanted to be as specific as possible. Thank you so much for reading, and any guidance will be appreciated!
Convert your sorting function to a generator function*, this way, you can yield it the time you update your rendering:
const sorter = selectionSortAnimated();
const array = Array.from( { length: 100 }, ()=> Math.round(Math.random()*50));
const max_element = 50;
renderVisualizer();
anim();
// The animation loop
// simply calls itself until our generator function is done
function anim() {
if( !sorter.next().done ) {
// schedules callback to before the next screen refresh
// usually 60FPS, it may vary from one monitor to an other
requestAnimationFrame( anim );
// you could also very well use setTimeout( anim, t );
}
}
// Converted to a generator function
function* selectionSortAnimated() {
const barContainer = document.getElementById("bar-container");
const barArr = barContainer.children;
for (let i = 0; i < barArr.length - 1; i++) {
let min_idx = i;
let minNum = parseInt(barArr[i].textContent);
for (let j = i + 1; j < barArr.length; j++) {
let jNum = parseInt(barArr[j].textContent, 10);
if (jNum < minNum) {
barArr[min_idx].classList.remove( 'selected' );
min_idx = j;
minNum = jNum;
barArr[min_idx].classList.add( 'selected' );
}
// highlight
barArr[j].classList.add( 'checking' );
yield; // tell the outer world we are paused
// once we start again
barArr[j].classList.remove( 'checking' );
}
barArr[min_idx].classList.remove( 'selected' );
barContainer.insertBefore(barArr[i], barArr[min_idx])
barContainer.insertBefore(barArr[min_idx], barArr[i]);
// pause here too?
yield;
}
}
// same as OP
function renderVisualizer() {
const barContainer = document.getElementById("bar-container");
//Empties bar-container div
while (barContainer.hasChildNodes()) {
barContainer.removeChild(barContainer.lastChild);
}
var heightMult = barContainer.offsetHeight / max_element;
var temp = barContainer.offsetWidth / array.length;
var barWidth = temp * 0.9;
var margin = temp * 0.05;
//Creating array element bars
for (var i = 0; i < array.length; i++) {
var arrayBar = document.createElement("div");
arrayBar.className = "array-bar"
if (barWidth > 30)
arrayBar.textContent = array[i];
//Style
arrayBar.style.textAlign = "center";
arrayBar.style.height = array[i] * heightMult + "px";
arrayBar.style.width = barWidth;
arrayBar.style.margin = margin;
barContainer.appendChild(arrayBar);
}
}
#bar-container {
height: 250px;
white-space: nowrap;
width: 3500px;
}
.array-bar {
border: 1px solid;
width: 30px;
display: inline-block;
background-color: #00000022;
}
.checking {
background-color: green;
}
.selected, .checking.selected {
background-color: red;
}
<div id="bar-container"></div>
So I thought about this, and it's a little tricky, what I would do is just store the indexes of each swap as you do the sort, and then do all of the animation seperately, something like this:
// how many elements we want to sort
const SIZE = 24;
// helper function to get a random number
function getRandomInt() {
return Math.floor(Math.random() * Math.floor(100));
}
// this will hold all of the swaps of the sort.
let steps = [];
// the data we are going to sort
let data = new Array(SIZE).fill(null).map(getRandomInt);
// and a copy that we'll use for animating, this will simplify
// things since we can just run the sort to get the steps and
// not have to worry about timing yet.
let copy = [...data];
let selectionSort = (arr) => {
let len = arr.length;
for (let i = 0; i < len; i++) {
let min = i;
for (let j = i + 1; j < len; j++) {
if (arr[min] > arr[j]) {
min = j;
}
}
if (min !== i) {
let tmp = arr[i];
// save the indexes to swap
steps.push({i1: i, i2: min});
arr[i] = arr[min];
arr[min] = tmp;
}
}
return arr;
}
// sort the data
selectionSort(data);
const container = document.getElementById('container');
let render = (data) => {
// initial render...
data.forEach((el, index) => {
const div = document.createElement('div');
div.classList.add('item');
div.id=`i${index}`;
div.style.left = `${2 + (index * 4)}%`;
div.style.top = `${(98 - (el * .8))}%`
div.style.height = `${el * .8}%`
container.appendChild(div);
});
}
render(copy);
let el1, el2;
const interval = setInterval(() => {
// get the next step
const {i1, i2} = steps.shift();
if (el1) el1.classList.remove('active');
if (el2) el2.classList.remove('active');
el1 = document.getElementById(`i${i1}`);
el2 = document.getElementById(`i${i2}`);
el1.classList.add('active');
el2.classList.add('active');
[el1.id, el2.id] = [el2.id, el1.id];
[el1.style.left, el2.style.left] = [el2.style.left, el1.style.left]
if (!steps.length) {
clearInterval(interval);
document.querySelectorAll('.item').forEach((el) => el.classList.add('active'));
}
}, 1000);
#container {
border: solid 1px black;
box-sizing: border-box;
padding: 20px;
height: 200px;
width: 100%;
background: #EEE;
position: relative;
}
#container .item {
position: absolute;
display: inline-block;
padding: 0;
margin: 0;
width: 3%;
height: 80%;
background: #cafdac;
border: solid 1px black;
transition: 1s;
}
#container .item.active {
background: green;
}
<div id="container"></div>