I am trying to create a minesweeper game using html css and js. I created the board, the fields(div elements), planted the bombs, every field "knows" his position (x,y) (the x and y coordinates are saved as data attributes of the div), "knows" if he is a bomb and the number of bombs nearby. I managed to create onclick event to reveal if the tile is a bomb or to show the number of bombs nearby. Now my problem is that if i click on a tile which has no bombs nearby (empty tile) i want to reveal all empty tiles which are in the surrounding.
So the next function is called if i click on an empty field:
function zero(x, y) {
for (var i = x - 1; i <= x + 1; i++) {
for (var j = y - 1; j <= y + 1; j++) {
$('.tile[row-d=' + i + '][col-d=' + j + ']').text($('.tile[row-d=' + i + '][col-d=' + j + ']').attr('bomb_number')); // set the field text = bomb -number nearby
}
}
}
With this i reveal all the surrounding fields near the clicked empty field, so i reveal just 9 fields. I want to reveal all empty fields nearby not just 9. I tried to use recursive function but the page crashed.
So anybody can help me with a recursive function which could solve this. Thank you. (As you can see i am also using jQuery)
The basic rule is:
If you find a neighboring field that is next to a bomb, stop searching
If a neighboring field has no bomb, keep expanding to the new field's neighbors
In code:
function zero(x, y) {
for (var i = x - 1; i <= x + 1; i++) {
for (var j = y - 1; j <= y + 1; j++) {
var neighbor = $('.tile[row-d=' + i + '][col-d=' + j + ']');
var bombNr = neighbor.attr('bomb_number');
neighbor.text(bombNr);
if (bombNr === "0") zero(i, j);
}
}
}
Edit: a quick & dirty example of the recursion pattern:
// The recursive function
const handleInput = i => {
if (tapped.has(i)) return;
// Count the bombs in neighboring tiles
const bombs = neighbors(i)
.map(j => board[j])
.filter(v => v === x)
.length;
// Store the count so we can render it
tapped.set(i, bombs);
// If there are no bombs, handle all the neighbors'
// as well.
if (bombs === 0) {
neighbors(i).forEach(handleInput);
}
};
// Game state
const x = "x";
const board = [
x, 0, 0, 0, 0,
x, 0, 0, 0, 0,
x, x, x, 0, 0,
0, 0, 0, 0, 0,
0, 0, x, 0, 0
];
const tapped = new Map();
// Quick & dirty render method to visualize what happens
const render = board => {
const el = document.querySelector(".board")
el.innerHTML =
board
.map((v, i) => tapped.has(i) ? tapped.get(i) : v)
.map(txt => `<div>${txt}</div>`)
.join("");
Array.from(el.children).forEach((c, i) => {
c.addEventListener("click", () => {
if (board[i] === x) return console.log("Boom!");
handleInput(i);
render(board);
});
c.classList.toggle("tapped", tapped.has(i));
});
}
const row = i => i / 5 << 0;
const col = i => i % 5;
const neighbors = i => {
const top = [ -6, -5, -4 ];
const right = [ -4, 1, 6 ];
const bottom = [ 4, 5, 6 ];
const left = [ -6, -1, 4 ];
const ds = new Set([...top, ...right, ...bottom, ...left]);
const remove = d => ds.delete(d);
if (row(i) === 0) top.forEach(remove);
if (row(i) === 4) bottom.forEach(remove);
if (col(i) === 0) left.forEach(remove);
if (col(i) === 4) right.forEach(remove);
return [...ds].map(d => d + i);
};
render(board);
.board {
width: 100px;
height: 100px;
}
.board > div {
width: 20px;
height: 20px;
border: 1px solid black;
box-sizing: border-box;
display: inline-block;
text-align: center;
}
.board > div.tapped {
background: yellow;
}
<div class="board"></div>
Related
I am trying to create a merge sort visualizer.
The merge sort works in the sense that if I give it an array, it'll spit back a correctly sorted array. The problem is that the "visualization" aspect of my program isn't quite working as I thought it would.
The way I attempted the implementation was that mergeSort() would take the two elements being compared, highlight them in red (no longer focusing on this for now to simplify problem), then swap the height if necessary, then turn the two elements back to the default colour:
const {
useState,
useEffect
} = React;
const numberOfBars = 8; //Number of bars shown on screen
const animationSpeed = 100; //How fast the bars animate
function SortingVisualizer() {
const [arr, setArr] = useState([]);
useEffect(() => {
newArray();
}, [])
function newArray() {
const tempArr = [];
for (let i = 0; i < numberOfBars; i++) {
tempArr.push(Math.floor(Math.random() * 100) + 5)
}
setArr(tempArr);
}
return (
<div id = "main-container" >
<button id = "new-array-button" onClick = {() => newArray()}> New Array </button>
<button id = "merge-sort-button" onClick = {() => mergeSort(arr)}> Merge Sort < /button>
<div id = "bar-container" >
{
arr.map((value, index) => (
<div className = 'bar'
key = {
index
}
style = {
{
backgroundColor: "aquamarine",
height: value + "px"
}
}
/>
))
}
</div>
</div >
)
}
// Recursively breaks down unsortedArray into lowIndex, midIndex, highIndex and pass to mergeSortHelper()
// Initialized aux, lowIndex and highIndex in argument.
function mergeSort(unsortedArray, aux = [...unsortedArray], lowIndex = 0, highIndex = unsortedArray.length - 1) {
// Base case
if (highIndex === lowIndex) return;
// Get midIndex
const midIndex = Math.floor((highIndex + lowIndex) / 2);
// Recursively run left side and right side until base case reached.
mergeSort(unsortedArray, aux, lowIndex, midIndex);
mergeSort(unsortedArray, aux, midIndex + 1, highIndex);
// Merge the left sides and right sides
mergeSortHelper(unsortedArray, aux, lowIndex, midIndex, highIndex);
}
// Does the work of sorting list and animating the height swap
function mergeSortHelper(unsortedArray, aux, lowIndex, midIndex, highIndex) {
let auxkey = lowIndex;
let i = lowIndex;
let j = midIndex + 1;
let barI = undefined;
let barJ = undefined;
// While there are elements in left/right sides, put element in auxillary array
// then increment the indexes by 1.
while (i <= midIndex && j <= highIndex) {
let arrayBars = document.getElementsByClassName('bar');
barI = arrayBars[i].style;
barJ = arrayBars[j].style;
if (unsortedArray[i] <= unsortedArray[j]) {
aux[auxkey] = unsortedArray[i];
auxkey++;
i++;
} else {
let tmpHeight = barJ.height; //swaps value at index i and j
barJ.height = barI.height; //swaps value at index i and j
barI.height = tmpHeight; //swaps value at index i and j
aux[auxkey] = unsortedArray[j];
auxkey++;
j++;
}
}
// Move any remaining elements from unsortedArray into auxillary array.
while (i <= midIndex) {
aux[auxkey] = unsortedArray[i];
auxkey++;
i++;
}
// Copy the auxillary array to the unsortedArray so that in the next iteration, the unsortedArray will reflect the new array
for (let k = 0; k <= highIndex; k++) {
unsortedArray[k] = aux[k];
}
}
ReactDOM.render( < SortingVisualizer / > , document.querySelector("#app"))
#new-array-button {
position: absolute;
}
#merge-sort-button {
position: absolute;
left: 100px;
}
#bar-container {
align-items: flex-end;
background-color: lightgray;
display: flex;
height: 200px;
justify-content: center;
}
.bar {
margin: 0 2px;
width: 20px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I've been trying to implement the color change and height swap in different parts of my program but it never seems to work correctly. Where am I going wrong with the visualization?
Removed my jsfiddle link from the question as I am using the on-site code snippet now. But in case anyone still wants it, it's here: https://jsfiddle.net/SushiCode/k0954yep/17/
EDIT: The instructions were really confusing for me.
"Step Seven: findSpotForCol and endGame
Right now, the game drops always drops a piece to the top of the column, even if a piece is already there. Fix this function so that it finds the lowest empty spot in the game board and returns the y coordinate (or null if the column is filled)."
This makes me think that findSpotForCol is supposed to put where each piece goes, however step 5 says:
"While not everything will work, you should now be able to click on a column and see a piece appear at the very bottom of that column."
So the piece should already be at the bottom on step 5, not waiting til step 7?
*****
I am doing an assignment creating a connect 4 game. There was some code already, and instructions on how to complete the rest. I, uh, don't think I followed the instructions to a "t" (as in certain code should go inside a certain function), but it all works as intended (mostly).
Every click is a different turn(changing the piece colors). Pieces stack how they're supposed to. If the board is completely full (as in tie), the game ends. Basically everything works in the game except for "ending the game" when 4 of the same pieces match.
The code for checking the win had already been written by my instructor. Is anyone able to check my code and advise how the check for win should be written for the code I have? I've spent quite a few hours on this, and rather not start over to write code that works for the check-for-win function pre-set by my instructor.
HTML
<!doctype html>
<head>
<title>Connect 4</title>
<link href="connect4.css" rel="stylesheet">
</head>
<body>
<div id="game">
<table id="board"></table>
</div>
<script src="connect4.js"></script>
</body>
</html>
CSS
/* game board table */
#board td {
width: 50px;
height: 50px;
border: solid 1px #666;
}
/* pieces are div within game table cells: draw as colored circles */
.piece {
/* TODO: make into circles */
margin: 5px;
width: 80%;
height: 80%;
border-radius: 50%;
}
/* TODO: make pieces red/blue, depending on player 1/2 piece */
.plyr1 {
background-color: blue;
}
.plyr2 {
background-color: red;
}
/* column-top is table row of clickable areas for each column */
#column-top td {
border: dashed 1px lightgray;
}
#column-top td:hover {
background-color: gold;
}
JS
/** Connect Four
*
* Player 1 and 2 alternate turns. On each turn, a piece is dropped down a
* column until a player gets four-in-a-row (horiz, vert, or diag) or until
* board fills (tie)
*/
const WIDTH = 7;
const HEIGHT = 6;
let currPlayer = 1; // active player: 1 or 2
const board = []; // array of rows, each row is array of cells (board[y][x])
/** makeBoard: create in-JS board structure:
* board = array of rows, each row is array of cells (board[y][x])
*/
function makeBoard() {
let board2 = [];
for (let i = 0; i < HEIGHT; i++) {
board2.push(null);
for (let j = 0; j < WIDTH; j++) {
board[i] = board2;
}
}
}
/** makeHtmlBoard: make HTML table and row of column tops. */
function makeHtmlBoard() {
const htmlBoard = document.querySelector('#board'); //selecting the board
const top = document.createElement('tr'); //creating a table row element
top.setAttribute('id', 'column-top'); //setting id of tr just created
top.addEventListener('click', handleClick); // adding an event listener that listens for handleClick( function)
for (let x = 0; x < WIDTH; x++) {
const headCell = document.createElement('td'); //creating a table data element equal to WIDTH
headCell.setAttribute('id', x); //setting id of td just created to x
top.append(headCell); //displaying headingCell right under where top is displayed (nesting the td inside of the tr)
}
htmlBoard.append(top); //displaying top right under where htmlBoard is displayed
for (let y = 0; y < HEIGHT; y++) {
const row = document.createElement('tr'); //creating a table row element * HEIGHT
for (let x = 0; x < WIDTH; x++) {
const cell = document.createElement('td'); //creating a table data element equal to WIDTH
cell.setAttribute('id', `${y}-${x}`); //setting id of td just created
row.append(cell); //displaying cell right under where row is displayed (nesting each td inside of each tr)
}
htmlBoard.append(row); //displaying row right under where htmlBoard is displayed
}
}
/** findSpotForCol: given column x, return top empty y (null if filled) */
function findSpotForCol(x) {
// TODO: write the real version of this, rather than always returning 0
return 0;
}
/** placeInTable: update DOM to place piece into HTML table of board */
function placeInTable(y, x) {
// TODO: make a div and insert into correct table cell
const div = document.createElement('div');
div.classList.add('piece');
const top = document.querySelector(`[id='0-${x}']`);
if (currPlayer === 1 && top.innerHTML === '') {
div.classList.add('plyr1');
currPlayer = 2;
} else if (currPlayer === 2 && top.innerHTML === '') {
div.classList.add('plyr2');
currPlayer = 1;
}
let arrHeight = [];
for (let i = 0; i < HEIGHT; i++) {
arrHeight.push(i);
}
for (i of arrHeight.reverse()) {
if (document.getElementById(`${i}-${x}`).innerHTML === '') {
const selected = document.getElementById(`${i}-${x}`);
const top = document.getElementById(`6 -${x}`);
return selected.append(div);
}
}
}
/** endGame: announce game end */
function endGame(msg) {
// TODO: pop up alert message
alert('Test');
}
/** handleClick: handle click of column top to play piece */
function handleClick(evt) {
// get x from ID of clicked cell
let x = +evt.target.id;
// get next spot in column (if none, ignore click)
let y = findSpotForCol(x);
if (y === null) {
return;
}
// place piece in board and add to HTML table
// TODO: add line to update in-memory board
placeInTable(y, x);
// check for win
if (checkForWin()) {
return endGame(`Player ${currPlayer} won!`);
}
// check for tie
// TODO: check if all cells in board are filled; if so call, call endGame
const tie = document.querySelectorAll('td');
const tieArr = [ ...tie ];
tieArr.reverse();
for (let i = 0; i < WIDTH; i++) {
tieArr.pop();
}
let tie42 = tieArr.filter((v) => {
return v.innerHTML !== '';
});
if (tie42.length === 42) {
setTimeout(() => {
endGame();
}, 1);
}
}
/** checkForWin: check board cell-by-cell for "does a win start here?" */
function checkForWin() {
function _win(cells) {
// Check four cells to see if they're all color of current player
// - cells: list of four (y, x) cells
// - returns true if all are legal coordinates & all match currPlayer
return cells.every(([ y, x ]) => y >= 0 && y < HEIGHT && x >= 0 && x < WIDTH && board[y][x] === currPlayer);
}
// TODO: read and understand this code. Add comments to help you.
for (let y = 0; y < HEIGHT; y++) {
for (let x = 0; x < WIDTH; x++) {
let horiz = [ [ y, x ], [ y, x + 1 ], [ y, x + 2 ], [ y, x + 3 ] ];
let vert = [ [ y, x ], [ y + 1, x ], [ y + 2, x ], [ y + 3, x ] ];
let diagDR = [ [ y, x ], [ y + 1, x + 1 ], [ y + 2, x + 2 ], [ y + 3, x + 3 ] ];
let diagDL = [ [ y, x ], [ y + 1, x - 1 ], [ y + 2, x - 2 ], [ y + 3, x - 3 ] ];
if (_win(horiz) || _win(vert) || _win(diagDR) || _win(diagDL)) {
return true;
}
}
}
}
makeBoard();
makeHtmlBoard();
There are a few essential things you did not code:
// TODO: add line to update in-memory board
// TODO: write the real version of this, rather than always returning 0
Without those implementations you cannot expect the 4-in-a-row detection to work.
But let's take the issues one by one, starting at the top:
(1) The way you create the board is wrong: it only creates one board2 array, and so all the entries of board are referencing the same array. Whatever happens to it will be the same whether you look at it via board[0], board[1], or whichever entry... Here is how you could do it correctly:
function makeBoard() {
for (let i = 0; i < HEIGHT; i++) {
board[i] = Array(WIDTH).fill(null); // create a new array each time!
}
}
(2) The function findSpotForCol always returns 0, which of course is wrong. It is a pity you did not attempt to complete this function. The purpose of such assignments is that you work on them, and learn as you try.
It could look like this:
function findSpotForCol(x) {
for (let y = 0; y < HEIGHT; y++) {
if (board[y][x] === null) return y;
}
return null;
}
(3) placeInTable should not change currPlayer. It is not its job to do so. Changing it here will give it the wrong value at the time you call checkForWin. Instead you should modify currPlayer as the last thing you do in handleClick, and it can be simply this line:
currPlayer = 3 - currPlayer;
(3b) placeInTable is overly complicated. Although it works fine, I would write it like below, making use of the rows and cells methods; no need for HTML id attributes, except for the id of the table element.
function placeInTable(y, x) {
const div = document.createElement('div');
div.classList.add('piece', 'plyr' + currPlayer);
document.querySelector("#board").rows[HEIGHT - y].cells[x].append(div);
}
(4) The endGame shows "Test", which is not its purpose. Again, you should have updated that code. It should alert this:
alert(msg);
(5) In handleClick you did not do anything to modify the board. You should add this line just before the call to placeInTable:
board[y][x] = currPlayer;
(6) The check for a tie should not be based on what is in the document, but on what is in memory. A move counter would make it easier, but let's just do it by scanning board. You should of course pass an argument to endGame:
if (board.every(row => row.every(Boolean))) {
return endGame("It's a draw!");
}
Note the use of Boolean here. It is the callback function given to every, and will return false when it is given null, true when it is given 1 or 2.
With these adaptations it should work.
Just judging from a quick glance on your code: There is an in-memory table called board, which the check for a win is using. However, your placeInTable implementation does not update this table, but directly inserts DOM-elements.
What I would advise you to do instead, is to sepearte the model (the state of the game) from the visualization. When you get the click-event, you update the in-memory table, then you visualize the entire table, e.g. by setting the appropriate css-classes in the already created html-table.
Then you check for a win.
Also, the check for winning compares against a variable called test, which does not seem to be defined anywhere.
So I am looking to create look up tables. However I am running into a problem with integer ranges instead of just 1, 2, 3, etc. Here is what I have:
var ancient = 1;
var legendary = 19;
var epic = 251;
var rare = 1000;
var uncommon = 25000;
var common = 74629;
var poolTotal = ancient + legendary + epic + rare + uncommon + common;
var pool = general.rand(1, poolTotal);
var lootPool = {
1: function () {
return console.log("Ancient");
},
2-19: function () {
}
};
Of course I know 2-19 isn't going to work, but I've tried other things like [2-19] etc etc.
Okay, so more information:
When I call: lootPool[pool](); It will select a integer between 1 and poolTotal Depending on if it is 1 it will log it in the console as ancient. If it hits in the range of 2 through 19 it would be legendary. So on and so forth following my numbers.
EDIT: I am well aware I can easily do this with a switch, but I would like to try it this way.
Rather than making a huge lookup table (which is quite possible, but very inelegant), I'd suggest making a (small) object, choosing a random number, and then finding the first entry in the object whose value is greater than the random number:
// baseLootWeight: weights are proportional to each other
const baseLootWeight = {
ancient: 1,
legendary: 19,
epic: 251,
rare: 1000,
uncommon: 25000,
common: 74629,
};
let totalWeightSoFar = 0;
// lootWeight: weights are proportional to the total weight
const lootWeight = Object.entries(baseLootWeight).map(([rarity, weight]) => {
totalWeightSoFar += weight;
return { rarity, weight: totalWeightSoFar };
});
console.log(lootWeight);
const randomType = () => {
const rand = Math.floor(Math.random() * totalWeightSoFar);
return lootWeight
.find(({ rarity, weight }) => weight >= rand)
.rarity;
};
for (let i = 0; i < 10; i++) console.log(randomType());
Its not a lookup, but this might help you.
let loots = {
"Ancient": 1,
"Epic": 251,
"Legendary": 19
};
//We need loots sorted by value of lootType
function prepareSteps(loots) {
let steps = Object.entries(loots).map((val) => {return {"lootType": val[0], "lootVal": val[1]}});
steps.sort((a, b) => a.lootVal > b.lootVal);
return steps;
}
function getMyLoot(steps, val) {
let myLootRange;
for (var i = 0; i < steps.length; i++) {
if((i === 0 && val < steps[0].lootVal) || val === steps[i].lootVal) {
myLootRange = steps[i];
break;
}
else if( i + 1 < steps.length && val > steps[i].lootVal && val < steps[i + 1].lootVal) {
myLootRange = steps[i + 1];
break;
}
}
myLootRange && myLootRange['lootType'] ? console.log(myLootRange['lootType']) : console.log('Off Upper Limit!');
}
let steps = prepareSteps(loots);
let pool = 0;
getMyLoot(steps, pool);
Building a basic Perceptron. My results after training are very inconsistent, even after 1000's of epochs. The weights seem to adjust properly, however the model fails to accurately predict. A second pairs of eyes on the structure would be greatly appreciated, struggling to find where I went wrong. The accuracy consistently tops out at 60%.
// Perceptron
class Perceptron {
constructor (x_train, y_train, learn_rate= 0.1, epochs=10) {
this.epochs = epochs
this.x_train = x_train
this.y_train = y_train
this.learn_rate = learn_rate
this.weights = new Array(x_train[0].length)
// initialize random weights
for ( let n = 0; n < x_train[0].length; n++ ) {
this.weights[n] = this.random()
}
}
// generate random float between -1 and 1 (for generating weights)
random () {
return Math.random() * 2 - 1
}
// activation function
activation (n) {
return n < 0 ? 0 : 1
}
// y-hat output given an input tensor
predict (input) {
let total = 0
this.weights.forEach((w, index) => { total += input[index] * w }) // multiply each weight by each input vector value
return this.activation(total)
}
// training perceptron on data
fit () {
for ( let e = 0; e < this.epochs; e++) { // epochs loop
for ( let i = 0; i < this.x_train.length; i++ ) { // iterate over each training sample
let prediction = this.predict(this.x_train[i]) // predict sample output
console.log('Expected: ' + this.y_train[i] + ' Model Output: ' + prediction) // log expected vs predicted
let loss = this.y_train[i] - prediction // calculate loss
for ( let w = 0; w < this.weights.length; w++ ) { // loop weights for update
this.weights[w] += loss * this.x_train[i][w] * this.learn_rate // update all weights to reduce loss
}
}
}
}
}
x = [[1, 1, 1], [0, 0, 0], [0, 0, 1], [1, 1, 0], [0, 0, 1]]
y = [1, 0, 0, 1, 0]
p = new Perceptron(x, y, epochs=5000, learn_rate=.1)
Updated:
// Perceptron
module.exports = class Perceptron {
constructor (x_train, y_train, epochs=1000, learn_rate= 0.1) {
// used to generate percent accuracy
this.accuracy = 0
this.samples = 0
this.x_train = x_train
this.y_train = y_train
this.epochs = epochs
this.learn_rate = learn_rate
this.weights = new Array(x_train[0].length)
this.bias = 0
// initialize random weights
for ( let n = 0; n < x_train[0].length; n++ ) {
this.weights[n] = this.random()
}
}
// returns percent accuracy
current_accuracy () {
return this.accuracy/this.samples
}
// generate random float between -1 and 1 (for generating weights)
random () {
return Math.random() * 2 - 1
}
// activation function
activation (n) {
return n < 0 ? 0 : 1
}
// y-hat output given an input tensor
predict (input) {
let total = this.bias
this.weights.forEach((w, index) => { total += input[index] * w }) // multiply each weight by each input vector value
return this.activation(total)
}
// training perceptron on data
fit () {
// epochs loop
for ( let e = 0; e < this.epochs; e++) {
// for each training sample
for ( let i = 0; i < this.x_train.length; i++ ) {
// get prediction
let prediction = this.predict(this.x_train[i])
console.log('Expected: ' + this.y_train[i] + ' Model Output: ' + prediction)
// update accuracy measures
this.y_train[i] === prediction ? this.accuracy += 1 : this.accuracy -= 1
this.samples++
// calculate loss
let loss = this.y_train[i] - prediction
// update all weights
for ( let w = 0; w < this.weights.length; w++ ) {
this.weights[w] += loss * this.x_train[i][w] * this.learn_rate
}
this.bias += loss * this.learn_rate
}
// accuracy post epoch
console.log(this.current_accuracy())
}
}
}
It's just a syntactic error :)
Switch the order of the last two parameters, like this:
p = new Perceptron(x, y, learn_rate=.1, epochs=5000)
And now everything should work fine.
However, a more serious problem lies in your implementation:
You forgot the bias
With a perceptron you're trying to learn a linear function, something of the form of
y = wx + b
but what you're currently computing is just
y = wx
This is fine if what you're trying to learn is just the identity function of a single input, like in your case. But it fails to work as soon as you start doing something slightly more complex like trying to learn the AND function, which can be represented like this:
y = x1 + x2 - 1.5
How to fix?
Really easy, just initialise this.bias = 0 in the constructor. Then, in predict(), you initialise let total = this.bias and, in fit(), add this.bias += loss * this.learn_rate right after the inner-most loop.
I've just implemented a topological sort algorithm on my isometric game using this guide: https://mazebert.com/2013/04/18/isometric-depth-sorting/
The issue
Here's a little example (this is just a drawing to illustrate my problem because as we say, a picture is worth a thousand words), what I'm expecting is in left and the result of the topological sorting algorithm is in right
So in the right image, the problem is that the box is drawn BEFORE the character and I'm expecting it to be drawn AFTER like in the left image.
Code of the topological sorting algorithm (Typescript)
private TopologicalSort2() {
// https://mazebert.com/2013/04/18/isometric-depth-sorting/
for(var i = 0; i < this.Stage.children.length; i++) {
var a = this.Stage.children[i];
var behindIndex = 0;
for(var j = 0; j < this.Stage.children.length; j++) {
if(i == j) {
continue;
}
var b = this.Stage.children[j];
if(!a.isoSpritesBehind) {
a.isoSpritesBehind = [];
}
if(!b.isoSpritesBehind) {
b.isoSpritesBehind = [];
}
if(b.posX < a.posX + a.sizeX && b.posY < a.posY + a.sizeY && b.posZ < a.posZ + a.sizeZ) {
a.isoSpritesBehind[behindIndex++] = b;
}
}
a.isoVisitedFlag = 0;
}
var _sortDepth = 0;
for(var i = 0; i < this.Stage.children.length; ++i) {
visitNode(this.Stage.children[i]);
}
function visitNode(n: PIXI.DisplayObject) {
if(n.isoVisitedFlag == 0) {
n.isoVisitedFlag = 1;
if(!n.isoSpritesBehind) {
return;
}
for(var i = 0; i < n.isoSpritesBehind.length; i++) {
if(n.isoSpritesBehind[i] == null) {
break;
} else {
visitNode(n.isoSpritesBehind[i]);
n.isoSpritesBehind[i] = null;
}
}
n.isoDepth = _sortDepth++;
}
}
this.Stage.children.sort((a, b) => {
if(a.isoDepth - b.isoDepth != 0) {
return a.isoDepth - b.isoDepth;
}
return 0;
});
}
Informations
Player:
posX: [the x coordinate of the player]
posY: [the y coordinate of the player]
posZ: 0
sizeX: 1
sizeY: 1
sizeZ: 1
Box:
posX: [the x coordinate of the box]
posY: [the y coordinate of the box]
posZ: 0
sizeX: 3
sizeY: 1
sizeZ: 1
X and Y axis
Do you have any idea of the source of this problem? and maybe how to solve it?
The way to determine whether one object is before the other requires a bit more linear algebra.
First of all, I would suggest to translate the coordinates from the "world" coordinates to the "view" 2D coordinates, i.e. to the rows and columns of the display.
Note also that the original Z coordinate does not influence the sort order (imagine that an object would be lifted up along the Z axis: we can find a sort order where this move would not have any impact). So the above-mentioned translation could assume all points are at Z=0.
Let's take this set-up, but depicted from "above", so when looking along the Z axis down to the game floor:
In the picture there are 7 objects, numbered from 0 to 6. The line of view in the game would be from the bottom-left of this picture. The coordinate system in which I would suggest to translate some points is depicted with the red row/col axis.
The white diagonals in each object link the two points that would be translated and used in the algorithm. The assumption is that when one object is in front of another, their diagonal lines will not intersect. If they would, it would mean that objects are overlapping each other in the game world, which would mean they are like gasses, not solids :) I will assume this is not the case.
One object A could be in front of another object B when in the new coordinate system, the left-most column coordinate of B falls between the two column coordinates of A (or vice versa). There might not really be such an overlap when their Z coordinates differ enough, but we can ignore that, because when there is no overlap we can do no harm in specifying a certain order anyway.
Now, when the coordinates indicate an overlap, the coordinates of diagonals (of A and B) must be compared with some linear algebra formula, which will determine which one is in front of the other.
Here is your adapted function that does that:
topologicalSort() {
// Exit if sorting is a non-operation
if (this.Stage.children.length < 2) return;
// Add two translated coordinates, where each of the resulting
// coordinates has a row (top to bottom) and column
// (left to right) part. They represent a position in the final
// rendered view (the screen).
// The two pairs of coordinates are translations of the
// points (posX + sizeX, Y, 0) and (posX, posY + sizeY, 0).
// Z is ignored (0), since it does not influence the order.
for (let obj of this.Stage.children) {
obj.leftCol = obj.posY - obj.posX - obj.sizeX;
obj.rightCol = obj.posY - obj.posX + obj.sizeY;
obj.leftRow = obj.posY + obj.posX + obj.sizeX;
obj.rightRow = obj.posY + obj.posX + obj.sizeY;
obj.isoSpritesBehind = [];
}
for(let i = 0; i < this.Stage.children.length; i++) {
let a = this.Stage.children[i];
// Only loop over the next objects
for(let j = i + 1; j < this.Stage.children.length; j++) {
let b = this.Stage.children[j];
// Get the two objects in order of left column:
let c = b.leftCol < a.leftCol ? b : a;
let d = b.leftCol < a.leftCol ? a : b;
// See if they overlap in the view (ignoring Z):
if (d.leftCol < c.rightCol) {
// Determine which is behind: some linear algebra
if (d.leftRow <
(d.leftCol - c.leftCol)/(c.rightCol - c.leftCol)
* (c.rightRow - c.leftRow) + c.leftRow) {
// c is in front of d
c.isoSpritesBehind.push(d);
} else { // d is in front of c
d.isoSpritesBehind.push(c);
}
} // in the else-case it does not matter which one comes first
}
}
// This replaces your visitNode function and call:
this.Stage.children.forEach(function getDepth(obj) {
// If depth was already assigned, this node was already visited
if (!obj.isoDepth) {
// Get depths recursively, and retain the maximum of those.
// Add one more to get the depth for the current object
obj.isoDepth = obj.isoSpritesBehind.length
? 1+Math.max(...obj.isoSpritesBehind.map(getDepth))
: 1; // Depth when there is nothing behind it
}
return obj.isoDepth; // Return it for easier recursion
});
// Sort like you did, but in shorter syntax
this.Stage.children.sort((a, b) => a.isoDepth - b.isoDepth);
}
I add a snippet where I completed the class with a minimum of code, enough to make it run and output the final order in terms of object index numbers (as they were originally inserted):
class Game {
constructor() {
this.Stage = { children: [] };
}
addObject(posX, posY, posZ, sizeX, sizeY, sizeZ) {
this.Stage.children.push({posX, posY, posZ, sizeX, sizeY, sizeZ,
id: this.Stage.children.length}); // add a unique id
}
topologicalSort() {
// Exit if sorting is a non-operation
if (this.Stage.children.length < 2) return;
// Add two translated coordinates, where each of the resulting
// coordinates has a row (top to bottom) and column
// (left to right) part. They represent a position in the final
// rendered view (the screen).
// The two pairs of coordinates are translations of the
// points (posX + sizeX, Y, 0) and (posX, posY + sizeY, 0).
// Z is ignored (0), since it does not influence the order.
for (let obj of this.Stage.children) {
obj.leftCol = obj.posY - obj.posX - obj.sizeX;
obj.rightCol = obj.posY - obj.posX + obj.sizeY;
obj.leftRow = obj.posY + obj.posX + obj.sizeX;
obj.rightRow = obj.posY + obj.posX + obj.sizeY;
obj.isoSpritesBehind = [];
}
for(let i = 0; i < this.Stage.children.length; i++) {
let a = this.Stage.children[i];
// Only loop over the next objects
for(let j = i + 1; j < this.Stage.children.length; j++) {
let b = this.Stage.children[j];
// Get the two objects in order of left column:
let c = b.leftCol < a.leftCol ? b : a;
let d = b.leftCol < a.leftCol ? a : b;
// See if they overlap in the view (ignoring Z):
if (d.leftCol < c.rightCol) {
// Determine which is behind: some linear algebra
if (d.leftRow <
(d.leftCol - c.leftCol)/(c.rightCol - c.leftCol)
* (c.rightRow - c.leftRow) + c.leftRow) {
// c is in front of d
c.isoSpritesBehind.push(d);
} else { // d is in front of c
d.isoSpritesBehind.push(c);
}
} // in the else-case it does not matter which one comes first
}
}
// This replaces your visitNode function and call:
this.Stage.children.forEach(function getDepth(obj) {
// If depth was already assigned, this node was already visited
if (!obj.isoDepth) {
// Get depths recursively, and retain the maximum of those.
// Add one more to get the depth for the current object
obj.isoDepth = obj.isoSpritesBehind.length
? 1+Math.max(...obj.isoSpritesBehind.map(getDepth))
: 1; // Depth when there is nothing behind it
}
return obj.isoDepth; // Return it for easier recursion
});
// Sort like you did, but in shorter syntax
this.Stage.children.sort((a, b) => a.isoDepth - b.isoDepth);
}
toString() { // Just print the ids of the children
return JSON.stringify(this.Stage.children.map( x => x.id ));
}
}
const game = new Game();
game.addObject( 2, 2, 0, 1, 1, 1 );
game.addObject( 1, 3, 0, 3, 1, 1 );
game.addObject( 6, 1, 0, 1, 3, 1 );
game.addObject( 9, 3, 0, 1, 1, 1 );
game.addObject( 5, 3, 0, 1, 3, 1 );
game.addObject( 7, 2, 0, 1, 1, 1 );
game.addObject( 8, 2, 0, 3, 1, 1 );
game.topologicalSort();
console.log(game + '');
The objects in the snippet are the same as in the picture with the same numbers. The output order is [0,1,4,2,5,6,3] which is the valid sequence for drawing the objects.