/* A*(えースター)アルゴリズムの実装 ver2
* 二次元長方形の迷路から脱出する
* 上下左右にしか移動できないとする
*/
// ゴールまでの推定移動コストのみでコストを算出
// 探索候補配列中のコスト最小の座標を、次に探索する座標とする
// 最短ルートは、「この座標のコストを求めたときの現在地」をゴール地点からたどって求める
/* マップその1(ゴールは1通り)
this.mapCellsW = 40; // x軸方向の幅(セル数)
this.mapCellsH = 10; // y軸方向の高さ(セル数)
this.map = '' +
'****************************************' +
'* *' +
'* ****************************** *' +
'* * * *G* * * * *' +
'* * * * * *' +
'* * * *************** * *' +
'* * * * ******** *' +
'* * * *** * * *' +
'*S * * * * *' +
'****************************************';
*/
/* マップその2(ゴールは2通り)
this.mapCellsW = 40; // x軸方向の幅(セル数)
this.mapCellsH = 10; // y軸方向の高さ(セル数)
this.map = '' +
'****************************************' +
'* *' +
'* *********** ****************** *' +
'* * * *G* * * * *' +
'* * * * * *' +
'* * * *************** * *' +
'* * * * ****** * *' +
'* * * *** * * *' +
'*S * * * * *' +
'****************************************';
*/
var astarTest = {}; // namespace
// 定数
astarTest.MAXLOOPCOUNT = 500; // ループ回数上限
astarTest.SHOWRESULT = true; // 探索結果を表示するならtrue
astarTest.SHOWCOUNT = 50; // 何回の探索ごとに結果を表示するか
astarTest.SHOWDIVID = 'outdiv'; // 探索結果表示divのid
// グローバル変数
/* 移動コストを格納した配列
* 1つめのindexは迷路上のx座標(一番左が0)
* 2つめのindexは迷路上のy座標(一番上が0)
* 壁の値は Infinity
*/
astarTest.mazeCostToGArr = []; // ゴール地点までの推定移動コスト
astarTest.backPosArr = []; // この座標のコストを求めたときの現在地座標{x,y}
/* コスト格納クラス
* {int} x 迷路上のx座標(一番左が0)
* {int} y 迷路上のy座標(一番上が0)
* {int} cost ゴール地点までの推定移動コスト
*/
astarTest.PosCost = function(x, y, cost) {
// 座標は左上が(0,0)
this.x = x; // 現在のx座標
this.y = y; // 現在のy座標
this.cost = cost; // ゴール地点までの推定移動コスト。壁なら Infinity
};
// 探索候補配列操作クラス
astarTest.ForSearch = function() {
// 探索候補配列
this.fArr = [];
/* 配列に値を格納
* {PosCost} posCost コスト格納クラス
* return なし
*/
this.pushVal = function(posCost) {
// posCostがPosCostクラスのインスタンスかのチェックは略
this.fArr[this.fArr.length] = posCost;
};
/* 配列をcostの昇順にソート
* return なし
*/
this.sortArr = function() {
this.fArr.sort(this.sortfunc);
};
/* this.sortArr()の比較関数
* {PosCost} v1 比較対象
* {PosCost} v2 比較対象
* return {int} v1 > v2 なら正数 v1 < v2 なら負数 v1 == v2 なら 0
*/
this.sortfunc = function(v1, v2) {
return v1.cost - v2.cost;
};
/* 配列から値を削除
* {int} idx 削除対象のidx(0から始まる)
* return なし
*/
this.delval = function(idx) {
if ( idx < this.fArr.length && idx >= 0) {
this.fArr.splice(idx,1);
}
};
};
// 迷路クラス
astarTest.Maze = function() {
this.mapCellsW = 40; // x軸方向の幅(セル数)
this.mapCellsH = 10; // y軸方向の高さ(セル数)
var startC = 'S'; // マップ上のスタート地点を示す文字
var goalC = 'G'; // マップ上のゴール地点を示す文字
this.wallC = '*'; // マップ上の壁を示す文字
// マップ文字列。*で壁を表す
// stringであることを明示するため空文字列を足す
this.map = '' +
'****************************************' +
'* *' +
'* *********** ****************** *' +
'* * * *G* * * * *' +
'* * * * * *' +
'* * * *************** * *' +
'* * * * ****** * *' +
'* * * *** * * *' +
'*S * * * * *' +
'****************************************';
// スタート地点が文字列の何番目か
this.startP = this.map.indexOf(startC);
// ゴール地点が文字列の何番目か
this.goalP = this.map.indexOf(goalC);
/* 文字列上の位置から迷路上の座標を求める
* {int} index 文字列上の位置(0から始まる)
* return {object} 迷路上の座標{x,y} (左上が{0,0})
*/
this.getMatrixPosition = function(index) {
return { x: (index % this.mapCellsW),
y: (Math.floor(index / this.mapCellsW)) };
};
/* 迷路上の座標からマップ文字列上の位置を求める
* {int} x 迷路上のx座標(一番左が0)
* {int} y 迷路上のy座標(一番上が0)
* return {int} 文字列上の位置(0から始まる),エリア外だったら-1
*/
this.getIndex = function(x, y) {
if (x >= this.mapCellsW || x < 0) { return -1; }
if (y >= this.mapCellsH || y < 0) { return -1; }
return x + y * this.mapCellsW;
};
// スタート地点の迷路上の座標
this.startM = this.getMatrixPosition(this.startP);
// ゴール地点の迷路上の座標
this.goalM = this.getMatrixPosition(this.goalP);
/* 座標からゴールまでのコストを求める
* {int} x 迷路上のx座標(一番左が0)
* {int} y 迷路上のy座標(一番上が0)
* return {int} 座標のコスト
*/
this.calcCostToG = function(x, y) {
var dx = Math.abs(x - this.goalM.x); // 横方向のゴールまでの距離
var dy = Math.abs(y - this.goalM.y); // 縦方向のゴールまでの距離
// 斜めには移動できないので、単純に足した値をコストとする
return dx + dy;
};
/* 座標が壁かどうかチェック
* {int} x 迷路上のx座標(一番左が0)
* {int} y 迷路上のy座標(一番上が0)
* return {boolean} 壁またはエリア外だったらtrue
*/
this.isWall = function(x, y) {
var strIndex = this.getIndex(x, y); // マップ文字列上の位置
if (strIndex < 0) { return true; }
if (this.map[strIndex] == this.wallC) { return true; }
return false;
};
};
/* 現在地の座標の周りの移動コストを求め、探索候補配列に格納
* {int} x 現在地の迷路上のx座標(一番左が0)
* {int} y 現在地の迷路上のy座標(一番上が0)
* {Maze} Maze Mazeクラス
* {ForSearch} ForSearch ForSearchクラス
* return なし
*/
astarTest.search = function(x, y, Maze,ForSearch) {
var tx, ty; // 座標のワークエリア
var tempcost, tempPosCost; // ワークエリア
for (var i = 0; i < 4; i++) {
tx = x + astarTest.relMatrix[i][0];
ty = y + astarTest.relMatrix[i][1];
// コスト格納配列に無い場合はコストを求める
if (!astarTest.mazeCostToGArr[tx][ty]) {
if (Maze.isWall(tx, ty)) {
// 該当座標が壁の場合
astarTest.mazeCostToGArr[tx][ty] = Infinity;
} else {
// ゴール地点までの移動コストを求めて探索候補配列に格納
tempcost = Maze.calcCostToG(tx, ty);
astarTest.mazeCostToGArr[tx][ty] = tempcost;
astarTest.backPosArr[tx][ty] = { x: x, y: y };
tempPosCost = new astarTest.PosCost(tx, ty, tempcost);
ForSearch.pushVal(tempPosCost);
}
}
}
};
// 上下左右を調べるための相対座標
astarTest.relMatrix = [ [1,0], [0,-1], [-1,0], [0,1] ];
/* 探索結果出力
* {int} count 現在のループカウンタ値
* {int} x 現在地の迷路上のx座標(一番左が0)
* {int} y 現在地の迷路上のy座標(一番上が0)
* {object} Maze Mazeクラス
* return なし
*/
astarTest.outSearch = function(count, x, y, Maze) {
var outstr = '
count = ' + count + '
'; // 出力文字列
var i1, i2, work, outc;
for (i1 = 0; i1 < Maze.mapCellsH; i1++){
for (i2 = 0; i2 < Maze.mapCellsW; i2++){
work = astarTest.mazeCostToGArr[i2][i1];
outc = "";
if (work == undefined ) {
// 未探索地点は空白で表示
outc = ' ';
} else if (work == Infinity) {
// 壁は'**'で表示
outc = Maze.wallC + Maze.wallC;
} else {
// コストを表示。100以上は99と表示
if (work < 10) {
outc = '0' + work;
} else if (work < 100) {
outc = work;
} else {
outc = '99';
}
}
if (i1 == y && i2 == x) {
// 現在地なら赤太字で表示
outstr += '' + outc + '';
} else {
outstr += outc;
}
}
outstr += '
';
}
var outDiv = document.getElementById(astarTest.SHOWDIVID);
outDiv.innerHTML += outstr;
};
/* onload時に実行される処理 */
astarTest.start = function() {
var Maze = new astarTest.Maze();
var ForSearch = new astarTest.ForSearch();
// 移動コスト格納配列、移動前座標配列初期化
for (var i = 0; i < Maze.mapCellsW; i++) {
astarTest.mazeCostToGArr[i] = [];
astarTest.backPosArr[i] = [];
}
// スタート地点の座標を取得しコストを格納
var startM = Maze.getMatrixPosition(Maze.startP);
var startCost = Maze.calcCostToG(startM.x, startM.y);
astarTest.mazeCostToGArr[startM.x][startM.y] = startCost;
var startPosCost = new astarTest.PosCost(startM.x, startM.y, startCost);
ForSearch.pushVal(startPosCost);
var thisPosCost = null; // 現在地のコスト格納クラス
var loopCnt = 0; // 無限ループ防止用ループカウンタ
var loopflg = true; // falseならループを抜ける
// 経路探索処理
while (loopflg) {
// ループ上限に達したら強制的にループを抜ける
if (loopCnt > astarTest.MAXLOOPCOUNT) { break; }
loopCnt++;
// 調査対象の座標取得
thisPosCost = ForSearch.fArr[0];
if (!ForSearch.fArr[0]) {
window.alert('ゴールに到達できません');
return false;
}
// ゴール地点までの推定移動コストが0ならループを抜ける
if (thisPosCost.cost == 0) {
//window.alert('loopCnt=' + loopCnt + ' でゴール発見');
break;
}
// 現在地の座標の周りのコストを求める
!astarTest.search(thisPosCost.x, thisPosCost.y, Maze, ForSearch);
ForSearch.delval(0); // 現在地を探索候補配列から削除
ForSearch.sortArr(); // 探索候補配列ソート
// SHOWCOUNT回ごとに探索結果出力
if (loopCnt % astarTest.SHOWCOUNT == 0) {
astarTest.outSearch(loopCnt, thisPosCost.x, thisPosCost.y, Maze);
}
}
// 探索後処理
if (loopCnt > astarTest.MAXLOOPCOUNT) {
window.alert('ループカウンタオーバーフロー');
} else {
var routeMap = []; // 最短ルートを示したマップ配列
// string[] は標準機能ではないため、Maze.mapを配列に格納
for (var i = 0; i < Maze.map.length; i++) {
routeMap[i] = Maze.map.charAt(i);
}
// 最短ルートをrouteMapに格納
var lastCnt = loopCnt; // ゴール発見までのループ回数
loopCnt = 0;
var tx, ty; // 現在地の座標
var nx, ny; // 次の座標
tx = Maze.goalM.x;
ty = Maze.goalM.y;
while (loopflg) {
// ループ上限に達したら強制的にループを抜ける
if (loopCnt > astarTest.MAXLOOPCOUNT) { break; }
loopCnt++;
nx = astarTest.backPosArr[tx][ty].x;
ny = astarTest.backPosArr[tx][ty].y;
// 次の座標がスタート地点ならループを抜ける
if (nx == Maze.startM.x && ny == Maze.startM.y) {
break;
} else {
// 次の座標をマップ上に'X'で格納
routeMap[Maze.getIndex(nx, ny)] = 'X';
tx = nx;
ty = ny;
}
}
// 最短ルート出力
var outstr = '
count = ' + lastCnt + '
'; // 出力文字列
var i1, i2, work, outc, thisIdx, thisCostToG;
for (i1 = 0; i1 < Maze.mapCellsH; i1++){
for (i2 = 0; i2 < Maze.mapCellsW; i2++){
thisIdx = i1 * Maze.mapCellsW + i2; // 現在座標のrouteMap上のindex
work = astarTest.mazeCostToGArr[i2][i1];
outc = "";
if (work == undefined ) {
// 未探索地点は、壁でなければ空白で表示
if (routeMap[thisIdx] == Maze.wallC) {
outc = Maze.wallC + Maze.wallC;
} else {
outc = ' ';
}
} else if (work == Infinity) {
// 壁は'**'で表示
outc = Maze.wallC + Maze.wallC;
} else {
// コストを表示。100以上は99と表示
if (work < 10) {
outc = '0' + work;
} else if (work < 100) {
outc = work;
} else {
outc = '99';
}
}
// routeMapの値に応じて表示内容を変える
switch (routeMap[thisIdx]) {
case 'G': {
outstr += '' + 'GG' + '';
break;
}
case 'S': {
outstr += '' + 'SS' + '';
break;
}
case 'X': {
outstr += '' + outc + '';
break;
}
default: {
outstr += outc;
break;
}
}
}
outstr += '
';
}
var outDiv = document.getElementById(astarTest.SHOWDIVID);
outDiv.innerHTML += outstr;
}
};