Ledger Guild Simulator Devlog


Building The Ledger: balancing a game with no combat

A devlog about indirect control, a headless balance harness, and breaking the economy.

The thesis came before the game

The starting question was what a management sim looks like if the manager's distance from the action is the point rather than a budget constraint. Not "you can't afford combat animation, so it's text." Instead: you never see the fight, and that absence is the design.

Everything followed from that. You never assign anyone to a contract. You decide what exists on the board, what it pays, and what you have told them about it. Adventurers read the board and self-select based on greed, ego, caution, morale, and whatever they hold against you. The fight happens somewhere you are not and returns as a report.

That single decision routes all tension through incomplete information, which is exactly what a browser game can do well. Text resolution becomes the authored surface instead of the fallback.

The second load-bearing choice was a terminal clock. An open-ended management sim sags into unbounded optimization. So there is a thing accumulating underneath, fed by contracts you decline to answer, and eventually it gets a name.

The prototype and the first thing that broke

The first playable build had a working board, a roster, an ecology, and an appeal function. It also had a fatal problem: almost nothing got signed. Three hundred and sixty-three contracts lapsed against one closed.

The appeal math was the culprit. It compared the per-head purse against an adventurer's expected wage, subtracted a risk term, and required the sum to clear a threshold. The numbers happened to land such that a Copper evaluating a routine job scored 0.98 against a threshold of 0.9. Marginal. Anything harder failed outright.

The fix was not a constant. It was a model. Adventurers now compute an asking price: their wage for the duration, inflated by how dangerous they read the job to be. The purse is compared against that, on a log scale, so doubling the pay is worth a fixed amount of appeal regardless of tier.

asking = wage * (1 + max(0, risk) * 2.6) pay    = greed * clamp(log2(per / asking), -2.6, 2.0) * 1.45 

That one change made the whole economy legible. To get somebody onto a suicidal job you have to pay for the risk.

The harness

Balancing by playing is hopeless for a game with a three hundred day run and five interacting economies. So the engine was written to be headless from early on, and a build script strips the HTML wrapper and exports the game state as a CommonJS module:

node -e "extract <script> contents, prepend DOM stubs, append module.exports" 

DOM access lives behind a thin proxy stub. advanceDay() runs the whole day in order. From there I could write bot managers with different policies and run twelve seeded runs each in about four seconds.

Five policies, held constant across every rebalance:

  • selective, contracts only (post only work the roster clearly clears)
  • balanced, contracts only
  • selective + engagements
  • balanced + engagements
  • aggressive + engagements

The target was never "all bots win." It was a spread: distinct strategic errors producing distinct failure modes. That target caught things playtesting would have taken weeks to surface.


Five balance bugs worth naming

1. Failure priced off the fee

Early on, a failed contract cost the guild 25% of the purse. Since fees scale steeply with tier, one posted tier-six job that went badly could bankrupt a starting guild in twenty-one days. The fix was to price failure off who you sent, not what it paid: survivors are owed time served at their rank, and the dead are owed a death benefit. A failure now costs what the people cost, which is both fairer and more legible.

2. Adventurers could see through the fog

Roughly 40% of contracts carry a hidden complication that shifts real difficulty by up to a third. But the risk estimate used c.diff, the true value. Adventurers were perfectly informed about a thing the player had to pay to learn.

Splitting baseDiff (what the tier implies) from diff (the truth) fixed it, and it fixed the outcome distribution at the same time. Before the split, 86% of contracts came back as Triumph. Self-selection was so good at avoiding risk that nothing was ever in doubt. Once the parties were reading the posting rather than the truth, marginal jobs started going wrong in ways that were the player's fault.

3. Party size was never priced

This one hurt. Adding solo contracts exposed that difficulty scaled with tier only. A one-body tier-two job carried the same workload as a two-body tier-two job. One person could not do it.

Fixing that with partyScale(tier, party) = (party / expectedParty(tier)) ^ 0.70 also removed something nobody had noticed: 30% of contracts had always arrived with one more body than their difficulty assumed, because of a random +1 in the party formula. The entire economy had been tuned on that free bonus. Twelve of twelve runs went bankrupt the moment it was priced correctly.

Recalibrating from that took a coefficient sweep and a full band refit.

4. Outcome bands fitted to the wrong distribution

Outcomes are decided by a margin: party power against difficulty, plus noise. Setting those band thresholds by intuition produced nonsense, because self-selection means parties only sign when they are already comfortably ahead. The measured median margin was +0.60, not zero.

So the bands are measured, not guessed. A probe runs eight seeded games, records the margin at the moment every party departs, adds the resolution noise, sorts, and reads percentiles:

n=949   disaster<-0.31   failed<-0.02   costly<0.30   clean<1.19 

Those numbers go straight into the resolution function. Whenever a difficulty coefficient changes, the probe re-runs and the bands get refit. There is a shell script for it.

5. Splitting the day broke recovery

When the day was split into phases, wound recovery moved into the Rest standing order, which only applies to idle members. Anybody signed, in the field, or on engagement stopped healing entirely. Meanwhile unanswered house matters were defaulting to their last option, which was frequently the costly one.

The simulation caught both inside one run: victories dropped from ten of twelve to two. Everyone mends a little at dusk now, Rest adds on top, and an unanswered matter costs nothing.

From realtime to phases, in three passes

The first build ran in realtime with speed controls. It was legible on paper and terrible in practice. The complaint that mattered: "My standing kept dropping even though I thought I was managing things well, because I did not realize the adventurers were failing quests."

That is an information architecture failure, not a polish problem. Outcomes were buried in a scrolling log on a different tab, so consequences arrived without their causes.

Pass one added a mandatory day report. Every return, with its outcome, its money, its standing change, and the field report prose, on one page. Plus a "Why standing moved" section, which required routing every standing change through a function that will not accept a delta without a reason string.

Pass two made manual the slowest setting and gave the day a beat before it landed.

Pass three removed realtime entirely and committed to five phases with transition plates. The design risk there was empty phases, so each one got content it did not have before:

  • Dawn got the runner, one free errand a morning
  • The Board got the signing moment moved to the phase exit, so leaving it is a visible beat
  • The Yard got standing orders for idle members and, later, companies
  • The House got a matter deck, now twenty-seven cards, one decision every day

The lesson is that phase structure is only worth taking if you are willing to build content for the phases. Gating existing UI into a sequence just adds taps.

The information layer is the game

Three systems all turned out to be the same system.

Scouting started as a gold purchase and was priced off tier while listing was priced off fee. At tier two that was 64g to scout against 14g to list, so nobody ever scouted. Both are proportional now, and the morning runner does one for free, which turned scouting from a luxury into a daily habit.

The bestiary was the second half. Thirty-one creatures, each with a bane: the one preparation it is worst against.

scout
scouted party worth +9%
provision
kit worth half again as much
study
guild knowledge worth 11.5% per level instead of 7.5%
numbers
full party +10%, understrength −8%

Effect

The counter stays sealed until you have run a contract against the creature or bought a level of study, which gives study a second payoff beyond the raw multiplier.

The interest list was the third. For a posted contract, the game shows every eligible member and their position: keen, considering, wants more pay, too dangerous for the pay, beneath them, aggrieved. That is not a hint. It is the same appealParts() function that drives the actual decision, printed out. The self-selection system stopped being mysterious the moment it was legible.



Less luck, more preparation

A late note asked for less RNG, so that good decisions get rewarded. Resolution variance dropped from σ 0.19 to σ 0.13 and the bands were refit. To fill the gap, preparation got stronger: provisions from 2.4 to 3.2 per member, study from 6% to 7.5% per level.

That immediately made the game too easy, because with less noise a competent policy wins reliably. The answer was not to put the noise back. It was to make the late game actually steepen: a compounding +17.5% difficulty per year, an omen that accrues faster each year, and an opening tier cap that steps up one per season instead of jumping after three weeks.

The shape now is what was asked for. Year one is forgiving. Year four is not.

Being too selective starves you. Leaning entirely on standing engagements buys safe money and loses the war. Reckless posting bankrupts you. There is a band in the middle where the decisions actually matter, and finding that band was the entire job.

Files

the-ledger.html Play in browser
6 days ago

Leave a comment

Log in with itch.io to leave a comment.