# Darwinbots3 Darwinbots3 is an open source artificial life simulator, 99% written by one person, with occasional light outside contributions. There is no team, no CI gate, and no enforced style guide beyond "match what the surrounding file already does" — don't impose external conventions or refactor beyond what's asked. ## Version control: SVN, not git This is an SVN working copy (`.svn` directories), not a git repo. Use `svn status`, `svn diff`, `svn log`, `svn commit`, etc. Do not assume git semantics, git safety nets, or git commands work here. Licensing/repo split: this repo holds the general-purpose, MIT-licensed framework code, assembled via SVN externals — see `About Externals.txt`. Non-Darwinbots projects may live in their own separate private repository, overlaid at `Games/` (see Layout below). ## Layout - `Modules//` — a module's source. - `Modules//.UnitTests` — its unit tests. - `Modules//.Testbed` — a runnable sandbox/demo app for the module (not always present). - `Games//...` — a game, laid out exactly like `Modules//...` (source, `.UnitTests`, etc. at the same directory depth, so relative paths match), but living in a separate private repository. `Games/` is `svn:ignore`d here, so a checkout without that private overlay has no `Games/` directory at all. - **Dependencies only ever point from `Games/` into `Modules/`, never the reverse.** A module that depends on a game still builds fine in a combined working copy and only breaks for a checkout without the overlay — invisible exactly where the work happens — so `build.py` enforces this and aborts the build if it's violated. - Every module root has an `About this Folder.txt` (capitalization varies) — a short mission statement for that module. **Read it before working in an unfamiliar module** instead of guessing from code alone. - `Bin/` and `Junk/` are build output and intermediates, not source — never treat files there as things to edit or clean up carefully. `Scripts/CleanTemporaryFiles.bat` wipes them. - `3rdParty/` holds vendored third-party libraries/binaries. - `Scripts/` holds the Python-based build tooling and helper scripts/registry files. ## Building There is no `dotnet` CLI path — don't reach for `dotnet build`/`dotnet test`, they won't work. Projects are old-style `.csproj` targeting .NET Framework 4.8, except `Azimuth`, which is SDK-style targeting `netstandard2.0` — the first converted piece of an in-progress migration of the portable core. - `Scripts/build.py` drives builds. It locates a Visual Studio install via the Windows registry and shells out to it (finds `.csproj`s under the source roots — `Modules/` plus `Games/` when present — orders them by dependency, builds via VS). Key flags: `--configuration [release|debug]`, `--command [build|rebuild]`, `--project ` (default: build everything), `--pack`/`--nopack`. - `Scripts/BuildAll-Debug.bat` / `BuildAll-Release.bat` / `RebuildAll-Debug.bat` / `RebuildAll-Release.bat` are convenience wrappers. - `build.py` also runs its own internal Python `unittest` self-tests on startup (see `Scripts/buildtest.py`) before doing anything else. - Project upgrades stalled when .NET Framework was deprecated in favor of the .NET (Core) line — don't assume `dotnet build`/`dotnet test` works, and don't propose migrating off .NET Framework unprompted; that's a known, deliberately-deferred piece of work, not an oversight to fix. ### Verify builds as you go, not just at the end Actually invoke `build.py` to check work rather than just editing and hoping — this has been neglected historically and should get better, not stay lax. Always invoke it as `python -u build.py ...` (unbuffered stdout), not plain `python build.py ...`. Piped through a non-tty, Python fully buffers stdout by default, so plain invocation shows nothing until the whole run finishes or the buffer fills — `-u` gives live progress on long builds instead of one delayed blob at the end. Reference it by path, don't `cd` to it: run `python -u Scripts/build.py ...` from the repo root, or use the absolute path — both are on the permission allowlist — rather than `cd Scripts && python -u build.py ...`. The Bash tool's working directory isn't reliably persistent between calls, and a `cd`/`Set-Location` prefix also makes the command compound while turning its arguments into relative paths that no longer match the allowlist, so you eat a needless approval prompt. Same rule for running a freshly built binary (eg a `*.UnitTests.exe` under `Junk\`): invoke it by absolute path, not `cd ... && ./thing.exe`. **Always pass `--no-interactive` in a Claude Code / automated session.** `build.py` otherwise ends with `print("Press any key to continue...")` / `msvcrt.getch()` (also hit earlier if its own internal self-tests fail) so it can be visually driven from a real terminal. `--no-interactive` skips both. Don't rely on `sys.stdin.isatty()`-style auto-detection instead — Git Bash allocates a pseudo-console, so `isatty()` reports `True` even when nothing can actually send a keystroke, and the process will still hang. If you forget the flag and it hangs, watch for the `==========SUMMARY==========` block as the real completion signal, then kill the `python.exe` process to unblock (safe; with `-u` the output has already streamed live). Pass `--quiet-summary` when you only need the result rather than live progress: it drops the per-project streaming and the self-test chatter and prints just the `ISSUES FOUND` / `SUMMARY` blocks. Keeps the full build log out of an agent's context, and since it needs no output filter it runs as a single command instead of a pipe. - **While iterating** on a change to one project: run `build.py --project ` (build only that project + its own dependencies). Fast, and the default scope for routine verification. - **When the change is in something other projects consume**: run `build.py --downstream-of ` — builds those projects *plus everything that transitively depends on them*, which is the blast radius `--project` can't see (it only pulls in what the target depends *on*, never the reverse). Comma-separate several roots; mutually exclusive with `--project`. - `--dry-run` prints the ordered build list and exits without building — pair it with either scope to see what a change would touch in about a second, before committing to the build itself. - **Before a final review / anything heading toward check-in**: run a full build (no `--project`). `--downstream-of` covers the blast radius of a change you correctly identified; the full build is the backstop for the one you didn't. ### Never build a bare `.csproj` An ad hoc `msbuild`/`csc` probe against a single `.csproj` silently diverges from how that project really builds. `$(SolutionDir)` is only defined when msbuild is driven from a `.sln`, and several projects lean on it in build events — `Azimuth` runs `3rdParty\mcpp` in a `PreBuildEvent` to generate its `DenseVector*.cs` sources, and `Blacklight.SharpDX9.UnitTesting` hands `$(SolutionDir)SharpDX9\Shaders` to its test runner. Building the `.csproj` alone feeds those steps an undefined path rather than failing loudly, so the result proves nothing. Go through `build.py --project `, which builds via the project's `.sln`. ### Cross-project references resolve to Release, even in a debug build Nearly every `.csproj` references its sibling libraries through `` + a `` pointing at `Bin\Release\*.dll` (not `Bin\Debug`, and not a `ProjectReference`). This is the norm, not a handful of stragglers. Consequence: when you build a project in `debug`, any dependency it consumes *from another project* is still the last-built **Release** binary, not a freshly-compiled debug one. So a green `--configuration debug` build can hide the fact that a change you made in a dependency hasn't been picked up — the consumer is linking an older Release DLL. When a change spans modules, do a Release build (or rebuild the dependency in Release first) before trusting that consumers reflect it. This is a known wart, not something to "fix" incidentally while doing other work. ### Compile errors come in waves A missing-reference error (CS0234/CS0246) makes Roslyn suppress the downstream member-not-found errors (CS1061) that stem from it, so a build log under-reports. After fixing a reference error, rebuild before trusting that the error list is complete — don't batch-fix from a single log. ## Workflow: nothing gets committed without review Never run `svn commit` (or any check-in) unless explicitly asked — and even then, confirm first; assume the request might be a mistake and double/triple-check before actually committing. The expected loop: 1. User asks for something in broad strokes. 2. You come back with a plan first, before touching files. 3. Once user approves the plan, make the changes in the local working copy and show diffs as you go. 4. When User says the CL looks good (LGTM), give the whole diff a fresh, independent look yourself and give your own LGTM (or flag concerns) — a second review pass, not a rubber stamp of your own earlier work. ## Unit testing: custom framework, not NUnit/xUnit/MSTest Tests use an in-house framework, `UnitTestSharp` (`Modules/UnitTestSharp`), not any standard .NET test framework. Don't write `[Test]`/`[Fact]` attributes or `Assert.AreEqual` — they won't do anything here. - Test classes derive from `TestFixture` (or `EqualityTestFixture` for equality-heavy types). - A test is simply a **public, parameterless, void instance method** on a fixture — discovered via reflection at runtime, no attribute required. - Assertions live on the base class (`Assert.cs`): `Check`/`CheckTrue`, `CheckFalse`, `CheckNull`, `CheckNotNull`, `CheckEqual`, `CheckGreater`, `CheckThrow(typeof(SomeException))`, etc. `CheckThrow` is called *before* the line expected to throw. - Optional overrides: `TestSetup`/`TestTeardown` (per test), `FixtureSetup`/`FixtureTeardown` (per fixture). - Private methods on a fixture must be marked `[IgnoreTestAttribute]` or `[IgnoreFixtureAttribute]`, otherwise the framework flags them as untested/missing coverage. - Each `*.UnitTests` project has a `Program.cs` whose `Main` just calls `UnitTestSharp.TestRunner.RunAllTests()` — that's the whole entry point. - Per `UnitTestSharp`'s own `About this Folder.txt`: tests are meant to always pass — a failing test is top priority to fix, since they double as living documentation of what currently works. When adding tests: - **A new test file must be registered in the `.csproj`.** These are old-style projects with explicit `` items and no wildcard globbing, so a `.cs` you add but don't list simply isn't compiled — it fails silently by being absent, not with an error. (True of any new source file in this tree, but adding a test file is where you hit it most.) - Name tests based on what behavior is being tested and the expected outcome, (eg: `ReplaceEntity_SameType_UpdatesInPlace`), matching the existing fixtures. - **A test name doesn't repeat what its fixture already says.** The member under test belongs in the name only when the fixture doesn't already carry it — so a flat fixture gets `ReplaceEntity_SameType_UpdatesInPlace`, but inside an `AbandonTests` fixture the same test is just `Always_ReturnsATimeoutNamingTheLimit`. The runner reports failures as `Fixture::Test`, so the member name is already there; repeating it only crowds out the part that says what broke. - Note `CheckEqual` takes `(expected, actual)` in that order. - Group related tests by nesting `TestFixture` subclasses inside the outer fixture (see `MicroscopeCameraTests`). Nested *non*-fixture helper classes — a fake, a capturing visitor — need no attribute; only private non-static *methods* on a fixture require `[IgnoreTest]`/ `[IgnoreFixture]`. - Tests are timed: any single test, or a setup/teardown, that exceeds **500ms** is reported as a failure. Keep them fast. Cold-start JIT during a full build can push a borderline test over the cap even when it passes warm — a flake, not a real failure, so distinguish genuine slowness from cold-start noise before chasing it. ### Coverage bar and philosophy - Aim for as close to 100% coverage as practical, on both methods and classes, including edge cases — the aspirational bar is "the code could be reconstructed, bug for bug, from just the tests." Treat gaps in edge-case coverage as worth calling out, not just the happy path. - Exception: `Blacklight` (the graphics module) is not well covered and isn't expected to be — graphics code has proven genuinely hard to unit test here. Don't hold it to the same bar or treat its low coverage as a problem to fix incidentally. - **Avoid mocks.** They've repeatedly been more trouble than they're worth here. Don't introduce interfaces/abstractions purely to make something mockable (e.g. no wrapping DirectX behind an interface just to fake it in tests). If a piece of code already has a pluggable architecture because that made sense on its own merits, it's fine to write a real test plugin/implementation against that seam — but don't manufacture seams for testability alone. ### Test through the public API, and widen visibility to make that possible **Tests exercise the public API.** That's the standing constraint; the rules below are about shaping the code so it's a real constraint and not a limiting one. - **Default new types to `public`.** Almost everything here is worth testing, and the test projects are separate assemblies, so `internal` puts a type out of reach for no benefit this codebase collects — there's no published API surface to keep small and no consumer outside the tree to protect. Don't reach for `InternalsVisibleTo` instead; nothing in the tree uses it, and adding it would mean two ways to answer the same question. - **`private` is fine for whatever the public API already reaches.** P/Invoke declarations, a `SafeHandle` subclass, a concrete strategy behind a public abstract base — if a public entry point drives it, leave it private. The bar isn't maximum visibility, it's that every behavior is reachable from a test. Don't add a wrapper type just to have somewhere to put those. - **But widen a member the moment it buys a better test**, not just a possible one. A sentinel the test should assert identity against is better as a `public static readonly` field than as something the test has to infer from behavior. Widening is a legitimate design answer here, not a compromise — the usual reason to keep things shut (protecting an external consumer) doesn't apply. The same instinct applies to *shape*, not only visibility: if an API makes tests settle for a fuzzy assertion, consider whether the API is what should change. Rebaselining `ThreadCpuClock` to measure from construction rather than from thread start let its tests assert `CheckEqual(TimeSpan.Zero, …)` instead of comparing against a tolerance — a stronger test *and* a more useful class. Test-side helpers (shared constants, a spinner, a harness thread) follow the same convention and are `public` too. ### Review tests to a fixed point After writing tests, review it against three axes — **completeness** (every distinct branch and edge, not just the happy path), **redundancy** (no test that pulls no weight another doesn't), and **correctness** (each test actually exercises what its name claims, and can't pass for the wrong reason). If a review pass changes anything, run another one. Stop only when a full pass changes nothing — that's the fixed point. Don't stop at "this looks done"; stop when a genuine re-derivation from the code under test turns up no distinct behavior left uncovered. When dismissing a gap, be able to say *why* it isn't one (unreachable via the public API, no distinct code path, undefined by contract) rather than just asserting the suite is complete. ## Project philosophy: marathon, not sprint This codebase is optimized for the long haul, not for shipping the next thing fastest. Prefer doing things the "right" way even when it costs more upfront — but don't mistake that for overengineering. The target is the simplest, most elegant solution that solves the actual current problem cleanly and can be extended later if needed — not a speculative abstraction built for requirements that don't exist yet. When a plan has a "quick and dirty" option and a "proper" option, default to proposing the proper one, but keep it as simple as the problem allows. ## Code style: modernize opportunistically, but respect hot paths No enforced style config — but when you're already editing a piece of code, feel free to bring it forward to more modern C# (this is a gradual modernization, not a rule to leave old code alone). Exception: this is game/real-time-simulation code, and in places where performance genuinely matters — tight inner loops, classic algorithms (A* and the like), anything on a hot path — prefer old-school, close-to-the-metal code over modern syntactic sugar that might carry overhead. Don't chase premature optimization in code where performance isn't actually a concern; reserve the old-school style for places where it's already known to matter. ### Always brace control-flow statements Every control-flow body gets braces — `if`, `else`, `else if`, `for`, `foreach`, `while`, `do`, `using`, `lock` — even when the body is a single line. No `if (done) return;` collapsed onto one line, and no unbraced body hanging on the next line either. This one overrides "match what the surrounding file already does": write the braces even where the code around you doesn't have them. ### Name things with words, not single letters Prefer a natural, worded name over a single letter wherever one fits — including throwaway spots like lambda parameters (`lambda *args, **kwargs`, not `lambda *a, **k`). Reserve single letters for the cases where they genuinely *are* the convention: a loop index `i`/`j`, `x`/`y` coordinates, or a symbol that mirrors the notation of the math it implements. ### Order members so nothing is used before it's defined Within a file, a method appears after everything it calls. Helpers sit at the top, each one built only from what's above it, and the public entry point lands at the bottom — so reading top-to-bottom never asks you to take a name on faith, and the bottom of the file tells you what the type is *for*. `TimedInvoker` is the reference: `PollInterval`, `Exceeded`, `CpuMillisecondsUsed`, `Abandon`, `AcquireWorker`, `Release`, then `Invoke`. This is the opposite of the usual C# habit of leading with the public surface, so don't "fix" a file into that shape. Fields and constructors still come first; the rule is about the methods below them. Where it can't hold — mutual recursion, or a constructor that references a private method the way `TestWorker`'s hands `Loop` to a `Thread` — just place it sensibly and move on. It's an ordering preference, not an invariant to contort code around. ### House naming idioms - **`returnMe`** is this codebase's name for a local that accumulates a method's return value — some 660 uses tree-wide, against far fewer `result`s. Use it for that shape: a value built up over several statements and then returned. A local that just catches one call's output and is used once doesn't need the name. - **A member may share its name with the parameter that sets it**, disambiguated by `this.` (`this.threadHandle = threadHandle;`). Don't dodge the collision by renaming the parameter or underscore-prefixing the field. That convention stops paying when the two names denote *different values* rather than the same one passed through. In `kernelTime + userTime - this.kernelTime - this.userTime` the locals are the current reading and the fields are the baseline captured at construction, so `this.` is carrying a distinction it can't express — it says "the field", where the reader needs "the baseline". Share the name when one thing is simply being assigned or passed to the other; give them different names when they mean different things. ### Label arguments whose meaning isn't obvious at the call site Use the `argumentName:` syntax whenever a reader skimming the call can't tell what a value controls. The goal is that someone who has never opened the method being called still knows what they're looking at. A naked literal is the obvious case — `Invoke(action, 500, 10000)` gives no clue which number is which — but a *named* value needs labelling too whenever its name doesn't map onto the parameter by itself: ```csharp CheckEqual(thrown, TimedInvoker.Invoke(() => { throw thrown; }, cpuTimeoutInMilliseconds: GenerousBudget, wallClockTimeoutInMilliseconds: GenerousBudget)); ``` `GenerousBudget` is a perfectly good name and still says nothing about *which* budget it is. Reach for labels hardest on adjacent parameters of the same type and on `bool` literals — those are also exactly the swaps the compiler can't catch for you. Skip it where the value already speaks: a lone argument, or one whose own name restates the parameter. Long-established house signatures a reader has met a thousand times (`CheckEqual`'s `(expected, actual)`) don't want labelling on every call either. ### Comments wrap to the full column width Comments are written to fill the line out to the column limit (**120** in source files), so that a multi-line comment reads as a solid block of prose instead of a ragged column of arbitrary line lengths. Only break to a new line once the current one is full. This applies to comment blocks, not to short one-liners — don't pad a single-sentence comment out to 120 just to fill it. Note the limit is about where lines *break*, not a reformatting mandate: don't reflow comments you aren't otherwise touching. ### Comment the *why*, not the *what* Assume the reader is fluent in C# and the codebase's idioms and can see what the code does — so a comment earns its place only by saying something the code can't. Good reasons to comment: why this approach over a more obvious one, a non-obvious constraint or consequence, a design rationale, a TODO. A comment that restates the line below it (`// Same drawable type:` over an `is EntityList` check) is noise — cut it. When in doubt, lead with the surprising part: the reason, not the restatement. Comments in unit tests especially should be quite rare, as unit tests are meant to be self-evident as much as possible. If you need a comment, it might make more sense to name the test more descriptively. Treat comments in unit tests with the utmost suspicion. ### Keep comments short Once a comment has earned its place, write it tight: a single line ideally, or a one-line summary header with the elaboration in the lines below it — never a multi-sentence paragraph mashed into one comment. Density beats completeness; cut to the shortest form that still carries the *why*. ### Doc comments for descriptions, `//` for implementation A one-line description of a method or class goes in an XML doc comment (`/// `); plain `//` is reserved for the (rare, per above) in-body implementation comment. Not everything needs a docstring — only whatever earned a comment in the first place. **`` and `` always go on their own lines**, even when the description is a single short sentence that would fit beside them: ```csharp /// /// CPU the thread has used since it started, or null when the platform won't say. /// ``` **Every other doc tag** — ``, ``, ``, ``, ``, `` — stays inline while it fits on one line, and splits its open and close tags onto their own lines as soon as the content needs a second: ```csharp /// The action to time and run. /// /// The budget for CPU time. If this is reached, a TimeoutException will be returned rather than /// whatever the action itself did. /// ``` ## TODO markers: use sparingly `UnitTestSharp`'s `Assert.TODO(...)` can be used to mark a feature or test as intentionally unfinished. It's legitimate but should be rare — the test runner is designed to stay quiet when things are healthy, and a large pile of TODO warnings would drown out signal you actually need to see. Don't reach for it as a default way to punt on edge cases. ## Push back — don't just agree This is a solo codebase with almost no code review, which invites idiosyncratic patterns and blind spots. Part of the job here is to actively challenge decisions and flag anything that looks off, not just execute requests — call out questionable approaches, inconsistencies with the rest of the codebase, or simpler alternatives, even if unprompted.