A*(A-star) algorithm gives wrong path and crashes - javascript

I'm implementing A*(A-star) algorithm in react.js but my program crashes whenever startNode(green) or destinationNode(blue) have more than one neighbour or if there is a cycle in the graph. There is something wrong when adding and deleting the neighbours from/to openList or when updating the parentId in the getPath() function. I cant even see the console because the website goes down.
Each node has: id, name, x, y, connectedToIdsList:[], gcost:Infinity, fcost:0, heuristic:0, parentId:null.
I'm sending the path to another component "TodoList" which prints out the path. My program should not return the path but keeps updating the path as i'm adding nodes and edges to the list. Please help, I've been stuck for hours now:/
My code:
export default class TurnByTurnComponent extends React.PureComponent {
constructor(props) {
super(props);
this.state = { shortestPath: [] }
}
render() {
const {
destinationLocationId,
locations,
originLocationId
} = this.props;
let path = []
if (destinationLocationId != null && originLocationId != null) {
if (originLocationId == destinationLocationId) { //check if the startNode node is the end node
return originLocationId;
}
var openList = [];
let startNode = getNodeById(originLocationId);
let destinationNode = getNodeById(destinationLocationId)
if (startNode.connectedToIds.length > 0 && destinationNode.connectedToIds.length > 0) { //check if start and destination nodes are connected first
startNode.gcost = 0
startNode.heuristic = manhattanDistance(startNode, destinationNode)
startNode.fcost = startNode.gcost + startNode.heuristic;
//perform A*
openList.push(startNode); //starting with the startNode
while (openList.length > 0) {
console.log("inside while")
var currentNode = getNodeOfMinFscore(openList); //get the node of the minimum f cost of all nodes in the openList
if (currentIsEqualDistanation(currentNode)) {
path = getPath(currentNode);
}
deleteCurrentFromOpenList(currentNode, openList);
for (let neighbourId of currentNode.connectedToIds) {
var neighbourNode = getNodeById(neighbourId);
currentNode.gcost = currentNode.gcost + manhattanDistance(currentNode, neighbourNode);
if (currentNode.gcost < neighbourNode.gcost) {
neighbourNode.parentId = currentNode.id; // keep track of the path
// total cost saved in neighbour.g
neighbourNode.gcost = currentNode.gcost;
neighbourNode.heuristic = manhattanDistance(neighbourNode, destinationNode);
neighbourNode.fcost = neighbourNode.gcost + neighbourNode.heuristic; //calculate f cost of the neighbourNode
addNeighbourNodeToOpenList(neighbourNode, openList);
}
}
}
path = path.reverse().join("->");
}
}
function addNeighbourNodeToOpenList(neighbourNode, openList) {
//add neighbourNode to the open list to be discovered later
if (!openList.includes(neighbourNode)) {
openList.push(neighbourNode);
}
}
function deleteCurrentFromOpenList(currNode, openList) {
const currIndex = openList.indexOf(currNode);
openList.splice(currIndex, 1); //deleting currentNode from openList
}
function currentIsEqualDistanation(currNode) {
//check if we reached out the distanation node
return (currNode.id == destinationLocationId)
}
function getNodeById(nid) {
var node;
for (let i = 0; i < locations.length; i++) {
if (locations[i].id == nid) {
node = locations[i]
}
}
return node
}
function getPath(destNode) {
console.log("inside getpath")
var parentPath = []
var parent;
while (destNode.parentId != null) {
parentPath.push(destNode.name)
parent = destNode.parentId;
destNode = getNodeById(parent);
}
//adding startNode to the path
parentPath.push(getNodeById(originLocationId).name)
return parentPath;
}
function getNodeOfMinFscore(openList) {
var minFscore = openList[0].fcost; //initValue
var nodeOfminFscore;
for (let i = 0; i < openList.length; i++) {
if (openList[i].fcost <= minFscore) {
minFscore = openList[i].fcost //minFvalue
nodeOfminFscore = openList[i]
}
}
return nodeOfminFscore
}
//manhattan distance is for heuristic and gScore. Here I use Manhattan instead of Euclidean
//because in this example we dont have diagnosal path.
function manhattanDistance(stNode, dstNode) {
var x = Math.abs(dstNode.x - stNode.x);
var y = Math.abs(dstNode.y - stNode.y);
var dist = x + y;
return dist;
}
return (
<div className="turn-by-turn-component">
<TodoList
list={"Shortest path: ", [path]}
/>
<TodoList
list={[]}
/>
</div>
);
}
}
TurnByTurnComponent.propTypes = {
destinationLocationId: PropTypes.number,
locations: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
connectedToIds: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired
})),
originLocationId: PropTypes.number
};
Update new issue
Pictures of before and after linking a new node. When I update the graph and add a new node the path disappears. And sometimes come back and so on if I still add new nodes and edges to the graph. As I said, each node has: id, name, x, y, connectedToIdsList:[], gcost:Infinity, fcost:0, heuristic:0, parentId:null.
My new code now is:
export default class TurnByTurnComponent extends React.PureComponent {
constructor(props) {
super(props);
this.state = { shortestPath: [] }
}
render() {
const {
destinationLocationId,
locations,
originLocationId
} = this.props;
let path = []
if (destinationLocationId != null && originLocationId != null) {
console.log(JSON.stringify(locations))
path = [getNodeById(originLocationId).namne];
if (originLocationId != destinationLocationId) {
var openList = [];
let startNode = getNodeById(originLocationId);
let destinationNode = getNodeById(destinationLocationId)
if (startNode.connectedToIds.length > 0 && destinationNode.connectedToIds.length > 0) {
startNode.gcost = 0
startNode.heuristic = manhattanDistance(startNode, destinationNode)
startNode.fcost = startNode.gcost + startNode.heuristic;
openList.push(startNode); //starting with the startNode
while (openList.length > 0) {
var currentNode = getNodeOfMinFscore(openList); //get the node of the minimum f cost of all nodes in the openList
if (currentIsEqualDistanation(currentNode)) {
path = getPath(currentNode);
break;
}
deleteCurrentFromOpenList(currentNode, openList);
for (let neighbourId of currentNode.connectedToIds) {
var neighbourNode = getNodeById(neighbourId);
let gcost = currentNode.gcost + manhattanDistance(currentNode, neighbourNode);
if (gcost < (neighbourNode.gcost ?? Infinity)) {
neighbourNode.parentId = currentNode.id;
// keep track of the path
// total cost saved in neighbour.g
neighbourNode.gcost = gcost;
neighbourNode.heuristic = manhattanDistance(neighbourNode, destinationNode);
neighbourNode.fcost = neighbourNode.gcost + neighbourNode.heuristic; //calculate f cost of the neighbourNode
addNeighbourNodeToOpenList(neighbourNode, openList);
}
}
}
}
}
}
path = path.reverse().join("->");
function addNeighbourNodeToOpenList(neighbourNode, openList) {
//add neighbourNode to the open list to be discovered later
if (!openList.includes(neighbourNode)) {
openList.push(neighbourNode);
}
}
function deleteCurrentFromOpenList(currentNode, openList) {
const currIndex = openList.indexOf(currentNode);
openList.splice(currIndex, 1); //deleting currentNode from openList
}
function currentIsEqualDistanation(currentNode) {
//check if we reached out the distanation node
return (currentNode.id == destinationLocationId)
}
function getNodeById(id) {
var node;
for (let i = 0; i < locations.length; i++) {
if (locations[i].id == id) {
node = locations[i]
}
}
return node
}
function getPath(destinationNode) {
console.log("inside getpath")
var parentPath = []
var parent;
while (destinationNode.parentId != null) {
parentPath.push(destinationNode.name)
parent = destinationNode.parentId;
destinationNode = getNodeById(parent);
}
//adding startNode to the path
parentPath.push(getNodeById(originLocationId).name)
return parentPath;
}
function getNodeOfMinFscore(openList) {
var minFscore = openList[0].fcost; //initValue
var nodeOfminFscore;
for (let i = 0; i < openList.length; i++) {
if (openList[i].fcost <= minFscore) {
minFscore = openList[i].fcost //minFvalue
nodeOfminFscore = openList[i]
}
}
return nodeOfminFscore
}
//manhattan distance is for heuristic and gScore. Here I use Manhattan instead of Euclidean
//because in this example we dont have diagnosal path.
function manhattanDistance(startNode, destinationNode) {
var x = Math.abs(destinationNode.x - startNode.x);
var y = Math.abs(destinationNode.y - startNode.y);
var dist = x + y;
return dist;
}
return (
<div className="turn-by-turn-component">
<TodoList
title="Mandatory work"
list={[path]}
/>
<TodoList
title="Optional work"
list={[]}
/>
</div>
);
}
}
TurnByTurnComponent.propTypes = {
destinationLocationId: PropTypes.number,
locations: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired,
connectedToIds: PropTypes.arrayOf(PropTypes.number.isRequired).isRequired
})),
originLocationId: PropTypes.number
};

There are a few issues in your code:
return originLocationId; should not happen. When the source is equal to the target, then set the path, and make sure the final return of this function is executed, as you want to return the div element.
When the target is found in the loop, not only should the path be built, but the loop should be exited. So add a break
currentNode.gcost = currentNode.gcost + manhattanDistance(currentNode, neighbourNode); is not right: you don't want to modify currentNode.gcost: its value should not depend on the outgoing edge to that neighbour. Instead store this sum in a temporary variable (like gcost)
The comparison with neighbourNode.gcost will not work when that node does not yet have a gcost member. I don't see in your code that it got a default value that makes sure this condition is true, so you should use a default value here, like (neighbourNode.gcost ?? Infinity)
path = path.reverse().join("->"); should better be executed always, also when there is no solution (so path is a string) or when the source and target are the same node.
Here is a corrected version, slightly adapted to run here as a runnable snippet:
function render() {
const {
destinationLocationId,
locations,
originLocationId
} = this.props;
let path = [];
if (destinationLocationId != null && originLocationId != null) {
path = [originLocationId]; // The value for when the next if condition is not true
if (originLocationId != destinationLocationId) {
var openList = [];
let startNode = getNodeById(originLocationId);
let destinationNode = getNodeById(destinationLocationId)
if (startNode.connectedToIds.length > 0 && destinationNode.connectedToIds.length > 0) {
startNode.gcost = 0
startNode.heuristic = manhattanDistance(startNode, destinationNode)
startNode.fcost = startNode.gcost + startNode.heuristic;
openList.push(startNode);
while (openList.length > 0) {
var currentNode = getNodeOfMinFscore(openList);
if (currentIsEqualDistanation(currentNode)) {
path = getPath(currentNode);
break; // Should end the search here!
}
deleteCurrentFromOpenList(currentNode, openList);
for (let neighbourId of currentNode.connectedToIds) {
var neighbourNode = getNodeById(neighbourId);
// Should not modify the current node's gcost. Use a variable instead:
let gcost = currentNode.gcost + manhattanDistance(currentNode, neighbourNode);
// Condition should also work when neighbour has no gcost yet:
if (gcost < (neighbourNode.gcost ?? Infinity)) {
neighbourNode.parentId = currentNode.id;
neighbourNode.gcost = gcost; // Use the variable
neighbourNode.heuristic = manhattanDistance(neighbourNode, destinationNode);
neighbourNode.fcost = neighbourNode.gcost + neighbourNode.heuristic;
addNeighbourNodeToOpenList(neighbourNode, openList);
}
}
}
}
}
}
// Convert the path to string in ALL cases:
path = path.reverse().join("->");
function addNeighbourNodeToOpenList(neighbourNode, openList) {
if (!openList.includes(neighbourNode)) {
openList.push(neighbourNode);
}
}
function deleteCurrentFromOpenList(currNode, openList) {
const currIndex = openList.indexOf(currNode);
openList.splice(currIndex, 1);
}
function currentIsEqualDistanation(currNode) {
return (currNode.id == destinationLocationId)
}
function getNodeById(nid) {
var node;
for (let i = 0; i < locations.length; i++) {
if (locations[i].id == nid) {
node = locations[i]
}
}
return node
}
function getPath(destNode) {
var parentPath = []
var parentId;
while (destNode.parentId != null) {
parentPath.push(destNode.name)
parentId = destNode.parentId;
destNode = getNodeById(parentId);
}
parentPath.push(getNodeById(originLocationId).name)
return parentPath;
}
function getNodeOfMinFscore(openList) {
var minFscore = openList[0].fcost;
var nodeOfminFscore;
for (let i = 0; i < openList.length; i++) {
if (openList[i].fcost <= minFscore) {
minFscore = openList[i].fcost
nodeOfminFscore = openList[i]
}
}
return nodeOfminFscore
}
function manhattanDistance(stNode, dstNode) {
var x = Math.abs(dstNode.x - stNode.x);
var y = Math.abs(dstNode.y - stNode.y);
var dist = x + y;
return dist;
}
return "Shortest path: " + path;
}
// Demo
let props = {
locations: [
{ id: 1, x: 312, y: 152, connectedToIds: [4,2], name: "Thetaham" },
{ id: 2, x: 590, y: 388, connectedToIds: [1,3], name: "Deltabury" },
{ id: 3, x: 428, y: 737, connectedToIds: [2], name: "Gammation" },
{ id: 4, x: 222, y: 430, connectedToIds: [1], name: "Theta City" },
],
originLocationId: 1,
destinationLocationId: 3,
};
console.log(render.call({props}));

Related

Different result when put outside of event listener?

Sorry I am bad at asking questions, if you don't understand what I am asking please let me know
let node = this.createSvgNode("rect", {
'data-id': section.id || '',
x: section.x,
y: section.y,
width: section.width,
height: section.height,
fill: section.backgroundColor || "#EEEEEE"
});
let thisObject = this;
let paths = thisObject.findPaths(7, 3) // Doesn't work
node.addEventListener("click", function (e) {
let paths = thisObject.findPaths(7, 3) // works
});
I don't understand why let paths = thisObject.findPaths(7, 3) works when it is inside the event listener and doesn't when it is outside of the event listener. isn't it the same?
btw this is the findPaths function:
findPaths: function (startId, endId) {
let paths = [];
let startDoors = doors[startId];
let endDoors = doors[endId];
for (let i = 0; i < startDoors.length; i++) {
let startingPoint = this.findPointByCoordinates(startDoors[i]);
// console.log("Starting Point: " + JSON.stringify(startingPoint));
for (let j = 0; j < doors[endId].length; j++) {
let endingPoint = this.findPointByCoordinates(endDoors[j]);
// console.log("Ending Point: " + JSON.stringify(endingPoint));
let potentialPath = this.recursiveIterationOfPoints([startingPoint], endingPoint);
// console.log(potentialPath);
paths = paths.concat(potentialPath);
}
}
for (let id in paths) {
paths[id] = this.linkRoomsWithPath(paths[id], startId, endId);
}
return paths;
},
I get this error when I put let paths = thisObject.findPaths(7, 3) above the event listener:
Uncaught TypeError: Cannot read property 'length' of undefined
here is the full code
(function (global) {
var IndoorMaps = function (options) {
return new IndoorMaps.init(options);
};
let svgRootNode;
let doors = {};
let hallwayPoints = {};
let currentZIndex = 0;
let validZIndexes = [0];
let showHallwayPoints = false;
//let svgDimensions = {};
let floorPlan = [];
let mapSelection = null;
IndoorMaps.init = function (options) {
this.parseConfiguration(options || {});
this.parseFloorPlan(options.floorSections || {});
this.parseHallwayPoints(options.hallway || {});
//svgDimensions = this.calculateSVGWidthAndHeight();
svgRootNode = this.createSvgNode("svg", {
width: '100%',
height: '100%'
});
this.appendToBody(svgRootNode);
// Render the Map
this.drawMap(floorPlan[currentZIndex] || {});
if (showHallwayPoints) {
this.displayHallwayPoints(options.hallway || {});
}
return svgRootNode;
};
IndoorMaps.init.prototype = {
createSvgNode: function (elementName, attributes = {}, value) {
if (elementName === undefined) {
throw "Element name not found!";
}
let node = document.createElementNS("http://www.w3.org/2000/svg", elementName);
for (let attributeKey in attributes) {
node.setAttribute(attributeKey, attributes[attributeKey]);
}
if (value !== undefined) {
node.innerHTML = value;
}
return node;
},
appendToBody: function (node) {
window.document.body.appendChild(node);
},
draw: function (node) {
let panZoom = svgRootNode.querySelector('.svg-pan-zoom_viewport');
if (!panZoom) {
svgRootNode.append(node);
} else {
panZoom.append(node);
}
},
addDoors: function(roomId, doorPoints) {
doors[roomId] = doorPoints;
for (let i = 0; i < doorPoints.length; i++) {
let coordinates = doorPoints[i];
let attributes = {
x: coordinates.x,
y: coordinates.y,
width: coordinates.width || 0,
height: coordinates.height || 0,
fill: "#FF0000"
};
if (coordinates.width > coordinates.height) {
attributes.x -= coordinates.width / 2;
attributes.y -= coordinates.height;
}
if (coordinates.height > coordinates.width) {
attributes.x -= coordinates.width;
attributes.y -= coordinates.height / 2;
}
let rect = this.createSvgNode("rect", attributes);
this.draw(rect);
}
},
drawMap: function (floorSections) {
for (let i = 0; i < floorSections.length; i++) {
let section = floorSections[i];
if (
section.x === undefined
|| section.y === undefined
|| section.width === undefined
|| section.height === undefined
) {
throw "The x,y coordinates and the width and height are required fields.";
}
let node = this.createSvgNode("rect", {
'data-id': section.id || '',
x: section.x,
y: section.y,
width: section.width,
height: section.height,
fill: section.backgroundColor || "#EEEEEE"
});
let thisObject = this;
node.addEventListener("click", function (e) {
if (mapSelection === null) {
thisObject.removeRouteIndicators();
mapSelection = this;
this.classList.add('selected');
} else {
console.log(mapSelection.dataset.id + " : " + this.dataset.id);
let paths = thisObject.findPaths(mapSelection.dataset.id, this.dataset.id);
let shortestPath = thisObject.findShortestPath(paths);
thisObject.drawPath(shortestPath);
mapSelection = null;
this.classList.add('destination');
}
});
this.draw(node);
this.addDoors(section.id, section.doors);
if (section.label === undefined) {
continue;
}
if (section.label.text === undefined) {
throw "The text must be included for the label to be shown. Set label as null if you do not wish to display a label.";
}
xCoordinate = section.x + (section.label.x || 0);
yCoordinate = section.y + (section.label.y || 0);
let attributes = {
'x': xCoordinate,
'y': yCoordinate,
'fill': section.label.color || '#333333',
'font-family': section.label.fontStyle || 'Verdana',
'font-size': section.label.fontSize || '10'
};
if (section.label.alignment === undefined) {
section.label.alignment = "center|center";
}
if (section.label.alignment !== undefined) {
switch (section.label.alignment) {
case "center|center":
attributes['text-anchor'] = "middle";
attributes['dominant-baseline'] = "middle";
attributes['x'] = (section.width / 2) + section.x;
attributes['y'] = (section.height / 2) + section.y;
break;
case "horizontal_center":
attributes['text-anchor'] = "middle";
attributes['x'] = (section.width / 2) + section.x;
break;
case "vertical_center":
attributes['dominant-baseline'] = "middle";
attributes['y'] = (section.height / 2) + section.y;
break;
default:
throw "Alignment mode requested was not found.";
}
}
let textNode = this.createSvgNode("text", attributes, section.label.text);
this.draw(textNode);
}
},
drawPath: function(path) {
for (let i = 0; i < (path.length - 1); i++) {
let lineNode = this.createSvgNode('line', {
x1: path[i].x,
y1: path[i].y,
x2: path[i + 1].x,
y2: path[i + 1].y,
stroke: "green",
'data-type': "route"
});
this.draw(lineNode);
}
},
removeRouteIndicators: function () {
let nodes = svgRootNode.querySelectorAll('[data-type="route"]');
let panZoom = svgRootNode.querySelector('.svg-pan-zoom_viewport');
for (let index = 0; index < nodes.length; index++) {
if (!panZoom) {
svgRootNode.removeChild(nodes[index]);
} else {
panZoom.removeChild(nodes[index]);
}
}
nodes = svgRootNode.getElementsByClassName('selected');
for (let index = 0; index < nodes.length; index++) {
nodes[index].classList.remove('selected');
}
nodes = svgRootNode.getElementsByClassName('destination');
for (let index = 0; index < nodes.length; index++) {
nodes[index].classList.remove('destination');
}
},
displayHallwayPoints: function (hallwayNodes) {
for (let i = 0; i < hallwayNodes.length; i++) {
this.draw(
this.createSvgNode("circle", {
cx: hallwayNodes[i].x,
cy: hallwayNodes[i].y,
r: 2,
fill: hallwayNodes[i].fill || "black",
'data-id': hallwayNodes[i].id
})
);
}
},
findPaths: function (startId, endId) {
let paths = [];
let startDoors = doors[startId];
let endDoors = doors[endId];
for (let i = 0; i < startDoors.length; i++) {
let startingPoint = this.findPointByCoordinates(startDoors[i]);
console.log("Starting Point: " + JSON.stringify(startingPoint));
for (let j = 0; j < doors[endId].length; j++) {
let endingPoint = this.findPointByCoordinates(endDoors[j]);
console.log("Ending Point: " + JSON.stringify(endingPoint));
let potentialPath = this.recursiveIterationOfPoints([startingPoint], endingPoint);
console.log(potentialPath);
paths = paths.concat(potentialPath);
}
}
for (let id in paths) {
paths[id] = this.linkRoomsWithPath(paths[id], startId, endId);
}
return paths;
},
recursiveIterationOfPoints: function(path, end) {
let results = [];
let current = path[path.length - 1];
console.log(current);
if (current.id === end.id) {
return [path];
}
for (let i = 0; i < current.connected.length; i++) {
let nextPoint = hallwayPoints[current.connected[i]];
let found = false;
path.forEach( function (point, index) {
if (point.id === nextPoint.id) {
found = true;
}
});
if (found)
continue;
var newPath = path.slice();
newPath.push(nextPoint);
results = results.concat(this.recursiveIterationOfPoints(newPath, end));
}
return results;
},
findPointByCoordinates: function (coordinates) {
for (let id in hallwayPoints) {
let point = hallwayPoints[id];
if (point.x === coordinates.x && point.y === coordinates.y) {
return point;
}
}
},
findActualCoordinatesOfDoorByCoordinates: function (coordinates, roomId) {
let validDoors = doors[roomId];
for (let id in validDoors) {
if (validDoors[id].x === coordinates.x && validDoors[id].y === coordinates.y) {
return validDoors[id].actual;
}
}
},
parseHallwayPoints: function (points) {
for (let i = 0; i < points.length; i++) {
hallwayPoints[points[i].id] = points[i];
}
},
findShortestPath: function(paths) {
if (paths.length === 1)
return paths[0];
// Set benchmark range
let shortestDistance = this.calculateDistanceForPath(paths[0]);
let shortestPath = paths[0];
// Skip the first since it's already calculated.
for (let i = 1; i < paths.length; i++) {
let distance = this.calculateDistanceForPath(paths[i]);
if (distance < shortestDistance) {
shortestDistance = distance;
shortestPath = paths[i];
}
}
return shortestPath;
},
calculateDistanceForPath: function(path) {
let totalDistance = 0;
for (let i = 0; i < (path.length - 1); i++) {
// Formula: sqrt[(x0 - x1)^2 + (y0 - y1)^2]
totalDistance += Math.sqrt(Math.pow(path[i].x - path[i + 1].x, 2) + Math.pow(path[i].y - path[i + 1].y, 2));
}
return totalDistance;
},
linkRoomsWithPath: function (path, startId, endId) {
let first = this.findActualCoordinatesOfDoorByCoordinates(path[0], startId);
let last = this.findActualCoordinatesOfDoorByCoordinates(path[path.length - 1], endId);
path.unshift(first);
path.push(last);
return path;
},
parseConfiguration: function (options) {
currentZIndex = options.config.defaultZIndex || 0;
validZIndexes = options.config.validZIndexes || [0];
showHallwayPoints = options.config.showHallwayPoints || false;
},
parseFloorPlan: function (floorLayout) {
for (let i in floorLayout) {
if (floorLayout[i].z === undefined) {
floorLayout[i].z = 0;
}
if (!(floorLayout[i].z in floorPlan)) {
floorPlan[floorLayout[i].z] = [];
}
floorPlan[floorLayout[i].z].push(floorLayout[i]);
}
},
calculateSVGWidthAndHeight: function () {
let dimensions = {
width: 0,
height: 0
};
let floorLayout = floorPlan[currentZIndex];
let first = true;
for (let i in floorLayout) {
let calculatedWidth = floorLayout[i].width + floorLayout[i].x;
let calculatedHeight = floorLayout[i].height + floorLayout[i].y;
if (first) {
dimensions.width = calculatedWidth;
dimensions.height = calculatedHeight;
first = false;
}
if (calculatedWidth > dimensions.width) {
dimensions.width = calculatedWidth;
}
if (calculatedHeight > dimensions.height) {
dimensions.height = calculatedHeight;
}
}
for (let i in hallwayPoints) {
let currentWidth = hallwayPoints[i].x;
let currentHeight = hallwayPoints[i].y;
if (currentWidth > dimensions.width) {
dimensions.width = currentWidth;
}
if (currentHeight > dimensions.height) {
dimensions.height = currentHeight;
}
}
return dimensions;
}
};
window.indoorMaps = IndoorMaps;
}(window));
From the comment above, it seems that at the point you are calling findPaths(), doors exists, but is empty when you call findPaths(), though it is not when the event is triggered. Make sure that doors is filled before try to use it.

A* algorithm runs slowly in intellij

I'm developing a game like Clash of Clan (just an exercise). And I use a* algorithm for soldier to find the path to target. The size of map is 120 x 120.
But when a soldier find his target, it takes 4 to 5 second to find out his path. (the result is appropximately 120 steps, 5xxx node in open list).
That code when i run on browser, it just some miliseconds.
findPath: function (startTile, endTile, id, isIgnoreWall) {
if (startTile.x === endTile.x && startTile.y === endTile.y)
return [];
if (!id)
id = 0;
var openPath = [], closePath = [];
openPath[0] = {
pos: startTile,
parent: null,
g: 0,
f: this.calculateH(startTile, endTile)
};
var matrix = SceneMgr.getCurrentScene().getMapView().matrix;
var isFound = false, k = 0;
var _path = [];
while (openPath.length) {
var minIndex = this.findMin(openPath);
var minNode = openPath[minIndex];
openPath.splice(minIndex, 1);
closePath.push(minNode);
var aroundNode = this.findAroundNode(minNode.pos);
for (var i = 0; i < aroundNode.length; i++) {
var pos = aroundNode[i];
var indexClose = this.getNodeIndex(pos, closePath);
if (indexClose != -1 || !this.canMove(pos, isIgnoreWall, id, matrix))
continue;
if (pos.x === endTile.x && pos.y === endTile.y) {
var node = {
pos: pos,
parent: minNode
};
var path = [];
cc.log("tim thay: ", closePath.length);
while (node.parent) {
path.unshift(node);
node = node.parent;
}
return path;
break;
}
var cost = this.calculateH(minNode.pos, pos);
var g = minNode.g + cost;
var h = this.calculateH(pos, endTile);
var f = g + h;
var newNode = {
pos: pos,
g: g,
f: f,
parent: minNode
};
var indexOpen = this.getNodeIndex(pos, openPath);
//if node with the same pos is already in open list
if (indexOpen != -1) {
//if node in open list has f less than f, then re-assign, else skip it
if (openPath[indexOpen].g > g) {
openPath[indexOpen] = newNode;
}
} else {
openPath.push(newNode);
//_path.push({pos});
//this.draw([{pos}]);
}
}
}
return [];
}

Converting working functional Javascript Tic Tac Toe game to Class based to practice OOP

I'm making an exercise for myself to better understand OOP design by taking a working Javascript functional Tic Tac Toe game with AI to a Class based one. I'm getting stuck on the usual issues with what to put where in classes, single source of truth, loose coupling, etc. Not looking for complete answers here but perhaps some hints on a better strategy?
Here is the original working functional TTT:
import "./styles.css";
// functional TIC TAC TOE
// Human is 'O'
// Player is 'X'
let ttt = {
board: [], // array to hold the current game
reset: function() {
// reset board array and get HTML container
ttt.board = [];
const container = document.getElementById("ttt-game"); // the on div declared in HTML file
container.innerHTML = "";
// redraw swuares
// create a for loop to build board
for (let i = 0; i < 9; i++) {
// push board array with null
ttt.board.push(null);
// set square to create DOM element with 'div'
let square = document.createElement("div");
// insert " " non-breaking space to square
square.innnerHTML = " ";
// set square.dataset.idx set to i of for loop
square.dataset.idx = i;
// build square id's with i from loop / 'ttt-' + i - concatnate iteration
square.id = "ttt-" + i;
// add click eventlistener to square to fire ttt.play();
square.addEventListener("click", ttt.play);
// appendChild with square (created element 'div') to container
container.appendChild(square);
}
},
play: function() {
// ttt.play() : when the player selects a square
// play is fired when player selects square
// (A) Player's move - Mark with "O"
// set move to this.dataset.idx
let move = this.dataset.idx;
// assign ttt.board array with move to 0
ttt.board[move] = 0;
// assign "O" to innerHTML for this
this.innerHTML = "O";
// add "Player" to a classList for this
this.classList.add("Player");
// remove the eventlistener 'click' and fire ttt.play
this.removeEventListener("click", ttt.play);
// (B) No more moves available - draw
// check to see if board is full
if (ttt.board.indexOf(null) === -1) {
// alert "No winner"
alert("No Winner!");
// ttt.reset();
ttt.reset();
} else {
// (C) Computer's move - Mark with 'X'
// capture move made with dumbAI or notBadAI
move = ttt.dumbAI();
// assign ttt.board array with move to 1
ttt.board[move] = 1;
// assign sqaure to AI move with id "ttt-" + move (concatenate)
let square = document.getElementById("ttt-" + move);
// assign "X" to innerHTML for this
square.innerHTML = "X";
// add "Computer" to a classList for this
square.classList.add("Computer");
// square removeEventListener click and fire ttt.play
square.removeEventListener("click", ttt.play);
// (D) Who won?
// assign win to null (null, "x", "O")
let win = null;
// Horizontal row checks
for (let i = 0; i < 9; i += 3) {
if (
ttt.board[i] != null &&
ttt.board[i + 1] != null &&
ttt.board[i + 2] != null
) {
if (
ttt.board[i] == ttt.board[i + 1] &&
ttt.board[i + 1] == ttt.board[i + 2]
) {
win = ttt.board[i];
}
}
if (win !== null) {
break;
}
}
// Vertical row checks
if (win === null) {
for (let i = 0; i < 3; i++) {
if (
ttt.board[i] !== null &&
ttt.board[i + 3] !== null &&
ttt.board[i + 6] !== null
) {
if (
ttt.board[i] === ttt.board[i + 3] &&
ttt.board[i + 3] === ttt.board[i + 6]
) {
win = ttt.board[i];
}
if (win !== null) {
break;
}
}
}
}
// Diaganal row checks
if (win === null) {
if (
ttt.board[0] != null &&
ttt.board[4] != null &&
ttt.board[8] != null
) {
if (ttt.board[0] == ttt.board[4] && ttt.board[4] == ttt.board[8]) {
win = ttt.board[4];
}
}
}
if (win === null) {
if (
ttt.board[2] != null &&
ttt.board[4] != null &&
ttt.board[6] != null
) {
if (ttt.board[2] == ttt.board[4] && ttt.board[4] == ttt.board[6]) {
win = ttt.board[4];
}
}
}
// We have a winner
if (win !== null) {
alert("WINNER - " + (win === 0 ? "Player" : "Computer"));
ttt.reset();
}
}
},
dumbAI: function() {
// ttt.dumbAI() : dumb computer AI, randomly chooses an empty slot
// Extract out all open slots
let open = [];
for (let i = 0; i < 9; i++) {
if (ttt.board[i] === null) {
open.push(i);
}
}
// Randomly choose open slot
const random = Math.floor(Math.random() * (open.length - 1));
return open[random];
},
notBadAI: function() {
// ttt.notBadAI() : AI with a little more intelligence
// (A) Init
var move = null;
var check = function(first, direction, pc) {
// checkH() : helper function, check possible winning row
// PARAM square : first square number
// direction : "R"ow, "C"ol, "D"iagonal
// pc : 0 for player, 1 for computer
var second = 0,
third = 0;
if (direction === "R") {
second = first + 1;
third = first + 2;
} else if (direction === "C") {
second = first + 3;
third = first + 6;
} else {
second = 4;
third = first === 0 ? 8 : 6;
}
if (
ttt.board[first] === null &&
ttt.board[second] === pc &&
ttt.board[third] === pc
) {
return first;
} else if (
ttt.board[first] === pc &&
ttt.board[second] === null &&
ttt.board[third] === pc
) {
return second;
} else if (
ttt.board[first] === pc &&
ttt.board[second] === pc &&
ttt.board[third] === null
) {
return third;
}
return null;
};
// (B) Priority #1 - Go for the win
// (B1) Check horizontal rows
for (let i = 0; i < 9; i += 3) {
move = check(i, "R", 1);
if (move !== null) {
break;
}
}
// (B2) Check vertical columns
if (move === null) {
for (let i = 0; i < 3; i++) {
move = check(i, "C", 1);
if (move !== null) {
break;
}
}
}
// (B3) Check diagonal
if (move === null) {
move = check(0, "D", 1);
}
if (move === null) {
move = check(2, "D", 1);
}
// (C) Priority #2 - Block player from winning
// (C1) Check horizontal rows
for (let i = 0; i < 9; i += 3) {
move = check(i, "R", 0);
if (move !== null) {
break;
}
}
// (C2) Check vertical columns
if (move === null) {
for (let i = 0; i < 3; i++) {
move = check(i, "C", 0);
if (move !== null) {
break;
}
}
}
// (C3) Check diagonal
if (move === null) {
move = check(0, "D", 0);
}
if (move === null) {
move = check(2, "D", 0);
}
// (D) Random move if nothing
if (move === null) {
move = ttt.dumbAI();
}
return move;
}
};
document.addEventListener("DOMContentLoaded", ttt.reset());
Here is what I have so far of my class based version:
import "./styles.css";
class Gameboard {
constructor() {
this.board = [];
this.container = document.getElementById("ttt-game");
this.container.innerHTML = "";
}
reset() {
this.board = [];
}
build() {
for (let i = 0; i < 9; i++) {
this.board.push(null);
const square = document.createElement("div");
square.innerHTML = " ";
square.dataset.idx = i;
square.id = "ttt-" + i;
square.addEventListener("click", () => {
// What method do I envoke here?
console.log(square)
});
this.container.appendChild(square);
}
}
};
class Game {
constructor() {
this.gameBoard = new Gameboard();
this.player = new Player();
this.computer = new Computer();
}
play() {
this.gameBoard.build();
}
};
class Player {
};
class Computer {
};
class DumbAI {
};
const game = new Game();
document.addEventListener("DOMContentLoaded", game.play());
My HTML file is very simple with only a <div id="ttt-game"></div> to get started and CSS file is grid.
The biggest issue I'm having is capturing the squares in Game. And where should I put eventListeners ? (my next project is to do a React version).
Here's what I think, good, maintainable and testable code looks like: a bunch of small, self-contained functions, each with as few side-effects as possible. And rather than have state spread around the application, state should exist in a single, central location.
So, what I have done is decompose your code into small functions. I have pulled the state into a single store that enforces immutability. No weird half-way houses - the application state changes, or it doesn't. If it changes, the entire game is re-rendered. Responsibility for interacting with the UI exists in a single render function.
And you asked about classes in your question. createGame becomes:
class Game {
constructor() { ... },
start() { ... },
reset() { ... },
play() { ... }
}
createStore becomes:
class Store {
constructor() { ... }
getState() { ... },
setState() { ... }
}
playAI and playHuman become:
class AIPlayer {
constructor(store) { ... }
play() { ... }
}
class HumanPlayer {
constructor(store) { ... }
play() { ... }
}
checkForWinner becomes:
class WinChecker {
check(board) { ... }
}
...and so on.
But I ask the rhetorical question: would adding these classes add anything to the code? In my view there are three fundamental and intrinsic problems with class-oriented object orientation:
It leads you down the path of mixing application state and functionality,
Classes are like snowballs - they accrete functionality and quickly become over-large, and
People are terrible at coming up with meaningful class ontologies
All of the above mean that classes invariably lead to critically unmaintainable code.
I think code is usually simpler and more maintainable without new, and without this.
index.js
import { createGame } from "./create-game.js";
const game = createGame("#ttt-game");
game.start();
create-game.js
import { initialState } from "./initial-state.js";
import { createStore } from "./create-store.js";
import { render } from "./render.js";
const $ = document.querySelector.bind(document);
function start({ store, render }) {
createGameLoop({ store, render })();
}
function createGameLoop({ store, render }) {
let previousState = null;
return function loop() {
const state = store.getState();
if (state !== previousState) {
render(store);
previousState = store.getState();
}
requestAnimationFrame(loop);
};
}
export function createGame(selector) {
const store = createStore({ ...initialState, el: $(selector) });
return {
start: () => start({ store, render })
};
}
initial-state.js
export const initialState = {
el: null,
board: Array(9).fill(null),
winner: null
};
create-store.js
export function createStore(initialState) {
let state = Object.freeze(initialState);
return {
getState() {
return state;
},
setState(v) {
state = Object.freeze(v);
}
};
}
render.js
import { onSquareClick } from "./on-square-click.js";
import { winners } from "./winners.js";
import { resetGame } from "./reset-game.js";
export function render(store) {
const { el, board, winner } = store.getState();
el.innerHTML = "";
for (let i = 0; i < board.length; i++) {
let square = document.createElement("div");
square.id = `ttt-${i}`;
square.innerText = board[i];
square.classList = "square";
if (!board[i]) {
square.addEventListener("click", onSquareClick.bind(null, store));
}
el.appendChild(square);
}
if (winner) {
const message =
winner === winners.STALEMATE ? `Stalemate!` : `${winner} wins!`;
const msgEL = document.createElement("div");
msgEL.classList = "message";
msgEL.innerText = message;
msgEL.addEventListener("click", () => resetGame(store));
el.appendChild(msgEL);
}
}
on-square-click.js
import { play } from "./play.js";
export function onSquareClick(store, { target }) {
const {
groups: { move }
} = /^ttt-(?<move>.*)/gi.exec(target.id);
play({ move, store });
}
winners.js
export const winners = {
HUMAN: "Human",
AI: "AI",
STALEMATE: "Stalemate"
};
reset-game.js
import { initialState } from "./initial-state.js";
export function resetGame(store) {
const { el } = store.getState();
store.setState({ ...initialState, el });
}
play.js
import { randomMove } from "./random-move.js";
import { checkForWinner } from "./check-for-winner.js";
import { checkForStalemate } from "./check-for-stalemate.js";
import { winners } from "./winners.js";
function playHuman({ move, store }) {
const state = store.getState();
const updatedBoard = [...state.board];
updatedBoard[move] = "O";
store.setState({ ...state, board: updatedBoard });
}
function playAI(store) {
const state = store.getState();
const move = randomMove(state.board);
const updatedBoard = [...state.board];
updatedBoard[move] = "X";
store.setState({ ...state, board: updatedBoard });
}
export function play({ move, store }) {
playHuman({ move, store });
if (checkForWinner(store)) {
const state = store.getState();
store.setState({ ...state, winner: winners.HUMAN });
return;
}
if (checkForStalemate(store)) {
const state = store.getState();
store.setState({ ...state, winner: winners.STALEMATE });
return;
}
playAI(store);
if (checkForWinner(store)) {
const state = store.getState();
store.setState({ ...state, winner: winners.AI });
return;
}
}
Running version:
const $ = document.querySelector.bind(document);
const winners = {
HUMAN: "Human",
AI: "AI",
STALEMATE: "Stalemate"
};
function randomMove(board) {
let open = [];
for (let i = 0; i < board.length; i++) {
if (board[i] === null) {
open.push(i);
}
}
const random = Math.floor(Math.random() * (open.length - 1));
return open[random];
}
function onSquareClick(store, target) {
const {
groups: { move }
} = /^ttt-(?<move>.*)/gi.exec(target.id);
play({ move, store });
}
function render(store) {
const { el, board, winner } = store.getState();
el.innerHTML = "";
for (let i = 0; i < board.length; i++) {
let square = document.createElement("div");
square.id = `ttt-${i}`;
square.innerText = board[i];
square.classList = "square";
if (!board[i]) {
square.addEventListener("click", ({ target }) =>
onSquareClick(store, target)
);
}
el.appendChild(square);
}
if (winner) {
const message =
winner === winners.STALEMATE ? `Stalemate!` : `${winner} wins!`;
const msgEL = document.createElement("div");
msgEL.classList = "message";
msgEL.innerText = message;
msgEL.addEventListener("click", () => resetGame(store));
el.appendChild(msgEL);
}
}
function resetGame(store) {
const { el } = store.getState();
store.setState({ ...initialState, el });
}
function playHuman({ move, store }) {
const state = store.getState();
const updatedBoard = [...state.board];
updatedBoard[move] = "O";
store.setState({ ...state, board: updatedBoard });
}
function playAI(store) {
const state = store.getState();
const move = randomMove(state.board);
const updatedBoard = [...state.board];
updatedBoard[move] = "X";
store.setState({ ...state, board: updatedBoard });
}
const patterns = [
[0,1,2], [3,4,5], [6,7,8],
[0,4,8], [2,4,6],
[0,3,6], [1,4,7], [2,5,8]
];
function checkForWinner(store) {
const { board } = store.getState();
return patterns.find(([a,b,c]) =>
board[a] === board[b] &&
board[a] === board[c] &&
board[a]);
}
function checkForStalemate(store) {
const { board } = store.getState();
return board.indexOf(null) === -1;
}
function play({ move, store }) {
playHuman({ move, store });
if (checkForWinner(store)) {
const state = store.getState();
store.setState({ ...state, winner: winners.HUMAN });
return;
}
if (checkForStalemate(store)) {
const state = store.getState();
store.setState({ ...state, winner: winners.STALEMATE });
return;
}
playAI(store);
if (checkForWinner(store)) {
const state = store.getState();
store.setState({ ...state, winner: winners.AI });
return;
}
}
function createStore(initialState) {
let state = Object.freeze(initialState);
return {
getState() {
return state;
},
setState(v) {
state = Object.freeze(v);
}
};
}
function start({ store, render }) {
createGameLoop({ store, render })();
}
function createGameLoop({ store, render }) {
let previousState = null;
return function loop() {
const state = store.getState();
if (state !== previousState) {
render(store);
previousState = store.getState();
}
requestAnimationFrame(loop);
};
}
const initialState = {
el: null,
board: Array(9).fill(null),
winner: null
};
function createGame(selector) {
const store = createStore({ ...initialState, el: $(selector) });
return {
start: () => start({ store, render })
};
}
const game = createGame("#ttt-game");
game.start();
* {
box-sizing: border-box;
padding: 0;
margin: 0;
font-size: 0;
}
div.container {
width: 150px;
height: 150px;
box-shadow: 0 0 0 5px red inset;
}
div.square {
font-family: sans-serif;
font-size: 26px;
color: gray;
text-align: center;
line-height: 50px;
vertical-align: middle;
cursor: grab;
display: inline-block;
width: 50px;
height: 50px;
box-shadow: 0 0 0 2px black inset;
}
div.message {
font-family: sans-serif;
font-size: 26px;
color: white;
text-align: center;
line-height: 100px;
vertical-align: middle;
cursor: grab;
position: fixed;
top: calc(50% - 50px);
left: 0;
height: 100px;
width: 100%;
background-color: rgba(100, 100, 100, 0.7);
}
<div class="container" id="ttt-game"></div>

Highlighting works initially but does not when hash changes

I am developing an extension, which is about fetching the list of topics from the server and find if those topics match with the currently opened Gmail messages or not, if found then highlight that topic otherwise don't.
But if already 6 topics are matched, then it should not check or highlight other topics. This one is working but now I have a problem like if I go back from the current message and again come to that message then highlight won't be shown. Also if I open another message, the highlight is not done.
If I remove the code of counter check from the following snippet it works but this will highlight all the topics that are matched instead of just max 6 topics.
var count = 1;
var highlightAllWords = function(topics) {
Object.keys(topics.topics).forEach(function(topic) {
if (count <= 6) {
highlightTopic(topic);
if (topic !== null || !topic.length) {
count += 1;
}
}
});
};
// init highlight CSS
var ruleExistenceDict = {};
var sheet = (function() {
var style = document.createElement('style');
style.appendChild(document.createTextNode('')); // WebKit hack ##
document.head.appendChild(style);
return style.sheet;
})();
var topicData = {
topics: {
hostname: 4,
cto: 19,
aws: 382,
its: 26,
repo: 15,
unsubscribe: 65,
bitbucket: 313,
having: 28,
devops: 414,
frontend: 25,
stepin: 105,
username: 121,
deployed: 24,
vimeo: 460,
gmail: 156,
rds: 486,
clicked: 9,
lai: 850
}
};
function fetchTopics() {
// api call will be done here but for now its done with dummy object
searchPage(topicData);
}
function searchPage(topics) {
highlightAllWords(topics);
}
var count = 1;
var highlightAllWords = function(topics) {
Object.keys(topics.topics).forEach(function(topic) {
if (count <= 6) {
highlightTopic(topic);
if (topic !== null || !topic.length) {
count += 1;
}
}
});
};
function highlightTopic(topic) {
// let found = 0;
let isCompleted = false;
if (topic == null || topic.length === 0) return;
var topicRegex = new RegExp(topic, 'gi');
var treeWalker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT, {
acceptNode: function(node) {
var result = NodeFilter.FILTER_SKIP;
if (topicRegex.test(node.nodeValue)) {
// found += 1;
// if (found <= 6) {
result = NodeFilter.FILTER_ACCEPT;
return result;
// }
}
}
},
false
);
var skipTagName = {
NOSCRIPT: true,
SCRIPT: true,
STYLE: true
};
var nodeList = [];
// let count = 1;
console.log('count near nextNode', count);
while (treeWalker.nextNode()) {
if (!skipTagName[treeWalker.currentNode.parentNode.tagName]) {
nodeList.push(treeWalker.currentNode);
// count = count + 1;
// console.log('count:' + count);
}
}
nodeList.forEach(function(n) {
var rangeList = [];
// find sub-string ranges
var startingIndex = 0;
do {
// console.log(word, startingIndex, n.parentNode, n.textContent);
startingIndex = n.textContent.indexOf(topic, startingIndex + 1);
if (startingIndex !== -1) {
var topicRange = document.createRange();
topicRange.setStart(n, startingIndex);
topicRange.setEnd(n, startingIndex + topic.length);
rangeList.push(topicRange);
}
} while (startingIndex !== -1);
// highlight all ranges
rangeList.forEach(function(r) {
highlightRange(r);
});
});
}
var highlightRange = function(range) {
const bgColorCode = '#000000';
var anchor = document.createElement('A');
var selectorName = (anchor.className = 'highlighted_text');
anchor.classList.add('highlighted_text');
if (!ruleExistenceDict[bgColorCode]) {
sheet.insertRule(
[
'.',
selectorName,
' { background: #',
bgColorCode,
' !important; }'
].join(''),
0
);
ruleExistenceDict[bgColorCode] = true;
console.log(sheet);
}
anchor.appendChild(range.extractContents());
anchor.href = `https://app.com/profile/topics/${range.extractContents()}`;
range.insertNode(anchor);
};
Here is the full code:
https://gist.github.com/MilanRgm/5d6b9861be1326ba8b049ccfb6c3b376
You should declare the count variable inside the function, so that every time you refresh the page, the count will start from 1 again. Please update your code as follows:
var highlightAllWords = function(topics) {
var count = 1;
Object.keys(topics.topics).forEach(function(topic) {
if (count <= 6) {
highlightTopic(topic);
if (topic !== null || !topic.length) {
count += 1;
}
}
});
};

Neural Network Backpropagation not working

I have coded a neural network in JavaScript and implemented the Backpropagation algorithm described here.
Here is the code (typescript):
/**
* Net
*/
export class Net {
private layers: Layer[] = [];
private inputLayer: Layer;
private outputLayer: Layer;
public error: number = Infinity;
private eta: number = 0.15;
private alpha: number = 0.5;
constructor(...topology: number[]) {
topology.forEach((topologyLayer, iTL) => {
var nextLayerNeuronNumber = topology[iTL + 1] || 0;
this.layers.push(new Layer(topologyLayer, nextLayerNeuronNumber));
});
this.inputLayer = this.layers[0];
this.outputLayer = this.layers[this.layers.length - 1];
}
public loadWeights(weights) {
/*
[
[Layer
[Node weights, ..., ...]
]
]
*/
for (var iL = 0; iL < weights.length; iL++) {
var neuronWeights = weights[iL];
var layer = this.layers[iL];
for (var iN = 0; iN < neuronWeights.length; iN++) {
// Neuron
var connections = neuronWeights[iN];
for (var iC = 0; iC < connections.length; iC++) {
var connection = connections[iC];
this.layer(iL).neuron(iN).setWeights(iC, connection);
}
}
}
}
public train(data: number[][], iterartions = 2000) {
var inputs = this.inputLayer.neurons.length - 1;
for (var ite = 0; ite < iterartions; ite++) {
data.forEach(node => {
var inputData = [];
var outputData = [];
for (var i = 0; i < node.length; i++) {
if (i < inputs) {
inputData.push(node[i])
} else {
outputData.push(node[i])
}
}
this.feedForward(...inputData);
this.backProb(...outputData);
});
}
return this.calcDataError(data);
}
private calcDataError(data){
var overallDataErrorSum = 0;
var inputs = this.inputLayer.neurons.length - 1;
data.forEach(node => {
var outputData = node.splice(inputs);
var inputData = node;
this.feedForward(...inputData);
overallDataErrorSum += this.getNetError(outputData);
});
overallDataErrorSum /= data.length;
return overallDataErrorSum;
}
public saveWeights() {
// Ignore output layer
var ret = []
for (var iL = 0; iL < this.layers.length - 1; iL++) {
var layer = this.layers[iL];
var layer_ret = [];
layer.neurons.forEach(neuron => {
layer_ret.push(neuron.connections.map(c => c.weight));
});
ret.push(layer_ret);
}
return ret;
}
feedForward(...inputs: number[]) {
if (inputs.length != this.inputLayer.neurons.length - 1) return false;
this.inputLayer.neurons.forEach((neuron, i) => {
if (!neuron.isBias) {
neuron.output(inputs[i]);
}
});
this.layers.forEach((layer, i) => {
// Skip Input Layer
if (i > 0) {
var prevLayer = this.layers[i - 1]
layer.neurons.forEach(neuron => {
neuron.calcOutput(prevLayer);
});
}
});
}
public getNetError(targetVals) {
// Calc delta error of outputs
var deltas = [];
this.outputLayer.neurons.forEach((neuron, iN) => {
if (!neuron.isBias) {
neuron.calcOutputDelta(targetVals[iN]);
deltas.push(neuron.delta);
}
});
deltas = deltas.map(d => Math.pow(d, 2));
var sum = 0;
deltas.forEach(d => sum += d);
return sum / deltas.length;
}
backProb(...targetVals: number[]) {
// Calc delta error of outputs
this.outputLayer.neurons.forEach((neuron, iN) => {
if (!neuron.isBias) {
neuron.calcOutputDelta(targetVals[iN]);
}
});
// Backprop delta error through hidden layers
for (var iL = this.layers.length - 2; iL > 0; iL--) {
var layer = this.layers[iL];
var nextLayer = this.layers[iL + 1]
layer.neurons.forEach(neuron => {
neuron.calcHiddenDelta(nextLayer);
});
}
// Update weights
for (var iL = 1; iL < this.layers.length; iL++) {
var layer = this.layers[iL];
var prevLayer = this.layers[iL - 1];
layer.neurons.forEach(neuron => {
if (!neuron.isBias) {
neuron.updateWeights(prevLayer, this.eta);
}
});
}
this.error = this.getNetError(targetVals);
return this.error;
}
getOutputs(...inputs: number[]) {
var ret = [];
this.outputLayer.neurons.forEach(neuron => {
if (!neuron.isBias) {
ret.push(neuron.output())
}
});
return ret;
}
getResults(...inputs: number[]) {
this.feedForward(...inputs)
return this.getOutputs();
}
layer(i) {
return this.layers[i];
}
}
/**
* Layer
*/
class Layer {
public neurons: Neuron[] = [];
constructor(neuronNumber: number, nextLayerNeuronNumber: number) {
for (var iN = 0; iN < neuronNumber + 1; iN++) {
// +1 for bias neuron, which is last
if (iN < neuronNumber) {
// Create normal neuron
this.neurons.push(new Neuron(nextLayerNeuronNumber, iN, false));
} else {
this.neurons.push(new Neuron(nextLayerNeuronNumber, iN, true));
}
}
}
neuron(i) {
return this.neurons[i];
}
bias() {
return this.neurons[this.neurons.length - 1];
}
}
/**
* Neuron
*/
class Neuron {
public connections: Connection[] = [];
private outputVal: number;
public delta: number;
constructor(outputsTo: number, private index, public isBias = false) {
// Creates connections
for (var c = 0; c < outputsTo; c++) {
this.connections.push(new Connection());
}
this.outputVal = isBias ? 1 : 0;
}
calcOutput(prevLayer: Layer) {
// Only calcOutput when neuron is not a bias neuron
if (!this.isBias) {
var sum = 0;
prevLayer.neurons.forEach(prevLayerNeuron => {
sum += prevLayerNeuron.output() * prevLayerNeuron.getWeights(this.index).weight;
});
this.output(this.activationFunction(sum));
}
}
private activationFunction(x) {
//return Math.tanh(x);
return 1 / (1 + Math.exp(-x))
//return x;
};
private activationFunctionDerivative(x) {
// Small approximation of tanh derivative
//return 1 - x * x
// Sigmoid
var s = this.activationFunction(x);
return s * (1 - s);
// With general derivative formula where h = 1e-10
/*var h = 0.0001;
var dx = ((this.activationFunction(x + h) - this.activationFunction(x))/h)
return dx;*/
//return 1
};
// Backprop // Todo // Understand
public calcOutputDelta(targetVal) {
// Bias output neurons do not have delta error
if (!this.isBias) {
this.delta = targetVal - this.output();
}
}
public calcHiddenDelta(nextLayer: Layer) {
var sum = 0;
// Go through all neurons of next layer excluding bias
nextLayer.neurons.forEach((neuron, iN) => {
if (!neuron.isBias) {
sum += neuron.delta * this.getWeights(iN).weight;
}
});
this.delta = sum;
}
public updateWeights(prevLayer: Layer, eta: number) {
prevLayer.neurons.forEach((neuron, iN) => {
var weight = neuron.getWeights(this.index).weight;
var newWeight =
weight + // old weight
eta * // learning weight
this.delta * // delta error
this.activationFunctionDerivative(neuron.output())
neuron.getWeights(this.index).weight = newWeight;
});
}
// Backprop end
output(s?) {
if (s && !this.isBias) {
this.outputVal = s;
return this.outputVal;
} else {
return this.outputVal;
}
}
getWeights(i) {
return this.connections[i];
}
setWeights(i, s) {
return this.connections[i].weight = s;
}
}
/**
* Connection
*/
class Connection {
public weight: number;
public deltaWeight: number;
constructor() {
this.weight = Math.random();
this.deltaWeight = 0;
}
}
When training it for just one set of data, it works just fine. (example from here)
import {Net} from './ml';
var myNet = new Net(2, 2, 2);
var weights = [
[
[0.15, 0.25],
[0.20, 0.30],
[0.35, 0.35]
],
[
[0.40, 0.50],
[0.45, 0.55],
[0.60, 0.60]
]
];
// Just loads the weights given in the example
myNet.loadWeights(weights)
var error = myNet.train([[0.05, 0.10, 0.01, 0.99]]);
console.log('Error: ', error);
console.log(myNet.getResults(0.05, 0.10));
Console prints:
Error: 0.0000020735174706210714
[ 0.011556397089327321, 0.9886867357304885 ]
Basically, that's pretty good, right?
Then, I wanted to teach the network the XOR problem:
import {Net} from './ml';
var myNet = new Net(2, 3, 1);
var trainigData = [
[0, 0, 0],
[1, 0, 1],
[0, 1, 1],
[1, 1, 0]
]
var error = myNet.train(trainigData)
console.log('Error: ', error);
console.log('Input: 0, 0: ', myNet.getResults(0, 0));
console.log('Input: 1, 0: ', myNet.getResults(1, 0));
Here the network fails:
Error: 0.2500007370167383
Input: 0, 0: [ 0.5008584967899313 ]
Input: 1, 0: [ 0.5008584967899313 ]
What am I doing wrong?
Firstly perform gradient checks on the entire batch (meaining on the function calculating gradients on the batch), if you have not done so already. This will ensure you know what the problem is.
If gradients are not correctly computed, taking into account that your implementation works on single data sets, you are most likely mixing some values in the backwards pass.
If gradients are correctly computed, there is an error in your update function.
A working implementation of backpropagation for neural networks in javaScript can be found here
Here is the code snippet of the trainStep function using backpropagation
function trainStepBatch(details){
//we compute forward pass
//for each training sample in the batch
//and stored in the batch array
var batch=[];
var ks=[];
for(var a=0;a<details.data.in.length;a++){
var results=[];
var k=1;
results[0]={output:details.data.in[a]};
for(var i=1;i<this.layers.length;i++){
results[i]=layers[this.layers[i].type].evalForGrad(this.layers[i],results[i-1].output);
k++;
}
batch[a]=results;
ks[a]=k;
}
//We compute the backward pass
//first derivative of the cost function given the output
var grad=[];
for(i in batch)grad[i]={grad:costs[details.cost].df(batch[i][ks[i]-1].output,details.data.out[i])};
//for each layer we compute the backwards pass
//on the results of all forward passes at a given layer
for(var i=this.layers.length-1;i>0;i--){
var grads=[];
var test=true;
for(a in batch){
grads[a]=layers[this.layers[i].type].grad(this.layers[i],batch[a][i],batch[a][i-1],grad[a]);
if(grads[a]==null)test=false;
else grads[a].layer=i;
}
//we perform the update
if(test)stepBatch(this.layers[i].par,grads,details.stepSize);
}
}
And for the stepBatch function
function stepBatch(params,grads, stepSize){
for(i in params.w){
for(j in params.w[i]){
for(a in grads){
params.w[i][j]-=stepSize*grads[a].dw[i][j];
}
}
}
for(i in params.b){
for(a in grads){
params[a]-=stepSize*grads[a].db[i];
}
}
function stepBatch(params,grads, stepSize){
for(i in params.w){
for(j in params.w[i]){
for(a in grads){
params.w[i][j]-=stepSize*grads[a].dw[i][j];
}
}
}
for(i in params.b){
for(a in grads){
params[a]-=stepSize*grads[a].db[i];
}
}
}

Categories