nuin » blocks world agent

Blocks-world agent

This example shows a single agent planning and carrying out a series of simulated actions in a simple blocks world. The objective of this example is to illustrate some features of Nuinscript programs, rather than to provide a complete solution to such linear-planning problems. Indeed, it is likely that Nuinscript by itself is not suitable for even moderately complex planning tasks, and these should be handled by a dedicated planning service.

Some of the features Nuinscript illustrated by the blocks world agent are outlined and briefly discussed below.

In-line axioms for knowledge bases

The blocks-world script includes some background knowledge for the agent in an axioms declaration. Typically, agent's knowledge bases are specified externally, for example as OWL ontology files or persistent data models. However, it can be convenient to associate some background knowledge directly with the script. Note that, as no KS is named by the axioms declaration, the axiom sentences are asserted into the default KS, which is the agent's own beliefs.

// first declare some starting facts
axioms contains
  bw:on( bw:a, bw:table ).
  bw:on( bw:b, bw:a ).
  bw:on( bw:c, bw:b ).
end.
       

Startup plan

The main plan is triggered by the startup of the agent, and immediately invokes the moveLoop plan. Invoke is an intention-to perform a named plan, but which also waits for the sub-plan to terminate before continuing. In this example, the goal of the agent's activity is specified implicitly by the argument to the loop plan, which in the fragment shown below is to move block a on top of block b.

plan bw:main
    trigger
        on event( event:startup )
    do
    	println( "Starting up ..." ) ;
    	invoke bw:moveLoop( bw:a, bw:b ) ;
    	println( "All done." )
end.
       

Move loop

The move loop simply tries a fixed set of alternatives in order. This is where the blocks-world agent most departs from a true linear-planner: the set of alternative actions and the order to try them in is known and fixed in advance. More realistically, it should depend on the current state of the world or world-model what actions could be taken. The consequence is that each of the action steps has to check carefully that the step is possible.

Action steps

The remaining plans embody the various move alternatives: move ?x to ?y, move ?x onto the table top, and clear ?x by removing blocks that cover it. These are fairly straightforward in their operation. Note that the action sequence operator (i.e. ;) binds more tightly than the action alternative operator (i.e. |), so the expression a | b ; c parses as a | (b ; c).

Notice also that the combination of commit and fail is a common way to ensure that a plan passes back a failure termination to the parent plan. This will be familiar to Prolog programmers as the !, fail combination.

The whole script

Note that this is about twice as long as it needs to be as copious print statements have been added to show what's happening:

use bw for <urn:x-nuin-example:blocks:>.

// first declare some starting facts
axioms contains
  bw:on( bw:a, bw:table ).
  bw:on( bw:b, bw:a ).
  bw:on( bw:c, bw:b ).
end.

/*
 * The main entry point - triggered by the startup event
 */
plan bw:main
    trigger
      on event( event:startup )
    do
      println( "Starting up ..." ) ;
      invoke bw:moveLoop( bw:a, bw:b ) ;
      println( "All done." )
end.

/*
 * The search loop - searches for a solution, but is not a
 * proper search algorithm (like A*)
 */
plan bw:moveLoop( ?x, ?y )
    do
        holds bw:on( ?x, ?y )
      |
        println( "trying alternatives to move" ) ;
        (  invoke bw:moveOnto( ?x, ?y ) | 
           invoke bw:moveToTable( ?x ) | 
           invoke bw:moveToTable( ?y ) |
           invoke bw:clear( ?y )
         ) ;
        println( "successful move completed, recursing" );
        invoke bw:moveLoop( ?x, ?y )
end.

/*
 * Plan to move block ?x to the table, if it is not on the
 * table and not covered by another block
 */
plan bw:moveToTable( ?z )
    do
      println( "[moveToTable] Entering move table with ?z = ", ?z ) ;
      (
         println( "[moveToTable] checking ", bw:on( ?z, bw:table ) ) ;
         holds bw:on( ?z, bw:table ) ;
         println( "[moveToTable] ", ?z, " is already on the table " ) ;
         commit ;
         fail
      |
         println( "[moveToTable] checking ", bw:on( ?a, ?z ) ) ;
         holds bw:on( ?a, ?z );
         println( "[moveToTable] Cannot move ", ?z, 
                  " onto table - it is covered by ", ?a ) ;
         commit ;
         fail
      |
         println( "[moveToTable] attempting move to table for ", ?z );
         holds bw:on( ?z, ?b );
         retract bw:on( ?z, ?b ) ;
         assert bw:on( ?z, bw:table ) ;
         println( "[moveToTable] Moved ", ?z, " from ", ?b, " to table" )
      )
end.

/*
 * Plan to move ?x onto ?y, if ?x is not on ?y, and both are clear.
 */
plan bw:moveOnto( ?x, ?y )
    do
      println( "[moveOnto] Entering move onto with ?x = ", ?x, 
               ", ?y = ", ?y ) ;
      (
         println( "[moveOnto] checking ", bw:on( ?x, ?y ) ) ;
         holds bw:on( ?x, ?y ) ;
         println( "[moveOnto] ", ?x, " is already on ", ?y ) ;
         commit ;
         fail
      |
         println( "[moveOnto] checking ", bw:on( ?z, ?y ) ) ;
         holds bw:on( ?z, ?y ) ;
         println( "[moveOnto] Cannot move anything onto ", ?y, 
                  " it is covered by ", ?z ) ;
         commit ;
         fail
      |
         println( "[moveOnto] checking ", bw:on( ?z, ?x ) ) ;
         holds bw:on( ?z, ?x ) ;
         println( "[moveOnto] Cannot move ", ?x, 
                  " because it is covered by ", ?z ) ;
         commit ;
         fail
      |
         println( "[moveOnto] attempting move to ", ?x, " to ", ?y );
         retract bw:on( ?x, ?b ) ;
         assert bw:on( ?x, ?y ) ;
         println( "[moveOnto] Moved ", ?x, " to ", ?y )
      )
end.

/*
 * Ensure that block ?x is clear (has no block on top of it) by moving
 * blocks onto the table as necessary
 */
plan bw:clear( ?x )
    do
      println( "[clear] Entering clear ", ?x ) ;
      (
        holds( bw:on( ?y, ?x ) ) ;
        println( "[clear] - need to shift ", ?y, " from ", ?x );
        (
           holds bw:on( ?z, ?y );
           println( "[clear] ", ?z, " is on ", ?y, 
                    " - need to clear ", ?y, " first" );
           commit;
           invoke bw:clear( ?y );
           invoke bw:clear( ?x )
        |
           println( "[clear] attempting move to table for ", ?y );
           holds bw:on( ?y, ?x );
           retract bw:on( ?y, ?x ) ;
           assert bw:on( ?y, bw:table ) ;
           println( "[clear] Moved ", ?y, " to table" )
        )
      |
        println( "[clear] already clear: ", ?x )
      )
end.