Liza Shulyayeva

The Snails Are Eating Again

Back to the brain we go.

I already had snails eating food in jars before, but it was kind of a hack. Each snail would just eat a bite of whatever consumable item was in the jar. When I stupidly decided to try to make a simple snail brain, I knew I’d have to make things like eating fit into it. Today I made a very rough version of something like that.

Each snail has a number of motor neurons in its brain. Out of all the motor neurons, there is one for biting and one for swallowing. So far I am bypassing the swallowing completely and just firing the bite neuron and having the snail bite food and swallow it at the same time. Basically…here’s how it works. This is kind of a brain dump so may not actually make any sense.

Items and snails are now given some properties: luminance, temperature, hardness, roughness. There may be others down the line, but for now that’s it.

I’m also setting some general pain thresholds for the snails (so far all the snails have the same thresholds), as well as totally random default properties (as per above):

    public $luminance = 25;
    public $temperature = 20;
    public $roughness = 5;
    public $hardness = 15;
    public $luminancePainThreshold = 100;
    public $temperaturePainThreshold = 50;

Items are a little more tailored - their individual luminance, temp, etc properties can be set by item template in the db (though right now I’m just using some default values there too).

So basically if a snail sees something with a luminance that’s too high it will not be having such a good time.

Oh, and snails now have a currentMood property. In the memory tables (which are already created but not yet in use) there is a consequenceRating property, which reflects an action’s impact on the snail’s currentMood.

So the main thing you need to bite an item is to actually be close enough to it to touch it. I made a small change to checkSensor() to allow for a maxDistance to be passed:

    public function checkSensor($sensorQual, $maxDistance = 999999) {
    // ...blah blah blah

Also, we set luminance, temp, roughness, and hardness to null by default in the array of detected objects and then specify which attributes to grab for each sensor (because we don’t care about roughness if the snail isn’t touching the thing, for example).

                    $toPush = [
                        'id' => $object->$idString,
                        'idString' => $idString,
                        'inputWeight' => $inputWeight,
                        'distance' => $distance,
                        'luminance' => null,
                        'temperature' => null,
                        'roughness' => null,
                        'hardness' => null
                    ];

So our TactileSensor ends up looking like this:

class TactileSensor extends Neuron
{
    
    public function checkForInput() {
        $maxDistance = 5;
        $detectedArr = $this->checkSensor($this->snail->currentTactileReceptorQual, $maxDistance);
        foreach ($detectedArr as &$detectedObject) {
         //   $controller = null;
            if ($detectedObject['idString'] === 'snailID') {
            //    $controller = $this->snailController;
                $object = $this->snailController->findSnail($detectedObject['id']);
            }
            else if ($detectedObject['idString'] === 'itemID') {
            //    $controller = $this->itemController;
                $object = $this->itemController->findItem($detectedObject['id']);

            }
            $detectedObject['hardness'] = $object->hardness;
            $detectedObject['temperature'] = $object->temperature;
            $detectedObject['roughness'] = $object->roughness;
        }
        $this->detectedArr = $detectedArr;

    }
}

Another change is in setWeights(). Whereas before we’d just add something like 0.05 to the inputWeight if an object had been detected by more than one sensor, now we actually add all the inputs together. If a specific sensor had a maxDistance specified we give it a higher inputWeight by default (2 instead of 1) because it had a tougher criteria to meet (not sure I’ll keep this, since the closest object already gets a boost anyway down the line).

Anyway, then in the decision neuron we decide what to do…right now none of the memory stuff is implemented, so we go straight to a random “curious or not” check:

    public function checkDecision($object, $memories = null) {
        Log::info('checkDecision object: ' , $object);
        if (count($memories) === 0) {
            $rand = Utility::RandomInteger(0,1);
            if ($rand === 1) {
                // Curious. Approach if far away
                if ($object['distance'] >= 5) {
                    Log::info('approaching object1: ' . $object['id'] . ' distance: ' . $object['distance']);
                    $this->approachObject($object);
                }
                else {
                    Log::info('TOUCHING. TRY TO BITE ' . $object['id']);
                    $this->biteObject($object);

                }

            }
            else {
                // Indifferent. Ignore;
            }
        }
    }

approachObject() and biteObject() then fire the biting and moveleft/moveright/moveup/movedown motor neurons.

As you can see it’s still not really that decision-makey. Snails will eventually have other criteria involved in whether they try to bite something or not (that is, they won’t necessarily try to bite an object just because they’re touching it).

That’s it for now…at least they’re eating again.

Websocketing It Up for Snail Movement

Over the past few days I’ve paused work on the brain to tackle snail movement on the client.

I ended up using Latchet, a Laravel-specific extended version of Ratchet. To start with I just wanted to get something running. I think I ended up doing this in a really roundabout way…basically we subscribe to a specific jar topic on the client, then ping the server every 5 seconds to get back the target position of each snail within that jar. The snail then crawls toward this target position. Target pos on the server is calculated using the snail’s currentSpeed and each snail crawls at the same speed in the browser, so in theory by the time the snail reaches its target position on the server it will also have reached the same location on the client…maybe…I hope. We’ll see.

    var jarString = 'jar/' + jarID;

    var conn = new ab.Session(
        'ws://gastropoda.app:1111', // The host (our Latchet WebSocket server) to connect to
        function() { // Once the connection has been established
            console.log('established');
            conn.subscribe(jarString, function(topic, event, jarID) {
                var parsedMsg = JSON.parse(event.msg);
                if (allSnailsArr.length > 0) {
                    for (var i = 0; i < allSnailsArr.length - 1; i++) {
                        if (parsedMsg.snailID === allSnailsArr[i].snailID) {
                            allSnailsArr[i].targetPosX = parsedMsg.targetPosX * jarCapacityWidthPercentage / 100;
                            allSnailsArr[i].targetPosY = parsedMsg.targetPosY * jarCapacityWidthPercentage / 100;
                        }
                    }
                 }
            });

            (function checkPositions() {
                conn.call(jarString, "HI WORLD");
                setTimeout(checkPositions, 5000);
            })();
        },
        
        function() {
            // When the connection is closed
            console.log('WebSocket connection closed');
        },
        
        {
            // Additional parameters, we're ignoring the WAMP sub-protocol for older browsers
            'skipSubprotocolCheck': true
        }
    );
</script>

Then, in JarTopic.php:

    public function subscribe($connection, $topic, $jarID = null)

    {

        $jarController = new JarController();

        $jar = $jarController->findJar($jarID);

        $allSnails = $jar->snails;

        foreach ($allSnails as $snail) {

            array_push($this->connectedSnailsArr, $snail->snailID);

        }

        $this->publish($connection, $topic, "BROADCASTTEST", $exclude = array(), $eligible = array());

    }

    public function call($connection, $id, $topic, array $params)

    {

        $this->broadcastTargetPositions($topic);

    }
    public function broadcastTargetPositions($topic) {

        $snailController = new SnailController();

        foreach ($this->connectedSnailsArr as $id) {

            $snail = $snailController->findSnail($id);

            $msg = json_encode([

                'snailID' => $id,

                'targetPosX' => $snail->targetPosX,

                'targetPosY' => $snail->targetPosY

            ]);

            $this->broadcast($topic, array('msg' => $msg ));

        }

    }

You can’t see it…but they’re moving! When they don’t find anything interesting to approach they stop, and when they sense something they’re curious about they start crawling toward it.

Go your own way

Braaaaaainsss

I appear to have gotten myself into a bit of a mess.

“Make a snail brain/nerve-cluster for your snails,” they said. “It’ll be easy”, they said.

No. Nobody said that. Who would say that?! I got into this mess on my own and now I don’t know if I can dig myself out. I’ve already spent so much effort on this brain thing that it’s too late to turn back.

And it’s not all bad - it’s coming along, and it’s fun to think about this stuff. The thing is, I have no idea what I’m doing. I’m just making up how I think a snail brain should work. I’m afraid at some point, probably when I’m very far in, I will realize that I’m doing everything all wrong and have to start from scratch.

Snails have a very primitive brain. They are capable of associative learning. This is what I want to simulate.

A couple of nights ago I talked through the parts of the brain I have so far with David pretending to listen. It helped a lot, since when I got back to snailing after js13kGames I had forgotten where I left off or wtf any of what I’d written meant. Now, as a refresher, I’m going to go over it again here.

Typing this up I can already see a bunch of stuff I need to fix, or scrap completely. I guess doing this does help.

The brain

Each snail has a brain:

    protected function getBrainAttribute() {
        $brain = new Brain($this);
        return $brain;
    }

In the brain constructor, we tell the brain which snail it belongs to (since it is not a Model we can’t use the usually handy belongsTo relationship). We also create the olfactory, optical, tactile, and gustatory sensors and what I’m calling the attention and recognition neurons.

    function __construct($snail) {
        $this->snail = $snail;
        $this->olfactorySensor = new OlfactorySensor($snail);
        $this->opticalSensor = new OpticalSensor($snail);
        $this->tactileSensor = new TactileSensor($snail);
        $this->gustatorySensor = new GustatorySensor($snail);
        $this->attentionNeuron = new AttentionNeuron($snail);
        $this->recognitionNeuron = new RecognitionNeuron();
    }

Each sensor is a child class of Neuron.

Every minute a job that checks for recurring events runs. One of these recurring events runs checkAllInputs() within the brain.

    public function checkAllInputs() {
        $this->olfactorySensor->checkForInput();
        $this->opticalSensor->checkForInput();
        $this->tactileSensor->checkForInput();
        $this->gustatorySensor->checkForInput();
        $this->setWeights();
    }

The checkForInput() function then does this. We pass the snail’s current olfactory receptor quality into the checkSensor function.

class OlfactorySensor extends Neuron
{
    
    public function checkForInput() {
        $detectedArr = $this->checkSensor($this->snail->currentOlfactoryReceptorQual);
        $this->detectedArr = $detectedArr;
    }

}

We check the sensor like this in the main Neuron class.

    public function checkSensor($sensorQual) {
        // Create a new jar controller and get all objects inside the jar that the snail is in.
        $jarController = new JarController();
        $allObjectsInJar = $jarController->getAllObjectsInJar($this->snail->jar->jarID);
        
        // Create a blank array of detected objects.
        $detectedArr = [];

        // Set point1 to the snail's current position from the db.
        $point1 = [
            'x' => $this->snail->posX,
            'y' => $this->snail->posY
        ];

        // Loop through each object in the jar and get its position.
        foreach ($allObjectsInJar as $object) {
            // Set point2 to object's current position from db.
            $point2 = [
                'x' => $object->posX,
                'y' => $object->posY
            ];    

            // Get the distance between point1 (the snail) and point2 (the object).
            $distance = Utility::getDistance($point1, $point2); // in cm

            // If the distance is less than or equal to the sensor quality, object is detected 
            if ($distance <= $sensorQual) {

                // What KIND of an object is it? Another snail? Item? 
                // Because each of these have their own tables in the DB and their own primary key columns we need to get this information. 
                // We do this by using the name of the object class and appending "ID" to it. 
                // This is the primary key column name.
                $idString = strtolower(get_class($object)) . 'ID';
                // If the ID of the object === ID of the snail that has detected it,
                // continue through that iteration of the loop and do nothing futher, since we don't need the snail to detect itself.
                if ($object->$idString === $this->snail->snailID) {
                    continue;
                }

                // Otherwise, create an array with the details of the object we're detected. 
                $toPush = [
                    'id' => $object->$idString,
                    'idString' => $idString,
                    'inputWeight' => 1,
                    'distance' => $distance
                ];
                array_push($detectedArr, $toPush);
            }
        }
        return $detectedArr;
    }    

The next step in the checkAllInputs() function is setWeights(). This is where all kinds of screwed up stuff happens:

    protected function setWeights() {
        // Create an array of all sensors we care about
        $allSensors = [
            'olfactory' => $this->snail->currentOlfactoryReceptorQual,
            'optical' => $this->snail->currentOpticalReceptorQual,
            'tactile' => $this->snail->currentTactileReceptorQual,
            'gustatory' => $this->snail->currentGustatoryReceptorQual
        ];

        // Create blank array of detected IDs
        $allDetectedIDs = [];

        // Get index of highest value in array and value itself
        $bestReceptors = Utility::doubleMax($allSensors);

        // Best receptor name is receptor + Sensor. 
        // Note: why not just name them with Sensor already appended in allSensors array?
        $bestReceptorName = $bestReceptors['i'] . 'Sensor';
        Log::info('best receptor array: ' , $this->$bestReceptorName->detectedArr);
        // Loop through all objects detected by best receptor by reference and add 0.5 to their input weight
        foreach ($this->$bestReceptorName->detectedArr as &$object) {
            $object['inputWeight'] += 0.5;
        }

        // Merge objects detected by all sensors into one array
        $allDetected = array_merge($this->olfactorySensor->detectedArr, $this->opticalSensor->detectedArr, $this->tactileSensor->detectedArr, $this->gustatorySensor->detectedArr);
        
        // Create blank array for deduped detected object list
        $allDetectedDeDuped = [];
        // Count occurrences of idential ID in array 
        $countedArr = array_count_values(array_map(function($value){return $value['id'];}, $allDetected));
        // Loop through array of counted values
        foreach ($countedArr as $objectCount) {
            // Loop through all detected objects
            foreach ($allDetected as &$object) {
                // Add 0.1 for each occurrence (so if object is detected by multiple sensors, its inputWeight is higher)
                $object['inputWeight'] += 0.1 * $objectCount;
                
                $count = 0;
                // Loop through all detected deduped array
                foreach ($allDetectedDeDuped as $dedupedDetectedObject) {
                    // If object id already exists, add to count
                    if ($object['id'] === $dedupedDetectedObject['id']) {
                        $count++;
                    }
                }
                // If count is 0/object isn't already in there
                if ($count === 0) {
                    // Push to array of all detected deduped
                    array_push($allDetectedDeDuped, $object);
                }
            }
        }

        /**** whaaaaaaaattttttt??! *****/


        // Get closest object
        $closestObject = null;

        // Loop through all detected deduped array
        foreach ($allDetectedDeDuped as $object) {
            // If closest object hasn't yet been set...
            if ($closestObject === null) {
                // Set this object as closest object
                $closestObject = $object;
            }

            // If distance to this object is less than distance to current closest object...
            if ($object['distance'] < $closestObject['distance']) {
                $closestObject = $object;
            }
        }

        // Loop through deduped...wait again?
        foreach($allDetectedDeDuped as &$object) {
            // If this is the closest object, add to inputWeight
            if ($object['id'] === $closestObject['id']) {
                $object['inputWeight'] += 0.05;
            }

            // Create new synapse for this impulse
            $synapse = new Synapse($object['inputWeight'], $this->attentionNeuron, $object);

        }

        // Pick object to focus on
        $this->attentionNeuron->chooseFocus();
    }


And here is what we have in the Synapse constructor:

    function __construct($inputWeight, $destinationNeuron, $object = null ) {
        $this->inputWeight = $inputWeight;
        $this->destinationNeuron = $destinationNeuron;
        $this->targetObjectID = $object['id'];
        $this->targetObjectClass = $object['idString'];
        $this->targetObjectInputWeight = $object['inputWeight'];
        $this->sendImpulse();
    }

sendImpulse():

    private function sendImpulse() {
        $this->destinationNeuron->receiveImpulse($this);
    }

In this case the destination neuron is the attention neuron.

    public function receiveImpulse($impulse) {
        $object = [
            'id' => $impulse->targetObjectID,
            'idField' => $impulse->targetObjectClass,
            'inputWeight' => $impulse->targetObjectInputWeight
        ];
        array_push($this->allObjects, $object);
    }

After all this is done, and the attention neuron has a list of objects with their IDs, ID column name, and inputWeight, we try and pick which object the snail will focus on.

    function chooseFocus() {
        $focusedObject = null;
        foreach ($this->allObjects as $object) {
            // If there is no focused object, set the first one to focused
            if ($focusedObject === null) {
                $focusedObject = $object;
            }
            // If the focused object inputWeight < current object inputWeight...
            if ($focusedObject['inputWeight'] < $object['inputWeight']) {
                // Set current object as focused
                $focusedObject = $object;
            }
        }
        // Focus chosen, send to recognition neuron
        $brain = $this->snail->brain;
         $brain->recognitionNeuron->checkRecognition($focusedObject);
    }
}

And that’s sort of where I left off on the brain side so far:

class RecognitionNeuron extends Neuron
{

    function checkRecognition($object) {
        Log::info('checking if object id ' . $object['id'] . ' is recognized');
    }

}

The memory tables

Right now I am preparing the memory tables. There will be 3:

  • memories_sensory - for sensory memories, kept for just a few minutes
  • memories_st - for short term memories. If an object collects n sensory memories it will be stored in short term memory
  • memores_lt - for long term memories. This never gets deleted, but memories get weaker as they get older (but they can be refreshed)

So far I only have the sensory memory table, which currently consists of the following columns:

  • memoryID
  • snailID
  • objectID
  • objectIDField
  • objectProximity
  • neuronFiredID
  • consequenceRating

I also have a table of motor neurons, which is what I’ll specify in neuronFiredID. It is basically the action the snail took in relation to the memory. So far it just consists of two columns:

  • neuronID
  • name

And the motor neurons in there so far are:

  • move_left
  • move_right
  • move_up
  • move_down
  • bite
  • mate
  • touch
  • swallow

The memory models

I’ve also created a memory model class, with child classes for sensory, short term, and long term memories. Each memory has a belongsTo Eloquent relationship with a snail. A snail hasMany memories. And of course there is a MemoryController. This is pretty much where I left off.

Like I said - I have a feeling I’m going to look at this in the morning and decide that it’s all wrong and needs to be scrapped. But I guess even if that’s the case, it will be a lesson learned and the next version will be better.

And We’re Back

I took a one-month break from Gastropoda to participate in the JS13kGames challenge with a tiny game called Breaking Brad

But now it’s back to simulations! It’s taking me a while to remember where exactly I left off with Gastropoda last month, but now I think it’s time to figure out how to calculate a snail’s position within a jar. This is important because each snail will detect other objects (items, snails, etc) differently based on its senses. I have to have an actual location for both objects and snails. Right now I’m setting a position randomly when a snail is hatched in a jar, but I have to decide how I’m going to update this position as it crawls. Currently snails crawl at random on HTML5 canvas - they are already starting their crawl from their DB position, but pick direction randomly and then move back and forth across the screen using their speed attribute. So far all I have are some random notes:

  • User actions while watching rendered snails (ie drag and drop, etc) have to influence thes snail’s position in the db, but only the jar’s owner can move snails around.
  • Do I update the general crawl position on the client and then send that data to the server every x seconds? This wouldn’t really work because more than one user can be watching a snail at any one time, so if we take the random turn direction etc and record it, you will get all kinds of dodgy results in the db. Also I’d then need a separate solution for snail movement when the snails are not being rendered at all. Snails do things/move continuously - in this case a tree that falls in a forest does make a sound even if no one is there to hear it. I think I have to set direction or target position on the server and then have the client reflect this if a user is watching a jar, not the other way around.
  • How do I make this scalable to potentially hundreds/thousands of snails?

Today I’m sick and not thinking straight, so I’m not actually going to even try implementing anything at this point. Back to hot tea for me :(

Pathfinder: Two Heroes Are Born

It was a chilly summer evening when Valeros the valiant fighter and Merisiel the super cool rogue walked through the gates of a tiny abandoned farmhouse. They were heroes, you see, and here to rescue the people of the land from a gang of notorious thieving wanderers - the Sczarni - who have been terrorizing Sandpoint under the leadership of a punk named Jubrayl Vhiski.

When the townsfolk put out an ad for two brave heroes to take care of the problem Valeros and Merisiel felt it was unnecessary to mention their lack of experience in adventuring. But now the creepy creaking gate and the menacing-sounding wind was making Merisiel second-guess their decision.

But they pressed on. Merisiel had her dagger and darts at the ready and Valeros had his…some other weapon…also at the ready as they tiptoed down the path to the house. Suddenly, a Mercenary jumped out of the nearby apple tree and kicked Merisiel to the ground! She recovered and threw a dart at his face. It missed, so she threw it away and tried again. Got him! Merisiel kept his eye as a souvenir - and his elite throwing axe. Unfortunately she wasn’t that great with proper weapons, so as she practice-swung the axe around in victory it nicked the side of her thigh. Worth it, though.

Valeros was the image of heroism as he, emboldened by their defeat of the Mercenary, kicked open the door of the farmhouse and strolled inside. That’s when the siren leaped out at his face from the rafters. He slashed it with his short sword, but she swiped one wing into his head and Valeros dropped to the floor, dead.

Merisiel was alone. “I will bring you back, Valeros!” she yelled as the ran away from the farmhouse very quickly. She ran to exhaustion and found herself at a wooden bridge, where a henchman waiting for her! She threw the axe at it, dislocating her shoulder in the process. But it worked. The henchman perished and she secured the entire location. She fought monsters across the land, hoping to stumble into anything that could bring Valeros back to life. She journeyed from the bridge to the waterfront and then to the woods with no luck. Close to death, she managed to barely escape a Giant Gecko when suddenly she realized…He’s not dead! He’s just unconscious!

She ran over to the farmhouse to wake up Valeros, who was very glad to find out that he wasn’t dead after all and just napping for seven hours or so while Merisiel was beating up monsters and henchmen on his behalf with her massive axe. Together they stole a couple of farm horses and rode over to the waterfront, where Jubrayl Vhiski himself was waiting for them! Valeros, well rested from his long nap, took on the gang leader. Unfortunately, he got away. The gang leader, not Valeros. However, at least they got to secure another location.

“We’ve got to split up,” said Merisiel. “We have two locations left - the farmhouse and the woods. If one of us runs into Vhiski again, the other can secure the remaining location.”

It was decided that Merisiel would take the woods (since she’s an elf and all) and Valeros would go back and finish clearing the farmhouse. Vhiski had to be hiding in one of those places.

Except it just so happened that the siren wasn’t done with Valeros. She lunged at him again when he arrived at the house. Only wisdom could defeat her and neither Merisiel nor Valeros had much of that going for them (though let it be known that Merisiel had a little more). Merisiel helped Valeros escape from afar, but the siren herself was not defeated. She was still out there…hiding.

“No way, man.” said Valeros. “I’m not fighting that thing again. She’ll kill me!”

“Fine, let’s trade places.” Merisiel relocated to the farmhouse and Valeros took over the forest. Suddenly, a bandit henchman (or something) leapt out at Merisiel from the tall grass of a pasture. She threw a dagger through his heart and then, as a result, managed to close the location. Yet as she was boarding up the doors and windows of the farmhouse she spotted Vhiski himself hiding in a corner. She decided not to let her presence be known just yet and communicated with Valeros (who was in the woods) via carrier pigeon.

“Enemy spotted. He’s strong. I think I can take him, but you may be better with your superhuman strength.” she wrote.

They did some quick math and confirmed that Valeros, with his mighty strength and stuff, did indeed have a better chance of defeating the villain. Merisiel could provide some healing assistance if required. They formulated their cunning plan. It went like this:

Merisiel and Valeros quietly switched locations again - Merisiel retreating to the woods and Valeros sneaking into the farmhouse. Valeros pounced out at Jubrayl Vhiski, charging at him with all his might. A very fast carrier pigeon sent word to Merisiel that the battle was beginning. She, in the meantime, was trying to figure out how to temporarily close the woods to ensure Jubrayl couldn’t escape there when Valeros defeated them.

“But oh no!” she exclaimed. “It requires wisdom! We are toast. Even if Valeros manages to defeat Jubrayl now, he’ll just escape into the woods and we’ll have to wade through all these hostile creatures. We’ll be dead before we get to him!”

They should probably have taken the time to figure this out before putting their plan in action, but as we’ve stablished - not much wisdom on either end.

Pathfinder: wisdom roll Merisiel had one shot at solving a seemingly impossible puzzle. All she could do was guess. Out of six choices, only one was correct. There was zero room for error. Sweating profusely, she guessed…”Six!

The monsters of the forest cried out in horror and retreated to their dens. The forest closed in on itself. Success!

In the meantime, Valeros was fighting an epic battle with a gang leader who was beating him with a club.

“You can do it, Valeros!”

And he did! With a final good kick to the head, Jubrayl was knocked unconscious (permanently). Valeros looked to the sky at just the right angle for sunlight to shine on his chiseled features.

And so, Sandpoint was freed from the clutches of an evil villain.

“We did it!” Merisiel punched the air victoriously, nearly stabbing Valeros in the face with her dagger. “Let’s do that again!”

They started dividing their loot, preparing for their next (big) adventure.

It was decided that Merisiel should probably hand over the axe to Valeros, who was able to use it without injuring himself. Valeros then passed out, clearly requiring another nap (seriously, he’s snoring right now).

Wolfling Runs

Wolfling run: “a method of playing the Creatures games in which the players do not intervene in the lives of their norns.” Creatures wiki.

Here is one of the original detailed explanations - Wolfling Run FAQ

I’ve mentioned implementing a way to do wolfling runs in Gastropoda a few times, but then realized that it’s not exactly a commonly known term (at least for people who’ve never played Creatures). I only recently learned that the term is actually based on David Brin’s Uplift novels.

Most of humanity believes itself to be a wolfling species that emerged into sapiency solely through natural evolution, without genetic manipulation of a patron species. Uplift Universe wiki

There are different kinds of wolfling runs, but basically when I played Creatures 3 I mostly ran relaxed wolfling runs. I’d set up the world, then leave my norns for a few hours to breed on their own. It was always really interesting to see which ‘lines’ survived. I even made up stories for them, documenting each norn’s life and breeding patterns in a notebook. At the end of the wolfling run I’d crown an alpha male and female - those who spread their genes the most during the run.

Anyway, I want to set up something similar for the snails. I haven’t yet decided if this will be the default state for the breeding jars…I’ll either just have a Wolfling Run checkbox (or item) that makes the jar disregard capacity and let the snails breed wild. At the moment, breeding events are only triggered when you move a snail into the breeding jar. In reality, I suppose snails would periodically try to mate whenever they’re in the breeding jar. I’d have to get the balance right, make it realistic…they can’t just be breeding like rabbits. The other alternative is requiring the user to get a really high capacity breeding jar. These would be very expensive in terms of the virtual currency, but would be perfect for wolfling runs.

I might start on this now and then move on to race actions.

On Snail Eggs

Even though I’ve had a functional breeding system in Gastropoda for a while, it still isn’t actually done. Most common snails do not give birth to live young…and definitely not to one live young. Initially it was good to just have the snails pop out one live baby snail so that I could properly observe and test the genetics and other attributes generated for the newborn. Now, however, with that being well on track, it is time to implement…egg laying.

The requirements were this:

  • Quantity of eggs is based in part on the snail’s fertility attribute. Environmental and health factors will eventually come into effect.
  • The snail now has to be pregnant for some time before actually laying the eggs. Eggs have to stay in the jar for some time before they hatch into baby snails.
  • The baby snails are pregenerated at the time of the mating, not on egg hatching. This is because eventually environmental factors and the parents’ health may have some effect here as well. I can easily modify additional attributes before hatching if needed, but the main attributes of the newborn snails must be set based on the state the parents were in when they actually mated.
  • Snails can’t be born at their full size - they have to be born tiny and grow based on a maturity rate attribute.

At first I considered having a separate table for eggs, but then I decided not to complicate things. I added an eggLaid attribute to the snail table. By default eggLaid is set to false, so at the time of conception the snail-to-be exists but it doesn’t appear as an egg. The birthDate is NULL at this time. In the jar we can see the eggs displayed on top of the pregnant snail’s shell like so:

Pregnant snail

(The number of circles on the shell doesn’t represent how many eggs will actually be laid. In the future I’ll make this much more subtle, the quantity will be indistinguishable).

A LayEggs event is created in the db. Currently it’s always scheduled for exactly an hour after mating. When this event runs eggLaid is set to true in the snails table for those snails and they now appear as eggs (you can see an example of one of the eggs in the jar above). The color of the egg represents (to an extent) the shell color of the newborn, though without the pattern overlay it’s impossible to tell what the final snail will look like at this time. For me that’s pretty exciting, because I can come back in a few hours to start seeing what kind of shell pattern and eye color my baby snail is developing.

When the eggs are laid, a HatchEggs event is created. Eggs will hatch roughly 24 hours after they’re laid, but this will eventually have a tiny degree of randomization as well as be affected by factors like temperature and subtrate within the jar.

When the eggs are hatched, the snail is so tiny that you can barely see it. From there every hour the scale of the snail is increased based on the maturity rate attribute. Eventually the snail grows to its full size!

Up next I have to:

  • Make sure that snails under 3 days of age do not count toward the jar capacity quota
  • Make sure that snails can’t be picked for breeding until they’re fully mature
  • Implement aggression - baby snails are vulnerable and can be eaten by other snails

New Snail Shell Patterns

This post originally started out like this:

I’ve made a few more shell patterns for the snails!

I then started to descibe how the basic genetics work in the simulation (limited to pattern color). While doing so, I began to realize that it is all wrong. I went back and ripped apart some of the breeding system to get it to a slightly more realistic state. Needless to say it’s still not really correct, but at least it’s a bit closer to what could be some sort of reality. Now I have to go back and clean everything up/rewrite it.

I’ll keep the explanation to pattern color, but there is also pattern shape, shell color, eye color, and shell radius genetic traits (and more background genetic traits that don’t actually translate to anything visually on the snail).

(Sidenote: I’m already noticing bugs with this as I type this…and filing them on the Trello board…this obviously has a long way to go)

For randomly generated wild snails

When generating a random snail, random integers between 0 and 255 are chosen for the pattern color.

    $this->patternColorR = Utility::randomInteger(0,255);
    $this->patternColorG = Utility::randomInteger(0,255);
    $this->patternColorB = Utility::randomInteger(0,255);

When this and the other genetic traits have been randomly generated, generateGeneString() is run.

    $arrayOfColors = [
        'r' => $this->patternColorR, 
        'g' => $this->patternColorG, 
        'b' => $this->patternColorB
    ];
    $this->patternColorGeneAllele1 = strtoupper($this->findDominantColor($arrayOfColors));
    $this->patternColorGeneAllele2 = $this->pickRandomRecessiveColorAllele($this->patternColorGeneAllele1);

Why convert the allele to upper case instead of just naming the keys in upper case? I preferred to do it this way because I want it to be clear that the keys in $arrayOfColors do not represent alleles. These refer to color amounts themselves. Alleles are in upper case.

So, to get the first allele in the randomly generated snails we find the dominant color in arrayOfColors and convert it to an allele (in upper case): $this->patternColorGeneAllele1 = strtoupper($this->findDominantColor($arrayOfColors));

When we know the first allele, we need to generate a second one (which the first is dominant over) to complete the desired genotype via $this->patternColorGeneAllele2 = $this->pickRandomRecessiveColorAllele($this->patternColorGeneAllele1);

In pickRandomRecessiveColorAllele there is this:

        $possibleAllelesArray = [];
        switch (true) {
            case ($dominantAllele === 'R'):
                $possibleAllelesArray = ['R','G','B'];
                break;
            case ($dominantAllele === 'G'):
                $possibleAllelesArray = ['G','B'];
                break;
            case ($dominantAllele === 'B'):
                $possibleAllelesArray = ['B'];
                break;
        }
        $allele = Utility::randomInteger(0,count($possibleAllelesArray)-1);
        return $possibleAllelesArray[$allele];
  • Red is always dominant over Green and Blue, so if there is one R allele it doesn’t matter what the second one is.
  • Green is dominant over Blue, so if the dominant allele should be G the only possible recessive alleles are G and B
  • Blue is always recessive, so if we want B to be the dominant allele the only possible second allele is B.

For newborn snails with two parents

When generating a newborn snail and not just a random snail the process is kind of reversed. We get the alleles first, then the colors themselves:

    $this->patternColorGeneAllele1 = $this->generateAlleles($stag->patternColorGeneAllele1, $stag->patternColorGeneAllele2);
    $this->patternColorGeneAllele2 = $this->generateAlleles($doe->patternColorGeneAllele1, $doe->patternColorGeneAllele2);
    protected function generateAlleles($traitAllele1, $traitAllele2) {
        // Make an array of the two possibilities
        $possibilities = [$traitAllele1, $traitAllele2]; 
        // Pick one of the elements in the array at random
        $rand = Utility::randomInteger(0,1);
        return $possibilities[$rand];
    }

We then make an array of each of the parent snails’ pattern colors and run $patternColor = $this->generateVisibleTraits('patternColor', $stagPatternColor, $doePatternColor);

In generateVisibleTraits() we get the alleles for the trait in the form of an array.

    $alleles = $this->getAllele($trait); // Array (2)
    if ($trait !== 'patternShape' && $trait !== 'patternRadius') {
        $a1 = strtoupper($alleles['allele1']);
        $a2 = strtoupper($alleles['allele2']);
        switch (true) {
            // If both alleles are identical use both stag and doe traits 
            case ($a1 === 'R' && $a2 === 'R') || ($a1 === 'G' && $a2 === 'G') || ($a1 === 'B' && $a2 === 'B'):
                return $this->pickColor($a1, $stagTrait, $doeTrait);

            // If either allele is R (for red), set it as the color - Red is always dominant
            case $a1 === 'R':
                return $this->pickColor('R', $stagTrait);
            case $a2 === 'R':
                return $this->pickColor('R', $doeTrait);
            // Else if either allele is G (for green), set it as the color - Green is second dominant
            case $a1 === 'G':
                return $this->pickColor('G', $stagTrait);
            case $a2 === 'G':
                return $this->pickColor('G', $doeTrait);
            // Else if either allele is B (for blue), set it as the color - Blue is always submissive
            case $a1 === 'B':
                return $this->pickColor('B', $stagTrait);
            case $a2 === 'B':
                return $this->pickColor('B', $doeTrait);
        }
    }

In pickColor() you can optionally pass up to two parentTrait arguments. In some cases you don’t need to, but for patternColor there should always be at least one. If there are two, we assume incomplete dominance and return a mix of both parents’ dominant color.

    protected function pickColor($allele, $parentTrait1 = null, $parentTrait2 = null) {
        // RGB min & max values
        $minValue = 0;
        $maxValue = 255;
        $posKeys = ['R','G','B'];
        $color = strtolower($posKeys[array_search($allele, $posKeys)]);

        if (isset($parentTrait1) && isset($parentTrait2)) {
            $minValue = $parentTrait2[$color];
            $maxValue = $parentTrait1[$color];
        }
        else if (isset($parentTrait1)) {
            $minValue = $parentTrait1[$color] - 15;
            $maxValue = $parentTrait1[$color] + 15;
        }

        $randomColorAttributes = "";

        foreach ($posKeys as $key) {
            if ($key === $allele) {
                $min = $minValue;
                $max = $maxValue;
            }
            else {
                $min = 0;
                $max = $minValue;
            }
            $randomColorAttributes .= $min . ',' . $max;
            if ($key !== 'B') {
                $randomColorAttributes .= ',';
            }
        }
        unset($key);
        $randColor = call_user_func_array("Utility::randomColor", str_getcsv($randomColorAttributes));
        return $randColor;
    }

Breeding example

Here’s an example of a breeding. In the simulation I refer to a snail that bred as a male as a “stag”, female as “doe”, baby snail as “foal”.

In this case Ice (#327) and Fire (#329) were placed in the breeding jar. They were the only two snails in the jar to make sure they bred with each other.

Stag: Ice (#327)

Ice snail
Bred as a male
Shell color genotype: GB
Eye color genotype: RB
Pattern color genotype: BB
Pattern shape genotype: FE
Pattern radius genotype BS

Doe: Fire (#329)

Fire snail
Bred as a female
Shell color genotype: RG
Eye color genotype: GB
Pattern color genotype: RR
Pattern shape genotype: EE
Pattern radius genotype BS

Foal: Unnamed (#330)

Foal snail
Shell color genotype: BG (G > B)
Eye color genotype: RG (R > G)
Pattern color genotype: BR (R > B)
Pattern shape genotype: EE
Pattern radius genotype SS

But the new pattern shapes!

Here are some screenshots of various pattern shape/color combinations in the simulation today. These are still placeholders and will change. I don’t like all the super obvious tacky gradients. But it kind of gives me more variety to work with. There are currently 6 pattern options (one of which is lack of pattern)

Snail patternSnail patternSnail patternSnail patternSnail patternSnail patternSnail patternSnail patternSnail patternSnail patternSnail patternSnail patternSnail patternSnail patternSnail patternSnail patternSnail patternSnail pattern

Laravel Migration Done

The first commit auto-tweeted from the @gastropoda_ twitter account yesterday was actually the last commit dedicated to the Laravel port reimplementation:

Yeah, that one. It contained basically some racing fixes and the reimplementation of recurring events (snail idle actions, store restocking, nightly snail energy depletion), the corner store, and user inventory.

Laravel - you have arrivedI started the port on July 10, so accounting for 2 days away in Amsterdam where no snail work got done, overall it took 16 days to migrate everything to Laravel (between about 1-4 hours of work per day).

From now on any further work on Gastropoda will move past the functionality of the vanilla PHP version. And I’ve already started! I’ve implemented rudimentary race logs and action templates (right now the only action template is ‘move’, but there will be lots of other actions a snail can perform during a race).

The Gastropoda board on Trello has been helping me keep track of my progress and it’s getting fuller by the day. If you haven’t checked out Trello yet, you should. Here is a non-referral link or, if you’re feeling nice, a referral link.

Up next I’ll be working on whatever the crap I want. More specifically, I might start with adding a couple more action templates for the races. Then I might switch to something totally different and look into generating better shell patterns and colors on the snails. The snails are actually displayed on HTML5 canvas (since they crawl), so all of that is done in JS/HTML5. I bet I can get some really cool and more natural looking visual effects in place of the placeholder patterns I have now. I also still need to capture a static snail image and store it in the DB for cases where a single snail avatar needs to be displayed as a simple image only.

A Battle With Racing

I’m still on the Laravel migration. It’s going pretty well, except for last night. Last night I was porting racing.

It was awful.

I didn’t get to bed until something like 4am and my brain seems to have blocked out a sizeable chunk of what actually happened from memory, but during the course of the afternoon I went through problem after problem. Each time I solved one thing some other functionality broke and by the end I didn’t know wtf was going on.

The problem started with my fundamental misunderstanding of the Eloquent update(). After reading the Laravel documentation I got the impression that update’s entire purpose is updating specific attributes of a model. You pass an array of attributes, Eloquent updates the table.

Apparently that’s not how it works. When you run Model::update($propertiesToUpdate) it doesn’t just update those properties - it looks for any dirtying of the model and apparently tries to save everything that may have changed. Unfortunately in my current setup the model also represents a complete instance of a snail that has additional properties set after it is loaded. So as an example…snail->currentEnergy is not stored in the database, but calculated when the model is loaded based on the snail’s current protein, fat, and carb counts. When I tried making neccessary changes to the racing snail post-race to record depletion of these macros I was getting an error about a column not existing. But it wasn’t supposed to exist at all.

I went to Laravel IRC for help. People were very helpful and responsive. It took a while of going in circles. I was getting a little frustrated with the whole situation because it cannot be that difficult to save a fricking change to some specific fricking fields and only those fields in the db through Eloquent. I would have thought it would be at least as simple a process as manually building a query and running mysqli was. In the end we decided that using the query builder was my best bet. So now instead of using $snail->update($propertiesToUpdate) I use DB::table('snails')->where('snailID', $this->snailID)->update($propertiesToUpdate)

This works. I do eventually want to go back and change my entire setup to avoid dirtying the model. For now, though, I just want to get this thing back to the state it was in the vanilla PHP version.

Anyway, then there were some other issues with Eloquent relationships, but those were caused by sleepiness more so than any actual bug or source of confusion. Here’s an example of how my Laravel relationships are set up for races and their entries at the moment:

  • Race model hasMany entries
  • RaceEntry model hasOne snail

This makes it easy to immediately load the entries associated with each race as well as the snails associated with each entry for use during the race. In a similar way, I can easily get a snail’s owner via a belongsTo relationship. From there it’s just a matter of Eager loading the model associated with the relationship when loading the parent model itself. Or lazy loading it via $race->load('entries') for example.

Anyway, the races are running now. They’re not done, but they’re now up to the same state as the vanilla PHP version. Up next I want to do a bit of cleanup and then start reimplementing items. This port is taking longer than I thought, but I can see the benefits of using Laravel. I’m finding myself cutting out huge chunks of code I had in the vanilla version that, with the help of Laravel and Eloquent, are just no longer needed. Eloquent makes data retrieval and validation much easier and faster to implement.

Up next I want to do some cleanup and then start on the item reimplementation. Wish me luck.