Algorithms find shortest path to all cells on grid - algorithm

I have a grid [40 x 15] with 2 to 16 units on it, and unknown amount of obstacles.
How to find the shortest path to all the units from my unit location.
I have two helper methods that we can consider as O(1)
getMyLocation() - return the (x, y) coordinates of my location on the grid
investigateCell(x, y) - return information about cell at (x,y) coordinates
I implemented A* search algorithm, that search simultaneously to all the directions. At the end it output a grid where each cell have a number representing the distance from my location, and collection of all the units on the grid. It performs with O(N) where N is the number of cells - 600 in my case.
I implement this using AS3, unfortunately it takes my machine 30 - 50 milliseconds to calculate.
Here is my source code. Can you suggest me a better way?
package com.gazman.strategy_of_battle_package.map
{
import flash.geom.Point;
/**
* Implementing a path finding algorithm(Similar to A* search only there is no known target) to calculate the shortest path to each cell on the map.
* Once calculation is complete the information will be available at cellsMap. Each cell is a number representing the
* number of steps required to get to that location. Enemies and Allies will be represented with negative distance. Also the enemy and Allys
* coordinations collections are provided. Blocked cells will have the value 0.<br><br>
* Worth case and best case efficiency is O(N) where N is the number of cells.
*/
public class MapFilter
{
private static const PULL:Vector.<MapFilter> = new Vector.<MapFilter>();
public var cellsMap:Vector.<Vector.<int>>;
public var allys:Vector.<Point>;
public var enemies:Vector.<Point>;
private var stack:Vector.<MapFilter>;
private var map:Map;
private var x:int;
private var y:int;
private var count:int;
private var commander:String;
private var hash:Object;
private var filtered:Boolean;
public function filter(map:Map, myLocation:Point, commander:String):void{
filtered = true;
this.commander = commander;
this.map = map;
this.x = myLocation.x;
this.y = myLocation.y;
init();
cellsMap[x][y] = 1;
excecute();
while(stack.length > 0){
var length:int = stack.length;
for(var i:int = 0; i < length; i++){
var mapFilter:MapFilter = stack.shift();
mapFilter.excecute();
PULL.push(mapFilter);
}
}
}
public function navigateTo(location:Point):Point{
if(!filtered){
throw new Error("Must filter before navigating");
}
var position:int = Math.abs(cellsMap[location.x][location.y]);
if(position == 0){
throw new Error("Target unreachable");
}
while(position > 2){
if(canNavigateTo(position, location.x + 1, location.y)){
location.x++;
}
else if(canNavigateTo(position, location.x - 1, location.y)){
location.x--;
}
else if(canNavigateTo(position, location.x, location.y + 1)){
location.y++;
}
else if(canNavigateTo(position, location.x, location.y - 1)){
location.y--;
}
position = cellsMap[location.x][location.y];
}
return location;
throw new Error("Unexpected filtering error");
}
private function canNavigateTo(position:int, targetX:int, targetY:int):Boolean
{
return isInMapRange(targetX, targetY) && cellsMap[targetX][targetY] < position && cellsMap[targetX][targetY] > 0;
}
private function excecute():void
{
papulate(x + 1, y);
papulate(x - 1, y);
papulate(x, y + 1);
papulate(x, y - 1);
}
private function isInMapRange(x:int, y:int):Boolean{
return x < cellsMap.length &&
x >= 0 &&
y < cellsMap[0].length &&
y >= 0;
}
private function papulate(x:int, y:int):void
{
if(!isInMapRange(x,y) ||
cellsMap[x][y] != 0 ||
hash[x + "," + y] != null ||
map.isBlocked(x,y)){
return;
}
// we already checked that is not block
// checking if there units
if(map.isEmpty(x,y)){
cellsMap[x][y] = count;
addTask(x,y);
}
else{
cellsMap[x][y] = -count;
if(map.isAlly(x,y, commander)){
hash[x + "," + y] = true;
allys.push(new Point(x,y));
}
else {
hash[x + "," + y] = true;
enemies.push(new Point(x,y));
}
}
}
private function addTask(x:int, y:int):void
{
var mapFilter:MapFilter = PULL.pop();
if(mapFilter == null){
mapFilter = new MapFilter();
}
mapFilter.commander = commander;
mapFilter.hash = hash;
mapFilter.map = map;
mapFilter.cellsMap = cellsMap;
mapFilter.allys = allys;
mapFilter..enemies = enemies;
mapFilter.stack = stack;
mapFilter.count = count + 1;
mapFilter.x = x;
mapFilter.y = y;
stack.push(mapFilter);
}
private function init():void
{
hash = new Object();
cellsMap = new Vector.<Vector.<int>>();
for(var i:int = 0; i < map.width;i++){
cellsMap.push(new Vector.<int>);
for(var j:int = 0; j < map.height;j++){
cellsMap[i].push(0);
}
}
allys = new Vector.<Point>();
enemies = new Vector.<Point>();
stack = new Vector.<MapFilter>();
count = 2;
}
}
}

You can use Floyd Warshall to find the shortest path between every pair of points. This would be O(|V|^3) and you would not have to run it for each unit, just once on each turn. It's such a simple algorithm I suspect it might be faster in practice than running something like BFS / Bellman Ford for each unit.

Related

Efficient algorithm to generate a kind of maze

I have done a lot of searching for this and have found lots of help to generate a maze, but I have a very specific requirement and all the loops Iv tried have failed horribly.
I created an editor where I could draw what I need, but a generator would help a great deal and this has failed.
Requirement:
Given a square grid of DIV elements (no smaller than 10x10 and no larger than 60x60) I need a joined path through and around the grid that will not touch itself at any point except at start/finish.
There must always be at least one blank square between all path squares (any number of blanks is fine so long as the path never comes into contact with itself).
There can be no dead ends and no loops (Where the path would cross itself).
This is kind of like a reverse maze - I do not need to fill the entire grid, in fact I have no problem with lots of space around the path. It might be easier to think of this along similar lines to a Monopoly board game where the path around the board wanders about instead of going around the edges. I'm actually stuck for an adequate description, hence calling it a reverse maze.
Things I tried:
Lots and lots of overly complex loops. Iv not really come very close and the issue is also one of performance.
Lots and lots of code designed to generate a maze. Some of these have been very good indeed, but they all generate a typical maze, which is not what I need at all really, and adapting the code has proven trickier than writing an insane set of loops within loops.
Any ideas would be helpful. Thanks.
Update Code
Okay, I have translated KIKO's PHP code into Javascript but somewhere along the line I have made a simple error that I cannot track down: The code works and generates a table of the correct dimensions and generates a path through it.
However, in the function "isWithinGrid" I have to subtract 1 from the width and height of the table or the entire thing will fail, and, if I do this, the code will work and created a path through the table minus one cell which will be incorrectly colored although clearly a part of the path.
Note that sometimes the path will be broken or touching itself. I have little doubt that some small problem is causing all of this, but currently this is the best I have come up with and any further help would be much appreciated.
class Grid{
constructor(width,height){
this.width = width;
this.height = height;
this.cells = [];
for(var x=0; x < this.width; x++){
var tmparray = [];
for(var y=0; y < this.height; y++){
tmparray.push(false);
}
this.cells.push(tmparray);
}
}
isWithinGrid(x,y){
return (x >= 0) && (x <= this.width-1) && (y >= 0) && (y <= this.height-1);
}
isWithinPath(x,y){
return this.isWithinGrid(x,y) && this.cells[x][y];
}
setCellInPath(x,y,boolean){
this.cells[x][y] = boolean;
return this;
}
drawHorizontalLine(x1,x2,y){
for(var x=x1; x < x2; x++){
this.setCellInPath(x,y,true);
}
return this;
}
drawVerticalLine(x,y1,y2){
for(var y=y1; y < y2; y++){
this.setCellInPath(x,y,true);
}
return this;
}
drawSquare(){
var left = Math.round(this.width/5);
var right = Math.round(4*this.width/5);
var top = Math.round(this.height/5);
var bottom = Math.round(4*this.height/5);
this.drawHorizontalLine(left,right,top)
.drawHorizontalLine(left,right,bottom)
.drawVerticalLine(left,top,bottom)
.drawVerticalLine(right,top,bottom);
return this;
}
moveCell(x,y,dx,dy){
this.setCellInPath(x,y,false);
this.setCellInPath(x+dx,y+dy,true);
}
canMoveCell(x,y,dx,dy){
return this.isWithinPath(x,y) &&
this.isWithinGrid(x+dx,y+dy) &&
!this.isWithinPath(x+dx,y+dy) &&
!this.isWithinPath(x+2*dx,y+2*dy) &&
!this.isWithinPath(x+dy+dx,y+dx+dy)
!this.isWithinPath(x-dy+dx,y-dx+dy);
}
tryToDistortOnce(x,y,dx,dy){
if (!this.canMoveCell(x,y,dx,dy)) return false;
if (!this.canMoveCell(x+dy,y+dx,dx,dy)) return false;
if (!this.canMoveCell(x-dy,y-dx,dx,dy)) return false;
this.moveCell(x,y,dx,dy);
this.setCellInPath(x+dy+dx,y+dx+dy,true);
this.setCellInPath(x-dy+dx,y-dx+dy,true);
return true;
}
distortOnce(){
var x=0, y=0, dx=0, dy=0;
do {
x = Math.floor(Math.random() * this.width) + 1;
y = Math.floor(Math.random() * this.height) + 1;
} while (!this.isWithinPath(x,y));
switch (Math.floor(Math.random() * 4) + 1){
case 1: dx = -1; dy = 0; break;
case 2: dx = +1; dy = 0; break;
case 3: dx = 0; dy = +1; break;
case 4: dx = 0; dy = -1; break;
}
if (this.tryToDistortOnce(x,y,dx,dy)){
do {
x += dx;
y += dy;
} while (this.tryToDistortOnce(x,y,dx,dy));
return true;
}
return false;
}
distortPath(numberOfDistortions = 10){
for(var counter=1; counter < numberOfDistortions; counter++){
var tries = 0;
while (!this.distortOnce() && (tries < this.width+this.height)){ tries++; }
}
return this;
}
renderGrid(){
var str = '<table class="TSTTAB">';
for(var y=0; y < this.width; y++){
for(var x=0; x < this.height; x++){
str += '<td'+(this.cells[y][x] ? ' class="path">' : '>');
}
str += '</tr>';
}
str += '</table>';
document.getElementById('cont').innerHTML =str;
return this;
}
}
var Testgrid = new Grid(20,20);
Testgrid.drawSquare().distortPath(10).renderGrid();
.TSTTAB{background-color:#7F7F7F;border-collapse:collapse;}
.TSTTAB td{ width:20px; height: 20px; border: 1px solid #000;background-color: #E5E5E5; }
.TSTTAB td.path { background-color: #44F; }
<div id='cont'></div>
Well, I've given it a try. One hour of work seems more than enough for a simple question. It is, of course, far from perfect, but it illustrates what I was talking about. It generates solutions like this:
The complete code is:
<?php
// error reporting
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// configuration
const SIZE_X = 20;
const SIZE_Y = 20;
const COMPLEXITY = 20;
// grid class
class Grid
{
public function __construct($width,$height)
{
// remember
$this->width = $width;
$this->height = $height;
// initiate grid
foreach (range(1,$width) as $x) {
foreach (range(1,$height) as $y) {
$this->cells[$x][$y] = FALSE; // false means: not in path
}
}
}
public function isWithinGrid($x,$y)
// testb whether (x,y) is within the grid
{
return ($x >= 1) && ($x <= $this->width) &&
($y >= 1) && ($y <= $this->height);
}
public function isWithinPath($x,$y)
// is a cell part of the path?
{
return $this->isWithinGrid($x,$y) && $this->cells[$x][$y];
}
public function setCellInPath($x,$y,$boolean)
// remember whether a cell is part of the path or not
{
$this->cells[$x][$y] = $boolean;
return $this;
}
public function drawHorizontalLine($x1,$x2,$y)
// simple horizontal line
{
foreach (range($x1,$x2) as $x) $this->setCellInPath($x,$y,TRUE);
return $this;
}
public function drawVerticalLine($x,$y1,$y2)
// simple vertical line
{
foreach (range($y1,$y2) as $y) $this->setCellInPath($x,$y,TRUE);
return $this;
}
public function drawSquare()
// simple square
{
$left = round($this->width/5);
$right = round(4*$this->width/5);
$top = round($this->height/5);
$bottom = round(4*$this->height/5);
$this->drawHorizontalLine($left,$right,$top)
->drawHorizontalLine($left,$right,$bottom)
->drawVerticalLine($left,$top,$bottom)
->drawVerticalLine($right,$top,$bottom);
return $this;
}
private function moveCell($x,$y,$dx,$dy)
// move a cell
{
$this->setCellInPath($x,$y,FALSE);
$this->setCellInPath($x+$dx,$y+$dy,TRUE);
}
private function canMoveCell($x,$y,$dx,$dy)
// answers the question whether or not we can move (x,y) by (dx,dy)
{
return $this->isWithinPath($x,$y) && // must be part of path
$this->isWithinGrid($x+$dx,$y+$dy) && // stay within grid
!$this->isWithinPath($x+$dx,$y+$dy) && // but not on the path
!$this->isWithinPath($x+2*$dx,$y+2*$dy) && // and don't touch path
!$this->isWithinPath($x+$dy+$dx,$y+$dx+$dy) && // and don't touch path
!$this->isWithinPath($x-$dy+$dx,$y-$dx+$dy); // and don't touch path
}
private function tryToDistortOnce($x,$y,$dx,$dy)
{
// this one should be able to move
if (!$this->canMoveCell($x,$y,$dx,$dy)) return FALSE;
// but also its neighbours must be able to move
if (!$this->canMoveCell($x+$dy,$y+$dx,$dx,$dy)) return FALSE;
if (!$this->canMoveCell($x-$dy,$y-$dx,$dx,$dy)) return FALSE;
// move the target cell by displacement
$this->moveCell($x,$y,$dx,$dy);
// move neighbours by adding two cells
$this->setCellInPath($x+$dy+$dx,$y+$dx+$dy,TRUE);
$this->setCellInPath($x-$dy+$dx,$y-$dx+$dy,TRUE);
return TRUE; // success!
}
private function distortOnce()
// distort a random cell, returns success or failure
{
// find a random cell in path
do {
$x = rand(1,$this->width);
$y = rand(1,$this->height);
} while (!$this->isWithinPath($x,$y));
// choose one of four directions to move in
switch (rand(1,4))
{
case 1: $dx = -1; $dy = 0; break;
case 2: $dx = +1; $dy = 0; break;
case 3: $dx = 0; $dy = +1; break;
case 4: $dx = 0; $dy = -1; break;
}
// try to do it
if ($this->tryToDistortOnce($x,$y,$dx,$dy))
{
// more moves
do {
$x += $dx;
$y += $dy;
} while ($this->tryToDistortOnce($x,$y,$dx,$dy));
return TRUE; // it was a success!
}
return FALSE; // we failed
}
public function distortPath($numberOfDistortions = 10)
// distort up to a certain amount of times
{
// find a random cell that is part of the path to distort
for ($counter = 1; $counter <= $numberOfDistortions; $counter++) {
// we try that a limited number of times, depending on the grid size
$tries = 0;
while (!$this->distortOnce() &&
($tries < $this->width+$this->height)) { $tries++; }
}
return $this;
}
public function renderGrid()
// render grid
{
echo '<!DOCTYPE HTML><html><head><style>'.
' td { width:20px; height: 20px; border: 1px solid #000; }'.
' .path { background-color: #44F; }'.
'</style></head><body><table>';
foreach (range(1,SIZE_Y) as $y) {
echo '<tr>';
foreach (range(1,SIZE_X) as $x) {
echo '<td'.($this->cells[$x][$y] ? ' class="path">' : '>');
}
echo '</tr>';
}
echo '</body></html></table>';
return $this;
}
}
// create grid
$grid = new Grid(SIZE_X,SIZE_Y);
// start with a square, distort and then render
$grid->drawSquare()
->distortPath(COMPLEXITY)
->renderGrid();
There are lots of things you can do to improve on this.... have fun!
On my server this code takes between 2 and 5 milliseconds to execute. Mileage may vary...

Grouping points after cut plane three js

I found all intersection points between the object and plane, as in this great example. But now I want to connect these points between themselves (dividing into separate arrays) where the plane passes and connect them again. I tried to connect them by distance, but this does not give an effective result
//SORT POINTS DISTANCE
var pointsArray = []; //point after intersection
var sortedPoints = [];
var sortedPointsDis = [];
sortedPoints.push( pointsArray.pop() );
while( pointsArray.length ) {
var distance = sortedPoints[sortedPoints.length - 1].distanceTo( pointsArray[0] );
var index = 0;
for(var i = 1; i < pointsArray.length; i++) {
var tempDistance = sortedPoints[sortedPoints.length - 1].distanceTo( pointsArray[i] );
if( tempDistance < distance ) {
distance = tempDistance;
index = i;
}
}
sortedPoints.push( pointsArray.splice(index, 1)[0] );
sortedPointsDis.push( distance );
}
//GROUP POINTS
var result = [[]];
for(var i = 0; i < sortedPoints.length; i++) {
var lastArr = result[result.length - 1];
if( lastArr.length < 3 ) {
lastArr.push( sortedPoints[i] );
} else {
var distance = lastArr[0].distanceTo( sortedPoints[i] );
if( distance < sortedPointsDis[i - 1] ) {
result.push([]);
lastArr = result[result.length - 1];
}
lastArr.push(sortedPoints[i]);
}
}
JSfiddle.
Ideas? Examples? Thank in advance for your replies!
So, yes, this answer based on that one and extends it. The solution is rough and can be optimized.
I've used modified .equals() method of THREE.Vector3() (I hope it (or something similar) will be a part of the core one day as it's a very useful feature), taken from here:
THREE.Vector3.prototype.equals = function(v, tolerance) {
if (tolerance === undefined) {
return ((v.x === this.x) && (v.y === this.y) && (v.z === this.z));
} else {
return ((Math.abs(v.x - this.x) < tolerance) && (Math.abs(v.y - this.y) < tolerance) && (Math.abs(v.z - this.z) < tolerance));
}
}
The idea:
When we're getting points of intersection, to each point we add information about which face a point belongs to. It means that there are always pairs of points with the same face index.
Then, we recursively find all the contours our points form.
Also, all points mark as unchecked (.checked = false).
Find first unchecked point. Add it to the array of the current contour.
Find its pair point (with the same face index). Add it to the array of the current contour.
Find an unchecked point, the closest one to the point found last. Makr it as checked .checked = true.
Find its pair point (with the same face index). Mark it as checked .checked = true.
Check, if the last found point equals (with some tolerance) to the first found point (the beginning of the contour)
5.1. If no, then just add the last found point in the array of the current contour and go to step 3.
5.2. If yes, then clone the first point of the current contour and add it to the array of the current contour, add the contour to the array of contours.
Check, if we have have all points marked as checked.
6.1. If no, then go to step 1.
6.2. If yes, we finished. Return the array of contours.
Modified function of setting a point of intersection:
function setPointOfIntersection(line, plane, faceIdx) {
pointOfIntersection = plane.intersectLine(line);
if (pointOfIntersection) {
let p = pointOfIntersection.clone();
p.faceIndex = faceIdx;
p.checked = false;
pointsOfIntersection.vertices.push(p);
};
}
How to get contours and how to draw them:
var contours = getContours(pointsOfIntersection.vertices, [], true);
contours.forEach(cntr => {
let cntrGeom = new THREE.Geometry();
cntrGeom.vertices = cntr;
let contour = new THREE.Line(cntrGeom, new THREE.LineBasicMaterial({
color: Math.random() * 0xffffff
}));
scene.add(contour);
});
Where
function getContours(points, contours, firstRun) {
console.log("firstRun:", firstRun);
let contour = [];
// find first line for the contour
let firstPointIndex = 0;
let secondPointIndex = 0;
let firsPoint, secondPoint;
for (let i = 0; i < points.length; i++) {
if (points[i].checked == true) continue;
firstPointIndex = i;
firstPoint = points[firstPointIndex];
firstPoint.checked = true;
secondPointIndex = getPairIndex(firstPoint, firstPointIndex, points);
secondPoint = points[secondPointIndex];
secondPoint.checked = true;
contour.push(firstPoint.clone());
contour.push(secondPoint.clone());
break;
}
contour = getContour(secondPoint, points, contour);
contours.push(contour);
let allChecked = 0;
points.forEach(p => { allChecked += p.checked == true ? 1 : 0; });
console.log("allChecked: ", allChecked == points.length);
if (allChecked != points.length) { return getContours(points, contours, false); }
return contours;
}
function getContour(currentPoint, points, contour){
let p1Index = getNearestPointIndex(currentPoint, points);
let p1 = points[p1Index];
p1.checked = true;
let p2Index = getPairIndex(p1, p1Index, points);
let p2 = points[p2Index];
p2.checked = true;
let isClosed = p2.equals(contour[0], tolerance);
if (!isClosed) {
contour.push(p2.clone());
return getContour(p2, points, contour);
} else {
contour.push(contour[0].clone());
return contour;
}
}
function getNearestPointIndex(point, points){
let index = 0;
for (let i = 0; i < points.length; i++){
let p = points[i];
if (p.checked == false && p.equals(point, tolerance)){
index = i;
break;
}
}
return index;
}
function getPairIndex(point, pointIndex, points) {
let index = 0;
for (let i = 0; i < points.length; i++) {
let p = points[i];
if (i != pointIndex && p.checked == false && p.faceIndex == point.faceIndex) {
index = i;
break;
}
}
return index;
}
jsfiddle example r87.

Path finding with theta* when the triangle inequality is not fulfilled

I try to use the theta* algorithm (aigamedev.com/open/tutorial/lazy-theta-star) to find the fastest path on a rectangular grid. Distances are euqlidian, but speed between nodes is time dependent and varies between directions. So the triangle inequality, which is a prerequisite for the algorithm, is violated. Still it works excellently in most cases. How could I modify the code to work nicely also in turbulent areas? I suspect I may have to reevaluate some closed nodes and put them back into the open list. If so, under what conditions? An extensive web search hasn't helped.
vertex_t *thetastar(vertex_t *startnode, vertex_t *finishnode, double starttime) {
vertex_t *s, *s1;
double gold, gnew;
int dir; //8 directions to search for node neighbours
//Initialize
vertex[0].row = startnode->row;
vertex[0].col = startnode->col;
vertex[0].g = starttime;
vertex[0].h = h(&vertex[0], finishnode);
vertex[0].open = true;
vertex[0].closed = false;
vertex[0].parent = &vertex[0];
openlist[0] = &vertex[0];
openlist[1] = NULL;
//Find path
while ((s = pop(openlist)) != NULL) {
if (s->row == finishnode->row && s->col == finishnode->col) {
return s;
}
s->closed = true;
for (dir = 0; dir < 8; dir++) {
if ((s1 = nghbrvis(s, dir)) != NULL) {
if (!s1->closed) {
if (!s1->open) {
s1->g = inftime;
s1->parent = NULL;
}
gold = s1->g;
//Path 2
if (lineofsight(s->parent, s1)) {
gnew = (s->parent)->g + c(s->parent, s1);
if (gnew < s1->g) {
s1->parent = s->parent;
s1->g = gnew;
} }
//Path 1
gnew = s->g + c(s, s1);
if (gnew < s1->g) {
s1->parent = s;
s1->g = gnew;
}
if (s1->g < gold) {
s1->h = h(s1, finishnode);
if (s1->open)
remove(s1, openlist);
insert(s1, openlist);
} } } } }
return NULL;
}

Why Is My Genetic Algorithm Terrible (Why Doesn't It Converge)?

I wrote a quick experiment with a genetic algorithm. It simply takes a grid of squares and tries to mutate their color to make them all yellow. It fails miserably and I can't seem to figure out why. I've included a link to JSFiddle that demonstrates working code, as well as a copy of the code in its entirety.
http://jsfiddle.net/mankyd/X6x9L/
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div class="container">
<h1>The randomly flashing squares <i>should</i> be turning yellow</h1>
<div class="row">
<canvas id="input_canvas" width="100" height="100"></canvas>
<canvas id="output_canvas" width="100" height="100"></canvas>
</div>
<div class="row">
<span id="generation"></span>
<span id="best_fitness"></span>
<span id="avg_fitness"></span>
</div>
</div>
</body>
</html>
Note that the below javascript relies on jquery in a few places.
// A bit of code that draws several squares in a canvas
// and then attempts to use a genetic algorithm to slowly
// make those squares all yellow.
// Knobs that can be tweaked
var mutation_rate = 0.1; // how often should we mutate something
var crossover_rate = 0.6; // how often should we crossover two parents
var fitness_influence = 1; // affects the fitness's influence over mutation
var elitism = 1; // how many of the parent's generation to carry over
var num_offspring = 20; // how many spawn's per generation
var use_rank_selection = true; // false == roulette_selection
// Global variables for easy tracking
var children = []; // current generation
var best_spawn = null; // keeps track of our best so far
var best_fitness = null; // keeps track of our best so far
var generation = 0; // global generation counter
var clear_color = 'rgb(0,0,0)';
// used for output
var $gen_span = $('#generation');
var $best_fit = $('#best_fitness');
var $avg_fit = $('#avg_fitness');
var $input_canvas = $('#input_canvas');
var input_ctx = $input_canvas[0].getContext('2d');
var $output_canvas = $('#output_canvas');
var output_ctx = $output_canvas[0].getContext('2d');
// A spawn represents a genome - a collection of colored
// squares.
var Spawn = function(nodes) {
var _fitness = null; // a cache of our fitness
this.nodes = nodes; // the squares that make up our image
this.fitness = function() {
// fitness is simply a function of how close to yellow we are.
// This is defined through euclidian distance. Smaller fitnesses
// are better.
if (_fitness === null) {
_fitness = 0;
for (var i = 0; i < nodes.length; i++) {
_fitness += Math.pow(-nodes[i].color[0], 2) +
Math.pow(255 - nodes[i].color[1], 2) +
Math.pow(255 - nodes[i].color[2], 2);
}
_fitness /= 255*255*3*nodes.length; // divide by the worst possible distance
}
return _fitness;
};
this.mutate = function() {
// reset our cached fitness to unknown
_fitness = null;
var health = this.fitness() * fitness_influence;
var width = $output_canvas[0].width;
var height = $output_canvas[0].height;
for (var i = 0; i < nodes.length; i++) {
// Sometimes (most times) we don't mutate
if (Math.random() > mutation_rate) {
continue;
}
// Mutate the colors.
for (var j = 0; j < 3; j++) {
// colors can move by up to 32 in either direction
nodes[i].color[j] += 64 * (.5 - Math.random()) * health;
// make sure that our colors stay between 0 and 255
nodes[i].color[j] = Math.max(0, Math.min(255, nodes[i].color[j]));
}
}
};
this.draw = function(ctx) {
// This draw function is a little overly generic in that it supports
// arbitrary polygons.
ctx.save();
ctx.fillStyle = clear_color;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
for (var i = 0; i < nodes.length; i++) {
ctx.fillStyle = 'rgba(' + Math.floor(nodes[i].color[0]) + ',' + Math.floor(nodes[i].color[1]) + ',' + Math.floor(nodes[i].color[2]) + ',' + nodes[i].color[3] + ')';
ctx.beginPath();
ctx.moveTo(nodes[i].points[0][0], nodes[i].points[0][1]);
for (var j = 1; j < nodes[i].points.length; j++) {
ctx.lineTo(nodes[i].points[j][0], nodes[i].points[j][1]);
}
ctx.fill();
ctx.closePath();
}
ctx.restore();
};
};
Spawn.from_parents = function(parents) {
// Given two parents, mix them together to get another spawn
var nodes = [];
for (var i = 0; i < parents[0].nodes.length; i++) {
if (Math.random() > 0.5) {
nodes.push($.extend({}, parents[0].nodes[i]));
}
else {
nodes.push($.extend({}, parents[1].nodes[i]));
}
}
var s = new Spawn(nodes);
s.mutate();
return s;
};
Spawn.random = function(width, height) {
// Return a complete random spawn.
var nodes = [];
for (var i = 0; i < width * height; i += 10) {
var n = {
color: [Math.random() * 256, Math.random() * 256, Math.random() * 256, 1],
points: [
[i % width, Math.floor(i / width) * 10],
[(i % width) + 10, Math.floor(i / width) * 10],
[(i % width) + 10, Math.floor(i / width + 1) * 10],
[i % width, Math.floor(i / width + 1) * 10],
]
};
nodes.push(n);
}
return new Spawn(nodes);
};
var select_parents = function(gene_pool) {
if (use_rank_selection) {
return rank_selection(gene_pool);
}
return roulette_selection(gene_pool);
};
var roulette_selection = function(gene_pool) {
var mother = null;
var father = null;
gene_pool = gene_pool.slice(0);
var sum_fitness = 0;
var i = 0;
for (i = 0; i < gene_pool.length; i++) {
sum_fitness += gene_pool[i].fitness();
}
var choose = Math.floor(Math.random() * sum_fitness);
for (i = 0; i < gene_pool.length; i++) {
if (choose <= gene_pool[i].fitness()) {
mother = gene_pool[i];
break;
}
choose -= gene_pool[i].fitness();
}
// now remove the mother and repeat for the father
sum_fitness -= mother.fitness();
gene_pool.splice(i, 1);
choose = Math.floor(Math.random() * sum_fitness);
for (i = 0; i < gene_pool.length; i++) {
if (choose <= gene_pool[i].fitness()) {
father = gene_pool[i];
break;
}
choose -= gene_pool[i].fitness();
}
return [mother, father];
};
var rank_selection = function(gene_pool) {
gene_pool = gene_pool.slice(0);
gene_pool.sort(function(a, b) {
return b.fitness() - a.fitness();
});
var choose_one = function() {
var sum_fitness = (gene_pool.length + 1) * (gene_pool.length / 2);
var choose = Math.floor(Math.random() * sum_fitness);
for (var i = 0; i < gene_pool.length; i++) {
// figure out the sume of the records up to this point. if we exceed
// our chosen spot, we've found our spawn.
if ((i + 1) * (i / 2) >= choose) {
return gene_pool.splice(i, 1)[0];
}
}
return gene_pool.pop(); // last element, if for some reason we get here
};
var mother = choose_one();
var father = choose_one();
return [mother, father];
};
var start = function() {
// Initialize our first generation
var width = $output_canvas[0].width;
var height = $output_canvas[0].height;
generation = 0;
children = [];
for (var j = 0; j < num_offspring; j++) {
children.push(Spawn.random(width, height));
}
// sort by fitness so that our best comes first
children.sort(function(a, b) {
return a.fitness() - b.fitness();
});
best_spawn = children[0];
best_fitness = best_spawn.fitness();
best_spawn.draw(output_ctx);
};
var generate = function(spawn_pool) {
// generate a new set of offspring
var offspring = [];
for (var i = 0; i < num_offspring; i++) {
var parents = select_parents(spawn_pool);
// odds of crossover decrease as we get closer
if (Math.random() * best_fitness < crossover_rate) {
var s = Spawn.from_parents(parents);
}
else {
// quick hack to copy our mother, with possible mutation
var s = Spawn.from_parents([parents[0], parents[0]]);
}
offspring.push(s);
}
// select a number of best from the parent pool (elitism)
for (var i = 0; i < elitism; i++) {
offspring.push(spawn_pool[i]);
}
// sort our offspring by fitness (this includes the parents from elitism). Fittest first.
offspring.sort(function(a, b) {
return a.fitness() - b.fitness();
});
// pick off the number that we want
offspring = offspring.slice(0, num_offspring);
best_spawn = offspring[0];
best_fitness = best_spawn.fitness();
best_spawn.draw(output_ctx);
generation++;
return offspring;
};
var average_fitness = function(generation) {
debugger;
var a = 0;
for (var i = 0; i < generation.length; i++) {
a += generation[i].fitness();
}
return a / generation.length;
};
//Draw yellow and then initialize our first generation
input_ctx.fillStyle = 'yellow';
input_ctx.fillRect(0, 0, input_ctx.canvas.width, input_ctx.canvas.height);
start();
// Our loop function. Use setTimeout to prevent things from freezing
var gen = function() {
children = generate(children);
$gen_span.text('Generation: ' + generation);
$best_fit.text('Best Fitness: ' + best_fitness);
$avg_fit.text('Avg. Fitness: ' + average_fitness(children));
if (generation % 100 === 0) {
console.log('Generation', generation);
console.log('Fitness', best_fitness);
}
setTimeout(gen, 1);
};
gen();​
I've commented the code to try to make parsing it easy. The basic idea is quite simple:
Select 1 or 2 parents from the current generation
Mix those one or two parents together
Mutate the result slightly and add it to the next generation
Select the best few parents (1 in the example) and add them to the next generation
Sort and slice off N results and use them for the next generation (potentially a mix of parents and offspring)
Rinse and repeat
The output never gets anywhere near yellow. It quickly falls into a steady state of a sort that looks awful. Where have I gone wrong?
Solved it. It was in the "from_parents" method:
if (Math.random() > 0.5) {
nodes.push($.extend({}, parents[0].nodes[i]));
}
else {
nodes.push($.extend({}, parents[1].nodes[i]));
}
The $.extend() was doing a shallow copy. The obvious solution was to either put true as the first argument which causes a deep copy. This, however, is incredibly slow performance-wise. The better solution was to remove the $.extend() from that chunk of code entirely and instead to move it up to the mutate() method, where I call $.extend() only if a node is actually about to be changed. In other words, it becomes a copy-on-write.
Also, the color I put in the fitness function was wrong :P

Slicing up a Rectangle

I need to get AS3 Rectangle objects from a function receiving other Rectangles as parameters. The result is very similar to the slice tool in Photoshop. It is quite hard to explain, so here is a picture:
(source: free.fr)
The blue squares are the rectangles that are given as parameters and the green ones are the result. Given Rectangles can overlap, as seen on picture 2 or be out of frame.
I don't look for a graphical realisation but for a way to get Rectangle objects as result.
Do you know any lib to do that?
Looked like a fun problem, so I gave it a crack. My idea was to just brute force it by:
Determine which points where the corners of the generated rectangles could be.
Remove all duplicates from this list of points.
Check all rectangles that could theoretically be drawn where the rect would have all 4 corners in the list of point.
Filter out all invalid rectangles (it intersects with one of our original rectangles etc.)
Reduce all valid rectangles to the minimum amount needed (if a valid rectangle contains another valid rectangle the "child" is removed.
It seems to work (although I haven't tested extensively).
Here's a demo. Sorry about the color palette. I was winging it.
Here's the source code (could probably be optimized quite a bit):
package
{
import flash.display.*;
import flash.events.*;
import flash.geom.*;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.text.TextFormat;
import flash.utils.getTimer;
public class Main extends Sprite {
private var m_colors : Array = [0xffaaaa, 0x77ff77, 0xaaaaff, 0xffff44, 0xff44ff, 0xaaffff, 0x444444, 0xffaa55, 0xaaff55, 0x55aaff, 0x55ffaa];
private var m_roomRect : Rectangle;
private var m_sourceRects : Vector.<Rectangle> = new Vector.<Rectangle>();
private var m_currentDragRect : Rectangle;
private var m_dragMousePoint : Point = new Point();
private var m_outputTextField : TextField;
public function Main() : void {
m_roomRect = new Rectangle(40, 40, 400, 400);
m_sourceRects.push(new Rectangle(60, 60, 60, 80));
m_sourceRects.push(new Rectangle(130, 220, 70, 80));
m_sourceRects.push(new Rectangle(160, 260, 100, 80));
this.stage.addEventListener(MouseEvent.MOUSE_DOWN, onMouseEvent);
this.stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseEvent);
this.stage.addEventListener(MouseEvent.MOUSE_UP, onMouseEvent);
var tf : TextField = new TextField();
tf.defaultTextFormat = new TextFormat("_sans", 12);
tf.text = "Click and drag blue rectangles to move them";
tf.autoSize = TextFieldAutoSize.LEFT;
tf.x = (m_roomRect.left + m_roomRect.right) / 2 - tf.width / 2;
tf.y = m_roomRect.top - tf.height;
this.stage.addChild(tf);
m_outputTextField = new TextField();
m_outputTextField.defaultTextFormat = tf.defaultTextFormat;
m_outputTextField.width = m_roomRect.width;
m_outputTextField.x = m_roomRect.x;
m_outputTextField.y = m_roomRect.bottom + 5;
this.stage.addChild(m_outputTextField);
redraw();
}
private function onMouseEvent(event : MouseEvent):void {
switch(event.type) {
case MouseEvent.MOUSE_DOWN:
checkMouseDownOnRect();
break;
case MouseEvent.MOUSE_MOVE:
checkMouseDrag();
break;
case MouseEvent.MOUSE_UP:
m_currentDragRect = null;
break;
}
}
private function checkMouseDownOnRect():void {
m_currentDragRect = null;
m_dragMousePoint = new Point(this.stage.mouseX, this.stage.mouseY);
for each(var sourceRect : Rectangle in m_sourceRects) {
if (sourceRect.containsPoint(m_dragMousePoint)) {
m_currentDragRect = sourceRect;
break;
}
}
}
private function checkMouseDrag():void {
if (m_currentDragRect != null) {
m_currentDragRect.x += this.stage.mouseX - m_dragMousePoint.x;
m_currentDragRect.y += this.stage.mouseY - m_dragMousePoint.y;
m_dragMousePoint.x = this.stage.mouseX;
m_dragMousePoint.y = this.stage.mouseY;
redraw();
}
}
private function redraw():void {
// calculate data
var time : int = getTimer();
var data : CalculationData = calculate();
var calcTime : int = getTimer() - time;
// draw room bounds
this.graphics.clear();
this.graphics.lineStyle(3, 0x0);
this.graphics.drawRect(m_roomRect.x, m_roomRect.y, m_roomRect.width, m_roomRect.height);
// draw generated rectangles
for (var i : int = 0; i < data.outputRects.length; i++) {
var color : int = m_colors[i % m_colors.length];
var rect : Rectangle = data.outputRects[i];
this.graphics.lineStyle(2, color, 0.5);
this.graphics.beginFill(color, 0.5);
this.graphics.drawRect(rect.x, rect.y, rect.width, rect.height);
this.graphics.endFill();
}
// draw horisontal lines (a line that crosses each red point) for debug purposes
for each (var lineY : int in data.lines) {
this.graphics.lineStyle(1, 0, 0.2);
this.graphics.moveTo(m_roomRect.x, lineY);
this.graphics.lineTo(m_roomRect.x + m_roomRect.width, lineY);
this.graphics.endFill();
}
// the original rectangles
for each (var sourceRect : Rectangle in m_sourceRects) {
this.graphics.lineStyle(2, 0x0);
this.graphics.beginFill(0x0000aa, 0.5);
this.graphics.drawRect(sourceRect.x, sourceRect.y, sourceRect.width, sourceRect.height);
this.graphics.endFill();
}
// draw all points that was used to generate the output rectangles for debug purposes
for each (var p : Point in data.points) {
this.graphics.lineStyle(0, 0, 0);
this.graphics.beginFill(0xff0000, 1);
this.graphics.drawCircle(p.x, p.y, 3);
this.graphics.endFill();
}
m_outputTextField.text = "Rect count: " + data.outputRects.length + " (calculation time: " + calcTime + "ms)";
}
private function calculate(): CalculationData {
// list of y coords for horisontal lines,
// which are interesting when determining which rectangles to generate
var lines : Vector.<int> = new Vector.<int>();
// list of all points which are interesting
// when determining where the corners of the generated rect could be
var points : Vector.<Point> = new Vector.<Point>();
// add the 4 corners of the room to interesting points
points.push(new Point(m_roomRect.left, m_roomRect.top));
points.push(new Point(m_roomRect.right, m_roomRect.top));
points.push(new Point(m_roomRect.left, m_roomRect.bottom));
points.push(new Point(m_roomRect.right, m_roomRect.bottom));
for (var i:int = 0; i < m_sourceRects.length; i++) {
var sourceRect : Rectangle = m_sourceRects[i];
// source rect is completely outside of the room, we shoud ignore it
if (!m_roomRect.containsRect(sourceRect) && !m_roomRect.intersects(sourceRect)) {
continue;
}
// push the y coord of the rect's top edge to the list of lines if it's not already been added
if (lines.indexOf(sourceRect.y) == -1) {
lines.push(sourceRect.y);
}
// push the y coord of the rect's bottom edge to the list of lines if it's not already been added
if (lines.indexOf(sourceRect.bottom) == -1) {
lines.push(sourceRect.bottom);
}
// add the 4 corners of the source rect to the list of interesting points
addCornerPoints(points, sourceRect);
// find the intersections between source rectangles and add those points
for (var j:int = 0; j < m_sourceRects.length; j++) {
if (j != i) {
var intersect : Rectangle = m_sourceRects[i].intersection(m_sourceRects[j]);
if (intersect.width != 0 && intersect.height != 0) {
addCornerPoints(points, intersect);
}
}
}
}
for (i = 0; i < lines.length; i++) {
// add the points where the horisontal lines intersect with the room's left and right edges
points.push(new Point(m_roomRect.x, lines[i]));
points.push(new Point(m_roomRect.right, lines[i]));
var lineRect : Rectangle = new Rectangle(m_roomRect.x, m_roomRect.y,
m_roomRect.width, lines[i] - m_roomRect.y);
// add all points where the horisontal lines intersect with the source rectangles
for (a = 0; a < m_sourceRects.length;a++) {
intersect = m_sourceRects[a].intersection(lineRect);
if (intersect.width != 0 && intersect.height != 0) {
addCornerPoints(points, intersect);
}
}
}
// clamp all points that are outside of the room to the room edges
for (i = 0; i < points.length; i++) {
points[i].x = Math.min(Math.max(m_roomRect.left, points[i].x), m_roomRect.right);
points[i].y = Math.min(Math.max(m_roomRect.top, points[i].y), m_roomRect.bottom);
}
removeDuplicatePoints(points);
var outputRects : Vector.<Rectangle> = new Vector.<Rectangle>();
var pointsHash : Object = { };
for (a = 0; a < points.length; a++) {
pointsHash[points[a].x + "_" + points[a].y] = true;
}
for (var a:int = 0; a < points.length; a++) {
for (var b:int = 0; b < points.length; b++) {
if (b != a && points[b].x > points[a].x && points[b].y == points[a].y) {
for (var c:int = 0; c < points.length; c++) {
// generate a rectangle that has its four corners in our points of interest
if (c != b && c != a && points[c].y > points[b].y && points[c].x == points[b].x) {
var r : Rectangle = new Rectangle(points[a].x, points[a].y, points[b].x - points[a].x, points[c].y - points[b].y);
// make sure the rect has the bottom left corner in one of our points
if (pointsHash[r.left+"_"+r.bottom]) {
var containsOrIntersectsWithSource : Boolean = false;
for (i = 0; i < m_sourceRects.length;i++) {
if (r.containsRect(m_sourceRects[i]) || r.intersects(m_sourceRects[i])) {
containsOrIntersectsWithSource = true;
break;
}
}
// we don't add any rectangles that either intersects with a source rect
// or completely contains a source rect
if (!containsOrIntersectsWithSource) {
outputRects.push(r);
}
}
}
}
}
}
}
trace("outputRects before cleanup:", outputRects.length);
combineOutputRects(outputRects)
trace("outputRects after cleanup", outputRects.length);
var data : CalculationData = new CalculationData();
data.outputRects = outputRects;
data.lines = lines;
data.points = points;
return data;
}
private function addCornerPoints(points : Vector.<Point>, rect : Rectangle) : void {
points.push(new Point(rect.left, rect.top));
points.push(new Point(rect.right, rect.top));
points.push(new Point(rect.left, rect.bottom));
points.push(new Point(rect.right, rect.bottom));
}
// removes all rectangle that are already contained in another rectangle
private function combineOutputRects(outputRects : Vector.<Rectangle>):Boolean {
for (var a : int = 0; a < outputRects.length; a++) {
for (var b : int = 0; b < outputRects.length; b++) {
if (b != a) {
if (outputRects[a].containsRect(outputRects[b])) {
trace("\tremoved rect " + outputRects[b] + ", it was contained in " + outputRects[a]);
outputRects.splice(b, 1);
b--;
a = 0;
}
}
}
}
return false;
}
private function removeDuplicatePoints(points : Vector.<Point>) : void {
var usedPoints : Object = {};
for (var i : int = 0; i < points.length; i++) {
if (usedPoints[points[i].toString()]) {
points.splice(i, 1);
i--;
} else {
usedPoints[points[i].toString()] = true;
}
}
}
}
}
import flash.geom.Point;
import flash.geom.Rectangle;
class CalculationData {
public var outputRects : Vector.<Rectangle> = new Vector.<Rectangle>;
public var lines : Vector.<int> = new Vector.<int>;
public var points : Vector.<Point> = new Vector.<Point>;
}

Resources