save/e2e: #51 — wire generateRing attach into resolveJumpTask + spread-limit e2e

- resolveJumpTask: build existingEdgeKeys from all jump routes (both
  endpoints in the save's system set), pass all systems except the
  target as attachCandidates, insert closing routes for each attach.
- save.test.ts: jump test now expects 1–4 fresh systems (was pinned
  to exactly 4); A/B snapshot test stays green (both paths share the
  same resolveJumpTask code).
- e2e/helpers.ts: update stale comment ('4 fresh neighbours' →
  '1–4 new systems').
- e2e/smoke.spec.ts: new test verifying 1–4 mints, all undiscovered,
  each routed to the target, and BFS connectivity of the full graph.
This commit is contained in:
2026-10-06 12:18:33 +00:00
parent 90de8818f8
commit ab22a7e68b
4 changed files with 71 additions and 11 deletions
+3 -2
View File
@@ -114,9 +114,10 @@ describe('jumpToSystem success path', () => {
expect(currentOf(after).id).toBe(target.id)
expect(after.systems.find((s) => s.id === target.id)!.discovered).toBe(true)
// Expansion: 4 new undiscovered neighbours around the target, each routed to it.
// #51: 1–4 new undiscovered systems minted around the target, each routed to it.
const fresh = after.systems.filter((s) => !w0.systems.some((p) => p.id === s.id))
expect(fresh).toHaveLength(4)
expect(fresh.length).toBeGreaterThanOrEqual(1)
expect(fresh.length).toBeLessThanOrEqual(4)
expect(fresh.every((s) => !s.discovered)).toBe(true)
for (const n of fresh) {
expect(after.edges.some(([a, b]) => (a === target.id && b === n.id) || (a === n.id && b === target.id))).toBe(true)
+21 -3
View File
@@ -354,15 +354,33 @@ export async function resolveJumpTask(db: GameDb, task: Task): Promise<void> {
await db.db.update(schema.solarSystems).set({ isCurrent: true, discovered: true }).where(eq(schema.solarSystems.id, target.id))
if (!target.discovered) {
// Growing the chart: a ring of new (undiscovered) neighbours around the newly reached
// system, so there is always somewhere to jump next and a route back.
// #51 — growing the chart: a seeded-random 1–4 leg ring around the newly reached
// system; each leg may attach an existing system (closing a loop) instead of minting.
const rng = expansionRng(save.seed, save.turn)
const takenNames = new Set(systems.map((s) => s.name))
const ring = generateRing(rng, target.x, target.y, takenNames, systems)
// Edge keys already touching any pair of this save's systems — an attach must not
// duplicate one (the PK on (fromSystem, toSystem) would reject it).
const systemIds = new Set(systems.map((s) => s.id))
const routes = await db.db.select().from(schema.jumpRoutes)
const existingEdgeKeys = new Set(
routes
.filter((r) => systemIds.has(r.fromSystem) && systemIds.has(r.toSystem))
.map((r) => `${Math.min(r.fromSystem, r.toSystem)}-${Math.max(r.fromSystem, r.toSystem)}`),
)
const candidates = systems.filter((s) => s.id !== target.id)
const ring = generateRing(rng, target.x, target.y, takenNames, systems, candidates, existingEdgeKeys, target.id)
const ringIds: number[] = []
for (const s of ring.systems) ringIds.push(await insertSystemWithBodies(db, save.id, s, save.seed))
const resolve = (index: number) => (index === 0 ? target.id : ringIds[index - 1])
await insertRoutes(db, ring.edges, resolve)
// Loop-closing attaches: a two-way route from the anchor to each attached candidate.
for (const ci of ring.attaches) {
const cand = candidates[ci]!
await db.db.insert(schema.jumpRoutes).values({ fromSystem: target.id, toSystem: cand.id })
await db.db.insert(schema.jumpRoutes).values({ fromSystem: cand.id, toSystem: target.id })
}
}
await db.db.insert(schema.events).values({ saveId: save.id, turn: save.turn, type: 'jump', message: `Jumped to ${target.name}`, createdAt: new Date().toISOString() })