The LightJason system architecture does not require any environment, but you can easily write your own.
Based on the asynchronous and parallel execution model in LightJason you have to create thread-safe data structures for your environment to avoid any race condition. Keep in mind that all calls of the environment are done in parallel and asynchronously, so your environment must handle these accesses correctly.
The tutorial consists of three steps:
For this example we use a small structure within which the agents should change their positions. The agents can move from one cell to the left or right, but the agents can only move if the cell is free. The number of cells is $1.5 \cdot \text{number of agents}$, so there are guaranteed free cells. The agent position will be set at random on initialisation.
In the environment, we need two structures for storing, both structures must be thread-safe:
For execution we need two methods:
With a trigger on line 66-73 we notify all other agents if an agent changes the position in line 79-80. If the agent cannot move because the cell is not empty, we throw an exception on line 83 and this will fail the agent action.
package myagentproject;
import org.lightjason.agentspeak.language.CLiteral;
import org.lightjason.agentspeak.language.CRawTerm;
import org.lightjason.agentspeak.language.instantiable.plan.trigger.CTrigger;
import org.lightjason.agentspeak.language.instantiable.plan.trigger.ITrigger;
import javax.annotation.Nonnull;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReferenceArray;
final class CEnvironment
{
private final AtomicReferenceArray<MyAgent> m_position;
private final Map<MyAgent, Integer> m_agentposition = new ConcurrentHashMap<>();
private final int m_size;
CEnvironment( final int p_size )
{
m_size = p_size;
m_position = new AtomicReferenceArray<>( new MyAgent[(int) ( m_size * 1.5 )] );
}
final int length()
{
return m_size;
}
final int size()
{
return m_position.length();
}
final boolean initialset( @Nonnull final MyAgent p_agent, final int p_position )
{
if ( m_agentposition.size() >= m_size )
return true;
if ( ( p_position < 0 ) || ( p_position >= m_position.length() ) )
return false;
if ( m_position.compareAndSet( p_position, null, p_agent ) )
{
m_agentposition.put( p_agent, p_position );
return true;
}
return false;
}
final void move( @Nonnull final MyAgent p_agent, @Nonnull final Number p_value )
{
if ( ( p_value.intValue() < 0 ) || ( p_value.intValue() >= m_position.length() ) )
throw new RuntimeException( "position index is incorrect" );
if ( m_position.compareAndSet( p_value.intValue(), null, p_agent ) )
{
final int l_oldposition = m_agentposition.get( p_agent );
m_position.set( l_oldposition, null );
m_agentposition.put( p_agent, p_value.intValue() );
final ITrigger l_trigger = CTrigger.from(
ITrigger.EType.ADDGOAL,
CLiteral.from(
"other/agent-position/changed",
CLiteral.from( "from", CRawTerm.from( l_oldposition ) ),
CLiteral.from( "to", CRawTerm.from( p_value.intValue() ) )
)
);
m_agentposition
.keySet()
.parallelStream()
.filter( i -> !i.equals( p_agent ) )
.forEach( i -> i.trigger( l_trigger ) );
}
else
throw new RuntimeException( "position is not free" );
}
final int position( @Nonnull final MyAgent p_agent )
{
return m_agentposition.get( p_agent );
}
}
The agent class must define the actions which passed the call to the environment. Based on this definition the action can be called inside the agent script.
In this example we implement the action as object-action (internal action) on line 23-28. The method name can be arbitrarily chosen, so the annotation defines the action name. Arguments can be passed with the native Java type inside the method. The method passes the data to the method of the environment object in line 27. The environment object is set by the constructor and stored inside the agent object line 15, 17, 20. All agents references the same environment, because in Java all inheritance of objects are passed as references.
package myagentproject;
import org.lightjason.agentspeak.action.binding.IAgentAction;
import org.lightjason.agentspeak.action.binding.IAgentActionFilter;
import org.lightjason.agentspeak.action.binding.IAgentActionName;
import org.lightjason.agentspeak.agent.IBaseAgent;
import org.lightjason.agentspeak.configuration.IAgentConfiguration;
import javax.annotation.Nonnull;
@IAgentAction
final class MyAgent extends IBaseAgent<MyAgent>
{
private static final long serialVersionUID = -5105593330267677627L;
private final CEnvironment m_environment;
MyAgent( @Nonnull final IAgentConfiguration<MyAgent> p_configuration, @Nonnull final CEnvironment p_environment )
{
super( p_configuration );
m_environment = p_environment;
}
@IAgentActionFilter
@IAgentActionName( name = "env/move" )
private void envmove( @Nonnull final Number p_value )
{
m_environment.move( this, p_value );
}
final int position()
{
return m_environment.position( this );
}
}
The agent script can use the action env/move
at line 10. The parameter is the new position, but this can fail, so the whole can fail also. Here we encapsulate the action with a plan to handle failure execution. The failure execution starts with the plan line 15-20. Each of the movement plans calls itself to create an infinity loop of moving. In parallel the plan line 21-23 will be executed if another agents have been moved.
!main.
+!main <-
NewPosition = MyPosition + 1;
NewPosition = NewPosition % EnvSize;
!!move( NewPosition )
.
+!move(X) <-
generic/print( "agent", MyName, "is on position", MyPosition, "move to ", X );
env/move( X );
X++;
X = X % EnvSize;
!move( X )
.
-!move(X) <-
Y = MyPosition - 1;
Y = Y < 0 ? EnvSize + Y : Y;
generic/print( "agent", MyName, "cannot move from", MyPosition, "to ", X, "try to move to", Y );
!move(Y)
.
+!other/agent-position/changed( from(X), to(Y) ) <-
generic/print( "agent", MyName, "get information that other agent has moved from", X, "to", Y )
.
The agent generator is structured for a flexible environment, so with the constructor the environment object is set into line 13, 15, 29
package myagentproject;
import org.lightjason.agentspeak.common.CCommon;
import org.lightjason.agentspeak.generator.IBaseAgentGenerator;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.InputStream;
import java.util.stream.Collectors;
import java.util.stream.Stream;
final class MyAgentGenerator extends IBaseAgentGenerator<MyAgent>
{
private final CEnvironment m_environment;
MyAgentGenerator( @Nonnull final InputStream p_stream, @Nonnull final CEnvironment p_environment ) throws Exception
{
super(
p_stream,
Stream.concat(
CCommon.actionsFromPackage(),
CCommon.actionsFromAgentClass( MyAgent.class )
).collect( Collectors.toSet() ),
new CVariableBuilder( p_environment )
);
m_environment = p_environment;
}
@Nullable
@Override
public final MyAgent generatesingle( @Nullable final Object... p_data )
{
final MyAgent l_agent = new MyAgent( m_configuration, m_environment );
int l_position = (int) ( Math.random() * m_environment.size() );
while ( !m_environment.initialset( l_agent, l_position ) )
l_position = (int) ( Math.random() * m_environment.size() );
return l_agent;
}
}
For creating fast access to data of the environment (size) and agent name (hashcode) we use a variable builder that gets the environment also as reference in line 14, 16, 18. The generate method creates constants for each plan line 23-30. Regarding the environment data, line 28 creates a variable with the size of the environment
package myagentproject;
import org.lightjason.agentspeak.agent.IAgent;
import org.lightjason.agentspeak.language.execution.IVariableBuilder;
import org.lightjason.agentspeak.language.instantiable.IInstantiable;
import org.lightjason.agentspeak.language.variable.CConstant;
import org.lightjason.agentspeak.language.variable.IVariable;
import javax.annotation.Nonnull;
import java.text.MessageFormat;
import java.util.stream.Stream;
final class CVariableBuilder implements IVariableBuilder
{
private final CEnvironment m_environment;
CVariableBuilder( final CEnvironment p_environment )
{
m_environment = p_environment;
}
@Nonnull
@Override
public final Stream<IVariable<?>> apply( @Nonnull final IAgent<?> p_agent, @Nonnull final IInstantiable p_runningcontext )
{
return Stream.of(
new CConstant<>( "MyPosition", p_agent.<MyAgent>raw().position() ),
new CConstant<>( "MyName", MessageFormat.format( "{0}", p_agent.hashCode() ) ),
new CConstant<>( "EnvSize", m_environment.length() )
);
}
}
If you struggled at some point or wish to obtain our exemplary solution with code documentation of this tutorial, you can download the archive containing the source code and an executable jar file:
We run the example with 5 agents and 5 cycles (different runs returns different outputs)
agent 2.003.087.617 is on position 6 move to 2
agent 544.479.674 is on position 2 move to 3
agent 793.503.139 is on position 5 move to 1
agent 2.135.449.562 is on position 3 move to 4
agent 1.713.875.358 get information that other agent has moved from 3 to 4
agent 1.713.875.358 get information that other agent has moved from 5 to 1
agent 1.713.875.358 is on position 0 move to 1
agent 2.135.449.562 get information that other agent has moved from 5 to 1
agent 2.135.449.562 is on position 4 move to 0
agent 544.479.674 get information that other agent has moved from 3 to 4
agent 544.479.674 get information that other agent has moved from 5 to 1
agent 544.479.674 cannot move from 2 to 3 try to move to 1.0
agent 2.003.087.617 get information that other agent has moved from 3 to 4
agent 2.003.087.617 get information that other agent has moved from 5 to 1
agent 2.003.087.617 cannot move from 6 to 2 try to move to 5.0
agent 793.503.139 is on position 1 move to 2
agent 793.503.139 get information that other agent has moved from 3 to 4
agent 1.713.875.358 cannot move from 0 to 1 try to move to 4.0
agent 2.003.087.617 is on position 6 move to 5.0
agent 544.479.674 is on position 2 move to 1.0
agent 2.135.449.562 cannot move from 4 to 0 try to move to 3.0
agent 793.503.139 cannot move from 1 to 2 try to move to 0.0
agent 1.713.875.358 is on position 0 move to 4.0
agent 1.713.875.358 get information that other agent has moved from 6 to 5
agent 2.003.087.617 is on position 5 move to 1
agent 2.135.449.562 get information that other agent has moved from 6 to 5
agent 2.135.449.562 is on position 4 move to 3.0
agent 544.479.674 get information that other agent has moved from 6 to 5
agent 544.479.674 cannot move from 2 to 1.0 try to move to 1.0
agent 793.503.139 is on position 1 move to 0.0
agent 793.503.139 get information that other agent has moved from 6 to 5
agent 1.713.875.358 cannot move from 0 to 4.0 try to move to 4.0
agent 1.713.875.358 get information that other agent has moved from 4 to 3
agent 2.003.087.617 get information that other agent has moved from 4 to 3
agent 2.135.449.562 is on position 3 move to 4
agent 2.003.087.617 cannot move from 5 to 1 try to move to 4.0
agent 544.479.674 get information that other agent has moved from 4 to 3
agent 544.479.674 is on position 2 move to 1.0
agent 793.503.139 get information that other agent has moved from 4 to 3
agent 793.503.139 cannot move from 1 to 0.0 try to move to 0.0
agent 1.713.875.358 get information that other agent has moved from 3 to 4
agent 1.713.875.358 is on position 0 move to 4.0