Simple genetic simulator, 1

This commit is contained in:
Pyrex 2025-04-20 15:32:33 -07:00
commit b323eef0e2
10 changed files with 767 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

217
Cargo.lock generated Normal file
View File

@ -0,0 +1,217 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "colornamer"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbf1702f41ec8b3a350e44afdcbae669b761712bd9dd839299775f428b811957"
dependencies = [
"bitflags 1.3.2",
"lazy_static",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "genetics"
version = "0.1.0"
dependencies = [
"colornamer",
"itertools",
"rand",
"serde",
]
[[package]]
name = "getrandom"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
]
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.9.0",
]
[[package]]
name = "zerocopy"
version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

10
Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "genetics"
version = "0.1.0"
edition = "2021"
[dependencies]
colornamer = "1.0.1"
itertools = "0.14.0"
rand = "0.9.1"
serde = {version = "1.0.219", features=["derive"]}

18
src/choice_function.rs Normal file
View File

@ -0,0 +1,18 @@
use rand::{rng, seq::IndexedRandom, Rng};
#[derive(Clone, Copy)]
pub enum ChoiceFunction {
First,
Last,
Random,
}
impl ChoiceFunction {
pub fn choose<'a, T>(&self, xs: &'a [T]) -> &'a T {
match self {
ChoiceFunction::First => &xs[0],
ChoiceFunction::Last => &xs[xs.len() - 1],
ChoiceFunction::Random => xs.choose(&mut rng()).unwrap(),
}
}
}

View File

@ -0,0 +1,65 @@
use colornamer::{ColorNamer, Colors};
use crate::genetics::{Genetic, Transcriber};
use super::stats::Stat;
#[derive(Default)]
pub struct Coloration {
pub eyes: Color,
pub body: Color,
}
impl Coloration {
pub fn profile(&self) -> String {
return format!(
"Eyes: {}\nBody: {}\n",
self.eyes.to_name(),
self.body.to_name()
);
}
}
impl Genetic for Coloration {
fn transcribe(&mut self, t: Transcriber) {
self.eyes.transcribe(t.nest("eyes"));
self.body.transcribe(t.nest("body"));
}
}
#[derive(Clone, Copy, Default)]
pub struct Color {
pub r: Stat<'.', '!', 0, 8>,
pub g: Stat<'.', '!', 0, 8>,
pub b: Stat<'.', '!', 0, 8>,
pub pallor: Stat<'.', '!', 0, 8>,
}
impl Genetic for Color {
fn transcribe(&mut self, t: Transcriber) {
self.r.transcribe(t.nest("r"));
self.g.transcribe(t.nest("g"));
self.b.transcribe(t.nest("b"));
self.pallor.transcribe(t.nest("pallor"));
}
}
impl Color {
fn to_name(&self) -> String {
let mut r = self.r.value() * 255 / 8;
let mut g = self.g.value() * 255 / 8;
let mut b = self.b.value() * 255 / 8;
let pallor_amt = self.pallor.value() * 255 / 8;
r = (r * (255 - pallor_amt) + 255 * pallor_amt) / 255;
g = (g * (255 - pallor_amt) + 255 * pallor_amt) / 255;
b = (b * (255 - pallor_amt) + 255 * pallor_amt) / 255;
// let colornamer = ColorNamer::new(Colors::NTC);
let colornamer = ColorNamer::new(Colors::X11);
let hex_color = format!("#{:02x}{:02x}{:02x}", r, g, b);
return format!(
"{} ({})",
colornamer.name_hex_color(&hex_color).unwrap(),
hex_color
);
}
}

62
src/creature/mod.rs Normal file
View File

@ -0,0 +1,62 @@
mod coloration;
mod species;
mod stats;
use coloration::Coloration;
use species::Parts;
use stats::Stats;
use crate::genetics::{Genetic, Genotype, Transcriber};
#[derive(Clone, Debug)]
pub struct Creature {
// pub name: String,
pub genotype: Genotype,
}
impl Creature {
pub fn profile(&self) -> String {
let phenotype: Phenotype = self.genotype.load();
let mut profile = phenotype.profile(); // format!("Name: {}\n{}", self.name, phenotype.profile());
profile.push_str("\nGenome:\n");
for (chromosome, (lhs, rhs)) in self.genotype.chromosomes.iter() {
profile.push_str(&format!(" {}[l]: {}\n", chromosome, lhs));
profile.push_str(&format!(" {}[r]: {}\n", chromosome, rhs));
}
return profile;
}
pub fn breed(&self, other: &Creature) -> Creature {
return Creature {
// name: String::from("Pyrex"),
genotype: self.genotype.breed(&other.genotype),
};
}
}
#[derive(Default)]
pub struct Phenotype {
pub stats: Stats,
pub coloration: Coloration,
pub parts: Parts,
}
impl Phenotype {
pub fn profile(&self) -> String {
format!(
"{}{}{}",
self.stats.profile(),
self.coloration.profile(),
self.parts.profile()
)
}
}
impl Genetic for Phenotype {
fn transcribe(&mut self, t: Transcriber) {
self.stats.transcribe(t.nest("stats"));
self.coloration.transcribe(t.nest("coloration"));
self.parts.transcribe(t.nest("parts"));
}
}

64
src/creature/species.rs Normal file
View File

@ -0,0 +1,64 @@
use crate::genetics::{Genetic, Transcriber};
use super::stats::Stat;
#[derive(Default)]
pub struct Parts {
stare: Stat<'e', 'E', 0, 6>,
fangs: Stat<'f', 'F', 0, 6>,
wings: Stat<'w', 'W', 0, 6>,
}
impl Parts {
pub fn profile(&self) -> String {
format!("Species: {:?}\n", self.species())
}
pub fn has_stare(&self) -> bool {
return self.stare.value() >= 5;
}
pub fn has_fangs(&self) -> bool {
return self.fangs.value() >= 5;
}
pub fn has_wings(&self) -> bool {
return self.wings.value() >= 5;
}
pub fn species(&self) -> Species {
let mut ix = 0;
if self.has_stare() {
ix += 4;
}
if self.has_fangs() {
ix += 2;
}
if self.has_wings() {
ix += 1;
}
use Species::*;
return [Bunny, Pigeon, Rat, Dragon, Hare, Crow, Snake, Bat][ix];
}
}
impl Genetic for Parts {
fn transcribe(&mut self, t: Transcriber) {
self.stare.transcribe(t.nest("stare"));
self.fangs.transcribe(t.nest("fangs"));
self.wings.transcribe(t.nest("wings"));
}
}
#[derive(Clone, Copy, Debug)]
pub enum Species {
Bunny,
Pigeon,
Rat,
Dragon,
Hare,
Crow,
Snake,
Bat,
}

57
src/creature/stats.rs Normal file
View File

@ -0,0 +1,57 @@
use crate::genetics::{Genetic, Transcriber};
#[derive(Default)]
pub struct Stats {
pub iq: Stat<'.', '!', 5, 10>, // 5-15
pub eq: Stat<'.', '!', 5, 10>, // 5-15
}
impl Stats {
pub fn profile(&self) -> String {
format!("IQ: {}\nEQ: {}\n", self.iq.value(), self.eq.value())
}
}
impl Genetic for Stats {
fn transcribe(&mut self, t: Transcriber) {
self.iq.transcribe(t.nest("IQ"));
self.eq.transcribe(t.nest("EQ"));
}
}
#[derive(Clone, Copy)]
pub struct Stat<const OFF: char, const ON: char, const MIN: usize, const RANGE: usize> {
values: [bool; RANGE],
}
impl<const OFF: char, const ON: char, const MIN: usize, const RANGE: usize>
Stat<OFF, ON, MIN, RANGE>
{
pub fn value(&self) -> usize {
let mut value = MIN;
for v in self.values.iter() {
value += *v as usize;
}
return value;
}
}
impl<const OFF: char, const ON: char, const MIN: usize, const RANGE: usize> Default
for Stat<OFF, ON, MIN, RANGE>
{
fn default() -> Self {
Self {
values: [false; RANGE],
}
}
}
impl<const OFF: char, const ON: char, const MIN: usize, const RANGE: usize> Genetic
for Stat<OFF, ON, MIN, RANGE>
{
fn transcribe(&mut self, mut t: Transcriber) {
for v in self.values.iter_mut() {
t.express(v, &[(OFF, false), (ON, true)]);
}
}
}

199
src/genetics.rs Normal file
View File

@ -0,0 +1,199 @@
use std::{
cell::RefCell,
collections::{BTreeMap, HashSet},
rc::Rc,
};
use itertools::{EitherOrBoth, Itertools};
use rand::{seq::IndexedRandom, RngCore};
use serde::{Deserialize, Serialize};
use crate::choice_function::ChoiceFunction;
#[derive(Clone, Deserialize, Serialize, Debug)]
pub struct Genotype {
pub chromosomes: BTreeMap<String, Chromosomes>,
}
type Chromosomes = (String, String);
fn merge(rng: &mut impl RngCore, one: &str, two: &str) -> String {
let mut results = String::new();
for pair in one.chars().zip_longest(two.chars()) {
results.push(match pair {
EitherOrBoth::Both(l, r) => *[l, r].choose(rng).unwrap(),
EitherOrBoth::Left(l) => l,
EitherOrBoth::Right(r) => r,
})
}
return results;
}
impl Genotype {
pub fn breed(&self, other: &Genotype) -> Genotype {
let mut rng = rand::rng();
let mut chromosomes: HashSet<String> = HashSet::new();
chromosomes.extend(self.chromosomes.keys().cloned());
chromosomes.extend(other.chromosomes.keys().cloned());
let mut result: BTreeMap<String, Chromosomes> = BTreeMap::new();
let placeholder = (String::new(), String::new());
for k in chromosomes {
let mine = self.chromosomes.get(&k).unwrap_or(&placeholder);
let theirs = self.chromosomes.get(&k).unwrap_or(&placeholder);
let mine_merged = merge(&mut rng, &mine.0, &mine.1);
let theirs_merged = merge(&mut rng, &theirs.0, &theirs.1);
result.insert(k, (mine_merged, theirs_merged));
}
return Genotype {
chromosomes: result,
};
}
pub fn generate<T: Genetic>(choice_function: ChoiceFunction) -> Self {
let root = RootTranscriber::Generator(Generator {
choice_function,
progress: BTreeMap::new(),
});
let root_cell = Rc::new(RefCell::new(root));
let transcriber = Transcriber {
root: root_cell.clone(),
chromosome: String::from("base"),
};
let mut base = T::default();
base.transcribe(transcriber);
match &*root_cell.borrow() {
RootTranscriber::Generator(generator) => {
let chromosomes = generator.progress.clone();
return Genotype { chromosomes };
}
_ => panic!("a generator should not able to turn into a loader"),
};
}
pub fn load<T: Genetic>(&self) -> T {
let mut progress = BTreeMap::new();
for (key, (lhs, rhs)) in self.chromosomes.iter() {
progress.insert(
key.clone(),
(0, Vec::from_iter(lhs.chars()), Vec::from_iter(rhs.chars())),
);
}
let root = RootTranscriber::Loader(Loader { progress });
let root_cell = Rc::new(RefCell::new(root));
let transcriber = Transcriber {
root: root_cell.clone(),
chromosome: String::from("base"),
};
let mut base = T::default();
base.transcribe(transcriber);
return base;
}
}
enum RootTranscriber {
Generator(Generator),
Loader(Loader),
}
impl RootTranscriber {
pub fn express<T: Clone>(
&mut self,
chromosome: &str,
field: &mut T,
possibilities: &[(char, T)],
) {
match self {
RootTranscriber::Generator(generator) => {
generator.express(chromosome, field, possibilities)
}
RootTranscriber::Loader(loader) => loader.express(chromosome, field, possibilities),
}
}
}
#[derive(Clone)]
pub struct Transcriber {
root: Rc<RefCell<RootTranscriber>>,
chromosome: String,
}
impl Transcriber {
pub fn express<T: Clone>(&mut self, field: &mut T, possibilities: &[(char, T)]) {
self.root
.borrow_mut()
.express(&self.chromosome, field, possibilities)
}
pub fn nest(&self, suffix: &str) -> Transcriber {
return Transcriber {
root: self.root.clone(),
chromosome: format!("{}.{}", self.chromosome, suffix),
};
}
}
pub trait Genetic: Default {
fn transcribe(&mut self, t: Transcriber);
}
struct Generator {
choice_function: ChoiceFunction,
progress: BTreeMap<String, Chromosomes>,
}
impl Generator {
pub fn express<T>(&mut self, chromosome: &str, _field: &mut T, possibilities: &[(char, T)]) {
let lhs = self.choice_function.choose(possibilities).0;
let rhs = self.choice_function.choose(possibilities).0;
let progress = self
.progress
.entry(chromosome.to_string())
.or_insert((String::new(), String::new()));
progress.0.push(lhs);
progress.1.push(rhs);
}
}
struct Loader {
progress: BTreeMap<String, (usize, Vec<char>, Vec<char>)>,
}
impl Loader {
pub fn express<T: Clone>(
&mut self,
chromosome: &str,
field: &mut T,
possibilities: &[(char, T)],
) {
let (mut lhs, mut rhs) = ('?', '?');
if let Some((ix, lhs_buf, rhs_buf)) = self.progress.get_mut(chromosome) {
lhs = lhs_buf.get(*ix).cloned().unwrap_or('?');
rhs = rhs_buf.get(*ix).cloned().unwrap_or('?');
*ix += 1;
}
let mut expressed = lhs; // lhs wins if not dominated
if dominance(rhs) > dominance(lhs) {
expressed = rhs;
}
for (allele, value) in possibilities.iter() {
if *allele == expressed {
*field = value.clone();
return;
}
}
}
}
fn dominance(allele: char) -> u8 {
if allele.is_ascii_lowercase() {
return 1;
}
if allele.is_ascii_uppercase() {
return 2;
}
return 0;
}

74
src/main.rs Normal file
View File

@ -0,0 +1,74 @@
use choice_function::ChoiceFunction;
use creature::{Creature, Phenotype};
use genetics::Genotype;
use rand::{rng, seq::SliceRandom};
mod choice_function;
mod creature;
mod genetics;
fn generate_creature() -> Creature {
let genotype: Genotype = Genotype::generate::<Phenotype>(ChoiceFunction::Random);
let creature = Creature {
// name: String::from("Pyrex"),
genotype,
};
return creature;
}
fn evolve() {
const N_ITERATIONS: usize = 200;
const N_CREATURES: usize = 100;
const N_SURVIVORS: usize = 90;
let mut pool = vec![];
for _ in 0..N_CREATURES {
pool.push(generate_creature());
}
for iteration in 0..N_ITERATIONS {
report(iteration, &pool);
// kill N creatures
let mut survivors = pool.clone();
survivors.sort_by(|x, y| fitness(x).partial_cmp(&fitness(y)).unwrap());
survivors.reverse();
survivors.drain(N_SURVIVORS..N_CREATURES);
let mut pool2 = vec![];
let mut survivors_queue = vec![];
while survivors_queue.len() < N_CREATURES * 2 {
survivors_queue.extend(survivors.clone());
}
survivors_queue.shuffle(&mut rng());
for _ in 0..N_CREATURES {
let lhs = survivors_queue.pop().unwrap();
let rhs = survivors_queue.pop().unwrap();
pool2.push(lhs.breed(&rhs));
}
pool = pool2;
}
}
fn report(iteration: usize, pool: &[Creature]) {
println!("== Iteration {} ==", iteration + 1);
println!("{}", pool[0].profile());
println!("");
}
fn fitness(creature: &Creature) -> f64 {
let phenotype: Phenotype = creature.genotype.load();
let mut value = 0.0;
if phenotype.parts.has_wings() {
value += 0.5;
}
value -= phenotype.coloration.body.r.value() as f64 / 4.0;
value += (phenotype.stats.iq.value() as f64) / 10.0;
return value;
}
fn main() {
evolve();
}