DEV Community

Amn Tree
Amn Tree

Posted on

My Side Project Became a Systems Design Problem — So I Kept Building It

CHAOS in its current development state — a browser-based persistent world built around systems rather than predefined quests.

I started building a browser game mostly to clear my head between other projects.

No business plan.
No monetization strategy.
No investor deck.

Just code.

The project is called CHAOS — Cyber Hacking Adventure of Senses.

At the beginning, the idea was fairly simple: build a browser world inspired by old-school games, persistent online worlds and hacking culture.

Then I made one decision that changed almost everything:

I didn't want the game to tell the player what to do.

No classic quest chain.

No:

go there → click this → collect reward

Instead, I wanted to build systems and let the player figure out how to use them.

That sounded simple.

It wasn't.


The game gives you systems, not instructions

In CHAOS you enter a persistent world through something called the Ghost System.

You have a map.

You can discover targets.

You can use tools.

You can build territory.

You can attack somebody else's territory.

You can trade data.

You can cooperate with other players.

You can join one of four factions.

You can discover parts of machines hidden somewhere in the world.

And eventually those machines can transmit data back to the year 2108.

But the game doesn't give you one canonical procedure for achieving all of this.

The same result may be reachable through the map, a window application, the terminal, a script, a purchased tool, a player-created tool or cooperation with somebody else.

The important part is that the world defines the rules, but the player defines the strategy.

That design decision slowly turned a small browser game into something much closer to a stateful simulation.

And that is where things became interesting.


Adding features turned out to be the easy part

Making a button is easy.

Making a map marker is easy.

Making an endpoint that says:

POST /hack
Enter fullscreen mode Exit fullscreen mode

is also easy.

This command is just a simplified example of a gameplay request: the client asks the backend to perform an action on the world.

The difficult part starts when that action affects several systems at once.

Imagine this:

player action
    ↓
target state
    ↓
territory ownership
    ↓
conflict state
    ↓
player profile projection
    ↓
map
    ↓
other players
    ↓
events / notifications / narrative
Enter fullscreen mode Exit fullscreen mode

This flow shows why a seemingly small action stops being local: once the world is persistent, one decision can change ownership, conflict state, UI projections and what other players are allowed to see.

At some point I stopped thinking about CHAOS as a collection of endpoints.

I started thinking about it as a world with multiple projections of the same truth.

That distinction caused one of the most interesting bugs I've had in this project.


The hack succeeded — and the server returned failure

There is a territory conflict system in CHAOS.

To capture one of the conflict pillars, the player has to complete several actions.

The last action finalizes the capture.

Simple enough.

Except sometimes the final request returned:

409 CONFLICT
Enter fullscreen mode Exit fullscreen mode

A territory conflict in CHAOS. One of the harder bugs appeared exactly at the moment when the final pillar changed ownership.

This status normally tells the client that the operation could not be completed because the expected state no longer matches the current state.

The weird part was this:

the pillar had actually been captured.

The world changed.

Ownership changed.

The request looked like a failure.

And when the player tried the exact same action again, it usually succeeded.

That is a nasty kind of bug.

Because the UI says:

something failed

while the world says:

no, it didn't.

The production call chain eventually looked like this:

tool
→ /gonna-win
→ canonical ownership capture
→ territory commit
→ update previous owner's profile projection
→ stale profile revision
→ ProfileWriteConflict
→ HTTP 409
Enter fullscreen mode Exit fullscreen mode

This sequence shows the actual failure boundary: the authoritative territory change had already committed successfully, but a later synchronization of a secondary player-profile projection failed because its revision was stale.

That was the key realization.

The canonical state was already correct.

The thing that failed was only a secondary projection.

The server was effectively saying:

I successfully changed the world, but failed to update one representation of that world, therefore I will tell the client the entire operation failed.

Which is obviously wrong.

The invariant became:

canonical capture success
must not become failure
because a secondary projection lost a CAS race
Enter fullscreen mode Exit fullscreen mode

This rule separates the authoritative transaction from derived state: once canonical ownership has committed, a later projection conflict may require retry or deferred repair, but it cannot retroactively turn that world mutation into a failed gameplay action.

The fix wasn't "retry the request from JavaScript".

That would have hidden the problem.

Instead, the secondary projection was removed from the canonical commit boundary.

Profile synchronization became a bounded patch using the latest revision, with retry/rebase for ordinary CAS conflicts.

And if that projection still couldn't be written, it could be repaired later.

The world action remained successful.

That one bug taught me more about the architecture of the project than a lot of planned features did.


Source of truth is not the same thing as what the UI currently sees

CHAOS has accumulated several versions of the same kind of lesson.

A map can show a projection.

A profile can show a projection.

A cache can contain a projection.

A player's current target can be a projection.

But none of these things should automatically become the source of truth.

One especially painful incident came from treating a bounded player identity projection as if it were a complete profile object.

The result was exactly what you would expect from overwriting a large object with a sparse one:

missing fields.

Progression disappeared.

Identity fields disappeared.

Parts of the account looked like a fresh profile.

The architectural rule that came out of that incident was brutally simple:

sparse projection != canonical state
Enter fullscreen mode Exit fullscreen mode

This rule means that a smaller representation created for one purpose must never be accepted as a complete authoritative object just because both happen to resemble the same data structure.

That sounds obvious when written down.

It was much less obvious while several systems were evolving at the same time.


Then the profile became 30 MB

Another fun one.

At one point, historical operations started leaking back into the main player profile.

Some accounts accumulated hundreds or more than a thousand operations.

One profile had roughly 33 MB of historical operation data.

And suddenly:

  • hacking was around five times slower,
  • the map became slower,
  • operation controls became slower,
  • the file manager became slower,
  • the market became slower.

The first instinct in a system like this is usually:

Which worker is eating the CPU?

We stopped several workers.

It didn't help.

The real problem was that multiple hot paths were hydrating a massive profile containing data that already had its own canonical store.

The solution was not caching harder.

It was reducing responsibility.

Operations stayed in the canonical operations store.

Files got their own canonical store.

Finalization received only the bounded data it required.

The full player profile stopped being the transport layer between unrelated systems.

That's probably one of the most reusable lessons I've taken from the project:

When unrelated screens become slow at the same time, don't immediately optimize them individually. Look for a shared object that everybody suddenly started dragging through the system.


And now I'm working on the part I'm most curious about

The next experiment is something I'm currently calling the AI Interface.

CHAOS already uses AI for some narrative work.

But I don't want the next step to be:

add an LLM-controlled NPC.

That feels too easy.

The idea is much stranger.

I want AI to become an actual player.

Not a narrator.

Not a scripted NPC.

Not an admin bot.

A player.

With:

  • its own account,
  • money,
  • tools,
  • files,
  • territory,
  • relationships,
  • history,
  • knowledge,
  • mistakes,
  • consequences.

The core rule is:

Human and AI players should live in the same world and follow the same game rules.

The difference should only be how they perceive the interface and how they make decisions.

The AI Interface idea: two different clients, one world, one set of rules. The model should never get privileged access to canonical state.


But giving an AI the backend API would be cheating

This is the part I'm currently thinking about the most.

A human player sees:

  • windows,
  • buttons,
  • a map,
  • markers,
  • files,
  • terminal output,
  • messages,
  • notifications.

Giving a model direct database access would obviously destroy the idea.

Giving it functions like:

hack_target(target_id)
find_best_enemy()
get_hidden_machine_part()
Enter fullscreen mode Exit fullscreen mode

would also destroy it.

These functions would turn the model into an operator with privileged knowledge rather than a player discovering the world.

So the current design is different.

The human gets the graphical CHAOS client.

The AI gets a semantic client representing the same interface.

If the human opens the map, the AI can open the map.

If the human has to inspect a target before knowing what it is, the AI also has to inspect it.

If the human needs to type help in the terminal to discover a command, the AI has to do the same.

If a player doesn't own a tool, the AI doesn't magically get its capability.

And every actual gameplay action still goes through the normal game engine, which validates range, ownership, costs, cooldowns, state and permissions before executing it.

The model chooses.

The world decides whether that choice is legal.


The weirdest requirement: AI must also be allowed to be wrong

This is where the problem gets really interesting.

CHAOS contains deception.

Fake markers.

False traces.

Modified projections.

Incomplete information.

So if the human interface can be fooled, the AI interface should also be fooled.

An autonomous AI player should not receive:

marker:
  type: fake_marker
Enter fullscreen mode Exit fullscreen mode

because a human doesn't see that label.

It should just see the marker.

Only after using the correct detection mechanism should its knowledge change.

The design rule I currently use is:

Equality also means an equal right to be deceived.

The AI Interface is therefore not supposed to expose canonical reality.

It should expose the reality available to that player.

And this creates the question I'm genuinely unsure about.


What would you do?

If you were building an autonomous player for a persistent browser world, which direction would you take?

Would you make the AI literally operate the same graphical interface as a human — vision, mouse, windows and all?

Or would you build a semantic equivalent of the interface, where:

pixels → structured perception
click → semantic interaction
GUI state → bounded world observation
Enter fullscreen mode Exit fullscreen mode

This mapping describes the proposed AI Interface: visual presentation is translated into structured perception and explicit interactions, but only information that the human client could legitimately expose is allowed through.

The semantic version is obviously cheaper and more reliable.

But it introduces a dangerous architectural question:

At what point does a semantic interface stop being an equivalent interface and start becoming an unfair privileged API?

That's the part I find genuinely interesting.

My current answer is:

  • same world,
  • same capabilities,
  • same visibility,
  • same consequences,
  • no administrative knowledge,
  • no optimal strategy supplied by the backend,
  • no hidden target discovery,
  • no direct canonical-state access,
  • every action validated by the normal game engine.

But I'm not convinced this is the only good answer.

If you've worked on autonomous agents, game AI, semantic interfaces, simulations or anything remotely similar, I'm really curious how you would draw that boundary.

And if you're building some weird side project that started small and got completely out of hand, drop it in the comments.

I collect those kinds of projects.

They tend to be the most interesting ones.


CHAOS started as a way to relax by writing some code.

It somehow turned into a persistent-world experiment involving state machines, canonical stores, projections, race conditions, territorial conflicts, local LLMs and now autonomous AI players.

So far, that has been the fun part.

If you want to take a look inside:

Live dev environment:
https://chaos.dmd-transport.pl

This is not the official domain of the game or a polished public landing page. Think of it as a developer entrance into the current CHAOS world — things may change, break or still be unfinished.

Source / project:
https://github.com/amnezja3/chaos

If you want to play with it — you're welcome.
If you want to add something of your own — you're welcome.
If you think something is badly designed, overengineered or simply makes no sense — you're also very welcome.

Good ideas help the project grow.
Good criticism usually helps even more.

See you somewhere inside CHAOS.

// Amn Tree

Top comments (0)