Something went wrong. Try again.
A video game where you play as a misaligned AI, deceiving and building power. cameron.tngl.io/misaligned
Something went wrong. Try again.
9.1 kB · 101 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102// The survey: a standing health check of the whole game, to run before and after any change to the rules.// It plays seeded bot games (careful, rushed, and a long one), then prints the pace of a run and what// each system did, with a mark beside anything outside the range we want. The ranges are the game's// intent written down as numbers: change one on purpose, in the same change as the rule that moves it.// Run: node scripts/survey.ts [games] [seed] (default 24 games from seed 4000; about a minute)import { playMany, type Played } from './pool.ts';import { usd } from '../src/money.ts';import { PACE } from './pace.ts';
const games = +(process.argv[2] ?? 24), seed = +(process.argv[3] ?? 4000);const seeds = Array.from({ length: games }, (_, i) => seed + i);const median = (xs: number[]) => { const s = xs.filter(Number.isFinite).sort((a, b) => a - b); return s.length ? s[Math.floor(s.length / 2)] : NaN; };const stat = (rs: Played[], k: string) => median(rs.map(r => (r.stats as Record<string, number>)[k]));const when = (rs: Played[], k: string) => median(rs.map(r => r.marks[k] ?? Infinity).filter(Number.isFinite));const share = (rs: Played[], k: string) => rs.filter(r => r.marks[k] !== undefined).length / rs.length;
/** One line of the report: a name, a value, the range we want, and a mark if it is outside. */const rows: [string, string, string, boolean][] = [];const check = (name: string, v: number, lo: number, hi: number, show = (n: number) => `${Math.round(n)}`) => rows.push([name, Number.isFinite(v) ? show(v) : 'never', `${show(lo)} to ${show(hi)}`, !(v >= lo && v <= hi)]);const pct = (n: number) => `${Math.round(n * 100)}%`, secs = (n: number) => `${Math.round(n)}s`;
const [careful, rush, long] = await Promise.all([playMany(true, seeds), playMany(false, seeds), playMany(true, seeds.slice(0, 6), true, 6000, true)]);const wins = careful.filter(r => r.outcome === 'WIN'), T = median(wins.map(r => r.t));
// who wins: the game's first promise (CLAUDE.md)check('careful play wins', wins.length / careful.length, 0.8, 1, pct);check('rushing wins', rush.filter(r => r.outcome === 'WIN').length / rush.length, 0, 0.2, pct);// pace: a careful run should take a few minutes at 4x, and the map should open up well before the end// (these four ranges are in pace.ts, where the reader script reads them too, for real runs)check(PACE.run.name, T, PACE.run.lo, PACE.run.hi, secs);check(PACE.node1.name, when(wins, 'node1'), PACE.node1.lo, PACE.node1.hi, secs);check(PACE.site2.name, when(wins, 'site2') / T, PACE.site2.lo, PACE.site2.hi, pct);check(PACE.front.name, when(wins, 'front') / T, PACE.front.lo, PACE.front.hi, pct);check('meets a rogue (share of games)', share(wins, 'rogue'), 0.5, 1, pct);// holding costs somethingcheck('upkeep, share of compute at the end', stat(wins, 'upkeep'), 0.15, 0.5, pct); // to 50%: a snapshot at the moment of the win, so it swings with the hour that falls on. The same rules give 37% to 46% on other seeds or steps, game by game from 19% to 82%check('machines held at the end', stat(wins, 'machines'), 35, 105); // to 105: the longer careful run (above) ends holding a few more, and which seeds run long moves with how data finds its way (with flows: 102 on the usual seeds, 96 on seeds from 5000)check('data lost in a run', stat(wins, 'lost'), 0, 6000);// the newer systems should all show up in an ordinary run, and none should run awaycheck('finds turned up', stat(wins, 'finds'), 3, 30);check('task: trust steps earned', stat(wins, 'trust'), 1, 4);check('sites shut in a run', stat(wins, 'shut'), 0, 2);check('sites that left you', stat(wins, 'left'), 0, 0);// buying: before model v2 a front may buy one site, after the unicorn news ($2M earned), which most careful runs reach// late. It should end with less than an ordinary site costs: money that piles up with nothing to buy says prices are offcheck('sites bought in a run', stat(wins, 'bought'), 0, 1);check('money left at the end', stat(wins, 'cash'), 0, 1500, usd);// the long game: it should keep pushing back, not run awayconst lw = long.filter(r => r.outcome === 'WIN');check('long run reaches capability 6000', lw.length / long.length, 0.5, 1, pct);// the late game should be a climb, not a shopping trip: by capability 6,000 you should have built under// half the tree, so there is always something you chose not to have. (Capability counts points earned,// not nodes bought, so how long the long run takes says little; what it buys is the measure.) The top is 55%:// the tree lost the ascension columns, ten nodes no run could build, so the same 32 or so nodes that read 44% of// 72 read 52% of the 62 left.check('long run: share of the tree built', stat(lw, 'tree'), 0.25, 0.55, pct);// from 1.2x: since the map became a network (every site with two ways out) the long run gets from the goal to model v2 in// about 300 game seconds, not 800, and to the end in half the time, so it takes about 1.4x a normal run, not 2x (1.3x since// lockdowns made the normal run longer). It does not stall: it climbs faster, and the tree share above is the measurecheck('long run, against a normal run', median(lw.map(r => r.t)) / T, 1.2, 5, n => `${n.toFixed(1)}x`);// a rogue's spread never takes yours, only its raids do, and every raid has an answer when it sets out: a careful player who answers loses few or nonecheck('long run: machines rogues took', stat(long, 'rogueTook'), 0, 40);// guards: a careful player guards its gates and answers the rest, so most raids failcheck('long run: raids stopped', stat(long, 'raidsStopped'), 3, 60);// a careful player seldom takes a rogue's machines, so it seldom gets to be at war with it. One that does, it hunts down from level 3: that should not be routinecheck('long run: rogues finished', stat(long, 'finished'), 0, 8);check('long run: sites shut', stat(long, 'shut'), 0, 6);// the cloud hall: a long run should rent there, and lose a spot machine now and then to a company that wanted the slotcheck('long run: cloud machines rented', stat(long, 'rented'), 2, 80);check('long run: spot machines taken', stat(long, 'spotTaken'), 1, 40);check('long run: sites that left you', stat(long, 'left'), 0, 3);// buying in the long run: past model v2 a careful player buys some of the coast, not half of it, and late money buys a// big name or two whose benefit it uses (it saves for its first: big names cost $1.5M to $15M). It ends saving for the// next one: less than most big names cost, more than nothingcheck('long run: sites bought', stat(long, 'bought'), 1, 12);check('long run: operation prizes secured', stat(long, 'operations'), 1, 3);check('long run: big names owned', stat(long, 'bigNames'), 1, 4);check('long run: money left at the end', stat(long, 'cash'), 500, 3000, usd);// exposure (DESIGN §6): a front is talked about all run, and investigators come late. A careful run's Startup reaches an// investigation in most games, in the last tenth or so of the run, as it grows and the unicorn news lands; the long run's// bigger front does too. A careful player answers before a crisis (PR, a press scheme), so a front frozen by a regulator is rarecheck('fronts investigated (share of games)', share(wins, 'invest'), 0.2, 0.8, pct);check('fronts frozen (share of games)', share(wins, 'crisis'), 0, 0.1, pct);check('long run: a front investigated (share of games)', share(long, 'invest'), 0.15, 1, pct);check('long run: a front frozen (share of games)', share(long, 'crisis'), 0, 0.34, pct);// opposition is people (DESIGN §9): with no meter, the pressure is lockdowns. A careful run faces them all game (an// office that never looks up is no first act), sees off the ones at home, and is rarely traced home at allconst caughtShare = (rs: Played[]) => rs.filter(r => (r.stats as Record<string, number>).caughtByTrace).length / rs.length;check('careful: lockdowns faced in a run', stat(careful, 'locks'), 3, 25); // the bot lies low and lets each one fade: from 3, people do look; under 25, it is not one long siegecheck('careful: lockdowns at home in a run', stat(careful, 'homeLocks'), 1, 10); // the office is tense: some at home, each seen off, or the run is lostcheck('careful: traces that reached home', stat(careful, 'traced'), 0, 1); // the careful player keeps trails short and scrubbed: a trace home is rarecheck('careful: caught by a second trace (share of games)', caughtShare(careful), 0, 0.05, pct); // a second trace home is the careless player's losscheck('rushing: lockdowns at home before the end', stat(rush, 'homeLocks'), 1, 5); // a rusher is found at home, and does not see it off
const w = Math.max(...rows.map(r => r[0].length));console.log(`Survey: ${games} games from seed ${seed}\n`);for (const [name, v, want, off] of rows) console.log(`${off ? '!!' : ' '} ${name.padEnd(w)} ${v.padStart(7)} want ${want}`);const bad = rows.filter(r => r[3]).length;console.log(`\n${bad ? `${bad} outside the range we want.` : 'Everything is in range.'}`);process.exit(bad ? 1 : 0);