Writing breve Simulations With "steve"

Simulations in breve can be written using a language called "steve". steve is a simple language which aims to allow rapid construction of advanced simulations while avoiding a great deal of the programming overhead associated with constructing simulations in other languages.

Don't Be Overwhelmed!

Writing simulations in steve is ultimately quite simple. Due to the number of features provided by the simulation engine, however, this documentation may seem at times overwhelming. Don't panic—make liberal use of the demo simulations provided with the environment and don't be put off if some of the features discussed here are difficult to understand.

"steve" is an object-oriented language. This means that programming in "steve" involves working with components called objects which contain data (variables) and behaviors (methods). In breve, objects can be either real, meaning they have a presence in the simulated world; or abstract meaning that they are used to store data or to perform computations, but do not appear in the simulated world. In order to define agents and their behaviors, one needs to construct these objects and behaviors using the "steve" language. The language is described in this chapter.

First, the classic "Hello, World" program is constructed in steve (the section called “Hello, World”). Then, a simple multiagent 3D simulation written in steve is presented (the section called “A Simple Example”). This sample should give the basic idea of what a breve simulation looks like.

As discussed above, all agents in the simulated world are represented by programming objects. These objects are defined in terms of object templates called classes. The most important object, called the Controller is described in the section The Controller Object (the section called “The Controller Object”). The section Building Classes (the section called “Building Classes”) describes how to construct basic classes in steve.

In order to define the data and behaviors of your classes, you'll need to be familiar with types (the section called “Types in "steve"”) and expressions (the section called “Expressions”). The section Program Control Structures (the section called “Program Control Structures”) discusses loops and conditional statements.

Finally, after learning how to program with steve, you'll want to explore the Chapter 17 to learn how to use the features of the breve engine in your simulations.

Hello, World

The traditional introduction to any programming language is a program which repeatedly prints out the text "Hello, World!". Here it is written in steve:

@include "Control.tz"           

Controller HelloWorld.

Control : HelloWorld {
        + to iterate:
                print "Hello, world!".
}

The specifics will be discussed in more detail through the rest of this chapter. Briefly, we first declare the simulation's controller object to be a class called HelloWorld (line 3). Then we construct the HelloWorld class (lines 5-8), and as part of that class, we define a method called iterate (line 6) which will print out the text "Hello, world!". The "include" line (line 1) simply tells breve to load in a file called "Control.tz", which is included with the breve distribution. This built-in class file contains the class control, which is the parent class for our "HelloWorld" controller.

When this simulation is run, the controller object is created, and the method named iterate gets run automatically at every step of the simulation. Thus, "Hello, World!". Ad nauseum.

A Simple Example

"Hello, World" is a very simple example which can be used for any programming language. It does not, however, give any insight into how 3D multiagent simulations are written in steve. This section shows the code for a simple 3D multiagent simulation.

In the RandomWalker demo, you can see a simple simulation containing hundreds of independent agents.

The simulation below is very heavily commented—all of the lines beginning with the # character are descriptive comments which do not effect the execution of the simulation, but clarify for the user what the simulation is doing. Comments are described in more detail below.

The simple simulation below features a number of agents which perform a "random walk" through 3D space. That is to say that at each time-step, the agents will move in a random direction.

#
# RandomWalker is a simulation in which small spheres do
# a random walk.  This is a very simple simulation which
# can be used as a skeleton for more complex simulations.
#

# include some required breve class files
@use Control.
@use Mobile.

# First tell the breve engine the name of our controller class

Controller myControl.             

# Subclass "Control" and define our "myControl" object.

Control : myControl {
    # Here we define the class variables.  

    + variables:
        walkerShape (object).
        
    # Next we define a method called init.  The init method is called 
    # automatically when our class is created.  Since this is the controller
    # class, an instance gets created when we launch the simulation,
    # so this is the entry point for our simulation.
        
    + to init:
        print "Setting up the simulation.".

        self point-camera at (0, 0, 0) from (0, 60, 0).

        # set up a shape that all of the RandomWalker objects will use.
        
        walkerShape = (new Sphere init-with radius 1).
        
        # Create a bunch of RandomWalkers.  You can create as few or 
        # as many as you want... 

        200 new RandomWalker.

    # The get-walker-shape is a method that allows other objects 
    # to look at the walkerShape variable.  we do this so that 
    # each RandomWalker object can reuse the same Shape object.
    # This is not strictly required--each RandomWalker could create
    # it's own copy of the same Shape, but fewer objects means 
    # less memory used and more efficient simulations, so it's 
    # a good programming practice.
        
    + to get-walker-shape:
        return walkerShape.
}

# The "RandomWalker" object is the physical object in the simulation
# that does the random walk.  It is a subclass of "Mobile".

Mobile : RandomWalker {
    + to init:
        # During init, the object asks the controller for the shape 
        # it should use.  It then sets itself to a random color.

        self set-shape to (controller get-walker-shape).
        self set-color to random[(1.0, 1.0, 1.0)].
    
    + to iterate:
        # Set a new random velocity at every timestep.
        
        self set-velocity to random[(60, 60, 60)] - (30, 30, 30).
}

Comments

When writing code in any language, it is typically useful to include comments which clarify what the code is doing. In steve, there are two ways to include comments in code. A "single line" comment starts with a "#" is and continues to the end of the line; "multiline comments" are written starting with "#!" and ending with "!#". Some example of both kinds of comments are shown below:

        # this is a simple one-line comment.

        print "this is not a comment...".                # but this is.

        #!
                this
                is 
                a 
                multiline 
                comment
        !#

The Controller Object

The controller object is the foundation of a breve simulation. The controller object is a single instance which acts as the "boss" for the simulation. When the simulation starts, it is the controller object which is responsible for creating and setting up the rest of the simulation. The controller object is the only object which is automatically instantiated when the simulation begins. If you're used to programming in C or C++, the controller is like the main function.

The first step in writing a breve simulation is to create a controller class which will setup and manage the simulation. The controller class you create must be a subclass of the class Control or one of its subclasses. Details on how to do this is described in the rest of this chapter.

As shown in the previous section (the section called “A Simple Example”), the controller object is specified at the top of your simulation file using the word controller:

Controller myControllerClass.

This tells breve to create an instance of myControllerClass when the simulation begins. This controller object will create and setup the rest of the objects in the simulation. The controller object does so by implementing the simulation setup code in the init method. Details on constructing classes and implementing an init method are described in more detail in the rest of this chapter.

In addition to setting up the simulation, the controller object is also used as a liaison between simulation objects, the breve engine and the user. While other instances may come and go, the controller object is forever. The controller class contains a great number of methods for controlling the behavior of simulations, the appearance of the graphical display and interactions with the user interface. See the class documentation on Control for more information. As with some of the other topics covered in this section, the significance of these concepts will become clearer in the rest of the documentation.

Building Classes

All objects in the simulated world correspond to programming objects in steve. In order to define an agent in the simulated world, you'll start by constructing a programming object, or class. This class will serve as a template which defines the agent's behaviors. This section describes how to construct and use these classes.

The section Defining Classes (the section called “Defining Classes”) describes how to define an empty class.

All classes have two major components: methods, which define a class's behavior and variables which define the data that the class can hold. These data can be used both to store information about the state of an agent, or information required for computation. The section Defining Instance Variables (the section called “Defining Instance Variables”) details how variables can be added to objects, while the section Defining Class Methods (the section called “Defining Methods”) shows how methods are defined.

Two special methods are critical for an agent's behavior: one that gets called automatically when an agent is created, init, and another that is run automatically at every step of a simulation, iterate. These methods, and a few other special methods, are discussed in the section Special Method Names (the section called “Special Method Names”)

Even after the class is defined, it will still not be present in the simulation. This is because a class is nothing more than a "template" for an agent. In order to bring agents into the simulation, you must use the template to create instances of the class. The section on Creating and Destroying Instances (the section called “Creating and Destroying Instances”) describes how instances of classes are created and destroyed.

Defining Classes

When building a class, you typically don't start from scratch—instead, you make the new class the child of an existing class. This is called creating a subclass. By subclassing a class, the new class will inherit all of the parent's methods and variables. This approach means that most of the difficult work will already be done by an existing breve class, and we can simply override and customize the behaviors that we need to.

For example, if we're building an object which will move through the 3D world, we'd like to have an object that understands the relationship between position, velocity and acceleration. Instead of implementing such a class ourselves, we can subclass Mobile.tz which is included with breve. Our custom subclass will contain the custom behaviors we desire, while the parent class takes care of the details.

When building a class, you must first decide the class name and its parent class. The parent class is the class from which the new class will inherit its behaviors. Classes which are to be used primarily for computation and do not require any special inherited behaviors, will typically use the generic root class Object. Classes which move around the world will inherit behaviors from Mobile, while immobile objects in the world will inherit behaviors from Stationary. A full list of classes is available in the appendix (???).

An empty class is simply defined by the following steve code:

parent_class_name : class_name {

}

Because we often deal with classes in their plural form (like when creating multiple instances of an object), it can be useful to give a class an alias which will allow us to refer to the class in its plural form. This is not required but may make code easier to read. This alias is defined by adding the text (aka alias_name) after the class name.

As an example of defining a class, both with and without an alias, consider a class called myMobile which will be a child of the class Mobile:

# first without the alias...

Mobile : myMobile {

}

# now with the alias...

Mobile : myMobile (aka myMobiles) {

}

This code above defines an empty class with no variables and no methods. This means that it will behave exactly as its parent class does. The next step is to customize the class's behavior by adding in methods and variables.

Defining Instance Variables

An instance variable is a variable associated with a class. Each instance of the class will have its own private copies of the class's instance variables.

Once the empty class declaration has been written, variables can be added using the heading + variables, followed by the list of instance variables. Variables are listed in the format variable_name (variable_type).

The variable name must start with a letter, but afterwards it may contain any alphanumeric characters, as well as the characters _ and -.

Multiple variables of the same type can also be declared on the same line:

variable1, variable2, variable3, ..., variableN (variableType). 

Variable types are covered in detail in the section Types (the section called “Types in "steve"”).

As an example, we'll add some variables to the simple class we created in the previous section:

Mobile : myMobile {
        + variables:
                myInt, myOtherInt (int).
                myObject (object).
                myFloat (float).
}

Defining Methods

The most elementary method call in steve is a call to a method that takes no arguments. The definition of such a method is simple. Inside the definition of instanceName, we create a line:

+ to methodName:

The statements that follow this line will be part of the newly defined method until either the end of the object definition, or until the next method definition.

To define a method that takes arguments we will need the keyword, variable name and type of each argument. The keyword identifies the variable when it is being called, while the variable name is how the variable will be referenced from within the method. Finally, the type is simply the type of variable that will be passed in. The layout of this information is keyword, variable_name, (type), such that a method which takes one variable could be defined by the following line:

+ to set-velocity to-value theValue (float):

If the method takes two variables, we add another keyword/name/type triplet:

+ to set-rotation of-joint theJoint (Object) to-value theValue (float):

The code associated with the second method would then use the variables theJoint and theValue: "of-joint" and "to-value" are not actual variables, but instead the keywords which indicate which variables follows.

The calling convention of these methods is simple. After the instance name and method name, we give a list of keywords and values to be passed in. The order of the keyword/value pairs does not effect how the code is executed, though it may effect the readability of the code. The following lines call the set-rotation method which we defined above:

# the following lines are equivalent

myObject set-rotation of-joint myJoint to-value 200.
myObject set-rotation to-value 200 of-joint myJoint.

Methods may also have local variables associated with them. These variable definitions look just like class variable definitions, except that they follow after the method definition line, and not after the variable definition line. Method variables are automatically initialized to zero every time the method is called. Variable declarations in a method must precede all statements in the method.

For example, here is a simple method that uses local variables:

+ to find-closest-creature in creatureList (list):
        item (object).
        closestItem (object).
        distance (float).

        # we start with an unreasonably high "closestDistance" so that 
        # we are sure to find something closer.

        closestDistance = 1000000.

        foreach item in creatureList: {
                distance = |(self get-location) - (item get-location)|.

                if distance < closestDistance: {
                        closestItem = item.
                        closestDistance = distance.
                }
        }

        return closestItem.

For developer use only

When examining the internal classes included with the breve distribution, you might notice some methods defined using a minus sign instead of a plus sign:

- to methodName:

This syntax simply means that the method should be treated as a non-public method and that the method should not be documented. Though these methods function as all other methods, their use in user simulations is discouraged.

Optional Arguments in Methods

Method definitions may also specify optional arguments. Optional arguments are arguments that are given default values, and therefore do not need to be provided when calling the method. The use of optional arguments can greatly simplify writing code in steve.

To make an argument optional, you need to provide it with a default value. To do so, you'll need to modify the argument definition to include the text = value after the argument name. For example, a variable called theHeight with keyword with-height could be given a default value like this: with-height theHeight = 100 (int). Default values for optional arguments must be literal values (and not expressions or variables).

Below is an example of a method defined with a set of optional arguments.

        # Create a new agent, with some default values.

        + to create-new-agent with-color color = (1, 0, 0) (vector) 
                with-energy energy = 100 (int)
                with-radius radius = 10 (int) 
                with-name name = "agent" (string):

The method above could be called in a number of ways, optionally including or excluding each of the arguments:

        # no arguments 
        self create-new-agent.

        # some of the arguments 
        self create-new-agent with-energy 10 with-name "Becky".

        # all of the arguments
        self create-new-agent with-color (1, 1, 1) 
                with-energy 100
                with-radius 20
                with-name "Robert".

Special Method Names

Certain method names have special meaning in steve, in that they are called automatically by the simulation at special times. These methods, in particular init and iterate are critical, as they are the entry-point into how your agents are initialized and how they will behave. These special method names are outlined below:

  • init, if it exists, is called automatically when a class is instantiated. The method is called not only for the class being instantiated, but also for its superclass and all other ancestors up to the root object. Though you should implement an init method for your class which will set up the instance when the class is instantiated, the init method should never be called manually.

  • iterate, if it exists, is called automatically during every iteration of the breve engine. If your class must perform a task during each iteration, then you may choose to implement an iterate method. The order in which the objects in the simulation are iterated cannot be controlled—if you need to control the order in which actions are performed, consider using iterate in conjunction with the post-iterate method described below.

    Unlike the init and destroy methods, iterate is not automatically called for the superclasses of an instance. This means that your iterate method must call super iterate if you wish to incorporate the parent's iterate method. This is absolutely necessary for subclasses of Control.

  • post-iterate, if it exists, is called automatically during every iteration of the breve engine after the iterate methods for all objects have been called. It is occasionally desirable to perform an action during each iteration, which requires data produced during that very same iteration from other objects. If this action is to be performed in the iterate method, than object A cannot be certain that object B has been iterated yet (and vice-versa). To solve this problem, objects may implement a post-iterate method which is automatically called after all objects have been iterated. The PatchLife demo uses this technique.

  • destroy, if it exists, is called automatically when a class is being freed. However, unlike init, and like iterate, you must explicitly call the super class destroy method if you need it to be called as well. If your class needs to perform certain tasks before being destroyed, you should implement this method. Be warned the you need to be carefull not to free an object referenced in the base class if it is needed for the base class destroy method.

Creating and Destroying Instances

Creating a new instance of an object is called instantiating the object. Instantiating in steve is done using the new command. Instantiation creates a single new instance if no number is given, or as many objects as you want by preceding the command with a number. The syntax follows:

new object_type.
number new object_type.

If a single object is created, it is returned as an object type. If several are created, they are returned as a list. For example:

myBird (object).
myBugs (list).

myBird = new Bird.
myBugs = 100 new Bugs.

The method init is called automatically for a class and all of its superclasses when the class is instantiated.

Destroying instances is simply accomplished using the command free:

free instance.

free accepts both single instances and lists of instances.

If an instance frees itself, then execution of the code is stopped immediately, as though the free command was followed by a return.

When an object is freed, the destroy method is automatically called for the instance. Prior to version 1.9, destroy would automatically be called for all superclasses. This is no longer the case—you must call "super destroy" if you wish for the superclass destroy method to be run.

Types in "steve"

The int type

An int is a whole number, identical to the int type in C.

int operators

ints can be operated on using the following mathematical operators, which operate just like their C counterparts: +, -, *, /, and %. Additionally, the ^ operator can be used to raise an integer value to an integer power. Divisions or mods by zero are treated as errors and will stop the execution of a simulation. The absolute value of an integer is given by | integer expression |.

Conversions to Other Types

ints can be converted to floats automatically in expressions or during assignments, but cannot be converted to any other type.

The float type

A real number, also known as "double", identical to the double type in C. Internally, floats are represented by 8-byte doubles.

float Operators

All of the operators used for ints—including %—can also be applied to floats.

Conversions to Other Types

floats can be converted to (and from) ints automatically in expressions or assignments. floats cannot be converted to any other type.

The object Type

An object is an instance of a steve class.

object Operators

objects cannot be used in mathematical expressions as some of the other types can. Instead, objects are mainly used for one type of expression: method calling. Method calling is outlined in the section Method Calls (the section called “Method Calls”).

Conversion to Other Types

Objects cannot be explicitly converted to other types. They do, however, have meaning as "boolean" (true or false) expressions in control structures (the section called “Program Control Structures”). If used in the context of a boolean expression (like an if statement, the section called “The if Statement”), the expression is true only if the variable refers to an active object. This means that an object variable which has not yet been associated with an instance in the simulation is false.

The vector Type

A vector is used to represent a point or vector in 3D space. A vector is expressed as three floating point numbers, such as (1.0, 2.0, 5.0).

vector Operators

Vectors may be added (+) to and subtracted (-) from other vectors, which yields another vector. Vectors may be multiplied (*) or divided (/) by ints or floats, which also results in other vectors. The length of the vector is given using the following construct: | vector expression |.

Some examples of using vectors follow:

v = (1, 2, 3).  # sets the vector to a constant vector.

v = v * 4.      # multiplies the vector by 4

v = v / |v|.    # normalizes the vector by dividing it by it's own 
                                # length such that the new length is 1.

Individual vector components can be extracted or set using the "::" notation:

xValue = myVector::x.

myVector::y = 100.

Starting in breve 2.5, the standard list index syntax may also be used with indices 0, 1, and 2:

xValue = myVector{ 0 }.

myVector{ 1 } = 100.
Conversions to Other Types

Vectors cannot be converted to any other types. They do, however, have meaning as "boolean" (true or false) expressions in control structures (the section called “Program Control Structures”). If used in the context of a boolean expression (like an if statement, the section called “The if Statement”), the vector will be true if it's length is greater than 0, and false if its length is equal to 0. This is to say that only the vector (0, 0, 0) is false in a boolean context.

The matrix type

A matrix in steve refers to a 3x3 matrix which describes a transformation in 3D space. Using transformation matrices is somewhat advanced and they are not generally used in most simulations. Still, they may be useful when dealing with physical simulations.

Matrices may be written in steve as three comma-separated vectors, enclosed in braces ('[' and ']'), as in this example:

# the matrix m will be initialized to:
#
# [ 1 2 3 ]
# [ 4 5 6 ]
# [ 7 8 9 ]

m = [ (1, 2, 3), (4, 5, 6), (7, 8, 9) ].

Matrix components can be extracted using the list index syntax. Each row of the matrix is a vector:

myVector = myMatrix{ 0 }.

myMatrix{ 1 } = ( 1, 2, 3 ).

Individual numbers may be extracted from matrices just as they are from two-dimensional lists:

myNumber = myMatrix{ 0 }{ 0 }.

myMatrix{ 1 }{ 1 } = 100.
matrix Operators

Matrices may be multiplied (*), divided (/), added (+) to and subtracted (-) from other matrices. The results of these operations are other matrices.

Matrices may be multiplied (*) or divided (/) by scalars (ints and doubles). The result of these operations are other matrices.

Matrices may be used to transform vectors by multiplying the matrix times the vector (*). The result of this operation is a vector.

Conversions to Other Types

Matrices cannot be converted to any other types. They do, however, have meaning as "boolean" (true or false) expressions in control structures (the section called “Program Control Structures”). If used in the context of a boolean expression (like an if statement, the section called “The if Statement”), the expression will be true if there are any non-zero values in the matrix. This means that a matrix of all zeros will be false, while all other matrices are true.

The list Type

The list datatype allows you to keep a list of other variables. lists can contain any datatype, including other lists. lists can even contain multiple datatypes simultaneously.

lists are formed using the syntax { item1, item2, ... }.

Some simple examples of constructing lists are shown below:

myList = { 1, 2, 3.0 }.                 # a list of numbers (both int and float)
myList = { "a", "b", "c" }.             # a list of strings
myList = { "a", 30, new Mobile }.       # a list of mixed types
myList = { 1, "a", { "dog", "cow" } }.  # a list with a nested list

An important feature of lists in steve is that they are always passed by reference and are not copied. This means that if you pass a list to a function, then any modifications done to the list inside the function will modify the original list.

list Operators

The following operations can be used with list expressions:

  • insert expression, at list, { index }: inserts expression at the specified index in the list, shifting up other list elements with higher indices

  • remove list, { index }: removes the element of list at the specified index and returns it, shifting down other list elements with higher indices

  • push expression, onto list: appends expression onto the end of list

  • pop list: removes the last element of list and returns it

  • prepend expression, onto list: prepends expression onto the start of list

  • unprepend list: removes the first element of list and returns it

  • list, { expression }: returns the element of the list at offset expression. The expression index is zero based, as in C, such that 0 refers to the first element, 1 to the second, and so forth. If the offset expression is less than zero, or greater than the length of the list minus one (because the access is zero based), an error is triggered and the simulation is stopped.

  • list, { expression, } = value: sets an element of the list at offset expression to value. The offset index is again zero based. If the offset expression is less than zero or greater than the size of the list an error is triggered and the simulation is stopped. If the offset expression is equal to the size of the list, the list is extended by one element; the operation has the same effect as pushing a value on to the end.

  • sort list, with method-name: sorts list using the method specified with method-name. method-name must be a method which takes two list elements (the keywords are unimportant) and compares them, returning a negative number if the first list element belongs before the second in the sorted list, a positive number if the second belongs before the first, and 0 if the two entries are equal. In most cases, this confusing sounding method returns a certain value associated with one argument minus the same value in the other.

    Unlike the perl sort operator, sort operates on the list it is given and does not return a copy of it. This means that the original list is modified during the sort operation.

  • copylist list: copies the entire list. Normally, assigning a list to a variable will not copy the list but instead will yield two variables pointing to the same list.

  • | list |: gives the length of a list. Lists are automatically converted to integers when use in mathematical expressions, but this construct can be used too force the conversion.

Conversions to Other Types

lists can be converted to ints simply by using them in the context of integers. They can thus also be used as floats. In the event that the context of the expression does not force the list to become an integer, you can force it yourself by using it in a mathematical context:

myInt = (myList + 0).

The string Type

The string type holds a character string. A string is written in code (as in C) as a quoted string. For example, the built-in print operator is capable of printing strings:

print "this is a string".
string Operators

The following operations can be done on strings:

  • string { index }: returns, as a string, the letter at the specified index.

  • string { index } = otherString: replaces the letter at the specified location with the string otherString.

  • | string |: returns the length of the string.

In addition to the operators listed above, strings can be formatted using embedded variables. Variables can be embedded in strings so that the strings are interpreted dynamically, as in Perl. To indicate a variable embedded in a string, use the "$" character. The following, for example, will use the variable "self" in order to build a string to be printed:

print "my value is $self.".

This technique can be used to performing string concatenations as well:

$longstring = "$string1$string2".
Conversions to Other Types

strings can be converted to ints and floats, but not to any other types.

In the context of an int or a float, the string is translated into the appropriate type by converting the numerical component of the string. For more information on this conversion, consult the man pages for the ANSI C atoi() (for ints) or atof() (for doubles) functions.

The hash Type

A hash is a type which works like a dictionary: it allows expressions ("values") to be stored and looked-up using other expressions as the "keys". The following example shows a steve hash being used like a dictionary:

dictionary (hash).

dictionary{ "dog" } = "a four-legged pet".
dictionary{ "fish" } = "a zero-legged pet".

# this will print the definition we stored above.

print "the definition of dog is: ", dictionary{ "dog" }.

As shown in this example, we're able to store data using strings as the keys. When data is later retrieved from the hash table using the same key, the value stored previously is returned.

hashes are not limited to using strings as keys. They can use any valid type. The most useful application is a hash table which uses objects as keys. In this way, relationships between objects can be stored in a hash. Consider an object that wants to keep track of neighbors it has encountered previously:

# let's say that this method gets called when a neighbor is seen...
# if the neighbor has already been seen, print a message — otherwise,
# just add it to the hash for next time!

+ to meet with neighbor (object):
        if seenHash{ neighbor }: print "I've seen this neighbor before!"
        else seenHash{ neighbor } = 1.

The example above also shows that the syntax of hashes is the same as the syntax for lists when storing or retrieving data. Unlike lists, however, hashes do not hold ordered data, and none of the other list operators work with hashes.

When using hashes with keys that are ints, floats, vectors, matrices or strings, steve will test the equivalence of the keyed data when looking up or storing the data. This means that two equivalent strings, even if they are stored in different variables, will refer to the same value in the hash. For the other types (objects, pointers, data, lists and hashes themselves), the hash will only return the same value for the same exact variable key. This means that two different lists, even if they contain "equal" data, will access different values in the hash.

An important feature of hashes in steve is that they are always passed by reference and are not copied. This means that if you pass a hash to a function, then any modifications done to the hash inside the function will modify the original hash.

hash Operators

A list of the keys in a hash may be retrieved using the built-in function keys. This key list is often used to iterate through the items in a hash:

foreach key in keys( myHash ): {
        print myHash{ key }.
}
Conversion to Other Types

hashes cannot be converted to any other types.

The pointer Type

For developer use only

The pointer type is for use by breve and plugin developers only.

pointer variables store C-style pointers to internal data. They are not used for writing simulations in steve and are only used by breve developers and plugin authors.

pointer variables are only useful in the context of interacting with internal C-style function calls: they do not contain methods, variables or any other meaning in the context of most steve code. Like objects, however, pointers can be tested to see if they are NULL (0), but cannot be used in mathematical expressions. That is to say that pointers have a meaning in a boolean context of control structures such as if and while (as well as the logical operators && and ||).

As in C, copying a pointer (assigning it to another variable) will not copy the data it points to.

Conversion to Other Types

pointers cannot be converted to any other types.

The data Type

For developer use only

The data type is for use by breve and plugin developers only.

data variables are similar to pointer variables in that they contain a reference to internal data. Also like pointer variables, they are not to be used in regular simulation code—they are only to be used by breve developers and custom plugins.

The difference between data and pointer is that data refers to a linear block of internal data of known size. This means that data variables can be successfully archived, while pointers cannot. The only use for data variables is archiving and dearchiving internal data. For more information on using the data type with plugins, see the section on Archiving Plugin Data With The data Type (the section called “Archiving Plugin Data With The data Type”).

Expressions

Special variables and values

Certain variables have special meanings in steve. Their values are managed automatically by the breve engine and should not be assigned manually.

  • self (object).

    This variable always contains the value of the instantiation in which the current method is being run. The variable is used most frequently to call other methods within the same instantiation. For example, an object of class Mobile could move itself with the call:

        self move to (10, 10, 10).
    
  • super (object).

    This special value refers to the parent, or super-instance. This is used to invoke a parent class implementation of a method, instead of the current class implementation.

    For example, in the Controller's iterate method, the superclass iterate method is often called: super iterate. This is because custom subclasses of Control typically preform simulation specific tasks, but must then call on the superclass implementation (Control) to actually step the physical simulation forward and update the state of the world. Anytime you wish to implement a custom object behavior in addition a parent class behavior, you should invoke the parent class method as well.

  • controller (object).

    The controller variable is defined for all instances. It always refers to the simulation's controller instance.

Assignments

The most simple expressions are simple assignments of literal values to variables. It may help to refer to the documentation on steve types before continuing.

Below are a few examples of this type of expression. In each case, the variable on the left will take the value of the expression on the right. If the expression on the right is not the correct type, it will be automatically converted if possible. If the conversion is not possible, an error will occur.

myInt = 4.
myDouble = 12.345.
myString = "Hello!".
    
# If we assign a double to an int, it will be automatically converted by 
# truncating the decimal portion.  In this example, myInt will take the value 
# 4:  
    
myInt = 4.8.
    
# Similarly, if we assign a string to an int or a double, the variable will
# take the numeric value of the string, in accordance with the atoi() or
# atof() ANSI C function.  Here the double will get the value 10000: 
     
myDouble = "10000 miles away.".

Mathematical Expressions

Mathematical operators in steve function almost exactly the same as they do in C, although there are some additions to account for vector and matrix types.

The following binary operators are valid for int and double types (the descriptions refer to x and y, as though they were used in an expression: x operator y):

  • +, addition

  • -, subtraction

  • *, multiplication

  • /, division

  • %, modulus (the remainder of x when divided by y)

  • ^, power (x raised to the power of y)

Their functions should be self-explanatory, with the possible exception of modulus, which cannot be used with double types in C. When used with doubles, the result is calculated using the ANSI C fmod() function.

The following operators are valid for use with two vectors:

  • +, vector addition

  • -, vector subtraction

The following operators are valid for a vector and a double (although an int is automatically promoted to a double in this case):

  • *, vector multiplied by scalar

  • /, vector divided by scalar

As in many languages, parentheses are used to group expressions when the default order of operations does not compute the desired result:

# this is the default order, but we can make it explicit with parens
x = x + (4 / y).                    # computes 4 / y first...

# this is NOT the default order -- we need to force it
x = (x + 4) / y.                    # computes x + 4 first

Mathematical expressions are often used in conjunction with assignments in order to modify the value of a variable. A few examples of using mathematical expressions in conjunction with assignments follow:

    x = x + 4.

    y = (1, 2, 3) / x.                  # assumes x is an int or double

    z = z + x^2.

Mathematical Assignment Expressions

In addition to the mathematical assignment operators above, steve also support mathematical assignment operators. Mathematical assignment operators are used as shortcuts to perform a calculation and update the value of a variable simultaneously, instead of as two separate steps. These expressions are useful, but since they are only shortcuts for other expressions, understanding them is not critical.

The following mathematical assignment operators are available:

  • +=

  • -=

  • *=

  • /=

  • %=

  • ^=

These operators are simply shortcuts for cases in which the left operand of the mathematical expression is also the location where the output of the expression will be stored. For example, the following expression pairs are equivalent:

a = a + b.               # "a equals a plus b" can also be written as...
a += b.                  # "a plus equals b"

a = a - (2, 2, 2).
a -= (2, 2, 2).

steve also has "increment" and "decrement" assignment operators:

  • ++

  • --

As in languages like C and Perl, these operators increment and decrement a variable by one, respectively. Unlike C and Perl, these operators may only be placed after the variable. As an example, the following expression pairs are equivalent:

x = x + 1.          # updates the variable x by adding 1
x++.

x += 1.             # does the same...
x++.

y = (x += 1).       # a little confusing, but sets both x and y to (x + 1)
y = x++.            # as does this.

Mathematical Functions

A number of internal functions (which are otherwise typically not used in breve simulations) are available for math-related expressions. Internal functions are called just like C functions: functionName (arguments).

  • sin(input) gives the sine of the radian angle input.

  • cos(input) gives the cosine of the radian angle input.

  • tan(input) gives the tangent of the radian angle input.

  • asin(input) gives the radian angle arc sine of input.

  • acos(input) gives the radian angle arc cosine of the input.

  • atan(input) gives the radian angle arc tangent of input.

  • sqrt(input) gives the float square root of input.

  • angle(a, b) gives the float angle in radians between vectors a and b.

  • max(a, b) gives the maximum of floatsa and b.

  • min(a, b) gives the minimum of floatsa and b.

  • cross(v1, v2) gives the vector cross product of vectors v1 and v2.

  • dot(v1, v2) gives the float dot product of vectors v1 and v2.

  • log(input) gives the float natural log of input.

  • randomGauss() gives a float random number with a Gaussian distribution.

  • transpose(input) gives the transpose of the matrix input.

The following internal functions are used for testing float variables for special values which have meaning primarily to developers and plugin authors.

  • isnan(input) returns 1 if the input is a "not-a-number" float value, 0 otherwise.

  • isinf(input) returns 1 if the input is a float value representing infinity, 0 otherwise.

Random Numbers

Random numbers are available in steve using the command random. The syntax is random[ expression ], where expression is an expression of either int (the section called “The int type”), float (the section called “The float type”) or vector (the section called “The vector Type”). The value returned is always the same type as the expression. In the case of int or float, the returned value is a random value between 0 and the expression. In the case of a vector expression, the returned value is a vector in which each element is between 0 and the corresponding value of the expression. For example, a call to random[(10, 10, 20)] returns a vector with X and Y elements between 0 and 10, and the Z element between 0 and 20.

Note that random[intValue] returns a value between 0 and intValue, inclusive, as opposed to the behavior that many C programmers expect in which the returned value is between 0 and intValue - 1.

Because many simulations use the origin (0, 0, 0) as the "center" of their world, it is often useful to obtain a random vector centered around (0, 0, 0). For example, if we want to move agents somewhere within an imaginary box surrounding the origin, we might use the expression random[(40, 40, 40)] - (20, 20, 20). This convention gives us a vector with each element between -20 and 20. This type of expression appears frequently in simulations.

The values are produced using the standard C library rand() routine. The library is seeded with the current time when the breve application is launched. To explicitly set the random seed, you may call the internal function randomSeed( value ), where value is an integer.

In the event that multiple breve simulations are launched simultaneously (typically only relevant in cluster environments), it may be necessary to pick unique random seeds for each such simulation to prevent them from producing the exact same results. Refer to the Controller method set-random-seed-from-dev-random for more information on automatically picking unique random seeds on systems that support it.

Method Calls

As discussed in the section on defining class methods, each method is identified by a name and may have any number of input arguments. The most simple method call to a method with no arguments is simply:

instancemethodName.

If the method takes arguments, each argument is associated with a keyword: the keyword identifies which argument will follow and what it's type is. This is somewhat different from C where the argument's type and meaning is specified by its order relative to other arguments. The steve method is more similar to Objective C and allows a more natural language structure to method calls and protects against arguments being passed in the wrong order.

To pass arguments to the method, simply precede the argument value with the keyword name. Consider a method move-object which takes a keyword to:

myObject move-object to (1, 2, 3). 

If the method takes more than a single argument, the convention is the same—just add the next argument afterwards. Note that the order in which the arguments are passed does not affect the method call, though it may affect the readability of the code. For example, the Control object implements a method to point the camera at a certain vector location from a vector offset—first we'll see the method definition, then how it's called:

# if the method is defined using:
    
+ to point-camera at location (vector) from offset (vector):
    ...

# then from another method, we can call point-camera using the code below.
# these two method calls are equivalent, though the first reads more 
# naturally.

+ to do-something-else:
    self point-camera at (0, 0, 0) from (100, 100, 100).
    self point-camera from (100, 100, 100) at (0, 0, 0). 

If you wish to call a method for multiple objects, you can use the method call syntax with a list of objects. Note, however, that the arguments to the list are computed separately for each item in the list. This makes a difference in the following example:

# assume that mobileList is of type list

mobileList = 10 new Mobile.

# The random statement is evaluated for each instance, meaning that all the 
# instances go to different random locations, not to a single random location.

mobileList move to random[(20, 20, 20)].

The all Expression

You can find all instances of a given class using the all expression. all is given the name of a class, and returns a list containing all objects of that type.

# get a list of all mobile objects in the simulation

myMobile = all Mobiles.

Printing information with print and printf

The print and printf statements are used to print output from a simulation to the output log. Both of these statements accept any type of expression, as well as multiple expressions separated by commas. Also, since strings may contain embedded variables, you can format the output of variables however you'd like. See the section on strings (the section called “The string Type”) for more information.

The only difference between print and printf is that printf does not automatically print a newline character. This means that subsequent prints will continue on the same line (as though the "return" key was never hit). This can be useful if you're trying to produce output in a specific format, but is typically not desirable. If in doubt, stick to print

Here are some examples of print and printf:

# print two variables, side by side.

print (self get-time), populationSize.

# use a variable embedded in a string.

print "the population size is $populationSize".

# the following statements would produce the text:
# A B C
# D E F

print "A B C ".
print "D E F".

# the following statements would produce the text:
# A B C D E F

printf "A B C ".
printf "D E F".

Using Subexpressions

As in C, of course, users can use subexpressions as part of larger expressions. For example, you can use a mathematical expression as part of a method call, or a method call as part of a mathematical expression. Because of the syntax of steve , however, subexpressions frequently need to be parenthesized in situations where it would not be required in C. The following important rules apply to using subexpressions: If a method call is not the entire statement, it must be parenthesized. If you wish to assign the result of a method call, use it in a mathematical expression or use it as an argument for another method, for example:

myInt = self get-speed.                   # incorrect
myInt = (self get-speed).                 # correct

myInt = self get-speed + 5.               # incorrect
myInt = (self get-speed) + 5.             # correct

self set-speed to neighbor get-speed.     # incorrect 
self set-speed to (neighbor get-speed).   # correct

All method arguments must be a single "unit"—arguments which are not simply a variable or literal value must be parenthesized.

This means that if you use mathematical expressions, instantiations or other method calls as input arguments to a method, they must be parenthesized. The first rule about method calls, of course, still applies:

self set-location to ((neighbor get-location) + (10, 10, 10)). # correct
self set-location to (neighbor get-location) + (10, 10, 10).   # incorrect

Internal Function Calls

For developer use only

Internal function calls are for use by breve and plugin developers only.

The final expression type is the internal function call. Internal function calls look just like C calls:

methodName(arg1,arg2, ... argN)

Although internal function calls grant the most direct access to the breve libraries and features, the included class hierarchy provides a formal interface to the internal functions such that user simulations should never use these internal functions. The only exception to this is for certain mathematical functions.

Program Control Structures

Control structures effect the flow of simulation code. Though many of these structures function the same as their counterparts in C, the syntax is slightly different in each case. The main difference is that the statement being tested is not (necessarily) surrounded by parentheses, but is followed by a colon (':') character.

Control statements evaluate test statements and then execute code according to the result. In the case of for (the section called “The for Loop”), foreach (the section called “The foreach Loop”) and while (the section called “The for Loop”), these structures are used as loops to repeat execution of a piece of code a certain number of times or while a certain condition is met. The if statement is used to execute a block of code if a certain condition is true or, optionally, a different block of code if the statement is false. A call to return will exit any control structure block immediately, as well as the method to which it belongs.

The conditional statements are comprised of C-style comparison operators. The following comparison operators are available:

  • ==, Equals

  • !=, Not equals

  • >=, Greater than or Equals

  • <=, Less than or Equals

  • >, Greater than

  • <, Less than

  • &&, And (short circuit operator)

  • ||, Or (short circuit operator)

  • !, Negation

For all of these structures, the code to be executed may be either a single statement, or several statements enclosed in braces ('{' and '}').

The if Statement

The if statement is used to execute one piece of code if a test statement is true, or (optionally) another if the statement is false:

if test_statement: true_code
[ else false_code ]

Examples of the if statement are shown below.

# here we execute a single statement

if x > 5: x = 20.
else x = 0.

# here we execute multiple...

if x > 5: {
        x = 20.
        y = 40.
} 

# here we execute multiple statements in the if, but only one in the else...

if x > 5: {
        x = 20.
        y = 40.
} else x = 200.

The while Loop

The while control structure works just like the while statement in C. While executes a block of code repeatedly, as long as the test condition is true:

while test_condition: code

An example of the while loop is shown below.

# for example...

while x < 10: {
        print "x = $x".
        x++.
}

The foreach Loop

The foreach control structure is similar to the foreach loop in Perl. The loop iterates through a list, and executes the associated code each time. The current item in the list is stored in a temporary variable as supplied by the user:

foreach temporary_variable in list_variable: code.

An example of the foreach loop is shown below.

# so, for example, if we have a variable called agent and a list
# of objects stored in agentList:

foreach agent in agentList: {
        print (agent get-location).
}

The for Loop

The for loop (similar to the for loop in C) repeatedly executes a block of code. Though it can function more generally like the while loop, it is typically used to run a block of code for each value of a "counter" variable.

The loop is separated into three statements—an initializer, a test statement, and an increment statement.

for expression, test_expression, increment_expression: code.

The initializer is executed once when the loop starts. It is typically used to set the iteration variable before proceeding. The test statement is run at every iteration to determine whether the loop will continue to execute (similar to the while loop). Finally, the increment statement is run at every iteration of the loop, typically to update a counter variable. Examples of the for loop are shown below.

# so, for example, if we have a variable called n (int), this loop will
# print the numbers from 0 to 29.

for n=0, n<30, n+=1: {
        print n.
}

# we can also use a different increment statement in order to run the 
# loop a bit differently—let's print only even numbers between 2 and 28

for n=2, n<=28, n+=2: {
        print n.
}

Garbage Collection and Memory Management

breve includes built-in garbage collection (sometimes called GC). Garbage collection is a form of memory management in which the system detects when an object is no longer referenced by any other variable. When an object is no longer referenced by any other variable, it is an indication that the memory is no longer in use and can thus be safely deallocated.

Memory Management and Garbage Collection of Basic Types

Memory management and garbage collection of basic types happens automatically and requires no user interaction. ints, floats, matrices and vectors are passed by reference and do not require garbage collection. lists, hashes and data are automatically garbage collected when appropriate.

Memory Management and Garbage Collection of Objects

breve's garbage collection is slightly complicated by the fact that objects do not need to be referenced in memory to be "in use". An unreferened object may, for example, "come back to life" because of an all (the section called “Finding All Objects of a Given Type”) expression. Furthermore, objects in the simulated world (members of subclasses of the class "Real") may physically interact even without referencing each other in their variables. Because of these complications, garbage collection cannot be automatically enabled for all objects in a simulation.

Garbage collection for objects is thus enabled on a per-object basis and the programmer must decide when its use is appropriate. The following guidelines should generally help to decide when garbage collection is appropriate for an object:

  • the object is not a member of a subclass of Real.

  • the object does not have it's own iterate or post-iterate methods.

  • the object is not a dependency of any object that does not hold a reference to it.

A Garbage Collection Caveat: Circular References

One important caveat applies to garbage collection of both basic types and objects. The steve garbage collection scheme does not correctly deallocate memory when there are circular references. A circular reference occurs when two (or more) objects refer to each other in a circular fashion. An example of a circular reference between three objects is shown below:

When a circular reference occurs, the objects are never recognized as "unused" and are thus never deleted as they should be. Circular references thus lead to "islands" of unused memory which do not get released. This type of circular reference is rare, but if your simulation design makes use of these types of structures, you may have to explicitly overwrite variables in order to ensure that no circular references exist.

Directives

Before getting started with the code for a simulation or class, there are often a few special lines at the top of a steve file which begin with an @-character. These lines are called @-directives ("at directives") and they specify information that can help breve to locate classes or other files, or to define some constants that will be used in your simulation. These directives are described below.

include and @use: Load Another Source File

breve includes a rich hierarchy of classes that are used to construct simulations. To use a class that comes with breve (or another file which you have created yourself), you must tell breve to load in its associated class file. You need to load a class before you instantiate it or subclass it.

Classes are loaded using the @include directive. It is used simply by specifying the name of the file to include:

@include "Control.tz".

@use works the same way, but with a slightly different syntax, leaving out the quotes and the ".tz" from the file name:

@use Control.

There is no difference between @include and @use in terms of how files are actually loaded.

@include directives are not only used to include classes that come with the breve distribution, but also potentially classes that you construct yourself. They are often used in conjunction with the @path directive (the section called “path: Specify a Search Path”) to specify the location of classes before loading them.

path: Specify a Search Path

Each @path directive specifies a directory that breve should search within to find files. These directives apply to finding class files, image files, sound files, archives and any other type of resource that breve might need to search for. They should go at the top of source files, before any other files are included or used.

Here's an example @path directive that allows a folder containing class files to be searched:

@path "/Users/jk/breve_classes".

You may specify as many directories with @path directives as necessary.

define: Define a Global Constant

A global constant lets you associate a name with a constant value in your simulation. The @define directive allows you to associate names with ints, floats and strings.

These constants can be very useful when you have the same value used several times in a simulation—then if you wanted to change the value, instead of making the change several times, you would make it once in the @define directive. They can also be useful for assigning meaningful symbols to numbers in your simulation. For example, if your cellular automata is arbitrarily chosen to be 50x50, then instead of hardcoding the number 50, it is more flexible and more descriptive to use a global constant.

Global constants are defined with the following form:

@define constant-nameconstant-value .

Here are some examples:

@define CA_SIZE                 50.
@define PI_VALUE                3.14159.
@define STRING_CONSTANT         "Hello".

By setting these constants at the top of the source file, you can use them later on. For example:

+ to print-pi:
        print "pi is approximately: ", PI_VALUE.

It is not required, but, by convention, global constants are typically written with all capital letters, to distinguish them from variables.