├── .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 | # aima-scala [](https://travis-ci.org/aimacode/aima-scala) [](https://gitter.im/aima-scala/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 2 | Scala implementation of algorithms from [Russell](http://www.cs.berkeley.edu/~russell/) And [Norvig's](http://www.norvig.com/) [Artificial Intelligence - A Modern Approach 3rd Edition](http://aima.cs.berkeley.edu/). You can use this in conjunction with a course on AI, or for study on your own. 3 | 4 | ## Formating the code 5 | ```bash 6 | sbt scalafmt 7 | ``` 8 | 9 | ## Test Coverage 10 | ``` 11 | sbt ";clean;test" 12 | sbt coverageReport 13 | ``` 14 | 15 | reports are generated in the target/scoverage-report folder of the sub-projects 16 | -------------------------------------------------------------------------------- /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 ??.
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") --------------------------------------------------------------------------------