Search This Blog

Amazing Technicolor Sharks

When you start a new C3 world, your aquarium has a variety of colorful rainbow sharklings, but as time goes on they tend to become... not very rainbow. That does make sense, because offspring will have colors between those of their parents, so over the generations the colors will tend to go toward the middle of the color wheel. But maybe with a little tweaking, this could be fixed!

I do love that I can open C3/DS cos scripts and get the initial injection code, rather than having to backwards engineer it as in C1 and C2. Given how complicated many of the objects in C3/DS are, this sort of modification may well have been impossible without this small change in code accessibility! Here's the initial shark script - with a lot of unnecessary empty lines removed. Actually, this whole script seems to contain a lot of unnecessary stuff, such as a reps loop that runs once, or object variables that are set but never referenced. It feels very much like code that was never cleaned up after they got it functional.

The Original Script

For reference, the object variables that we care about are as follows:

  • ov01: age
  • ov02: fullness; the lower it is the hungrier the shark is
  • ov05: adulthood flag; 2 indicates a mature adult.
  • ov10: horizontal direction of movement
  • ov11: vertical direction of movement
  • ov19: lock A; makes sure a particular part of the code only runs once.
  • ov20, ov21, ov22: genetic red, green, and blue values
  • ov23, ov24, ov25: record of mate's red, green, and blue values.
  • ov70: mated flag
  • ov71: species identifier of the type of fish it hunts. If it can't find this type of fish, it'll randomly choose a different one.
  • ov72: technically not unused, but it's only ever set and checked alongside ov05, and therefore seems to be redundant with it.
  • ov80: marked for death flag; marks the shark to die after some time.
  • ov81: death timer used in connection to the impending death flag.
  • ov92: lock B; makes sure a different part of the code only runs once.
  • ov99: supposed-to-be-dead flag; the shark was already supposed to be killed but for some reason couldn't be, so this flag indicates that it should be killed as soon as it's valid to do so.

Sharkling Inject Script
setv va00 0
reps rand 3 5
  setv va60 rand 60 255
  setv va61 rand 60 255
  setv va62 rand 60 255
  setv va40 3879
  setv va41 2192
  reps 1
    addv va60 1
    new: simp 2 16 4 "shark"
      …153 0 4100
    attr 199
    clac -1
    elas 20
    aero 1
    accg 0
    perm 64
    tint va60 va61 va62 128 128
    mvto va40 va41
    setv ov01 0
    setv ov02 550
    seta ov16 null
    setv va50 rand 0 1
    setv ov61 40
    setv ov73 1
    setv ov74 0
    doif va50 = 0
      setv ov10 -1
      pose 0
    elif va50 = 1
      setv ov10 1
      pose 8
    endi
    velo 0 0
    setv va51 rand 1 4
    doif va51 = 1
      setv ov71 14
    elif va51 = 2
      setv ov71 15
    elif va51 = 3
      setv ov71 16
    elif va51 = 4
      setv ov71 19
    endi
    setv ov20 va60
    setv ov21 va61
    setv ov22 va62
    tick 1
  repe
repe
endm
set unused variable
do this 3 to 5 times:
{ generate red value
  generate green value
  generate blue value
  define starting position x
  define starting position y
  do this once:
  { add 1 to red value for some reason
    create new shark object
      …
    C/H carry, activatable, collisions and physics
    when clicked, send no script
    set bounciness
    set aerodynamics
    not affected by gravity
    sets what walls it can go through
    color according to the color values
    move to starting position
    set age to 0
    set fullness to 550
    save an unused reference
    generate random cointoss value
    save an unused value
    save an unused value
    save an unused value
    if cointoss is 0
    { set direction to left
      left-facing sprite
    } else if cointoss is 1
    { set direction to right
      right-facing sprite
    }
    no velocity
    generate randomizer between 1 and 4
    if randomizer is 1
    { set prey type to angelfish
    } else if randomizer is 2
    { set prey type to clownfish
    } else if randomizer is 3
    { set prey type to hatchetfish
    } else if randomizer is 4
    { set prey type to neonfish
    }
    save genetic red value
    save genetic green value
    save genetic blue value
    set timer to one tick
  }
}

Most of the shark's behavior is contained in the timer script, with several subroutines for different activities. Some of them aren't relevant to this project, so I'm not going to annotate them. Some of these are fairly self-explanatory: DETH kills the shark, OBST handles collisions with obstacles, GORT sets the velocity to the right and plays the rightward swimming animation, etc. Others require a little more explanation but don't merit an in-depth explanation: ROOM detects whether the shark is in salt water and adjusts its physics accordingly, and eventually kills the shark if out of water too long. INIM handles the shark's velocity. MARK literally just sets the impending death flag. RNDM randomly changes the direction the shark is moving in. FLOK allows the sharks to swim together in a school.

Sharkling Timer Script
scrp 2 16 4 9
  gsub room
  doif ov99 = 5
    gsub deth
  endi
  doif ov01 >= 3000
    gsub deth
  endi
  addv ov01 1
  doif ov01 > 1200 and ov19 = 0
    setv ov05 2
    setv ov72 0
    setv ov19 1
  endi
  subv ov02 1
  doif ov70 = 1
    gsub repr
    setv ov70 0
    gsub mark
  endi
  doif ov80 = 1
    addv ov81 1
    doif ov81 = 15
      gsub deth
    endi
  endi
  doif ov05 = 2 and ov72 = 0
      …and ov80 <> 1
    gsub mate
  endi
  doif ov02 >= 500
    doif ov92 = 0
      gsub inim
      gsub obst
      gsub swim
      gsub move
      setv ov92 1
    elif ov92 = 1
      setv va00 rand 1 5
      doif va00 <= 2
        gsub flok
      else
        gsub rndm
      endi
      gsub obst
      gsub swim
      gsub move
    endi
  elif ov02 < 500 and ov02 > 0
    gsub hunt
    gsub obst
    gsub swim
    gsub move
    gsub eat_
  elif ov02 <= 0
    gsub deth
  endi
SHARKLING TIMER SCRIPT
  run ROOM subroutine
  if the shark is supposed to be dead
  { run DETH subroutine
  }
  if age >= 3000
  { run DETH subroutine
  }
  add 1 to age
  if age > 1200 and lock A isn't set
  { set adulthood flag
    set redundant adulthood flag
    set lock A
  }
  decrease fullness by 1
  if mated
  { run REPR subroutine
    set not mated
    run MARK subroutine
  }
  if marked for death
  { add 1 to death timer
    if death timer = 15
    { run DETH subroutine
    }
  }
  if adult and redundantly adult
      …and not marked for death
  { run MATE subroutine
  }
  if fullness >= 500
  { if lock B not set
    { run INIM subroutine
      run OBST subroutine
      run SWIM subroutine
      run MOVE subroutine
      set lock B
    } else if lock B is set
    { generate randomizer between 1 and 5
      if randomizer <= 2
      { run FLOK subroutine
      } else
      { run RNDM subroutine
      }
      run OBST subroutine
      run SWIM subroutine
      run MOVE subroutine
    }
  } else if fullness between 500 and 0
  { run HUNT subroutine
    run OBST subroutine
    run SWIM subroutine
    run MOVE subroutine
    run EAT subroutine
  } else if fullness <= 0
    run DETH subroutine
  }
  subr mate
    rnge 100
    inst
    esee 2 16 4
      doif targ <> null
        doif ov05 = 2
          setv va70 1
          setv va20 ov20
          setv va21 ov21
          setv va22 ov22
        endi
      endi
    next
    slow
    doif va70 = 1
      setv ov70 1
      setv ov23 va20
      setv ov24 va21
      setv ov25 va22
    endi
  retn
  MATE SUBROUTINE
    set the range for detection
    run this section instantly
    for each shark in range
    { if target isn't null
      { if target is adult
        { locally set mated flag
          locally save target's red value
          locally save target's green value
          locally save target's blue value
        }
      }
    }
    end instantaneous section
    if local mated flag
    { set real mated flag
      save mate's red value
      save mate's green value
      save mate's blue value
    }
   end subroutine
  subr repr
    setv ov08 0
    setv va34 0
    rnge 1000
    esee 2 16 4
      addv va34 1
    next
    doif va34 <= 5
      setv ov08 rand 1 20
      doif ov08 = 5
        setv ov08 rand 1 3
      else
        setv ov08 rand 1 2
      endi
    else
      setv ov08 0
    endi
    doif ov08 <> 0
      reps ov08
        setv va00 rand 1 5
        doif va00 = 5
          setv va20 rand 06 255
          addv va20 ov20
          divv va20 2
          doif va20 < 50
            addv va20 20
          endi
        else
          setv va20 ov20
        endi
        setv va00 rand 1 5
        doif va00 = 5
          setv va21 rand 60 255
          addv va21 ov21
          divv va21 2
          doif va20 < 50
            addv va20 20
          endi
        else
          setv va21 ov21
        endi
        setv va00 rand 1 5
        doif va00 = 5
          setv va22 rand 60 255
          addv va22 ov22
          divv va22 2
          doif va20 < 50
            addv va20 20
          endi
        else
          setv va22 ov22
        endi
        addv va26 ov20
        addv va26 ov23
        addv va26 va20
        addv va27 ov21
        addv va27 ov24
        addv va27 va21
        addv va28 ov22
        addv va28 ov25
        addv va28 va22
        divv va26 3
        divv va27 3
        divv va28 3
        setv va80 posl
        setv va81 post
        new: simp 2 16 4 "shark"
            …153 0 4100
        attr 199
        clac -1
        elas 20
        aero 1
        accg 0
        perm 64
        tint va26 va27 va28 128 128
        mvto va80 va81
        setv ov01 0
        setv ov02 550
        seta ov16 null
        setv va50 rand 0 1
        setv ov61 40
        setv va51 rand 1 4
        doif va51 = 1
          setv ov71 14
        elif va51 = 2
          setv ov71 15
        elif va51 = 3
          setv ov71 16
        elif va51 = 4
          setv ov71 19
        endi
        setv ov73 1
        setv ov74 0
        doif va50 = 0
          setv ov10 -1
          pose 0
        elif va50 = 1
          setv ov10 1
          pose 8
        endi
        setv ov20 va26
        setv ov21 va27
        setv ov22 va28
        velo 0 0
        tick 1
      repe
      targ ownr
      setv ov70 0
      gsub mark
    endi
  retn
  REPR SUBROUTINE
    set number of offspring to 0
    set shark counter to 0
    set long range
    for each detected shark
    { add 1 to shark counter
    }
    if shark counter <= 5
    { set number of offspring between 1 and 20
      if number of offspring is 5
      { set number of offspring between 1 and 3
      } else
      { set number of offspring between 1 and 2
      }
    } else
    { set number of offspring to 0
    }
    if number of offspring is not 0
    { for that number of times
      { set mutation flag between 1 and 5
        if mutation flag = 5
        { set temp red between 6 and 255
          add parent's red value
          divide by 2 (for average)
          if temp red < 50
          { add 20 to temp red
          }
        } else
        { set temp red to parent red
        }
        set mutation flag between 1 and 5
        if mutation flag = 5
        { set temp green between 60 and 255
          add parent's green value
          divide by 2 (for average)
          if temp red < 50
          { add 20 to temp red
          }
        } else
        { set temp green to parent green
        }
        set mutation flag between 1 and 5
        if mutation flag = 5
        { set temp blue between 60 and 255
          add parent's blue value
          divide by 2 (for average)
          if temp red < 50
          { add 20 to temp red
          }
        } else
        { set temp blue to parent blue
        }
        add parent red to offspring red
        add mate red to offspring red
        add temp red to offspring red
        add parent green to offspring green
        add mate green to offspring green
        add temp green to offspring green
        add parent blue to offspring blue
        add mate blue to offspring blue
        add temp blue to offspring blue
        divide offspring red by 3 (for average)
        divide offspring green by 3 (for average)
        divide offspring blue by 3 (for average)
        save left position
        save top position
        create new shark object
            … 
        C/H carry, activatable, collisions, physics
        when clicked send no script
        set bounciness
        set aerodynamics
        not affected by gravity
        sets what walls it can go through
        color according to offspring colors
        move to saved position
        set age to 0
        set fullness to 550
        save an unused reference
        generate random cointoss value
        save an unused value
        generate randomizer between 1 and 4
        if randomizer is 1
        { set prey type to angelfish
        } else if randomizer is 2
        { set prey type to clownfish
        } else if randomizer is 3
        { set prey type to hatchetfish
        } else if randomizer is 4
        { set prey type to neonfish
        }
        save an unused value
        save an unused value
        if cointoss is 0
        { set direction to left
          left-facing sprite
        } else if cointoss is 1
        { set direction to right
          right-facing sprite
        }
        save genetic red value
        save genetic green value
        save genetic blue value
        no velocity
        set timer to one tick
      }
      target the parent again
      set not mated
      run MARK subroutine
    }
  end subroutine

There are numerous errors in the reproduction script. A typo causes the temporary red value to be generated between 6 and 255 rather than 60 and 255. When the green and blue values mutate, they erroneously verify that the red value is greater than 50 rather than checking themselves. Not to mention, even when they do mutate, the mutated color is averaged with the parent's value, and then averaged again with the parent and the mate, severely muting it.

Simulation and Algorithm Selection

Because it would take a while to breed generations of sharks for testing, I figured it would be wise to write a program that simulated the shark genetics quickly. This script cuts the shark behavior down to the essentials - instead of tracking hunger and hunting, I just occasionally kill a shark at random to simulate starvation; instead of tracking whether sharks are near enough to each other to mate, I just use another random number to determine if a mate is successfully found, and then select a second shark randomly from the list. Aging, reaching adulthood, and dying of old age or after mating are simulated.

The program was initially text-only, but seeing a wall of numbers wasn't nearly as helpful as seeing actual colors, so I added some limited graphical capabilities: a grid of boxes, each representing the color of a specific shark (with its ID number written on it for clarity). Each row, starting at the top, represents a point in time - a new row is printed whenever a shark dies or is born.

The Original Algorithm

To get a good range of data, I ran each algorithm a few times, and in the images below, I've put the different outputs side by side, divided by a thin black line.

As expected, implementing the existing algorithm results in a population that doesn't take long to converge on a single dull color, such as the ones I've seen in game.

The Fixed Original Algorithm

The original algorithm had several errors, as I've pointed out. I decided to see what would happen if I left the overall structure intact, but fixed the obvious mistakes (correcting 06 to 60, making sure the mutation code actually checks the correct color, etc).

These corrections do result in brighter colors! However, they still converge on a single color relatively quickly, making for a boring population.

The "Wild Mutation" Algorithm

My first attempt at improving the algorithm was to do away with the complexities of averaging the mutations into the parent colors, allowing them to be more dramatic. I changed the color calculation to average the parent colors first, and then randomly determining whether to mutate afterwards. If the color did mutate, it did so to a random value between 60 and 255, completely unrelated to the parent colors, end of story.

As expected, my wild mutations resulted in a population that changed considerably over time, showing off a variety of usually-bright colors. Unfortunately, the individuals within the population generally were fairly similar to each other, with the occasional mutated oddball providing genetic drift rather than inducing variety in the population.

The Weighted Mutation Algorithm

I made one more edit to my algorithm to specifically combat samey-ness in the population. When determining whether to mutate the offspring, I cheated a little bit: for each color channel, if the parents' color values were particularly close to each other, I forced a mutation. This way, parents with wildly different colors will produce logically averaged offspring, but if they're too similar, the gene pool gets a new injection of color.

Now that is the desired result! The colors aren't completely random; a red shark and a blue shark will produce purple offspring, giving a sense of continuity to the population, but at the same time, variety is enforced enough to truly earn the name rainbow sharkling!

If you'd like to play with the python script I used to simulate the populations, you can download it here. You'll need python 3.8 and the tkinter library installed to use it, and you can switch between the algorithms I tested by changing self.weighteddmutationCalcColor() on line 209 to self.originalCalcColor(), self.fixedCalcColor(), or self.wildmutationCalcColor().

The Alteration

With a new algorithm selected, I modified the sharkling script and injected it into the world. We shall see how it pans out! I only changed the REPR subroutine, basically cutting out the entire color calculation chunk and replacing it with all new code.

Modified Sharkling REPR Script

doif ov08 <> 0
  reps ov08
    setv va00 rand 1 4
    setv va26 ov20
    addv va26 ov23
    divv va26 2
    setv va20 ov20
    subv va20 va26
    absv va20
    doif va20 < 15
      setv va00 4
    endi
    doif va00 = 4
      setv va26 rand 60 255
    endi
    setv va00 rand 1 4
    setv va27 ov21
    addv va27 ov24
    divv va27 2
    setv va21 ov21
    subv va21 va27
    absv va21
    doif va21 < 15
      setv va00 4
    endi
    doif va00 = 4
      setv va27 rand 60 255
    endi
    setv va00 rand 1 4
    setv va28 ov22
    addv va28 ov25
    divv va28 2
    setv va22 ov21
    subv va22 va28
    absv va22
    doif va22 < 15
      setv va00 4
    endi
    doif va00 = 4
      setv va28 rand 60 255
    endi
    setv va80 posl
    setv va81 post
    new: simp 2 16 4 "shark" 153 0 4100
    attr 199
    clac -1
    elas 20
    aero 1
    accg 0
    perm 64
    tint va26 va27 va28 128 128
    mvto va80 va81
    …

if number of offspring is not 0
{ for that number of times
  { set mutation flag between 1 and 4
    set offspring red to parent red
    add mate red to offspring red
    divide by 2 (for average)
    set diff red to parent red
    subtract offspring red from diff red
    take absolute value of diff red
    if diff red < 15
    { set mutation flag to 4
    }
    if mutation flag = 4
    { set offspring red between 60 and 255
    }
    set mutation flag between 1 and 4
    set offspring green to parent green
    add mate green to offspring green
    divide by 2 (for average)
    set diff green to parent green
    subtract offspring green from diff green
    take absolute value of diff green
    if diff green < 15
    { set mutation flag to 4
    }
    if mutation flag = 4
    { set offspring green between 60 and 255
    }
    set mutation flag between 1 and 4
    set offspring blue to parent blue
    add mate blue to offspring blue
    divide by 2 (for average)
    set diff blue to parent blue
    subtract offspring blue from diff blue
    take absolute value of diff blue
    if diff blue < 15
    { set mutation flag to 4
    }
    if mutation flag = 4
    { set offspring blue between 60 and 255
    }
    save left position
    save top position
    create new shark object
    C/H carry, activatable, collisions, physics
    when clicked send no script
    set bounciness
    set aerodynamics
    not affected by gravity
    set what walls it can go through
    color according to offspring colors
    move to saved position
    …

All that remains now is to observe how this modified code pans out, but that will take time, so I'll do that over the next few sessions in Eden. If I like the results, I'll add it to the downloads page. Otherwise, we've got more work to do.

4 comments:

  1. Have you ever tried the C3 Bootstrap V2? That comes with the initial developers' comments. https://creatures.wiki/C3_Bootstrap_V2

    ReplyDelete
  2. Very interesting read!

    ReplyDelete
  3. This is super cool! Good work. I think you could've settled for the slight modified version but you persevered till you got the result you wanted. Congrats!

    ReplyDelete