Here is a rudimentary tutorial, built around sample types. It assumes you already know how to load sides and run rounds, and have some idea how a stack-based language works.
You probably won't go through this tutorial in order. You probably won't build sides very similar to the examples. But the examples will show you what's important, what you should learn, and what you can imitate. Not everything is explained, so don't hesitate to look things up.
Additional examples and explanations (or even just suggestions) are eagerly accepted.
The simplest side that reproduces:
#side Tutorial
#color FD4
#type Vegetable
#hardware
solar-cells .4
constructor .4
armor 50
processor 2
#code
do
autoconstruct
forever
#end
do ... forever
is an infinite loop. Most types have at least one of these. autoconstruct
turns the constructor on and off as energy is available.
Vegetables are delicious, and many sides will kill and eat them. Let's give them a weapon.
#type Killer Vegetable
#color F0E
#hardware
solar-cells .3
constructor .3
armor 100
processor 5
energy 100 0
grenades 30 15 34
robot-sensor 15
#code
do
autoconstruct
grenades-reload-time periodic-robot-sensor if
robot-found if
robot-position robot-velocity lead-grenade
then
then
forever
Obviously you need something to shoot at, so load up some other sides if you haven't already.
test if body then
is a conditional, written in a different order than in most languages.
periodic-robot-sensor
takes an argument, and fires the robot sensor if that many frames have passed. Using grenades-reload-time
as the argument gives the weapon just enough time to reload between scans.
lead-grenade
is a convenient operator for shooting at moving targets. If you replace robot-position robot-velocity lead-grenade
with robot-distance robot-direction fire-grenade
and test against some moving targets, you can see why shot-leading matters.
Vegetable grows very slowly, especially if it spends energy buying weapons. Let's make a type which eats manna, so it can grow faster.
#type Animal
#color
#hardware
engine .05
constructor 1
eater 2
energy 250 25
food-sensor 8
processor 5
armor 100
#code
do
autoconstruct
44 periodic-food-sensor drop
food-found if
food-position seek-location
else
0 engine-power!
then
forever
periodic-food-sensor
returns a boolean indicating whether it fired the sensor, but since we don't care, we use drop
to stop the results from piling up on the stack.
seek-location
sets the engine to move toward the coordinates given as arguments. It's the most common movement operator. engine-power!
is a lower-level way of controlling the engine, which we use here to turn it off when there's nothing to eat.
Animals tend to starve. When there's no food nearby, waiting for some to appear doesn't work very quickly. Why not go looking instead?
#code
#vector dest
new-dest:
0 world-width random 0 world-height random dest!
return
#start
new-dest
do
autoconstruct
44 periodic-food-sensor drop
food-found if
food-position seek-location
else
dest seek-location
position dest 3 in-range new-dest& ifc
then
forever
new-dest
defines a label, which can be called as a user-defined operator, or passed as an argument to other operators, like ifc
.
If execution started at the beginning of this type, it would blunder into new-dest
and fail when trying to return from it. #start
makes execution start elsewhere.
#type Mineral
#color FFF
#hardware
processor 10
robot-sensor 10
blaster 25 3 25
engine .1
energy 200 5
solar-cells .12
armor 300
#code
do
0 engine-power!
energy 20 > if 57 periodic-robot-sensor and-if robot-found and-if
attack^
then
armor max-armor < energy 10 > and repair-rate!
forever
attack:
robot-position robot-velocity seek-moving-location
do
blaster-reload-time periodic-robot-sensor if
robot-found if
robot-position robot-velocity lead-blaster
robot-position robot-velocity seek-moving-location
else
return ;nobody here - give up
then
then
energy armor min 20 > while-loop
return
Read other sides. (But bear in mind that they don't always do things in the best way. In particular, old sides don't use newer features.) Try to figure out how they work. Modify them and see if you can do better.
Don't be afraid to learn math. In particular, vector arithmetic is easy and very useful. (It's useful for other things than Grobots, too.)
Don't try to make a side perfect from the beginning. Just try to make it do one thing well. You can improve the rest later.
Keep it simple. Complicated code is hard to debug and probably won't work much better. Don't try too hard to make it behave properly in all circumstances. For Grobots, code that works most of the time is almost as good as code that works all the time.
You can control what type a baby will be with constructor-type!
. Types are numbered (not named, sorry) in order from 1. If you set constructor-type
when there's already a baby in progress, it will abort, so it's usually set only when constructor-type is 0.
Most sides want to build whatever type they're short on. You can find out the populations with type-population
:
constructor-type nif
2 type-population 3 type-population < 2 3 ifev constructor-type!
then
nif
is short for not if
. ifev
is a value conditional often used for this purpose.
Mineral ignores incoming shots, so an enemy with a long-range weapon can safely kill it. Most sides avoid this by watching for incoming shots, and following them back to their origin.
The simplest way to adapt Mineral to chase shots is to generalize attack
to chase an arbitrary location, not just robot-position
. Then when we see a shot, we can look at its velocity and attack the area it came from.
do
0 engine-power!
energy 20 > if 57 periodic-robot-sensor and-if robot-found and-if
robot-position attack^
then
energy armor min 50 > if ;only attack if we're healthy
15 periodic-shot-sensor if shot-found and-if
shot-velocity unitize -20 vs* shot-position v+ attack^
then
then
armor max-armor < energy 10 > and repair-rate!
forever
#vector target
;;Go to a location and kill any enemies seen
;takes target as an argument
attack: ; tx ty --
target!
do
robot-found if
target robot-velocity seek-moving-location
else
target seek-location
then
blaster-reload-time periodic-robot-sensor if
robot-found if
robot-position robot-velocity lead-blaster
;stay a short distance away from the target:
position robot-position v- unitize 2.5 vs*
robot-position v+ robot-velocity 10 vs* v+ target!
else
position target 3 in-range ifr ;nobody here - give up
then
then
robot-found nif 13 periodic-shot-sensor and-if shot-found and-if
shot-velocity unitize -20 vs* shot-position v+ target!
then
energy armor min 20 > while-loop
return
Notice that a boolean is being used as repair-rate
. Unlike real hardware, Grobots hardware is forgiving about out-of-range arguments, so when repair-rate
is set to 1, it's treated as max-repair-rate
.
One fighter tends to die when facing a group. So let's make the fighters call for help when they see something. When they receive a call, they treat it just like chasing a shot:
#const call-ch 5
do
0 engine-power!
energy 20 > if 57 periodic-robot-sensor and-if robot-found and-if
robot-position 2 call-ch send
robot-position attack^
then
energy armor min 50 > if ;only attack if we're healthy
15 periodic-shot-sensor if shot-found and-if
shot-velocity unitize -20 vs* shot-position v+
2dup 2 call-ch send attack^
then
call-ch receive attack& ifc
else
call-ch clear-messages ;so we don't respond to old calls
then
armor max-armor < energy 10 > and repair-rate!
forever
attack
doesn't change at all.
Places where there's been a recent call for help are often dangerous. It might be useful to make Animals avoid them when wandering.
Animal, like any gatherer, tends to wander near enemies and get killed. We can fix this by making it run away when it sees a shot. This is like chasing shots, but in the opposite direction.
Many sides run to somewhere well-defended (a stationary colony? a group of fighters?) instead of just running away, to avoid running into things. It's also possible to run away when damaged instead of watching for shots.
Animals could also call for help when they see a shot, or get hurt. This is easy to add to their shot-fleeing code.
There are two main uses for forcefields: getting food and getting rid of enemies. Look at Untouchable or Shepherds for examples of both.
Exercise: Make a long-range gunner which uses a forcefield to make the target easier to hit. This has been tried a few times (in Flyswatter and early versions of Untouchable) but never with much success.
The center of the world is a dangerous place to be. Productive therefore goes to the nearest corner at the beginning of the round. Unfortunately it often runs into another side along the way and dies. More cautious hiding could be a large advantage in the early game.
Shepherds runs away as a group, and tends to end up in corners without trying. So does MicroAlgae. Unproductive accelerates this by deliberately moving next to a wall if one is close.
Syphons let you separate energy production from consumption, giving more flexibility in side design. There are several ways to use them. The most popular is to have the energy producers carry the syphon, and hungry cells announce their locations in messages. The syphoners listen and point the syphons wherever they're asked.
Blasters can be hard to use in groups, because it's easy to hit your allies instead of enemies. One way to use them is to organize cells so they're unlikely to be in each other's way. If they avoid each other, or stay in a formation facing the enemy, they won't suffer much from friendly fire. Very short-range blasters avoid the problem by not having enough room for anyone to get in the way.
Noffee has a brute-force solution: it looks at nearby friendly cells and checks to see if they will be in the line of fire. This involves a lot of vector math. If you're willing to be more conservative about when you shoot, you can make this much simpler. Here's a version adapted from Ring of Fire 3:
#var target-distance
#var blast-direction
#vector blast-velocity
shoot:
robot-distance target-distance!
;aim the shot
robot-velocity velocity v- 2dup target-distance blaster-speed / vs* robot-position v+
position dist blaster-speed / vs* robot-position v+
position v- angle blast-direction!
velocity blaster-speed blast-direction polar-to-rect v+ blast-velocity!
;look for friends
0 robot-sensor-sees-enemies!
1 robot-sensor-sees-friends!
target-distance 2 / robot-sensor-focus-distance!
blast-direction robot-sensor-focus-direction!
fire-robot-sensor sync
1 robot-sensor-sees-enemies!
0 robot-sensor-sees-friends!
0 robot-sensor-focus-distance!
;anybody in the way?
robot-found if
do
robot-position position v- 2dup ;vector to this robot
blast-velocity robot-velocity v- unitize dot ;how far along the shot-path
dup radius > swap target-distance < and ;is it between us and the target?
rrot blast-velocity robot-velocity v- unitize cross ;distance from shot-path
abs robot-radius .2 + < and ifr ;...and is it close to the path?
next-robot while-loop
then
;shoot
blast-direction fire-blaster
return
Obviously this takes a large processor and a multiresult sensor. The result is impressive: you can fire through the cracks in a crowd and rarely hit anyone.
Most gatherers waste a good deal of time when two cells are trying to eat the same food. Fool detects when it's been pushed off a food, and looks elsewhere. This is easy to implement.
Frog Celestial 2 has a fancy solution: it keeps a hashtable in shared memory, showing which foods have been claimed already:
#comment Food hashing
Keep a hashtable of position -> (time, x-position) showing who claims this food.
Ignore foods other people have claimed recently.
(x-position is used to confirm this is the right food.)
Collisions are ignored; at worst they lead to pushing matches.
Food hashing was first used in YAR, a not-yet-released side by Warren.
#code
#const hashtable-size 100
#const hashtable-base 1
#const claim-time 240
food-hash: ;(-- hash-index)
food-position + dup floor - epsilon / floor hashtable-size mod hashtable-base +
return
claim-food: ;(-- claimed?)
food-energy 750 > if 1 return then
do
food-velocity or nif
;preload with (time t-addr x-pos x-addr time-threshold t-addr x-pos x-addr)
time food-hash dup hashtable-size + food-position drop swap
time claim-time - 2over over hashtable-size +
sync
read = if
read > if write write 1 return
else 2drop 2drop then
else
2drop write write 1 return
then
then
next-food while-loop
0 return
;Then when you see food, call claim-food before trying to eat:
food-found if claim-food energy 20 < or eat& ifg then
;Eat anyway if we're short on energy.
This is very effective, but complex. In order to update the hashtable atomically, it preloads some data on the stack.
You may want to try using some other method than a shared hashtable. Maybe use messages, and remember only the last few nearby claims. Segregated Eaters keeps track of 10x10 tiles rather than individual foods. This is even more effective, because cells don't waste time wandering past foods that are taken.
Grobots by Devon Schudy and Warren Schudy