Skip to content

Commit

Permalink
rearrange defaults
Browse files Browse the repository at this point in the history
  • Loading branch information
stjohnfinn committed Oct 20, 2024
1 parent 781de29 commit 49047cf
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 55 deletions.
7 changes: 0 additions & 7 deletions TODO.md

This file was deleted.

125 changes: 77 additions & 48 deletions src/terrarium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
*/

/**
* Organism
*
* Defines the type for a single member of a genetic algorithm's
* population. The only assumption we make is that it has genes.
*
Expand All @@ -22,8 +20,6 @@ interface Organism {
}

/**
* GeneticAlgorithmModel
*
* The "model" that represents the state of a genetic algorithm at any given
* moment.
*/
Expand All @@ -34,23 +30,19 @@ interface GeneticAlgorithmModel {
}

/**
* GeneticAlgorithm
*
* The "controller" that manages a genetic algorithm model. This class handles
* the flow of the genetic algorithm from start to finish, from frame to frame,
* and from generation to generation.
*/
class GeneticAlgorithm {
/**
* stepFunction
* Progresses a genetic algorithm model to it's next frame.
*
* @param model - the GeneticAlgorithmModel that the step function should
* progress.
*/
private stepFunction: (model: GeneticAlgorithmModel) => void;
/**
* shouldTerminate
* checks if the genetic algorithm should stop running
*
* @param model - the GeneticAlgorithmModel that should be checked.
Expand All @@ -60,7 +52,6 @@ class GeneticAlgorithm {
*/
private shouldTerminate: (model: GeneticAlgorithmModel) => boolean;
/**
* shouldProgressGeneration
* checks if the genetic algorithm should progress to the next generation
*
* @param model - the GeneticAlgorithmModel that should be checked.
Expand All @@ -70,7 +61,6 @@ class GeneticAlgorithm {
*/
private shouldProgressGeneration: (model: GeneticAlgorithmModel) => boolean;
/**
* produceNextGeneration
* this function is called to produce the next generation after a generation
* ends.
*
Expand All @@ -80,15 +70,13 @@ class GeneticAlgorithm {
*/
private produceNextGeneration: (model: GeneticAlgorithmModel) => GeneticAlgorithmModel;
/**
* createOrganism
* used to create the firstGeneration and optionally, used in more places than
* that (but that's up to user's discretion).
*
* @returns a completely new Organism.
*/
private createOrganism: () => Organism;
/**
* calculateFitness
* calculates the fitness of a single organism.
*
* @param organism - the organism that should be analyzed.
Expand All @@ -97,7 +85,6 @@ class GeneticAlgorithm {
*/
private calculateFitness: (organism: Organism) => number;
/**
* crossover
* performs "reproduction" of two organisms, producing a single offspring
*
* @param parentA - the first parent organism
Expand All @@ -107,7 +94,6 @@ class GeneticAlgorithm {
*/
private crossover: (parentA: Organism, parentB: Organism) => Organism;
/**
* mutate
* mutates a single organism
*
* @param organism - the organism that should be mutated
Expand All @@ -125,6 +111,10 @@ class GeneticAlgorithm {
* moment.
*/
private isRunning: boolean;
/**
* debug - enables or disables logging.
*/
private debug: boolean;

/**
*
Expand Down Expand Up @@ -152,39 +142,16 @@ class GeneticAlgorithm {
createOrganism: () => Organism,
stepFunction: (model: GeneticAlgorithmModel) => void,
calculateFitness: (organism: Organism) => number,
crossover: (parentA: Organism, parentB: Organism) => Organism,
mutate: (organism: Organism) => Organism,
shouldTerminate: (model: GeneticAlgorithmModel) => boolean,
shouldProgressGeneration: (model: GeneticAlgorithmModel) => boolean,
crossover?: (parentA: Organism, parentB: Organism) => Organism,
// this is super ugly, but I decided it was the best method for having a
// default value.
produceNextGeneration: (model: GeneticAlgorithmModel) => GeneticAlgorithmModel = (model: GeneticAlgorithmModel) => {
let newModel = structuredClone(model);
newModel.generation++;

// find two best parents
const sortedPopulation: Organism[] = newModel.population.sort((a, b) => {
return this.calculateFitness(b) - this.calculateFitness(a);
});

const parentA: Organism = sortedPopulation[0];
const parentB: Organism = sortedPopulation[1];

// perform crossover
newModel.population = [];
for (let i: number = 0; i < newModel.populationSize; i++) {
newModel.population.push(this.crossover(parentA, parentB));
}

// mutation
for (let i: number = 0; i < newModel.populationSize; i++) {
newModel.population[i] = this.mutate(newModel.population[i]);
}

return newModel;
},
produceNextGeneration?: (model: GeneticAlgorithmModel) => GeneticAlgorithmModel,
// properties
populationSize: number = 50) {
populationSize: number = 50,
debug: boolean = false) {

// initialize the model ****************************************************
this.model = {
Expand All @@ -197,17 +164,19 @@ class GeneticAlgorithm {
this.createOrganism = createOrganism;
this.shouldTerminate = shouldTerminate;
this.shouldProgressGeneration = shouldProgressGeneration;
this.produceNextGeneration = produceNextGeneration;
this.stepFunction = stepFunction;
this.calculateFitness = calculateFitness;
this.crossover = crossover;
this.crossover = crossover ?? this.prefabs.crossover.standard.bind(this);
this.produceNextGeneration = produceNextGeneration ?? this.prefabs.produceNextGeneration.standard.bind(this);
this.mutate = mutate;

// member variables ********************************************************
this.debug = debug;
this.isRunning = false;

// generate new population *************************************************
for (let i = 0; i < this.model.populationSize; i++) {
this.log("creating the first generation.");
this.model.population.push(this.createOrganism());
}

Expand All @@ -216,10 +185,12 @@ class GeneticAlgorithm {
* initialize the genetic algorithm object and then start it whenever they
* want.
*/

this.log("Here's the model:");
this.log(this.model);
}

/**
* step
* this function handles the progression & flow of the genetic algorithm. it
* progresses from frame to frame, generation to generation, and stops running
* if that's appropriate.
Expand All @@ -230,21 +201,25 @@ class GeneticAlgorithm {
if (this.shouldTerminate(this.model)) {
// the genetic algorithm is completely finished, so let's stop
this.isRunning = false;
this.log("determined the GA should stop here.");
}

// this block must come before any block that calls next()
if (this.isRunning === false) {
this.log("the GA is not running right now, exiting game loop.");
// we should NOT continue the loop, so let's just exit
return;
}

// generation is finished, lets create offspring and mutate
if (this.shouldProgressGeneration(this.model)) {
this.log("progressing to the next generation.");
this.model = this.produceNextGeneration(this.model);
}

// we're still running the genetic algorithm, so go to the next frame
if (this.isRunning) {
this.log("progressing to the next frame.");
this.stepFunction(this.model);
this.next();
}
Expand All @@ -260,24 +235,78 @@ class GeneticAlgorithm {
* up more on requestAnimationFrame docs for more information.
*/
private next(): void {
this.log("adding the step function to the requestAnimationFrame queue.");
requestAnimationFrame(() => {
this.step();
});
}

/**
* play()
*
* Purpose: super abstract way to just be like "hey I want to start the
* algorithm".
*/
play(): void {
this.isRunning = true;
this.log("trying to start the GA.");
if (!this.isRunning) {
this.isRunning = true;

this.next();
this.next();
}
}

pause(): void {
this.log("stopping the GA.");
this.isRunning = false;
}

/**
* can be used to log content to the console, only actually logs stuff if the
* `debug` variable is `true`.
*
* @param message content that should be logged to the console
*/
log(message: unknown): void {
if (this.debug) {
console.log(message);
}
}

private readonly prefabs = {
produceNextGeneration: {
/**
* performs pretty basic stuff to produce a new generation. first, finds
* the two most fit organisms from the current population (to be used as
* parents). Then it performs crossover. Then it mutates the offspring.
* Can be used as a pretty reliable default.
*
* @param model genetic algorithm model to modify
* @returns a genetic algorithm model with a new generation
*/
standard: (model: GeneticAlgorithmModel): GeneticAlgorithmModel => {
let newModel: GeneticAlgorithmModel = structuredClone(model);
newModel.generation++;

// find two best parents
const sortedPopulation: Organism[] = newModel.population.sort((a, b) => {
return this.calculateFitness(b) - this.calculateFitness(a);
});

const parentA: Organism = sortedPopulation[0];
const parentB: Organism = sortedPopulation[1];

// perform crossover
newModel.population = [];
for (let i: number = 0; i < newModel.populationSize; i++) {
newModel.population.push(this.crossover(parentA, parentB));
}

// mutation
for (let i: number = 0; i < newModel.populationSize; i++) {
newModel.population[i] = this.mutate(newModel.population[i]);
}

return newModel;
}
}
}
}

0 comments on commit 49047cf

Please sign in to comment.