JS: detecting a win in a tictactoe game? - javascript

I am just doing some practice with a color alignment game. I want to run a function after every move to see if it's winner but I can't figure out the best way to check if 3 colors are in a row or diagonal. I guess I could iterate down the DOM tree each time and see if certain multiples are fulfilled but I don't know if that's the best option.
(() => {
let counter = 0;
let conts = document.querySelectorAll('.parent div')
conts.forEach(x => {
x.addEventListener('click', (evt) => {
if (!!evt.target.style.backgroundColor) {
return;
}
counter++
if (counter % 2 == 0) {
evt.target.style.backgroundColor = "red"
} else {
evt.target.style.backgroundColor = "green"
}
})
});
})();
html, body {
height: 100%;
}
.parent {
height: 100%;
display: grid;
grid-template-columns: 50px 50px 50px;
grid-template-rows: 50px 50px 50px;
grid-gap: 2px;
width: 40%;
}
.parent div {
border: 1px solid #000;
}
<div class="parent">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>

You could solve this problem traversing the grid horizontally, vertically, and diagonally, in search of a sequence of "boxes" with the same color.
// used to walk the grid
const size = Math.sqrt(boxes.length);
// used to check grid columns, rows, and diagonal
const check = (color, index, inc = 1) => {
for (let y = 0; y < size; y += inc) {
let score = 0;
for (let x = 0; x < size; x++) {
// index callback to retrieve the next box
if (boxes[index(x, y)].style.backgroundColor === color)
score++;
else
break;
}
if (score === size)
return true;
}
return false;
};
Whenever a color is set, you can perform such check via:
if (
// horizontally
// 0 1 2,
// 3 4 5,
// 6 7 8
check(color, (x, y) => (y * size) + x) ||
// vertically
// 0 3 6,
// 1 4 7,
// 2 5 8
check(color, (x, y) => (y + (size * x))) ||
// diagonally (y loops only once)
// 0 4 8
check(color, (x, y) => x * (size + 1), size) ||
// diagonally (y loops only once)
// 2 4 6
check(color, (x, y) => ((x + 1) * (size - 1)), size)
)
alert(color + ' won!');
Differently from other answers, this logic is independent of the grid size.
You can see the 3x3 live and play around, but you can also see that without changing code you can have a 4x4 grid too.
The whole code in a nutshell:
(() => {
let counter = 0;
const boxes = document.querySelectorAll('.parent div');
const size = Math.sqrt(boxes.length);
const check = (color, index, inc = 1) => {
for (let y = 0; y < size; y += inc) {
let score = 0;
for (let x = 0; x < size; x++) {
if (boxes[index(x, y)].style.backgroundColor === color)
score++;
else
break;
}
if (score === size)
return true;
}
return false;
};
boxes.forEach(x => {
x.addEventListener('click', (evt) => {
if (!!evt.target.style.backgroundColor)
return;
counter++;
const color = counter % 2 ? "green" : "red";
evt.target.style.backgroundColor = color;
if (
check(color, (x, y) => (y * size) + x) ||
check(color, (x, y) => (y + (size * x))) ||
check(color, (x, y) => x * (size + 1), size) ||
check(color, (x, y) => ((x + 1) * (size - 1)), size)
)
alert(color + ' won!');
})
});
})();
I hope this helps 👋

you can try something like this.
let counter = 0;
let winner = null;
const match = (player, row, col) => {
const div = document.querySelector(`.parent #r${row}c${col}`);
const bgColor = div.style.backgroundColor;
return bgColor === player;
}
const win_hori = (player, row) => {
return ![1, 2, 3].some(col => !match(player, row, col));
}
const win_verti = (player, col) => {
return ![1, 2, 3].some(row => !match(player, row, col));
}
const win_slash = (player) => {
return match(player, 1, 1) && match(player, 2, 2) && match(player, 3, 3);
}
const win_backslash = (player) => {
return match(player, 3, 1) && match(player, 2, 2) && match(player, 1, 3);
}
const win = (player, row, col) => {
return win_hori(player, row) || win_verti(player, col) || win_slash(player) || win_backslash(player);
}
document.querySelectorAll('.parent div').forEach(x => {
x.addEventListener('click', (evt) => {
const cell = evt.target;
if (winner || !!cell.style.backgroundColor) {
return;
}
const player = (++counter % 2 == 0) ? "red" : "green";
cell.style.backgroundColor = player;
const row = parseInt(cell.id.substring(1, 2));
const col = parseInt(cell.id.substring(3, 4));
if (win(player, row, col)) {
winner = player;
document.getElementById("winner").innerText = `${player} wins`;
}
})
});
html, body {
height: 100%;
}
.parent {
height: 100%;
display: grid;
grid-template-columns: 50px 50px 50px;
grid-template-rows: 50px 50px 50px;
grid-gap: 2px;
width: 40%;
}
.parent div {
border: 1px solid #000;
}
<div id="winner">nobody wins</div>
<div class="parent">
<div id="r1c1"></div>
<div id="r1c2"></div>
<div id="r1c3"></div>
<div id="r2c1"></div>
<div id="r2c2"></div>
<div id="r2c3"></div>
<div id="r3c1"></div>
<div id="r3c2"></div>
<div id="r3c3"></div>
</div>

Related

Is there a way to make a big loop work in react?

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>

How to show boxes properly? (Algorithm, UI)

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>

Jquery UI Dragabble is not enabled

class Cell {
constructor(game, index) {
this.isEmpty = false;
this.game = game;
this.index = index;
this.height = (game.height / game.dimension);
this.width = (game.width / game.dimension);
this.id = "cell-" + index;
this.cell = this.createCell();
$("#content").append(this.cell);
if (this.index === this.game.dimension * this.game.dimension - 1) {
this.isEmpty = true;
return;
}
this.setImage();
this.setPosition(this.index)
}
createCell() {
const cell = document.createElement("div");
$(cell).css({
"width": this.height + "px",
"height": this.width + "px",
"position": "absolute",
"border": "1px solid #fff"
})
$(cell).attr("id", this.id);
//On click Move to the Empty Space
$(cell).on("click", () => {
if (this.game.gameStarted) {
let validCells = this.game.checkValidCells();
let compareCells = [];
if (validCells.right) {
compareCells.push(this.game.cells[validCells.right].cell);
}
if (validCells.left || validCells.left === 0) {
compareCells.push(this.game.cells[validCells.left].cell);
}
if (validCells.top || validCells.top === 0) {
compareCells.push(this.game.cells[validCells.top].cell);
}
if (validCells.bottom) {
compareCells.push(this.game.cells[validCells.bottom].cell);
}
let i = this.game.cells.findIndex(item => item.cell === cell);
let j = this.game.findEmptyCell();
if (compareCells.indexOf(cell) != -1) {
[this.game.cells[i], this.game.cells[j]] = [this.game.cells[j], this.game.cells[i]];
this.game.cells[i].setPosition(i);
this.game.cells[j].setPosition(j);
if (this.game.checkWin()) {
alert("you won the game!!");
this.game.numberOfMoves = 0;
this.game.gameStarted = false;
}
this.game.numberOfMoves++;
$("#moves").html("Moves: " + this.game.numberOfMoves);
}
this.game.dragTheTile();
}
})
return cell;
}
setImage() {
const left = this.width * (this.index % this.game.dimension);
const top = this.height * Math.floor(this.index / this.game.dimension);
const bgPosition = -left + "px" + " " + -top + "px";
const bgSize = this.game.width + "px " + this.game.height + "px"
$(this.cell).css({
"background": 'url(' + this.game.imageSrc + ')',
"background-position" : bgPosition,
"background-size": bgSize
})
}
setPosition(index) {
const {left, top} = this.getPosition(index);
$(this.cell).css({
"left": left + "px",
"top": top + "px"
})
}
makeDraggable(index) {
let emptyCellIndex = this.game.findEmptyCell();
$(this.cell).draggable({
containment: "parent",
snap: this.game.cells[emptyCellIndex],
cursor: "move",
snapMode: "inner",
snapTolerance: 20,
helper: "clone",
opacity: 0.5
})
}
makeDroppable(index) {
$(this.cell).droppable({
drop: (event, ui) => {
let draggedCell;
draggedCell = ui.draggable;
let i = this.game.cells.findIndex(item => item.cell === draggedCell[0]);
let j = this.game.findEmptyCell();
[this.game.cells[i], this.game.cells[j]] = [this.game.cells[j], this.game.cells[i]];
this.game.cells[i].setPosition(i);
this.game.cells[j].setPosition(j);
this.game.clearDrag();
this.game.numberOfMoves++;
$("#moves").html("Moves: " + this.game.numberOfMoves);
if (this.game.checkWin()) {
alert("you won the game!!");
this.game.numberOfMoves = 0;
this.game.gameStarted = false;
} else {
this.game.dragTheTile();
}
}
})
}
getPosition(index) {
return {
left: this.width * (index % this.game.dimension),
top: this.height * Math.floor(index / this.game.dimension)
}
}
}
class GameBoard {
constructor(dimension){
this.dimension = dimension;
this.imageSrc = 'https://i.ibb.co/1XfXq6S/image.jpg'
this.cells = [];
let length = Math.min(window.innerHeight, window.innerWidth);
this.width = length - 100;
this.height = length - 100;
this.setup();
this.gameStarted = false;
this.numberOfMoves = 0;
}
setup() {
for(let i = 0;i < this.dimension * this.dimension; i++) {
this.cells.push(new Cell(this, i));
}
}
shuffle() {
for (let i = this.cells.length -1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.cells[i], this.cells[j]] = [this.cells[j], this.cells[i]];
this.cells[i].setPosition(i);
this.cells[j].setPosition(j);
}
}
findEmptyCell() {
return this.cells.findIndex(cell => cell.isEmpty)
}
checkValidCells() {
const emptyCell = this.findEmptyCell(),
topCell = emptyCell - this.dimension,
leftCell = emptyCell - 1,
rightCell = emptyCell + 1,
bottomCell = emptyCell + this.dimension;
const mod = emptyCell % this.dimension;
let left, right, top, bottom;
if (mod != 0) {
left = leftCell;
}
if (mod != this.dimension -1) {
right = rightCell;
}
if (emptyCell >= this.dimension) {
top = topCell;
}
if (emptyCell < this.dimension * (this.dimension - 1)) {
bottom = bottomCell
}
return {right: right, left: left, top: top, bottom: bottom};
}
findPosition(index) {
return this.cells.findIndex(cell => cell.index === index);
}
checkWin() {
for (let i = 0; i < this.cells.length; i++) {
if (i != this.cells[i].index) {
return false;
}
}
return true;
}
clearDrag() {
this.cells.forEach(cell => {
if($(cell.cell).data('ui-draggable')) $(cell.cell).draggable("destroy");
})
}
dragTheTile() {
this.clearDrag();
const validCells = this.checkValidCells();
let availableCells = [];
if (validCells.right) {
availableCells.push(this.cells[this.findPosition(validCells.right)].index);
}
if (validCells.left || validCells.left === 0) {
availableCells.push(this.cells[this.findPosition(validCells.left)].index);
}
if (validCells.top || validCells.top === 0) {
availableCells.push(this.cells[this.findPosition(validCells.top)].index);
}
if (validCells.bottom) {
availableCells.push(this.cells[this.findPosition(validCells.bottom)].index);
}
let emptyCellIndex = this.findEmptyCell();
availableCells.forEach(cell => {
this.cells[cell].makeDraggable(cell);
this.cells[emptyCellIndex].makeDroppable(cell);
})
}
solve() {
let i;
for (i = 0; i < this.cells.length; i++) {
let j = this.cells[i].index;
if (i != j) {
[this.cells[i], this.cells[j]] = [this.cells[j], this.cells[i]];
this.cells[i].setPosition(i);
this.cells[j].setPosition(j);
i--;
}
if (i === this.cells.length - 1) {
[this.cells[i], this.cells[i - 1]] = [this.cells[i - 1], this.cells[i]];
this.cells[i].setPosition(i);
this.cells[i - 1].setPosition(i - 1);
}
}
}
}
$(document).ready(() => {
let gb;
$("#startGame").on("click", () => {
gb.gameStarted = true;
gb.shuffle();
gb.numberOfMoves = 0;
$("#moves").html("Moves: " + gb.numberOfMoves);
gb.clearDrag();
gb.dragTheTile();
})
$("#solve").on("click", () => {
if (gb.gameStarted) {
gb.solve();
gb.clearDrag();
gb.dragTheTile();
}
})
$("#generate-puzzle").on("click", () => {
let number = parseInt($("#dimension").val());
if(number >= 3 && Number.isInteger(number)) {
gb = new GameBoard(number);
$("#content").css("display", "block");
$("#solve").css("display", "block");
$("#startGame").css("display", "block");
$("#enter-dimension").css("display", "none");
$("#content").css({
width: gb.width + "px",
height : gb.height + "px"
})
} else {
$("#alert").css("display", "block");
}
})
})
body {
background-color: #3d3d3d;
}
#content {
width: 400px;
height : 400px;
background-color: #fff;
margin: auto;
position: relative;
display: none;
}
#startGame {
margin: auto;
display: none;
}
#solve {
margin: auto;
display: none;
}
#alert {
display: none;
}
#moves {
margin: auto;
padding: 5px;
text-align: center;
color: #FFFF00;
}
#enter-dimension {
margin: auto;
}
#label-dimension {
color: #ddd;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- JQuery -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<!-- Bootstrap -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js" integrity="sha512-uto9mlQzrs59VwILcLiRYeLKPPbS/bT71da/OEBYEwcdNUk8jYIy+D176RYoop1Da+f9mvkYrmj5MCLZWEtQuA==" crossorigin="anonymous"></script>
<title>Image Puzzle</title>
</head>
<body>
<div class="container col-md-6">
<div id="moves">
Moves : 0
</div>
<div class="d-flex justify-content-center row">
<div class="col-md-6">
<div class="mt-5" id="enter-dimension">
<form>
<div class="form-group">
<label id="label-dimension" for="dimension">Enter the Dimension of the puzzle</label>
<input type="number" class="form-control" id="dimension">
</div>
<div class="alert alert-danger" id="alert" role="alert">
Enter a Valid Positive Integer Greater than or Equal to 3
</div>
<button type="button" class="btn btn-primary" id="generate-puzzle">Generate the Puzzle</button>
</form>
</div>
</div>
</div>
<div class="row">
<!--The GRid Layout and the tiles -->
<div class="mt-3" id="content">
</div>
</div>
<!--Button to Start the Game -->
<div class="row buttons">
<button type="button" class="btn btn-info mt-2" id="startGame">Start Game</button>
<button type="button" class="btn btn-success mt-2" id="solve">Solve This!</button>
</div>
</div>
</body>
</html>
The puzzle works perfectly but with one issue. If I drag-drop a cell to an empty cell I am unable to drag and drop that tile again to its previous position which is the current empty cell. The way I see it there is a problem somewhere in makeDraggable() and dragTheTile() functions.
When I move a cell with a mouse click I can drag and drop that same cell to its previous position (which is the current empty cell) or If I drag and drop a cell to an empty space I can move it to its previous position (which is the current empty cell) with mouse click. But after dragging the same cell cannot be dragged again. looks like draggable() is disabled for that particular cell.
Once a cell is dragged, The next move it should be a draggable cell (since it is a neighbour cell to the empty cell) When I make it draggable the "ui-draggable" class is added to it but "ui-draggable-handle" class is missing.
The problem was with the function where I cleared the draggable property:
clearDrag() {
this.cells.forEach(cell => {
if($(cell.cell).data('ui-draggable')) $(cell.cell).draggable("destroy");
})
}
The "destroy" method gives some conflicts.
The alternative method I found was to first initialize all the cells with draggable property but disable it in the beginning and enabling it again when I need.
the property used here was "disabled: true" and "disabled: false".
Instead of the dragTheTile() function I wrote a new function called dragTile() as follows:
dragTile() {
this.cells.forEach(cell => {
cell.makeDraggable();
})
const validCells = this.checkValidCells();
let availableCells = [];
if (validCells.right) {
availableCells.push(this.cells[this.findPosition(validCells.right)].index);
}
if (validCells.left || validCells.left === 0) {
availableCells.push(this.cells[this.findPosition(validCells.left)].index);
}
if (validCells.top || validCells.top === 0) {
availableCells.push(this.cells[this.findPosition(validCells.top)].index);
}
if (validCells.bottom) {
availableCells.push(this.cells[this.findPosition(validCells.bottom)].index);
}
let emptyCellIndex = this.findEmptyCell();
availableCells.forEach(cell => {
$(this.cells[cell].cell).draggable({disabled: false});
})
this.cells[emptyCellIndex].makeDroppable(availableCells);
}
This solved the problem very easily.

Using nested setTimeout to create an animated selection sort

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>

Detect collision between two elements

I want to detect a collision between two divs and I've tried to do that using the offsetLeft and offsetTop of each one of the divs and by using getBoundingClientRect(). But none of these work.
Is there a way I can get the exact coordinates in which these two elements touch?
const bigDiv = document.querySelector(".big")
const blue = document.querySelector(".blue")
const startBtn = document.querySelector("button")
const red = document.querySelector(".red")
const bigDivDimensions = bigDiv.getBoundingClientRect();
let speed = 5;
//let counter = 0;
let bigDivLeft = bigDivDimensions.left;
startGame();
function random() {
return Math.floor(Math.random() * (bigDivDimensions.width - 40));
}
function startGame() {
//let randomSnake = Math.floor(Math.random()*(bigDivDimensions.width-40));
//let randomApple = Math.ceil(Math.random()*(bigDivDimensions.width -40));
blue.style.left = random() + "px"
blue.style.top = random() + "px"
red.style.left = random() + "px"
red.style.top = random() + "px"
}
function move(e) {
let blueX = blue.offsetLeft;
let blueY = blue.offsetTop;
let key = e.keyCode;
if (key !== 37 && key !== 38 && key !== 39 && key !== 40) {
return
} else if (key === 37 && blueX > 3) {
blueX -= speed;
} else if (key === 38 && blueY > 3) {
blueY -= speed;
} else if (key === 39 && blueX < (bigDivDimensions.width - 25)) {
blueX += speed;
} else if (key === 40 && blueY < (bigDivDimensions.height - 23)) {
blueY += speed;
}
blue.style.left = blueX + "px";
blue.style.top = blueY + "px";
colision(blueX, blueY)
}
function colision(blueX, blueY) {
/* let redX = red.offsetLeft;
let redY = red.offsetTop;
if(redY === blueY || redX == blueX){console.log("hit")} */
let redRect = red.getBoundingClientRect();
let blueRect = blue.getBoundingClientRect();
if ((redRect.top < blueRect.bottom) || (redRect.bottom < blueRect.top) || (redRect.right > blueRect.left) || (redRect.left < blueRect.right)) {
console.log("hit")
}
}
startBtn.addEventListener("click", startGame)
document.addEventListener("keydown", move)
.big {
width: 400px;
height: 400px;
outline: 1px solid red;
margin: 0 auto;
position: relative;
}
.blue {
width: 20px;
height: 20px;
background: blue;
position: absolute;
}
.red {
width: 20px;
height: 20px;
background: red;
position: absolute;
}
<button type="button">Start Game</button>
<div class="big">
<div class="blue"></div>
<div class="red"></div>
</div>
codepen
Two rectangles do collide when their
horizontal side and vertical side
overlap.
Consider the following answer: Checking for range overlap
// l1 (line 1) --> [x1, x2]
// l2 (line 2) --> [x1, x2]
function checkOverlap(l1, l2) {
return ((l1[1] - l2[0]) > 0 && (l2[1] - l1[0]) > 0) ? true : false;
}
Check collision with range overlap for the x-side and y-side:
// Rectangle a --> [[x1, x2], [y1, y2]]
// Rectangle b --> [[x1, x2], [y1, y2]]
function collide(a, b) {
return (checkOverlap(a[0], b[0]) && checkOverlap(a[1], b[1]));
}
Spoiler
SVG-Example
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
var width = 200;
var height = 200;
svg.setAttribute("width", width);
svg.setAttribute("height", height);
document.body.appendChild(svg);
function createRect(svg, width, height, x, y, style, id) {
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("width", width);
rect.setAttribute("height", height);
rect.setAttribute("x", x);
rect.setAttribute("y", y);
rect.setAttribute("style", style);
rect.setAttribute("id", id);
svg.appendChild(rect);
}
function createSquare(svg, side, x, y, style, id) {
createRect(svg, side, side, x, y, style, id);
}
function getRandomNumber(a, b) {
return Math.round(Math.random() * (b - a)) + a;
}
function createRandomSquare(svg, xRange, yRange, side, style, id) {
var x = getRandomNumber(xRange[0], xRange[1]);
var y = getRandomNumber(yRange[0], yRange[1]);
createSquare(svg, side, x, y, style, id);
return [
[x, x + side],
[y, y + side]
];
}
function checkOverlap(l1, l2) {
return ((l1[1] - l2[0]) > 0 && (l2[1] - l1[0]) > 0) ? true : false;
}
// [[x1, x2], [y1, y2]]
function collide(a, b) {
return (checkOverlap(a[0], b[0]) && checkOverlap(a[1], b[1]));
}
function getCoordinates(arr) {
return {
"top-left": [arr[0][0], arr[1][0]],
"top-right": [arr[0][1], arr[1][0]],
"bottom-left": [arr[0][1], arr[1][1]],
"bottom-right": [arr[0][0], arr[1][1]]
}
}
function run() {
var bs = document.getElementById("blueSquare");
var rs = document.getElementById("redSquare");
var output = document.getElementById("output");
output.innerHTML = "";
if (bs !== null && rs !== null) {
var parent = bs.parentNode;
parent.removeChild(bs);
parent.removeChild(rs);
}
var side = 30;
var blueSquare = createRandomSquare(svg, [0, (width - side)], [0, (height - side)], side, "fill:blue", "blueSquare");
var redSquare = createRandomSquare(svg, [0, (width - side)], [0, (height - side)], side, "fill:red", "redSquare");
var output1 = "Collision? " + collide(blueSquare, redSquare);
var output2 = "Blue square: " +
JSON.stringify(getCoordinates(blueSquare));
var output3 = "Red square: " + JSON.stringify(getCoordinates(redSquare));
output.innerHTML = output1 + "<br>" + output2 + "<br>" + output3 + "<br>";
}
var button = document.createElement("button");
button.setAttribute("id", "button");
button.textContent = "Run";
button.addEventListener("click", run);
document.body.appendChild(button);
svg {
border: 1px solid;
}
<div id="output"></div>

Categories

Resources