Mouth and Body Automata

Fri May 02 2025 15:06:07 GMT+0100 (British Summer Time) aiautomatajavascripthtmlarchived

Mouth, Body setup

Mouth & Body screenshot

As another variation on the simple cellular automata we've been looking at, we'll now be looking at an example which applies the following rules:

  • Each cell has a random body & mouth colour
  • If a neighbouring cell has a body the same colour as this cells mouth, it can be consumed and replaced with a cell like this one
  • If the cell hasn't eaten for more than 3 updates, randomly change its mouth colour each turn until it can
  • If the cells mouth colour is the same as its body colour, change its body colour

This causes the cells to not allow a single colour to dominate, as this results in random mutation in cells which have exhausted their food source.

Mouth & Body Cellular Automata example

cell.js

We'll run through the cell class file here, and the complete source code can be downloaded from the link by this article. For more information, please have a look at the previous article "Rock, Paper, Scissors".

As before, we'll have a list of active cells (allCells), and cells that will be added at the end of the current update (newCells). We'll also allow directional biasing (biasDir) to allow cells to prioritize consuming other cells in a clockwise direction from North to West. Additionally, we create an array (cellTypes), that simply stores a list of colours that can be chosen for each cells body and mouth.


var allCells = [];
var newCells = [];

var biasDir    = false;

var cellTypes = [
    "#cc0000", "#cccc00", "#00cc00", "#00cccc", "#0000cc", "#cc00cc"
];

In the cell class itself, we start with a constructor to keep track of the cells position, its mouth and body colours, whether or not it's alive, and how many updates the cell has gone without eating (starved):


class Cell
{
    constructor(cx, cy, cm, cb)
    {
        this.x        = cx;
        this.y        = cy;
        this.tile    = (cy * Map.width) + cx;
        this.mouth    = cm;
        this.body    = cb;
        this.alive    = true;
        
        this.starved    = 0;
        
        newCells.push(this);
        Map.tiles[this.tile].cell = this;
    }

As in the Rock, Paper, Scissors example we have a method to fetch a list of valid neighbouring tiles of this cell, and a method to kill the cell when it is consumed:


    getNeighbours()
    {
        var n = [];
        
        if(this.y > 0) { n.push( ((this.y - 1) * Map.width) + this.x ); }
        if(this.x > 0) { n.push( (this.y * Map.width) + this.x - 1); }
        if(this.y < (Map.height - 1)) { n.push( ((this.y + 1) * Map.width) + this.x ); }
        if(this.x < (Map.width - 1)) { n.push( (this.y * Map.width) + this.x + 1); }
        
        return n;
    }
    
    kill()
    {
        this.alive = false;
        Map.tiles[this.tile].cell = null;
    }

Our update method for cells works by fetching a list of neighbouring tiles for the cell, and then looking for cells that have the same body colour as our current cells mouth colour. If such a cell is found, it is either consumed immediately or added to a list of potential prey from which one entry is selected at random, depending on whether or not directional biasing is on.


    update()
    {
        var n = this.getNeighbours();
        var ate = false;
        
        var prey = [];
        
        for(var x in n)
        {
            if(Map.tiles[n[x]].cell!=null && Map.tiles[n[x]].cell.body == this.mouth)
            {
                if(biasDir)
                {
                    Map.tiles[n[x]].cell.kill();
                    new Cell(Map.tiles[n[x]].x, Map.tiles[n[x]].y, this.mouth, this.body);
                    
                    ate = true;
                    break;
                }
                else { prey.push(n[x]); }
            }
        }
        
        if(prey.length)
        {
            var p = prey[Math.floor(Math.random() * prey.length)];
            
            Map.tiles[p].cell.kill();
            new Cell(Map.tiles[p].x, Map.tiles[p].y, this.mouth, this.body);
            
            ate = true;
        }

If the cell has eaten, we reset the starved counter; otherwise we increment it by 1:


        if(ate)
        {
            this.starved = 0;
            return true;
        }
        else { this.starved++; }

If the cell hasn't eaten for 3+ turns, we'll randomly select a new mouth colour for it. We can then check if the mouth and body colour are the same, in which case we'll change the cells body colour by advancing it to the next entry in the colours list:


        if(this.starved > 3) { this.mouth = Math.floor(Math.random() * cellTypes.length); }
        
        if(this.mouth==this.body) { this.body = ((this.body+1) % cellTypes.length); }

Finally, if the cell has not eaten and there is space to do so, we'll let the cell move to a random empty neighbouring cell.


        var open = [];
        
        for(var x in n)
        {
            if(Map.tiles[n[x]].cell == null)
            {
                open.push(n[x]);
            }
        }
        
        if(open.length)
        {
            var d = open[Math.floor(Math.random() * open.length)];
            
            Map.tiles[this.tile].cell = null;
                
            this.x = Map.tiles[d].x;
            this.y = Map.tiles[d].y;
            this.tile = d;
            
            Map.tiles[d].cell = this;
            
            return true;
        }
        
        return false;
    }
};

As in previous examples, our update method for all cells calls the update method for each cell in the allCells list, adds cells from the newCells list that have been newly created to the allCells list, and removes any cells that have the alive property set to false.


function updateCells()
{
    for(var x in allCells)
    {
        allCells[x].update();
    }
    
    for(var x in newCells) { if(newCells[x].alive) { allCells.push(newCells[x]); } }
    newCells.splice(0, newCells.length);
    
    var toRemove = [];
    for(var x in allCells)
    {
        if(!allCells[x].alive) { toRemove.push(allCells[x]); }
    }
    while(toRemove.length > 0)
    {
        allCells.splice( allCells.indexOf(toRemove[0]), 1 );
        toRemove.shift();
    }
}