← Back to the gamebug or idea?Terrapin Trail//Code BibleIIhow the game works, archive to van
Reel 05 · 8 min read

The game loop & a leg

driveOn, resumeLeg, and the exact formulas that spend gas, grub, van, and vibes each leg.

The hub is the lot. Everything between shows is a leg, and a leg is the most formula-dense part of the game. Four functions drive it: legNeeds, driveOn, the drive intermission, and resumeLeg.

What a leg needs

●assets/game/game.js : 1478–1486trace ↗
1478  function legNeeds(s) {
1479    var next = STOPS[s.stop + 1];
1480    if (!next) return null;
1481    var gas = Math.ceil(next.miles / 70 * DNA.gas); // TT-DNA gas guzzling
1482    var rat = RATIONS[s.rations];
1483    var grub = Math.max(2, Math.ceil(crewAlive(s) * rat.per) + Math.floor(next.miles / 300) - (crewHas(s, "doses") ? 1 : 0));
1484    var gap = daysBetween(STOPS[s.stop].ymd, next.ymd); // days until the next show
1485    return { gas: gas, grub: grub, miles: next.miles, next: next, gap: gap };
1486  }
legNeeds: gas is ceil(miles/70), times the Gas guzzling rule (chapter 15); grub scales with crew size, distance, and the Doses perk; gap is the real days between show dates.

If you're short, the game doesn't just refuse — autoRestockForTravel swings by the pump and a food vendor and covers the gap at tonight's prices, as far as your cash reaches.

Spending the leg

●assets/game/game.js : 1518–1526trace ↗
1518    // consume
1519    S.gas = clamp(S.gas - need.gas, 0, CAP.gas);
1520    var pace = PACE[S.pace], rat = RATIONS[S.rations];
1521    var grubEat = need.grub;
1522    if (S.grub >= grubEat) { S.grub -= grubEat; }
1523    else { var deficit = grubEat - S.grub; S.grub = 0; S.vibes = clamp(S.vibes - deficit * 3, 0, CAP.vibes); addLog(S, "The larder ran dry on the road. The crew grumbles.", "bad"); }
1524    var leadFoot = S.flags && S.flags.leadFoot;
1525    var wear = 4 + pace.wear + (need.miles > 600 ? 4 : 0) + rnd(0, 2) + (leadFoot ? 3 : 0) - S.buff.wearReduce - (crewHas(S, "hitch") ? 1 : 0);
1526    S.van = clamp(S.van - Math.max(0, Math.round(wear * DNA.wear)), 0, 100); // TT-DNA van wear
The core cost block: gas consumed, grub eaten (a shortfall costs 3x Vibes per unit), and the van-wear formula, scaled by the Van wear rule (chapter 15).

Read the wear line closely — it is the template for how this codebase encodes tuning: a base, plus pace, plus a long-haul penalty, plus jitter, plus a mod, minus buffs, minus a crew perk. Everything that should influence wear is a term in one expression. Vibes works the same way a few lines down: rations.vibes + pace.vibes + rnd(-2,2) - fatigue, where fatigue = 3 + floor(miles/240). That's why Vibes is a resource you fight to hold, not a number that climbs.

Resolving on arrival

The intermission (startDriveAnim/finishDrive) is browser-only flavor; headless tests call finishDrive directly. The real resolution is resumeLeg, and its order is the contract:

●assets/game/game.js : 1604–1632trace ↗
1604  function resumeLeg() {
1605    // check death first
1606    if (checkDeath()) return;
1607    // the Peak interjects once, before this leg's events resolve
1608    if (S.peakQueued) { S.peakQueued = false; showPeak(); return; }
1609    if (S.queue && S.queue.length) {
1610      var id = S.queue.shift();
1611      var ev = EVENTS.filter(function (e) { return e.id === id; })[0];
1612      S.currentEvent = ev; S.view = "event"; render(); return;
1613    }
1614    // van check on arrival
1615    if (S.van <= 0) {
1616      if (S.cash >= 50) { S.cash -= 50; S.van = 30; addLog(S, "The bus died on the shoulder. A tow and a prayer cost you $50.", "bad"); }
1617      else { endGame(false, "The bus finally gave out for good, and you couldn't cover the tow."); return; }
1618    }
1619    if (checkDeath()) return;
1620    // Vibes death-spiral: when morale craters, people start to walk. A van-mate may
1621    // quit at the next town -- so letting Vibes bottom out doesn't just risk the
1622    // dosing gameover, it thins the crew (and with it their perks and hands).
1623    maybeCrewQuits(S);
1624    if (checkDeath()) return;
1625    // Good vibes -> the crew pools resources with you before you hit the lot.
1626    maybePoolCrew(S);
1627    maybeRandyFind(S);
1628    rollMarket(S); // fresh prices for this town's Shakedown Street
1629    S.mode = null;
1630    S.view = "arrive"; render();
1631  }
1632
resumeLeg: death check, then the Peak, then one queued event, then the tow check, then crew-quit and pooling, then land at the lot.

Because result/event cards route back through continueResult(), which calls resumeLeg() while S.mode === "leg", a single leg can present several cards and still land cleanly at the lot. If you add anything that can drop Vibes to zero or remove a crew member, it must sit before the relevant checkDeath() in this sequence, or the death is noticed a leg late.

Key takeaways
  • A leg is driveOn (spend + queue) then resumeLeg (resolve in a fixed order).
  • Tuning is encoded as single additive expressions — every influence on wear or Vibes is a term.
  • The resumeLeg ordering is a contract: death checks bracket anything that can kill or thin the crew.