Last active
May 25, 2024 08:39
-
-
Save dacr/391e30a97ec78d15f7d19185f73a7b36 to your computer and use it in GitHub Desktop.
Playing with Breadth first search (bfs) algorithm. / published by https://github.com/dacr/code-examples-manager #244b6a6d-ee42-444e-bb4b-88464daed447/6d43951e5c24c5a94e7f7f5c43f1acba0b25b786
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// summary : Playing with Breadth first search (bfs) algorithm. | |
// keywords : scala, algorithm, scalatest, bfs, search, @testable | |
// publish : gist | |
// authors : David Crosson | |
// license : Apache NON-AI License Version 2.0 (https://raw.githubusercontent.com/non-ai-licenses/non-ai-licenses/main/NON-AI-APACHE2) | |
// id : 244b6a6d-ee42-444e-bb4b-88464daed447 | |
// created-on : 2019-06-25T16:47:49Z | |
// managed-by : https://github.com/dacr/code-examples-manager | |
// run-with : scala-cli $file | |
// --------------------- | |
//> using scala "3.4.2" | |
//> using dep "org.scalatest::scalatest:3.2.10" | |
// --------------------- | |
import org.scalatest._, flatspec.AnyFlatSpec, matchers.should.Matchers | |
// ========================================================================== | |
// generated with https://www.dcode.fr/maze-generator | |
val repr = | |
"""############################### | |
|# # # # # # # # | |
|# # ### ##### # ### # ### ### # | |
|# # # # # # # | |
|# ### ### ### # # ### ### ##### | |
|# # # # # # # # # # | |
|# ##### # # ### # # # ### ### # | |
|# # # # # # # # # # | |
|##### # ### # # ### ######### # | |
|# # # S# # # # # | |
|# # # ### ####### # ### ### # # | |
|# # # # # # # # | |
|# # ### ####### ##### ####### # | |
|# # # # # # | |
|### ### # ##### ##### # ### ### | |
|# # # # # # # # # # | |
|# ### # ######### # ##### ### # | |
|# # # # # # # # # # | |
|# # # # ### ##### # ##### # # # | |
|# # # # # #E# | |
|###############################""".stripMargin | |
trait Part { | |
val repr: Char | |
} | |
object Floor extends Part { | |
override val repr = ' ' | |
} | |
object Wall extends Part { | |
override val repr = '#' | |
} | |
object Exit extends Part { | |
override val repr = 'E' | |
} | |
object Entry extends Part { | |
override val repr = 'S' | |
} | |
object Explored extends Part { | |
override val repr = 'o' | |
} | |
case class Position(y: Int, x: Int) { | |
def up = Position(y - 1, x) | |
def down = Position(y + 1, x) | |
def right = Position(y, x + 1) | |
def left = Position(y, x - 1) | |
override def toString: String = s"($y,$x)" | |
} | |
trait World { | |
def moves(from:Position):Iterable[Position] | |
} | |
case class Labyrinth(parts: Array[Array[Part]]) extends World { | |
val width = parts.headOption.map(_.size).getOrElse(0) | |
val height = parts.size | |
override def toString: String = { | |
parts.map(_.map(_.repr).mkString).mkString("\n") | |
} | |
def findFirst(part: Part): Option[Position] = { | |
val result = for { | |
(row, y) <- parts.zipWithIndex | |
(cell, x) <- row.zipWithIndex | |
if cell == part | |
} yield Position(y, x) | |
result.headOption | |
} | |
val entry: Option[Position] = findFirst(Entry) | |
val exit: Option[Position] = findFirst(Exit) | |
def isValidMove(pos: Position): Boolean = { | |
pos.x >= 0 && pos.x < width && pos.y >= 0 && pos.y < height | |
} | |
def partAt(pos: Position): Option[Part] = { | |
if (isValidMove(pos)) Option(parts(pos.y)(pos.x)) | |
else None | |
} | |
override def moves(from: Position): Iterable[Position] = { | |
List(from.up, from.down, from.left, from.right) | |
.filter(isValidMove) | |
.filterNot(pos => partAt(pos) == Some(Wall)) | |
} | |
def mark(pos: Position): Option[Labyrinth] = { | |
if (isValidMove(pos) && partAt(pos) == Some(Floor)) { | |
val clonedParts = parts.clone() | |
clonedParts(pos.y)(pos.x) = Explored | |
Some(Labyrinth(clonedParts)) | |
} else None | |
} | |
} | |
object Labyrinth { | |
def apply(input: String): Labyrinth = { | |
val data = input.split("\n").map { row => | |
row.map { | |
case Floor.repr => Floor | |
case Wall.repr => Wall | |
case Entry.repr => Entry | |
case Exit.repr => Exit | |
}.toArray | |
} | |
Labyrinth(data) | |
} | |
} | |
object LabyrinthTest extends AnyFlatSpec with Matchers { | |
override val suiteName = "LabyrinthTest" | |
"Labyrinth" should "be loadable from a string" in { | |
val lab = Labyrinth(repr) | |
lab.toString shouldBe repr | |
} | |
it should "have an entry and an exit" in { | |
val lab = Labyrinth(repr) | |
lab.entry shouldBe defined | |
//lab.exit shouldBe defined | |
} | |
it should "be possible to know possible moves from a valid position" in { | |
val lab = Labyrinth(repr) | |
val moves = lab.moves(Position(1, 1)) | |
moves should have size (1) | |
moves shouldBe List(Position(2, 1)) | |
} | |
it should "be markable for example to display explored path" in { | |
val lab = Labyrinth(repr) | |
lab | |
.mark(Position(2, 1)) | |
.toString | |
.split("\n")(2)(1) shouldBe 'o' | |
} | |
} | |
LabyrinthTest.execute() | |
// ========================================================================== | |
val lab = Labyrinth(repr) | |
type Solution = Vector[Position] | |
case class Work(currentPosition:Position, currentPath:Vector[Position]) | |
// get the first shortest path | |
def bfs_iterative(world:World, from:Position, goal:Position): Solution = { | |
var visited = Set.empty[Position] | |
var solutions = List.empty[Solution] | |
val works = scala.collection.mutable.Queue(Work(from, Vector(from))) | |
//while(!works.isEmpty) { | |
while(!works.isEmpty && solutions.isEmpty) { | |
val work = works.dequeue() | |
val current = work.currentPosition | |
if (current == goal) { | |
solutions ::= work.currentPath | |
} else { | |
visited += current | |
val nextMoves = world.moves(current).filterNot(visited.contains) | |
nextMoves.foreach { move => | |
works.enqueue(Work(move, work.currentPath :+ move)) | |
} | |
} | |
} | |
solutions.headOption.getOrElse(Vector.empty) | |
} | |
val solution= bfs_iterative(lab, lab.entry.get, lab.exit.get) // TODO - of course dangerous ;) | |
println("Solution length : "+solution.size) | |
println("Solution : "+solution) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment