People assume a platform built for subscriptions, commerce and content can't host a real-time multiplayer game. We built one to find out. BizDevVibes is a dice-market board game — the genre you know from classic property games, with one rule changed that changes everything — and it runs as a VBWD plugin: backend, admin backoffice and player UI, on the same core that serves the shop and the CMS.

This is part one of two. Here we cover the architecture: how a game plugin is laid out, why the rules engine is kept surgically pure, and how that purity buys you auditable prices and free replay. Part two covers the board builder, LLM opponents and monetisation.

The BizDevVibes board in the VBWD user app: a 40-square loop whose colour groups are funnel stages, four seats each holding 15000 credits, a Roll the dice panel, and a game chat sidebar.
The running game inside the VBWD user app. The board is funnel-40: 40 squares whose colour groups are pipeline stages — Discovery Call, Needs Analysis and Budget Check form one set; Onboarding, Integration and Go-Live another. Completing a set means owning a whole stage of the funnel.

The one rule that makes it a market

Before the architecture, the mechanic — because it's what forces the engineering.

You roll two dice, a and b. You may move a, b, or a+b. The sum is free — that's fate, and fate never costs anything. Either single die is a priced purchase. And the fee you pay goes to your opponents, never to the bank.

That last clause is the whole design. In the classic game, fees paid to the bank are a sink: the leader's lead compounds and the loser's position decays. Here, buying your way out of fate funds the people you're beating. The game self-damps.

The same board mid-turn: chat shows the player rolled 6 plus 5 with dice chips 6, 5 and 11; a negotiation window is open; three target squares are highlighted showing 90 cr, 0 cr and FREE.
A live turn. The roll was 6 + 5, so the legal moves are 6, 5 and 11. The engine has priced each one directly on the board: CRM Platform costs 90 cr, Cold Email costs 0 cr, and the sum — Discovery Call — is FREE. The negotiation window is open, so opponents can bid to keep you on the free sum before you choose.

Every one of those prices is computed, not authored. Which means the pricing function has to be trustworthy enough that a player who loses 90 credits believes the number.

Layout: a game is just a plugin

There's no game-specific machinery in VBWD core. BizDevVibes is three plugins across the three repos, exactly like the shop or the booking system:

vbwd-backend/plugins/bdv/          # rules, API, migrations, tests
  bdv/core/                        # the PURE engine — no framework, no DB
  bdv/models/                      # SQLAlchemy: board.py, match.py
  bdv/services/                    # match_service, seeders, slug
  bdv/routes.py                    # /api/v1/bdv/* and /api/v1/admin/bdv/*
  migrations/versions/             # plugin-owned Alembic revisions
  tests/unit  tests/integration

vbwd-fe-user/plugins/bdv/          # the player UI (Vue 3 + TS)
vbwd-fe-admin/plugins/bdv-admin/   # the board builder

The plugin owns its own tables (bdv_*), its own routes, its own migrations and its own permissions (bdv.play, bdv.boards.*). Core never learns the word "game". That's the platform's central rule — the core stays agnostic, plugins are where domain knowledge lives — and a board game is a good stress test of it, because a game is about as far from "SaaS CRUD" as a domain gets.

What the plugin gets for free from core is most of the boring half of any multiplayer product: user accounts and auth, RBAC and access levels, the admin backoffice shell, a token economy for monetisation, an event bus, and the plugin loader that mounts all of it. The game code is the game.

The pure core, and the test that enforces it

Here's the decision everything else rests on. Everything in bdv/core/ — the dice, the options, the pricing, the fee policies, the economy, the state machine — is pure Python. No Flask. No SQLAlchemy. No clock. No global random.

That isn't style policing, and we don't rely on discipline to maintain it. There's an oracle test that walks the AST of every module in core/ and fails the build on violation:

BANNED_IMPORT_ROOTS = {
    "vbwd", "flask", "sqlalchemy", "alembic", "redis",
    "requests", "httpx", "openai", "anthropic", "plugins",
}

BANNED_CALLS = {
    ("random", "random"), ("random", "randint"), ("random", "choice"),
    ("random", "shuffle"), ("random", "seed"),
    ("time", "time"), ("datetime", "now"), ("datetime", "utcnow"),
    ("uuid", "uuid4"),
}

The subtlety is in what's allowed. dice.py may construct random.Random(seed) — a seeded generator is reproducible. What's banned is drawing from the global RNG or reading the wall clock, because no replay could ever reproduce those.

The strongest assertion in that file is the last one: import the engine and play a match with no Flask app, no database, nothing.

def test_engine_is_importable_without_an_app_context():
    spec = BoardSpec(squares=(...))
    state = new_match(spec, MatchConfig(seed="x", seat_count=2))
    assert len(state.seats) == 2

As the test's own docstring puts it: purity is what makes move prices reproducible and auditable, replay free, and the balance harness runnable at compute cost. If that test goes red, the product claim goes with it.

Deterministic dice

There is no shared RNG anywhere. A roll is a pure function of (seed, cursor):

def roll_at(seed: str, cursor: int) -> Tuple[int, int]:
    """The two dice for a given roll index of a given match."""
    generator = random.Random(f"{seed}:{cursor}")
    return generator.randint(1, 6), generator.randint(1, 6)

Roll 47 of match lucid-gadget-49 is the same two numbers today, next year, and on any machine. Card decks work the same way — shuffled_order(seed, deck, size, reshuffle) gives a draw order derivable from the seed alone.

This is worth doing even if you never build a replay feature, because it turns "the dice felt rigged" from an argument into a computation.

Priced options

The option set is four lines, and the doubles case is the one people miss:

def legal_options(roll):
    """Deduped, ascending. Doubles collapse to two options: {3,3} -> (3, 6)."""
    first, second = roll
    return tuple(sorted({first, second, first + second}))

Pricing is expected-value based, and the entire specification fits in the module docstring:

options(roll{a,b}) = dedup([a, b, a+b])
ev(option)         = immediate value delta of landing on the resulting square
price(option)      = max(0, round(k_price x (ev(option) - ev(sum))))
price(sum)         = 0                      # fate is always free
affordable         = price <= floor(cap_pct x cash)

You pay for advantage over fate. If a single die lands you somewhere no better than the free sum would, the price floors at zero — which is exactly why Cold Email shows 0 cr in the screenshot above while CRM Platform shows 90.

One detail that matters more than it looks: the rounding is explicit ROUND_HALF_UP, not Python's built-in round(). Python uses banker's rounding, which would make prices on a 0.5 boundary depend on the parity of the number — surprising behaviour for money, and the kind of thing that erodes trust in a computed price.

Fee distribution as a strategy, not an if-chain

Who receives the move-purchase fee is the game's main balance lever, so it's a resolvable policy rather than a branch:

class FeeDistributionPolicy(Protocol):
    policy_id: str
    def distribute(self, amount, state, payer) -> Mapping[int, int]: ...

Three ship today. AllToPoorest sends 100% to the seat with the least cash — the strongest self-damping rule, with ties broken on lowest seat index so the outcome stays deterministic. SplitAmongOpponents divides evenly and gives the remainder to the poorest so totals reconcile exactly. And ToBank exists purely as the classic-game control arm, so the balance harness has something to measure against.

Because it's resolved by id, an admin can change the economics of a board from a dropdown — which is where part two picks up.

Solvency: bankruptcy is computed, not assumed

The economy module handles selling, borrowing and interest, and makes one design choice worth copying. A seat that cannot pay is not immediately bankrupt. Bankruptcy is what happens when liquidating everything still falls short — a computed fact rather than a first resort.

That turns the worst moment of a match into a decision: sell, and raise cash while permanently shrinking your income; or borrow, and keep the asset earning while adding a cost that recurs every lap.

Interest is charged on passing GO — not on a timer. That hook is deterministic, already exists in the move resolver, and ties the cost of debt to tempo: a player buying bigger moves laps faster and therefore pays interest more often. The dice market and the debt market push against each other, which is precisely the tension you want.

Replay: the audit guarantee

All of the above adds up to one property. Same spec + same seed + same action log ⇒ byte-identical state, forever.

The replay module rejects a mismatch loudly: replaying against a different spec_hash raises, because a match must always be re-derivable under the exact rules it was played under. Change a board's constants and old matches don't silently re-derive differently — they refuse.

Practically, this means a dispute is resolvable. A player who thinks a 90-credit quote was wrong can have it re-derived from the log months later, and the answer is arithmetic rather than an appeal to server logs.

What this cost

The honest accounting. The plugin carries 15 unit test modules plus integration tests, and the ones that earn their keep are the boring ones — a purity oracle, a naming denylist, engine regressions, a fee-reconciliation test proving distributions sum exactly to the amount paid.

Keeping the engine pure is a real constraint. You cannot reach for the database in the middle of resolving a move; you assemble the state first, resolve, then persist. That's more upfront structure than the obvious approach. It's also why the engine can be exercised thousands of times per second in a balance harness with no infrastructure at all — which is the only practical way to tune an economy with this many interacting dials.

And the platform genuinely carried the rest. Auth, accounts, permissions, the admin shell, the token economy and the plugin loader were not written for this game.

Next

Part two gets to the parts you can see: the admin board builder where the economy constants are form fields, LLM-driven opponents seated at the table, the real-time player UI, and how a match gets paid for. Meanwhile the platform itself is at vbwd.cc — see the plugin catalogue, the architecture, and the developer docs.

Learn more about VBWD

VBWD is a self-hosted, source-available platform for building subscription products, marketplaces, and AI-powered apps. Explore it further: