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 @@ -226,6 +226,7 @@ export const CONCEPTS: Article[] = [
sub: 'War, peace and city-states',
sections: [
{ text: 'You meet a civilization when you first see its units or land. From the Diplomacy panel you can declare war, make peace (a peace treaty holds for a number of turns), and open your borders to its units.' },
{ title: 'Borders', text: 'In peace, no unit of yours, soldier or civilian, may enter another civilization’s land unless it has opened its borders to you: open borders come by a deal at the trade table (with Writing). Missionaries, inquisitors and Great Prophets go in regardless, and a city-state’s land is open to everyone. When open borders end, your units still in their land are moved out.' },
{ title: 'City-states', text: `City-states are small single-city nations, marked “City-state” over their banners on the map, with your influence on a bar beneath (a “!” while one asks something of you). Click a city-state’s city to speak to it. Gifts of gold and services (clearing barbarian camps near them) win influence with them, which drifts back each turn. At ${FRIEND} influence they are your friends; the friend with the most, at ${ALLY} or more, is their ally. Friends and allies get what their [[cat:citystate|kind]] gives; an ally also gets their luxuries and their vote in the United Nations.` },
{ title: 'Pledges to protect', text: `From a city-state’s card you may pledge to protect it (met, at peace, your influence not below 0). Your influence with it then rests ${PLEDGE_REST} higher, and anyone else who bullies it for tribute or declares war on it offends you, and the leaders who have pledged remember it. Bully or attack it yourself and the pledge is broken (${PLEDGE_BROKEN} influence lost); let your influence fall below 0 and it lapses. Either way you can’t pledge to it again for ${PLEDGE_TURNS} turns (at standard speed).` },
{ title: 'Liberating cities', text: `A city you take from a civilization that hadn’t founded it (it had been taken from its founder before) can be liberated instead of annexed, kept as a puppet or razed: it goes back to its founder, who never forgets it. A city-state given back its city gives you ${LIBERATE_INFLUENCE} influence with it, enough to be its ally. A civilization or city-state that had been destroyed comes back to life with the city, at peace with you, its quarrels with the world forgotten. Liberating cities also weighs against your conquests in other leaders’ eyes. An original capital given back counts again as its founder’s for [[victory:domination|Domination]].` },
Expand Down
8 changes: 4 additions & 4 deletions src/games/blockilization/client/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { UnitWire, View } from '../protocol';
import { dangerCost, dangerZones, type Danger } from '../rules/danger';
import { ALL, findWay, neighbours, type Walker } from '../rules/hex';
import { FEATURES, HILLS, RAIL_MOVE, ROAD_MOVE, TERRAINS } from '../rules/terrain';
import { isGreatPerson, isMilitary, stackKind, UNITS } from '../rules/units';
import { heedsBorders, isGreatPerson, isMilitary, stackKind, UNITS } from '../rules/units';
import { cityAt, enemyAt, me, outOfReach, store } from './store';

/**
Expand Down Expand Up @@ -67,7 +67,7 @@ function mover(u: UnitWire, view: View) {
if (city && city.owner !== me()) return false;
const owner = store.known[t]?.owner ?? -1;
const them = owner >= 0 && owner !== me() ? view.players[owner] : undefined;
return !(military && them && !them.war && !them.cityState && !them.opensBorders);
return !(heedsBorders(u.type) && them && !them.war && !them.cityState && !them.opensBorders);
};
/** Next to an enemy unit or city (a zone of control: stepping from one into another ends the move). */
const zoc = (t: number) =>
Expand Down Expand Up @@ -249,11 +249,11 @@ const ownThere = (u: UnitWire, t: number) => store.view?.units.find((x) => x.til
/** Another civ's unit on a tile that the player isn't at war with: no unit of theirs can stop there (nor attack it). */
const neutralThere = (t: number) => store.view?.units.find((x) => x.tile === t && x.owner !== me() && !store.view?.players[x.owner]?.war);

/** Whether a tile is in land closed to a soldier: another civ's, at peace, without open borders to it. */
/** Whether a tile is in land closed to a unit: another civ's, at peace, without open borders to it (missionaries, inquisitors and prophets aside). */
function closedTo(u: UnitWire, t: number): boolean {
const owner = store.known[t]?.owner ?? -1;
const them = owner >= 0 && owner !== me() ? store.view?.players[owner] : undefined;
return soldier(u) && !!them && !them.war && !them.cityState && !them.opensBorders;
return heedsBorders(u.type) && !!them && !them.war && !them.cityState && !them.opensBorders;
}

/** Why a unit can't go to a tile, in a few words: what's in the way there, or that there's no way. */
Expand Down
8 changes: 5 additions & 3 deletions src/games/blockilization/rules/ai/civilians.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,20 +101,22 @@ function slotUnderway(ctx: Ctx, work: 'art' | 'writing' | 'music'): boolean {

/**
* A Great Musician's concert tour, in the land of the civ the bot's culture holds least sway over (of
* those it's met and at peace with): there, and it plays. False if there's none to go to.
* those it's met, at peace with and whose borders are open to it): there, and it plays. False if
* there's none it can go to.
*/
function concertTour(ctx: Ctx, u: Unit): boolean {
const { game, p } = ctx;
const sway = (o: Player) => (p.tourism[o.index] ?? 0) / Math.max(1, o.lifetimeCulture);
const host = game.players.filter((o) => o !== p && o.alive && !o.cityState && o.civ !== 'barbarian' && hasMet(game, p, o) && !p.diplomacy.war[o.index]).sort((a, b) => sway(a) - sway(b))[0];
const host = game.players.filter((o) => o !== p && o.alive && !o.cityState && o.civ !== 'barbarian' && hasMet(game, p, o) && !p.diplomacy.war[o.index] && o.diplomacy.openBorders[p.index] > 0).sort((a, b) => sway(a) - sway(b))[0];
if (!host) return false;
if (game.tiles[u.tile].owner === host.index) return act(ctx, { t: 'greatPerson', unit: u.id, action: 'concert' });
// (The nearest of its land it knows, not a city's own tile.)
let best = -1;
for (let t = 0; t < game.tiles.length; t++) if (game.tiles[t].owner === host.index && p.explored[t] && !cityOn(ctx, t) && !civilianAt(ctx, t) && !TERRAINS[game.tiles[t].terrain].water && (best < 0 || dist(ctx, u.tile, t) < dist(ctx, u.tile, best))) best = t;
if (best < 0) return false;
if (goTo(ctx, u, best, 'great')) act(ctx, { t: 'greatPerson', unit: u.id, action: 'concert' });
return true;
// (There, or on its way: else, with no way there, it does something else.)
return u.tile === best || !!ctx.mem.orders[u.id];
}

/** A Great Admiral: repairs the fleet once enough of it is hurt about it; else to the most hurt of its ships, or a port. */
Expand Down
11 changes: 3 additions & 8 deletions src/games/blockilization/rules/ai/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { dangerCost, dangerZones, type Danger } from '../danger';
import { happiness, type CityYields } from '../economy';
import { apply, eraOf, type Order } from '../game';
import { distance, neighbours, path, within } from '../hex';
import { ALL, canCrossOcean, canEmbark, occupants, stepCost } from '../movement';
import { ALL, canCrossOcean, canEmbark, closedLand, occupants, stepCost } from '../movement';
import { questsFor, type QuestKind } from '../quests';
import type { City, Game, Player, Unit } from '../state';
import { teammates } from '../teams';
import { TERRAINS } from '../terrain';
import { unVote } from '../trade';
import { isGreatPerson, isMilitary, UNITS } from '../units';
import { heedsBorders, isGreatPerson, isMilitary, UNITS } from '../units';
import { memoryOf, type BotMemory } from './memory';
import { aimLeaning, personalityOf, type Personality } from './personality';
import { nearVictory } from './opinion';
Expand Down Expand Up @@ -215,12 +215,7 @@ export function passable(ctx: Ctx, u: Unit, t: number, passing = false): boolean
if (c && c.owner !== u.owner && !military) return false;
// (A caravan, cargo ship or great person may stop in its own city beside another civilian, as the rules have it.)
if (!passing && (military ? m && m !== u : c && c !== u && !((type.abilities?.includes('trade_route') || isGreatPerson(u.type)) && city?.owner === u.owner))) return false;
const owner = game.tiles[t].owner;
if (military && owner >= 0 && owner !== u.owner && !atWar(game, u.owner, owner)) {
const them = game.players[owner];
if (!them.cityState && !them.diplomacy.openBorders[u.owner]) return false;
}
return true;
return !(heedsBorders(u.type) && closedLand(game, u.owner, t));
}

/**
Expand Down
17 changes: 11 additions & 6 deletions src/games/blockilization/rules/ai/settle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { canFound } from '../game';
import { freshWater, tileYields } from '../economy';
import { neighbours, within } from '../hex';
import type { Unit } from '../state';
import { canEmbark } from '../movement';
import { canEmbark, closedLand } from '../movement';
import { teamOf } from '../teams';
import { RESOURCES, TERRAINS } from '../terrain';
import { act, cityOn, civilianAt, coastal, danger, dist, goTo, militaryAt, sameLand, seaRoute, type Ctx } from './context';
import { act, cityOn, civilianAt, coastal, danger, dist, goTo, landRoute, militaryAt, route, sameLand, seaRoute, type Ctx } from './context';

/**
* Where to found cities: each land tile's worth as a city site (its land's food and production, fresh
* water, the coast, a hill to stand on, luxuries, strategic resources and natural wonders); then, for
* each bot, the sites still open to it (not too near another city, not in another's land, not where
* enemies roam; on its own land or, with Optics, a coast over the water), nearer home better, new
* luxuries better still. Settlers go to the best and found there, and wait in a city while barbarians
* are about.
* each bot, the sites still open to it (not too near another city, not in another's land nor beyond
* land closed to it, not where enemies roam; on its own land or, with Optics, a coast over the
* water), nearer home better, new luxuries better still. Settlers go to the best and found there,
* and wait in a city while barbarians are about.
*/

/** What a site's tile of two food or more counts beyond its yields, for each food past the first (`FOOD_BONUS`: tests and scripts may set it). */
Expand Down Expand Up @@ -68,6 +68,8 @@ export function bestSite(ctx: Ctx, from: number, settler?: Unit): { tile: number
for (const tile of game.tiles) if (tile.owner === ctx.me && tile.resource && RESOURCES[tile.resource].kind === 'luxury') haveLux.add(tile.resource);
let best: { tile: number; score: number } | null = null;
const reach = ctx.cities.length ? 12 : 4;
// (Whether any land is closed to it: only then is a site on its own land maybe out of its settlers' reach.)
const fenced = game.tiles.some((_, x) => closedLand(game, ctx.me, x));
for (const t of within(game.grid, from, reach)) {
if (!p.explored[t] || !canFound(game, t) || !Number.isFinite(siteValue(ctx, t))) continue;
const tile = game.tiles[t];
Expand Down Expand Up @@ -96,6 +98,9 @@ export function bestSite(ctx: Ctx, from: number, settler?: Unit): { tile: number
if (best && s <= best.score) continue;
// (A way there by sea, looked for only when the site would do.)
if (overseas && !seaRoute(game, p, from, t)) continue;
// (Nor one it couldn't get to over its land, with another civ's land closed to it in the way: the
// settler's own way there, else a way that keeps out of such land.)
if (!overseas && fenced && !(settler ? route(ctx, settler, t) : landRoute(game, from, t, p))) continue;
best = { tile: t, score: s };
}
return best;
Expand Down
63 changes: 36 additions & 27 deletions src/games/blockilization/rules/ai/workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,35 +167,44 @@ export function workers(ctx: Ctx, list: Unit[]) {
if (u.work) continue;
const mine = mem.orders[u.id];
if (mine) busy.delete(mine.to);
let best: Job | null = null;
let bestScore = -Infinity;
for (const job of jobs) {
if (busy.has(job.tile) || (civilianAt(ctx, job.tile) && civilianAt(ctx, job.tile) !== u)) continue;
const far = dist(ctx, u.tile, job.tile);
if (far > (job.young ? 16 : 12)) continue;
const score = job.value - far * (job.young ? 0.35 : 0.7);
if (score > bestScore) (bestScore = score), (best = job);
}
if (!best && roads.length) {
const t = roads.filter((x) => !busy.has(x) && d[x] === 0 && canBuild(game, { ...u, tile: x }, 'road')).sort((a, b) => dist(ctx, a, u.tile) - dist(ctx, b, u.tile))[0];
if (t !== undefined) best = { tile: t, what: 'road', value: 1 };
}
if (!best) {
delete mem.orders[u.id];
// (Nothing to do: out of a city, where it would keep another civilian from it, to the land beside it, out of reach.)
if (cityOn(ctx, u.tile) && d[u.tile] === 0) {
const aside = neighbours(game.grid, u.tile).find((t) => game.tiles[t].owner === ctx.me && !d[t] && !cityOn(ctx, t) && !TERRAINS[game.tiles[t].terrain].water && passable(ctx, u, t));
if (aside !== undefined) act(ctx, { t: 'move', unit: u.id, to: aside });
// (A job it finds no way to, with another civ's land closed to it in the way, is passed over for the next best: a few at most.)
const noWay = new Set<number>();
for (let tries = 0; tries < 3; tries++) {
let best: Job | null = null;
let bestScore = -Infinity;
for (const job of jobs) {
if (busy.has(job.tile) || noWay.has(job.tile) || (civilianAt(ctx, job.tile) && civilianAt(ctx, job.tile) !== u)) continue;
const far = dist(ctx, u.tile, job.tile);
if (far > (job.young ? 16 : 12)) continue;
const score = job.value - far * (job.young ? 0.35 : 0.7);
if (score > bestScore) (bestScore = score), (best = job);
}
continue;
}
busy.add(best.tile);
if (best.tile === u.tile) {
delete mem.orders[u.id];
act(ctx, { t: 'build', unit: u.id, what: best.what });
continue;
if (!best && roads.length) {
const t = roads.filter((x) => !busy.has(x) && !noWay.has(x) && d[x] === 0 && canBuild(game, { ...u, tile: x }, 'road')).sort((a, b) => dist(ctx, a, u.tile) - dist(ctx, b, u.tile))[0];
if (t !== undefined) best = { tile: t, what: 'road', value: 1 };
}
if (!best) {
delete mem.orders[u.id];
// (Nothing to do: out of a city, where it would keep another civilian from it, to the land beside it, out of reach.)
if (cityOn(ctx, u.tile) && d[u.tile] === 0) {
const aside = neighbours(game.grid, u.tile).find((t) => game.tiles[t].owner === ctx.me && !d[t] && !cityOn(ctx, t) && !TERRAINS[game.tiles[t].terrain].water && passable(ctx, u, t));
if (aside !== undefined) act(ctx, { t: 'move', unit: u.id, to: aside });
}
break;
}
busy.add(best.tile);
if (best.tile === u.tile) {
delete mem.orders[u.id];
act(ctx, { t: 'build', unit: u.id, what: best.what });
break;
}
const there = goTo(ctx, u, best.tile, 'work', d);
if (there && u.moves > 0) act(ctx, { t: 'build', unit: u.id, what: best.what });
// (There, or on its way: else its orders are gone, as there's no way there.)
if (there || mem.orders[u.id]) break;
busy.delete(best.tile);
noWay.add(best.tile);
}
if (goTo(ctx, u, best.tile, 'work', d) && u.moves > 0) act(ctx, { t: 'build', unit: u.id, what: best.what });
}
mem.crew = Object.fromEntries(list.map((u) => [u.id, u.tile]));
}
Expand Down
21 changes: 10 additions & 11 deletions src/games/blockilization/rules/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { fromPromotions, promotionsDue, promotionsFor, PROMOTIONS } from './prom
import { liberate, liberation } from './liberation';
import { pledge, pledgeOffended } from './pledges';
import { distance, neighbours, within } from './hex';
import { advance, campHooks, canEnter, captureHooks, moveTo, occupants, planPath, ruinHooks, sightStops, swap, swapWith } from './movement';
import { advance, campHooks, canEnter, captureHooks, closedLand, moveTo, occupants, planPath, ruinHooks, sightStops, swap, swapWith } from './movement';
import { wondersBeside } from './nature';
import { adoptable, ALL_POLICIES, IDEOLOGIES, policyMet, TREES, treeDone } from './policies';
import { BELIEF_LIST, BELIEFS, pantheonCost, takenBeliefs } from './religion';
Expand All @@ -33,7 +33,7 @@ import { startSequence, waitsFor, warBegun } from './turns';
import { climateTurn } from './climate';
import { disasterTurn } from './disasters';
import { FALLOUT, FEATURES, IMPROVEMENTS, RESOURCES, SEA_WORK, tapsWith, TERRAINS, type ImprovementId } from './terrain';
import { canFortify, isGreatPerson, isMilitary, PARADROP_RANGE, UNIT_LIST, UNITS } from './units';
import { canFortify, heedsBorders, isGreatPerson, isMilitary, PARADROP_RANGE, UNIT_LIST, UNITS } from './units';

/**
* The rules: every order a player can give (each checked: a player only commands their own, and only
Expand Down Expand Up @@ -1117,8 +1117,7 @@ function applyOrder(game: Game, by: number, o: Order): Outcome {
// (A civilian sent at an enemy would only stop next to it.)
const foe = cityAt(game, o.to) ?? occupants(game, o.to).military ?? occupants(game, o.to).civilian;
if ((!isMilitary(UNITS[u.type]) || isGreatPerson(u.type)) && foe && atWar(game, by, foe.owner)) return fail(`${aUnit(u.type)} can't go where an enemy is`);
const owner = game.players[game.tiles[o.to].owner];
if (owner && owner !== p && !owner.cityState && !atWar(game, by, owner.index) && !owner.diplomacy.openBorders[by] && isMilitary(UNITS[u.type]) && !isGreatPerson(u.type)) return fail(`${owner.name}'s borders are closed to you (open borders come by a deal)`);
if (heedsBorders(u.type) && closedLand(game, by, o.to)) return fail(`${game.players[game.tiles[o.to].owner].name}'s borders are closed to you (open borders come by a deal)`);
// (A settler sent to a city site: it's told if the site's lost on the way.)
u.site = UNITS[u.type].abilities?.includes('found_city') && canFound(game, o.to) ? o.to : undefined;
if (moveTo(game, u, o.to)) return OK;
Expand Down Expand Up @@ -1850,17 +1849,15 @@ export function endTurn(game: Game) {
}

/**
* Soldiers standing in land that's closed to them (borders grown over them, open borders ended) are
* moved out, Civ V's way, to the nearest tile they may stand on (dry land first): else, unable to
* step anywhere in that land, they'd be stuck there for good.
* Units standing in land that's closed to them (borders grown over them, open borders ended), soldiers
* and civilians alike, are moved out, Civ V's way, to the nearest tile they may stand on (dry land
* first): else, unable to step anywhere in that land, they'd be stuck there for good. (Missionaries,
* inquisitors and prophets may stay: closed borders don't keep them out.)
*/
function expel(game: Game) {
for (const u of game.units) {
const type = UNITS[u.type];
if (!type || !isMilitary(type) || isGreatPerson(u.type) || isAircraft(u)) continue;
const owner = game.tiles[u.tile].owner;
const them = game.players[owner];
if (!them || owner === u.owner || them.cityState || atWar(game, u.owner, owner) || them.diplomacy.openBorders[u.owner]) continue;
if (!type || !heedsBorders(u.type) || isAircraft(u) || !closedLand(game, u.owner, u.tile)) continue;
const near = within(game.grid, u.tile, 8)
.filter((t) => t !== u.tile && canEnter(game, u, t))
.sort((a, b) => distance(game.grid, u.tile, a) - distance(game.grid, u.tile, b) || Number(!!TERRAINS[game.tiles[a].terrain].water) - Number(!!TERRAINS[game.tiles[b].terrain].water))[0];
Expand All @@ -1869,6 +1866,8 @@ function expel(game: Game) {
u.path = undefined;
u.stance = undefined;
u.fortified = 0;
// (A worker's job there is left behind with the tile.)
u.work = undefined;
u.embarked = type.domain === 'land' && TERRAINS[game.tiles[near].terrain].water ? true : undefined;
// (A carrier's aircraft go with it.)
carry(game, u);
Expand Down
Loading
Loading