├── .gitignore ├── .scalafmt ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── core └── src │ ├── main │ └── scala │ │ └── aima │ │ └── core │ │ ├── agent │ │ ├── Actuator.scala │ │ ├── Agent.scala │ │ ├── Environment.scala │ │ ├── ModelBasedReflexAgentProgram.scala │ │ ├── Sensor.scala │ │ ├── SimpleProblemSolvingAgentProgram.scala │ │ ├── SimpleReflexAgentProgram.scala │ │ ├── TableDrivenAgentProgram.scala │ │ └── basic │ │ │ ├── LRTAStarAgent.scala │ │ │ └── OnlineDFSAgent.scala │ │ ├── environment │ │ ├── map2d │ │ │ ├── Go.scala │ │ │ ├── InState.scala │ │ │ ├── IntPercept.scala │ │ │ ├── LabeledGraph.scala │ │ │ ├── Map2D.scala │ │ │ └── Map2DFunctionFactory.scala │ │ └── vacuum │ │ │ ├── ModelBasedReflexVacuumAgentProgram.scala │ │ │ ├── SimpleReflexVacuumAgentProgram.scala │ │ │ ├── TableDrivenVacuumAgentProgram.scala │ │ │ ├── VacuumEnvironment.scala │ │ │ ├── actions.scala │ │ │ ├── actuators.scala │ │ │ ├── percepts.scala │ │ │ └── sensors.scala │ │ ├── fp │ │ ├── Eqv.scala │ │ └── Show.scala │ │ ├── random │ │ └── Randomness.scala │ │ └── search │ │ ├── FrontierSearch.scala │ │ ├── ProblemSearch.scala │ │ ├── adversarial │ │ ├── Game.scala │ │ └── MinimaxSearch.scala │ │ ├── api │ │ └── OnlineSearchProblem.scala │ │ ├── contingency │ │ └── AndOrGraphSearch.scala │ │ ├── informed │ │ └── RecursiveBestFirstSearch.scala │ │ ├── local │ │ ├── GeneticAlgorithm.scala │ │ ├── HillClimbing.scala │ │ └── SimulatedAnnealingSearch.scala │ │ ├── problems │ │ ├── TwoPlayerGame.scala │ │ └── romania.scala │ │ └── uninformed │ │ ├── BreadthFirstSearch.scala │ │ ├── DepthLimitedTreeSearch.scala │ │ ├── GraphSearch.scala │ │ ├── IterativeDeepeningSearch.scala │ │ ├── TreeSearch.scala │ │ ├── UniformCostSearch.scala │ │ └── frontier.scala │ └── test │ └── scala │ └── aima │ └── core │ ├── agent │ └── basic │ │ ├── LRTAStarAgentSpec.scala │ │ └── OnlineDFSAgentSpec.scala │ ├── environment │ ├── map2d │ │ └── LabeledGraphSpec.scala │ └── vacuum │ │ ├── ModelBasedReflexVacuumAgentSpec.scala │ │ ├── ReflexVacuumAgentProgramSpec.scala │ │ ├── SimpleReflexVacuumAgentSpec.scala │ │ ├── TableDrivenVacuumAgentSpec.scala │ │ └── VacuumMapSpec.scala │ └── search │ ├── adversarial │ └── MinimaxSearchSpec.scala │ ├── contingency │ └── AndOrGraphSearchSpec.scala │ ├── local │ ├── GeneticAlgorithmSpec.scala │ ├── HillClimbingSpec.scala │ └── SimulatedAnnealingSearchSpec.scala │ ├── problems │ └── RomaniaRoadMapSpec.scala │ └── uninformed │ ├── RomaniaBreadthFirstSearchSpec.scala │ ├── RomaniaDepthLimitedTreeSearchSpec.scala │ ├── RomaniaIterativeDeepeningSearchSpec.scala │ └── RomaniaUniformCostSearchSpec.scala └── project ├── build.properties ├── dependencies.scala └── plugins.sbt /.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/ -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![](https://github.com/aimacode/aima-java/blob/gh-pages/aima3e/images/aima3e.jpg)aima-scala [![Build Status](https://travis-ci.org/aimacode/aima-scala.svg?branch=master)](https://travis-ci.org/aimacode/aima-scala) [![Gitter](https://badges.gitter.im/aima-scala/community.svg)](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 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import org.scalafmt.sbt.ScalafmtPlugin.autoImport._ 2 | import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} 3 | 4 | lazy val commonSettings = Seq( 5 | organization := "com.github.aimacode.aima-scala", 6 | version := "0.1.0-SNAPSHOT", 7 | scalaVersion := "2.13.1", 8 | scalafmtConfig := file(".scalafmt"), 9 | scalafmtOnCompile := true, 10 | coverageEnabled := false, 11 | coverageMinimum := 70, 12 | coverageFailOnMinimum := false, 13 | autoCompilerPlugins := true 14 | ) 15 | 16 | lazy val aima = (project in file(".")) 17 | .settings(commonSettings: _*) 18 | .aggregate(core.jvm, core.js) 19 | 20 | lazy val core = crossProject(JSPlatform, JVMPlatform) 21 | .crossType(CrossType.Pure) 22 | .in(file("core")) 23 | .settings(commonSettings: _*) 24 | .settings(name := "core") 25 | .settings(dependencies.commonDependencies) 26 | -------------------------------------------------------------------------------- /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/agent/Agent.scala: -------------------------------------------------------------------------------- 1 | package aima.core.agent 2 | 3 | /** 4 | * @author Shawn Garner 5 | * @author Damien Favre 6 | */ 7 | trait Agent[ENVIRONMENT, PERCEPT, ACTION] { 8 | def actuators: List[Actuator[ENVIRONMENT, ACTION]] 9 | def sensors: List[Sensor[ENVIRONMENT, PERCEPT]] 10 | def agentProgram: AgentProgram[PERCEPT, ACTION] 11 | 12 | def run(e: ENVIRONMENT): (ENVIRONMENT, AgentRunSummary[PERCEPT, ACTION]) = { 13 | val percepts: List[PERCEPT] = sensors.flatMap(_.perceive(e)) 14 | val actions: List[ACTION] = percepts.map(agentProgram.agentFunction) 15 | 16 | val newEnvironment = actions.foldLeft(e) { (env, action) => 17 | actuators.foldLeft(env) { (env2, actuator) => 18 | actuator.act(action, env2) 19 | } 20 | } 21 | (newEnvironment, AgentRunSummary(percepts, actions)) 22 | } 23 | } 24 | 25 | case class AgentRunSummary[PERCEPT, ACTION]( 26 | percepts: List[PERCEPT], 27 | actions: List[ACTION] 28 | ) 29 | 30 | trait AgentProgram[PERCEPT, ACTION] { 31 | type AgentFunction = PERCEPT => ACTION 32 | 33 | def agentFunction: AgentFunction 34 | } 35 | 36 | trait StatelessAgent[PERCEPT, ACTION, AGENT_STATE] { 37 | type AgentFunction = (PERCEPT, AGENT_STATE) => (ACTION, AGENT_STATE) 38 | 39 | def agentFunction: AgentFunction 40 | } 41 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/environment/vacuum/TableDrivenVacuumAgentProgram.scala: -------------------------------------------------------------------------------- 1 | package aima.core.environment.vacuum 2 | 3 | import aima.core.agent._ 4 | 5 | /** 6 | * @author Shawn Garner 7 | */ 8 | class TableDrivenVacuumAgentProgram extends TableDrivenAgentProgram[VacuumPercept, VacuumAction] { 9 | val lookupTable: LookupTable = { 10 | case List(_, DirtyPercept) => Suck 11 | case List(LocationAPercept, CleanPercept) => RightMoveAction 12 | case List(LocationBPercept, CleanPercept) => LeftMoveAction 13 | case List(_, _, _, DirtyPercept) => Suck 14 | case List(_, _, LocationAPercept, CleanPercept) => RightMoveAction 15 | case List(_, _, LocationBPercept, CleanPercept) => LeftMoveAction 16 | case List(_, _, _, _, _, DirtyPercept) => Suck 17 | case List(_, _, _, _, LocationAPercept, CleanPercept) => RightMoveAction 18 | case List(_, _, _, _, LocationBPercept, CleanPercept) => LeftMoveAction 19 | case List(_, _, _, _, _, _, _, DirtyPercept) => Suck 20 | case List(_, _, _, _, _, _, LocationAPercept, CleanPercept) => 21 | RightMoveAction 22 | case List(_, _, _, _, _, _, LocationBPercept, CleanPercept) => 23 | LeftMoveAction 24 | case _ => NoAction 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/main/scala/aima/core/search/adversarial/Game.scala: -------------------------------------------------------------------------------- 1 | package aima.core.search.adversarial 2 | 3 | final case class UtilityValue(value: Double) extends AnyVal 4 | 5 | object UtilityValue { 6 | implicit def utilityValueOrdering(implicit dOrdering: Ordering[Double]): Ordering[UtilityValue] = 7 | new Ordering[UtilityValue] { 8 | override def compare(x: UtilityValue, y: UtilityValue): Int = dOrdering.compare(x.value, y.value) 9 | } 10 | implicit class UtilityValueOps(value: UtilityValue) { 11 | def <(other: UtilityValue)(implicit o: Ordering[UtilityValue]): Boolean = { 12 | o.compare(value, other) < 0 13 | } 14 | 15 | def >(other: UtilityValue)(implicit o: Ordering[UtilityValue]): Boolean = { 16 | o.compare(value, other) > 0 17 | } 18 | 19 | def ==(other: UtilityValue)(implicit o: Ordering[UtilityValue]): Boolean = { 20 | o.compare(value, other) == 0 21 | } 22 | } 23 | } 24 | 25 | /** 26 | * @author Aditya Lahiri 27 | * @author Shawn Garner 28 | */ 29 | trait Game[PLAYER, STATE, ACTION] { 30 | def initialState: STATE 31 | def getPlayer(state: STATE): PLAYER 32 | def getActions(state: STATE): List[ACTION] 33 | def result(state: STATE, action: ACTION): STATE 34 | def isTerminalState(state: STATE): Boolean 35 | def getUtility(state: STATE): UtilityValue 36 | } 37 | -------------------------------------------------------------------------------- /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/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 | * 16 | * 17 | * @param 18 | * the type of the actions that can be performed. 19 | * @param 20 | * the type of the states that the agent encounters. 21 | * @author Shawn Garner 22 | */ 23 | trait OnlineSearchProblem[ACTION, STATE] { 24 | def actions(s: STATE): List[ACTION] 25 | def isGoalState(s: STATE): Boolean 26 | def stepCost(s: STATE, a: ACTION, sPrime: STATE): Double 27 | } 28 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /core/src/main/scala/aima/core/search/local/HillClimbing.scala: -------------------------------------------------------------------------------- 1 | package aima.core.search.local 2 | 3 | import aima.core.search.Problem 4 | import Ordering.Double.TotalOrdering 5 | 6 | import scala.annotation.tailrec 7 | 8 | /** 9 | * 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/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/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/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/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/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/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/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/main/scala/aima/core/search/uninformed/TreeSearch.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 TreeSearch[State, Action] 11 | extends ProblemSearch[State, Action, StateNode[State, Action]] 12 | with FrontierSearch[State, Action, StateNode[State, Action]] { 13 | type Node = StateNode[State, Action] 14 | 15 | def search(problem: Problem[State, Action], noAction: Action): List[Action] = { 16 | val initialFrontier = newFrontier(problem.initialState, noAction) 17 | 18 | @tailrec def searchHelper(frontier: Frontier[State, Action, Node]): List[Action] = { 19 | frontier.removeLeaf match { 20 | case None => List.empty[Action] 21 | case Some((leaf, _)) if problem.isGoalState(leaf.state) => solution(leaf) 22 | case Some((leaf, updatedFrontier)) => 23 | val childNodes = for { 24 | action <- problem.actions(leaf.state) 25 | } yield newChildNode(problem, leaf, action) 26 | 27 | val frontierWithChildNodes = updatedFrontier.addAll(childNodes) 28 | 29 | searchHelper(frontierWithChildNodes) 30 | } 31 | } 32 | 33 | searchHelper(initialFrontier) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/environment/vacuum/ReflexVacuumAgentProgramSpec.scala: -------------------------------------------------------------------------------- 1 | package aima.core.environment.vacuum 2 | 3 | import aima.core.agent.{Actuator, Agent, AgentProgram, Environment, Sensor} 4 | import org.scalacheck.Arbitrary 5 | import org.specs2.ScalaCheck 6 | import org.specs2.mutable.Specification 7 | 8 | import scala.annotation.tailrec 9 | 10 | /** 11 | * @author Shawn Garner 12 | */ 13 | class ReflexVacuumAgentProgramSpec extends Specification with ScalaCheck { 14 | 15 | implicit val arbVacuumEnvironment = Arbitrary(VacuumEnvironment()) 16 | 17 | "should eventually clean environment" in prop { env: VacuumEnvironment => 18 | val agent = new Agent[VacuumEnvironment, VacuumPercept, VacuumAction] { 19 | val agentProgram = new SimpleReflexVacuumAgentProgram 20 | val actuators = List[Actuator[VacuumEnvironment, VacuumAction]](new SuckerActuator(this), new MoveActuator(this)) 21 | lazy val sensors = List[Sensor[VacuumEnvironment, VacuumPercept]]( 22 | new DirtSensor(this), 23 | new AgentLocationSensor(this) 24 | ) 25 | } 26 | 27 | @tailrec def eventuallyClean(currentEnv: VacuumEnvironment): Boolean = { 28 | currentEnv match { 29 | case ve: VacuumEnvironment if ve.isClean() => true 30 | case _ => eventuallyClean(agent.run(currentEnv)._1) 31 | } 32 | } 33 | 34 | eventuallyClean(env.addAgent(agent)) must beTrue 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /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/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/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/test/scala/aima/core/search/adversarial/MinimaxSearchSpec.scala: -------------------------------------------------------------------------------- 1 | package aima.core.search.adversarial 2 | 3 | import aima.core.search.problems.TwoPlayerGame 4 | import org.specs2.mutable.Specification 5 | 6 | class MinimaxSearchSpec extends Specification { 7 | import TwoPlayerGame.{Action, State, Player, impl} 8 | import impl._ 9 | "Utility value of E" should { 10 | "be 3" in { 11 | getUtility(State("E")) must beEqualTo(UtilityValue(3)) 12 | } 13 | } 14 | 15 | "Utility value of I" should { 16 | "be 4" in { 17 | getUtility(State("I")) must beEqualTo(UtilityValue(4)) 18 | } 19 | } 20 | "Utility value of K" should { 21 | "be 14" in { 22 | getUtility(State("K")) must beEqualTo(UtilityValue(14)) 23 | } 24 | } 25 | 26 | "Initial State" should { 27 | "be A" in { 28 | initialState must beEqualTo(State("A")) 29 | } 30 | } 31 | 32 | "First player in the game" should { 33 | "be MAX" in { 34 | getPlayer(initialState) must beEqualTo(Player("MAX")) 35 | } 36 | } 37 | 38 | "State to move to from intial state" should { 39 | "be B" in { 40 | MinimaxDecision.minMaxDecision[Player, State, Action]( 41 | impl, 42 | Action("noAction") 43 | )(initialState) must beEqualTo(Action("B")) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /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/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/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/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/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/test/scala/aima/core/search/uninformed/RomaniaBreadthFirstSearchSpec.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 RomaniaBreadthFirstSearchSpec 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(Fagaras), 23 | GoTo(Bucharest) 24 | ) 25 | } 26 | 27 | trait context extends Scope with BreadthFirstSearch[RomaniaState, RomaniaAction] { 28 | 29 | override implicit val sCT: ClassTag[RomaniaState] = sCTag 30 | override implicit val aCT: ClassTag[RomaniaAction] = aCTag 31 | override implicit val nCT: ClassTag[StateNode[RomaniaState, RomaniaAction]] = snCTag 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /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/test/scala/aima/core/search/uninformed/RomaniaIterativeDeepeningSearchSpec.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 RomaniaIterativeDeepeningSearchSpec 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 beSuccessfulTry.like { 17 | case Solution(Nil) => ok 18 | } 19 | } 20 | 21 | "going from Arad to Bucharest must return a list of actions" in new context { 22 | search(new RomaniaRoadProblem(In(Arad), In(Bucharest)), NoAction) must beSuccessfulTry.like { 23 | case Solution(GoTo(Sibiu) :: GoTo(Fagaras) :: GoTo(Bucharest) :: Nil) => ok 24 | } 25 | } 26 | 27 | trait context extends Scope with IterativeDeepeningSearch[RomaniaState, RomaniaAction] { 28 | 29 | val depthLimitedTreeSearch = new DepthLimitedTreeSearch[RomaniaState, RomaniaAction] { 30 | override implicit val nCT: ClassTag[StateNode[RomaniaState, RomaniaAction]] = snCTag 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.3 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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") --------------------------------------------------------------------------------