├── project
├── build.properties
├── plugins.sbt
└── dependencies.scala
├── .scalafmt
├── .travis.yml
├── core
└── src
│ ├── main
│ └── scala
│ │ └── aima
│ │ └── core
│ │ ├── environment
│ │ ├── map2d
│ │ │ ├── InState.scala
│ │ │ ├── IntPercept.scala
│ │ │ ├── Go.scala
│ │ │ ├── LabeledGraph.scala
│ │ │ ├── Map2DFunctionFactory.scala
│ │ │ └── Map2D.scala
│ │ └── vacuum
│ │ │ ├── SimpleReflexVacuumAgentProgram.scala
│ │ │ ├── sensors.scala
│ │ │ ├── actions.scala
│ │ │ ├── percepts.scala
│ │ │ ├── actuators.scala
│ │ │ ├── TableDrivenVacuumAgentProgram.scala
│ │ │ ├── ModelBasedReflexVacuumAgentProgram.scala
│ │ │ └── VacuumEnvironment.scala
│ │ ├── agent
│ │ ├── Environment.scala
│ │ ├── SimpleReflexAgentProgram.scala
│ │ ├── TableDrivenAgentProgram.scala
│ │ ├── Sensor.scala
│ │ ├── Actuator.scala
│ │ ├── ModelBasedReflexAgentProgram.scala
│ │ ├── SimpleProblemSolvingAgentProgram.scala
│ │ ├── Agent.scala
│ │ └── basic
│ │ │ ├── LRTAStarAgent.scala
│ │ │ └── OnlineDFSAgent.scala
│ │ ├── search
│ │ ├── uninformed
│ │ │ ├── BreadthFirstSearch.scala
│ │ │ ├── IterativeDeepeningSearch.scala
│ │ │ ├── TreeSearch.scala
│ │ │ ├── GraphSearch.scala
│ │ │ ├── DepthLimitedTreeSearch.scala
│ │ │ ├── UniformCostSearch.scala
│ │ │ └── frontier.scala
│ │ ├── FrontierSearch.scala
│ │ ├── api
│ │ │ └── OnlineSearchProblem.scala
│ │ ├── adversarial
│ │ │ ├── Game.scala
│ │ │ └── MinimaxSearch.scala
│ │ ├── local
│ │ │ ├── HillClimbing.scala
│ │ │ ├── SimulatedAnnealingSearch.scala
│ │ │ └── GeneticAlgorithm.scala
│ │ ├── ProblemSearch.scala
│ │ ├── problems
│ │ │ ├── TwoPlayerGame.scala
│ │ │ └── romania.scala
│ │ ├── informed
│ │ │ └── RecursiveBestFirstSearch.scala
│ │ └── contingency
│ │ │ └── AndOrGraphSearch.scala
│ │ ├── fp
│ │ ├── Show.scala
│ │ └── Eqv.scala
│ │ └── random
│ │ └── Randomness.scala
│ └── test
│ └── scala
│ └── aima
│ └── core
│ ├── search
│ ├── problems
│ │ └── RomaniaRoadMapSpec.scala
│ ├── uninformed
│ │ ├── RomaniaUniformCostSearchSpec.scala
│ │ ├── RomaniaBreadthFirstSearchSpec.scala
│ │ ├── RomaniaIterativeDeepeningSearchSpec.scala
│ │ └── RomaniaDepthLimitedTreeSearchSpec.scala
│ ├── adversarial
│ │ └── MinimaxSearchSpec.scala
│ ├── local
│ │ ├── GeneticAlgorithmSpec.scala
│ │ ├── HillClimbingSpec.scala
│ │ └── SimulatedAnnealingSearchSpec.scala
│ └── contingency
│ │ └── AndOrGraphSearchSpec.scala
│ ├── environment
│ ├── vacuum
│ │ ├── SimpleReflexVacuumAgentSpec.scala
│ │ ├── ReflexVacuumAgentProgramSpec.scala
│ │ ├── ModelBasedReflexVacuumAgentSpec.scala
│ │ ├── TableDrivenVacuumAgentSpec.scala
│ │ └── VacuumMapSpec.scala
│ └── map2d
│ │ └── LabeledGraphSpec.scala
│ └── agent
│ └── basic
│ ├── LRTAStarAgentSpec.scala
│ └── OnlineDFSAgentSpec.scala
├── .gitignore
├── README.md
└── LICENSE
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.3.3
--------------------------------------------------------------------------------
/.scalafmt:
--------------------------------------------------------------------------------
1 | version = "2.3.2"
2 | align = most
3 | maxColumn = 120
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: trusty
2 | language: scala
3 | scala:
4 | - 2.13.1
5 | jdk:
6 | - openjdk11
7 | cache:
8 | directories:
9 | - $HOME/.m2/repository
10 | - $HOME/.sbt
11 | - $HOME/.ivy2
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/map2d/InState.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.map2d
2 |
3 | /**
4 | * @author Shawn Garner
5 | */
6 | final case class InState(location: String) extends AnyVal
7 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/map2d/IntPercept.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.map2d
2 |
3 | /**
4 | * @author Shawn Garner
5 | */
6 | final case class IntPercept(value: Int) extends AnyVal
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 | *.log
3 |
4 | # sbt specific
5 | .cache
6 | .history
7 | .lib/
8 | dist/*
9 | target/
10 | lib_managed/
11 | src_managed/
12 | project/boot/
13 | project/plugins/project/
14 |
15 | # Scala-IDE specific
16 | .scala_dependencies
17 | .worksheet
18 |
19 | .idea/
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/map2d/Go.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.map2d
2 |
3 | /**
4 | * @author Shawn Garner
5 | */
6 | sealed trait Map2DAction
7 | case object NoOp extends Map2DAction
8 | final case class Go(gotTo: String) extends Map2DAction
9 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.3.0")
2 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1")
3 | addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.6")
4 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.31")
5 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.1")
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/agent/Environment.scala:
--------------------------------------------------------------------------------
1 | package aima.core.agent
2 |
3 | /**
4 | * @author Shawn Garner
5 | */
6 | trait Environment[ENVIRONMENT, PERCEPT, ACTION] {
7 | def addAgent(agent: Agent[ENVIRONMENT, PERCEPT, ACTION]): ENVIRONMENT
8 | def removeAgent(agent: Agent[ENVIRONMENT, PERCEPT, ACTION]): ENVIRONMENT
9 | }
10 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/uninformed/BreadthFirstSearch.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.uninformed
2 |
3 | import aima.core.search._
4 |
5 | /**
6 | * @author Shawn Garner
7 | */
8 | trait BreadthFirstSearch[State, Action] extends GraphSearch[State, Action] { self =>
9 | def newFrontier(state: State, noAction: Action) = new FIFOQueueFrontier(StateNode(state, noAction, None))
10 | }
11 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/fp/Show.scala:
--------------------------------------------------------------------------------
1 | package aima.core.fp
2 |
3 | /**
4 | * @author Shawn Garner
5 | */
6 | trait Show[A] {
7 | def show(a: A): String
8 | }
9 |
10 | object Show {
11 | def apply[A: Show]: Show[A] = implicitly[Show[A]]
12 |
13 | object Implicits {
14 | final implicit class ShowOps[A: Show](a: A) {
15 | def show: String = Show[A].show(a)
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/project/dependencies.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 | import Keys._
3 | import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._
4 | import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
5 |
6 | object dependencies {
7 | val specs2 = "4.8.1"
8 | val scalacheck = "1.14.2"
9 |
10 | def commonDependencies =
11 | Seq(
12 | libraryDependencies ++= Seq(
13 | "org.specs2" %%% "specs2-core" % specs2 % Test,
14 | "org.specs2" %%% "specs2-scalacheck" % specs2 % Test,
15 | "org.scalacheck" %%% "scalacheck" % scalacheck % Test
16 | )
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/vacuum/SimpleReflexVacuumAgentProgram.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.vacuum
2 |
3 | import aima.core.agent.SimpleReflexAgentProgram
4 |
5 | /**
6 | * @author Shawn Garner
7 | */
8 | class SimpleReflexVacuumAgentProgram extends SimpleReflexAgentProgram[VacuumPercept, VacuumAction, VacuumPercept] {
9 | val interpretInput: InterpretInput = identity
10 |
11 | val rules: RuleMatch = {
12 | case DirtyPercept => Suck
13 | case LocationAPercept => RightMoveAction
14 | case LocationBPercept => LeftMoveAction
15 | case CleanPercept => NoAction
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/agent/SimpleReflexAgentProgram.scala:
--------------------------------------------------------------------------------
1 | package aima.core.agent
2 |
3 | /**
4 | * @author Shawn Garner
5 | */
6 | trait SimpleReflexAgentProgram[PERCEPT, ACTION, STATE] extends AgentProgram[PERCEPT, ACTION] {
7 | type InterpretInput = PERCEPT => STATE
8 |
9 | type RuleMatch = STATE => ACTION
10 |
11 | val agentFunction: AgentFunction = { percept =>
12 | val state = interpretInput(percept)
13 | ruleMatch(state)
14 | }
15 |
16 | def rules: RuleMatch
17 |
18 | def ruleMatch(state: STATE): ACTION = {
19 | rules(state)
20 | }
21 |
22 | def interpretInput: InterpretInput
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/test/scala/aima/core/search/problems/RomaniaRoadMapSpec.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.problems
2 |
3 | import org.specs2.mutable.Specification
4 | import Romania._
5 |
6 | /**
7 | * @author Shawn Garner
8 | */
9 | class RomaniaRoadMapSpec extends Specification {
10 |
11 | "Arad name" in {
12 | Arad.name must_== "Arad"
13 | }
14 |
15 | "Arad Connections" in {
16 | roadsFromCity.getOrElse(Arad, List.empty[Road]).map(_.to) must_== List(Sibiu, Timisoara, Zerind)
17 | }
18 |
19 | "Bucharest Connections" in {
20 | roadsFromCity.getOrElse(Bucharest, List.empty[Road]).map(_.to) must_== List(Giurgiu, Urziceni, Fagaras, Pitesti)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/agent/TableDrivenAgentProgram.scala:
--------------------------------------------------------------------------------
1 | package aima.core.agent
2 |
3 | import scala.collection.mutable
4 |
5 | /**
6 | * @author Shawn Garner
7 | */
8 | trait TableDrivenAgentProgram[PERCEPT, ACTION] extends AgentProgram[PERCEPT, ACTION] {
9 | type LookupTable = List[PERCEPT] => ACTION
10 |
11 | val percepts = new mutable.ListBuffer[PERCEPT]()
12 |
13 | def lookupTable: LookupTable
14 |
15 | val agentFunction: AgentFunction = { percept =>
16 | percepts += percept
17 | lookup(lookupTable, percepts.toList)
18 | }
19 |
20 | def lookup(lt: LookupTable, percepts: List[PERCEPT]): ACTION = {
21 | lt(percepts)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/agent/Sensor.scala:
--------------------------------------------------------------------------------
1 | package aima.core.agent
2 |
3 | import aima.core.random.{DefaultRandomness, Randomness}
4 |
5 | trait Sensor[ENVIRONMENT, PERCEPT] {
6 | def perceive(e: ENVIRONMENT): Option[PERCEPT]
7 | }
8 |
9 | object UnreliableSensor {
10 | def fromSensor[ENVIRONMENT, PERCEPT](
11 | sensor: Sensor[ENVIRONMENT, PERCEPT]
12 | )(
13 | reliability: Int = 50,
14 | randomness: Randomness = new DefaultRandomness {}
15 | ): Sensor[ENVIRONMENT, PERCEPT] = new Sensor[ENVIRONMENT, PERCEPT] {
16 | override def perceive(e: ENVIRONMENT): Option[PERCEPT] =
17 | randomness.unreliably(reliability)(sensor.perceive(e)).flatten
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/fp/Eqv.scala:
--------------------------------------------------------------------------------
1 | package aima.core.fp
2 |
3 | /**
4 | * @author Shawn Garner
5 | */
6 | /**
7 | * @author Shawn Garner
8 | */
9 | trait Eqv[A] {
10 | def eqv(a1: A, a2: A): Boolean
11 | }
12 |
13 | object Eqv {
14 | def apply[A: Eqv]: Eqv[A] = implicitly[Eqv[A]]
15 |
16 | object Implicits {
17 | final implicit class EqvOps[A: Eqv](a1: A) {
18 | val equiv = Eqv[A]
19 | import equiv.eqv
20 | def ===(a2: A): Boolean = eqv(a1, a2)
21 | def =!=(a2: A): Boolean = !eqv(a1, a2)
22 | }
23 |
24 | implicit val stringEq: Eqv[String] = new Eqv[String] {
25 | override def eqv(a1: String, a2: String): Boolean = a1 == a2
26 | }
27 |
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/vacuum/sensors.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.vacuum
2 |
3 | import aima.core.agent.{Agent, Sensor}
4 |
5 | /**
6 | * @author Shawn Garner
7 | */
8 | class AgentLocationSensor(val agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction])
9 | extends Sensor[VacuumEnvironment, VacuumPercept] {
10 | def perceive(vacuum: VacuumEnvironment): Option[VacuumPercept] =
11 | vacuum.map.getAgentLocation(agent)
12 | }
13 |
14 | class DirtSensor(val agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction])
15 | extends Sensor[VacuumEnvironment, VacuumPercept] {
16 | def perceive(vacuum: VacuumEnvironment): Option[VacuumPercept] =
17 | vacuum.map.getDirtStatus(agent)
18 | }
19 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/FrontierSearch.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search
2 |
3 | import scala.collection.immutable.Iterable
4 |
5 | trait Frontier[State, Action, Node <: SearchNode[State, Action]] {
6 | def replaceByState(childNode: Node): Frontier[State, Action, Node]
7 | def getNode(state: State): Option[Node]
8 | def removeLeaf: Option[(Node, Frontier[State, Action, Node])]
9 | def add(node: Node): Frontier[State, Action, Node]
10 | def addAll(iterable: Iterable[Node]): Frontier[State, Action, Node]
11 | def contains(state: State): Boolean
12 | }
13 |
14 | trait FrontierSearch[State, Action, Node <: SearchNode[State, Action]] {
15 | def newFrontier(state: State, noAction: Action): Frontier[State, Action, Node]
16 | }
17 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/agent/Actuator.scala:
--------------------------------------------------------------------------------
1 | package aima.core.agent
2 |
3 | import aima.core.random.{DefaultRandomness, Randomness}
4 |
5 | /**
6 | * @author Shawn Garner
7 | * @author Damien Favre
8 | */
9 | trait Actuator[ENVIRONMENT, ACTION] {
10 | def act(action: ACTION, e: ENVIRONMENT): ENVIRONMENT
11 | }
12 |
13 | object UnreliableActuator {
14 | def fromActuator[ENVIRONMENT, ACTION](
15 | actuator: Actuator[ENVIRONMENT, ACTION]
16 | )(
17 | reliability: Int = 50,
18 | randomness: Randomness = new DefaultRandomness {}
19 | ): Actuator[ENVIRONMENT, ACTION] = new Actuator[ENVIRONMENT, ACTION] {
20 | override def act(action: ACTION, e: ENVIRONMENT): ENVIRONMENT =
21 | randomness.unreliably(reliability)(actuator.act(action, e)).getOrElse(e)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/vacuum/actions.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.vacuum
2 |
3 | import aima.core.random.{DefaultRandomness, SetRandomness}
4 |
5 | /**
6 | * @author Shawn Garner
7 | */
8 | sealed trait VacuumAction
9 |
10 | sealed trait MoveAction extends VacuumAction
11 | case object LeftMoveAction extends MoveAction
12 | case object RightMoveAction extends MoveAction
13 |
14 | object MoveAction extends SetRandomness[MoveAction] with DefaultRandomness {
15 | val valueSet: Set[MoveAction] = Set(LeftMoveAction, RightMoveAction)
16 | }
17 |
18 | sealed trait SuckerAction extends VacuumAction
19 | case object Suck extends SuckerAction
20 |
21 | object SuckerAction extends SetRandomness[SuckerAction] with DefaultRandomness {
22 | val valueSet: Set[SuckerAction] = Set(Suck)
23 | }
24 |
25 | case object NoAction extends VacuumAction
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # aima-scala [](https://travis-ci.org/aimacode/aima-scala) [](https://gitter.im/aima-scala/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
2 | Scala implementation of algorithms from [Russell](http://www.cs.berkeley.edu/~russell/) And [Norvig's](http://www.norvig.com/) [Artificial Intelligence - A Modern Approach 3rd Edition](http://aima.cs.berkeley.edu/). You can use this in conjunction with a course on AI, or for study on your own.
3 |
4 | ## Formating the code
5 | ```bash
6 | sbt scalafmt
7 | ```
8 |
9 | ## Test Coverage
10 | ```
11 | sbt ";clean;test"
12 | sbt coverageReport
13 | ```
14 |
15 | reports are generated in the target/scoverage-report folder of the sub-projects
16 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/random/Randomness.scala:
--------------------------------------------------------------------------------
1 | package aima.core.random
2 |
3 | import scala.util.Random
4 |
5 | /**
6 | * @author Shawn Garner
7 | */
8 | trait Randomness {
9 | def rand: Random
10 | def unreliably[A](reliability: Int = 50)(a: => A): Option[A] =
11 | if (rand.nextInt(100) < reliability) Some(a)
12 | else None
13 | }
14 |
15 | trait DefaultRandomness extends Randomness {
16 | val rand = new Random()
17 | }
18 |
19 | trait EnumerationRandomness extends Randomness { self: Enumeration =>
20 |
21 | def randomValue: self.Value = {
22 | val selection = rand.nextInt(self.maxId)
23 | apply(selection)
24 | }
25 | }
26 |
27 | trait SetRandomness[T] extends Randomness {
28 | def valueSet: Set[T]
29 |
30 | def randomValue: T = {
31 | val selection = rand.nextInt(valueSet.size)
32 | val valueList = valueSet.toList
33 | valueList(selection)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/vacuum/percepts.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.vacuum
2 |
3 | import aima.core.random.{DefaultRandomness, SetRandomness}
4 |
5 | /**
6 | * @author Shawn Garner
7 | */
8 | sealed trait VacuumPercept
9 |
10 | sealed trait LocationPercept extends VacuumPercept
11 | case object LocationAPercept extends LocationPercept
12 | case object LocationBPercept extends LocationPercept
13 |
14 | object LocationPercept extends SetRandomness[LocationPercept] with DefaultRandomness {
15 | lazy val valueSet: Set[LocationPercept] = Set(LocationAPercept, LocationBPercept)
16 | }
17 |
18 | sealed trait DirtPercept extends VacuumPercept
19 | case object CleanPercept extends DirtPercept
20 | case object DirtyPercept extends DirtPercept
21 |
22 | object DirtPercept extends SetRandomness[DirtPercept] with DefaultRandomness {
23 | lazy val valueSet: Set[DirtPercept] = Set(CleanPercept, DirtyPercept)
24 | }
25 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/vacuum/actuators.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.vacuum
2 |
3 | import aima.core.agent.{Actuator, Agent}
4 |
5 | /**
6 | * @author Shawn Garner
7 | */
8 | class SuckerActuator(val agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction])
9 | extends Actuator[VacuumEnvironment, VacuumAction] {
10 | def act(action: VacuumAction, vacuum: VacuumEnvironment): VacuumEnvironment = action match {
11 | case Suck =>
12 | vacuum.copy(vacuum.map.updateStatus(agent, CleanPercept))
13 | case _ => vacuum
14 | }
15 | }
16 | class MoveActuator(val agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction])
17 | extends Actuator[VacuumEnvironment, VacuumAction] {
18 | def act(action: VacuumAction, vacuum: VacuumEnvironment): VacuumEnvironment = action match {
19 | case move: MoveAction =>
20 | vacuum.copy(vacuum.map.moveAgent(agent, move))
21 | case _ => vacuum
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/core/src/test/scala/aima/core/environment/vacuum/SimpleReflexVacuumAgentSpec.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.vacuum
2 |
3 | import org.specs2.mutable.Specification
4 | import org.specs2.specification.Scope
5 |
6 | /**
7 | * @author Shawn Garner
8 | */
9 | class SimpleReflexVacuumAgentSpec extends Specification {
10 |
11 | "should move right if location A" in new context {
12 | agent.agentFunction.apply(LocationAPercept) must beLike {
13 | case RightMoveAction => ok
14 | }
15 | }
16 |
17 | "should move left if location B" in new context {
18 | agent.agentFunction.apply(LocationBPercept) must beLike {
19 | case LeftMoveAction => ok
20 | }
21 | }
22 |
23 | "should suck if dirty" in new context {
24 | agent.agentFunction.apply(DirtyPercept) must beLike {
25 | case Suck => ok
26 | }
27 | }
28 |
29 | trait context extends Scope {
30 | val agent = new SimpleReflexVacuumAgentProgram
31 |
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/uninformed/IterativeDeepeningSearch.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.uninformed
2 |
3 | import aima.core.search.Problem
4 |
5 | import scala.annotation.tailrec
6 | import scala.util.{Failure, Success, Try}
7 |
8 | /**
9 | * @author Shawn Garner
10 | */
11 | trait IterativeDeepeningSearch[State, Action] {
12 | def depthLimitedTreeSearch: DepthLimitedTreeSearch[State, Action]
13 |
14 | def search(problem: Problem[State, Action], noAction: Action): Try[DLSResult[Action]] = {
15 | @tailrec def searchHelper(currentDepth: Int): Try[DLSResult[Action]] = {
16 | val result = depthLimitedTreeSearch.search(problem, currentDepth, noAction)
17 |
18 | result match {
19 | case Success(Solution(_)) | Failure(_) => result
20 | case _ if currentDepth == Int.MaxValue =>
21 | Failure[DLSResult[Action]](new Exception("Depth has reached Int.MaxValue"))
22 | case _ => searchHelper(currentDepth + 1)
23 | }
24 | }
25 |
26 | searchHelper(currentDepth = 0)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/core/src/test/scala/aima/core/search/uninformed/RomaniaUniformCostSearchSpec.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.uninformed
2 |
3 | import aima.core.search.CostNode
4 | import aima.core.search.problems.Romania._
5 | import org.specs2.mutable.Specification
6 | import org.specs2.specification.Scope
7 |
8 | import scala.reflect.ClassTag
9 |
10 | /**
11 | * @author Shawn Garner
12 | */
13 | class RomaniaUniformCostSearchSpec extends Specification {
14 |
15 | "going from Arad to Arad must return no actions" in new context {
16 | search(new RomaniaRoadProblem(In(Arad), In(Arad)), NoAction) must beEmpty
17 | }
18 |
19 | "going from Arad to Bucharest must return a list of actions" in new context {
20 | search(new RomaniaRoadProblem(In(Arad), In(Bucharest)), NoAction) must_== List(
21 | GoTo(Sibiu),
22 | GoTo(RimnicuVilcea),
23 | GoTo(Pitesti),
24 | GoTo(Bucharest)
25 | )
26 | }
27 |
28 | trait context extends Scope with UniformCostSearch[RomaniaState, RomaniaAction] {
29 | override implicit val nCT: ClassTag[CostNode[RomaniaState, RomaniaAction]] = cnCTag
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/agent/ModelBasedReflexAgentProgram.scala:
--------------------------------------------------------------------------------
1 | package aima.core.agent
2 |
3 | /**
4 | * @author Shawn Garner
5 | */
6 | trait ModelBasedReflexAgentProgram[PERCEPT, ACTION, STATE] extends AgentProgram[PERCEPT, ACTION] {
7 | type Model = (STATE, ACTION) => STATE //a description of how the next state depends on the current state and action
8 | type UpdateState = (STATE, ACTION, PERCEPT, Model) => STATE
9 | type RuleMatch = STATE => ACTION
10 |
11 | def initialState: STATE
12 | def noAction: ACTION
13 |
14 | var state: STATE = initialState // the agent's current conception of the world state
15 | var action: ACTION = noAction //most recent action
16 |
17 | val agentFunction: AgentFunction = { percept =>
18 | state = updateState(state, action, percept, model)
19 | action = ruleMatch(state)
20 | action
21 | }
22 |
23 | def rules: RuleMatch
24 | def model: Model // Figure depicts as persistent but doesn't define how to update it?
25 |
26 | def ruleMatch(state: STATE): ACTION = {
27 | rules(state)
28 | }
29 |
30 | def updateState: UpdateState
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 aimacode
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/agent/SimpleProblemSolvingAgentProgram.scala:
--------------------------------------------------------------------------------
1 | package aima.core.agent
2 |
3 | /**
4 | * @author Shawn Garner
5 | */
6 | trait SimpleProblemSolvingAgentProgram[PERCEPT, ACTION, STATE, GOAL, PROBLEM] extends AgentProgram[PERCEPT, ACTION] {
7 |
8 | def initialState: STATE
9 | def noAction: ACTION
10 |
11 | var actions = List.empty[ACTION]
12 | var state = initialState
13 |
14 | def agentFunction: AgentFunction = { percept =>
15 | state = updateState(state, percept)
16 | if (actions.isEmpty) {
17 | val goal = formulateGoal(state)
18 | val problem = formulateProblem(state, goal)
19 | actions = search(problem)
20 | }
21 |
22 | val (firstAction, restOfActions) = actions match {
23 | case Nil => (noAction, Nil)
24 | case first :: rest => (first, rest)
25 | }
26 |
27 | actions = restOfActions
28 | firstAction
29 | }
30 |
31 | def updateState(state: STATE, percept: PERCEPT): STATE
32 | def formulateGoal(state: STATE): GOAL
33 | def formulateProblem(state: STATE, goal: GOAL): PROBLEM
34 | def search(problem: PROBLEM): List[ACTION]
35 | }
36 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/api/OnlineSearchProblem.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.api
2 |
3 | /**
4 | * Artificial Intelligence A Modern Approach (4th Edition): page ??.
5 | *
6 | * An online search problem must be solved by an agent executing actions, rather
7 | * than by pure computation. We assume a deterministic and fully observable
8 | * environment (Chapter ?? relaxes these assumptions), but we stipulate that the
9 | * agent knows only the following:
10 | *
11 | * function HILL-CLIMBING(problem) returns a state that is a local maximum 12 | * 13 | * current ← MAKE-NODE(problem.INITIAL-STATE) 14 | * loop do 15 | * neighbor ← a highest-valued successor of current 16 | * if neighbor.VALUE ≤ current.VALUE then return current.STATE 17 | * current ← neighbor 18 | *19 | *
20 | *
21 | * @author Shawn Garner
22 | */
23 | object HillClimbing {
24 |
25 | final case class StateValueNode[State](state: State, value: Double)
26 |
27 | def apply[State, Action](stateToValue: State => Double)(problem: Problem[State, Action]): State = {
28 |
29 | def makeNode(state: State) = StateValueNode(state, stateToValue(state))
30 |
31 | def highestValuedSuccessor(current: StateValueNode[State]): StateValueNode[State] = {
32 | val successors = problem.actions(current.state).map(a => problem.result(current.state, a)).map(makeNode)
33 |
34 | if (successors.isEmpty) {
35 | current
36 | } else {
37 | successors.maxBy(_.value)
38 | }
39 |
40 | }
41 |
42 | @tailrec def recurse(current: StateValueNode[State]): State = {
43 | val neighbor = highestValuedSuccessor(current)
44 | if (neighbor.value <= current.value) {
45 | current.state
46 | } else {
47 | recurse(neighbor)
48 | }
49 | }
50 |
51 | recurse(makeNode(problem.initialState))
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/core/src/test/scala/aima/core/search/uninformed/RomaniaDepthLimitedTreeSearchSpec.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.uninformed
2 |
3 | import aima.core.search.StateNode
4 | import aima.core.search.problems.Romania._
5 | import org.specs2.mutable.Specification
6 | import org.specs2.specification.Scope
7 |
8 | import scala.reflect.ClassTag
9 |
10 | /**
11 | * @author Shawn Garner
12 | */
13 | class RomaniaDepthLimitedTreeSearchSpec extends Specification {
14 |
15 | "going from Arad to Arad must return no actions" in new context {
16 | search(new RomaniaRoadProblem(In(Arad), In(Arad)), 1, NoAction) must beSuccessfulTry.like {
17 | case Solution(Nil) => ok
18 | }
19 | }
20 |
21 | "going from Arad to Bucharest must return a list of actions with depth 19" in new context {
22 | search(new RomaniaRoadProblem(In(Arad), In(Bucharest)), 19, NoAction) must beSuccessfulTry.like {
23 | case Solution(GoTo(Sibiu) :: GoTo(RimnicuVilcea) :: GoTo(Pitesti) :: GoTo(Bucharest) :: Nil) => ok
24 | }
25 | }
26 |
27 | "going from Arad to Bucharest must return a list of actions with depth 9" in new context {
28 | search(new RomaniaRoadProblem(In(Arad), In(Bucharest)), 9, NoAction) must beSuccessfulTry.like {
29 | case Solution(GoTo(Sibiu) :: GoTo(RimnicuVilcea) :: GoTo(Pitesti) :: GoTo(Bucharest) :: Nil) => ok
30 | }
31 | }
32 |
33 | "going from Arad to Bucharest must return a Cuttoff with depth 1" in new context {
34 | search(new RomaniaRoadProblem(In(Arad), In(Bucharest)), 1, NoAction) must beSuccessfulTry.like {
35 | case CutOff(GoTo(Zerind) :: Nil) => ok
36 | }
37 | }
38 |
39 | trait context extends Scope with DepthLimitedTreeSearch[RomaniaState, RomaniaAction] {
40 | override implicit val nCT: ClassTag[StateNode[RomaniaState, RomaniaAction]] = snCTag
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/adversarial/MinimaxSearch.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.adversarial
2 |
3 | import scala.annotation.tailrec
4 |
5 | /**
6 | * Figure 5.3 MINIMAX-DECISION
7 | * @author Aditya Lahiri
8 | * @author Shawn Garner
9 | */
10 | object MinimaxDecision {
11 | import scala.math.Ordering.Double.IeeeOrdering
12 | def minMaxDecision[PLAYER, STATE, ACTION](
13 | g: Game[PLAYER, STATE, ACTION],
14 | noAction: ACTION
15 | ): (STATE) => ACTION = { (state: STATE) =>
16 | @tailrec def maxMinValue(Actions: List[ACTION]): ACTION = Actions match {
17 | case Nil => noAction
18 | case singularAction :: Nil => singularAction
19 | case action1 :: action2 :: rest =>
20 | maxMinValue(
21 | (if (minValue(g.result(state, action1)) > minValue(g.result(state, action2))) action1
22 | else action2) :: rest
23 | )
24 | }
25 |
26 | @tailrec def minMaxValue(Actions: List[ACTION]): ACTION = Actions match {
27 | case Nil => noAction
28 | case singularAction :: Nil => singularAction
29 | case action1 :: action2 :: rest =>
30 | minMaxValue(
31 | (if (maxValue(g.result(state, action1)) < maxValue(g.result(state, action2))) action1
32 | else action2) :: rest
33 | )
34 | }
35 |
36 | def maxValue(state: STATE): UtilityValue = {
37 | if (g.isTerminalState(state))
38 | g.getUtility(state)
39 | else
40 | minValue(g.result(state, maxMinValue(g.getActions(state))))
41 | }
42 |
43 | def minValue(state: STATE): UtilityValue = {
44 | if (g.isTerminalState(state))
45 | g.getUtility(state)
46 | else
47 | maxValue(g.result(state, minMaxValue(g.getActions(state))))
48 | }
49 |
50 | maxMinValue(g.getActions(g.initialState))
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/ProblemSearch.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search
2 |
3 | import scala.annotation.tailrec
4 | import scala.reflect.ClassTag
5 |
6 | trait Problem[State, Action] {
7 | def initialState: State
8 | def isGoalState(state: State): Boolean
9 | def actions(state: State): List[Action]
10 | def result(state: State, action: Action): State
11 | def stepCost(state: State, action: Action, childPrime: State): Int
12 | }
13 |
14 | sealed trait SearchNode[State, Action] {
15 | def state: State
16 | def action: Action
17 | def parent: Option[SearchNode[State, Action]]
18 | }
19 |
20 | final case class StateNode[State, Action](
21 | state: State,
22 | action: Action,
23 | parent: Option[StateNode[State, Action]]
24 | ) extends SearchNode[State, Action]
25 |
26 | final case class CostNode[State, Action](
27 | state: State,
28 | cost: Int,
29 | action: Action,
30 | parent: Option[CostNode[State, Action]]
31 | ) extends SearchNode[State, Action]
32 |
33 | final case class HeuristicsNode[State, Action](
34 | state: State,
35 | gValue: Double,
36 | hValue: Option[Double],
37 | fValue: Option[Double],
38 | action: Action,
39 | parent: Option[CostNode[State, Action]]
40 | ) extends SearchNode[State, Action]
41 |
42 | /**
43 | * @author Shawn Garner
44 | */
45 | trait ProblemSearch[State, Action, Node <: SearchNode[State, Action]] {
46 | implicit val nCT: ClassTag[Node]
47 |
48 | def newChildNode(problem: Problem[State, Action], parent: Node, action: Action): Node
49 |
50 | def solution(node: Node): List[Action] = {
51 | @tailrec def solutionHelper(n: Node, actions: List[Action]): List[Action] = {
52 | n.parent match {
53 | case None => actions
54 | case Some(parent: Node) => solutionHelper(parent, n.action :: actions)
55 | }
56 | }
57 |
58 | solutionHelper(node, Nil)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/core/src/test/scala/aima/core/search/local/GeneticAlgorithmSpec.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.local
2 |
3 | import aima.core.search.local.set.NonEmptySet
4 | import org.scalacheck.{Arbitrary, Gen}
5 | import org.specs2.ScalaCheck
6 | import org.specs2.mutable.Specification
7 |
8 | import scala.concurrent.duration._
9 |
10 | /**
11 | * @author Shawn Garner
12 | */
13 | class GeneticAlgorithmSpec extends Specification with GeneticAlgorithm[String] with ScalaCheck {
14 |
15 | import GeneticAlgorithm.StringIndividual._
16 |
17 | implicit val arbString: Arbitrary[String] = Arbitrary(
18 | Gen.listOfN(20, Gen.alphaChar).map(_.mkString)
19 | )
20 |
21 | implicit def arbSet[A: Arbitrary]: Arbitrary[NonEmptySet[A]] = Arbitrary {
22 | Gen.nonEmptyListOf(Arbitrary.arbitrary[A]).map(_.toSet).flatMap { set =>
23 | NonEmptySet.apply(set) match {
24 | case Right(nes) => Gen.const(nes)
25 | case Left(_) => Gen.fail[NonEmptySet[A]]
26 | }
27 | }
28 | }
29 |
30 | "must find strings of at least 50% vowels" >> prop { population: NonEmptySet[String] =>
31 | def vowelFitness(individual: String): Fitness = {
32 | val u = individual.foldLeft(0.0d) {
33 | case (acc, 'a' | 'e' | 'i' | 'o' | 'u' | 'y' | 'A' | 'E' | 'I' | 'O' | 'U' | 'Y') => acc + 1.0d
34 | case (acc, _) => acc
35 | }
36 | Fitness(u)
37 | }
38 |
39 | def fitEnough(individual: String): Boolean = {
40 | val length = individual.length
41 | if (length != 0) {
42 | vowelFitness(individual).value / length >= 0.50d
43 | } else {
44 | false
45 | }
46 | }
47 |
48 | val fitIndividual =
49 | geneticAlgorithm(population, vowelFitness)(fitEnough, 2.minutes, reproduce2, Probability(0.05), mutate)
50 | val fitness = vowelFitness(fitIndividual).value / fitIndividual.length
51 | fitness aka fitIndividual must be greaterThanOrEqualTo 0.50d
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/uninformed/GraphSearch.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.uninformed
2 |
3 | import aima.core.search._
4 |
5 | import scala.annotation.tailrec
6 | import scala.reflect.ClassTag
7 |
8 | /**
9 | * @author Shawn Garner
10 | */
11 | trait GraphSearch[State, Action]
12 | extends ProblemSearch[State, Action, StateNode[State, Action]]
13 | with FrontierSearch[State, Action, StateNode[State, Action]] {
14 | implicit val sCT: ClassTag[State]
15 | implicit val aCT: ClassTag[Action]
16 |
17 | type Node = StateNode[State, Action]
18 |
19 | def search(problem: Problem[State, Action], noAction: Action): List[Action] = {
20 | val initialFrontier = newFrontier(problem.initialState, noAction)
21 |
22 | @tailrec def searchHelper(
23 | frontier: Frontier[State, Action, Node],
24 | exploredSet: Set[State] = Set.empty[State]
25 | ): List[Action] = {
26 | frontier.removeLeaf match {
27 | case None => List.empty[Action]
28 | case Some((leaf, _)) if problem.isGoalState(leaf.state) => solution(leaf)
29 | case Some((leaf, updatedFrontier)) =>
30 | val updatedExploredSet = exploredSet + leaf.state
31 | val childNodes = for {
32 | action <- problem.actions(leaf.state)
33 | childNode = newChildNode(problem, leaf, action)
34 | if !(updatedExploredSet.contains(childNode.state)
35 | || updatedFrontier.contains(childNode.state))
36 | } yield childNode
37 |
38 | val frontierWithChildNodes = updatedFrontier.addAll(childNodes)
39 |
40 | searchHelper(frontierWithChildNodes, updatedExploredSet)
41 | }
42 | }
43 |
44 | searchHelper(initialFrontier)
45 | }
46 |
47 | def newChildNode(problem: Problem[State, Action], parent: Node, action: Action): Node = {
48 | val childState = problem.result(parent.state, action)
49 | StateNode(childState, action, Some(parent))
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/map2d/LabeledGraph.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.map2d
2 |
3 | /**
4 | * @author Shawn Garner
5 | */
6 | final class LabeledGraph[Vertex, Edge] {
7 | import scala.collection.mutable
8 | val globalEdgeLookup = new mutable.LinkedHashMap[Vertex, mutable.LinkedHashMap[Vertex, Edge]]() // TODO: get rid of mutability; ListMap should work
9 | val vertexLabelsList = new mutable.ArrayBuffer[Vertex]() // TODO: get rid of mutability
10 |
11 | def addVertex(v: Vertex): Unit = {
12 | checkForNewVertex(v)
13 | ()
14 | }
15 |
16 | def set(from: Vertex, to: Vertex, edge: Edge): Unit = {
17 | val localEdgeLookup = checkForNewVertex(from)
18 | localEdgeLookup.put(to, edge)
19 | checkForNewVertex(to)
20 | ()
21 | }
22 |
23 | def remove(from: Vertex, to: Vertex): Unit = {
24 | val localEdgeLookup = globalEdgeLookup.get(from)
25 | localEdgeLookup.foreach(l => l.remove(to))
26 | }
27 |
28 | def get(from: Vertex, to: Vertex): Option[Edge] = {
29 | val localEdgeLookup = globalEdgeLookup.get(from)
30 | localEdgeLookup.flatMap(_.get(to))
31 | }
32 |
33 | def successors(v: Vertex): List[Vertex] = {
34 | val localEdgeLookup = globalEdgeLookup.get(v)
35 | localEdgeLookup.toList.flatMap(_.keySet.toList)
36 | }
37 |
38 | def vertexLabels =
39 | vertexLabelsList.toList
40 |
41 | def isVertexLabel(v: Vertex): Boolean =
42 | globalEdgeLookup.get(v).isDefined
43 |
44 | def clear(): Unit = {
45 | vertexLabelsList.clear()
46 | globalEdgeLookup.clear()
47 | }
48 |
49 | private def checkForNewVertex(v: Vertex): mutable.LinkedHashMap[Vertex, Edge] = {
50 | val maybeExisting = globalEdgeLookup.get(v)
51 | maybeExisting match {
52 | case None =>
53 | val m = new mutable.LinkedHashMap[Vertex, Edge]
54 | globalEdgeLookup.put(v, m)
55 | vertexLabelsList.append(v)
56 | m
57 | case Some(existing) =>
58 | existing
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/map2d/Map2DFunctionFactory.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.map2d
2 |
3 | import aima.core.search.CostNode
4 | import Ordering.Double.TotalOrdering
5 |
6 | /**
7 | * @author Shawn Garner
8 | */
9 | object Map2DFunctionFactory {
10 | import aima.core.fp.Eqv
11 | import Eqv.Implicits._
12 |
13 | def actions(map2D: Map2D): InState => List[Map2DAction] =
14 | inState => map2D.locationsLinkedTo(inState.location).map(Go)
15 |
16 | def stepCost(map2D: Map2D): (InState, Map2DAction, InState) => Double =
17 | (s, _, sPrime) => map2D.distance(s.location, sPrime.location).map(_.value).getOrElse(Double.MaxValue)
18 |
19 | val result: (InState, Map2DAction) => InState =
20 | (s, action) =>
21 | action match {
22 | case Go(goTo) => InState(goTo)
23 | case NoOp => s
24 | }
25 |
26 | def goalTestPredicate(goalLocations: String*): InState => Boolean =
27 | inState => goalLocations.exists(location => location === inState.location)
28 |
29 | object StraightLineDistanceHeuristic {
30 |
31 | def apply(map2D: Map2D, goals: String*): CostNode[InState, Map2DAction] => Double =
32 | node => {
33 |
34 | def h(state: InState): Double = {
35 | val distances: List[Double] = goals.toList.flatMap { goal =>
36 | val distance: Option[Double] = for {
37 | currentPosition <- map2D.position(state.location)
38 | goalPosition <- map2D.position(goal)
39 | } yield distanceOf(currentPosition, goalPosition)
40 |
41 | distance.toList
42 | }
43 |
44 | distances match {
45 | case Nil => Double.MaxValue
46 | case _ => distances.min
47 | }
48 |
49 | }
50 |
51 | def distanceOf(p1: Point2D, p2: Point2D): Double = {
52 | math.sqrt(
53 | (p1.x - p2.x) * (p1.x - p2.x)
54 | + (p1.y - p2.y) * (p1.y - p2.y)
55 | )
56 | }
57 |
58 | h(node.state)
59 |
60 | }
61 |
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/core/src/test/scala/aima/core/environment/vacuum/ModelBasedReflexVacuumAgentSpec.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.vacuum
2 |
3 | import org.specs2.mutable.Specification
4 | import org.specs2.specification.Scope
5 |
6 | /**
7 | * @author Shawn Garner
8 | */
9 | class ModelBasedReflexVacuumAgentSpec extends Specification {
10 |
11 | "should suck if location A" in new context {
12 | agent.agentFunction.apply(LocationAPercept) must beLike {
13 | case Suck => ok
14 | }
15 | }
16 |
17 | "should move if location A is clean" in new context {
18 | agent.agentFunction.apply(LocationAPercept)
19 | agent.agentFunction.apply(CleanPercept) must beLike {
20 | case RightMoveAction => ok
21 | }
22 | }
23 |
24 | "should assume dirty after moving to location B" in new context {
25 | agent.agentFunction.apply(LocationAPercept)
26 | agent.agentFunction.apply(CleanPercept)
27 | agent.agentFunction.apply(LocationBPercept) must beLike {
28 | case Suck => ok
29 | }
30 | }
31 |
32 | "should suck if location B" in new context {
33 | agent.agentFunction.apply(LocationBPercept) must beLike {
34 | case Suck => ok
35 | }
36 | }
37 |
38 | "should move if location B is clean" in new context {
39 | agent.agentFunction.apply(LocationBPercept)
40 | agent.agentFunction.apply(CleanPercept) must beLike {
41 | case LeftMoveAction => ok
42 | }
43 | }
44 |
45 | "should assume dirty after moving to location A" in new context {
46 | agent.agentFunction.apply(LocationBPercept)
47 | agent.agentFunction.apply(CleanPercept)
48 | agent.agentFunction.apply(LocationAPercept) must beLike {
49 | case Suck => ok
50 | }
51 | }
52 |
53 | "should suck if dirty" in new context {
54 | agent.agentFunction.apply(DirtyPercept) must beLike {
55 | case Suck => ok
56 | }
57 | }
58 |
59 | "should do nothing if low on power" in new context {
60 | val resultStream = LazyList.continually(agent.agentFunction.apply(DirtyPercept))
61 | resultStream.take(100).last must beLike {
62 | case NoAction => ok
63 | }
64 | }
65 |
66 | trait context extends Scope {
67 | val agent = new ModelBasedReflexVacuumAgentProgram
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/core/src/test/scala/aima/core/search/local/HillClimbingSpec.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.local
2 |
3 | import aima.core.search.Problem
4 | import org.scalacheck.{Arbitrary, Gen}
5 | import org.specs2.ScalaCheck
6 | import org.specs2.mutable.Specification
7 |
8 | /**
9 | * @author Shawn Garner
10 | */
11 | class HillClimbingSpec extends Specification with ScalaCheck {
12 | import HillClimbingSpec._
13 |
14 | implicit val arbXCoordinate: Arbitrary[XCoordinate] = Arbitrary {
15 | for {
16 | x <- Gen.choose[Double](0.00d, math.Pi)
17 | } yield XCoordinate(x)
18 | }
19 |
20 | "must find pi/2 on sin graph between zero and pi" >> prop { xCoord: XCoordinate =>
21 | val stateToValue: XCoordinate => Double = {
22 | case XCoordinate(x) => math.sin(x)
23 | }
24 |
25 | val sinProblem = new Problem[XCoordinate, StepAction] {
26 | override def initialState: XCoordinate = xCoord
27 | override def isGoalState(state: XCoordinate): Boolean = false // Not used
28 | override def actions(state: XCoordinate): List[StepAction] = List(StepLeft, StepRight)
29 | override def result(state: XCoordinate, action: StepAction): XCoordinate = (state, action) match {
30 | case (XCoordinate(x), StepLeft) =>
31 | val newX = x - 0.001d
32 | if (x < 0) {
33 | XCoordinate(0)
34 | } else {
35 | XCoordinate(newX)
36 | }
37 |
38 | case (XCoordinate(x), StepRight) =>
39 | val newX = x + 0.001d
40 | if (x > math.Pi) {
41 | XCoordinate(math.Pi)
42 | } else {
43 | XCoordinate(newX)
44 | }
45 | }
46 |
47 | override def stepCost(state: XCoordinate, action: StepAction, childPrime: XCoordinate): Int = -1 // Not Used
48 | }
49 |
50 | val result = HillClimbing(stateToValue)(sinProblem)
51 | result match {
52 | case XCoordinate(x) => x must beCloseTo((math.Pi / 2) within 3.significantFigures)
53 | case other => ko(other.toString)
54 | }
55 | }
56 | }
57 |
58 | object HillClimbingSpec {
59 | final case class XCoordinate(x: Double)
60 | sealed trait StepAction
61 | case object StepLeft extends StepAction
62 | case object StepRight extends StepAction
63 | }
64 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/problems/TwoPlayerGame.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.problems
2 |
3 | import aima.core.search.adversarial._
4 |
5 | /**
6 | * @author Aditya Lahiri
7 | * @author Shawn Garner
8 | */
9 | object TwoPlayerGame {
10 |
11 | final case class Player(name: String)
12 | final case class State(name: String)
13 | final case class Action(name: String)
14 |
15 | sealed trait TwoPlayerGame extends Game[Player, State, Action]
16 |
17 | val impl: TwoPlayerGame = new TwoPlayerGame {
18 | val adjacencyMat = Map(
19 | "A" -> List(Action("B"), Action("C"), Action("D")),
20 | "B" -> List(Action("E"), Action("F"), Action("G")),
21 | "C" -> List(Action("H"), Action("I"), Action("J")),
22 | "D" -> List(Action("K"), Action("L"), Action("M"))
23 | )
24 | val TwoPlyStates = Map(
25 | "A" -> State("A"),
26 | "B" -> State("B"),
27 | "C" -> State("C"),
28 | "D" -> State("D"),
29 | "E" -> State("E"),
30 | "F" -> State("F"),
31 | "G" -> State("G"),
32 | "H" -> State("H"),
33 | "I" -> State("I"),
34 | "J" -> State("J"),
35 | "K" -> State("K"),
36 | "L" -> State("L"),
37 | "M" -> State("M")
38 | )
39 |
40 | @Override
41 | def initialState: State = TwoPlyStates("A")
42 |
43 | @Override
44 | def getPlayer(state: State): Player = state.name match {
45 | case "B" | "C" | "D" => Player("MIN")
46 | case _ => Player("MAX")
47 | }
48 |
49 | @Override
50 | def getActions(state: State): List[Action] = adjacencyMat(state.name)
51 |
52 | @Override
53 | def result(state: State, action: Action): State = TwoPlyStates(action.name)
54 |
55 | @Override
56 | def isTerminalState(state: State): Boolean = state.name match {
57 | case "A" | "B" | "C" | "D" => false
58 | case _ => true
59 |
60 | }
61 |
62 | @Override
63 | def getUtility(state: State): UtilityValue = state.name match {
64 | case "E" => UtilityValue(3)
65 | case "F" => UtilityValue(12)
66 | case "G" => UtilityValue(8)
67 | case "H" => UtilityValue(2)
68 | case "I" => UtilityValue(4)
69 | case "J" => UtilityValue(6)
70 | case "K" => UtilityValue(14)
71 | case "L" => UtilityValue(5)
72 | case "M" => UtilityValue(2)
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/core/src/test/scala/aima/core/environment/map2d/LabeledGraphSpec.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.map2d
2 |
3 | import org.specs2.mutable.Specification
4 |
5 | /**
6 | * @author Shawn Garner
7 | */
8 | class LabeledGraphSpec extends Specification {
9 | "add vertex" in {
10 | val graph = new LabeledGraph[Int, (Int, Int)]
11 |
12 | graph.vertexLabels must beEmpty
13 |
14 | graph.addVertex(2)
15 |
16 | graph.vertexLabels must haveSize(1)
17 | graph.isVertexLabel(2) must beTrue
18 | graph.isVertexLabel(1) must beFalse
19 |
20 | graph.addVertex(3)
21 |
22 | graph.vertexLabels must haveSize(2)
23 | graph.isVertexLabel(3) must beTrue
24 | graph.isVertexLabel(2) must beTrue
25 | graph.isVertexLabel(1) must beFalse
26 |
27 | graph.vertexLabels must contain(exactly(2, 3))
28 | }
29 |
30 | "edge creation" in {
31 | val graph = new LabeledGraph[Int, (Int, Int)]
32 |
33 | graph.addVertex(2)
34 |
35 | graph.vertexLabels must haveSize(1)
36 | graph.isVertexLabel(2) must beTrue
37 |
38 | graph.addVertex(6)
39 |
40 | graph.vertexLabels must haveSize(2)
41 | graph.isVertexLabel(6) must beTrue
42 |
43 | graph.set(2, 6, (2, 6))
44 |
45 | graph.vertexLabels must haveSize(2)
46 |
47 | graph.set(3, 12, (3, 12))
48 |
49 | graph.vertexLabels must haveSize(4)
50 | graph.isVertexLabel(3) must beTrue
51 | graph.isVertexLabel(12) must beTrue
52 |
53 | graph.set(2, 12, (2, 12))
54 |
55 | graph.vertexLabels must haveSize(4)
56 |
57 | graph.set(3, 6, (3, 6))
58 |
59 | graph.vertexLabels must haveSize(4)
60 |
61 | graph.set(6, 12, (6, 12))
62 |
63 | graph.vertexLabels must haveSize(4)
64 |
65 | graph.get(2, 3) must beNone
66 | graph.get(2, 6) must beSome((2, 6))
67 | graph.get(3, 6) must beSome((3, 6))
68 | graph.get(3, 12) must beSome((3, 12))
69 | graph.get(6, 12) must beSome((6, 12))
70 | }
71 |
72 | "get successors" in {
73 | val graph = new LabeledGraph[Int, (Int, Int)]
74 |
75 | graph.successors(2) must beEmpty
76 |
77 | graph.set(2, 12, (2, 12))
78 | graph.set(3, 12, (3, 12))
79 | graph.set(2, 6, (2, 6))
80 | graph.set(3, 6, (3, 6))
81 | graph.set(6, 12, (6, 12))
82 |
83 | graph.successors(2) must contain(exactly(6, 12))
84 | graph.successors(3) must contain(exactly(6, 12))
85 |
86 | graph.successors(6) must contain(exactly(12))
87 | graph.successors(12) must beEmpty
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/vacuum/ModelBasedReflexVacuumAgentProgram.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.vacuum
2 |
3 | import aima.core.agent.{ModelBasedReflexAgentProgram}
4 | import ModelBasedReflexVacuumAgentProgram._
5 |
6 | object ModelBasedReflexVacuumAgentProgram {
7 | val MOVE_BATTERY_COST = 2
8 | val SUCK_BATTERY_COST = 1
9 | }
10 |
11 | final case class VacuumWorldState(
12 | locationA: Boolean = false,
13 | locationB: Boolean = false,
14 | dirty: Boolean = true,
15 | batteryLife: Int = 100
16 | )
17 |
18 | /**
19 | * @author Shawn Garner
20 | */
21 | class ModelBasedReflexVacuumAgentProgram
22 | extends ModelBasedReflexAgentProgram[VacuumPercept, VacuumAction, VacuumWorldState] {
23 |
24 | val model: Model = {
25 | case (currentState, RightMoveAction) =>
26 | currentState.copy(
27 | locationA = false,
28 | locationB = true,
29 | dirty = true,
30 | batteryLife = currentState.batteryLife - MOVE_BATTERY_COST
31 | )
32 | case (currentState, LeftMoveAction) =>
33 | currentState.copy(
34 | locationA = true,
35 | locationB = false,
36 | dirty = true,
37 | batteryLife = currentState.batteryLife - MOVE_BATTERY_COST
38 | )
39 | case (currentState, Suck) =>
40 | currentState.copy(dirty = false, batteryLife = currentState.batteryLife - SUCK_BATTERY_COST)
41 | case (currentState, NoAction) => currentState
42 | }
43 |
44 | lazy val noAction: VacuumAction = NoAction
45 |
46 | val rules: RuleMatch = {
47 | case VacuumWorldState(_, _, _, batteryLife) if batteryLife < 10 => NoAction //too costly to continue
48 | case VacuumWorldState(_, _, dirty, _) if dirty => Suck
49 | case VacuumWorldState(locationA, _, _, _) if locationA => RightMoveAction
50 | case VacuumWorldState(_, locationB, _, _) if locationB => LeftMoveAction
51 | }
52 |
53 | lazy val initialState: VacuumWorldState = VacuumWorldState()
54 | val updateState: UpdateState = { (s: VacuumWorldState, a: VacuumAction, p: VacuumPercept, m: Model) =>
55 | val s2 = m(s, a)
56 | p match {
57 | case CleanPercept => s2.copy(dirty = false)
58 | case DirtyPercept => s2.copy(dirty = true)
59 | case LocationAPercept => s2.copy(locationA = true, locationB = false)
60 | case LocationBPercept => s2.copy(locationB = true, locationA = false)
61 | case _ => s2
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/uninformed/DepthLimitedTreeSearch.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.uninformed
2 |
3 | import aima.core.search.{Problem, ProblemSearch, StateNode}
4 |
5 | import scala.annotation.tailrec
6 | import scala.util.{Failure, Success, Try}
7 |
8 | sealed trait DLSResult[Action] {
9 | def actions: List[Action]
10 | }
11 |
12 | final case class Solution[Action](actions: List[Action]) extends DLSResult[Action]
13 | final case class CutOff[Action](actions: List[Action]) extends DLSResult[Action]
14 |
15 | /**
16 | * @author Shawn Garner
17 | */
18 | trait DepthLimitedTreeSearch[State, Action] extends ProblemSearch[State, Action, StateNode[State, Action]] {
19 |
20 | type Node = StateNode[State, Action]
21 |
22 | def search(problem: Problem[State, Action], initialLimit: Int, noAction: Action): Try[DLSResult[Action]] =
23 | Try {
24 |
25 | def recursiveDLS(node: Node, currentLimit: Int): Try[DLSResult[Action]] =
26 | Try {
27 | if (problem.isGoalState(node.state)) {
28 | Success(Solution(solution(node)))
29 | } else if (currentLimit == 0) {
30 | Success(CutOff(solution(node)))
31 | } else {
32 | val childNodes = for {
33 | action <- problem.actions(node.state)
34 | } yield newChildNode(problem, node, action)
35 |
36 | @tailrec def shortCircuitChildSearch(children: List[Node]): Try[DLSResult[Action]] = {
37 | children match {
38 | case Nil => Failure[DLSResult[Action]](new Exception("Exhausted child nodes"))
39 | case lastChild :: Nil =>
40 | recursiveDLS(lastChild, currentLimit - 1)
41 | case firstChild :: rest =>
42 | recursiveDLS(firstChild, currentLimit - 1) match {
43 | case result @ Success(Solution(_)) => result
44 | case _ => shortCircuitChildSearch(rest)
45 | }
46 | }
47 | }
48 |
49 | shortCircuitChildSearch(childNodes)
50 | }
51 | }.flatten
52 |
53 | recursiveDLS(makeNode(problem.initialState, noAction), initialLimit)
54 | }.flatten
55 |
56 | def makeNode(state: State, noAction: Action): Node = StateNode(state, noAction, None)
57 |
58 | def newChildNode(problem: Problem[State, Action], parent: Node, action: Action): Node = {
59 | val childState = problem.result(parent.state, action)
60 | StateNode(childState, action, Some(parent))
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/uninformed/UniformCostSearch.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.uninformed
2 |
3 | import aima.core.search._
4 |
5 | import scala.annotation.tailrec
6 |
7 | /**
8 | * @author Shawn Garner
9 | */
10 | trait UniformCostSearch[State, Action]
11 | extends ProblemSearch[State, Action, CostNode[State, Action]]
12 | with FrontierSearch[State, Action, CostNode[State, Action]] {
13 |
14 | type Node = CostNode[State, Action]
15 |
16 | def search(problem: Problem[State, Action], noAction: Action): List[Action] = {
17 | val initialFrontier = newFrontier(problem.initialState, noAction)
18 |
19 | @tailrec def searchHelper(
20 | frontier: Frontier[State, Action, Node],
21 | exploredSet: Set[State] = Set.empty[State]
22 | ): List[Action] = {
23 | frontier.removeLeaf match {
24 | case None => List.empty[Action]
25 | case Some((leaf, _)) if problem.isGoalState(leaf.state) => solution(leaf)
26 | case Some((leaf, updatedFrontier)) =>
27 | val updatedExploredSet = exploredSet + leaf.state
28 | val childNodes = for {
29 | action <- problem.actions(leaf.state)
30 | } yield newChildNode(problem, leaf, action)
31 |
32 | val frontierWithChildNodes = childNodes.foldLeft(updatedFrontier) { (accFrontier, childNode) =>
33 | if (!(updatedExploredSet.contains(childNode.state) || accFrontier.contains(childNode.state))) {
34 | accFrontier.add(childNode)
35 | } else if (accFrontier
36 | .getNode(childNode.state)
37 | .exists(existingNode => existingNode.cost > childNode.cost)) {
38 | accFrontier.replaceByState(childNode)
39 | } else {
40 | accFrontier
41 | }
42 | }
43 |
44 | searchHelper(frontierWithChildNodes, updatedExploredSet)
45 | }
46 | }
47 |
48 | searchHelper(initialFrontier)
49 | }
50 |
51 | def newFrontier(state: State, noAction: Action) = {
52 | val costNodeOrdering: Ordering[Node] = new Ordering[Node] {
53 | def compare(n1: Node, n2: Node): Int = Ordering.Int.reverse.compare(n1.cost, n2.cost)
54 | }
55 | new PriorityQueueHashSetFrontier[State, Action, Node](CostNode(state, 0, noAction, None), costNodeOrdering)
56 | }
57 |
58 | def newChildNode(problem: Problem[State, Action], parent: Node, action: Action): Node = {
59 | val childState = problem.result(parent.state, action)
60 | CostNode(
61 | state = childState,
62 | cost = parent.cost + problem.stepCost(parent.state, action, childState),
63 | action = action,
64 | parent = Some(parent)
65 | )
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/informed/RecursiveBestFirstSearch.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.informed
2 |
3 | import aima.core.search.{HeuristicsNode, Problem, ProblemSearch}
4 | import Ordering.Double.TotalOrdering
5 |
6 | import scala.annotation.tailrec
7 | import scala.reflect.ClassTag
8 |
9 | sealed trait RBFSearchResult
10 | final case class Solution[Action](actions: List[Action]) extends RBFSearchResult
11 | final case class SearchFailure(updatedFCost: Double) extends RBFSearchResult
12 |
13 | /**
14 | * @author Shawn Garner
15 | */
16 | trait RecursiveBestFirstSearch[State, Action] extends ProblemSearch[State, Action, HeuristicsNode[State, Action]] {
17 | implicit val sCT: ClassTag[State]
18 | implicit val aCT: ClassTag[Action]
19 |
20 | type Node = HeuristicsNode[State, Action]
21 | type Heuristic = Node => Double
22 |
23 | def search(problem: Problem[State, Action], noAction: Action, h: Heuristic): RBFSearchResult = {
24 | def rbfs(node: Node, fLimit: Double): RBFSearchResult = {
25 | if (problem.isGoalState(node.state))
26 | Solution(solution(node))
27 | else {
28 | val successors = for {
29 | action <- problem.actions(node.state)
30 | } yield newChildNode(problem, node, action)
31 |
32 | if (successors.isEmpty)
33 | SearchFailure(Double.PositiveInfinity)
34 | else {
35 | val updated = successors.collect {
36 | case s @ HeuristicsNode(_, gValue, Some(hValue), _, _, _) =>
37 | val updatedFValue = node.fValue.map(nodeFValue => math.max(gValue + hValue, nodeFValue))
38 | s.copy(fValue = updatedFValue)
39 | }
40 |
41 | @tailrec def getBestFValue(updatedSuccessors: List[Node]): RBFSearchResult = {
42 | val sortedSuccessors = updatedSuccessors.sortBy(_.fValue.getOrElse(Double.MaxValue))
43 | sortedSuccessors match {
44 | case HeuristicsNode(_, _, _, Some(fValue), _, _) :: _ if fValue > fLimit => SearchFailure(fValue)
45 | case best :: (second @ HeuristicsNode(_, _, _, Some(fValue), _, _)) :: rest =>
46 | val result = rbfs(best, math.min(fLimit, fValue))
47 | result match {
48 | case s: Solution[Action] => s
49 | case SearchFailure(updatedFValue) =>
50 | getBestFValue(best.copy(fValue = Some(updatedFValue)) :: second :: rest)
51 | }
52 | }
53 | }
54 |
55 | getBestFValue(updated)
56 | }
57 | }
58 | }
59 |
60 | rbfs(makeNode(problem.initialState, noAction, h), Double.PositiveInfinity)
61 | }
62 |
63 | def makeNode(state: State, noAction: Action, h: Heuristic): Node = {
64 | val basic = HeuristicsNode(state = state, gValue = 0, hValue = None, fValue = None, noAction, None)
65 | val hValue: Double = h(basic)
66 | val fValue: Double = basic.gValue + hValue
67 | basic.copy(hValue = Some(hValue), fValue = Some(fValue))
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/uninformed/frontier.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.uninformed
2 |
3 | import aima.core.search.{Frontier, SearchNode}
4 |
5 | import scala.collection.immutable.{Queue, Iterable}
6 | import scala.collection.mutable
7 | import scala.util.Try
8 |
9 | class FIFOQueueFrontier[State, Action, Node <: SearchNode[State, Action]](queue: Queue[Node], stateSet: Set[State])
10 | extends Frontier[State, Action, Node] { self =>
11 | def this(n: Node) = this(Queue(n), Set(n.state))
12 |
13 | def removeLeaf: Option[(Node, Frontier[State, Action, Node])] = queue.dequeueOption.map {
14 | case (leaf, updatedQueue) => (leaf, new FIFOQueueFrontier[State, Action, Node](updatedQueue, stateSet - leaf.state))
15 | }
16 | def addAll(iterable: Iterable[Node]): Frontier[State, Action, Node] =
17 | new FIFOQueueFrontier(queue.enqueueAll(iterable), stateSet ++ iterable.map(_.state))
18 | def contains(state: State): Boolean = stateSet.contains(state)
19 |
20 | def replaceByState(node: Node): Frontier[State, Action, Node] = {
21 | if (contains(node.state)) {
22 | new FIFOQueueFrontier(queue.filterNot(_.state == node.state).enqueue(node), stateSet)
23 | } else {
24 | self
25 | }
26 | }
27 | def getNode(state: State): Option[Node] = {
28 | if (contains(state)) {
29 | queue.find(_.state == state)
30 | } else {
31 | None
32 | }
33 | }
34 |
35 | def add(node: Node): Frontier[State, Action, Node] =
36 | new FIFOQueueFrontier[State, Action, Node](queue.enqueue(node), stateSet + node.state)
37 | }
38 |
39 | class PriorityQueueHashSetFrontier[State, Action, Node <: SearchNode[State, Action]](
40 | queue: mutable.PriorityQueue[Node],
41 | stateMap: mutable.Map[State, Node]
42 | ) extends Frontier[State, Action, Node] { self =>
43 |
44 | def this(n: Node, costNodeOrdering: Ordering[Node]) =
45 | this(mutable.PriorityQueue(n)(costNodeOrdering), mutable.Map(n.state -> n))
46 |
47 | def removeLeaf: Option[(Node, Frontier[State, Action, Node])] =
48 | Try {
49 | val leaf = queue.dequeue
50 | stateMap -= leaf.state
51 | (leaf, self)
52 | }.toOption
53 |
54 | def addAll(iterable: Iterable[Node]): Frontier[State, Action, Node] = {
55 | iterable.foreach { costNode =>
56 | queue += costNode
57 | stateMap += (costNode.state -> costNode)
58 | }
59 | self
60 | }
61 |
62 | def contains(state: State): Boolean = stateMap.contains(state)
63 |
64 | def replaceByState(node: Node): Frontier[State, Action, Node] = {
65 | if (contains(node.state)) {
66 | val updatedElems = node :: queue.toList.filterNot(_.state == node.state)
67 | queue.clear()
68 | queue.enqueue(updatedElems: _*)
69 | stateMap += (node.state -> node)
70 | }
71 | self
72 | }
73 |
74 | def getNode(state: State): Option[Node] = {
75 | if (contains(state)) {
76 | queue.find(_.state == state)
77 | } else {
78 | None
79 | }
80 | }
81 |
82 | def add(node: Node): Frontier[State, Action, Node] = {
83 | val costNode = node
84 | queue.enqueue(costNode)
85 | stateMap += (node.state -> costNode)
86 | self
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/core/src/test/scala/aima/core/environment/vacuum/TableDrivenVacuumAgentSpec.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.vacuum
2 |
3 | import org.specs2.mutable.Specification
4 | import org.specs2.specification.Scope
5 |
6 | import scala.util.Random
7 |
8 | /**
9 | * @author Shawn Garner
10 | */
11 | class TableDrivenVacuumAgentSpec extends Specification {
12 |
13 | "first level dirty sucks" in new context {
14 | invokeAgent(List(LocationAPercept, DirtyPercept)) must_== List(NoAction, Suck)
15 | }
16 |
17 | "first level A and Clean moves Right" in new context {
18 | invokeAgent(List(LocationAPercept, CleanPercept)) must_== List(NoAction, RightMoveAction)
19 | }
20 |
21 | "first level B and Clean moves Right" in new context {
22 | invokeAgent(List(LocationBPercept, CleanPercept)) must_== List(NoAction, LeftMoveAction)
23 | }
24 |
25 | "second level dirty sucks" in new context {
26 | val givenPercepts = List.fill(2)(LocationAPercept) ++ List(LocationAPercept, DirtyPercept)
27 | val expectedActions = List.fill(2)(NoAction) ++ List(NoAction, Suck)
28 | invokeAgent(givenPercepts) must_== expectedActions
29 | }
30 |
31 | "second level A and Clean moves Right" in new context {
32 | val givenPercepts = List.fill(2)(LocationAPercept) ++ List(LocationAPercept, CleanPercept)
33 | val expectedActions = List.fill(2)(NoAction) ++ List(NoAction, RightMoveAction)
34 | invokeAgent(givenPercepts) must_== expectedActions
35 | }
36 |
37 | "second level B and Clean moves Right" in new context {
38 | val givenPercepts = List.fill(2)(LocationAPercept) ++ List(LocationBPercept, CleanPercept)
39 | val expectedActions = List.fill(2)(NoAction) ++ List(NoAction, LeftMoveAction)
40 | invokeAgent(givenPercepts) must_== expectedActions
41 | }
42 |
43 | "fourth level dirty sucks" in new context {
44 | val givenPercepts = List.fill(6)(LocationAPercept) ++ List(LocationAPercept, DirtyPercept)
45 | val expectedActions = List.fill(6)(NoAction) ++ List(NoAction, Suck)
46 | invokeAgent(givenPercepts) must_== expectedActions
47 | }
48 |
49 | "fourth level A and Clean moves Right" in new context {
50 | val givenPercepts = List.fill(6)(LocationAPercept) ++ List(LocationAPercept, CleanPercept)
51 | val expectedActions = List.fill(6)(NoAction) ++ List(NoAction, RightMoveAction)
52 | invokeAgent(givenPercepts) must_== expectedActions
53 | }
54 |
55 | "fourth level B and Clean moves Right" in new context {
56 | val givenPercepts = List.fill(6)(LocationAPercept) ++ List(LocationBPercept, CleanPercept)
57 | val expectedActions = List.fill(6)(NoAction) ++ List(NoAction, LeftMoveAction)
58 | invokeAgent(givenPercepts) must_== expectedActions
59 | }
60 |
61 | "twenty dirty percepts is undefined because out of table definition range" in new context {
62 | val givenPercepts = List.fill(20)(LocationAPercept)
63 | val expectedActions = List.fill(20)(NoAction)
64 | invokeAgent(givenPercepts) must_== expectedActions
65 | }
66 |
67 | "table driven agent must persist all percepts" in new context {
68 | val rnd = new Random()
69 | val randomPerceptStream: LazyList[VacuumPercept] = LazyList.continually {
70 | val selector = rnd.nextInt(3)
71 | if (selector == 0)
72 | LocationPercept.randomValue
73 | else
74 | DirtPercept.randomValue
75 | }
76 |
77 | val givenPercepts = randomPerceptStream.take(100).toList
78 |
79 | invokeAgent(givenPercepts)
80 |
81 | agent.percepts.toList must_== givenPercepts
82 | }
83 |
84 | trait context extends Scope {
85 | val agent = new TableDrivenVacuumAgentProgram
86 | def invokeAgent(percepts: List[VacuumPercept]): List[VacuumAction] = {
87 | percepts.map(agent.agentFunction)
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/vacuum/VacuumEnvironment.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.vacuum
2 |
3 | import aima.core.agent._
4 | import aima.core.random.DefaultRandomness
5 |
6 | /**
7 | * @author Shawn Garner
8 | */
9 | case class VacuumEnvironment(map: VacuumMap = VacuumMap())
10 | extends Environment[VacuumEnvironment, VacuumPercept, VacuumAction] {
11 |
12 | def addAgent(agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction]): VacuumEnvironment = {
13 | VacuumEnvironment(map.addAgent(agent))
14 | }
15 |
16 | def removeAgent(agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction]): VacuumEnvironment = {
17 | VacuumEnvironment(map.removeAgent(agent))
18 | }
19 |
20 | def isClean(): Boolean = {
21 | map.isClean()
22 | }
23 | }
24 |
25 | case class VacuumMapNode(
26 | dirtStatus: DirtPercept = DirtPercept.randomValue,
27 | maybeAgent: Option[Agent[VacuumEnvironment, VacuumPercept, VacuumAction]] = None
28 | )
29 |
30 | case class VacuumMap(
31 | nodes: Vector[VacuumMapNode] = Vector.fill(2)(VacuumMapNode())
32 | ) extends DefaultRandomness {
33 | self =>
34 |
35 | def isClean(): Boolean = {
36 | nodes.forall { node =>
37 | node.dirtStatus match {
38 | case CleanPercept => true
39 | case _ => false
40 | }
41 | }
42 | }
43 |
44 | def getAgentNode(agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction]): Option[VacuumMapNode] =
45 | nodes.collectFirst {
46 | case VacuumMapNode(dirtStatus, Some(a)) if agent == a =>
47 | VacuumMapNode(dirtStatus, Some(a))
48 | }
49 | def getDirtStatus(agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction]): Option[VacuumPercept] =
50 | getAgentNode(agent).map(_.dirtStatus)
51 |
52 | def getAgentLocation(agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction]): Option[VacuumPercept] = {
53 | val maybeIndex = nodes.zipWithIndex.collectFirst {
54 | case (VacuumMapNode(_, Some(a)), index) if agent == a => index
55 | }
56 | maybeIndex.map(index => indexToLocationPercept(index))
57 | }
58 |
59 | private[this] def indexToLocationPercept(index: Int): VacuumPercept = {
60 | if (index == 0) {
61 | LocationAPercept
62 | } else {
63 | LocationBPercept
64 | }
65 | }
66 | private[this] def directionToMapIndex(direction: MoveAction): Int =
67 | direction match {
68 | case LeftMoveAction => 0
69 | case RightMoveAction => 1
70 | }
71 |
72 | def moveAgent(agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction], direction: MoveAction): VacuumMap = {
73 | removeAgent(agent).updateByIndex(directionToMapIndex(direction))(vacuumMapNode =>
74 | vacuumMapNode.copy(maybeAgent = Some(agent))
75 | )
76 | }
77 |
78 | private[this] def updateByAgent(
79 | target: Agent[VacuumEnvironment, VacuumPercept, VacuumAction]
80 | )(f: VacuumMapNode => VacuumMapNode): VacuumMap = {
81 | val updatedNodes = nodes.map {
82 | case n @ VacuumMapNode(_, Some(a)) if a == target => f(n)
83 | case x => x
84 | }
85 | VacuumMap(updatedNodes)
86 | }
87 |
88 | def updateStatus(
89 | agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction],
90 | dirtStatusPercepts: DirtPercept
91 | ): VacuumMap =
92 | updateByAgent(agent)(vacuumMapNode => vacuumMapNode.copy(dirtStatus = dirtStatusPercepts))
93 |
94 | def removeAgent(agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction]): VacuumMap =
95 | updateByAgent(agent)(vacuumMapNode => vacuumMapNode.copy(maybeAgent = None))
96 |
97 | private def updateByIndex(
98 | index: Int
99 | )(f: VacuumMapNode => VacuumMapNode): VacuumMap = {
100 | val node = nodes.apply(index)
101 | val updatedNodes = nodes.updated(index, f(node))
102 | VacuumMap(updatedNodes)
103 | }
104 |
105 | def addAgent(agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction]): VacuumMap = {
106 | if (nodes.count(_.maybeAgent.isDefined) == 0) {
107 | val selection = rand.nextInt(nodes.size)
108 | updateByIndex(selection)(vacuumMapNode => vacuumMapNode.copy(maybeAgent = Some(agent)))
109 | } else {
110 | self
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/environment/map2d/Map2D.scala:
--------------------------------------------------------------------------------
1 | package aima.core.environment.map2d
2 |
3 | /**
4 | * Provides a general interface for two dimensional maps.
5 | *
6 | * @author Shawn Garner
7 | */
8 | trait Map2D {
9 |
10 | /**
11 | *
12 | * @return a list of all locations in the map.
13 | */
14 | def locations: List[String]
15 |
16 | /**
17 | * Answers to the question: Where can I get, following one of the
18 | * connections starting at the specified location?
19 | *
20 | * @param fromLocation
21 | * locations linked from.
22 | * @return a list of the locations that are connected from the given
23 | * location.
24 | */
25 | def locationsLinkedTo(fromLocation: String): List[String]
26 |
27 | /**
28 | * Get the travel distance between the two specified locations if they are
29 | * linked by a connection and null otherwise.
30 | *
31 | * @param fromLocation
32 | * the starting from location.
33 | * @param toLocation
34 | * the to location.
35 | * @return the travel distance between the two specified locations if they
36 | * are linked by a connection and null otherwise.
37 | */
38 | def distance(fromLocation: String, toLocation: String): Option[Distance]
39 |
40 | /**
41 | * Get the position of the specified location.
42 | *
43 | * @param location
44 | * the location whose position is to be returned.
45 | * @return the position of the specified location in the two dimensional
46 | * space.
47 | */
48 | def position(location: String): Option[Point2D]
49 | }
50 |
51 | final case class Point2D(x: Double, y: Double)
52 | final case class Distance(value: Double) extends AnyVal
53 | object Point2D {
54 | def distance(p1: Point2D, p2: Point2D): Distance = {
55 | val x_distance: Double = (p1.x - p2.x) * (p1.x - p2.x)
56 | // Distance Between Y Coordinates
57 | val y_distance: Double = (p1.y - p2.y) * (p1.y - p2.y)
58 | // Distance Between 2d Points
59 | val total_distance = math.sqrt(x_distance + y_distance)
60 |
61 | Distance(total_distance)
62 | }
63 | }
64 |
65 | import scala.collection.mutable
66 | class ExtendableMap2D(
67 | val links: LabeledGraph[String, Distance],
68 | val locationPositions: mutable.LinkedHashMap[String, Point2D]
69 | ) extends Map2D {
70 |
71 | def this() = this(new LabeledGraph[String, Distance], new mutable.LinkedHashMap[String, Point2D])
72 |
73 | override def locations: List[String] = links.vertexLabels
74 |
75 | override def locationsLinkedTo(fromLocation: String): List[String] = links.successors(fromLocation)
76 |
77 | override def distance(fromLocation: String, toLocation: String): Option[Distance] =
78 | links.get(fromLocation, toLocation)
79 |
80 | override def position(location: String): Option[Point2D] = locationPositions.get(location)
81 |
82 | def clear(): Unit = {
83 | links.clear()
84 | locationPositions.clear()
85 | }
86 |
87 | /**
88 | * Add a one-way connection to the map.
89 | *
90 | * @param fromLocation
91 | * the from location.
92 | * @param toLocation
93 | * the to location.
94 | * @param distance
95 | * the distance between the two given locations.
96 | */
97 | def addUnidirectionalLink(fromLocation: String, toLocation: String, distance: Distance): Unit = {
98 | links.set(fromLocation, toLocation, distance)
99 | }
100 |
101 | /**
102 | * Adds a connection which can be traveled in both direction. Internally,
103 | * such a connection is represented as two one-way connections.
104 | *
105 | * @param fromLocation
106 | * the from location.
107 | * @param toLocation
108 | * the to location.
109 | * @param distance
110 | * the distance between the two given locations.
111 | */
112 | def addBidirectionalLink(fromLocation: String, toLocation: String, distance: Distance): Unit = {
113 | links.set(fromLocation, toLocation, distance)
114 | links.set(toLocation, fromLocation, distance)
115 | }
116 |
117 | /**
118 | * Defines the position of a location within the map. Using this method, one
119 | * location should be selected as a reference position (dist=0
120 | * and dir=0) and all the other locations should be placed
121 | * relative to it.
122 | *
123 | * @param loc
124 | * location name
125 | * @param dist
126 | * distance to a reference position
127 | * @param dir
128 | * bearing (compass direction) in which the location is seen from
129 | * the reference position
130 | */
131 | def setDistAndDirToRefLocation(loc: String, dist: Distance, dir: Int): Unit = {
132 | val coords = Point2D(-math.sin(dir * math.Pi / 180.0) * dist.value, math.cos(dir * math.Pi / 180.0) * dist.value)
133 | links.addVertex(loc)
134 | locationPositions.put(loc, coords)
135 | ()
136 | }
137 |
138 | }
139 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/search/local/SimulatedAnnealingSearch.scala:
--------------------------------------------------------------------------------
1 | package aima.core.search.local
2 |
3 | import aima.core.search.local.time.TimeStep
4 | import aima.core.search.Problem
5 |
6 | import scala.annotation.tailrec
7 | import scala.util.{Failure, Random, Success, Try}
8 | import scala.util.control.NoStackTrace
9 |
10 | package time {
11 | case object UpperLimitExceeded extends RuntimeException with NoStackTrace
12 |
13 | final case class TimeStep private[time] (value: Int) extends AnyVal
14 |
15 | object TimeStep {
16 | val start = TimeStep(1)
17 |
18 | object Implicits {
19 | implicit class TimeStepOps(t: TimeStep) {
20 | def step: Try[TimeStep] = t.value match {
21 | case Int.MaxValue => Failure(UpperLimitExceeded)
22 | case v => Success(TimeStep(v + 1))
23 | }
24 | }
25 | }
26 | }
27 |
28 | }
29 |
30 | /**
31 | *
32 | * function SIMULATED-ANNEALING(problem, schedule) returns a solution state 33 | * inputs: problem, a problem 34 | * schedule, a mapping from time to "temperature" 35 | * 36 | * current ← MAKE-NODE(problem.INITIAL-STATE) 37 | * for t = 1 to ∞ do 38 | * T ← schedule(t) 39 | * if T = 0 then return current 40 | * next ← a randomly selected successor of current 41 | * ΔE ← next.VALUE - current.value 42 | * if ΔE > 0 then current ← next 43 | * else current ← next only with probability eΔE/T 44 | *45 | * 46 | * @author Shawn Garner 47 | */ 48 | object SimulatedAnnealingSearch { 49 | 50 | sealed trait TemperatureResult 51 | final case class Temperature private[SimulatedAnnealingSearch] (double: Double) extends TemperatureResult 52 | case object OverTimeStepLimit extends TemperatureResult 53 | 54 | type Schedule = TimeStep => TemperatureResult 55 | 56 | object BasicSchedule { 57 | 58 | final case class ScheduleParams(k: Int, lam: Double, limit: Int) 59 | val defaultScheduleParams = ScheduleParams( 60 | k = 20, 61 | lam = 0.045d, 62 | limit = 100 63 | ) 64 | 65 | def schedule(params: ScheduleParams = defaultScheduleParams): Schedule = { t: TimeStep => 66 | import params._ 67 | if (t.value < limit) { 68 | Temperature(k * math.exp((-1) * lam * t.value)) 69 | } else { 70 | OverTimeStepLimit 71 | } 72 | } 73 | } 74 | 75 | final case class StateValueNode[State](state: State, value: Double) 76 | 77 | def apply[State, Action](stateToValue: State => Double, problem: Problem[State, Action]): Try[State] = 78 | apply(stateToValue, problem, BasicSchedule.schedule()) 79 | 80 | def apply[State, Action]( 81 | stateToValue: State => Double, 82 | problem: Problem[State, Action], 83 | schedule: Schedule 84 | ): Try[State] = { 85 | val random = new Random() 86 | 87 | def makeNode(state: State): StateValueNode[State] = StateValueNode(state, stateToValue(state)) 88 | 89 | def randomlySelectSuccessor(current: StateValueNode[State]): StateValueNode[State] = { 90 | // Default successor to current, so that in the case we reach a dead-end 91 | // state i.e. one without reversible actions we will return something. 92 | // This will not break the code above as the loop will exit when the 93 | // temperature winds down to 0. 94 | val actions = problem.actions(current.state) 95 | val successor = { 96 | if (actions.nonEmpty) { 97 | makeNode(problem.result(current.state, actions(random.nextInt(actions.size)))) 98 | } else { 99 | current 100 | } 101 | } 102 | 103 | successor 104 | } 105 | 106 | @tailrec def recurse(current: StateValueNode[State], t: Try[TimeStep]): Try[StateValueNode[State]] = { 107 | import time.TimeStep.Implicits._ 108 | t match { 109 | case Failure(f) => Failure(f) 110 | case Success(timeStep) => 111 | val T = schedule(timeStep) 112 | T match { 113 | case OverTimeStepLimit => Success(current) 114 | 115 | case Temperature(temperatureT) => 116 | val randomSuccessor = randomlySelectSuccessor(current) 117 | val DeltaE = randomSuccessor.value - current.value 118 | lazy val acceptDownHillMove = math.exp(DeltaE / temperatureT) > random.nextDouble() 119 | 120 | val nextNode = { 121 | if (DeltaE > 0.0d || acceptDownHillMove) { 122 | randomSuccessor 123 | } else { 124 | current 125 | } 126 | } 127 | 128 | recurse(nextNode, timeStep.step) 129 | 130 | } 131 | 132 | } 133 | 134 | } 135 | 136 | recurse(makeNode(problem.initialState), Success(TimeStep.start)).map(_.state) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /core/src/main/scala/aima/core/search/problems/romania.scala: -------------------------------------------------------------------------------- 1 | package aima.core.search.problems 2 | 3 | import aima.core.search.{CostNode, Problem, StateNode} 4 | 5 | import scala.reflect.{ClassTag, classTag} 6 | 7 | /** 8 | * @author Shawn Garner 9 | */ 10 | object Romania { 11 | final case class City(name: String) extends AnyVal 12 | case class Road(from: City, to: City, cost: Int) 13 | 14 | val Arad = City("Arad") 15 | val Zerind = City("Zerind") 16 | val Oradea = City("Oradea") 17 | val Sibiu = City("Sibiu") 18 | val Timisoara = City("Timisoara") 19 | val Lugoj = City("Lugoj") 20 | val Mehadia = City("Mehadia") 21 | val Drobeta = City("Drobeta") 22 | val RimnicuVilcea = City("Rimnicu Vilcea") 23 | val Craiova = City("Craiova") 24 | val Fagaras = City("Fagaras") 25 | val Pitesti = City("Pitesti") 26 | val Bucharest = City("Bucharest") 27 | val Giurgiu = City("Giurgiu") 28 | val Urziceni = City("Urziceni") 29 | val Neamt = City("Neamt") 30 | val Iasi = City("Iasi") 31 | val Vaslui = City("Vaslui") 32 | val Hirsova = City("Hirsova") 33 | val Eforie = City("Eforie") 34 | 35 | val roadsFromCity: Map[City, List[Road]] = List( 36 | Road(Arad, Zerind, 75), 37 | Road(Arad, Timisoara, 118), 38 | Road(Arad, Sibiu, 140), //Arad Edges 39 | Road(Zerind, Arad, 75), 40 | Road(Zerind, Oradea, 71), //Zerind Edges 41 | Road(Oradea, Zerind, 71), 42 | Road(Oradea, Sibiu, 151), //Oradea Edges 43 | Road(Sibiu, Arad, 140), 44 | Road(Sibiu, Oradea, 151), 45 | Road(Sibiu, Fagaras, 99), 46 | Road(Sibiu, RimnicuVilcea, 80), //Sibiu Edges 47 | Road(Timisoara, Arad, 118), 48 | Road(Timisoara, Lugoj, 111), //Timisoara Edges 49 | Road(Lugoj, Timisoara, 111), 50 | Road(Lugoj, Mehadia, 70), //Lugoj Edges 51 | Road(Mehadia, Lugoj, 70), 52 | Road(Mehadia, Drobeta, 75), //Mehadia Edges 53 | Road(Drobeta, Mehadia, 75), 54 | Road(Drobeta, Craiova, 120), //Drobeta Edges 55 | Road(RimnicuVilcea, Sibiu, 80), 56 | Road(RimnicuVilcea, Craiova, 146), 57 | Road(RimnicuVilcea, Pitesti, 97), //Rimnicu Vilcea Edges 58 | Road(Craiova, Drobeta, 120), 59 | Road(Craiova, RimnicuVilcea, 146), 60 | Road(Craiova, Pitesti, 138), //Craiova Edges 61 | Road(Fagaras, Sibiu, 99), 62 | Road(Fagaras, Bucharest, 211), //Fagaras Edges 63 | Road(Pitesti, Craiova, 138), 64 | Road(Pitesti, RimnicuVilcea, 97), 65 | Road(Pitesti, Bucharest, 101), //Pitesti Edges 66 | Road(Bucharest, Pitesti, 101), 67 | Road(Bucharest, Fagaras, 211), 68 | Road(Bucharest, Urziceni, 85), 69 | Road(Bucharest, Giurgiu, 90), //Bucharest Edges 70 | Road(Giurgiu, Bucharest, 90), //Giurgiu Edges 71 | Road(Urziceni, Bucharest, 85), 72 | Road(Urziceni, Vaslui, 142), 73 | Road(Urziceni, Hirsova, 98), //Urziceni Edges 74 | Road(Neamt, Iasi, 87), //Neamt Edges 75 | Road(Iasi, Neamt, 87), 76 | Road(Iasi, Vaslui, 92), //Iasi Edges 77 | Road(Vaslui, Iasi, 92), 78 | Road(Vaslui, Urziceni, 142), //Vaslui Edges 79 | Road(Hirsova, Urziceni, 98), 80 | Road(Hirsova, Eforie, 86), //Hirsova Edges 81 | Road(Eforie, Hirsova, 86) //Eforie Edges 82 | ).foldLeft(Map.empty[City, List[Road]]) { (acc, road) => 83 | val from = road.from 84 | val listForEdge = acc.getOrElse(from, List.empty[Road]) 85 | acc.updated(from, road :: listForEdge) 86 | } 87 | 88 | sealed trait RomaniaState 89 | final case class In(city: City) extends RomaniaState 90 | 91 | sealed trait RomaniaAction 92 | final case class GoTo(city: City) extends RomaniaAction 93 | case object NoAction extends RomaniaAction 94 | 95 | val sCTag: ClassTag[RomaniaState] = classTag[RomaniaState] 96 | val aCTag: ClassTag[RomaniaAction] = classTag[RomaniaAction] 97 | 98 | val snCTag: ClassTag[StateNode[RomaniaState, RomaniaAction]] = 99 | classTag[StateNode[RomaniaState, RomaniaAction]] 100 | 101 | val cnCTag: ClassTag[CostNode[RomaniaState, RomaniaAction]] = classTag[CostNode[RomaniaState, RomaniaAction]] 102 | 103 | class RomaniaRoadProblem(val initialState: RomaniaState, val goalState: RomaniaState) 104 | extends Problem[RomaniaState, RomaniaAction] { 105 | def result(currentState: RomaniaState, action: RomaniaAction): RomaniaState = action match { 106 | case GoTo(city) => In(city) 107 | case NoAction => currentState 108 | } 109 | 110 | def actions(state: RomaniaState): List[RomaniaAction] = state match { 111 | case In(city) => roadsFromCity(city).map(road => GoTo(road.to)) 112 | } 113 | 114 | def isGoalState(state: RomaniaState): Boolean = (state, goalState) match { 115 | case (In(city), In(goal)) => city == goal 116 | case _ => false 117 | } 118 | 119 | def stepCost(state: RomaniaState, action: RomaniaAction, statePrime: RomaniaState): Int = 120 | (state, statePrime) match { 121 | case (In(city), In(cityPrime)) => 122 | val maybeCost = roadsFromCity(city) collectFirst { 123 | case Road(c1, c2, cost) if c1 == city && c2 == cityPrime => cost 124 | } 125 | maybeCost.getOrElse(Int.MaxValue) 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /core/src/test/scala/aima/core/environment/vacuum/VacuumMapSpec.scala: -------------------------------------------------------------------------------- 1 | package aima.core.environment.vacuum 2 | 3 | import aima.core.agent.{Actuator, Agent, AgentProgram, Sensor} 4 | import org.scalacheck.Arbitrary 5 | import org.specs2.ScalaCheck 6 | import org.specs2.mutable.Specification 7 | 8 | /** 9 | * @author Shawn Garner 10 | */ 11 | class VacuumMapSpec extends Specification with ScalaCheck { 12 | 13 | "must be clean if all nodes clean" in { 14 | val map = VacuumMap(Vector(cleanNode, cleanNode)) 15 | 16 | map.isClean() must beTrue 17 | } 18 | 19 | "must be dirty if left node is dirty" in { 20 | val map = VacuumMap(Vector(dirtyNode, cleanNode)) 21 | 22 | map.isClean() must beFalse 23 | } 24 | 25 | "must be dirty if right node is dirty" in { 26 | val map = VacuumMap(Vector(cleanNode, dirtyNode)) 27 | 28 | map.isClean() must beFalse 29 | } 30 | 31 | "must be dirty if right node is dirty" in { 32 | val map = VacuumMap(Vector(cleanNode, dirtyNode)) 33 | 34 | map.isClean() must beFalse 35 | } 36 | 37 | "must be dirty if both nodes are dirty" in { 38 | val map = VacuumMap(Vector(dirtyNode, dirtyNode)) 39 | 40 | map.isClean() must beFalse 41 | } 42 | 43 | implicit lazy val arbVacuumMap = Arbitrary(VacuumMap()) 44 | 45 | "dirt status of agent not on map must be none" in prop { map: VacuumMap => 46 | val agent = noopAgent 47 | map.getDirtStatus(agent) must beNone 48 | } 49 | 50 | "dirt status of agent must be dirty if all dirty" in { 51 | val agent = noopAgent 52 | val map = VacuumMap(Vector(dirtyNode, dirtyNode)).addAgent(agent) 53 | 54 | map.getDirtStatus(agent) must beSome[VacuumPercept](DirtyPercept) 55 | } 56 | 57 | "dirt status of agent must be clean if all clean" in { 58 | val agent = noopAgent 59 | val map = VacuumMap(Vector(cleanNode, cleanNode)).addAgent(agent) 60 | 61 | map.getDirtStatus(agent) must beSome[VacuumPercept](CleanPercept) 62 | } 63 | 64 | "agent location of agent not on map must be none" in prop { map: VacuumMap => 65 | val agent = noopAgent 66 | map.getAgentLocation(agent) must beNone 67 | } 68 | 69 | "agent location must be A if in first spot" in { 70 | val agent = noopAgent 71 | val map = VacuumMap(Vector(agentNode(agent), cleanNode)).addAgent(agent) 72 | 73 | map.getAgentLocation(agent) must beSome[VacuumPercept](LocationAPercept) 74 | } 75 | 76 | "agent location must be B if in second spot" in { 77 | val agent = noopAgent 78 | val map = VacuumMap(Vector(cleanNode, agentNode(agent))).addAgent(agent) 79 | 80 | map.getAgentLocation(agent) must beSome[VacuumPercept](LocationBPercept) 81 | } 82 | 83 | "moving right will put in spot B" in prop { map: VacuumMap => 84 | val agent = noopAgent 85 | map.addAgent(agent).moveAgent(agent, RightMoveAction).getAgentLocation(agent) must beSome[VacuumPercept]( 86 | LocationBPercept 87 | ) 88 | } 89 | 90 | "moving left will put in spot A" in prop { map: VacuumMap => 91 | val agent = noopAgent 92 | map.addAgent(agent).moveAgent(agent, LeftMoveAction).getAgentLocation(agent) must beSome[VacuumPercept]( 93 | LocationAPercept 94 | ) 95 | } 96 | 97 | "updating dirt status of dirty will return dirt percept of dirty" in prop { map: VacuumMap => 98 | val agent = noopAgent 99 | map.addAgent(agent).updateStatus(agent, DirtyPercept).getDirtStatus(agent) must beSome[VacuumPercept](DirtyPercept) 100 | } 101 | 102 | "updating dirt status of clean will return clean percept of dirty" in prop { map: VacuumMap => 103 | val agent = noopAgent 104 | map.addAgent(agent).updateStatus(agent, CleanPercept).getDirtStatus(agent) must 105 | beSome[VacuumPercept](CleanPercept) 106 | } 107 | 108 | "removing agent must have no location percept" in prop { map: VacuumMap => 109 | val agent = noopAgent 110 | map.addAgent(agent).removeAgent(agent).getAgentLocation(agent) must beNone 111 | } 112 | 113 | "removing agent must have no status percept" in prop { map: VacuumMap => 114 | val agent = noopAgent 115 | map.addAgent(agent).removeAgent(agent).getDirtStatus(agent) must beNone 116 | } 117 | 118 | "adding an agent and then removing them should be the original input, eg f'(f(x)) = x" in prop { map: VacuumMap => 119 | val agent = noopAgent 120 | map.addAgent(agent).removeAgent(agent) must_== map 121 | } 122 | 123 | def noopAgentProgram = new AgentProgram[VacuumPercept, VacuumAction] { 124 | def agentFunction: AgentFunction = { _ => 125 | NoAction 126 | } 127 | } 128 | 129 | def noopAgent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction] = 130 | new Agent[VacuumEnvironment, VacuumPercept, VacuumAction] { 131 | override def actuators: List[Actuator[VacuumEnvironment, VacuumAction]] = List.empty 132 | override def sensors: List[Sensor[VacuumEnvironment, VacuumPercept]] = List.empty 133 | override def agentProgram: AgentProgram[VacuumPercept, VacuumAction] = 134 | noopAgentProgram 135 | 136 | } 137 | 138 | def agentNode(agent: Agent[VacuumEnvironment, VacuumPercept, VacuumAction] = noopAgent) = 139 | VacuumMapNode(maybeAgent = Some(agent)) 140 | def dirtyNode = VacuumMapNode(DirtyPercept, None) 141 | def cleanNode = VacuumMapNode(CleanPercept, None) 142 | } 143 | -------------------------------------------------------------------------------- /core/src/main/scala/aima/core/agent/basic/LRTAStarAgent.scala: -------------------------------------------------------------------------------- 1 | package aima.core.agent.basic 2 | 3 | import aima.core.agent.StatelessAgent 4 | import aima.core.agent.basic.LRTAStarAgent.IdentifyState 5 | import aima.core.agent.basic.LRTAStarAgentState.{COST_ESTIMATES, RESULT} 6 | import aima.core.search.api.OnlineSearchProblem 7 | import Ordering.Double.TotalOrdering 8 | 9 | /** 10 | * 11 | * 12 | * @author Shawn Garner 13 | */ 14 | final class LRTAStarAgent[PERCEPT, ACTION, STATE]( 15 | identifyStateFor: IdentifyState[PERCEPT, STATE], 16 | onlineProblem: OnlineSearchProblem[ACTION, STATE], 17 | h: STATE => Double, 18 | stop: ACTION 19 | ) extends StatelessAgent[PERCEPT, ACTION, LRTAStarAgentState[ACTION, STATE]] { 20 | 21 | import LRTAStarAgentState.Implicits._ 22 | 23 | type RESULT_TYPE = RESULT[ACTION, STATE] 24 | type COST_ESTIMATES_TYPE = COST_ESTIMATES[STATE] 25 | 26 | def lrtaCost(s: STATE, a: ACTION, sPrime: Option[STATE], H: COST_ESTIMATES_TYPE): Double = { 27 | val cost: Option[Double] = for { 28 | sPrime_ <- sPrime 29 | stepCost = onlineProblem.stepCost(s, a, sPrime_) 30 | tableLookupCost <- H.get(sPrime_) 31 | } yield stepCost + tableLookupCost 32 | 33 | cost.getOrElse(h(s)) 34 | } 35 | 36 | override val agentFunction: AgentFunction = { 37 | case (percept, priorAgentState) => 38 | val sPrime = identifyStateFor(percept) 39 | 40 | if (onlineProblem.isGoalState(sPrime)) { 41 | 42 | (stop, priorAgentState.copy(previousAction = Some(stop))) 43 | 44 | } else { 45 | 46 | val updatedH: COST_ESTIMATES_TYPE = 47 | priorAgentState.H.computeIfAbsent(sPrime, _ => h(sPrime)) 48 | 49 | val (updatedResult, updatedH2): (RESULT_TYPE, COST_ESTIMATES_TYPE) = 50 | (priorAgentState.previousState, priorAgentState.previousAction) match { 51 | case (Some(_s), Some(_a)) if !priorAgentState.result.get2(_s, _a).contains(sPrime) => 52 | val resultOrigActionToState: Map[ACTION, STATE] = 53 | priorAgentState.result.getOrElse(_s, Map.empty[ACTION, STATE]) 54 | val updatedResultActionToState 55 | : Map[ACTION, STATE] = resultOrigActionToState.put(_a, sPrime) // TODO: could be less verbose with lense 56 | 57 | val finalResult = priorAgentState.result.put(_s, updatedResultActionToState) 58 | val priorActionsCost = 59 | onlineProblem.actions(_s).map(b => lrtaCost(_s, b, finalResult.get2(_s, b), updatedH)) 60 | val minPriorActionCost = priorActionsCost match { 61 | case Nil => None 62 | case _ => Some(priorActionsCost.min) 63 | } 64 | val newH = minPriorActionCost match { 65 | case None => updatedH 66 | case Some(minCost) => updatedH.put(_s, minCost) 67 | } 68 | 69 | ( 70 | finalResult, 71 | newH 72 | ) 73 | case _ => 74 | ( 75 | priorAgentState.result, 76 | updatedH 77 | ) 78 | } 79 | 80 | val newActions: List[ACTION] = onlineProblem.actions(sPrime) 81 | val newAction: ACTION = newActions match { 82 | case Nil => stop 83 | case _ => newActions.minBy(b => lrtaCost(sPrime, b, updatedResult.get2(sPrime, b), updatedH2)) 84 | } 85 | 86 | val updatedAgentState = priorAgentState.copy( 87 | result = updatedResult, 88 | H = updatedH2, 89 | previousState = Some(sPrime), 90 | previousAction = Some(newAction) 91 | ) 92 | 93 | (newAction, updatedAgentState) 94 | } 95 | } 96 | 97 | } 98 | 99 | final case class LRTAStarAgentState[ACTION, STATE]( 100 | result: RESULT[ACTION, STATE], 101 | H: COST_ESTIMATES[STATE], 102 | previousState: Option[STATE], // s 103 | previousAction: Option[ACTION] // a 104 | ) 105 | 106 | object LRTAStarAgentState { 107 | 108 | def apply[ACTION, STATE] = 109 | new LRTAStarAgentState[ACTION, STATE]( 110 | result = Map.empty, 111 | H = Map.empty, 112 | previousState = None, 113 | previousAction = None 114 | ) 115 | 116 | type RESULT[ACTION, STATE] = Map[STATE, Map[ACTION, STATE]] 117 | type COST_ESTIMATES[STATE] = Map[STATE, Double] 118 | 119 | object Implicits { 120 | 121 | implicit class MapOps[K, V](m: Map[K, V]) { 122 | def put(k: K, v: V): Map[K, V] = 123 | m.updated(k, v) 124 | 125 | def computeIfAbsent(k: K, v: K => V): Map[K, V] = { 126 | if (m.contains(k)) { 127 | m 128 | } else { 129 | put(k, v(k)) 130 | } 131 | } 132 | 133 | def transformValue(k: K, fv: Option[V] => V): Map[K, V] = { 134 | val oldValue = m.get(k) 135 | val newValue = fv(oldValue) 136 | m.updated(k, newValue) 137 | } 138 | 139 | } 140 | 141 | implicit class Map2Ops[K1, K2, V](m: Map[K1, Map[K2, V]]) { 142 | def get2(k1: K1, k2: K2): Option[V] = 143 | m.get(k1).flatMap(_.get(k2)) 144 | 145 | } 146 | 147 | } 148 | } 149 | 150 | object LRTAStarAgent { 151 | type IdentifyState[PERCEPT, STATE] = PERCEPT => STATE 152 | } 153 | -------------------------------------------------------------------------------- /core/src/main/scala/aima/core/search/contingency/AndOrGraphSearch.scala: -------------------------------------------------------------------------------- 1 | package aima.core.search.contingency 2 | 3 | import aima.core.fp.Show 4 | 5 | import scala.annotation.tailrec 6 | import scala.reflect.ClassTag 7 | 8 | /** 9 | * 10 | *
11 | *
12 | * function AND-OR-GRAPH-SEARCH(problem) returns a conditional plan, or failure
13 | * OR-SEARCH(problem.INITIAL-STATE, problem, [])
14 | *
15 | * ---------------------------------------------------------------------------------
16 | *
17 | * function OR-SEARCH(state, problem, path) returns a conditional plan, or failure
18 | * if problem.GOAL-TEST(state) then return the empty plan
19 | * if state is on path then return failure
20 | * for each action in problem.ACTIONS(state) do
21 | * plan <- AND-SEARCH(RESULTS(state, action), problem, [state | path])
22 | * if plan != failure then return [action | plan]
23 | * return failure
24 | *
25 | * ---------------------------------------------------------------------------------
26 | *
27 | * function AND-SEARCH(states, problem, path) returns a conditional plan, or failure
28 | * for each si in states do
29 | * plani <- OR-SEARCH(si, problem, path)
30 | * if plani = failure then return failure
31 | * return [if s1 then plan1 else if s2 then plan2 else ... if sn-1 then plann-1 else plann]
32 | *
33 | *
34 | *
35 | * @author Shawn Garner
36 | */
37 | trait AndOrGraphSearch[ACTION, STATE] {
38 | implicit val aCT: ClassTag[ACTION]
39 | implicit val sCT: ClassTag[STATE]
40 | def andOrGraphSearch(problem: NondeterministicProblem[ACTION, STATE]): ConditionPlanResult =
41 | orSearch(problem.initialState(), problem, Nil)
42 |
43 | def orSearch(
44 | state: STATE,
45 | problem: NondeterministicProblem[ACTION, STATE],
46 | path: List[STATE]
47 | ): ConditionPlanResult = {
48 | if (problem.isGoalState(state)) {
49 | ConditionalPlan.emptyPlan
50 | } else if (path.contains(state)) {
51 | ConditionalPlanningFailure
52 | } else {
53 | val statePlusPath = state :: path
54 | val actions: List[ACTION] = problem.actions(state)
55 |
56 | @tailrec def recurse(a: List[ACTION]): ConditionPlanResult = a match {
57 | case Nil => ConditionalPlanningFailure
58 | case action :: rest =>
59 | andSearch(problem.results(state, action), problem, statePlusPath) match {
60 | case conditionalPlan: ConditionalPlan => newPlan(action, conditionalPlan)
61 | case ConditionalPlanningFailure => recurse(rest)
62 | }
63 | }
64 |
65 | recurse(actions)
66 | }
67 | }
68 |
69 | def andSearch(
70 | states: List[STATE],
71 | problem: NondeterministicProblem[ACTION, STATE],
72 | path: List[STATE]
73 | ): ConditionPlanResult = {
74 |
75 | @tailrec def recurse(currentStates: List[STATE], acc: List[(STATE, ConditionalPlan)]): ConditionPlanResult =
76 | currentStates match {
77 | case Nil => newPlan(acc)
78 | case si :: rest =>
79 | orSearch(si, problem, path) match {
80 | case ConditionalPlanningFailure => ConditionalPlanningFailure
81 | case plani: ConditionalPlan => recurse(rest, acc :+ (si -> plani))
82 | }
83 | }
84 |
85 | recurse(states, List.empty)
86 | }
87 |
88 | def newPlan(l: List[(STATE, ConditionalPlan)]): ConditionalPlan = l match {
89 | case (_, cp: ConditionalPlan) :: Nil => cp
90 | case ls => ConditionalPlan(ls.map(statePlan => ConditionedSubPlan(statePlan._1, statePlan._2)))
91 |
92 | }
93 |
94 | def newPlan(action: ACTION, plan: ConditionalPlan): ConditionalPlan =
95 | ConditionalPlan(ActionStep(action) :: plan.steps)
96 |
97 | }
98 |
99 | sealed trait Step
100 | final case class ActionStep[ACTION: ClassTag](action: ACTION) extends Step
101 | final case class ConditionedSubPlan[STATE: ClassTag](state: STATE, subPlan: ConditionalPlan) extends Step
102 |
103 | sealed trait ConditionPlanResult
104 | case object ConditionalPlanningFailure extends ConditionPlanResult
105 | final case class ConditionalPlan(steps: List[Step]) extends ConditionPlanResult
106 |
107 | object ConditionalPlan {
108 | val emptyPlan = ConditionalPlan(List.empty)
109 |
110 | object Implicits {
111 | import Show.Implicits._
112 | implicit def showConditionalPlan[STATE: ClassTag: Show, ACTION: ClassTag: Show]: Show[ConditionalPlan] =
113 | new Show[ConditionalPlan] {
114 |
115 | override def show(conditionalPlan: ConditionalPlan): String = {
116 |
117 | @tailrec def recurse(steps: List[Step], acc: String, lastStepAction: Boolean): String = steps match {
118 | case Nil => acc
119 | case ActionStep(a: ACTION) :: Nil => recurse(Nil, acc + a.show, true)
120 | case ActionStep(a: ACTION) :: rest => recurse(rest, acc + a.show + ", ", true)
121 | case ConditionedSubPlan(state: STATE, subPlan) :: rest if lastStepAction =>
122 | recurse(rest, acc + s"if State = ${state.show} then ${show(subPlan)}", false)
123 | case ConditionedSubPlan(_, subPlan) :: Nil =>
124 | recurse(Nil, acc + s" else ${show(subPlan)}", false)
125 | case ConditionedSubPlan(_, subPlan) :: ActionStep(a) :: rest =>
126 | recurse(ActionStep(a) :: rest, acc + s" else ${show(subPlan)}", false)
127 | case ConditionedSubPlan(state: STATE, subPlan) :: rest =>
128 | recurse(rest, acc + s" else if State = ${state.show} then ${show(subPlan)}", false)
129 | }
130 |
131 | recurse(conditionalPlan.steps, "[", true) + "]"
132 | }
133 | }
134 | }
135 |
136 | }
137 |
138 | trait NondeterministicProblem[ACTION, STATE] {
139 | def initialState(): STATE
140 | def actions(s: STATE): List[ACTION]
141 | def results(s: STATE, a: ACTION): List[STATE]
142 | def isGoalState(s: STATE): Boolean
143 | def stepCost(s: STATE, a: ACTION, childPrime: STATE): Double
144 | }
145 |
--------------------------------------------------------------------------------
/core/src/test/scala/aima/core/agent/basic/LRTAStarAgentSpec.scala:
--------------------------------------------------------------------------------
1 | package aima.core.agent.basic
2 |
3 | import aima.core.environment.map2d.{
4 | Distance,
5 | ExtendableMap2D,
6 | Go,
7 | InState,
8 | IntPercept,
9 | Map2DAction,
10 | Map2DFunctionFactory,
11 | NoOp
12 | }
13 | import aima.core.fp.Eqv
14 | import aima.core.search.api.OnlineSearchProblem
15 | import org.scalacheck.{Arbitrary, Gen}
16 | import org.specs2.ScalaCheck
17 | import org.specs2.mutable.Specification
18 |
19 | import scala.annotation.tailrec
20 |
21 | /**
22 | * @author Shawn Garner
23 | */
24 | class LRTAStarAgentSpec extends Specification with ScalaCheck {
25 | val distanceOne = Distance(1.0d)
26 | val mapAtoF = new ExtendableMap2D() {
27 | addBidirectionalLink("A", "B", distanceOne)
28 | addBidirectionalLink("B", "C", distanceOne)
29 | addBidirectionalLink("C", "D", distanceOne)
30 | addBidirectionalLink("D", "E", distanceOne)
31 | addBidirectionalLink("E", "F", distanceOne)
32 | }
33 |
34 | val alphabetPerceptToState: IntPercept => InState =
35 | percept => InState(new String(Array(('A' + percept.value).toChar)))
36 |
37 | val alphabetStateToPercept: InState => IntPercept =
38 | inState => IntPercept(inState.location.charAt(0) - 'A')
39 |
40 | "already at goal" in {
41 | val problem = new OnlineSearchProblem[Map2DAction, InState] {
42 | override def actions(s: InState): List[Map2DAction] =
43 | Map2DFunctionFactory.actions(mapAtoF)(s)
44 |
45 | import Eqv.Implicits.stringEq
46 | override def isGoalState(s: InState): Boolean =
47 | Eqv[String].eqv("A", s.location)
48 |
49 | override def stepCost(s: InState, a: Map2DAction, sPrime: InState): Double =
50 | Map2DFunctionFactory.stepCost(mapAtoF)(s, a, sPrime)
51 | }
52 |
53 | val lrtasa = new LRTAStarAgent[IntPercept, Map2DAction, InState](
54 | alphabetPerceptToState,
55 | problem,
56 | _ => 1.0d,
57 | NoOp
58 | )
59 |
60 | val resultAction = lrtasa.agentFunction(IntPercept(0), LRTAStarAgentState[Map2DAction, InState])
61 | resultAction._1 must_== NoOp
62 | }
63 |
64 | "normal search A to F" in {
65 | val problem = new OnlineSearchProblem[Map2DAction, InState] {
66 | override def actions(s: InState): List[Map2DAction] =
67 | Map2DFunctionFactory.actions(mapAtoF)(s)
68 |
69 | import Eqv.Implicits.stringEq
70 | override def isGoalState(s: InState): Boolean =
71 | Eqv[String].eqv("F", s.location)
72 |
73 | override def stepCost(s: InState, a: Map2DAction, sPrime: InState): Double =
74 | Map2DFunctionFactory.stepCost(mapAtoF)(s, a, sPrime)
75 | }
76 |
77 | val lrtasa = new LRTAStarAgent[IntPercept, Map2DAction, InState](
78 | alphabetPerceptToState,
79 | problem,
80 | inState => ('F' - inState.location.charAt(0)).toDouble,
81 | NoOp
82 | )
83 |
84 | val actions = actionSequence(lrtasa, alphabetStateToPercept(InState("A")))
85 | actions must_==
86 | List(
87 | Go("B"),
88 | Go("A"),
89 | Go("B"),
90 | Go("C"),
91 | Go("B"),
92 | Go("C"),
93 | Go("D"),
94 | Go("C"),
95 | Go("D"),
96 | Go("E"),
97 | Go("D"),
98 | Go("E"),
99 | Go("F"),
100 | NoOp
101 | )
102 |
103 | }
104 |
105 | "normal search F to A" in {
106 | val problem = new OnlineSearchProblem[Map2DAction, InState] {
107 | override def actions(s: InState): List[Map2DAction] =
108 | Map2DFunctionFactory.actions(mapAtoF)(s)
109 |
110 | import Eqv.Implicits.stringEq
111 | override def isGoalState(s: InState): Boolean =
112 | Eqv[String].eqv("A", s.location)
113 |
114 | override def stepCost(s: InState, a: Map2DAction, sPrime: InState): Double =
115 | Map2DFunctionFactory.stepCost(mapAtoF)(s, a, sPrime)
116 | }
117 |
118 | val lrtasa = new LRTAStarAgent[IntPercept, Map2DAction, InState](
119 | alphabetPerceptToState,
120 | problem,
121 | inState => (inState.location.charAt(0) - 'A').toDouble,
122 | NoOp
123 | )
124 |
125 | val actions = actionSequence(lrtasa, alphabetStateToPercept(InState("F")))
126 | actions must_==
127 | List(
128 | Go("E"),
129 | Go("D"),
130 | Go("C"),
131 | Go("B"),
132 | Go("A"),
133 | NoOp
134 | )
135 |
136 | }
137 |
138 | implicit val arbInState: Arbitrary[InState] = Arbitrary {
139 | for {
140 | perceptInt <- Gen.choose(0, 5)
141 | } yield alphabetPerceptToState(IntPercept(perceptInt))
142 | }
143 |
144 | "find solutions for all start and goal states" >> prop { (initialState: InState, goalState: InState) =>
145 | val problem = new OnlineSearchProblem[Map2DAction, InState] {
146 | override def actions(s: InState): List[Map2DAction] =
147 | Map2DFunctionFactory.actions(mapAtoF)(s)
148 |
149 | import Eqv.Implicits.stringEq
150 | override def isGoalState(s: InState): Boolean =
151 | Eqv[String].eqv(goalState.location, s.location)
152 |
153 | override def stepCost(s: InState, a: Map2DAction, sPrime: InState): Double =
154 | Map2DFunctionFactory.stepCost(mapAtoF)(s, a, sPrime)
155 | }
156 |
157 | val lrtasa = new LRTAStarAgent[IntPercept, Map2DAction, InState](
158 | alphabetPerceptToState,
159 | problem,
160 | inState => math.abs(inState.location.charAt(0) - goalState.location.charAt(0)).toDouble,
161 | NoOp
162 | )
163 |
164 | val actions = actionSequence(lrtasa, alphabetStateToPercept(initialState))
165 | import Eqv.Implicits.stringEq
166 | if (Eqv[String].eqv(initialState.location, goalState.location)) {
167 | actions must_== List(NoOp)
168 | } else {
169 | actions must contain[Map2DAction](Go(goalState.location))
170 | }
171 |
172 | }
173 |
174 | def actionSequence(
175 | lrtasa: LRTAStarAgent[IntPercept, Map2DAction, InState],
176 | initialPercept: IntPercept
177 | ): List[Map2DAction] = {
178 | @tailrec def findActions(
179 | agentState: LRTAStarAgentState[Map2DAction, InState],
180 | percept: IntPercept,
181 | acc: List[Map2DAction]
182 | ): List[Map2DAction] = {
183 | val (action, updatedAgentState) = lrtasa.agentFunction(percept, agentState)
184 |
185 | action match {
186 | case NoOp => action :: acc
187 | case a @ Go(goTo) =>
188 | val nextPercept = alphabetStateToPercept(InState(goTo))
189 | findActions(updatedAgentState, nextPercept, a :: acc)
190 | }
191 | }
192 |
193 | findActions(LRTAStarAgentState[Map2DAction, InState], initialPercept, Nil).reverse
194 | }
195 |
196 | }
197 |
--------------------------------------------------------------------------------
/core/src/main/scala/aima/core/agent/basic/OnlineDFSAgent.scala:
--------------------------------------------------------------------------------
1 | package aima.core.agent.basic
2 |
3 | import aima.core.agent.StatelessAgent
4 | import aima.core.agent.basic.OnlineDFSAgent.IdentifyState
5 | import aima.core.agent.basic.OnlineDFSAgentState.{RESULT, UNBACKTRACKED, UNTRIED}
6 | import aima.core.fp.Eqv
7 | import aima.core.fp.Eqv.Implicits._
8 | import aima.core.search.api.OnlineSearchProblem
9 |
10 | /**
11 | * 12 | * function ONLINE-DFS-AGENT(s′) returns an action 13 | * inputs: s′, a percept that identifies the current state 14 | * persistent: result, a table, indexed by state and action, initially empty 15 | * untried, a table that lists, for each state, the actions not yet tried 16 | * unbacktracked, a table that lists, for each state, the backtracks not yet tried 17 | * s, a, the previous state and action, initially null 18 | * 19 | * if GOAL-TEST(s′) then return stop 20 | * if s′ is a new state (not in untried) then untried[s′] ← ACTIONS(s′) 21 | * if s is not null and s′ ≠ result[s, a] then 22 | * result[s, a] ← s′ 23 | * add s to the front of the unbacktracked[s′] 24 | * if untried[s′] is empty then 25 | * if unbacktracked[s′] is empty then return stop 26 | * else a ← an action b such that result[s′, b] = POP(unbacktracked[s′]) 27 | * else a ← POP(untried[s′]) 28 | * s ← s′ 29 | * return a 30 | *31 | * 32 | * @author Shawn Garner 33 | */ 34 | final class OnlineDFSAgent[PERCEPT, ACTION, STATE: Eqv]( 35 | identifyStateFor: IdentifyState[PERCEPT, STATE], 36 | onlineProblem: OnlineSearchProblem[ACTION, STATE], 37 | stop: ACTION 38 | ) extends StatelessAgent[PERCEPT, ACTION, OnlineDFSAgentState[ACTION, STATE]] { 39 | 40 | import OnlineDFSAgentState.Implicits._ 41 | 42 | type RESULT_TYPE = RESULT[ACTION, STATE] 43 | type UNTRIED_TYPE = UNTRIED[ACTION, STATE] 44 | type UNBACKTRACKED_TYPE = UNBACKTRACKED[STATE] 45 | 46 | override val agentFunction: AgentFunction = { 47 | case (percept, priorAgentState) => 48 | val sPrime = identifyStateFor(percept) 49 | 50 | if (onlineProblem.isGoalState(sPrime)) { 51 | 52 | (stop, priorAgentState.copy(previousAction = Some(stop))) 53 | 54 | } else { 55 | 56 | val updatedUntried: UNTRIED_TYPE = 57 | priorAgentState.untried.computeIfAbsent(sPrime, _ => onlineProblem.actions(sPrime)) 58 | 59 | val (updatedResult, updatedUnbacktracked): (RESULT_TYPE, UNBACKTRACKED_TYPE) = 60 | (priorAgentState.previousState, priorAgentState.previousAction) match { 61 | case (Some(_s), Some(_a)) if !priorAgentState.result.get(_s).flatMap(_.get(_a)).contains(sPrime) => 62 | val resultOrigActionToState: Map[ACTION, STATE] = 63 | priorAgentState.result.getOrElse(_s, Map.empty[ACTION, STATE]) 64 | val updatedResultActionToState 65 | : Map[ACTION, STATE] = resultOrigActionToState.put(_a, sPrime) // TODO: could be less verbose with lense 66 | ( 67 | priorAgentState.result.put(_s, updatedResultActionToState), 68 | priorAgentState.unbacktracked.transformValue(sPrime, fv => fv.fold(List(_s))(st => _s :: st)) 69 | ) 70 | case _ => 71 | ( 72 | priorAgentState.result, 73 | priorAgentState.unbacktracked 74 | ) 75 | } 76 | 77 | val updatedUntriedList: List[ACTION] = updatedUntried.get(sPrime).toList.flatten 78 | 79 | val updatedAgentState: OnlineDFSAgentState[ACTION, STATE] = updatedUntriedList match { 80 | case Nil => 81 | val unbacktrackedList: List[STATE] = updatedUnbacktracked.get(sPrime).toList.flatten 82 | unbacktrackedList match { 83 | case Nil => 84 | priorAgentState.copy( 85 | previousAction = Some(stop), 86 | previousState = Some(sPrime), 87 | untried = updatedUntried, 88 | result = updatedResult, 89 | unbacktracked = updatedUnbacktracked 90 | ) 91 | 92 | case popped :: remainingUnbacktracked => 93 | val action: Option[ACTION] = 94 | updatedResult.getOrElse(sPrime, Map.empty[ACTION, STATE]).toList.collectFirst { 95 | case (action, state) if popped === state => action 96 | } 97 | 98 | priorAgentState.copy( 99 | previousAction = action, 100 | previousState = Some(sPrime), 101 | untried = updatedUntried, 102 | result = updatedResult, 103 | unbacktracked = updatedUnbacktracked.updated(sPrime, remainingUnbacktracked) 104 | ) 105 | } 106 | 107 | case popped :: remainingUntried => 108 | priorAgentState.copy( 109 | previousAction = Some(popped), 110 | previousState = Some(sPrime), 111 | untried = updatedUntried.updated(sPrime, remainingUntried), 112 | result = updatedResult, 113 | unbacktracked = updatedUnbacktracked 114 | ) 115 | } 116 | 117 | (updatedAgentState.previousAction.getOrElse(stop), updatedAgentState) 118 | } 119 | } 120 | 121 | } 122 | 123 | final case class OnlineDFSAgentState[ACTION, STATE]( 124 | result: RESULT[ACTION, STATE], 125 | untried: UNTRIED[ACTION, STATE], 126 | unbacktracked: UNBACKTRACKED[STATE], 127 | previousState: Option[STATE], // s 128 | previousAction: Option[ACTION] // a 129 | ) 130 | 131 | object OnlineDFSAgentState { 132 | 133 | def apply[ACTION, STATE] = 134 | new OnlineDFSAgentState[ACTION, STATE]( 135 | result = Map.empty, 136 | untried = Map.empty, 137 | unbacktracked = Map.empty, 138 | previousState = None, 139 | previousAction = None 140 | ) 141 | 142 | type RESULT[ACTION, STATE] = Map[STATE, Map[ACTION, STATE]] 143 | type UNTRIED[ACTION, STATE] = Map[STATE, List[ACTION]] 144 | type UNBACKTRACKED[STATE] = Map[STATE, List[STATE]] 145 | 146 | object Implicits { 147 | 148 | implicit class MapOps[K, V](m: Map[K, V]) { 149 | def put(k: K, v: V): Map[K, V] = 150 | m.updated(k, v) 151 | 152 | def computeIfAbsent(k: K, v: K => V): Map[K, V] = { 153 | if (m.contains(k)) { 154 | m 155 | } else { 156 | put(k, v(k)) 157 | } 158 | } 159 | 160 | def transformValue(k: K, fv: Option[V] => V): Map[K, V] = { 161 | val oldValue = m.get(k) 162 | val newValue = fv(oldValue) 163 | m.updated(k, newValue) 164 | } 165 | 166 | } 167 | 168 | } 169 | } 170 | 171 | object OnlineDFSAgent { 172 | type IdentifyState[PERCEPT, STATE] = PERCEPT => STATE 173 | } 174 | -------------------------------------------------------------------------------- /core/src/test/scala/aima/core/search/local/SimulatedAnnealingSearchSpec.scala: -------------------------------------------------------------------------------- 1 | package aima.core.search.local 2 | 3 | import aima.core.search.Problem 4 | import aima.core.search.local.SimulatedAnnealingSearch.{ 5 | BasicSchedule, 6 | OverTimeStepLimit, 7 | Temperature, 8 | TemperatureResult 9 | } 10 | import aima.core.search.local.time.TimeStep 11 | import org.scalacheck.{Arbitrary, Gen} 12 | import org.specs2.ScalaCheck 13 | import org.specs2.mutable.Specification 14 | 15 | import scala.util.{Success, Try} 16 | 17 | /** 18 | * @author Shawn Garner 19 | */ 20 | class SimulatedAnnealingSearchSpec extends Specification with ScalaCheck { 21 | 22 | "BasicSchedule" >> { 23 | import aima.core.search.local.SimulatedAnnealingSearch.BasicSchedule.schedule 24 | import aima.core.search.local.time.TimeStep.Implicits._ 25 | 26 | def increment(ts: TimeStep, times: Int): Try[TimeStep] = times match { 27 | case 0 => Success(ts) 28 | case _ => ts.step.flatMap(increment(_, times - 1)) 29 | } 30 | 31 | "lower limit check" in { 32 | schedule()(TimeStep.start) match { 33 | case Temperature(t) => t must beCloseTo(19.1d within 3.significantFigures) 34 | case other => ko(other.toString) 35 | } 36 | } 37 | 38 | "upper limit check" in { 39 | increment(TimeStep.start, 98).map(schedule()(_)) match { 40 | case Success(Temperature(t)) => t must beCloseTo(0.232d within 3.significantFigures) 41 | case other => ko(other.toString) 42 | } 43 | 44 | } 45 | 46 | "over limit check" in { 47 | increment(TimeStep.start, 99).map(schedule()(_)) must beSuccessfulTry[TemperatureResult](OverTimeStepLimit) 48 | } 49 | 50 | implicit val arbTimeStep: Arbitrary[TimeStep] = Arbitrary { 51 | for { 52 | numSteps <- Gen.choose[Int](0, 98) 53 | } yield increment(TimeStep.start, numSteps).get 54 | } 55 | 56 | "must not create negative temperature" >> prop { ts: TimeStep => 57 | schedule()(ts) match { 58 | case Temperature(t) => t must be_>(0.00d) 59 | case other => ko(other.toString) 60 | } 61 | } 62 | 63 | } 64 | 65 | "8 queens problem problem" >> { 66 | import SimulatedAnnealingSearchSpec._ 67 | implicit val arbTimeStep: Arbitrary[EightQueensState] = Arbitrary { 68 | val all = (0 to 7).toList 69 | for { 70 | q0row <- Gen.oneOf(all) 71 | l1 = all.filterNot(_ == q0row) 72 | q1row <- Gen.oneOf(l1) 73 | l2 = l1.filterNot(_ == q1row) 74 | q2row <- Gen.oneOf(l2) 75 | l3 = l2.filterNot(_ == q2row) 76 | q3row <- Gen.oneOf(l3) 77 | l4 = l3.filterNot(_ == q3row) 78 | q4row <- Gen.oneOf(l4) 79 | l5 = l4.filterNot(_ == q4row) 80 | q5row <- Gen.oneOf(l5) 81 | l6 = l5.filterNot(_ == q5row) 82 | q6row <- Gen.oneOf(l6) 83 | l7 = l6.filterNot(_ == q6row) 84 | q7row <- Gen.oneOf(l7) 85 | } yield EightQueensState( 86 | List( 87 | QueenPosition(q0row), 88 | QueenPosition(q1row), 89 | QueenPosition(q2row), 90 | QueenPosition(q3row), 91 | QueenPosition(q4row), 92 | QueenPosition(q5row), 93 | QueenPosition(q6row), 94 | QueenPosition(q7row) 95 | ) 96 | ) 97 | } 98 | 99 | "find solution" >> prop { s: EightQueensState => 100 | val eightQueensProblem = new Problem[EightQueensState, EightQueensAction] { 101 | override def initialState = s 102 | 103 | override def isGoalState(state: EightQueensState): Boolean = false // Not used 104 | 105 | override def actions(state: EightQueensState): List[EightQueensAction] = state match { 106 | case EightQueensState(cols) => 107 | cols.zipWithIndex.flatMap { 108 | case (QueenPosition(rowIndex), colIndex) => 109 | (0 to 7).toList.filterNot(r => rowIndex == r).map(newRowIndex => MoveTo(colIndex, newRowIndex)) 110 | } 111 | } 112 | 113 | override def result(state: EightQueensState, action: EightQueensAction): EightQueensState = 114 | (state, action) match { 115 | case (EightQueensState(cols), MoveTo(colIndex, newRowIndex)) => 116 | EightQueensState(cols.updated(colIndex, QueenPosition(newRowIndex))) 117 | 118 | } 119 | 120 | override def stepCost(state: EightQueensState, action: EightQueensAction, childPrime: EightQueensState): Int = 121 | -1 // Not used 122 | } 123 | 124 | val result = 125 | SimulatedAnnealingSearch.apply( 126 | queenStateToValue, 127 | eightQueensProblem, 128 | BasicSchedule.schedule(BasicSchedule.defaultScheduleParams.copy(limit = 10000)) 129 | ) 130 | 131 | result must beSuccessfulTry.like { 132 | case s @ EightQueensState(_) => queenStateToValue(s) must be beCloseTo (8.00d within 2.significantFigures) 133 | } 134 | 135 | result.foreach { 136 | case s @ EightQueensState(cols) => 137 | val numQueensRightScore = queenStateToValue(s) 138 | println(s"*** Score: $numQueensRightScore") 139 | println(List.fill(8)("-").mkString(" ", " ", " ")) 140 | 141 | for { 142 | currentRow <- 0 to 7 143 | row = cols.zipWithIndex.map { 144 | case (QueenPosition(r), _) if r == currentRow => "Q" 145 | case _ => " " 146 | } 147 | } { 148 | println(row.mkString("|", "|", "|")) 149 | println(List.fill(8)("-").mkString(" ", " ", " ")) 150 | } 151 | } 152 | 153 | ok 154 | }.set(minTestsOk = 1) 155 | } 156 | 157 | } 158 | 159 | object SimulatedAnnealingSearchSpec { 160 | sealed trait EightQueensAction 161 | final case class MoveTo(columnIndex: Int, newRowIndex: Int) extends EightQueensAction 162 | 163 | final case class QueenPosition(row: Int) extends AnyVal 164 | final case class EightQueensState(columns: List[QueenPosition]) 165 | 166 | def canAttackHorizontal(columnIndex: Int, s: EightQueensState): Boolean = { 167 | val currentRow = s.columns(columnIndex).row 168 | val rows: List[Int] = s.columns.zipWithIndex.filterNot(_._2 == columnIndex).map(_._1.row) 169 | rows.contains(currentRow) 170 | } 171 | 172 | def canAttackDiagonal(columnIndex: Int, s: EightQueensState): Boolean = { 173 | val currentRow = s.columns(columnIndex).row 174 | val rows: List[Boolean] = s.columns.zipWithIndex.filterNot(_._2 == columnIndex).map { 175 | case (QueenPosition(rowIdx), colIdx) => 176 | val run = math.abs(colIdx - columnIndex) 177 | val rise = math.abs(rowIdx - currentRow) 178 | rise == run 179 | } 180 | 181 | rows.contains(true) 182 | } 183 | 184 | val queenStateToValue: EightQueensState => Double = { 185 | case state @ EightQueensState(cols) => 186 | cols.zipWithIndex.foldLeft(0.0d) { 187 | case (acc, elem) => 188 | val currentColumnIndex = elem._2 189 | if (canAttackHorizontal(currentColumnIndex, state) || canAttackDiagonal(currentColumnIndex, state)) { 190 | acc 191 | } else { 192 | acc + 1.0d 193 | } 194 | 195 | } 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /core/src/test/scala/aima/core/agent/basic/OnlineDFSAgentSpec.scala: -------------------------------------------------------------------------------- 1 | package aima.core.agent.basic 2 | 3 | import aima.core.agent.basic.OnlineDFSAgent.IdentifyState 4 | import aima.core.fp.Eqv 5 | import aima.core.search.api.OnlineSearchProblem 6 | import org.scalacheck.{Arbitrary, Gen} 7 | import org.specs2.ScalaCheck 8 | import org.specs2.mutable.Specification 9 | 10 | import scala.annotation.tailrec 11 | 12 | /** 13 | * @author Shawn Garner 14 | */ 15 | class OnlineDFSAgentSpec extends Specification with ScalaCheck { 16 | 17 | /* 18 | 3 | x G 19 | xxxx---x-- 20 | 2 x x 21 | ---x---x-- 22 | 1 S | | 23 | 1 2 3 24 | 25 | x is impassable 26 | | or - is passable 27 | S is start 28 | G is goal 29 | 30 | */ 31 | "maze Figure 4.19" should { 32 | import OnlineDFSAgentSpec.Maze._ 33 | import MazeState.Implicits.mazeStateEq 34 | 35 | "find action solution from example" >> { 36 | val goalState = MazeXYState(3, 3) 37 | val agent = new OnlineDFSAgent[MazePositionPercept, MazeAction, MazeXYState]( 38 | idStateFn, 39 | mazeProblem(goalState), 40 | StopAction 41 | ) 42 | 43 | val initialState = MazeXYState(1, 1) 44 | val actions = determineActions(initialState, agent) 45 | actions must_== List(Up, Down, Right, Up, Up, Left, Right, Down, Down, Right, Up, Up) 46 | } 47 | 48 | "find moveTO action solution from example" >> { 49 | val goalState = MazeXYState(3, 3) 50 | val agent = new OnlineDFSAgent[MazePositionPercept, MazeAction, MazeXYState]( 51 | idStateFn, 52 | mazeProblem(goalState), 53 | StopAction 54 | ) 55 | 56 | val initialState = MazeXYState(1, 1) 57 | val states = determineMoveToStates(initialState, agent) 58 | val goToActions = states.map { 59 | case MazeXYState(x, y) => s"Go($x,$y)" 60 | } 61 | val display = goToActions.mkString("", " ", " NoOp") 62 | display must_== "Go(1,2) Go(1,1) Go(2,1) Go(2,2) Go(2,3) Go(1,3) Go(2,3) Go(2,2) Go(2,1) Go(3,1) Go(3,2) Go(3,3) NoOp" 63 | } 64 | 65 | import aima.core.agent.basic.OnlineDFSAgentSpec.Maze.MazeState.Implicits.arbMazeState 66 | "find solutions for all start and goal states" >> prop { (initialState: MazeXYState, goalState: MazeXYState) => 67 | val agent = new OnlineDFSAgent[MazePositionPercept, MazeAction, MazeXYState]( 68 | idStateFn, 69 | mazeProblem(goalState), 70 | StopAction 71 | ) 72 | 73 | val states = determineMoveToStates(initialState, agent) 74 | 75 | states must beLike { 76 | case Nil => ok // already on goal 77 | case ls => ls must contain[MazeXYState](goalState) 78 | } 79 | } 80 | 81 | } 82 | 83 | } 84 | 85 | object OnlineDFSAgentSpec { 86 | /* 87 | 88 | 3 6 | 7 | 8 89 | ---|---|-- 90 | 2 3 | 4 | 5 91 | ---|---|-- 92 | 1 0 | 1 | 2 93 | 1 2 3 94 | 95 | State is (x,y) eg (2,2) is middle 96 | Percept is number at state eg Percept 4 is State (2,2) 97 | 98 | **/ 99 | object Maze { 100 | sealed trait MazeAction 101 | case object Up extends MazeAction 102 | case object Right extends MazeAction 103 | case object Left extends MazeAction 104 | case object Down extends MazeAction 105 | case object StopAction extends MazeAction 106 | 107 | final case class MazeXYState(x: Int, y: Int) 108 | 109 | object MazeState { 110 | object Implicits { 111 | implicit val arbMazeState: Arbitrary[MazeXYState] = Arbitrary { 112 | for { 113 | x <- Gen.oneOf(1, 2, 3) 114 | y <- Gen.oneOf(1, 2, 3) 115 | } yield MazeXYState(x, y) 116 | } 117 | 118 | implicit val mazeStateEq: Eqv[MazeXYState] = new Eqv[MazeXYState] { 119 | override def eqv(a1: MazeXYState, a2: MazeXYState): Boolean = 120 | a1.x == a2.x && a1.y == a2.y 121 | 122 | } 123 | } 124 | } 125 | 126 | final case class MazePositionPercept(position: Int) 127 | 128 | def determineActions( 129 | initialState: MazeXYState, 130 | agent: OnlineDFSAgent[MazePositionPercept, MazeAction, MazeXYState] 131 | ): List[MazeAction] = { 132 | 133 | @tailrec 134 | def d( 135 | s: MazeXYState, 136 | currentAgentState: OnlineDFSAgentState[MazeAction, MazeXYState], 137 | acc: List[MazeAction] 138 | ): List[MazeAction] = { 139 | val p = stateToPerceptFn(s) 140 | val (action, updatedAgentState) = agent.agentFunction(p, currentAgentState) 141 | if (action == StopAction) { 142 | acc.reverse 143 | } else { 144 | val statePrime = nextState(s, action) 145 | 146 | d(statePrime, updatedAgentState, action :: acc) 147 | } 148 | 149 | } 150 | 151 | d(initialState, OnlineDFSAgentState[MazeAction, MazeXYState], Nil) 152 | } 153 | def determineMoveToStates( 154 | initialState: MazeXYState, 155 | agent: OnlineDFSAgent[MazePositionPercept, MazeAction, MazeXYState] 156 | ): List[MazeXYState] = { 157 | 158 | @tailrec 159 | def d( 160 | s: MazeXYState, 161 | currentAgentState: OnlineDFSAgentState[MazeAction, MazeXYState], 162 | acc: List[MazeXYState] 163 | ): List[MazeXYState] = { 164 | val p = stateToPerceptFn(s) 165 | val (action, updatedAgentState) = agent.agentFunction(p, currentAgentState) 166 | if (action == StopAction) { 167 | acc.reverse 168 | } else { 169 | val statePrime = nextState(s, action) 170 | 171 | d(statePrime, updatedAgentState, statePrime :: acc) 172 | } 173 | 174 | } 175 | 176 | d(initialState, OnlineDFSAgentState[MazeAction, MazeXYState], Nil) 177 | } 178 | 179 | val idStateFn: IdentifyState[MazePositionPercept, MazeXYState] = { 180 | case MazePositionPercept(0) => MazeXYState(1, 1) 181 | case MazePositionPercept(1) => MazeXYState(2, 1) 182 | case MazePositionPercept(2) => MazeXYState(3, 1) 183 | case MazePositionPercept(3) => MazeXYState(1, 2) 184 | case MazePositionPercept(4) => MazeXYState(2, 2) 185 | case MazePositionPercept(5) => MazeXYState(3, 2) 186 | case MazePositionPercept(6) => MazeXYState(1, 3) 187 | case MazePositionPercept(7) => MazeXYState(2, 3) 188 | case MazePositionPercept(8) => MazeXYState(3, 3) 189 | case _ => MazeXYState(1, 1) // should not be called 190 | } 191 | 192 | def nextState(state: MazeXYState, action: MazeAction): MazeXYState = action match { 193 | case Up => MazeXYState(state.x, state.y + 1) 194 | case Down => MazeXYState(state.x, state.y - 1) 195 | case Right => MazeXYState(state.x + 1, state.y) 196 | case Left => MazeXYState(state.x - 1, state.y) 197 | case StopAction => state 198 | } 199 | 200 | val stateToPerceptFn: MazeXYState => MazePositionPercept = { 201 | case MazeXYState(1, 1) => MazePositionPercept(0) 202 | case MazeXYState(2, 1) => MazePositionPercept(1) 203 | case MazeXYState(3, 1) => MazePositionPercept(2) 204 | case MazeXYState(1, 2) => MazePositionPercept(3) 205 | case MazeXYState(2, 2) => MazePositionPercept(4) 206 | case MazeXYState(3, 2) => MazePositionPercept(5) 207 | case MazeXYState(1, 3) => MazePositionPercept(6) 208 | case MazeXYState(2, 3) => MazePositionPercept(7) 209 | case MazeXYState(3, 3) => MazePositionPercept(8) 210 | case _ => MazePositionPercept(0) // should not be called 211 | } 212 | 213 | def mazeProblem(goal: MazeXYState) = new OnlineSearchProblem[MazeAction, MazeXYState] { 214 | override def actions(s: MazeXYState): List[MazeAction] = s match { 215 | case MazeXYState(1, 1) => List(Up, Right) 216 | case MazeXYState(2, 1) => List(Up, Right, Left) 217 | case MazeXYState(3, 1) => List(Up, Left) 218 | case MazeXYState(1, 2) => List(Down) 219 | case MazeXYState(2, 2) => List(Up, Down) 220 | case MazeXYState(3, 2) => List(Up, Down) 221 | case MazeXYState(1, 3) => List(Right) 222 | case MazeXYState(2, 3) => List(Left, Down) 223 | case MazeXYState(3, 3) => List(Down) 224 | case _ => List() 225 | } 226 | override def isGoalState(s: MazeXYState): Boolean = s == goal 227 | 228 | override def stepCost(s: MazeXYState, a: MazeAction, sPrime: MazeXYState): Double = 229 | ??? // not used 230 | } 231 | 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /core/src/test/scala/aima/core/search/contingency/AndOrGraphSearchSpec.scala: -------------------------------------------------------------------------------- 1 | package aima.core.search.contingency 2 | 3 | import aima.core.fp.Show 4 | import aima.core.search.contingency.AndOrGraphSearchSpec.{Action, VacuumWorldState} 5 | import org.scalacheck.{Arbitrary, Gen} 6 | import org.specs2.ScalaCheck 7 | import org.specs2.mutable.Specification 8 | 9 | import scala.reflect.ClassTag 10 | 11 | object AndOrGraphSearchSpec { 12 | sealed trait Action 13 | case object MoveLeft extends Action 14 | case object MoveRight extends Action 15 | case object Suck extends Action 16 | 17 | object Action { 18 | object Implicits { 19 | implicit val showAction: Show[Action] = new Show[Action] { 20 | override def show(action: Action): String = action match { 21 | case MoveLeft => "Left" 22 | case MoveRight => "Right" 23 | case Suck => "Suck" 24 | } 25 | } 26 | } 27 | } 28 | 29 | val allActions = List(MoveLeft, Suck, MoveRight) 30 | 31 | sealed trait Location 32 | case object LocationA extends Location 33 | case object LocationB extends Location 34 | 35 | sealed trait Status 36 | case object Dirty extends Status 37 | case object Clean extends Status 38 | 39 | object Status { 40 | object Implicits { 41 | implicit val statusShow: Show[Status] = new Show[Status] { 42 | override def show(s: Status): String = s match { 43 | case Dirty => "*" 44 | case Clean => " " 45 | } 46 | } 47 | } 48 | } 49 | 50 | final case class VacuumWorldState(vacuumLocation: Location, a: Status, b: Status) 51 | 52 | object VacuumWorldState { 53 | object Implicits { 54 | implicit val arbVacuumWorldState = Arbitrary { 55 | for { 56 | location <- Gen.oneOf(LocationA, LocationB) 57 | aStatus <- Gen.oneOf(Dirty, Clean) 58 | bStatus <- Gen.oneOf(Dirty, Clean) 59 | } yield VacuumWorldState(location, aStatus, bStatus) 60 | } 61 | 62 | import Show.Implicits._ 63 | implicit def vacuumWorldStateShow(implicit statusShow: Show[Status]): Show[VacuumWorldState] = 64 | new Show[VacuumWorldState] { 65 | override def show(s: VacuumWorldState): String = 66 | s.vacuumLocation match { 67 | case LocationA => s"[${s.a.show}_/][${s.b.show} ]" 68 | case LocationB => s"[${s.a.show} ][${s.b.show}_/]" 69 | } 70 | } 71 | } 72 | } 73 | 74 | def problem(initial: VacuumWorldState) = new NondeterministicProblem[Action, VacuumWorldState] { 75 | override def initialState(): VacuumWorldState = initial 76 | 77 | override def actions(s: VacuumWorldState): List[Action] = allActions 78 | 79 | override def results(s: VacuumWorldState, a: Action): List[VacuumWorldState] = (s, a) match { 80 | case (_, MoveRight) => List(s.copy(vacuumLocation = LocationB)) 81 | case (_, MoveLeft) => List(s.copy(vacuumLocation = LocationA)) 82 | 83 | case (VacuumWorldState(LocationA, Clean, _), Suck) => 84 | List(s, s.copy(a = Dirty)) // if current location is clean suck can sometimes deposit dirt 85 | case (VacuumWorldState(LocationB, _, Clean), Suck) => 86 | List(s, s.copy(b = Dirty)) // if current location is clean suck can sometimes deposit dirt 87 | 88 | case (VacuumWorldState(LocationA, Dirty, Dirty), Suck) => 89 | List(s.copy(a = Clean), s.copy(a = Clean, b = Clean)) // if current location is dirty sometimes also cleans up adjacent location 90 | case (VacuumWorldState(LocationB, Dirty, Dirty), Suck) => 91 | List(s.copy(b = Clean), s.copy(a = Clean, b = Clean)) // if current location is dirty sometimes also cleans up adjacent location 92 | 93 | case (VacuumWorldState(LocationA, Dirty, _), Suck) => 94 | List(s.copy(a = Clean)) 95 | case (VacuumWorldState(LocationB, _, Dirty), Suck) => 96 | List(s.copy(b = Clean)) 97 | 98 | } 99 | 100 | override def isGoalState(s: VacuumWorldState): Boolean = s match { 101 | case VacuumWorldState(_, Clean, Clean) => true 102 | case _ => false 103 | } 104 | 105 | override def stepCost(s: VacuumWorldState, a: Action, childPrime: VacuumWorldState): Double = 106 | ??? // Not used 107 | } 108 | 109 | import scala.reflect.classTag 110 | val aCTag: ClassTag[Action] = classTag[Action] 111 | val sCTag: ClassTag[VacuumWorldState] = classTag[VacuumWorldState] 112 | } 113 | 114 | /** 115 | * @author Shawn Garner 116 | */ 117 | class AndOrGraphSearchSpec extends Specification with AndOrGraphSearch[Action, VacuumWorldState] with ScalaCheck { 118 | import AndOrGraphSearchSpec._ 119 | 120 | import Action.Implicits.showAction 121 | import Status.Implicits.statusShow 122 | import VacuumWorldState.Implicits.vacuumWorldStateShow 123 | implicit def sCP: Show[ConditionalPlan] = ConditionalPlan.Implicits.showConditionalPlan[VacuumWorldState, Action] 124 | import Show.Implicits._ 125 | 126 | "AndOrGraphSearch" should { 127 | "handle State 1 [*_/][* ]" in { 128 | val initial = VacuumWorldState(LocationA, Dirty, Dirty) 129 | val prob = problem(initial) 130 | val cp = andOrGraphSearch(prob) 131 | cp match { 132 | case cp: ConditionalPlan => 133 | cp.show must_== "[Suck, if State = [ _/][* ] then [Right, Suck] else []]" 134 | case f => ko(f.toString) 135 | } 136 | } 137 | 138 | "handle State 2 [* ][*_/]" in { 139 | val initial = VacuumWorldState(LocationB, Dirty, Dirty) 140 | val prob = problem(initial) 141 | val cp = andOrGraphSearch(prob) 142 | cp match { 143 | case cp: ConditionalPlan => 144 | cp.show must_== "[Left, Suck, if State = [ _/][* ] then [Right, Suck] else []]" 145 | case f => ko(f.toString) 146 | } 147 | } 148 | 149 | "handle State 3 [*_/][ ]" in { 150 | val initial = VacuumWorldState(LocationA, Dirty, Clean) 151 | val prob = problem(initial) 152 | val cp = andOrGraphSearch(prob) 153 | cp match { 154 | case cp: ConditionalPlan => 155 | cp.show must_== "[Suck]" 156 | case f => ko(f.toString) 157 | } 158 | } 159 | 160 | "handle State 4 [* ][ _/]" in { 161 | val initial = VacuumWorldState(LocationB, Dirty, Clean) 162 | val prob = problem(initial) 163 | val cp = andOrGraphSearch(prob) 164 | cp match { 165 | case cp: ConditionalPlan => 166 | cp.show must_== "[Left, Suck]" 167 | case f => ko(f.toString) 168 | } 169 | } 170 | 171 | "handle State 5 [ _/][* ]" in { 172 | val initial = VacuumWorldState(LocationA, Clean, Dirty) 173 | val prob = problem(initial) 174 | val cp = andOrGraphSearch(prob) 175 | cp match { 176 | case cp: ConditionalPlan => 177 | cp.show must_== "[Right, Suck]" 178 | case f => ko(f.toString) 179 | } 180 | } 181 | 182 | "handle State 6 [ ][*_/]" in { 183 | val initial = VacuumWorldState(LocationB, Clean, Dirty) 184 | val prob = problem(initial) 185 | val cp = andOrGraphSearch(prob) 186 | cp match { 187 | case cp: ConditionalPlan => 188 | cp.show must_== "[Suck]" 189 | case f => ko(f.toString) 190 | } 191 | } 192 | 193 | "handle State 7 [ _/][ ]" in { 194 | val initial = VacuumWorldState(LocationA, Clean, Clean) 195 | val prob = problem(initial) 196 | val cp = andOrGraphSearch(prob) 197 | cp match { 198 | case cp: ConditionalPlan => 199 | cp.show must_== "[]" 200 | case f => ko(f.toString) 201 | } 202 | } 203 | 204 | "handle State 8 [ ][ _/]" in { 205 | val initial = VacuumWorldState(LocationB, Clean, Clean) 206 | val prob = problem(initial) 207 | val cp = andOrGraphSearch(prob) 208 | cp match { 209 | case cp: ConditionalPlan => 210 | cp.show must_== "[]" 211 | case f => ko(f.toString) 212 | } 213 | } 214 | 215 | import VacuumWorldState.Implicits.arbVacuumWorldState 216 | "find solutions for all initial states" >> prop { initial: VacuumWorldState => 217 | val prob = problem(initial) 218 | val cp = andOrGraphSearch(prob) 219 | cp match { 220 | case _: ConditionalPlan => ok 221 | case f => ko(f.toString) 222 | } 223 | } 224 | } 225 | override implicit val aCT: ClassTag[Action] = aCTag 226 | override implicit val sCT: ClassTag[VacuumWorldState] = sCTag 227 | } 228 | -------------------------------------------------------------------------------- /core/src/main/scala/aima/core/search/local/GeneticAlgorithm.scala: -------------------------------------------------------------------------------- 1 | package aima.core.search.local 2 | 3 | import aima.core.search.local.set.NonEmptySet 4 | 5 | import scala.annotation.tailrec 6 | import scala.concurrent.duration.FiniteDuration 7 | import scala.util.Random 8 | 9 | final case class Fitness(value: Double) extends AnyVal 10 | final case class Probability(value: Double) extends AnyVal // Need to make sure between 0.0 an 1.0 11 | 12 | package set { 13 | final class NonEmptySet[A] private[set] (val value: Set[A]) 14 | 15 | object NonEmptySet { 16 | def apply[A](set: Set[A]): Either[String, NonEmptySet[A]] = { 17 | if (set.isEmpty) { 18 | Left("Set can not be empty") 19 | } else { 20 | Right(new NonEmptySet[A](set)) 21 | } 22 | } 23 | } 24 | } 25 | 26 | trait Deadline { 27 | def isOverDeadline: Boolean 28 | } 29 | 30 | object Deadline { 31 | def start(timeLimit: FiniteDuration) = new Deadline { 32 | val start = System.currentTimeMillis() 33 | override def isOverDeadline: Boolean = { 34 | val current = System.currentTimeMillis() 35 | (current - start) > timeLimit.toMillis 36 | } 37 | } 38 | } 39 | 40 | object Util { 41 | def normalize(probDist: List[Double]): List[Double] = { 42 | val total = probDist.sum 43 | if (total != 0.0d) { 44 | probDist.map(d => d / total) 45 | } else { 46 | List.fill(probDist.length)(0.0d) 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * function GENETIC-ALGORITHM(population, FITNESS-FN) returns an individual 53 | * inputs: population, a set of individuals 54 | * FITNESS-FN, a function that measures the fitness of an individual 55 | * 56 | * repeat 57 | * new_population <- empty set 58 | * repeat 59 | * x <- RANDOM-SELECTION(population, FITNESS-FN) 60 | * y <- RANDOM-SELECTION(population, FITNESS-FN) 61 | * child <- REPRODUCE(x, y) 62 | * if (small random probability) then child <- MUTATE(child) 63 | * add child to new_population 64 | * until SIZE(new_population) = SIZE(population) 65 | * population <- new_population 66 | * until some individual is fit enough, or enough time has elapsed 67 | * return the best individual in population, according to FITNESS-FN 68 | * -------------------------------------------------------------------------------- 69 | * function REPRODUCE(x, y) returns an individual 70 | * inputs: x, y, parent individuals 71 | * 72 | * n <- LENGTH(x); c <- random number from 1 to n 73 | * return APPEND(SUBSTRING(x, 1, c), SUBSTRING(y, c+1, n)) 74 | * 75 | * 76 | * Figure ?? A genetic algorithm. The algorithm is the same as the one 77 | * diagrammed in Figure 4.6, with one variation: in this more popular version, 78 | * each mating of two parents produces only one offspring, not two. 79 | * 80 | * @author Shawn Garner 81 | */ 82 | trait GeneticAlgorithm[Individual] { 83 | 84 | type FitnessFunction = Individual => Fitness 85 | type FitEnough = Individual => Boolean 86 | type ReproductionFunction = (Individual, Individual, Random) => List[Individual] 87 | type MutationFunction = (Individual, Random) => Individual 88 | 89 | def geneticAlgorithm(initialPopulation: NonEmptySet[Individual], fitnessFunction: FitnessFunction)( 90 | fitEnough: FitEnough, 91 | timeLimit: FiniteDuration, 92 | reproduce: ReproductionFunction, 93 | mutationProbability: Probability, 94 | mutate: MutationFunction 95 | ): Individual = { 96 | val random = new Random() 97 | val deadline = Deadline.start(timeLimit) 98 | 99 | @tailrec def recurse(currentPopulation: Set[Individual], newPopulation: Set[Individual]): Individual = { 100 | val children: List[Individual] = (for { 101 | x <- randomSelection(currentPopulation, fitnessFunction)(random) 102 | y <- randomSelection(currentPopulation, fitnessFunction)(random) 103 | } yield reproduce(x, y, random)).toList.flatten 104 | 105 | val mutated = { 106 | children.map { c => 107 | if (isSmallRandomProbabilityOfMutation(mutationProbability, random)) { 108 | mutate(c, random) 109 | } else { 110 | c 111 | } 112 | } 113 | } 114 | 115 | val updatedNewPop = newPopulation ++ mutated 116 | 117 | if (updatedNewPop.size < currentPopulation.size) { 118 | recurse(currentPopulation, updatedNewPop) 119 | } else { 120 | val selected = selectBestIndividualIfReady(updatedNewPop, fitnessFunction)(fitEnough, deadline, random) 121 | selected match { 122 | case Some(ind) => ind 123 | case None => recurse(updatedNewPop, Set.empty[Individual]) 124 | } 125 | 126 | } 127 | } 128 | 129 | recurse(initialPopulation.value, Set.empty[Individual]) 130 | 131 | } 132 | 133 | def selectBestIndividualIfReady(population: Set[Individual], fitnessFunction: FitnessFunction)( 134 | fitEnough: FitEnough, 135 | deadline: Deadline, 136 | random: Random 137 | ): Option[Individual] = { 138 | if (deadline.isOverDeadline || population.exists(fitEnough)) { 139 | 140 | @tailrec def findBest(pop: List[Individual], best: Option[(Individual, Fitness)]): Option[Individual] = 141 | (pop, best) match { 142 | case (Nil, b) => b.map(_._1) 143 | case (current :: rest, None) => 144 | val currentValue = fitnessFunction(current) 145 | findBest(rest, Some((current, currentValue))) 146 | case (current :: rest, Some(b)) => 147 | val currentValue = fitnessFunction(current) 148 | if (currentValue.value > b._2.value) { 149 | findBest(rest, Some((current, currentValue))) 150 | } else if (currentValue.value == b._2.value) { 151 | if (random.nextBoolean()) { 152 | findBest(rest, Some((current, currentValue))) 153 | } else { 154 | findBest(rest, best) 155 | } 156 | } else { 157 | findBest(rest, best) 158 | } 159 | } 160 | 161 | findBest(population.toList, None) 162 | } else { 163 | None 164 | } 165 | 166 | } 167 | 168 | def isSmallRandomProbabilityOfMutation(mutationProbability: Probability, random: Random): Boolean = 169 | random.nextDouble <= mutationProbability.value 170 | 171 | def randomSelection(population: Set[Individual], fitnessFunction: FitnessFunction)( 172 | random: Random 173 | ): Option[Individual] = { 174 | val populationList = population.toList 175 | val populationFitness = populationList.map(fitnessFunction) 176 | val fValues = Util.normalize(populationFitness.map(_.value)) 177 | val probability = random.nextDouble() 178 | 179 | val popWithFValues = populationList zip fValues 180 | @tailrec def selectByProbability(l: List[(Individual, Double)], totalSoFar: Double): Option[Individual] = l match { 181 | case Nil => None 182 | case first :: Nil => Some(first._1) // if we are at end of list or only one element must select it 183 | case first :: rest => 184 | val newTotal = totalSoFar + first._2 185 | if (probability <= newTotal) { // seems weird 186 | Some(first._1) 187 | } else { 188 | selectByProbability(rest, newTotal) 189 | } 190 | 191 | } 192 | 193 | selectByProbability(popWithFValues, 0.0d) 194 | } 195 | 196 | } 197 | 198 | object GeneticAlgorithm { 199 | object StringIndividual { 200 | 201 | // function REPRODUCE(x, y) returns an individual 202 | def reproduce1(x: String, y: String, random: Random): List[String] = { 203 | // n <- LENGTH(x); 204 | val n = x.length 205 | // c <- random number from 1 to n 206 | val c = random.nextInt(n) 207 | // return APPEND(SUBSTRING(x, 1, c), SUBSTRING(y, c+1, n)) 208 | List( 209 | x.substring(0, c) + y.substring(c, n) 210 | ) 211 | } 212 | 213 | // function REPRODUCE(x, y) returns a pair of individual 214 | def reproduce2(x: String, y: String, random: Random): List[String] = { 215 | // n <- LENGTH(x); 216 | val n = x.length 217 | // c <- random number from 1 to n 218 | val c = random.nextInt(n) 219 | // return APPEND(SUBSTRING(x, 1, c), SUBSTRING(y, c+1, n)) and APPEND(SUBSTRING(y, 1, c), SUBSTRING(x, c+1, n)) 220 | List( 221 | x.substring(0, c) + y.substring(c, n), 222 | y.substring(0, c) + x.substring(c, n) 223 | ) 224 | } 225 | 226 | val alphabet: List[Char] = (('a' to 'z') ++ ('A' to 'Z')).toList 227 | 228 | def mutate(child: String, random: Random): String = { 229 | val replacement: Char = alphabet(random.nextInt(alphabet.size)) 230 | child.updated(random.nextInt(child.length), replacement) 231 | } 232 | } 233 | } 234 | --------------------------------------------------------------------------------