Help! Mixing colors makes everything green!

I’m trying to iteratively mix random colors but over time they all become green.

The provided code makes two sets of random-colored squares, and mixes the sets at every tap.

This should result in a constant array of random colors, but instead everything quickly turns green.

(Some code explanation follows the code)


--the big-three Codea functions
function setup()
    --create sets of squares of random colors
    createTwoSetsOfRandomSquares()
end

function draw()
    background(64, 64, 110)
    --draw all the squares in each set
    for _, square in ipairs(upperSquares) do
        square:draw()
    end
    for _, square in ipairs(lowerSquares) do
        square:draw()
    end
end

function touched(touch)
    if touch.state == ENDED then
        -- Shuffle the lower squares
        for i = #lowerSquares, 2, -1 do
            local j = math.random(i)
            lowerSquares[i], lowerSquares[j] = lowerSquares[j], lowerSquares[i]
        end
        
        -- mix each upper color with a corresponding lower color and vice versa
        for i, upperSquare in ipairs(upperSquares) do
            local lowerSquare = lowerSquares[i]
            local color1 = randomizeHueBetween(upperSquare.color, lowerSquare.color)
            local color2 = randomizeHueBetween(upperSquare.color, lowerSquare.color)
            upperSquare.color = color1
            lowerSquare.color = color2
        end
    end
end

--the color functions
--random RGB color
function randomColor()
    return color(math.random(255), math.random(255), math.random(255))
end

--conversion from RGB to HSB
function colorToHSB(aColor)
    local r, g, b = aColor.r / 255, aColor.g / 255, aColor.b / 255
    local max, min = math.max(r, g, b), math.min(r, g, b)
    local delta = max - min
    
    local hue = 0
    if delta ~= 0 then
        if max == r then
            hue = ((g - b) / delta) % 6
        elseif max == g then
            hue = ((b - r) / delta) + 2
        else
            hue = ((r - g) / delta) + 4
        end
        hue = hue * 60
        if hue < 0 then hue = hue + 360 end
    end
    
    local saturation = (max == 0) and 0 or (delta / max)
    
    local brightness = max
    
    return vec3(hue, saturation, brightness)
end

--conversion from HSB to RGB
function hsbToColor(h, s, b)
    h = h % 360
    s = math.max(0, math.min(1, s))
    b = math.max(0, math.min(1, b))
    
    local i = math.floor(h / 60)
    local f = (h / 60) - i
    local p = b * (1 - s)
    local q = b * (1 - (s * f))
    local t = b * (1 - (s * (1 - f)))
    
    local r1, g1, b1
    
    if i == 0 then
        r1, g1, b1 = b, t, p
    elseif i == 1 then
        r1, g1, b1 = q, b, p
    elseif i == 2 then
        r1, g1, b1 = p, b, t
    elseif i == 3 then
        r1, g1, b1 = p, q, b
    elseif i == 4 then
        r1, g1, b1 = t, p, b
    else
        r1, g1, b1 = b, p, q
    end
    
    return color(math.floor(r1 * 255), math.floor(g1 * 255), math.floor(b1 * 255), 255)
end

--randomizer between two colors--only randomizes hue
--keeps saturation and brightness of color1
function randomizeHueBetween(color1, color2)
    -- Convert RGB to HSB
    local hsb1 = colorToHSB(color1)
    local hsb2 = colorToHSB(color2)
    
    -- Mix hues with wrapping
    local hueDiff = math.abs(hsb2.x - hsb1.x)
    if hueDiff > 180 then
        hueDiff = 360 - hueDiff
    end
    local newHue = (math.random() * hueDiff + math.min(hsb1.x, hsb2.x)) % 360
    
    -- Convert back to RGB
    return hsbToColor(newHue, hsb1.y, hsb1.z)
end

--the class for color squares
Square = class()

function Square:init(x, y, size, color)
    self.x = x
    self.y = y
    self.size = size
    self.color = color
end

function Square:draw()
    fill(self.color)
    rect(self.x, self.y, self.size, self.size)
end

--the function to set up the square sets
function createTwoSetsOfRandomSquares()
    --function for setting up squares once dimensions are calculated
    function initializeSquares(numRows, numCols, squareSize, yOffset, margin)
        local squares = {}
        local totalWidth = numCols * squareSize
        local startX = (WIDTH - totalWidth) / 2  -- Centering the block of squares
        
        for row = 1, numRows do
            for col = 1, numCols do
                local x = startX + (col - 1) * squareSize
                local y = yOffset + (row - 1) * squareSize
                table.insert(squares, Square(x, y, squareSize, randomColor()))
            end
        end
        return squares
    end
    --calculate areas to draw squares in
    local margin = WIDTH * 0.05 
    local availableWidth = WIDTH - 2 * margin
    local availableHeight = (HEIGHT / 2) - margin
    
    local numSquares = 200
    local numRows = math.ceil(math.sqrt(numSquares * availableHeight / availableWidth))
    local numCols = math.ceil(numSquares / numRows)
    --calculate square size that fits in those areas
    local squareSize = math.min(availableWidth / numCols, availableHeight / numRows)
    --use intialization function to make squares using those margins
    upperSquares = initializeSquares(numRows, numCols, squareSize, HEIGHT / 2 + margin / 2, margin)
    lowerSquares = initializeSquares(numRows, numCols, squareSize, margin / 2, margin)
end

I think the commenting makes most things clear except for this: why mix colors using HSB not RBG?

RGB has a fatal flaw in color-mixing:over time all colors become gray. I believe this is a known problem.

So it has to be HSB. For simplicity, in this code I am only randomizing the H part. The results are consistent when S and B are also randomized, but there’s no need for that here.

It seems like the problem has to be in the colorToHSB and hsbToColor functions, but I can’t find it. Help! Anyone?

1 Like

I think the problem is your random mixing function. You are basically averaging the colours, and that will tend to draw them all to the hue midpoint, which is green.

It’s not really clear to me what you are hoping will happen with the random mixing.

1 Like

This is a demo of a problem I’m experiencing in my ColorCritters simulation.

The critters are circles that will reproduce with another circle if they are close enough in color. I’d like the ensuing color to have a random hue inside a range defined by the parents. The mating-compatibility-variance can be defined from 0 to 1, 1 meaning they will reproduce with every other color.

So this should produce generation after generation of colors that vary either slightly or wildly from the parent colors, but with no color consistently being dominantly represented. This does not happen though: over generations they all turn green. Just as happens in my demo.

Now, I’m nowhere as good at math as you are, but I don’t think you’re right about the hue midpoint—at least not if my math is right on the color-wrapping, which I’m not certain of by any means. This code is meant to take into account the fact that the hue range of 360 wraps around to hue 0:

  -- Mix hues with wrapping
    local hueDiff = math.abs(hsb2.x - hsb1.x)
    if hueDiff > 180 then
        hueDiff = 360 - hueDiff
    end
    local newHue = (math.random() * hueDiff + math.min(hsb1.x, hsb2.x)) % 360
    

…if that’s working correctly there is no true midpoint, or put another way, every color is a midpoint. So no color should consistently dominate.

Try this:

local a,b = math.min(hsb1.x,hsb2.x),math.max(hsb1.x,hsb2.x)
if a+180< b then
  a,b = b,a+180
end
local t = math.random()
local newHue = (t*a + (1-t)*b) % 360

Interesting result: now everything trends towards blue.

So I see! (I hadn’t tested the code when I posted it)

I’ve looked a bit deeper in your code. When two squares mix, you replace both their values by mixed values, so the separation of their hues is almost certainly less (the only case where it isn’t is if the two mixes happen to be both 0 and 1). So each iteration, the overall separation of hues decreases and the colours tend to some fixed colour. The exact destination will depend on certain choices in the code that make some colours ever so slightly more likely than others.

I’ll need to do some calculations before suggesting an alternative.

One small change I would make for aesthetic reasons is to your initial random colour routine:

return hsbToColor(math.random()*360,1,1)

This produces a random selection of vibrant colours.

3 Likes

I’ve been trying this, which replaces the approach of using the entire range between the parent colors with the approach of randomizing the range by n and then choosing an exact midpoint:

function calculateMiddleHue(hue1, hue2)
    -- Adjust hues for wrapping
    if math.abs(hue1 - hue2) > 180 then
        if hue1 > hue2 then
            hue2 = hue2 + 360
        else
            hue1 = hue1 + 360
        end
    end
    
    -- Calculate the middle hue
    local middleHue = (hue1 + hue2) / 2
    return middleHue % 360
end

function randomColorBetween(color1, color2)
    -- Convert RGB to HSB
    local hsb1 = colorToHSB(color1)
    local hsb2 = colorToHSB(color2)
    
    -- Add a random value between -n and n to both hues
    local n = 35
    local randomHueAdjustment = math.random(-n, n)
    local adjustedHue1 = (hsb1.x + randomHueAdjustment) % 360
    local adjustedHue2 = (hsb2.x + randomHueAdjustment) % 360
    
    -- Calculate the middle hue with wrapping
    local newHue = calculateMiddleHue(adjustedHue1, adjustedHue2)
    
    -- Add a random value between -m and m to saturation and brightness
    local m = 0.05
    local mSubtractor = m * 2
    local lowClamp = 0.15
    local randomSBAdjustment = math.random() * mSubtractor - m
    local adjustedSat1 = clamp(hsb1.y + randomSBAdjustment, lowClamp, 1)
    local adjustedSat2 = clamp(hsb2.y + randomSBAdjustment, lowClamp, 1)
    local adjustedBri1 = clamp(hsb1.z + randomSBAdjustment, lowClamp, 1)
    local adjustedBri2 = clamp(hsb2.z + randomSBAdjustment, lowClamp, 1)
    
    -- Calculate the midpoints for saturation and brightness
    local newSat = (adjustedSat1 + adjustedSat2) / 2
    local newBri = (adjustedBri1 + adjustedBri2) / 2
    
    -- Convert back to RGB
    return hsbToColor(newHue, newSat, newBri)
end

function clamp(value, minVal, maxVal)
    return math.max(minVal, math.min(maxVal, value))
end

…what happens here is that the overall colors converge—probably for the reasons you speculated—but they converge on an unpredictable color. It can apparently be any color. And for certain settings of n and clamp, they seem to never 100% even out to a single color, there’s always visible variation, even if within the same general color area.

I think I’m ok with this. It does seem to mimic the actual evolutionary result of a given population constantly interbreeding over time.

But I still don’t get why the fully-random approach gives more predictable results and the less-random approach gives more unpredictable results!

[btw I’m getting ChatGPT4 assistance with the math—the ideas are mine but the execution is GPT because it’s just way faster than I’d be at executing them]

1 Like

Oh and thanks for the tip on random colors! I see how that could be used to control palettes in a bunch of other ways too—good tip.

1 Like

The convergence to a colour doesn’t depend on how you mix the colours, just that you are mixing them. Picking the midpoint, the midpoint of a randomly adjusted range, or randomly picking a point in between doesn’t affect the fact of convergence, all it does is change the rate of convergence.

The destination of the convergence depends on subtle aspects of how the midpoint of the hue is calculated. The edge cases cause an imbalance and the way that this is handled determines the colour. Your range adjustment means that you get a random colour as the convergence point.

If you’re trying to model a population’s evolutionary path, then I suggest you add in the possibility of mutation. How about at the top of randomColorBetween then you have something like:

if math.random() > .9 then
    return randomColor()
end

One tenth of the time then this will add in a mutated colour which will re-seed the gene pool.

1 Like

Mutation is indeed necessary in generation modeling. In this case Color mutation is modeled elsewhere, at the same time as the potential for mutation of other critter properties like size and speed. It happens by applying the mixing function to the current color and a random color, which looks better than just a fully-random color emerging.

I haven’t let the critters run overnight with this system yet, so I can’t say what ultimately happens, but right off the bat the color convergence happens way slower than the green-shift did. It used to be that I could tell almost right away that the green critters were dominating, now if there’s a dominant color it’s not obvious even after a few minutes.

1 Like

My code should have been:

local a,b = math.min(hsb1.x,hsb2.x),math.max(hsb1.x,hsb2.x)
if a+180< b then
  a,b = b,a+360 -- this line changed
end
local t = math.random()
local newHue = (t*a + (1-t)*b) % 360

With that then the convergence colour varies from run to run.

Your original code didn’t swap the colours when it adjusted to the smaller interval, which had the effect of bringing the second colour down from bluish to greenish, which weighted the convergence colour to green. With mine, I adjusted the interval by 180 not 360 which had the effect of lifting one colour from redish to bluish and weighting the convergence colour to blue.

Fixing those, which my code now does correctly, means that the convergence colour is random but because you are continually mixing colours you will always get convergence.

Repeatedly reseeding the colours, either directly as I suggested or indirectly as you describe (they are almost the same technique) is one way to keep the level of randomness.

I have another suggestion, though, which is that you make the colour a secondary trait. So it isn’t directly inherited, but rather there are other parameters that are inherited and the colour is derived from them. Here’s an example where each square has two hues, their average determines the colour, and each child inherits one from each parent (the choice of which being random).

-- ColourMixing

--the big-three Codea functions
function setup()
    --create sets of squares of random colors
    upperSquares, lowerSquares = createTwoSetsOfRandomSquares()
end

function draw()
    background(64, 64, 110)
    --draw all the squares in each set
    for _, square in ipairs(upperSquares) do
        square:draw()
    end
    for _, square in ipairs(lowerSquares) do
        square:draw()
    end
end

function touched(touch)
    if touch.state == ENDED then
        -- Shuffle the lower squares
        local j
        for i = #lowerSquares, 2, -1 do
            j = math.random(i)
            lowerSquares[i], lowerSquares[j] = lowerSquares[j], lowerSquares[i]
        end
        
        -- mix each upper color with a corresponding lower color and vice versa
        local lowerSquare, hue1, hue2
        for i, upperSquare in ipairs(upperSquares) do
            lowerSquare = lowerSquares[i]
            hue1 = {upperSquare:getHue(), lowerSquare:getHue()}
            hue2 = {upperSquare:getHue(), lowerSquare:getHue()}
            upperSquare:setColor(hue1)
            lowerSquare:setColor(hue2)
        end
    end
end

--the color functions

--conversion from RGB to HSB
function colorToHSB(aColor)
    local r, g, b = aColor.r / 255, aColor.g / 255, aColor.b / 255
    local max, min = math.max(r, g, b), math.min(r, g, b)
    local delta = max - min
    
    local hue = 0
    if delta ~= 0 then
        if max == r then
            hue = ((g - b) / delta) % 6
        elseif max == g then
            hue = ((b - r) / delta) + 2
        else
            hue = ((r - g) / delta) + 4
        end
        hue = hue * 60
        if hue < 0 then hue = hue + 360 end
    end
    
    local saturation = (max == 0) and 0 or (delta / max)
    
    local brightness = max
    
    return vec3(hue, saturation, brightness)
end

--conversion from HSB to RGB
function hsbToColor(h, s, b)
    if s == nil then
        h,s,b = h.x,h.y,h.z
    end
    h = h % 360
    s = math.max(0, math.min(1, s))
    b = math.max(0, math.min(1, b))
    
    local i = math.floor(h / 60)
    local f = (h / 60) - i
    local p = b * (1 - s)
    local q = b * (1 - (s * f))
    local t = b * (1 - (s * (1 - f)))
    
    local r1, g1, b1
    
    if i == 0 then
        r1, g1, b1 = b, t, p
    elseif i == 1 then
        r1, g1, b1 = q, b, p
    elseif i == 2 then
        r1, g1, b1 = p, b, t
    elseif i == 3 then
        r1, g1, b1 = p, q, b
    elseif i == 4 then
        r1, g1, b1 = t, p, b
    else
        r1, g1, b1 = b, p, q
    end
    
    return color(math.floor(r1 * 255), math.floor(g1 * 255), math.floor(b1 * 255), 255)
end

--the class for color squares
Square = class()

function Square:init(x, y, size, hueA, hueB)
    self.x = x
    self.y = y
    self.size = size
    self:setColor({hueA, hueB})
end

function Square:setColor(hues)
    self.hues = hues
    self.color = hsbToColor((hues[1]+hues[2])/2,1,1)
end

function Square:getHue()
    return self.hues[math.random(2)]
end

function Square:draw()
    fill(self.color)
    rect(self.x, self.y, self.size, self.size)
end

--the function to set up the square sets
function createTwoSetsOfRandomSquares()
    --calculate areas to draw squares in
    local margin = WIDTH * 0.05 
    local availableWidth = WIDTH - 2 * margin
    local availableHeight = (HEIGHT / 2) - margin
    
    local numSquares = 200
    local numRows = math.ceil(math.sqrt(numSquares * availableHeight / availableWidth))
    local numCols = math.ceil(numSquares / numRows)
    --calculate square size that fits in those areas
    local squareSize = math.min(availableWidth / numCols, availableHeight / numRows)
    --use intialization function to make squares using those margins
    return initializeSquares(numRows, numCols, squareSize, HEIGHT / 2 + margin / 2, margin),
    initializeSquares(numRows, numCols, squareSize, margin / 2, margin)
end

--function for setting up squares once dimensions are calculated
function initializeSquares(numRows, numCols, squareSize, yOffset, margin)
    local squares = {}
    local totalWidth = numCols * squareSize
    local startX = (WIDTH - totalWidth) / 2  -- Centering the block of squares
    
    for row = 1, numRows do
        for col = 1, numCols do
            local x = startX + (col - 1) * squareSize
            local y = yOffset + (row - 1) * squareSize
            table.insert(squares, Square(x, y, squareSize, math.random()*360, math.random()*360))
        end
    end
    return squares
end
1 Like

That’s neat! I’ll have to mull on that. Thanks!

@ubergoober @ loopspace - like both of your investigations into this. Colour space just goes wooosh over my head.

Very impressed with the square grid class you’ve built - I built my own for a project I’m working on but yours is much shorter , slicker.

One thing I have never done is incorporate functions into functions. Is there some advantage to that other than localising the variables ?

Edit: - I like the idea of inheritance, it adds another dimension to the colour derivation. I wonder if you can take it back a further generation where you inherit from the ‘grandparents’ which would dilute the individual contributions of the parents and provide a wider scope of colour. We often say human inheritance skips a generation.

1 Like

I wish I could take credit for the grid but ChatGPT did it. Although actually, to be fair, it did it all wrong at first and I had to coach it repeatedly to get it right.

1 Like

The gamete colors concept is super neat, I wonder how it would play out with my ColorCritters in practice. Right now there turn out to be clusters of similar-colored critters, which looks pretty.

The way your code works seems to prevent too much uniformity but that might mean the critters don’t actually have descendants of similar colors. I can’t tell without trying it but I’m not ready just yet because, as I’m sure you observed, green is still always predominant.

It starts with a lot of green, and though it takes a much longer time, it still eventually resolves to almost fully green.

So I slightly tweaked your version to A) rename the hue variable to geneHues, because the singular/plural mismatch was tripping me up, and B) now any touch intiates new color sets, so when you drag your finger around there will be really rapid generations, um, generated. It’ll take a bunch of dragging to see everything become green but it’ll happen.


-- ColourMixing

--the big-three Codea functions
function setup()
    --create sets of squares of random colors
    upperSquares, lowerSquares = createTwoSetsOfRandomSquares()
end

function draw()
    background(64, 64, 110)
    --draw all the squares in each set
    for _, square in ipairs(upperSquares) do
        square:draw()
    end
    for _, square in ipairs(lowerSquares) do
        square:draw()
    end
end

function touched(touch)
    if touch.state ~= CANCELLED then
        -- Shuffle the lower squares
        local j
        for i = #lowerSquares, 2, -1 do
            j = math.random(i)
            lowerSquares[i], lowerSquares[j] = lowerSquares[j], lowerSquares[i]
        end
        
        -- mix each upper color with a corresponding lower color and vice versa
        local lowerSquare, hue1, hue2
        for i, upperSquare in ipairs(upperSquares) do
            lowerSquare = lowerSquares[i]
            hue1 = {upperSquare:eitherGeneHue(), lowerSquare:eitherGeneHue()}
            hue2 = {upperSquare:eitherGeneHue(), lowerSquare:eitherGeneHue()}
            upperSquare:setColor(hue1)
            lowerSquare:setColor(hue2)
        end
    end
end

--the color functions

--conversion from RGB to HSB
function colorToHSB(aColor)
    local r, g, b = aColor.r / 255, aColor.g / 255, aColor.b / 255
    local max, min = math.max(r, g, b), math.min(r, g, b)
    local delta = max - min
    
    local hue = 0
    if delta ~= 0 then
        if max == r then
            hue = ((g - b) / delta) % 6
        elseif max == g then
            hue = ((b - r) / delta) + 2
        else
            hue = ((r - g) / delta) + 4
        end
        hue = hue * 60
        if hue < 0 then hue = hue + 360 end
    end
    
    local saturation = (max == 0) and 0 or (delta / max)
    
    local brightness = max
    
    return vec3(hue, saturation, brightness)
end

--conversion from HSB to RGB
function hsbToColor(h, s, b)
    if s == nil then
        h,s,b = h.x,h.y,h.z
    end
    h = h % 360
    s = math.max(0, math.min(1, s))
    b = math.max(0, math.min(1, b))
    
    local i = math.floor(h / 60)
    local f = (h / 60) - i
    local p = b * (1 - s)
    local q = b * (1 - (s * f))
    local t = b * (1 - (s * (1 - f)))
    
    local r1, g1, b1
    
    if i == 0 then
        r1, g1, b1 = b, t, p
    elseif i == 1 then
        r1, g1, b1 = q, b, p
    elseif i == 2 then
        r1, g1, b1 = p, b, t
    elseif i == 3 then
        r1, g1, b1 = p, q, b
    elseif i == 4 then
        r1, g1, b1 = t, p, b
    else
        r1, g1, b1 = b, p, q
    end
    
    return color(math.floor(r1 * 255), math.floor(g1 * 255), math.floor(b1 * 255), 255)
end

--the class for color squares
Square = class()

function Square:init(x, y, size, hueA, hueB)
    self.x = x
    self.y = y
    self.size = size
    self.geneHues = {}
    self:setColor({hueA, hueB})
end

function Square:setColor(geneHues)
    self.geneHues = geneHues
    self.color = hsbToColor((geneHues[1]+geneHues[2])/2,1,1)
end

function Square:eitherGeneHue()
    return self.geneHues[math.random(2)]
end

function Square:draw()
    fill(self.color)
    rect(self.x, self.y, self.size, self.size)
end

--the function to set up the square sets
function createTwoSetsOfRandomSquares()
    --calculate areas to draw squares in
    local margin = WIDTH * 0.05 
    local availableWidth = WIDTH - 2 * margin
    local availableHeight = (HEIGHT / 2) - margin
    
    local numSquares = 200
    local numRows = math.ceil(math.sqrt(numSquares * availableHeight / availableWidth))
    local numCols = math.ceil(numSquares / numRows)
    --calculate square size that fits in those areas
    local squareSize = math.min(availableWidth / numCols, availableHeight / numRows)
    --use intialization function to make squares using those margins
    return initializeSquares(numRows, numCols, squareSize, HEIGHT / 2 + margin / 2, margin),
    initializeSquares(numRows, numCols, squareSize, margin / 2, margin)
end

--function for setting up squares once dimensions are calculated
function initializeSquares(numRows, numCols, squareSize, yOffset, margin)
    local squares = {}
    local totalWidth = numCols * squareSize
    local startX = (WIDTH - totalWidth) / 2  -- Centering the block of squares
    
    for row = 1, numRows do
        for col = 1, numCols do
            local x = startX + (col - 1) * squareSize
            local y = yOffset + (row - 1) * squareSize
            table.insert(squares, Square(x, y, squareSize, math.random()*360, math.random()*360))
        end
    end
    return squares
end

Functions inside of functions is for me a matter of clarity in code. It can make the code more readable and navigable for me sometimes, especially if it’s a large function. Also as we all know the larger a project gets in Codea the harder it is to find exactly what you’re looking for, and if one function can be nested in another it’s easier to skip past them both all at once.

Any procedure where the amount of information decreases will eventually converge. My code still loses information gradually so converges. It doesn’t always converge to green, but because I didn’t use the wrapping with the hue averaging when setting the colour then the greens are more likely then the extremes. That’s an easy fix.

But, as I said, it still tends to lose information - just much slower than the original code - so it will eventually converge.

An alternative scheme that never loses information is the following: each square has two hues associated with it. It uses one of these (picked at random) to set its colour. So it has a dominant and recessive colour. When two squares are used to produce the next generation, then each donates one of its colours to each of the new squares. So if the original square has hues A and B, then one child gets A and the other gets B. The child then sets its colour by choosing one of those at random, but it remembers both to pass on to the next generation.

This scheme never loses information so it doesn’t converge to a single colour, but the way that the hues are passed on and the way the colours are set mean that the colours do vary from generation to generation.

Functions inside functions is most useful when you want the inner function to have access to information outside itself but where you don’t want that information to be in the global scope. So if you want it to have some state that gets updated at each call but remembered between those calls, having a function within a function is a good way to do this. Generators and iterators use this paradigm.

The other reason to have a function inside another function is if it is a local function so that its memory can be reclaimed afterwards. But this needs the function to be defined using:

local name = function() ... end

otherwise, the function is globally defined.

@LoopSpace - thanks for the explanation. It confirms my thoughts on functions within functions. My style of programming is not ideal and I have steered away from programming in that way. Perhaps I’ll revisit it if I find the need later. It does seem to make the code more compact.

I like your new system for generational colour change. I’ve thought of a new idea for that. Human reproduction involves the incorporation of parts of the DNA for both partners. So - you could have a long random number for each unit and extract parts of the unit for each R, G, B or H, S, B whichever you wish to use. Then incorporate the new colour factors, derived in that way, into the new entities DNA truncating the new number to keep a consistent length.

Would that work ?