Testing Evolution in Project TwinStick

1 - The Evolution of Damage Resistance

How can we encode a damage resistance game mechanic that interacts well with a generational model of evolution?
Author

Barrie D. Robison

Published

August 9, 2023

Is it really an Evolutionary game?

When we are developing our games, we perform extensive testing to make sure the underlying biological models are performing as expected. In the case of evolutionary games, we need to test that the population of enemies is indeed adapting the game conditions as we intended. This post is (I hope) the first in a series in which we document those tests.

My hope is that performing these tests in this format will serve as an organized archive of our analyses, improving reproducibility and rigor. I also have a vain glimmer of hope that some person other than me might actually be interested in this topic.

PROJECT TWIN STICK

This is intended to be an evolutionary shooter. The game is described in detail here.

DATA

In this section, we ingest the data from whatever runs are relevant to the analysis. The data are written from the project in .csv files. The following code reads all .csv files from the working directory. It creates new variables for the source file name (file) and the number of offspring produced by each individual (offspring_count). It then appends all the data files into a single data frame called allfiles. I also create a few aggregations of the data by generating mean values of interest (traits, genes, fitness estimates) for each generation in each file (TraitAvg, GeneAvg, FitAvg)

Code
library(tidyverse)
library(pheatmap)


files <- list.files(pattern = "*.csv", full.names = TRUE)

allfiles = data.frame()
for(csv in files){
  Twin3 <- read.csv(csv, as.is=T, header=T)
  Twin3['file'] = csv


Twin3<-Twin3%>%
  mutate(Unique.Slime.ID = paste(Wave.Number, ".", Slime.ID))%>%
  mutate(Unique.Parent.One = paste(Wave.Number-1, ".", Parent.One))%>%
  mutate(Unique.Parent.Two = paste(Wave.Number-1, ".", Parent.Two))


df_parents <- Twin3 %>%
  select(Unique.Parent.One, Unique.Parent.Two) %>%
  pivot_longer(cols = everything(), names_to = "parent_type", values_to = "parent_id")

# Count the number of offspring for each parent
offspring_counts <- df_parents %>%
  group_by(parent_id) %>%
  summarise(offspring_count = n(), .groups = "drop")

offspring_counts <- offspring_counts%>%
  filter(parent_id != "-1 . N/A")


offspring_counts<- rename(offspring_counts, Unique.Slime.ID = parent_id)



Twin3 <- Twin3 %>%
  left_join(offspring_counts, by = "Unique.Slime.ID")%>%
  replace_na(list(offspring_count = 0))

allfiles<-rbind(allfiles,Twin3)

}



Traits <- c("Main.Resistance.Trait", "Secondary.Resistance.Trait", "Speed.Trait",
           "Tower.Attraction.Trait", "Slime.Optimal.Distance.Trait", "Turn.Rate.Trait", 
           "Slime.View.Range.Trait", "Tower.View.Range.Trait")

Genes <- c("Main.Resistance.Gene", "Secondary.Resistance.Gene", "Speed.Gene",
           "Tower.Attraction.Gene", "Slime.Optimal.Distance.Gene", "Turn.Rate.Gene", 
           "Slime.View.Range.Gene", "Tower.View.Range.Gene")

allfiles<-allfiles%>%
  mutate(Generation=as.factor(Wave.Number))%>%
  mutate(offspring.count.Fitness = offspring_count)%>%
  mutate(reproduce = if_else(offspring_count == 0, "N", "Y"))
   

TraitAvg <- allfiles %>%
  group_by(file, Generation) %>%
  summarize(across(ends_with("Trait"), mean,  na.rm = TRUE))

GeneAvg <- allfiles %>%
  group_by(file, Generation) %>%
  summarize(across(ends_with("Gene"), list(mean = mean, var = var), na.rm = TRUE, .names = "{.fn}.{.col}"))

FitAvg <- allfiles %>%
  group_by(file, Generation) %>%
  summarize(across(ends_with("Fitness"), list(mean = mean, var = var), na.rm = TRUE, .names = "{.fn}.{.col}"))

The allfiles dataframe contains the following variables (I also show a few columns of the example data):

Code
data.dictionary <- t(as.data.frame(head(allfiles)))
knitr::kable(data.dictionary)
1 2 3 4 5 6
Slime.ID 0 1 2 3 4 5
Wave.Number 0 0 0 0 0 0
Path.Distance.To.Player 11.578880 9.865794 11.072530 8.656490 12.428300 12.332610
Player.Distance.Fitness 3974.917 4601.597 4141.634 5177.865 3723.478 3750.202
Parent.One N/A N/A N/A N/A N/A N/A
Parent.Two N/A N/A N/A N/A N/A N/A
Main.Type Fire Laser Lightning Fire Laser Blaster
Secondary.Type Lightning Ice Fire Laser Fire Ice
Main.Resistance.Gene 2.060887 1.567136 0.538767 -1.465660 4.846877 1.900932
Main.Resistance.Trait 0.5781593 0.5477995 0.4836787 0.3620645 0.7333565 0.5683771
Secondary.Resistance.Gene 0.01938968 -0.25444700 -3.73592900 1.81140400 0.34911740 -0.43072260
Secondary.Resistance.Trait 0.18314960 0.17312900 0.08061782 0.25977300 0.19580530 0.16691090
Slime.View.Range.Gene 1.4521710 -1.3032780 -0.8455277 1.1033670 -2.8070340 -0.5393281
Slime.View.Range.Trait 9.846652 7.288872 7.710257 9.527897 5.971337 7.995144
Tower.View.Range.Gene 0.7422258 -1.4057490 -0.5228980 0.2925837 -3.1005300 -0.1302060
Tower.View.Range.Trait 22.85026 17.52136 19.69461 21.73113 13.61480 20.67451
Player.View.Range.Gene -0.7063282 1.1152780 -0.6345016 1.2877800 -0.4439387 0.6586446
Player.View.Range.Trait 3.356673 4.271331 3.389172 4.366956 3.476874 4.025174
Wall.View.Range.Gene 0.13138430 1.63479000 1.03197000 2.12256400 0.06305833 1.54371300
Wall.View.Range.Trait 5.131722 6.345041 5.838480 6.771520 5.080783 6.266964
Sheep.View.Range.Gene 1.7711210 -1.7662380 0.8589699 1.0857650 0.7905225 1.7732480
Sheep.View.Range.Trait 6.462865 3.869560 5.697818 5.882667 5.642784 6.464712
Slime.Attraction.Gene -0.97361480 2.85983300 -0.00132436 1.41690400 2.96294500 0.60026410
Slime.Attraction.Trait 0.4394477 0.6714959 0.4999172 0.5876420 0.6771568 0.5374463
Tower.Attraction.Gene -0.4425031 -2.3597370 -1.6222210 1.6432250 -5.8312040 0.7847461
Tower.Attraction.Trait -0.29616710 -0.49673860 -0.42417950 -0.04456747 -0.75258950 -0.15074900
Player.Attraction.Gene -0.6821441 0.7002198 -0.3265617 3.7018040 -1.6177000 0.2446628
Player.Attraction.Trait 0.01473093 0.18535970 0.05911088 0.50999690 -0.10185810 0.12984560
Wall.Attraction.Gene 0.1195688 -0.1511556 -2.9512140 3.4388130 3.0539160 0.5988827
Wall.Attraction.Trait -0.8776227 -0.8847079 -0.9392208 -0.7577341 -0.7749578 -0.8641599
Sheep.Attraction.Gene 0.8572116 -1.5254840 -0.7902940 -0.7743754 -0.2995906 -1.1352990
Sheep.Attraction.Trait 0.7710594 0.6499067 0.6904933 0.6913431 0.7160806 0.6717642
Slime.Optimal.Distance.Gene 0.2664202 3.6042880 -1.0556060 0.8011839 0.4063749 1.7429750
Slime.Optimal.Distance.Trait 0.2759589 0.6047078 0.1175039 0.3365067 0.2920418 0.4364783
Speed.Gene 2.6708090 2.7693390 1.7675290 1.2229970 0.9674612 -3.1236780
Speed.Trait 3.594294 3.638617 3.201688 2.978000 2.876638 1.588666
Turn.Rate.Gene -2.003947000 -0.003989722 2.665061000 -0.611806700 -0.087987470 0.942467800
Turn.Rate.Trait 0.1190994 0.1822768 0.3028560 0.1607104 0.1791677 0.2202203
Sprint.Duration.Gene 0.3580502 -0.3317875 -0.3822251 -0.3363649 0.5547686 0.4983550
Sprint.Duration.Trait 2.611816 2.396376 2.380646 2.394948 2.673088 2.655535
Sprint.Cooldown.Gene 0.17904180 1.97089800 0.69569630 -0.44410650 1.28146400 0.03339072
Sprint.Cooldown.Trait 2.723206 4.388537 3.336164 1.953815 3.913494 2.541734
X NA NA NA NA NA NA
file ./geneWriteFile08_10_2023_13-47-10.csv ./geneWriteFile08_10_2023_13-47-10.csv ./geneWriteFile08_10_2023_13-47-10.csv ./geneWriteFile08_10_2023_13-47-10.csv ./geneWriteFile08_10_2023_13-47-10.csv ./geneWriteFile08_10_2023_13-47-10.csv
Unique.Slime.ID 0 . 0 0 . 1 0 . 2 0 . 3 0 . 4 0 . 5
Unique.Parent.One -1 . N/A -1 . N/A -1 . N/A -1 . N/A -1 . N/A -1 . N/A
Unique.Parent.Two -1 . N/A -1 . N/A -1 . N/A -1 . N/A -1 . N/A -1 . N/A
offspring_count 0 0 0 0 0 0
Generation 0 0 0 0 0 0
offspring.count.Fitness 0 0 0 0 0 0
reproduce N N N N N N

Variables that end in .Gene are the values of the genome for that particular locus. Variables that end in .Trait are the values of the trait for that particular locus. Variables that end in .Fitness are the values of that particular Fitness component.

EXPERIMENTAL CONDITIONS

The enemies in this game are slimes of several different types. These types correspond to the different damage types that can be caused by the player’s towers (Lightning, Ice, Fire, etc). Our initial idea is that the slimes of a specific type should be resistant to that type of damage. The average resistance of the whole population should increase over time if the player relies on only one type of tower.

The current resistance model is as follows:

Damage Resistance = (Main.Type)0.6 + (Secondary.Type)0.4

Where Main.Type and Secondary.Type = 1 if they match the Damage Type and 0 if they do not.

Our hypothesis is that resistance of the population will adapt to the damage type of the defense used. If true, this hypothesis predicts that the mean resistance of the population will increase over time, primarily through the proliferation of slimes of a Type that matches the Damage Type.

We constructed a simple defense in the center of the playing area, consisting of three lightning towers. The only Fitness Function at this time is related to Path Distance to the Player (the closer they get to the player, the more fitness they acquire).

RESULTS

Slime Types

Each Slime has a Main.Type and a Secondary.Type. These types use the ~.Resistance.~ category to confer resistance to the appropriate damage type.

The following code creates two summary dataframes with the suffix ~Typecounts that count the number of slimes of each ~.Type in each generation for each replicate. It then creates the graphs of ~Type frequency over time.

Code
MainTypecounts <- allfiles %>%
  group_by(Main.Type, Generation, file) %>%
  summarise(Main.count = n(), .groups = "drop")

SecondaryTypecounts <- allfiles %>%
  group_by(Secondary.Type, Generation, file) %>%
  summarise(Secondary.count = n(), .groups = "drop")

            

ggplot(MainTypecounts, aes(x = Generation, y = Main.count, fill = as.factor(Main.Type))) +
  geom_col(position = "stack") +
  labs(x = "Generation", y = "Count", fill = "Main Slime Type") +
  theme_minimal()+
  facet_wrap(~file, ncol=2)

Code
ggplot(SecondaryTypecounts, aes(x = Generation, y = Secondary.count, fill = as.factor(Secondary.Type))) +
  geom_col(position = "stack") +
  labs(x = "Generation", y = "Count", fill = "Secondary Slime Type") +
  theme_minimal()+
  facet_wrap(~file, ncol=2)

These results align with the predictions of our hypothesis. There is an obvious proliferation of Lightning type slimes (both Main and Secondary) in all 6 replicates.

Slime Fitness

In most cases, it is useful to summarize the behavior of the fitness function for each experiment. In this case, the fitness function calculates a value of 50,000/(distance to player +1). I will also reverse calculate that for visualization, showing the actual distance to the player (Path.Distance.To.Player). We then use Roulette Wheel selection to determine the parents of the next generation.

Code
ggplot(allfiles, aes(x=Wave.Number, y= Path.Distance.To.Player))+
  geom_jitter(aes(x=Wave.Number, y= Path.Distance.To.Player, color = offspring_count, alpha = offspring_count))+
  geom_smooth()+
  facet_wrap(~file, ncol = 2)+
  scale_color_continuous(low="blue", high = "red")+
  ylim(0, 80)
`geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'
Warning: Removed 1 rows containing non-finite values (`stat_smooth()`).
Warning: Computation failed in `stat_smooth()`
Caused by error in `smooth.construct.cr.smooth.spec()`:
! x has insufficient unique values to support 10 knots: reduce k.
Warning: Removed 1469 rows containing missing values (`geom_point()`).
Warning: Removed 4 rows containing missing values (`geom_smooth()`).

This is a jitter plot, which helps prevent overplotting of all the points. The Y axis appears to be messed up because there was one slime in one replicate that died very far away from the player (value of 300). I’ve limited the axis to make the plots more readable. There is also a bug that sets the path distance to 0 in generation 0, and Justin is working on that.

In a plot like this, we expect to see that points closer to 0 on the y axis are more opaque and red, indicating that those individuals had more offspring. Looks pretty good in that regard. We also note that there are obviously two groups of slimes - one that dies far away and one that gets pretty close.

Code
ggplot(allfiles, aes(x=Wave.Number, y= log10(Player.Distance.Fitness)))+
  geom_jitter(aes(x=Wave.Number, y= log10(Player.Distance.Fitness), color = offspring_count, alpha = offspring_count))+
  geom_smooth()+
  facet_wrap(~file, ncol = 2)+
  scale_color_continuous(low="blue", high = "red")
`geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'
Warning: Computation failed in `stat_smooth()`
Caused by error in `smooth.construct.cr.smooth.spec()`:
! x has insufficient unique values to support 10 knots: reduce k.

This plot is the same general concept as the previous one, but I am using the transformed values for fitness. I’ve log transformed because of the distribution created by the transformation (something we’ll have to address in development I think). In this case, points near the top should be more red if the fitness function is working.

Code
ggplot(allfiles, aes(x=Path.Distance.To.Player, y = Player.Distance.Fitness))+
  geom_point(aes(x=Path.Distance.To.Player, y = Player.Distance.Fitness, color = offspring_count),  alpha = 0.5)+
  facet_wrap(~file, ncol = 2)+
  scale_color_continuous(low="blue", high = "red")

This is the relationship between true path distance and transformed values. I think this is going to be a problem. The function creates a weird distribution for fitness values.

Code
ggplot(allfiles, aes(x=log10(Path.Distance.To.Player), y = log10(Player.Distance.Fitness)))+
  geom_point(aes(x=log10(Path.Distance.To.Player), y = log10(Player.Distance.Fitness), color = offspring_count),  alpha = 0.5)+
  facet_wrap(~file, ncol = 2)+
  scale_color_continuous(low="blue", high = "red")

This is the log plot of the two variables, which suggests we might consider using log10(Fitness) during the selection step?

Code
ggplot(data = FitAvg, aes(x = Generation, y = var.offspring.count.Fitness))+
    geom_col()+
    theme(legend.position = "none") +
    facet_wrap(~file, ncol = 2) 

Code
ggplot(allfiles, aes(x = as.factor(Wave.Number), y = log10(offspring_count+1))) + 
  geom_boxplot(fill="lightblue") +
  theme(legend.position = "none")+
  facet_wrap(~file, ncol = 2)

These two plots help us understand the relationship between the fitness function and the TRUE measure of fitness - number of offpring. The first plot shows us the variance in offspring_count for each Generation. High variance indicates that not all individuals have an equal probability of mating, which is indicative that selection is acting on the population.

The second plot is a box plot of offspring_count by Generation, which gives us an idea of what the distribution looks like.

Overall, I’d say these game conditions create selection in the early game, between Generation 1 and 3. The question is, what TRAITS are associated with this variation in Fitness?

Evolutionary Responses

To estimate what traits might be under selection, we can calculate selection gradients for each trait. This is essentially the slope of the line between offspring_count and the Trait.

For each Trait, we should also try to understand its individual evolutionary trajectory. Is the population mean for the trait increasing or decreasing?

Since the first thing we are interested in is Damage Resistance conferred by Type, we’ll calculate Lightning resistance directly.

Code
allfiles <- allfiles %>%
  mutate(LResist.Trait = case_when(
    Main.Type == "Lightning" & Secondary.Type == "Lightning" ~ 1.0,
    Main.Type == "Lightning" & Secondary.Type != "Lightning" ~ 0.6,
    Main.Type != "Lightning" & Secondary.Type == "Lightning" ~ 0.4,
    TRUE ~ 0
  ))

traittemp <- allfiles %>%
  select(Generation, offspring_count, file, LResist.Trait) %>%
  group_by(Generation, file) %>%
  mutate(scaleST0 = as.vector(scale(LResist.Trait, center = TRUE))) %>%
  mutate(scaleST02 = scaleST0 * scaleST0) %>%
  mutate(Generation = as.numeric(as.character(Generation)))




Gradients <- traittemp %>%
  group_by(Generation, file) %>%
  do({
    model <- lm(offspring_count ~ scaleST0 + scaleST02, data = .)
    data.frame(
      Beta = coefficients(model)[2],
      PB = summary(model)$coef[2, 4],
      Trait = "LResist.Trait"
    )
  })

Gradients <- Gradients %>%
  mutate(sig = if_else(PB < 0.05 , "Y", "N"))

TraitAvg <- allfiles %>%
  group_by(file, Generation) %>%
  summarize(across(ends_with("Trait"), mean,  na.rm = TRUE))
`summarise()` has grouped output by 'file'. You can override using the
`.groups` argument.
Code
ggplot(data = TraitAvg, aes(x = as.numeric(Generation), y = LResist.Trait))+
    geom_smooth(data = TraitAvg, aes(x = as.numeric(Generation), y = LResist.Trait), method = "loess") +
    theme(legend.position = "none") +
    facet_wrap(~file, ncol = 2) 
`geom_smooth()` using formula = 'y ~ x'

Code
ggplot(Gradients, aes(x=Generation, y = Beta))+
  geom_point(aes(color = sig))+
  geom_smooth(fill="blue")+
  scale_color_manual(values = c("grey","red"))+
  geom_hline(yintercept=0, linetype="dashed", color = "black")+
  theme(legend.position = "none",
        panel.background = element_blank())
`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Now we’ll perform a similar analysis for the remaining traits.

Code
Gradientslong <- data.frame()
for(i in seq_along(Traits)){

traittemp<-allfiles%>%
  select(Generation, offspring_count, file, !!sym(Traits[i]))%>%
  group_by(Generation, file)%>%
  mutate(scaleST0 = as.vector(scale(!!sym(Traits[i]), center = TRUE)))%>%
  mutate(scaleST02 = scaleST0*scaleST0)%>%
  mutate(Generation = as.numeric(as.character(Generation)))

Gradients <- traittemp %>%
  group_by(Generation, file) %>%
  do({
    model <- lm(offspring_count ~ scaleST0 + scaleST02, data = .)
    data.frame(
      Beta = coefficients(model)[2],
      PB = summary(model)$coef[2, 4],
      Trait = Traits[i]
    )
  })

Gradients <- Gradients %>%
  mutate(sig = if_else(PB < 0.05 , "Y", "N"))

Gradientslong <- rbind(Gradientslong, Gradients)

G <- ggplot(data = GeneAvg, aes(x = as.numeric(Generation), y = !!sym(Genes[i])))+
    geom_point(data = allfiles, aes(x = as.numeric(Generation), y = !!sym(Genes[i])), size=0.1, alpha = 0.02)+
    geom_smooth(data = GeneAvg, aes(x = as.numeric(Generation), y = !!sym(paste("mean.",Genes[i], sep = "")), method = "loess")) +
    theme(legend.position = "none") +
    facet_wrap(~file, ncol = 2) 

P <- ggplot(data = TraitAvg, aes(x = as.numeric(Generation), y = !!sym(Traits[i])))+
    geom_smooth(data = TraitAvg, aes(x = as.numeric(Generation), y = !!sym(Traits[i])), method = "loess") +
    geom_point(data=allfiles, aes(x = as.numeric(Generation), y = !!sym(Traits[i]), color = offspring_count), size = 0.5, alpha =0.1)+
    theme(legend.position = "none") +
    facet_wrap(~file, ncol = 2) 



S <- ggplot(Gradients, aes(x=Generation, y = Beta))+
  geom_point(aes(color = sig))+
  geom_smooth(fill="blue")+
  scale_color_manual(values = c("grey","red"))+
  geom_hline(yintercept=0, linetype="dashed", color = "black")+
  theme(legend.position = "none",
        panel.background = element_blank())

print(G)

print(P)

print(S)

}
Warning in geom_smooth(data = GeneAvg, aes(x = as.numeric(Generation), y =
!!sym(paste("mean.", : Ignoring unknown aesthetics: method
`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

`geom_smooth()` using formula = 'y ~ x'

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'
Warning in geom_smooth(data = GeneAvg, aes(x = as.numeric(Generation), y =
!!sym(paste("mean.", : Ignoring unknown aesthetics: method

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

`geom_smooth()` using formula = 'y ~ x'

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'
Warning in geom_smooth(data = GeneAvg, aes(x = as.numeric(Generation), y =
!!sym(paste("mean.", : Ignoring unknown aesthetics: method

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

`geom_smooth()` using formula = 'y ~ x'

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'
Warning in geom_smooth(data = GeneAvg, aes(x = as.numeric(Generation), y =
!!sym(paste("mean.", : Ignoring unknown aesthetics: method

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

`geom_smooth()` using formula = 'y ~ x'

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'
Warning in geom_smooth(data = GeneAvg, aes(x = as.numeric(Generation), y =
!!sym(paste("mean.", : Ignoring unknown aesthetics: method

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

`geom_smooth()` using formula = 'y ~ x'

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'
Warning in geom_smooth(data = GeneAvg, aes(x = as.numeric(Generation), y =
!!sym(paste("mean.", : Ignoring unknown aesthetics: method

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

`geom_smooth()` using formula = 'y ~ x'

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'
Warning in geom_smooth(data = GeneAvg, aes(x = as.numeric(Generation), y =
!!sym(paste("mean.", : Ignoring unknown aesthetics: method

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

`geom_smooth()` using formula = 'y ~ x'

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'
Warning in geom_smooth(data = GeneAvg, aes(x = as.numeric(Generation), y =
!!sym(paste("mean.", : Ignoring unknown aesthetics: method

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

`geom_smooth()` using formula = 'y ~ x'

`geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Heatmap

Code
GradMatrix <- Gradientslong %>%
  select(Generation, Trait, Beta)%>%
  pivot_wider(names_from = Trait, values_from = Beta)
Adding missing grouping variables: `file`
Code
paletteLength <- 50
myColor <- colorRampPalette(c("blue", "white", "#ED2024"))(paletteLength)
# length(breaks) == length(paletteLength) + 1
# use floor and ceiling to deal with even/odd length pallettelengths


Heatmap <- GradMatrix %>%
  ungroup()%>%
  select(Main.Resistance.Trait, Secondary.Resistance.Trait, Slime.View.Range.Trait, 
         Tower.View.Range.Trait, 
         Tower.Attraction.Trait, Slime.Optimal.Distance.Trait, 
         Speed.Trait, Turn.Rate.Trait)







Heatmatrix2 <- as.matrix(Heatmap)

myBreaks2 <- c(seq(min(Heatmatrix2), 0, length.out=ceiling(paletteLength/2) + 1), seq(max(Heatmatrix2)/paletteLength, max(Heatmatrix2), 
                                                                                      length.out=floor(paletteLength/2)))


heatmap2 = pheatmap(Heatmatrix2,
         cluster_rows = FALSE, # don't cluster rows
         cluster_cols = TRUE, # don't cluster columns
         clustering_distance_cols = "euclidean",
         clustering_distance_rows = "euclidean",
         clustering_method = "complete",
         color = myColor,
         breaks = myBreaks2)

SINGLE GENERATION PLOTS

Code
singlegen <- allfiles %>%
  filter(Generation == 5)

singlegrad <- Gradientslong %>%
  filter(Generation == 3)


ggplot(singlegen, aes(x=Speed.Trait) )+
  geom_histogram()+
  facet_grid(reproduce~file)
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Code
ggplot(singlegen, aes(x=Turn.Rate.Trait) )+
  geom_histogram()+
  facet_grid(reproduce~file)
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.

Code
ggplot(singlegen, aes(x=Turn.Rate.Trait, y = Speed.Trait, color = reproduce))+
  geom_point()+
  facet_wrap(~file)