Auto Evo Re-Architecture

I’m currently starting the process of trying to implement / port over Thim’s autoevo to the current repo. Since a large amount of code is already going to be rewritten, I wanted to start a discussion about fixing the rampant architectural problems present in it.

  • Readability:
    Trying to get an understanding of AutoEvo by how it’s written is nightmarish. You are constantly jumping between files, many of which with just a few lines, trying to follow a trail. You often hit roadblocks in this trail, like when a step is dequeued and run. You must go back and find when it was queued. There are loops within loops within functions with more loops. The AutoEvo algorithm is not actually that complex, it’s just obfuscated behind all of this code.

  • Steps:
    This is part of the real core of the problem and partially the reason I made this post. The steps code mutilates readability for what? I can’t actually see in gain from it. If someone could explain this design choice that would be great, as this is where almost all of the readability issues stem from. A system like this throws all attempts at readability out of the window, hiding everything behind layers of obfuscation.

  • API:
    While the auto evo good is well segregated from the rest of the codebase, in many cases, it’s too well segregated. As I found out in the past, it’s very difficult to get data out. Almost everything except the main result is hidden by the layers of obfuscation mentioned before. It is very hard to interface with it.

Maybe someone can explain to me why the architectural decision were made the way they were, as for right now they are quite confounding and the source of the dread often associated with the auto evo code.

  • How does Thim’s Auto-Evo meaningfully differ from our current implementation?

  • What is the benefit in adopting his system? You realize Thim did a lot for our Auto-Evo, and likewise got shot down for his ideas just as much because people won’t agree.

  • How does this bring us closer to NickTheNick’s ultimate design? Or looking back even further, the original work started by TJWhale and our founding members?

  • Why should Auto-Evo be better integrated with a C# oriented code base when the original code was a C++ design and may have to go back to C++?

Those are my only real questions, we usually have big discussions before big decisions but, lately not much has been happening on here. (I suspect because of HH working on the prototypes and power-members such as FuJa, Narotiza, and Max no longer being around). I haven’t tried Thim’s branch but, I don’t have any bias against it, just curious.

As for why Auto-Evo is how it is, based on what I can gather, the math and code for Auto-Evo is actually pretty old. Originally drafted/written by people who say, knew a lot of math and biology but, not best practices like HH and Thim. There was probably a lot of “holy Belgium it works, nobody touch it!”. Add to this ideal C++ and ideal C# are different universes, on top of HH & Untrusted (or whoever) just starting to pickup the C# language when they translated it.

Basically, the same dilemma as the cloud code.

1 Like

The same thing exists all over the codebase. The assumption is that people must use a C# IDE with jump to definition and easy jump back to navigate the code. There’s no other sensible way to read the code. In my opinion once you get the hang of that, the only actually hard logic to follow is trying to navigate through Godot signal connections as you need to keep switching between the Godot editor and your IDE to follow the program flow. Compared to that jumping to the definition of each part of auto-evo and then jumping back is much easier.

It separates the auto-evo into two parts:

  • The overall framework with the steps setup
  • The actual population simulation

This allows flexibility in easily configuring the separate steps, reodering the steps if needed. The step approach also makes multithreading easy and allows the background threads to easily abort the run after one step is completed. The steps also allow providing a progress indicator to the player.

Without this design the auto-evo core logic would need to be littered with progress updates and each algorithm part would need to separately consider how to add multithreading.

The design with the decoupled steps also gives the flexibility that each step can do whatever it wants to. And with inheritance it is easy to make slight variants of steps.

Just writing tight loops of logic without any of the higher level control would result in a much more unmaintainable very tightly coupled code.

This is entirely intentional. Remember that the current auto-evo was just supposed to be a simple algorithm done before we do it properly. That’s why it doesn’t make sense to allow the other game code to read the auto-evo internals, because it would then be much, much harder to modify the auto-evo. Imagine if in addition to having to rework the entire auto-evo algorithm, you’d need to update a bunch of other code in the game as it touched too many internal auto-evo things that no longer existed with the update.

This is also an excellent point. Though, I think it is a bit unlikely, but we could theoretically move the auto-evo algorithm to C++ for a bit of extra performance. Then we’d definitely need a fully specified API interface to write and ready any data that needs to go in/out of auto-evo.

I kind of don’t agree with this sentiment… as I already said I think the current auto-evo system with the two different layers to it, is actually a good design. Though, I’m not going to claim that it is definitely the most optimal design here. If someone suggest a different architecture we can then discuss the cons and benefits of it.

1 Like

After reading this, I find the code’s structure a lot more agreeable. It’s true, we haven’t even probably done 50% of what TJWhale and the others before him formulated when they originally came up with the math for Auto-Evo (which was the entirety of this projects major design activity for years at one point…) To lock ourselves into something that would be a nightmare for an experienced person to change would be way worse than having something that’s inconvenient to navigate for the inexperienced, who shouldn’t be touching it anyways at this point I think.

We really need to do some proper software engineering and documentation more at this point rather than just making a mess of our game’s heart. So many exceptional systems in the ancient forums that haven’t gotten realized because, one guy speaks Greek and numbers while the other guy asks for pseudo-code, and then somebody just figures out something that is a placeholder but, becomes the feature.

I do think, after reading Thim’s write-up, that we should seriously consider some of the techniques and structures he came up with (Miches), and high-quality posts like that are more in-line with what should be done in the future, in my mind. That doesn’t mean add it all right now, at this second though. As intended, we need to actually use this forum for specifics.

1 Like

A big issue is that I’m basically the only line of defence against PRs that modify the game but don’t remember to update comments, which happens pretty often. With separate architecture documents that would be basically even worse. That’s one of the main reasons why I haven’t opened issues about writing more explanation documents. And well even the documentation issues we have currently don’t seem to be that interesting to people. The final nail in the coffin seems to be that basically no one reads the files in the repo docs folder, so even if such a document exists the flow I’d imagine would be something like: someone complains about something being hard to understand and then someone points out there’s a document for that.

But I agree that we should try to do better. And well maybe we should start with parts of the game that haven’t been written with the explicit goal in mind of replacing it all later.

I said on the last release stream that I tried to read the miches concept but couldn’t really grasp it. So I’d need to put major time into understanding the new concepts for auto-evo before I can participate in any discussions related to that.

to be fair hh, you are a Linux user, you know “RTFM” is nothing new and radical lol. But yeah, I see what you’re saying. I just think especially huge things should have something explaining it.

Well I’ll still be annoyed when I need to point out that some piece of documentation exists, despite it being linked in various places.

  • flexibility in easily configuring the separate steps
  • reordering the steps if needed

Directly replacing steps with normal function calls does not affect these points at all. Setting up functions and reordering them is easy. Just make a policy of never writing anything but function calls in the main class. This is instantly 1000 times more readable

  • makes multithreading easy
  • The steps also allow providing a progress indicator to the player
  • Without this design the auto-evo core logic would need to be littered with progress updates and each algorithm part would need to separately consider how to add multithreading.

Instead of steps, multithread at the species or patch level. That is way more readable as well as the same code will be run in every thread. Progression can also be shown by how many species have been run through. Before it was being shown by steps, which is an entirely arbitrary value to a player, and in most cases a dev.

  • Just writing tight loops of logic without any of the higher level control would result in a much more unmaintainable very tightly coupled code.

Who said they need to be tightly coupled? They simply don’t need to be abstracted in harmful ways. Good code practice still exists. Still keep them separate and call them in functions as you would run the step. It’s the exact same minus the abstraction.

Additionally, a lot of information is hidden within the step system. For example how much population sim gets called. It’s something you can’t really know without someone telling you or spending way to much time in that portion of the codebase.

My general points here are:

  • Replace steps with direct function calls.
  • Change multithreading the species level
  • Keep a strict style of function callers and function implementers to keep the split, but without the readability drawback. "For each species in each patch call this set of functions: "

For the first one, I think it’s worth trying. If we like it worse, simply don’t merge it. I see some advantages, but that’s something for a thread on its own.

And also, having a readable codebase will help with the second point. No matter what we decide to do it will be easier maintain and expand apon.

Except a progress counter and cancellation would need to be checked between each function call. Even then the function calls need to be really granular to allow the run to abort within about 100ms of the cancellation request.

I’d like some proof of that. People make multithreading bugs very easily. As such my opinion is that when the higher level controls threading and the lower level just has straightforward code it’s much better. Much less chance for bugs and someone working on auto-evo doesn’t also need to be an expert in multithreading their code.

How would this model fit when environmental compound changes are calculated? Is environmental changes a species shown in the progress? With steps it is all perfectly clear that anything that needs to be simulated for the world can be included.

Well, I look forward to your concrete suggestion as to what can be done.

I don’t really get this point. There’s a ton of places in the code that you don’t know how much they get called. Does the average Thrive programmer need to know how many times ChemicalEquation.UpdateEquation gets called? I suspect not, and even for me I only have a rough idea without checking the multiple code paths where it is called from. Which is another IDE function I think is essential.

Species can be in multiple patches at once. How do you handle that? Do you simulate potential migrations or species splits in all patches a species is in? That’s going to be much worse for performance.

Also the biodiversity fill operation operates on scarsely populated patches, not on any species.

Fundamentally the auto-evo is greared towards running specific kinds of operations, some of which are given a species as a parameter:

  • Fill up biodiversity lacking patches
  • Apply really harsh competition to cause extinctions
  • Calculate final populations
  • Find a good mutation
  • Find a good migration
  • Only apply 20% strength to the player species

This check can be made within the multithreading code, to stop spinning up threads on abort and simply wait for the current ones to be finished.

I agree wholeheartedly. But in neither suggestion so far do people need to touch multithreading code. Converting steps to function calls maintains the same split, just without the need for the additional step stuff.

That’s a choice we will have to make, but by no means a blocking one.

I used possibly a bad example, but it’s something I ran into. I wrote some code differently then I would have otherwise due to not having that information. Normally if you write something you have some general idea on how it will be used, but once it gets fed into auto evo, for most intents and purposes it goes into the void.

Given this new info, run a thread for each patch instead of species. Same general code, but I now agree that would be better.

You just above at least alluded to it. As I said auto-evo needs to be able to cancel running within a few hundred milliseconds. Moving to much bigger work units requires putting checks about needing to quite interleaved with the calculation code there. Otherwise they will not be able to quit fast.

It is, because if you want a simplified design that doesn’t fit all the parts, putting all the parts back will end up with more complexity anyway. So a simple design is only simple if it addresses all the parts it needs to without “overextending” the design.

How does that then generate the overall result with migrations, mutations and splits? A species split occurs in at least 2 patches at once, and that would be a thread communication nightmare to keep up consistent state for the species.

Overall the design is flexible enough that unless someone wants to fundamentally change how mutations or migrations are tested, nobody needs to touch the general auto-evo framework.

Instead any population simulation changes can be done just to the population simulation part, which I hope is straightforward enough code just calling functions for your liking. And if someone wants to change mutations or name generation or something like that there’s the specified classes handling those. The mutations code quite clearly demonstrates what happens when you just add more and more features without having an overall design: it has really long methods that do stuff and then modify the results more and more.

If I can cut in here, I don’t think you two will accomplish that much more with a code review of hypothetical code.


I fully agree with this assessment. I wanted to defend the architecture design decisions I made for the auto-evo system, which I think are still sound.