This page explores the use of Tcl for searching a state space from a problem. I define a GeneralSearch algorithm in Tcl, and then go on to define BreadthFirst and DepthFirst searches. Later, I might add heuristic search methods such as A*, iterative deepening etc. Finally, I demonstrate a simple example. - NEM
Note - see Searching A Star in Space for much better code by RS - NEM
################################################################################ # Some general search strategies in Tcl. # ################################################################################ catch {console show} # Let's first implement our node: proc createNode {name operator state pathCost} { puts "Creating node at $state as a result of $operator -> $pathCost" set name2 [split $name |]; # use | as a path seperator. set parent [string range $name 0 [expr {[string last "|" $name] - 1}]] if {$operator == "starting"} { set parent "Dummy" } else { set parent [${parent}::getState] } namespace eval $name [subst { variable parent $parent variable this $name variable operator $operator variable pathCost $pathCost variable depth [llength $name2] variable state $state }] proc ${name}::getState {} { variable state return $state } proc ${name}::getPathCost {} { variable pathCost return $pathCost } proc ${name}::getParent {} { variable parent return $parent } return $name } # We will implement the queue as a list. proc MakeQueue {initState} { set queue [list [createNode "rootNode" "starting" $initState 0]] return $queue } proc RemoveFront {n} { upvar 1 $n nodes set front [lindex $nodes 0] set nodes [lreplace $nodes 0 0] return $front } # The problem will be an array of the form: # problem(initState) - initial state # problem(goalTest) - the ideal state (i.e. the goal) # problem(operators) - the operators of the problem proc Expand {node operators} { # Apply all operators to node and return a list # of new nodes. set returnNodes {} set uniqueName 1 foreach operator $operators { set name $node append name "|node$uniqueName" set res [eval $operator $node] if {![string equal $res "Error"]} { foreach item $res { lappend returnNodes [eval createNode $name $operator $item] incr uniqueName set name $node append name "|node$uniqueName" } } } return $returnNodes } # All other functions are defined by the particular search. proc GeneralSearch {p QueuingFunc} { upvar 1 $p problem set nodes [MakeQueue $problem(initState)] while {[llength $nodes] > 0} { puts "Continue?" if {[string equal [gets stdin] "n"]} { exit } set currentNode [RemoveFront nodes] puts "$nodes" set result [eval $problem(goalTest) [${currentNode}::getState]] if {$result == 1} { return $currentNode } else { puts "->Expanding [${currentNode}::getState]" set res [Expand $currentNode $problem(operators)] if {[string length $res] == 0} { puts "No nodes expanded." continue } $QueuingFunc $res nodes } } return -code error "Goal not found" } ################################################################################ # To define a new search, you can use the building blocks above. # For instance, let's define breadth-first and depth-first: # ################################################################################ proc BreadthFirstSearch {p} { # First, define our queuing function. upvar 1 $p problem proc EnqueueAtEnd {newNodes q} { upvar 1 $q queue set queue [concat $queue $newNodes] return } return [GeneralSearch problem EnqueueAtEnd] } proc DepthFirstSearch {p} { upvar 1 $p problem proc EnqueueAtFront {newNodes q} { upvar 1 $q queue set queue [concat $newNodes $queue] return } return [GeneralSearch problem EnqueueAtFront] } ################################################################################ # To define a search problem you simply do the following: # * Define your problem with an initial state of some sort (a string in this case) # * Define the goal test as a procedure in the following form: # proc goalTest {state} { # # Work out if goal here # return 1 for success # return 0 for failure # } # * A list of operators in the form: # proc operator1 {node} { # return [list newState pathCost] # } # * The operators are resposible for deciding new states and calculating the pathCost # to this new state. # # # A simple example: # Search a maze for the quickest route. # This was a coursework assignment I had a while back. ################################################################################ # The problem: # Find your way from A to U in the following maze. # | ============\ # | A B C D E | # | |==|== | | # | F |G |H I |J | # | | |== | | # | K L M N |O | # | |==|==| | | # | P |Q |R |S |T | # |===| | |==| | # | U V W X Y | # | |===========/ # We apply the restrictions that no move can go back to the start state, and no # move can go back to the state it was _just_ at (i.e. the direct parent state). ################################################################################ # First, define our stateSpace. For this search, the stateSpace is known # but the procs would still work if your operators can get information from other # sources. It is easier though, if we just give them the info in the first place :-) # The state-space here consists of nodes followed by a list stating which # directions are possible and what the path cost is and what the destination is. array set map { A {e 1 B s 1 F} B {w 1 A e 1 C} C {w 1 B e 1 D} D {w 1 C e 1 E s 1 I} E {w 1 D s 1 J} F {n 1 A s 1 K} G {s 1 L} H {e 1 I} I {n 1 D w 1 H s 1 N} J {n 1 E s 1 O} K {n 1 F e 1 L s 1 P} L {n 1 G w 1 K e 1 M} M {w 1 L e 1 N} N {n 1 I w 1 M s 1 S} O {n 1 J s 1 T} P {n 1 K} Q {s 1 V} R {s 1 W} S {n 1 N} T {n 1 O s 1 Y} U {e 1 V} V {n 1 Q w 1 U e 1 W} W {n 1 R w 1 V e 1 X} X {w 1 W e 1 Y} Y {n 1 T w 1 X} } set stateSpace(initState) "A" proc GoalTest {state} { if {[string equal $state "U"]} { return 1 } else { return 0 } } set stateSpace(goalTest) GoalTest proc moveNorth {node} { # Check if node can move north, if it can # return the list, else return "" set moves $::map([${node}::getState]) set result "Error" set parent [${node}::getParent] foreach {dir cost newState} $moves { if {[string equal $dir "n"] && ![string equal $newState $parent]} { # We have a winner! set pathCost [expr {[${node}::getPathCost] + $cost}] set result [list $newState $pathCost] } } return $result } proc moveEast {node} { # Check if node can move north, if it can # return the list, else return "" set moves $::map([${node}::getState]) set result "Error" set parent [${node}::getParent] foreach {dir cost newState} $moves { if {[string equal $dir "e"] && ![string equal $newState $parent]} { # We have a winner! set pathCost [expr {[${node}::getPathCost] + $cost}] set result [list $newState $pathCost] } } return $result } proc moveWest {node} { # Check if node can move north, if it can # return the list, else return "" set moves $::map([${node}::getState]) set result "Error" set parent [${node}::getParent] foreach {dir cost newState} $moves { if {[string equal $dir "w"] && ![string equal $newState $parent]} { # We have a winner! set pathCost [expr {[${node}::getPathCost] + $cost}] set result [list $newState $pathCost] } } return $result } proc moveSouth {node} { # Check if node can move north, if it can # return the list, else return "" set moves $::map([${node}::getState]) set result "Error" set parent [${node}::getParent] foreach {dir cost newState} $moves { if {[string equal $dir "s"] && ![string equal $newState $parent]} { # We have a winner! set pathCost [expr {[${node}::getPathCost] + $cost}] set result [list $newState $pathCost] } } return $result } # These operators are all basically the same, but they could be wildly different in real life. set stateSpace(operators) [list moveNorth moveEast moveWest moveSouth] # you could change the order to see if it makes a difference. On uninformed strategies it will # change the order of search, but the outcome should still be the same, if the search is optimal & # complete. # Now we search: puts "Depth First Search:\n===================" catch {DepthFirstSearch stateSpace} result set result [split $result "|"] puts "$result" set route [lindex $result 0] puts -nonewline "[${route}::getState] -> " set result [lrange $result 1 end] foreach item $result { append route "|$item" puts -nonewline "[${route}::getState] -> " } puts "End!" puts "Total cost: [${route}::getPathCost]" puts "\nBreadth First Search:\n=====================" catch {DepthFirstSearch stateSpace} result set result [split $result "|"] puts "$result" set route [lindex $result 0] puts -nonewline "[${route}::getState] -> " set result [lrange $result 1 end] foreach item $result { append route "|$item" puts -nonewline "[${route}::getState] -> " } puts "End!" puts "Total cost: [${route}::getPathCost]"
And that should do it! Just copy and paste into tclsh or wish console and watch it whirl!