A*-algoritmia käytetään ratkaisun hakemiseen hakupuusta (kopypastaa wikipediasta :-). Eli suomeksi sanottuna, tällä voidaan toteuttaa esim. reitinhakua jos tiedetään lähtö- ja maalipisteet.
En ajatellut tässä sen kummemmin kertoa algoritmista, kun siihen on parempiakin selityksiä netti pullollaan (http://www.policyalmanac.org/games/aStarTutorial.htm). Suosittelenkin lukaisemaan tuon annetun artikkelin ainakin pintapuolisesti, niin pysyy kärryillä tässäkin koodissa.
Koodivinkin tarkoituksena on lähinnä näyttää, kuinka Boost-kirjaston avulla voidaan kohtuullisen kivuttomasti liittää C++ ja Python yhteen (olisi tässä kyllä melkeinpä kahteen vinkkiin ollut ainesta).
C++ osuuden kääntäminen onnistuu (YMMV) komennolla:
g++ -o astarcpp.so -Wall -I/usr/include/python2.6/ -lboost_python -lpython2.6 -fPIC -shared astar.cpp
Riippuvuuksia:
- Boost (http://www.boost.org/)
- Python (http://www.python.org/)
(eikä sitten tartte kertoa, että mie en osaa Pyyttonia ja/tai C++:aa ... ;-)
astar.cpp
#include <iostream> #include <string> #include <vector> #include <algorithm> #include <boost/foreach.hpp> #include <boost/python.hpp> #define foreach BOOST_FOREACH #define reverse_foreach BOOST_REVERSE_FOREACH // spice things up using namespace std; using namespace boost; /** * PathNode is used for in the A* algorithm to represent * different locations. */ struct PathNode { PathNode() : f(0), g(0), h(0), x(0), y(0), parent(NULL) {} PathNode(int x, int y) : f(0), g(0), h(0), x(x), y(y), parent(NULL) {} bool operator<(const PathNode& node) { return f < node.f; } bool operator>(const PathNode& node) { return f > node.f; } bool operator==(const PathNode& node) { return (x == node.x && y == node.y); } bool operator!=(const PathNode& node) { return !operator==(node); } void update() { f = g + h; } // f = g + h int f; // the cost of movement from starting node to this node along the // generated path int g; // the cost of movement from this node to the goal node ("heuristic") int h; // this node's location int x, y; // used for traversing the generated path PathNode *parent; }; struct PathNodePtrCmp: public binary_function<PathNode*, PathNode*, bool> { bool operator()(PathNode* a, PathNode* b) { return (*a > *b); } }; // a little helper struct to manage our nodes struct PathNodeHeap { void push(PathNode* v) { heap.push_back(v); push_heap(heap.begin(), heap.end(), PathNodePtrCmp()); } PathNode* pop() { pop_heap(heap.begin(), heap.end(), PathNodePtrCmp()); PathNode* res = heap.back(); heap.pop_back(); return res; } PathNode* top() { return heap.front(); } PathNode* find(int x, int y) { PathNode* node; foreach(node, heap) { if (node->x == x && node->y == y) { return node; } } return NULL; } void sort() { sort_heap(heap.begin(), heap.end(), PathNodePtrCmp()); } bool empty() { return heap.empty(); } vector<PathNode*> heap; }; // here is the algorithm python::object search_path(python::object map, python::tuple start, python::tuple goal) { PathNodeHeap open, closed; PathNode* current = NULL; PathNode* first = new PathNode(python::extract<int>(start[0]), python::extract<int>(start[1])); PathNode dest = PathNode(python::extract<int>(goal[0]), python::extract<int>(goal[1])); open.push(first); // stop, when the open list is empty (path was not found) while (!open.empty()) { // take the node that has the lowest f-score from the open list // and switch it to the closed list current = open.pop(); closed.push(current); // when the goal node is switched to the closed list, // we have found our path if (*current == dest) { break; } // we'll want to check every 8 directions int directions[][2] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {1, 1}, {1, -1}, {-1, 1} }; for (int i = 0; i < 8; ++i) { int dx = directions[i][0]; int dy = directions[i][1]; int x = current->x + dx; int y = current->y + dy; bool walkable = python::extract<bool>( map.attr("walkable")(x, y)); // ignore if on closed list and if not walkable if (!closed.find(x, y) && walkable) { PathNode* node = open.find(x, y); if (node == NULL) { // node wasn't on the open list, // so add it to there node = new PathNode(x, y); node->parent = current; // the cost of movement is bigger when movement is diagonal if (i > 3) { node->g = current->g + 14; } else { node->g = current->g + 10; } node->h = abs(dest.x - x) + abs(dest.y - y); node->update(); open.push(node); } else { // node was found from the open list, // check if current path is better ... int newg = current->g; if (i > 3) { newg += 14; } else { newg += 10; } if (newg < node->g) { node->g = newg; node->parent = current; node->update(); open.sort(); } } } } // if current is NULL after this loop, there was no path found current = NULL; } // collect the results in a nice python list python::list result; while (current) { result.append(python::make_tuple(current->x, current->y)); current = current->parent; } foreach (PathNode *p, open.heap) { delete p; } foreach (PathNode *p, closed.heap) { delete p; } return result; } BOOST_PYTHON_MODULE(astarcpp) { python::def("search", search_path); }
astar-test.py
#!/usr/bin/env python from __future__ import print_function import astarcpp class GameMap: def __init__(self, filename): self.data = [] self.load(filename) def load(self, filename): f = open(filename) lines = f.readlines() f.close() for line in lines: line = line.strip() tmp = map(ord, line) self.data.append(tmp) self.w = len(self.data[0]) self.h = len(self.data) def validCoord(self, x, y): return (x >= 0 and x < self.w and y >= 0 and y < self.h) # this must be implemented, because it's called from C++ def walkable(self, x, y): return (self.validCoord(x, y) and self.data[y][x] != ord("#")) def __str__(self): tmprepr = [] repr = "" for line in data: tmprepr.append("".join(map(chr, line))) repr = "\n".join(tmprepr) return repr def findpath(self, start, goal): return astarkikkare.search(self, start, goal) def print_with_path(self, start, goal): path = self.findpath(start, goal) for y in range(len(self.data)): for x in range(len(self.data[0])): if (x, y) in path: print("x", sep="", end="") else: print(chr(self.data[y][x]), sep="", end="") print() a = GameMap("astar-test.map") a.print_with_path((19,1), (1, 10))
astar-test.map
##################### #### # # # #### ### #### # ##### ## ### ## # # ## ### # # # # # # ## # # ## # # # ## # # # ## # # # ### # # # # # ### # # # #### # # ## # # #### # # # #### #####################
bool walkable = python::extract<bool>( map.attr("walkable")(x, y));
Tämä vaikuttaa sangen epäoptimaaliselta operaatiolta algoritmin sisäsilmukassa. Olisi tehokkaampaa lukea koko kartta kerralla C++-koodin puolelle.
if (i > 3) { node->g = current->g + 14; } else { node->g = current->g + 10; } node->h = abs(dest.x - x) + abs(dest.y - y);
Tämä on täysin ristiriitaista: lasket nykyisen kuljetun matkan 10-kertaisena, mutta sijainnin hyvyysfunktion arvon määrität suoraan manhattan-distancena loppupisteeseen.
Tässä on siis kaksi virheoletusta: Ensiksikin koodissa oletetaan hyvyysfunktion laskussa, että sivuttain liikkuminen on mahdotonta. Toiseksi et suorita siinä samaa kymmenellä kertomista, kuin kuljetun matkan laskussa. Nämä virheet yhdistettynä kyllä sattuvat toimimaan oikein useimmissa tapauksissa, mutta nyt koodi ei ole suorituskyvyltään juuri Dijkstran algoritmia parempi.
Lisähidasteina koodissa ovat vielä sisäsilmukassa suoritettavat open.find() ja closed.find() -funktiokutsut, jotka vievät pahimmillaan lineaarisen ajan, ja jotka voitaisiin korvata esim. kartan kokoisella taulukolla tai hajautustaululla.
Koodivinkissä ainoaa järkevää sisältöä on lähinnä boostin avulla tapahtuva C++:n ja pythonin yhdistäminen, joten olisi ollut parempi, jos se olisi nostettu enemmän pääaiheeksi huonon A*-toteutuksen sijaan.
Tjoo, ton 10-kertaisuuden vetäisin suoraan tuolta artikkelista johon viittasin, mutta en tietysti tuota hyvyysarvoa tajunnut ottaa mukaan. Mitä tarkoitat tuolla, että "sivuttain liikkuminen on mahdotonta"?
Mielessä kävi tuo taulukkototeutus listalta etsimisen sijaan kyllä, mutten sitä tähän jostain syystä kirjoitellut. Täytynee tutkailla. :-)
Ääh, piti siis kirjoittaa vinottain eikä sivuttain liikkuminen.
Jos etäisyys lasketaan abs(x-ex)+abs(y-ey), niin silloin etäisyysarvio maaliin on liian pessimistinen, sillä esim. tapauksessa: x=1,y=1,ex=2,ey=2 etäisyys olisi vain sqrt(2)=1,4142..., mutta tuo kaava antaa tuloksen 2. (Tässä siis (x,y) on nykyinen sijainti ja (ex,ey) maalipiste.)
A*-algoritmissa maalin etäisyyden arvioinnissa on tärkeää, että etäisyysarvio on optimistinen, eli se ei saa olla koskaan pidempi kuin todellinen etäisyys. Jos tämä ei toteudu, ei tuloksena välttämättä saada lyhintä reittiä.
Niinjuu. Kyllähän miekin sen ymmärsin tuota tehdessä, ettei tuo kaava anna ihan täydellistä arviota siitä matkasta. Olettaisin kuitenkin, että peruspikkupeleihin tuo soveltuu paremman puutteessa ihan riittävästi. :-)
Miksi teit vertailua varten luokan etkä funktiota? Eikö
inline bool PathNodePtrCmp(PathNode* a, PathNode* b) { return (*a > *b); }
olisi ollut järkevämpi, kun kuitenkin kutsut vain yhtä luokan sisäfunktiota.
Tjaa-a. Aika hyvä kysymys. Ei varmaan käynyt mielessä.. :-P
Koska em. virheitä ei vinkistä ikinä korjattu ja koska vinkki muutenkin oli hieman sekava, kirjoitin itse uuden version.
Aihe on jo aika vanha, joten et voi enää vastata siihen.