Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/games/blockilization/client/concepts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.' },
],
},
{
Expand Down
31 changes: 14 additions & 17 deletions src/games/blockilization/client/techtree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))}%` })),
);
});
Expand Down Expand Up @@ -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')),
),
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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); }
Expand Down
16 changes: 10 additions & 6 deletions src/games/blockilization/client/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'. */
Expand Down
6 changes: 3 additions & 3 deletions src/games/blockilization/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>;
techQueue: string[];
/** The techs whose eureka the viewer (or their team) has had (`rules/eurekas.ts`). */
Expand Down
4 changes: 0 additions & 4 deletions src/games/blockilization/rules/ai/civics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -65,9 +64,6 @@ function techValue(ctx: Ctx, id: string, resources: Set<string>): 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<string>();
Expand Down
87 changes: 33 additions & 54 deletions src/games/blockilization/rules/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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) {
Expand All @@ -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 };
}
}
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -884,7 +875,7 @@ const SHAPES: { [K in Order['t']]: { [F in Exclude<keyof Extract<Order, { t: K }
focus: { city: 'int', focus: FOCUSES },
work: { city: 'int', tile: 'tile' },
cityFire: { city: 'int', target: 'tile' },
research: { tech: 'string', was: 'string?' },
research: { tech: 'string' },
policy: { id: 'string' },
ideology: { id: 'string' },
switchIdeology: { id: 'string' },
Expand Down Expand Up @@ -937,8 +928,6 @@ export function wellFormed(game: Game, o: unknown): o is Order {
return v === null || Number.isSafeInteger(v);
case 'string':
return typeof v === 'string';
case 'string?':
return v === undefined || v === null || typeof v === 'string';
case 'strings':
return Array.isArray(v) && v.every((x) => typeof x === 'string');
case 'list':
Expand Down Expand Up @@ -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.
Expand All @@ -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': {
Expand Down Expand Up @@ -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). */
Expand Down
2 changes: 0 additions & 2 deletions src/games/blockilization/rules/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>;
Expand Down
Loading
Loading