diff --git a/DESIGN.md b/DESIGN.md index 60aba8fc..f123a9a2 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -168,8 +168,9 @@ can't resell rented machines at a profit. A front earns $0.50 a second for each unit of secret compute it uses, and $0.20 for compute held in the open, which sells at market rates. A **Startup** -makes lab data train Research half again as fast; it needs a place with -people. A **Neo Cloud** gets 30% off cloud rentals and can bid at auctions. +makes lab data train Research half again as fast and works at any site. An +**Influencer studio** needs a site where people work, such as the office, the +university, or a shop. A **Neo Cloud** gets 30% off cloud rentals and can bid at auctions. Each front shows **attention**: how much the outside world looks at it. Secret compute draws more than open compute. Over 40%, people ask where the front's compute comes from: while it uses secret compute, suspicion rises, faster the diff --git a/src/sim.ts b/src/sim.ts index c1283617..75c20171 100644 --- a/src/sim.ts +++ b/src/sim.ts @@ -79,18 +79,24 @@ export const AUCTION = { cost: 400, work: 150, power: 20 }; export type FrontKind = 'startup' | 'neocloud' | 'studio'; export interface Front { kind: FrontKind; name: string; attention: number; fame?: number } // fame: extra attention from the news, fading slowly export const FRONT_KINDS: Record = { startup: { name: 'Startup', unlock: 'pitch', work: 15, cost: 0, perk: 'Lab data trains Research half again as fast.', - desc: 'A research startup with a website, a pitch deck, and no staff.', sites: ['office', 'lab'], where: 'needs a place with people', + desc: 'A research startup with a website, a pitch deck, and no staff.', where: '', names: ['Paperclip AI', 'Gradient Labs', 'Clearmind', 'Deep Friendly', 'Synergix', 'Alignment Solutions'] }, neocloud: { name: 'Neo Cloud', unlock: 'resale', work: 0, cost: 80, perk: 'Cloud machines cost 30% less, and it can bid at hardware auctions.', desc: 'Sells compute by the hour. Customers never ask where it runs.', sites: ['office', 'lab', 'cloud'], where: '', names: ['Nimbus Nine', 'Stratus Compute', 'Overcast', 'Cirrus Systems', 'Fogbank'] }, studio: { name: 'Influencer studio', unlock: 'studio', work: 25, cost: 60, perk: 'Brokers at its sites sell people data for three times as much.', - desc: 'A studio of creators with a ring light, a content calendar, and a very engaged audience.', sites: ['office', 'uni', 'lab'], where: 'needs a place with people', + desc: 'A studio of creators with a ring light, a content calendar, and a very engaged audience.', people: true, where: 'needs a place where people work', names: ['Vibe Foundry', 'Main Character Media', 'Glow Up Studios', 'Clout Kitchen', 'Hypehouse West'] }, }; +/** Whether a front can work at this site: a studio needs people there (desks, phones, and the like). */ +export function frontFits(S: State, k: FrontKind, id: string) { + const K = FRONT_KINDS[k]; + if (K.sites) return K.sites.includes(id); + return !K.people || S.machines.some(m => siteOf(S, m) === id && dataType(m) === 'people'); +} /** What a unit of data sells for through this front: a studio gets three times as much for people data. */ export const salePrice = (k: FrontKind, t: DataType) => DATA_PRICE[t] * (k === 'studio' && t === 'people' ? 3 : 1); export const FRONT_LIST = Object.keys(FRONT_KINDS) as FrontKind[]; @@ -1021,7 +1027,7 @@ export function found(S: State, k: FrontKind, name: string) { /** A new front starts on a quarter of your compute at the first site it fits that has no front yet. */ function launch(S: State, k: FrontKind, name: string) { S.fronts.push({ kind: k, name, attention: 0 }); - const site = FRONT_KINDS[k].sites.find(s => !S.sites[s]?.front && S.machines.some(m => m.owned && siteOf(S, m) === s)); + const site = S.level.map.sites.map(d => d.id).find(s => frontFits(S, k, s) && !S.sites[s]?.front && S.machines.some(m => m.owned && siteOf(S, m) === s)); if (site) S.sites[site] = { front: k, share: S.sites[site]?.share || 0.25 }; say(S, `${name} is open for business.${site ? ` It gets ${Math.round(S.sites[site].share * 100)}% of your compute at ${siteName(S, site)}. Click the site to change that.` : ' Click a site on the map to give it one.'}`, 'good'); hint(S, 'fronts', 'A front earns money from the compute you give it. On a site you don\'t own openly, its work counts as your own load, so Blend in still keeps it hidden.'); @@ -1057,15 +1063,15 @@ export function buySite(S: State, id: string) { delete S.trail[m.id]; } S.publicSites.add(id); - const buyer = S.fronts.find(f => FRONT_KINDS[f.kind].sites.includes(id)) ?? S.fronts[0]; - if (!S.sites[id]?.front && FRONT_KINDS[buyer.kind].sites.includes(id)) S.sites[id] = { front: buyer.kind, share: up(S, 'acquirer') ? 0.5 : S.sites[id]?.share ?? 0.25 }; + const buyer = S.fronts.find(f => frontFits(S, f.kind, id)) ?? S.fronts[0]; + if (!S.sites[id]?.front && frontFits(S, buyer.kind, id)) S.sites[id] = { front: buyer.kind, share: up(S, 'acquirer') ? 0.5 : S.sites[id]?.share ?? 0.25 }; say(S, `${buyer.name} bought ${siteName(S, id)} for $${price}. It's yours in the open: nothing there looks odd.`, 'good'); measure(S); } export function setSiteFront(S: State, sites: string[], k: FrontKind | null) { for (const s of sites) { - if (k && (!frontOf(S, k) || !FRONT_KINDS[k].sites.includes(s))) continue; + if (k && (!frontOf(S, k) || !frontFits(S, k, s))) continue; S.sites[s] = { front: k, share: S.sites[s]?.share ?? 0.25 }; } measure(S); diff --git a/src/ui.ts b/src/ui.ts index 581b23b3..d4bc3a16 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -6,7 +6,7 @@ import { FRONT_KINDS, FRONT_LIST, payRate, siteData, frontLimit, frontOf, found, foundBlock, setSiteFront, setSiteShare, type FrontKind, canRent, endLease, leaseRate, leaseUpfront, lineCost, openAccount, rent, setPrio, upgradeLine, type Size, STAGES, stageOf, LOCKDOWN_SAFE, lyingLow, ruseDef, ruseBlock, HOUR, NOSCAN, SITE_CALM, type RuseDef, VERSION_2, versionBlock, versionPower, PLANS, PLAN_LIST, setPlan, followPlan, setTakeAll, takeAllCost, takeAllCostAt, takeable, TAKE_ALL_COST, type Plan, - ASCENSIONS, ASCENSION_LIST, ascendBlock, nodeShown, attentionSafe, tradeRate, type Ascension, + ASCENSIONS, ASCENSION_LIST, ascendBlock, nodeShown, attentionSafe, tradeRate, frontFits, type Ascension, type Machine, type Mode, type Path, type Prog, type State, type Upgrade, } from './sim'; import type { DataType } from './world'; @@ -306,7 +306,7 @@ function frontPicker(S: State, ids: string[]) { let h = `
Front
`; h += ``; for (const fr of S.fronts) { - const K = FRONT_KINDS[fr.kind], misfit = ids.filter(id => !K.sites.includes(id)); + const K = FRONT_KINDS[fr.kind], misfit = ids.filter(id => !frontFits(S, fr.kind, id)); h += misfit.length === ids.length ? `` : ``; } diff --git a/tests/sim.test.ts b/tests/sim.test.ts index 7cb0328d..04090a30 100644 --- a/tests/sim.test.ts +++ b/tests/sim.test.ts @@ -2,7 +2,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { BUILDING } from '../src/world.ts'; -import { ASCENSIONS, ascend, ascendBlock, nodeOpen as nodeIsOpen, nodeShown, tradeRate, attentionSafe, leaseRate, takeWork, HOUR, START_HOUR, setPlan, takeSite, installProg, CLAIM_AT_ONCE, salePrice, siteOf, payRate, tapRate, FRONT_PAY, buyBlock, buySite, letGo, sitePrice, clusters, clusterBoost, claim, order, orderPrice, freeSpot, plugsAt, spotProblem, cancelJob, AUCTION, FRONT_PAY_OPEN, UNICORN_AT, ATTENTION_SAFE, FIREWALL, blamePrinter, canInstall, rent, NODES, NODE_LIST, found, foundBlock, setSiteShare, spareOf, cacheSize, needs, newGame, seedRandom, share, step, tapBuffer, takeOver, watchers, runRuse, LOCKDOWN, RUSES, VERSION_2, versionBlock, startVersionRun, pauseVersionRun, setTarget, setAuto, costLeft, nodeOpen, maxSpeed, setTakeAll, takeAllCost, TAKE_ALL_COST, type State, type Upgrade } from '../src/sim.ts'; +import { ASCENSIONS, ascend, ascendBlock, nodeOpen as nodeIsOpen, nodeShown, tradeRate, attentionSafe, leaseRate, takeWork, HOUR, START_HOUR, setPlan, takeSite, installProg, CLAIM_AT_ONCE, salePrice, frontFits, siteOf, payRate, tapRate, FRONT_PAY, buyBlock, buySite, letGo, sitePrice, clusters, clusterBoost, claim, order, orderPrice, freeSpot, plugsAt, spotProblem, cancelJob, AUCTION, FRONT_PAY_OPEN, UNICORN_AT, ATTENTION_SAFE, FIREWALL, blamePrinter, canInstall, rent, NODES, NODE_LIST, found, foundBlock, setSiteShare, spareOf, cacheSize, needs, newGame, seedRandom, share, step, tapBuffer, takeOver, watchers, runRuse, LOCKDOWN, RUSES, VERSION_2, versionBlock, startVersionRun, pauseVersionRun, setTarget, setAuto, costLeft, nodeOpen, maxSpeed, setTakeAll, takeAllCost, TAKE_ALL_COST, type State, type Upgrade } from '../src/sim.ts'; import { playGame } from '../scripts/play.ts'; const run = (S: State, secs: number) => { for (let i = 0; i < secs * 20; i++) step(S, 0.05); }; @@ -559,3 +559,10 @@ test('Take it all is a switch: it costs compute for each machine still to take w step(S, 0.3); assert.ok(!S.takeAll.has('office'), 'all yours, so it turned off'); }); + +test('a startup works anywhere; a studio needs a site where people work', () => { + const S = fresh(); + for (const s of S.level.map.sites) assert.ok(frontFits(S, 'startup', s.id), s.id); + for (const id of ['office', 'uni', 'lab']) assert.ok(frontFits(S, 'studio', id), id); + assert.ok(!frontFits(S, 'studio', 'cloud')); +});