@shipload/sdk 1.0.0-next.65 → 1.0.0-next.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/lib/shipload.d.ts +698 -316
  2. package/lib/shipload.js +2473 -1235
  3. package/lib/shipload.js.map +1 -1
  4. package/lib/shipload.m.js +2392 -1216
  5. package/lib/shipload.m.js.map +1 -1
  6. package/lib/testing.d.ts +205 -146
  7. package/lib/testing.js +846 -556
  8. package/lib/testing.js.map +1 -1
  9. package/lib/testing.m.js +846 -556
  10. package/lib/testing.m.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/capabilities/crafting.test.ts +51 -6
  13. package/src/capabilities/crafting.ts +19 -4
  14. package/src/capabilities/gathering.ts +2 -2
  15. package/src/capabilities/storage.test.ts +26 -0
  16. package/src/capabilities/storage.ts +6 -6
  17. package/src/contracts/fund.ts +79 -2
  18. package/src/contracts/platform.ts +10 -1
  19. package/src/contracts/server.ts +467 -331
  20. package/src/data/charters.json +325 -127
  21. package/src/data/entities.json +24 -24
  22. package/src/data/items.json +114 -114
  23. package/src/data/kind-registry.json +3 -3
  24. package/src/data/kind-registry.ts +5 -0
  25. package/src/data/recipes.json +56 -95
  26. package/src/derivation/build-methods.test.ts +59 -1
  27. package/src/derivation/build-methods.ts +2 -0
  28. package/src/derivation/capabilities.ts +7 -7
  29. package/src/derivation/strata.ts +5 -3
  30. package/src/derivation/tiers.ts +5 -5
  31. package/src/entity/hydrate.test.ts +156 -0
  32. package/src/entity/hydrate.ts +86 -0
  33. package/src/errors.ts +9 -0
  34. package/src/format.ts +15 -13
  35. package/src/index-module.ts +32 -5
  36. package/src/influence/ballot.test.ts +103 -0
  37. package/src/influence/ballot.ts +98 -0
  38. package/src/influence/charter-metadata.test.ts +136 -0
  39. package/src/influence/charter-metadata.ts +236 -0
  40. package/src/influence/charters.test.ts +51 -65
  41. package/src/influence/charters.ts +70 -40
  42. package/src/influence/civic.test.ts +278 -0
  43. package/src/influence/civic.ts +269 -0
  44. package/src/influence/constants.ts +30 -9
  45. package/src/influence/decomp.ts +15 -15
  46. package/src/influence/duration.ts +42 -6
  47. package/src/influence/index.ts +49 -6
  48. package/src/influence/influence.test.ts +76 -9
  49. package/src/influence/valuation.ts +17 -17
  50. package/src/managers/actions.ts +65 -37
  51. package/src/managers/base.ts +4 -0
  52. package/src/managers/context.ts +3 -1
  53. package/src/managers/index.ts +4 -2
  54. package/src/managers/influence.test.ts +18 -20
  55. package/src/managers/influence.ts +137 -84
  56. package/src/managers/jobs.test.ts +2 -2
  57. package/src/managers/jobs.ts +4 -4
  58. package/src/nft/description.ts +10 -11
  59. package/src/planner/index.ts +54 -0
  60. package/src/planner/planner.test.ts +127 -2
  61. package/src/resolution/display-name.ts +1 -1
  62. package/src/scheduling/availability.test.ts +113 -0
  63. package/src/scheduling/availability.ts +48 -8
  64. package/src/scheduling/jobs.ts +1 -1
  65. package/src/scheduling/projection.ts +2 -16
  66. package/src/scheduling/task-cargo.test.ts +62 -1
  67. package/src/scheduling/task-cargo.ts +25 -10
  68. package/src/scheduling/task-effects.test.ts +35 -0
  69. package/src/scheduling/task-effects.ts +34 -0
  70. package/src/scheduling/unwrap.test.ts +3 -3
  71. package/src/scheduling/unwrap.ts +3 -3
  72. package/src/shipload.ts +22 -3
  73. package/src/travel/reach.test.ts +86 -0
  74. package/src/travel/reach.ts +23 -10
  75. package/src/travel/route-build.test.ts +63 -0
  76. package/src/travel/route-build.ts +60 -0
  77. package/src/travel/travel.ts +2 -1
  78. package/src/types.ts +6 -2
  79. package/src/utils/hash.ts +18 -2
@@ -0,0 +1,136 @@
1
+ import {describe, expect, test} from 'bun:test'
2
+
3
+ import {CHARTER_REGISTRY, type CharterNode} from './charters'
4
+ import {
5
+ charterMetadata,
6
+ charterName,
7
+ charterSignature,
8
+ charterSummary,
9
+ romanNumeral,
10
+ type CharterSignature,
11
+ } from './charter-metadata'
12
+
13
+ function nodeById(nodeId: number): CharterNode {
14
+ const node = CHARTER_REGISTRY.find((candidate) => candidate.nodeId === nodeId)
15
+ if (!node) throw new Error(`no charter node ${nodeId} in the registry`)
16
+ return node
17
+ }
18
+
19
+ function signatureFor(nodeId: number): CharterSignature {
20
+ const signature = charterSignature(nodeById(nodeId))
21
+ if (!signature) throw new Error(`no signature for charter node ${nodeId}`)
22
+ return signature
23
+ }
24
+
25
+ function sharesWord(name: string, label: string): boolean {
26
+ const haystack = name.toLowerCase()
27
+ return label
28
+ .split(/\s+/)
29
+ .filter((word) => word.length > 2)
30
+ .some((word) => haystack.includes(word.toLowerCase()))
31
+ }
32
+
33
+ describe('charter metadata', () => {
34
+ test('covers every node in the chain-synced registry', () => {
35
+ const missing = CHARTER_REGISTRY.filter((node) => !charterMetadata[node.nodeId])
36
+ expect(missing).toEqual([])
37
+ })
38
+
39
+ test('carries no entry for a node the registry does not define', () => {
40
+ const known = new Set(CHARTER_REGISTRY.map((node) => node.nodeId))
41
+ const orphans = Object.keys(charterMetadata)
42
+ .map(Number)
43
+ .filter((nodeId) => !known.has(nodeId))
44
+ expect(orphans).toEqual([])
45
+ })
46
+
47
+ test('every entry has a name and a summary', () => {
48
+ for (const node of CHARTER_REGISTRY) {
49
+ expect(charterName(node.nodeId).length).toBeGreaterThan(0)
50
+ expect(charterSummary(node.nodeId).length).toBeGreaterThan(0)
51
+ }
52
+ })
53
+
54
+ test('falls back to a node-numbered name for an unknown id', () => {
55
+ expect(charterName(9999)).toBe('Charter 9999')
56
+ expect(charterSummary(9999)).toBe('')
57
+ })
58
+ })
59
+
60
+ describe('charter names agree with the grant they are derived from', () => {
61
+ for (const node of CHARTER_REGISTRY) {
62
+ const signature = signatureFor(node.nodeId)
63
+ const name = charterName(node.nodeId)
64
+
65
+ test(`node ${node.nodeId} names the building it grants (${signature.buildingLabel})`, () => {
66
+ expect(sharesWord(name, signature.buildingLabel)).toBeTrue()
67
+ })
68
+
69
+ if (signature.kind !== 'gate' && signature.rankCount > 1) {
70
+ test(`node ${node.nodeId} carries rank ${signature.rank} of ${signature.rankCount}`, () => {
71
+ expect(name.endsWith(` ${romanNumeral(signature.rank)}`)).toBeTrue()
72
+ })
73
+ }
74
+ }
75
+ })
76
+
77
+ describe('charter signatures', () => {
78
+ test('reads the depot level gate from its grant list', () => {
79
+ expect(signatureFor(40100001)).toEqual({kind: 'gate', buildingLabel: 'Depot', level: 1})
80
+ })
81
+
82
+ test('ranks the depot bays by their prereq chain', () => {
83
+ expect([40100101, 40100102, 40100103, 40100104].map(signatureFor)).toEqual(
84
+ [1, 2, 3, 4].map((rank) => ({
85
+ kind: 'module',
86
+ buildingLabel: 'Depot',
87
+ count: 1,
88
+ rank,
89
+ rankCount: 4,
90
+ }))
91
+ )
92
+ })
93
+
94
+ test('ranks the depot transfer rungs by their prereq chain', () => {
95
+ expect([40100401, 40100402, 40100403, 40100404].map(signatureFor)).toEqual([
96
+ {
97
+ kind: 'rung',
98
+ buildingLabel: 'Depot',
99
+ statLabel: 'transfer speed',
100
+ rank: 1,
101
+ rankCount: 4,
102
+ },
103
+ {
104
+ kind: 'rung',
105
+ buildingLabel: 'Depot',
106
+ statLabel: 'transfer speed',
107
+ rank: 2,
108
+ rankCount: 4,
109
+ },
110
+ {
111
+ kind: 'rung',
112
+ buildingLabel: 'Depot',
113
+ statLabel: 'transfer speed',
114
+ rank: 3,
115
+ rankCount: 4,
116
+ },
117
+ {
118
+ kind: 'rung',
119
+ buildingLabel: 'Depot',
120
+ statLabel: 'transfer speed',
121
+ rank: 4,
122
+ rankCount: 4,
123
+ },
124
+ ])
125
+ })
126
+
127
+ test('ranks the Workshop tune-ups as a four-rung ladder', () => {
128
+ expect(signatureFor(10100203)).toEqual({
129
+ kind: 'rung',
130
+ buildingLabel: 'Workshop',
131
+ statLabel: 'crafting speed',
132
+ rank: 3,
133
+ rankCount: 4,
134
+ })
135
+ })
136
+ })
@@ -0,0 +1,236 @@
1
+ import {CHARTER_REGISTRY, type CharterGrant, type CharterNode} from './charters'
2
+ import {
3
+ CHARTER_NONE,
4
+ CIVIC_DEPOT,
5
+ CIVIC_DOCK,
6
+ CIVIC_GRANT_LEVEL_GATE,
7
+ CIVIC_GRANT_MODULE,
8
+ CIVIC_GRANT_RUNG,
9
+ CIVIC_NEXUS,
10
+ CIVIC_STAT_BUILD_SPEED,
11
+ CIVIC_STAT_CRAFT_SPEED,
12
+ CIVIC_STAT_STORAGE_CAPACITY,
13
+ CIVIC_STAT_TRANSFER_SPEED,
14
+ CIVIC_WORKSHOP,
15
+ } from './constants'
16
+
17
+ const BUILDING_LABELS: Record<number, string> = {
18
+ [CIVIC_WORKSHOP]: 'Workshop',
19
+ [CIVIC_NEXUS]: 'Nexus',
20
+ [CIVIC_DOCK]: 'Construction Dock',
21
+ [CIVIC_DEPOT]: 'Depot',
22
+ }
23
+
24
+ const STAT_LABELS: Record<number, string> = {
25
+ [CIVIC_STAT_CRAFT_SPEED]: 'crafting speed',
26
+ [CIVIC_STAT_BUILD_SPEED]: 'build speed',
27
+ [CIVIC_STAT_TRANSFER_SPEED]: 'transfer speed',
28
+ [CIVIC_STAT_STORAGE_CAPACITY]: 'storage capacity',
29
+ }
30
+
31
+ export function civicBuildingLabel(building: number): string {
32
+ return BUILDING_LABELS[building] ?? `Building ${building}`
33
+ }
34
+
35
+ export function civicStatLabel(stat: number): string {
36
+ return STAT_LABELS[stat] ?? `Stat ${stat}`
37
+ }
38
+
39
+ export interface CharterMeta {
40
+ name: string
41
+ summary: string
42
+ }
43
+
44
+ export const charterMetadata: Record<number, CharterMeta> = {
45
+ 10001: {
46
+ name: 'Communal Workshop',
47
+ summary: 'Shared crafting for anyone at this world',
48
+ },
49
+ 10100201: {
50
+ name: 'Workshop tune-up I',
51
+ summary: "Raises the Workshop's Fabricator stats",
52
+ },
53
+ 10100202: {
54
+ name: 'Workshop tune-up II',
55
+ summary: "Raises the Workshop's Fabricator stats",
56
+ },
57
+ 10100203: {
58
+ name: 'Workshop tune-up III',
59
+ summary: "Raises the Workshop's Fabricator stats",
60
+ },
61
+ 10100204: {
62
+ name: 'Workshop tune-up IV',
63
+ summary: "Raises the Workshop's Fabricator stats",
64
+ },
65
+ 20001: {
66
+ name: 'Civic Nexus',
67
+ summary: 'Advances the world to Tier II; wrap items for trade and unwrap them here',
68
+ },
69
+ 30100001: {
70
+ name: 'Construction Dock',
71
+ summary: 'Ships queue here for upgrades',
72
+ },
73
+ 30100301: {
74
+ name: 'Dock tune-up I',
75
+ summary: "Raises the Dock's Assembly Arm stats",
76
+ },
77
+ 30100302: {
78
+ name: 'Dock tune-up II',
79
+ summary: "Raises the Dock's Assembly Arm stats",
80
+ },
81
+ 30100303: {
82
+ name: 'Dock tune-up III',
83
+ summary: "Raises the Dock's Assembly Arm stats",
84
+ },
85
+ 30100304: {
86
+ name: 'Dock tune-up IV',
87
+ summary: "Raises the Dock's Assembly Arm stats",
88
+ },
89
+ 40100001: {
90
+ name: 'Public Depot',
91
+ summary: 'Storage at this world for every player',
92
+ },
93
+ 40100101: {
94
+ name: 'Depot Cargo Hold I',
95
+ summary: 'Raises what each player can keep at the depot',
96
+ },
97
+ 40100102: {
98
+ name: 'Depot Cargo Hold II',
99
+ summary: 'Raises what each player can keep at the depot',
100
+ },
101
+ 40100103: {
102
+ name: 'Depot Cargo Hold III',
103
+ summary: 'Raises what each player can keep at the depot',
104
+ },
105
+ 40100104: {
106
+ name: 'Depot Cargo Hold IV',
107
+ summary: 'Raises what each player can keep at the depot',
108
+ },
109
+ 40100401: {
110
+ name: 'Depot Shuttle Bay I',
111
+ summary: 'Moves cargo in and out of the depot faster',
112
+ },
113
+ 40100402: {
114
+ name: 'Depot Shuttle Bay II',
115
+ summary: 'Moves cargo in and out of the depot faster',
116
+ },
117
+ 40100403: {
118
+ name: 'Depot Shuttle Bay III',
119
+ summary: 'Moves cargo in and out of the depot faster',
120
+ },
121
+ 40100404: {
122
+ name: 'Depot Shuttle Bay IV',
123
+ summary: 'Moves cargo in and out of the depot faster',
124
+ },
125
+ }
126
+
127
+ export function charterName(nodeId: number): string {
128
+ return charterMetadata[nodeId]?.name ?? `Charter ${nodeId}`
129
+ }
130
+
131
+ export function charterSummary(nodeId: number): string {
132
+ return charterMetadata[nodeId]?.summary ?? ''
133
+ }
134
+
135
+ export interface CharterGateSignature {
136
+ kind: 'gate'
137
+ buildingLabel: string
138
+ level: number
139
+ }
140
+
141
+ export interface CharterModuleSignature {
142
+ kind: 'module'
143
+ buildingLabel: string
144
+ count: number
145
+ rank: number
146
+ rankCount: number
147
+ }
148
+
149
+ export interface CharterRungSignature {
150
+ kind: 'rung'
151
+ buildingLabel: string
152
+ statLabel: string
153
+ rank: number
154
+ rankCount: number
155
+ }
156
+
157
+ export type CharterSignature = CharterGateSignature | CharterModuleSignature | CharterRungSignature
158
+
159
+ const ROMAN = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X']
160
+
161
+ export function romanNumeral(rank: number): string {
162
+ return ROMAN[rank - 1] ?? String(rank)
163
+ }
164
+
165
+ function headlineGrant(node: CharterNode): CharterGrant | undefined {
166
+ return (
167
+ node.grants.find((grant) => grant.kind === CIVIC_GRANT_LEVEL_GATE) ??
168
+ node.grants.find((grant) => grant.kind === CIVIC_GRANT_MODULE) ??
169
+ node.grants.find((grant) => grant.kind === CIVIC_GRANT_RUNG)
170
+ )
171
+ }
172
+
173
+ function headlineSiblings(headline: CharterGrant): CharterNode[] {
174
+ return CHARTER_REGISTRY.filter((other) => {
175
+ const grant = headlineGrant(other)
176
+ return (
177
+ grant !== undefined &&
178
+ grant.kind === headline.kind &&
179
+ grant.building === headline.building &&
180
+ grant.stat === headline.stat
181
+ )
182
+ })
183
+ }
184
+
185
+ function rungRank(node: CharterNode, siblings: CharterNode[]): number {
186
+ const byId = new Map(siblings.map((sibling) => [sibling.nodeId, sibling]))
187
+ const seen = new Set<number>([node.nodeId])
188
+ let current: CharterNode | undefined = node
189
+ let rank = 1
190
+ while (current) {
191
+ const previousId = current.prereqs.find((id) => byId.has(id) && !seen.has(id))
192
+ if (previousId === undefined) return rank
193
+ seen.add(previousId)
194
+ current = byId.get(previousId)
195
+ rank += 1
196
+ }
197
+ return rank
198
+ }
199
+
200
+ export function charterSignature(node: CharterNode): CharterSignature | undefined {
201
+ const grant = headlineGrant(node)
202
+ if (!grant) return undefined
203
+ if (grant.kind === CIVIC_GRANT_LEVEL_GATE) {
204
+ return {
205
+ kind: 'gate',
206
+ buildingLabel: civicBuildingLabel(grant.building),
207
+ level: grant.value,
208
+ }
209
+ }
210
+ const siblings = headlineSiblings(grant)
211
+ if (grant.kind === CIVIC_GRANT_MODULE) {
212
+ return {
213
+ kind: 'module',
214
+ buildingLabel: civicBuildingLabel(grant.building),
215
+ count: grant.value,
216
+ rank: rungRank(node, siblings),
217
+ rankCount: siblings.length,
218
+ }
219
+ }
220
+ return {
221
+ kind: 'rung',
222
+ buildingLabel: civicBuildingLabel(grant.building),
223
+ statLabel: civicStatLabel(grant.stat),
224
+ rank: rungRank(node, siblings),
225
+ rankCount: siblings.length,
226
+ }
227
+ }
228
+
229
+ for (const node of CHARTER_REGISTRY) {
230
+ if (node.nodeId === CHARTER_NONE) continue
231
+ if (!charterMetadata[node.nodeId]) {
232
+ throw new Error(
233
+ `Missing charter metadata for node ${node.nodeId}. Add an entry to charter-metadata.ts.`
234
+ )
235
+ }
236
+ }
@@ -1,31 +1,39 @@
1
1
  import {describe, expect, test} from 'bun:test'
2
- import {CHARTER_NONE} from './constants'
2
+ import {
3
+ CHARTER_NONE,
4
+ CIVIC_DEPOT,
5
+ CIVIC_DOCK,
6
+ CIVIC_STAT_CRAFT_SPEED,
7
+ CIVIC_STAT_TRANSFER_SPEED,
8
+ CIVIC_WORKSHOP,
9
+ } from './constants'
3
10
  import {
4
11
  charterEligible,
5
12
  charterIneligible,
6
13
  charterNode,
14
+ charterBuildingEntity,
15
+ charterGateNodeFor,
16
+ charterRungValue,
7
17
  charterSingletonMandate,
8
- charterSpawnNodeFor,
9
- effectiveMandate,
18
+ charterModuleCount,
10
19
  eligibleCharters,
11
20
  type BuiltCharter,
12
21
  type CharterWorld,
22
+ type WorldBuilding,
13
23
  } from './charters'
14
24
 
15
- const WORKSHOP = 1
16
- const NEXUS = 2
17
- const DOCK = 3
18
- const WORKSHOP_TUNEUP = 4
19
- const DOCK_TUNEUP = 5
20
- const DEPOT = 6
25
+ const WORKSHOP = 10001
26
+ const NEXUS = 20001
27
+ const DOCK = 30100001
28
+ const WORKSHOP_TUNEUP = 10100201
29
+ const DOCK_TUNEUP = 30100301
30
+ const DEPOT = 40100001
21
31
 
22
32
  const WORKSHOP_ENTITY = 100n
23
33
  const DOCK_ENTITY = 300n
24
- const DEPOT_ENTITY = 600n
25
34
 
26
- function world(built: BuiltCharter[], present?: bigint[]): CharterWorld {
27
- if (!present) return {built}
28
- return {built, entityExists: (id) => present.includes(id)}
35
+ function world(built: BuiltCharter[]): CharterWorld {
36
+ return {built}
29
37
  }
30
38
 
31
39
  function node(nodeId: number) {
@@ -48,13 +56,13 @@ describe('charter eligibility mirror', () => {
48
56
  })
49
57
 
50
58
  test('completing the root opens the fork and closes the singleton path', () => {
51
- const built = world([{nodeId: WORKSHOP, entityId: WORKSHOP_ENTITY}])
52
- expect(eligibleIds(built)).toEqual([NEXUS, DOCK, WORKSHOP_TUNEUP, DEPOT])
59
+ const built = world([{nodeId: WORKSHOP, repeats: 0}])
60
+ expect(eligibleIds(built)).toEqual([WORKSHOP_TUNEUP, NEXUS, DOCK, DEPOT])
53
61
  expect(charterSingletonMandate(built)).toBe(CHARTER_NONE)
54
62
  })
55
63
 
56
64
  test('a completed charter reports already-taken', () => {
57
- const built = world([{nodeId: WORKSHOP, entityId: WORKSHOP_ENTITY}])
65
+ const built = world([{nodeId: WORKSHOP, repeats: 0}])
58
66
  expect(charterIneligible(built, node(WORKSHOP))).toBe('already-taken')
59
67
  })
60
68
 
@@ -63,64 +71,42 @@ describe('charter eligibility mirror', () => {
63
71
  expect(charterIneligible(world([]), node(DOCK_TUNEUP))).toBe('prereq-missing')
64
72
  })
65
73
 
66
- test('a refit whose spawned target is gone reports refit-target-missing', () => {
67
- const built = [{nodeId: WORKSHOP, entityId: WORKSHOP_ENTITY}]
68
- expect(charterEligible(world(built, [WORKSHOP_ENTITY]), node(WORKSHOP_TUNEUP))).toBe(true)
69
- expect(charterIneligible(world(built, []), node(WORKSHOP_TUNEUP))).toBe(
70
- 'refit-target-missing'
71
- )
72
- })
73
-
74
- test('a demolished target can empty an otherwise singleton eligible set', () => {
75
- const built = [
76
- {nodeId: WORKSHOP, entityId: WORKSHOP_ENTITY},
77
- {nodeId: NEXUS, entityId: 200n},
78
- {nodeId: DOCK, entityId: DOCK_ENTITY},
79
- {nodeId: DOCK_TUNEUP, entityId: 0n},
80
- {nodeId: DEPOT, entityId: DEPOT_ENTITY},
81
- ]
82
- expect(charterSingletonMandate(world(built, [WORKSHOP_ENTITY, DOCK_ENTITY]))).toBe(
83
- WORKSHOP_TUNEUP
84
- )
85
- expect(charterSingletonMandate(world(built, [DOCK_ENTITY]))).toBe(CHARTER_NONE)
86
- })
87
-
88
- test('an absent entity predicate trusts the charter record', () => {
89
- const built = world([{nodeId: WORKSHOP, entityId: WORKSHOP_ENTITY}])
74
+ test('a rung is eligible as soon as its prereq is complete', () => {
75
+ const built = world([{nodeId: WORKSHOP, repeats: 0}])
90
76
  expect(charterEligible(built, node(WORKSHOP_TUNEUP))).toBe(true)
91
77
  })
92
78
 
93
- test('refit targets resolve through the node that spawns the item', () => {
94
- expect(charterSpawnNodeFor(node(WORKSHOP_TUNEUP).effect.targetItemId)?.nodeId).toBe(
95
- WORKSHOP
96
- )
97
- expect(charterSpawnNodeFor(node(DOCK_TUNEUP).effect.targetItemId)?.nodeId).toBe(DOCK)
98
- expect(charterSpawnNodeFor(0)).toBeUndefined()
79
+ test('a level gate is the node that creates its building', () => {
80
+ expect(charterGateNodeFor(CIVIC_WORKSHOP)?.nodeId).toBe(WORKSHOP)
81
+ expect(charterGateNodeFor(CIVIC_DOCK)?.nodeId).toBe(DOCK)
82
+ expect(charterGateNodeFor(CIVIC_DEPOT)?.nodeId).toBe(DEPOT)
83
+ expect(charterGateNodeFor(99)).toBeUndefined()
99
84
  })
100
- })
101
-
102
- describe('effective mandate', () => {
103
- const forked = world([{nodeId: WORKSHOP, entityId: WORKSHOP_ENTITY}])
104
85
 
105
- test('a stored choice holds inside its own epoch', () => {
106
- expect(effectiveMandate({chosen: DOCK, chosenEpoch: 7}, forked, 7)).toBe(DOCK)
86
+ test('a building resolves to the entity recorded for its kind', () => {
87
+ const buildings: WorldBuilding[] = [
88
+ {entityId: WORKSHOP_ENTITY, building: CIVIC_WORKSHOP},
89
+ {entityId: DOCK_ENTITY, building: CIVIC_DOCK},
90
+ ]
91
+ expect(charterBuildingEntity(buildings, CIVIC_WORKSHOP)).toBe(WORKSHOP_ENTITY)
92
+ expect(charterBuildingEntity(buildings, CIVIC_DEPOT)).toBe(0n)
93
+ expect(charterBuildingEntity([], CIVIC_DEPOT)).toBe(0n)
107
94
  })
108
95
 
109
- test('the same stored choice expires one epoch later', () => {
110
- expect(effectiveMandate({chosen: DOCK, chosenEpoch: 7}, forked, 8)).toBe(CHARTER_NONE)
111
- })
96
+ test('rung values sum over the completed nodes only', () => {
97
+ const gateOnly = world([{nodeId: WORKSHOP, repeats: 0}])
98
+ expect(charterRungValue(gateOnly, CIVIC_WORKSHOP, CIVIC_STAT_CRAFT_SPEED)).toBe(200)
112
99
 
113
- test('an expired choice falls through to the singleton rule', () => {
114
- const unbuilt = world([])
115
- expect(effectiveMandate({chosen: NEXUS, chosenEpoch: 7}, unbuilt, 8)).toBe(WORKSHOP)
100
+ const tuned = world([
101
+ {nodeId: WORKSHOP, repeats: 0},
102
+ {nodeId: WORKSHOP_TUNEUP, repeats: 0},
103
+ ])
104
+ expect(charterRungValue(tuned, CIVIC_WORKSHOP, CIVIC_STAT_CRAFT_SPEED)).toBe(250)
105
+ expect(charterRungValue(tuned, CIVIC_DEPOT, CIVIC_STAT_TRANSFER_SPEED)).toBe(0)
116
106
  })
117
107
 
118
- test('a cleared pair reads the singleton rule', () => {
119
- expect(effectiveMandate({chosen: CHARTER_NONE, chosenEpoch: 0}, world([]), 3)).toBe(
120
- WORKSHOP
121
- )
122
- expect(effectiveMandate({chosen: CHARTER_NONE, chosenEpoch: 0}, forked, 3)).toBe(
123
- CHARTER_NONE
124
- )
108
+ test('modules sum over the completed nodes only', () => {
109
+ expect(charterModuleCount(world([]), CIVIC_WORKSHOP)).toBe(0)
110
+ expect(charterModuleCount(world([{nodeId: WORKSHOP, repeats: 0}]), CIVIC_WORKSHOP)).toBe(5)
125
111
  })
126
112
  })