/BigWhoop/MazeRunning/Maze.php

https://github.com/bigwhoop/Maze-Routing-Algorithm · PHP · 253 lines · 148 code · 54 blank · 51 comment · 27 complexity · d048e2eba3a60699b5ce77c0edbb82e2 MD5 · raw file

  1. <?php
  2. namespace BigWhoop\MazeRunning;
  3. class Maze
  4. {
  5. /**
  6. * @var array
  7. */
  8. private $grid = array();
  9. /**
  10. * @var Point
  11. */
  12. private $startPoint = null;
  13. /**
  14. * @var Point
  15. */
  16. private $finishPoint = null;
  17. /**
  18. * @var bool
  19. */
  20. private $isScored = false;
  21. /**
  22. * @static
  23. * @param string $path
  24. * @return Maze
  25. */
  26. static public function createFromImage($path)
  27. {
  28. $img = imagecreatefrompng($path);
  29. $width = imagesx($img);
  30. $height = imagesy($img);
  31. $grid = array();
  32. for ($y = 0; $y < $height; $y++) {
  33. for ($x = 0; $x < $width; $x++) {
  34. $color = imagecolorat($img, $x, $y);
  35. switch ($color)
  36. {
  37. case 255 : $type = Point::TYPE_START; break; // Blue
  38. case 16776960 : $type = Point::TYPE_FINISH; break; // Yellow
  39. case 0 : $type = Point::TYPE_WALL; break; // Black
  40. case 16777215 : // White
  41. default : $type = Point::TYPE_PATH; break;
  42. }
  43. $point = new Point($x, $y, $type);
  44. if (!isset($grid[$y])) {
  45. $grid[$y] = array();
  46. }
  47. $grid[$y][$x] = $point;
  48. }
  49. }
  50. return new Maze($grid);
  51. }
  52. /**
  53. * throws \OutOfBoundsException
  54. * @param array $grid
  55. */
  56. private function __construct(array $grid)
  57. {
  58. foreach ($grid as $xs) {
  59. foreach ($xs as $point) {
  60. $type = $point->getType();
  61. if ($type == Point::TYPE_START) {
  62. $this->startPoint = $point;
  63. } elseif ($type == Point::TYPE_FINISH) {
  64. $this->finishPoint = $point;
  65. }
  66. }
  67. }
  68. if (!$this->startPoint) {
  69. throw new \OutOfBoundsException('No start point found.');
  70. }
  71. if (!$this->finishPoint) {
  72. throw new \OutOfBoundsException('No finish point found.');
  73. }
  74. $this->grid = $grid;
  75. }
  76. /**
  77. * @return array
  78. */
  79. public function getGrid()
  80. {
  81. return $this->grid;
  82. }
  83. /**
  84. * @return Maze
  85. */
  86. public function scoreGrid()
  87. {
  88. if (!$this->isScored) {
  89. $this->scorePathsRecursively(array($this->startPoint), 0);
  90. $this->isScored = true;
  91. }
  92. return $this;
  93. }
  94. /**
  95. * @param array $paths
  96. * @param int $score
  97. */
  98. private function scorePathsRecursively(array $paths, $score)
  99. {
  100. if (empty($paths)) {
  101. return;
  102. }
  103. $upcomingPaths = array();
  104. foreach ($paths as $path) {
  105. $path->setScore($score);
  106. if ($path->getType() == Point::TYPE_FINISH) {
  107. return;
  108. }
  109. foreach ($this->findConnectingPaths($path) as $connectingPath) {
  110. if (!$connectingPath->isScored()) {
  111. $upcomingPaths[] = $connectingPath;
  112. }
  113. }
  114. }
  115. $this->scorePathsRecursively($upcomingPaths, $score + 1);
  116. }
  117. /**
  118. * @return array
  119. */
  120. public function findRoute()
  121. {
  122. if (!$this->isScored) {
  123. $this->scoreGrid();
  124. }
  125. // We didn't reach the finish point :/
  126. if (!$this->finishPoint->isScored()) {
  127. return array();
  128. }
  129. return $this->findRouteRecursively($this->finishPoint);
  130. }
  131. /**
  132. * @param Point $point
  133. * @param array $route
  134. * @return array
  135. */
  136. private function findRouteRecursively(Point $point, array $route = array())
  137. {
  138. $nextPoint = null;
  139. foreach ($this->findConnectingPaths($point) as $nextPointCandidate) {
  140. // If possbile next point is the start, we found what we're looking for
  141. if ($nextPointCandidate->getType() == Point::TYPE_START) {
  142. return array_reverse($route);
  143. }
  144. // Possible next point must be scored
  145. if (!$nextPointCandidate->isScored()) {
  146. continue;
  147. }
  148. // Possible next point's score must be below the current point's score
  149. if (!$nextPointCandidate->getScore() >= $point->getScore()) {
  150. continue;
  151. }
  152. // Possible next point's score must be below the current possible next point's score
  153. if (null === $nextPoint || $nextPointCandidate->getScore() < $nextPoint->getScore()) {
  154. $nextPoint = $nextPointCandidate;
  155. }
  156. }
  157. // Seems like we could not find the start. -> No route.
  158. if (!$nextPoint) {
  159. return array();
  160. }
  161. // Add next point to the route
  162. $route[] = $nextPoint;
  163. return $this->findRouteRecursively($nextPoint, $route);
  164. }
  165. /**
  166. * @param Point $point
  167. * @return array
  168. */
  169. private function findConnectingPaths(Point $point)
  170. {
  171. $possiblePointTypes = array(Point::TYPE_START, Point::TYPE_PATH, Point::TYPE_FINISH);
  172. if (!in_array($point->getType(), $possiblePointTypes)) {
  173. return array();
  174. }
  175. $offsets = array(
  176. array('x' => -1, 'y' => 0), // left of $point
  177. array('x' => 0, 'y' => -1), // above $point
  178. array('x' => 0, 'y' => 1), // below $point
  179. array('x' => 1, 'y' => 0), // right of $point
  180. );
  181. $connectingPaths = array();
  182. foreach ($offsets as $offset) {
  183. // Build x/y of connecting point
  184. $x = $point->getX() + $offset['x'];
  185. $y = $point->getY() + $offset['y'];
  186. if (!isset($this->grid[$y])) {
  187. continue;
  188. }
  189. if (!isset($this->grid[$y][$x])) {
  190. continue;
  191. }
  192. $connectingPoint = $this->grid[$y][$x];
  193. if (in_array($connectingPoint->getType(), $possiblePointTypes)) {
  194. $connectingPaths[] = $connectingPoint;
  195. }
  196. }
  197. return $connectingPaths;
  198. }
  199. }