From 8f9f4a01631e5d81f9454b5e639676669abd84b7 Mon Sep 17 00:00:00 2001 From: Patrick Blais Date: Wed, 21 Oct 2026 22:32:02 -0400 Subject: [PATCH] Blockilization: the widest maps build (the large Earth waited at 0% for good) The server loads the world 12 chunks round its people at most, and the build waited for every column across the map to load with everyone at its middle. The large Earth reaches 14 chunks from its middle (the huge 17), so its far edges never loaded and Shaping the world sat at 0%. The build now takes its people to the map's far parts in turn, which stay loaded once made, then back to the middle. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/games/blockilization/server.ts | 25 +++++++++++++---- tests/headless/blockilization-build.ts | 38 ++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 tests/headless/blockilization-build.ts diff --git a/src/games/blockilization/server.ts b/src/games/blockilization/server.ts index c1da412..b9364b3 100644 --- a/src/games/blockilization/server.ts +++ b/src/games/blockilization/server.ts @@ -496,12 +496,19 @@ function begin(game: GameContext, g: Game, withSeats: LobbySeat[]) { sendLobby(game); } -/** The map's ground is loaded on the server: a point every chunk across it (an unloaded one collides). */ -function mapLoaded(game: GameContext, g: Game): boolean { +/** + * Where the map's ground isn't loaded on the server yet, if anywhere: a point every chunk across it + * (an unloaded one collides). Null: all of it is. + */ +function unloadedSpot(game: GameContext, g: Game): { x: number; z: number } | null { const size = extent(g.grid); // (Its last column is just inside: a world that wraps ends at a chunk's edge.) - for (let x = 0; x < size.w + 16; x += 16) for (let z = 0; z < size.d + 16; z += 16) if (game.world.collisionHeight(Math.min(x, size.w - 0.5), SEA_Y + 100, Math.min(z, size.d)) > 0) return false; - return true; + for (let x = 0; x < size.w + 16; x += 16) + for (let z = 0; z < size.d + 16; z += 16) { + const at = { x: Math.min(x, size.w - 0.5), z: Math.min(z, size.d) }; + if (game.world.collisionHeight(at.x, SEA_Y + 100, at.z) > 0) return at; + } + return null; } /** Somewhere far off: screens looking there let the map's ground go. */ @@ -1555,7 +1562,15 @@ export default defineServer(shared, { const size = extent(civGame.grid); const middle = { x: size.w / 2, y: SEA_Y + 40, z: size.d / 2 }; if (stage === 'gather') { - if (!mapLoaded(game, civGame)) return; + // The server loads the world round where people are, 12 chunks at most: a map wider than that + // (the large Earth's 14 from its middle) is loaded by taking them to its far parts in turn, + // which stay loaded once made. Then back to the middle. + const spot = unloadedSpot(game, civGame); + if (spot) { + for (const p of humans(game)) p.teleport({ x: spot.x, y: SEA_Y + 40, z: spot.z }); + return; + } + for (const p of humans(game)) p.teleport(middle); stage = 'away'; stageAt = game.clock.now; for (const p of humans(game)) lookAt(p, awayFrom(middle)); diff --git a/tests/headless/blockilization-build.ts b/tests/headless/blockilization-build.ts new file mode 100644 index 0000000..42ee05d --- /dev/null +++ b/tests/headless/blockilization-build.ts @@ -0,0 +1,38 @@ +import { readFileSync } from 'node:fs'; +import { GameHost } from '../../src/platform/host/game'; +import type { Game } from '../../src/games/blockilization/rules/state'; +import { extent } from '../../src/games/blockilization/geometry'; +import { check, games } from './_harness'; +import { devGames } from '../../src/games/server'; + +/** + * Blockilization builds every map, the widest too: the server loads the world 12 chunks round its + * people at most (as far as a screen asks), and the large Earth reaches 14 from its middle, so the + * build takes them to its far parts first. (It waited at 0% for them, for good, before.) + */ +export default async function blockilizationBuild() { + const def = [...games, ...(await devGames())].find((g) => g.id === 'blockilization')!; + for (const size of ['large', 'huge'] as const) { + // (As in a room: 8 columns round each person, raised to what their screens ask, a few columns a tick.) + const host = new GameHost(def, { engine: readFileSync('engine/pkg/voxel_engine_bg.wasm'), seed: 3, remote: true, radius: 8, cheats: true, player: { id: 'p1', name: 'Player' } }); + const civ = (globalThis as unknown as { __civ: { game: () => Game | null; phase: () => string } }).__civ; + const step = (seconds: number) => { + for (let t = 0; t < seconds; t += 1 / 30) host.step(1 / 30); + }; + const send = (id: string, name: string, data: unknown) => { + host.command(id, { t: 'message', msg: { t: 'game', player: id, name, data } }); + step(2 / 30); + }; + const ann = host.connect('Ann'); + host.command(ann.id, { t: 'start' }); + host.command(ann.id, { t: 'radius', columns: 12 }); + step(0.5); + send(ann.id, 'lobby', { a: 'settings', type: 'earth', size, cityStates: 4 }); + send(ann.id, 'lobby', { a: 'bots', count: 7 }); + send(ann.id, 'lobby', { a: 'start' }); + for (let i = 0; i < 240 && civ.phase() !== 'playing'; i++) step(0.5); + const across = extent(civ.game()!.grid).w / 32; + check(across > 12, `the ${size} Earth is wider than the server loads round one place (${across} chunks from its middle)`); + check(civ.phase() === 'playing', `the ${size} Earth is built and the game begins (${civ.phase()})`); + } +}