diff --git a/src/games/blockilization/client/concepts.ts b/src/games/blockilization/client/concepts.ts index 7f4f180..2c122f4 100644 --- a/src/games/blockilization/client/concepts.ts +++ b/src/games/blockilization/client/concepts.ts @@ -161,6 +161,7 @@ export const CONCEPTS: Article[] = [ { text: 'Science goes toward one technology at a time. Each needs the ones before it; click any later one in the tree and the way there is queued. Technologies unlock units, buildings, wonders and improvements, and the eras follow from them.' }, { text: 'Each city you have makes technologies cost a little more, so a wide empire needs more science. Libraries, universities and scientists are the way to it; [[unit:great_scientist|Great Scientists]] jump ahead.' }, { title: 'Eurekas', text: 'Nearly every technology has a deed that brings it a third of the way: build a Mine and The Wheel is a third researched, kill a unit and Archery is, found a religion and Theology is. The lightning bolt on each technology in the tree says its deed (point at it), and lights up once you have done it. A deed counts whenever it is done, even long before you can research the technology (its third waits for you there), and on a team, once for all of you.' }, + { title: 'On a team', text: 'Each of you chooses your own research, and the progress is the team’s: research the same technology and both your science goes into it; research different ones and the team works on both at once. A technology any of you learns, all of you know. The tree shows what each teammate is researching. Technologies cost a team more: half again for each civilization past the first.' }, ], }, { diff --git a/src/games/blockilization/client/techtree.ts b/src/games/blockilization/client/techtree.ts index 76cc5dc..e25ad9e 100644 --- a/src/games/blockilization/client/techtree.ts +++ b/src/games/blockilization/client/techtree.ts @@ -3,8 +3,8 @@ import { ERA_NAMES, ERAS, eraOfCol, pathTo, TECH_LIST, TECHS } from '../rules/te import { ERA_COLOR, icon, svg, techEra, techGlyph } from './icons'; import { entryArt, entryName, techUnlocks } from './pedia'; import { adviceChips, adviceTag, researchAdvice, techAdvice } from './advice'; -import { ask, store } from './store'; -import { h, researchChooser, send, tile, togglePanel, type El } from './ui'; +import { store } from './store'; +import { h, matesResearch, send, tile, togglePanel, type El } from './ui'; /** * The technology tree, as Civilization V draws it: the eras across the top, each tech a card in its @@ -194,7 +194,10 @@ export function renderTechTree(): El { const state = known.has(t.id) ? 'known' : s.researching === t.id ? 'now' : queued >= 0 ? 'queued' : can ? 'can' : 'later'; const cost = s.techCosts[t.id] ?? 0; const have = s.research[t.id] ?? 0; - const own = s.researchScience > 0 ? Math.max(1, Math.ceil((cost - have) / s.researchScience)) : null; + // (Researched by teammates too: their science goes into it with what this player would put in.) + const onIt = s.mates.filter((m) => m.researching === t.id); + const rate = s.science + onIt.reduce((n, m) => n + m.science, 0); + const own = rate > 0 ? Math.max(1, Math.ceil((cost - have) / rate)) : null; const meta = known.has(t.id) ? h('span', {}, icon('check', 11, '#6fd27a'), 'Known') : turns.has(t.id) @@ -240,6 +243,7 @@ export function renderTechTree(): El { ), ), queued >= 0 && h('span.tt-badge', {}, String(queued + 1)), + onIt.length > 0 && h('span.tt-mate', { title: `Your team: ${onIt.map((m) => v.players[m.index]?.name).join(' and ')} researching it` }, onIt.map((m) => v.players[m.index]?.name ?? 'Teammate').join(', ')), have > 0 && !known.has(t.id) && h('div.tt-bar', {}, h('i', { style: `width:${Math.min(100, Math.round((have / Math.max(1, cost)) * 100))}%` })), ); }); @@ -273,7 +277,7 @@ export function renderTechTree(): El { 'div.tt-top', {}, tile('science', '#2f6fc0', 40), - h('div', {}, h('div.cv-h.big', {}, 'Technology'), h('small.cv-muted', {}, `${s.science > 0 ? `+${s.science} science a turn` : 'No science yet: found a city'}${s.researchScience !== s.science ? ` (your team’s together: +${s.researchScience}, researching as one${s.researchBy !== null ? `; ${researchChooser()}` : ''})` : ''} · ${s.techs.length} known`)), + h('div', {}, h('div.cv-h.big', {}, 'Technology'), h('small.cv-muted', {}, `${s.science > 0 ? `+${s.science} science a turn` : 'No science yet: found a city'}${s.researchScience !== s.science ? ` (+${s.researchScience} with your teammates on it)` : ''} · ${s.techs.length} known${s.mates.length ? ` · Team: ${matesResearch()}` : ''}`)), queue, h('div.tt-find', {}, icon('lens', 15, '#8fa1b3'), search, h('small.tt-found.cv-muted')), ), @@ -310,21 +314,13 @@ export function renderTechTree(): El { /** * Research chosen: with nothing being researched before (what the turn was waiting on), the tree - * closes and it's back to the map; choosing another later, it stays open. On a team, a teammate's - * choice is the whole team's: it's replaced only on a yes, and the order says what was seen (one - * a teammate made meanwhile isn't undone unseen, `research` in `rules/game.ts`). + * closes and it's back to the map; choosing another later, it stays open. (On a team, each chooses + * their own: a teammate's research isn't changed by it.) */ function chooseResearch(tech: string) { - const v = store.view!; - const was = v.self.researching; - const go = () => { - send({ t: 'research', tech, was }); - if (!was && store.panel === 'tech') togglePanel('tech'); - }; - const by = v.self.researchBy; - const mate = by !== null && by !== v.you ? v.players[by] : undefined; - if (was && was !== tech && mate && !mate.bot) ask(`${mate.name} chose ${TECHS[was].name} for the team. Research ${TECHS[tech].name} instead?`, 'Change research', go, false); - else go(); + const first = !store.view?.self.researching; + send({ t: 'research', tech }); + if (first && store.panel === 'tech') togglePanel('tech'); } function scrollTo(scroller: El, id: string) { @@ -424,6 +420,7 @@ export const TECH_CSS = ` .tt-card.later .n { color: #c3ccd6; } .tt-card.now { background: #2a2416 !important; border-color: var(--gold) !important; box-shadow: 0 0 0 3px rgba(242,193,78,.2), 0 6px 18px rgba(0,0,0,.4); } .tt-card.queued { background: #221f17 !important; border-color: rgba(242,193,78,.55) !important; } +.tt-mate { position: absolute; top: -8px; left: 10px; max-width: calc(100% - 40px); height: 16px; padding: 0 7px; border-radius: 99px; background: #2f6fc0; color: #fff; font-size: 10px; font-weight: 800; line-height: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; box-shadow: 0 2px 6px rgba(0,0,0,.4); } .tt-badge { position: absolute; top: -7px; right: -7px; min-width: 20px; height: 20px; padding: 0 5px; border-radius: 99px; background: linear-gradient(180deg, #f6cf62, #dca23a); color: #1b1407; font-size: 11px; font-weight: 800; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 6px rgba(0,0,0,.4); } .tt-bar { position: absolute; left: 10px; right: 10px; bottom: 4px; height: 3px; border-radius: 2px; background: rgba(255,255,255,.08); overflow: hidden; } .tt-bar i { display: block; height: 100%; background: linear-gradient(90deg, #4a9ae8, #7fc4ff); } diff --git a/src/games/blockilization/client/ui.ts b/src/games/blockilization/client/ui.ts index 3f66cea..a1018c3 100644 --- a/src/games/blockilization/client/ui.ts +++ b/src/games/blockilization/client/ui.ts @@ -874,17 +874,21 @@ function renderResearch(): El { h('div.cv-bar', {}, h('i', { style: `width:${t ? Math.round((have / cost) * 100) : 0}%` })), !t && h('small.cv-muted', {}, `${click(true)} to open the tech tree`), !t && researchHint() && h('div.adv-hint', {}, icon('lightbulb', 11, '#ffe48a'), researchHint()), - // (On a team: the team's research, and whose choice it is.) - t && s.researchBy !== null && h('small.cv-muted', {}, `Team research · ${researchChooser()}`), + // (On a team: what each teammate is researching.) + s.mates.length > 0 && h('small.cv-muted', {}, `Team · ${matesResearch()}`), ), ); } -/** Who chose a team's research, in words ('chosen by Bo', 'your choice'). */ -export function researchChooser(): string { +/** On a team, what each teammate is researching, in words ('Bo on it too', 'Bo: Pottery'). */ +export function matesResearch(): string { const v = store.view!; - const by = v.self.researchBy; - return by === v.you ? 'your choice' : `chosen by ${v.players[by ?? -1]?.name ?? 'your team'}`; + return v.self.mates + .map((m) => { + const name = v.players[m.index]?.name ?? 'A teammate'; + return !m.researching ? `${name}: nothing yet` : m.researching === v.self.researching ? `${name} on it too` : `${name}: ${TECHS[m.researching]?.name ?? m.researching}`; + }) + .join(' · '); } /** '2 units need orders'. */ diff --git a/src/games/blockilization/protocol.ts b/src/games/blockilization/protocol.ts index 96bbd82..dcc2f49 100644 --- a/src/games/blockilization/protocol.ts +++ b/src/games/blockilization/protocol.ts @@ -332,10 +332,10 @@ export interface SelfWire { goldenTurns: number; techs: string[]; researching: string | null; - /** Science toward research a turn: the viewer's, or (on a team) the whole team's. */ + /** Science toward research a turn: the viewer's, and (on a team) that of each teammate researching the same tech. */ researchScience: number; - /** On a team: which of them chose what's researched (null: alone, or nobody yet). */ - researchBy: number | null; + /** On a team: what each teammate is researching (each chooses their own; what they put in is the team's). */ + mates: { index: number; researching: string | null; science: number }[]; research: Record; techQueue: string[]; /** The techs whose eureka the viewer (or their team) has had (`rules/eurekas.ts`). */ diff --git a/src/games/blockilization/rules/ai/civics.ts b/src/games/blockilization/rules/ai/civics.ts index fdce4ec..1ae82c2 100644 --- a/src/games/blockilization/rules/ai/civics.ts +++ b/src/games/blockilization/rules/ai/civics.ts @@ -5,7 +5,6 @@ import { dissent, policyCost } from '../economy'; import { canChooseIdeology, eraOf, ideologySwitch, reformationDue, SWITCH_DISSENT } from '../game'; import { adoptable, ALL_POLICIES, IDEOLOGIES, TREES, treeOpen, type Policy } from '../policies'; import { BELIEF_LIST, pantheonCost, takenBeliefs, type BeliefKind } from '../religion'; -import { teamOf } from '../teams'; import { canResearch, pathTo, TECH_LIST, TECHS } from '../techs'; import { IMPROVEMENTS, RESOURCES } from '../terrain'; import { isMilitary, UNIT_LIST, UNITS } from '../units'; @@ -65,9 +64,6 @@ function techValue(ctx: Ctx, id: string, resources: Set): number { /** Research: toward the goal with the best worth for its cost; a new goal when that's reached. */ export function research(ctx: Ctx) { const { game, p, mem } = ctx; - // A team researches together: a person's choice stands; on a team of bots, the first of them chooses. - const team = teamOf(game, ctx.me); - if (team.length > 1 && p.researching && (team.some((m) => !m.bot) || team.find((m) => m.bot) !== p)) return; const known = new Set(p.techs); if (p.researching && mem.techGoal && !known.has(mem.techGoal) && (p.techQueue.length || p.researching === mem.techGoal)) return; const resources = new Set(); diff --git a/src/games/blockilization/rules/game.ts b/src/games/blockilization/rules/game.ts index 7e71fb2..a996aec 100644 --- a/src/games/blockilization/rules/game.ts +++ b/src/games/blockilization/rules/game.ts @@ -70,8 +70,7 @@ export type Order = | { t: 'work'; city: number; tile: number } | { t: 'specialist'; city: number; kind: SpecialistKind; add: boolean } | { t: 'cityFire'; city: number; target: number } - /** `was`: the research the one choosing saw (on a team, a teammate's choice made since stands, not undone unseen). */ - | { t: 'research'; tech: string; was?: string | null } + | { t: 'research'; tech: string } | { t: 'policy'; id: string } | { t: 'ideology'; id: string } | { t: 'switchIdeology'; id: string } @@ -600,10 +599,11 @@ export function learn(game: Game, p: Player, id: string) { } /** - * A team researches together, Civilization V's way: one research for the team, chosen by any of them - * (`research` order; the latest choice holds); the science each puts in, pooled toward it (a tech - * costs a team more, `techCost`); a tech learned, by any of them or any way, learned by all. Each - * keeps the team's research as their own: made the same again here, after every order and as the + * A team researches together, Civilization V's way: each of them chooses their own research + * (`research` order), and the science each puts into a tech is the team's progress toward it (a tech + * costs a team more, `techCost`): two of them on one tech finish it sooner, on two different ones + * they research both at once. A tech learned, by any of them or any way, is learned by all. Each + * keeps the team's progress as their own: made the same again here, after every order and as the * turn ends (what each has put in since the last time, added up). */ export function shareResearch(game: Game) { @@ -627,34 +627,25 @@ export function shareResearch(game: Game) { const sum = team.reduce((n, m) => n + (m.research[id] ?? 0) - was, was); if (sum > 0) pooled[id] = sum; } - // What they research: the choice of whoever made it last, else a person's, else anyone's. - const chooser = team.find((m) => m.index === p.researchBy && m.researching) ?? team.find((m) => !m.bot && m.researching) ?? team.find((m) => m.researching) ?? p; - let researching = chooser.researching; - let queue = [...chooser.techQueue]; - // Finished between them: learned by all, what's over going on. - while (researching && (pooled[researching] ?? 0) >= techCost(game, p, researching)) { - const over = pooled[researching] - techCost(game, p, researching); - delete pooled[researching]; - for (const m of team) { - m.researching = researching; - m.techQueue = [...queue]; - learn(game, m, researching); - } - researching = p.researching; - queue = [...p.techQueue]; - if (researching) pooled[researching] = (pooled[researching] ?? 0) + over; - else { - // (Nothing next: what's over waits for the next choice, as it does for a civ alone.) - p.spareScience = (p.spareScience ?? 0) + over; - for (const m of team) if (!m.bot) note(game, m.index, { text: 'Choose what to research next.', kind: 'tech', remind: true }); - } - } - for (const m of team) { - m.research = { ...pooled }; - m.researching = researching; - m.techQueue = [...queue]; - m.researchBy = chooser.index; + // Finished between them: learned by all; what's over goes on into the next research of those + // who were on it (shared out), or waits for their next choice, as it does for a civ alone. + const finished = () => Object.keys(pooled).find((id) => pooled[id] >= techCost(game, p, id)); + for (let id = finished(); id; id = finished()) { + const over = pooled[id] - techCost(game, p, id); + delete pooled[id]; + const on = team.filter((m) => m.researching === id); + for (const m of team) learn(game, m, id); + const to = on.length ? on : [p]; + to.forEach((m, i) => { + const share = Math.floor(over / to.length) + (i === 0 ? over - Math.floor(over / to.length) * to.length : 0); + if (m.researching) pooled[m.researching] = (pooled[m.researching] ?? 0) + share; + else { + m.spareScience = (m.spareScience ?? 0) + share; + if (!m.bot && on.includes(m)) note(game, m.index, { text: 'Choose what to research next.', kind: 'tech', remind: true }); + } + }); } + for (const m of team) m.research = { ...pooled }; game.teamResearch[p.team] = { ...pooled }; } } @@ -847,7 +838,7 @@ export function workTurns(game: Game, what: string): number { } /** What an order's field must be: a whole number, a tile of the map, a whole number or nothing, a string, one of some strings (or nothing, null or left out, where null is one), a list of strings, a list (checked where it's used), a yes or no, a production item; `?` when it may be left out. */ -type Field = 'int' | 'tile' | 'int|null' | 'string' | 'string?' | 'strings' | 'list' | 'bool' | 'bool?' | 'item' | readonly (string | null)[]; +type Field = 'int' | 'tile' | 'int|null' | 'string' | 'strings' | 'list' | 'bool' | 'bool?' | 'item' | readonly (string | null)[]; /** The luxuries' ids (a resolution banning one names it). */ const LUXURY_IDS = Object.keys(RESOURCES).filter((r) => RESOURCES[r].kind === 'luxury'); @@ -884,7 +875,7 @@ const SHAPES: { [K in Order['t']]: { [F in Exclude typeof x === 'string'); case 'list': @@ -1404,16 +1393,11 @@ function applyOrder(game: Game, by: number, o: Order): Outcome { } case 'research': { if (!Object.hasOwn(TECHS, o.tech)) return fail('No such tech'); - // On a team, the research is the team's (any of them may set it), but a bot doesn't undo a person's choice. - const chooser = p.researchBy !== undefined ? game.players[p.researchBy] : undefined; - if (p.bot && p.researching && chooser && !chooser.bot && chooser !== p) return fail("The team's people choose its research"); - // Two of them choosing at once: whoever was first keeps theirs, and the other's told (and can choose again, seeing it). - if (o.was !== undefined && p.researching && o.was !== p.researching && chooser && !chooser.bot && chooser !== p) return fail(`${chooser.name} just chose ${TECHS[p.researching].name} for the team`); + // (On a team, each chooses their own: what they put in is the team's, `shareResearch`.) if (canResearch(new Set(p.techs), o.tech)) { - for (const m of teamOf(game, by)) m.researchBy = by; p.researching = o.tech; p.techQueue = []; - spendSpare(game, p); + spendSpare(p); return OK; } // A far tech: queue the way there. @@ -1426,10 +1410,9 @@ function applyOrder(game: Game, by: number, o: Order): Outcome { }; visit(o.tech); if (!order.length) return fail('Already known'); - for (const m of teamOf(game, by)) m.researchBy = by; p.researching = order[0]; p.techQueue = order.slice(1); - spendSpare(game, p); + spendSpare(p); return OK; } case 'policy': { @@ -1727,14 +1710,10 @@ function bought(c: City, item: Production) { } /** Science kept with nothing to research goes into what's chosen. */ -function spendSpare(game: Game, p: Player) { - if (!p.researching) return; - // (On a team, what any of them has spare: theirs is the team's research too, `shareResearch`.) - for (const m of teamOf(game, p.index)) { - if (!m.spareScience) continue; - p.research[p.researching] = (p.research[p.researching] ?? 0) + m.spareScience; - m.spareScience = undefined; - } +function spendSpare(p: Player) { + if (!p.spareScience || !p.researching) return; + p.research[p.researching] = (p.research[p.researching] ?? 0) + p.spareScience; + p.spareScience = undefined; } /** Keep the production toward what's being built now (it's being put aside). */ diff --git a/src/games/blockilization/rules/state.ts b/src/games/blockilization/rules/state.ts index f9360b2..7ff395a 100644 --- a/src/games/blockilization/rules/state.ts +++ b/src/games/blockilization/rules/state.ts @@ -220,8 +220,6 @@ export interface Player { techQueue: string[]; /** Science with no tech to go to (one just learned with none chosen next): it goes into the next one chosen. */ spareScience?: number; - /** Who chose what's being researched (on a team, which of them: `shareResearch`). */ - researchBy?: number; /** The techs they've had their eureka for (a third of the research, for a deed: `eurekas.ts`), and the deeds counted for them (kills, by what killed). */ eurekas?: string[]; deeds?: Record; diff --git a/src/games/blockilization/tests/cases.ts b/src/games/blockilization/tests/cases.ts index b039ae7..93ca872 100644 --- a/src/games/blockilization/tests/cases.ts +++ b/src/games/blockilization/tests/cases.ts @@ -3398,60 +3398,66 @@ test('teams: a team wins together (conquest: holding every capital between them; ok([0, 1].includes(late.over?.winner ?? -1), `the team wins on score (${JSON.stringify(late.over)})`); }); -test('team research: one research for the team, set by any of them; a bot doesn’t undo a person’s choice', () => { +test('team research: each of them chooses their own (Civ V’s): a teammate’s choice changes nobody else’s', () => { const { game, at } = blank(['egypt', 'babylon', 'germany']); teamUp(game, [0, 1]); [at(3, 3), at(14, 3), at(22, 15)].forEach((t, i) => city(game, i, t)); const [a, b] = game.players; for (const p of game.players) p.techs.push('agriculture'); - a.bot = false; - ok(apply(game, 0, { t: 'research', tech: 'pottery' }).ok, 'a person chooses'); - eq(b.researching, 'pottery', 'the teammate researches it too'); - eq(b.researchBy, 0, 'and knows whose choice it is'); - ok(!apply(game, 1, { t: 'research', tech: 'mining' }).ok, 'a bot on the team doesn’t undo it'); - eq(a.researching, 'pottery', 'still pottery'); + a.bot = b.bot = false; + ok(apply(game, 0, { t: 'research', tech: 'pottery' }).ok, 'one chooses'); + eq(b.researching, null, 'the other’s isn’t chosen for them'); + ok(apply(game, 1, { t: 'research', tech: 'mining' }).ok, 'the other chooses another'); + eq(a.researching, 'pottery', 'the first one’s stands'); + eq(b.researching, 'mining', 'and the second one’s'); ok(apply(game, 0, { t: 'research', tech: 'writing' }).ok, 'a far tech'); - eq(b.researching, 'pottery', 'its way there, for both'); - ok(b.techQueue.includes('writing'), 'queued for both'); - ok(apply(game, 2, { t: 'research', tech: 'mining' }).ok && game.players[2].researching === 'mining', 'a civ alone is its own master'); + ok(a.researching === 'pottery' && a.techQueue.includes('writing'), 'its way there, queued'); + ok(b.researching === 'mining' && !b.techQueue.length, 'for them alone'); + b.bot = true; + ok(apply(game, 1, { t: 'research', tech: 'animal_husbandry' }).ok && a.researching === 'pottery', 'a bot teammate chooses its own too'); + ok(apply(game, 2, { t: 'research', tech: 'mining' }).ok && game.players[2].researching === 'mining', 'a civ alone as ever'); }); -test('team research: two people choosing at once, the first keeps theirs (the second is told, and can choose again)', () => { +test('team research: on different techs, a team researches both at once; on the same one, both put into it', () => { const { game, at } = blank(['egypt', 'babylon', 'germany']); teamUp(game, [0, 1]); [at(3, 3), at(14, 3), at(22, 15)].forEach((t, i) => city(game, i, t)); const [a, b] = game.players; - for (const p of game.players) p.techs.push('agriculture'); + // (Eurekas aside: they'd be science too.) + for (const p of game.players) (p.techs.push('agriculture'), (p.eurekas = Object.keys(EUREKAS))); a.bot = b.bot = false; - // Both see nothing chosen; the first's order lands first. - ok(apply(game, 0, { t: 'research', tech: 'pottery', was: null }).ok, 'the first chooses'); - const late = apply(game, 1, { t: 'research', tech: 'mining', was: null }); - ok(!late.ok && /just chose Pottery/.test(late.error ?? ''), `the second's, from what they saw before it, is turned back (${late.error})`); - eq(a.researching, 'pottery', 'still the first one’s'); - eq(b.researchBy, 0, 'and theirs it says'); - // Seeing it, the other may change it. - ok(apply(game, 1, { t: 'research', tech: 'mining', was: 'pottery' }).ok, 'seeing it, changed on purpose'); - eq(a.researching, 'mining', 'for both'); - eq(a.researchBy, 1, 'the choice now the second one’s'); - ok(apply(game, 1, { t: 'research', tech: 'mining', was: 'pottery' }).ok, 'their own choice they may change as they like'); - ok(!apply(game, 0, { t: 'research', tech: 'agriculture' }).ok && a.researchBy === 1, 'a choice turned back changes nobody’s'); -}); - -test('team research: a turn with nothing chosen, the team’s science waits for the choice (as a civ alone’s does)', () => { + const science = (i: number) => game.cities.filter((c) => c.owner === i).reduce((n, c) => n + cityYields(game, c).science, 0); + ok(science(0) > 0 && science(1) > 0, 'both make science'); + apply(game, 0, { t: 'research', tech: 'pottery' }); + apply(game, 1, { t: 'research', tech: 'mining' }); + endTurn(game); + eq(a.research.pottery, science(0), 'the one’s science into theirs'); + eq(a.research.mining, science(1), 'the other’s into theirs'); + eq(JSON.stringify(b.research), JSON.stringify(a.research), 'the progress the team’s: the same for both'); + ok(a.researching === 'pottery' && b.researching === 'mining', 'each still on their own'); + // The other joins in: both into one. + apply(game, 1, { t: 'research', tech: 'pottery' }); + const before = a.research.pottery; + endTurn(game); + eq(a.research.pottery, before + science(0) + science(1), `a turn's science from both (${science(0)} + ${science(1)})`); + eq(a.research.mining, science(1), 'what went into the other is kept'); +}); + +test('team research: a turn with nothing chosen, each one’s science waits for their own choice (as a civ alone’s does)', () => { const { game, at } = blank(['egypt', 'babylon', 'germany']); teamUp(game, [0, 1]); [at(3, 3), at(14, 3), at(22, 15)].forEach((t, i) => city(game, i, t)); const [a, b] = game.players; - for (const p of game.players) p.techs.push('agriculture'); + for (const p of game.players) (p.techs.push('agriculture'), (p.eurekas = Object.keys(EUREKAS))); a.bot = b.bot = false; const science = (i: number) => game.cities.filter((c) => c.owner === i).reduce((n, c) => n + cityYields(game, c).science, 0); - const both = science(0) + science(1); - ok(both > 0, 'they make science'); endTurn(game); - eq(a.researching, null, 'nothing chosen'); + ok(a.researching === null && b.researching === null, 'nothing chosen'); ok(apply(game, 1, { t: 'research', tech: 'pottery' }).ok, 'then one chooses'); - eq(a.research.pottery, both, `what both made meanwhile goes into it (${science(0)} + ${science(1)})`); - eq(b.research.pottery, both, 'for both'); + eq(a.research.pottery, science(1), 'what they made meanwhile goes into it, the team’s'); + eq(a.spareScience, science(0), 'the other’s waits for their own choice'); + ok(apply(game, 0, { t: 'research', tech: 'mining' }).ok, 'which they make'); + eq(b.research.mining, science(0), 'and theirs goes into it'); }); test('teams: a bot on a team with a person sells no peace for the team (its people make it)', () => { @@ -3472,27 +3478,32 @@ test('team research: what each puts in is pooled; finished between them, it’s const [a, b] = game.players; // (Eurekas aside: Writing's, for having met each other, would be science too.) for (const p of game.players) (p.techs.push('agriculture'), (p.eurekas = Object.keys(EUREKAS))); + a.bot = b.bot = false; apply(game, 0, { t: 'research', tech: 'pottery' }); + apply(game, 1, { t: 'research', tech: 'pottery' }); const cost = techCost(game, a, 'pottery'); a.research.pottery = Math.ceil(cost * 0.3); b.research.pottery = Math.ceil(cost * 0.3); shareResearch(game); eq(a.research.pottery, b.research.pottery, 'the same for both'); eq(a.research.pottery, Math.ceil(cost * 0.3) * 2, 'what each put in, together'); + // Each with their own next. a.techQueue = ['writing']; - b.techQueue = ['writing']; + b.techQueue = ['mining']; a.research.pottery += Math.ceil(cost * 0.3); b.research.pottery += Math.ceil(cost * 0.3); shareResearch(game); ok(a.techs.includes('pottery') && b.techs.includes('pottery'), 'learned by both'); - eq(a.researching, 'writing', 'on to the next'); - eq(a.research.writing, Math.ceil(cost * 0.3) * 4 - cost, 'what was over goes on'); - eq(b.research.writing, a.research.writing, 'for both'); - // A turn: each one's science goes in, and the team's together is what counts. - const before = a.research.writing; - const science = (i: number) => game.cities.filter((c) => c.owner === i).reduce((n, c) => n + cityYields(game, c).science, 0); - endTurn(game); - eq(a.research.writing, before + science(0) + science(1), `a turn's science from both (${science(0)} + ${science(1)})`); + ok(a.researching === 'writing' && b.researching === 'mining', 'each on to their own next'); + eq((a.research.writing ?? 0) + (a.research.mining ?? 0), Math.ceil(cost * 0.3) * 4 - cost, 'what was over goes on, shared between them'); + eq(JSON.stringify(b.research), JSON.stringify(a.research), 'the same for both'); + // One finishing theirs: the other's goes on as it was. + const mining = a.research.mining ?? 0; + a.research.writing = techCost(game, a, 'writing'); + shareResearch(game); + ok(b.techs.includes('writing'), 'writing learned by both'); + eq(b.researching, 'mining', 'the other still on theirs'); + eq(b.research.mining, mining, 'with what was in it'); }); test('team research: a tech learned by one, any way, is learned by all; a team’s techs cost more', () => { diff --git a/src/games/blockilization/views.ts b/src/games/blockilization/views.ts index d79de97..e7460af 100644 --- a/src/games/blockilization/views.ts +++ b/src/games/blockilization/views.ts @@ -111,9 +111,13 @@ function selfWire(game: Game, p: Player): SelfWire { gold += y.gold; } const up = goldUpkeep(game, p); - // On a team, research is the team's: the science of all of them goes into it (`shareResearch`). - const mates = p.team === undefined ? [] : teamOf(game, p.index).filter((m) => m !== p); - const teamScience = science + mates.reduce((n, m) => n + citiesOf(game, m.index).reduce((s, c) => s + cityYields(game, c, happiness(game, m)).science, 0), 0); + // On a team, each researches their own, the progress the team's (`shareResearch`): a teammate on the same tech adds theirs to it. + const mates = (p.team === undefined ? [] : teamOf(game, p.index).filter((m) => m !== p)).map((m) => ({ + index: m.index, + researching: m.researching, + science: citiesOf(game, m.index).reduce((s, c) => s + cityYields(game, c, happiness(game, m)).science, 0), + })); + const teamScience = science + mates.reduce((n, m) => n + (p.researching && m.researching === p.researching ? m.science : 0), 0); const techCosts: Record = {}; for (const id of Object.keys(TECHS)) techCosts[id] = techCost(game, p, id); // (A vassal pays its master a share of what it makes; a master gets its vassals' tribute: `vassals.ts`.) @@ -141,7 +145,7 @@ function selfWire(game: Game, p: Player): SelfWire { techs: p.techs, researching: p.researching, researchScience: teamScience, - researchBy: mates.length && p.researchBy !== undefined ? p.researchBy : null, + mates, research: p.research, techQueue: p.techQueue, eurekas: p.eurekas ?? [],