@benev/archimedes 0.1.0-1

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 (133) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +19 -0
  3. package/package.json +44 -0
  4. package/s/_archive/GLOSSARY.md +31 -0
  5. package/s/_archive/README.md +209 -0
  6. package/s/_archive/core/authority.ts +62 -0
  7. package/s/_archive/core/contact/codecs.i.ts +3 -0
  8. package/s/_archive/core/contact/codecs.ts +14 -0
  9. package/s/_archive/core/contact/contact.test.ts +32 -0
  10. package/s/_archive/core/contact/contact.ts +63 -0
  11. package/s/_archive/core/contact/types.ts +24 -0
  12. package/s/_archive/core/contact/wiring.i.ts +3 -0
  13. package/s/_archive/core/contact/wiring.ts +212 -0
  14. package/s/_archive/core/fiber.ts +94 -0
  15. package/s/_archive/core/liaison.ts +73 -0
  16. package/s/_archive/core/parcels/inbox.ts +83 -0
  17. package/s/_archive/core/parcels/parceller.ts +20 -0
  18. package/s/_archive/core/parcels/parcels.test.ts +136 -0
  19. package/s/_archive/core/parcels/types.ts +5 -0
  20. package/s/_archive/core/parcels/utils/nanny.ts +23 -0
  21. package/s/_archive/core/simulator.ts +11 -0
  22. package/s/_archive/core/speculator.ts +63 -0
  23. package/s/_archive/core/types.ts +32 -0
  24. package/s/_archive/core/utils/handle-telegrams.ts +34 -0
  25. package/s/_archive/core/utils/is-input-dispatch.ts +10 -0
  26. package/s/_archive/core/utils/make-telegram-for-inputs.ts +12 -0
  27. package/s/_archive/core/utils/on-channel-message.ts +11 -0
  28. package/s/_archive/eureka/eureka.ts +20 -0
  29. package/s/_archive/eureka/index.ts +0 -0
  30. package/s/_archive/eureka/integration/context.ts +9 -0
  31. package/s/_archive/eureka/integration/simulator.ts +75 -0
  32. package/s/_archive/eureka/integration/types.ts +12 -0
  33. package/s/_archive/eureka/integration/utils/inputs.ts +24 -0
  34. package/s/_archive/eureka/integration/utils/relevance.ts +10 -0
  35. package/s/_archive/eureka/parts/entity.ts +37 -0
  36. package/s/_archive/eureka/parts/system.ts +38 -0
  37. package/s/_archive/eureka/parts/types.ts +21 -0
  38. package/s/_archive/eureka/parts/world.ts +140 -0
  39. package/s/_archive/eureka/testing/eureka.test.ts +27 -0
  40. package/s/_archive/eureka/testing/integration.test.ts +40 -0
  41. package/s/_archive/eureka/testing/situations/health.ts +43 -0
  42. package/s/_archive/eureka/testing/situations/integration.ts +45 -0
  43. package/s/_archive/index.ts +6 -0
  44. package/s/_archive/session/client.ts +56 -0
  45. package/s/_archive/session/host.ts +71 -0
  46. package/s/_archive/session/meta/meta-client.ts +5 -0
  47. package/s/_archive/session/meta/meta-host.ts +18 -0
  48. package/s/_archive/session/meta/types.ts +15 -0
  49. package/s/_archive/session/parts/client-on.ts +8 -0
  50. package/s/_archive/session/parts/fiber-rpc.ts +28 -0
  51. package/s/_archive/session/parts/host-on.ts +9 -0
  52. package/s/_archive/session/parts/hub.ts +40 -0
  53. package/s/_archive/session/parts/netfibers.ts +14 -0
  54. package/s/_archive/session/parts/seat.ts +35 -0
  55. package/s/_archive/session/parts/spoke.ts +10 -0
  56. package/s/_archive/sugar/easy-host.ts +68 -0
  57. package/s/_archive/tests.test.ts +17 -0
  58. package/s/_archive/tools/averager.ts +29 -0
  59. package/s/_archive/tools/bucket.ts +17 -0
  60. package/s/_archive/tools/chronicle.ts +32 -0
  61. package/s/_archive/tools/disposers.ts +5 -0
  62. package/s/_archive/tools/id-counter.ts +13 -0
  63. package/s/_archive/tools/loop.ts +12 -0
  64. package/s/_archive/tools/pub.ts +22 -0
  65. package/s/_archive/tools/ticker.ts +22 -0
  66. package/s/_archive/tools/u8.ts +7 -0
  67. package/s/_archive/transports/sparrow/fibers.ts +19 -0
  68. package/s/_archive/transports/sparrow/start-sparrow-host.ts +26 -0
  69. package/s/_archive/transports/sparrow/utils/netfibers-from-cable.ts +10 -0
  70. package/s/ecs/index.ts +7 -0
  71. package/s/ecs/parts/changers.ts +16 -0
  72. package/s/ecs/parts/make-id.ts +7 -0
  73. package/s/ecs/parts/world.ts +68 -0
  74. package/s/ecs/test/setup-example-world.ts +42 -0
  75. package/s/ecs/test/setup-lifecycle-counts.ts +17 -0
  76. package/s/ecs/test.ts +99 -0
  77. package/s/ecs/types.ts +19 -0
  78. package/s/ecs/utils/is-match.ts +7 -0
  79. package/s/ecs/utils/optimizer.ts +38 -0
  80. package/s/index.ts +3 -0
  81. package/s/net/test.ts +9 -0
  82. package/s/net/types.ts +1 -0
  83. package/s/sim/test.ts +9 -0
  84. package/s/sim/types.ts +1 -0
  85. package/s/test.ts +8 -0
  86. package/x/ecs/index.d.ts +4 -0
  87. package/x/ecs/index.js +5 -0
  88. package/x/ecs/index.js.map +1 -0
  89. package/x/ecs/parts/changers.d.ts +4 -0
  90. package/x/ecs/parts/changers.js +11 -0
  91. package/x/ecs/parts/changers.js.map +1 -0
  92. package/x/ecs/parts/make-id.d.ts +1 -0
  93. package/x/ecs/parts/make-id.js +5 -0
  94. package/x/ecs/parts/make-id.js.map +1 -0
  95. package/x/ecs/parts/world.d.ts +11 -0
  96. package/x/ecs/parts/world.js +59 -0
  97. package/x/ecs/parts/world.js.map +1 -0
  98. package/x/ecs/test/setup-example-world.d.ts +10 -0
  99. package/x/ecs/test/setup-example-world.js +31 -0
  100. package/x/ecs/test/setup-example-world.js.map +1 -0
  101. package/x/ecs/test/setup-lifecycle-counts.d.ts +6 -0
  102. package/x/ecs/test/setup-lifecycle-counts.js +15 -0
  103. package/x/ecs/test/setup-lifecycle-counts.js.map +1 -0
  104. package/x/ecs/test.d.ts +13 -0
  105. package/x/ecs/test.js +85 -0
  106. package/x/ecs/test.js.map +1 -0
  107. package/x/ecs/types.d.ts +12 -0
  108. package/x/ecs/types.js +2 -0
  109. package/x/ecs/types.js.map +1 -0
  110. package/x/ecs/utils/is-match.d.ts +2 -0
  111. package/x/ecs/utils/is-match.js +4 -0
  112. package/x/ecs/utils/is-match.js.map +1 -0
  113. package/x/ecs/utils/optimizer.d.ts +9 -0
  114. package/x/ecs/utils/optimizer.js +37 -0
  115. package/x/ecs/utils/optimizer.js.map +1 -0
  116. package/x/index.d.ts +1 -0
  117. package/x/index.js +2 -0
  118. package/x/index.js.map +1 -0
  119. package/x/net/test.d.ts +4 -0
  120. package/x/net/test.js +7 -0
  121. package/x/net/test.js.map +1 -0
  122. package/x/net/types.d.ts +1 -0
  123. package/x/net/types.js +2 -0
  124. package/x/net/types.js.map +1 -0
  125. package/x/sim/test.d.ts +4 -0
  126. package/x/sim/test.js +7 -0
  127. package/x/sim/test.js.map +1 -0
  128. package/x/sim/types.d.ts +1 -0
  129. package/x/sim/types.js +2 -0
  130. package/x/sim/types.js.map +1 -0
  131. package/x/test.d.ts +1 -0
  132. package/x/test.js +6 -0
  133. package/x/test.js.map +1 -0
@@ -0,0 +1,63 @@
1
+
2
+ import {Liaison} from "./liaison.js"
3
+ import {loop} from "../tools/loop.js"
4
+ import {Simulator} from "./simulator.js"
5
+ import {Chronicle} from "../tools/chronicle.js"
6
+ import {AuthorId, Schema, Telegram} from "./types.js"
7
+ import {makeTelegramForInputs} from "./utils/make-telegram-for-inputs.js"
8
+
9
+ export class Speculator<xSchema extends Schema> {
10
+ #currentTick = 0
11
+ #chronicle = new Chronicle<xSchema["input"]>()
12
+
13
+ maxIncomingTicks = 5
14
+
15
+ constructor(
16
+ public authorId: AuthorId,
17
+ public liaison: Liaison<Telegram<xSchema>>,
18
+ public pastSimulator: Simulator<xSchema>,
19
+ public futureSimulator: Simulator<xSchema>,
20
+ public hz: number,
21
+ ) {}
22
+
23
+ get ticksAhead() {
24
+ const rtt = this.liaison.pingponger.averageRtt
25
+ return Math.round(rtt / (1000 / this.hz))
26
+ }
27
+
28
+ tick() {
29
+ const telegrams = this.liaison.recv().slice(-this.maxIncomingTicks)
30
+ const lastTelegram = telegrams.at(-1)
31
+
32
+ for (const telegram of telegrams)
33
+ this.pastSimulator.simulate(telegram)
34
+
35
+ if (lastTelegram) {
36
+ const [latestTick] = lastTelegram
37
+ this.#currentTick = latestTick
38
+ }
39
+ else this.#currentTick += 1
40
+
41
+ // roll-forward
42
+ this.futureSimulator.state = structuredClone(this.pastSimulator.state)
43
+
44
+ for (const t of loop(this.ticksAhead)) {
45
+ for (const inputs of this.#chronicle.at(t)) {
46
+ this.futureSimulator.simulate(
47
+ makeTelegramForInputs(t, this.authorId, inputs)
48
+ )
49
+ }
50
+ }
51
+ }
52
+
53
+ sendInputs(inputs: xSchema["input"][]) {
54
+ const futureTick = this.#currentTick + this.ticksAhead
55
+
56
+ // immediately send down the wire
57
+ this.liaison.send(makeTelegramForInputs(futureTick, this.authorId, inputs))
58
+
59
+ // schedule local inputs into the future
60
+ this.#chronicle.add(inputs, futureTick)
61
+ }
62
+ }
63
+
@@ -0,0 +1,32 @@
1
+
2
+ import {Simulator} from "./simulator.js"
3
+
4
+ export type TickNumber = number
5
+ export type AuthorId = number
6
+ export type Authored<Content> = [AuthorId, Content]
7
+
8
+ export type Schema = {
9
+ state: any
10
+ delta: any
11
+ input: any
12
+ }
13
+
14
+ export type StateDispatch<xSchema extends Schema> = ["state", xSchema["state"]]
15
+ export type DeltaDispatch<xSchema extends Schema> = ["delta", xSchema["delta"]]
16
+ export type InputDispatch<xSchema extends Schema> = ["input", Authored<xSchema["input"]>]
17
+
18
+ export type Dispatch<xSchema extends Schema> = (
19
+ | StateDispatch<xSchema>
20
+ | DeltaDispatch<xSchema>
21
+ | InputDispatch<xSchema>
22
+ )
23
+
24
+ export type Telegram<xSchema extends Schema> = [TickNumber, Dispatch<xSchema>[]]
25
+ export type InputTelegram<xSchema extends Schema> = [TickNumber, InputDispatch<xSchema["input"]>]
26
+
27
+ export type InferSimulatorSchema<S extends Simulator<any>> = (
28
+ S extends Simulator<infer xSchema>
29
+ ? xSchema
30
+ : never
31
+ )
32
+
@@ -0,0 +1,34 @@
1
+
2
+ import {AuthorId, Schema, Telegram} from "../types.js"
3
+
4
+ export function handleTelegram<xSchema extends Schema>(
5
+ telegram: Telegram<xSchema>,
6
+ callbacks: DispatchCallbacks<xSchema>,
7
+ ) {
8
+
9
+ const [authorId, dispatches] = telegram
10
+
11
+ for (const [key, content] of dispatches) {
12
+ switch (key) {
13
+
14
+ case "state":
15
+ callbacks.state(content, authorId)
16
+ break
17
+
18
+ case "delta":
19
+ callbacks.delta(content, authorId)
20
+ break
21
+
22
+ case "input":
23
+ callbacks.input(content, authorId)
24
+ break
25
+ }
26
+ }
27
+ }
28
+
29
+ export type DispatchCallbacks<xSchema extends Schema> = {
30
+ state: (state: xSchema["state"], authorId: AuthorId) => void
31
+ delta: (delta: xSchema["delta"], authorId: AuthorId) => void
32
+ input: (input: xSchema["input"], authorId: AuthorId) => void
33
+ }
34
+
@@ -0,0 +1,10 @@
1
+
2
+ import {Dispatch, InputDispatch, Schema} from "../types.js"
3
+
4
+ export function isInputDispatch<S extends Schema>(
5
+ dispatch: Dispatch<S>
6
+ ): dispatch is InputDispatch<S["input"]> {
7
+ const [kind] = dispatch
8
+ return kind === "input"
9
+ }
10
+
@@ -0,0 +1,12 @@
1
+
2
+ import {Schema, Telegram} from "../types.js"
3
+
4
+ export function makeTelegramForInputs<xSchema extends Schema>(
5
+ futureTick: number,
6
+ authorId: number,
7
+ inputs: xSchema["input"][],
8
+ ): Telegram<xSchema> {
9
+
10
+ return [futureTick, [["input", [authorId, inputs]]]]
11
+ }
12
+
@@ -0,0 +1,11 @@
1
+
2
+ export function onChannelMessage(
3
+ channel: RTCDataChannel,
4
+ onmessage: (message: any) => void,
5
+ ) {
6
+
7
+ const listener = (event: MessageEvent) => onmessage(event.data)
8
+ channel.addEventListener("message", listener)
9
+ return () => channel.removeEventListener("message", listener)
10
+ }
11
+
@@ -0,0 +1,20 @@
1
+
2
+ import {World} from "./parts/world.js"
3
+ import {System} from "./parts/system.js"
4
+ import {Components, SystemFn} from "./parts/types.js"
5
+
6
+ export const setupEureka = <Context, C extends Components>() => ({
7
+ system: (label: string) => ({
8
+ select: <K extends keyof C>(...keys: K[]) => ({
9
+ fn: (fn: SystemFn<Context, C, K, undefined>) => new System(label, keys, [], fn),
10
+ andMaybe: <K2 extends keyof C>(...keysOptional: K2[]) => ({
11
+ fn: (fn: SystemFn<Context, C, K, K2>) => new System(label, keys, keysOptional, fn),
12
+ }),
13
+ }),
14
+ }),
15
+
16
+ world: (context: Context, systems: System[]) => (
17
+ new World<Context, C>(context, systems)
18
+ ),
19
+ })
20
+
File without changes
@@ -0,0 +1,9 @@
1
+
2
+ import {Inputs} from "./utils/inputs.js"
3
+ import {Relevance} from "./utils/relevance.js"
4
+
5
+ export class EurekaContext {
6
+ inputs = new Inputs()
7
+ relevance = new Relevance()
8
+ }
9
+
@@ -0,0 +1,75 @@
1
+
2
+ import {World} from "../parts/world.js"
3
+ import {EurekaSchema} from "./types.js"
4
+ import {EurekaContext} from "./context.js"
5
+ import {Components, EntityId} from "../parts/types.js"
6
+ import {Simulator} from "../../core/simulator.js"
7
+ import {AuthorId, Dispatch, Telegram} from "../../core/types.js"
8
+
9
+ export class EurekaSimulator
10
+ <xContext extends EurekaContext, C extends Components>
11
+ extends Simulator<EurekaSchema<C>> {
12
+
13
+ #deltas: EurekaSchema<C>["delta"] = []
14
+ #authorityId = 0
15
+
16
+ constructor(public world: World<xContext, C>) {
17
+ super([...world.data()])
18
+ world.on((id, entity) => void this.#deltas.push([id, entity?.components ?? null]))
19
+ }
20
+
21
+ simulate(telegram: Telegram<EurekaSchema<C>>): EurekaSchema<C>["delta"] {
22
+ this.#deltas = []
23
+
24
+ Simulator.handleTelegram(telegram, {
25
+ input: (inputs) => {
26
+ this.world.context.inputs.add(inputs)
27
+ },
28
+
29
+ state: (state, authorId) => {
30
+ if (authorId === this.#authorityId) {
31
+ this.world.clear()
32
+ this.world.overwrite(state)
33
+ }
34
+ },
35
+
36
+ delta: (deltas, authorId) => {
37
+ if (authorId === this.#authorityId)
38
+ return undefined
39
+ for (const [id, components] of deltas)
40
+ this.world.write(id, components)
41
+ },
42
+ })
43
+
44
+ this.world.execute()
45
+ return this.#deltas
46
+ }
47
+
48
+ tailor(audienceAuthorId: AuthorId, telegram: Telegram<EurekaSchema<C>>): Telegram<EurekaSchema<C>> {
49
+ const check = (eid: EntityId) => this.world.context.relevance.check(audienceAuthorId, eid)
50
+ const [telegramAuthorId, dispatches] = telegram
51
+ const relevantDispatches: Dispatch<EurekaSchema<C>>[] = []
52
+ for (const [kind, x] of dispatches) {
53
+ switch (kind) {
54
+
55
+ case "state": {
56
+ relevantDispatches.push([kind, x.filter(([id]) => check(id))])
57
+ } break
58
+
59
+ case "delta": {
60
+ relevantDispatches.push([kind, x.filter(([id]) => check(id))])
61
+ } break
62
+
63
+ case "input": {
64
+ const [authorId, inputEntries] = x
65
+ relevantDispatches.push([
66
+ kind,
67
+ [authorId, inputEntries.filter(([id]) => check(id))],
68
+ ])
69
+ } break
70
+ }
71
+ }
72
+ return [telegramAuthorId, relevantDispatches]
73
+ }
74
+ }
75
+
@@ -0,0 +1,12 @@
1
+
2
+ import {Components, EntityId, EntityData} from "../parts/types.js"
3
+
4
+ export type InputEntry = [EntityId, unknown[]]
5
+ export type Delta<C extends Components> = EntityData<C>
6
+
7
+ export type EurekaSchema<C extends Components> = {
8
+ state: EntityData<C>[]
9
+ delta: Delta<C>[]
10
+ input: InputEntry[]
11
+ }
12
+
@@ -0,0 +1,24 @@
1
+
2
+ import {MapG} from "@e280/stz"
3
+ import {InputEntry} from "../types.js"
4
+ import {EntityId} from "../../parts/types.js"
5
+
6
+ export class Inputs {
7
+ #inbox = new MapG<EntityId, unknown[]>()
8
+
9
+ add(entries: InputEntry[]) {
10
+ for (const [id, newInputs] of entries) {
11
+ const inputs = this.#inbox.guarantee(id, () => [])
12
+ inputs.push(...newInputs)
13
+ }
14
+ }
15
+
16
+ read<Input = unknown>(id: EntityId) {
17
+ return (this.#inbox.get(id) ?? []) as Input[]
18
+ }
19
+
20
+ clear() {
21
+ this.#inbox.clear()
22
+ }
23
+ }
24
+
@@ -0,0 +1,10 @@
1
+
2
+ import {EntityId} from "../../parts/types.js"
3
+ import {AuthorId} from "../../../core/types.js"
4
+
5
+ export class Relevance {
6
+ check(_authorId: AuthorId, _entityId: EntityId) {
7
+ return true
8
+ }
9
+ }
10
+
@@ -0,0 +1,37 @@
1
+
2
+ import {Components, EntityId, EntityData} from "./types.js"
3
+
4
+ export class Entity<C extends Components = any> {
5
+ static setComponents(entity: Entity, components: Components) {
6
+ entity.#components = new Proxy(components, {
7
+ set: (target: any, key: string, value: any) => {
8
+ if (value === undefined) delete target[key]
9
+ else target[key] = value
10
+ entity.onChange()
11
+ return true
12
+ },
13
+ deleteProperty: (target: any, key: string) => {
14
+ delete target[key]
15
+ entity.onChange()
16
+ return true
17
+ },
18
+ })
19
+ }
20
+
21
+ #components!: C
22
+ get components() { return this.#components }
23
+
24
+ constructor(
25
+ public readonly id: EntityId,
26
+ private onChange: () => void,
27
+ ) {}
28
+
29
+ write() {
30
+ this.onChange()
31
+ }
32
+
33
+ data() {
34
+ return [this.id, this.components] as EntityData<C>
35
+ }
36
+ }
37
+
@@ -0,0 +1,38 @@
1
+
2
+ import {MapG} from "@e280/stz"
3
+ import {Entity} from "./entity.js"
4
+ import {World} from "./world.js"
5
+ import {SystemFn, UnknownComponents} from "./types.js"
6
+
7
+ export class System {
8
+ #cacheMap = new MapG<number, Entity>()
9
+ #cacheArray: Entity[] = []
10
+
11
+ constructor(
12
+ public label: string,
13
+ public keys: any[],
14
+ public keysOptional: any[],
15
+ public fn: SystemFn<any, any, any, any>,
16
+ ) {}
17
+
18
+ execute(world: World<any, any>) {
19
+ this.fn(this.#cacheArray, world)
20
+ }
21
+
22
+ cache(id: number, entity: Entity | null) {
23
+ if (entity) {
24
+ if (this.#matching(entity)) this.#cacheMap.set(id, entity)
25
+ else this.#cacheMap.delete(id)
26
+ this.#cacheArray = [...this.#cacheMap.values()]
27
+ }
28
+ else {
29
+ this.#cacheMap.delete(id)
30
+ this.#cacheArray = [...this.#cacheMap.values()]
31
+ }
32
+ }
33
+
34
+ #matching(entity: UnknownComponents) {
35
+ return this.keys.every(requiredKey => requiredKey in entity.components)
36
+ }
37
+ }
38
+
@@ -0,0 +1,21 @@
1
+
2
+ import {World} from "./world.js"
3
+ import {Entity} from "./entity.js"
4
+
5
+ export type Components = Record<string, any>
6
+ export type EntityId = number
7
+
8
+ export type EntityData<C extends Components = any> = [EntityId, UnknownComponents<C> | null]
9
+ export type UnknownComponents<C extends Components = any> = Partial<C>
10
+ export type SpecificComponents<C extends Components, K extends keyof C> = {[X in K]: C[X]}
11
+
12
+ export type FancySelect<C extends Components, K extends keyof C, K2 extends (keyof C) | undefined> = (
13
+ K2 extends keyof C
14
+ ? Entity<SpecificComponents<C, K> & Partial<SpecificComponents<C, K2>>>
15
+ : Entity<SpecificComponents<C, K>>
16
+ )
17
+
18
+ export type SystemFn<Context, C extends Components, K extends keyof C, K2 extends (keyof C) | undefined> = (
19
+ (entities: FancySelect<C, K, K2>[], world: World<Context, C>) => void
20
+ )
21
+
@@ -0,0 +1,140 @@
1
+
2
+ import {deep, MapG, sub} from "@e280/stz"
3
+ import {System} from "./system.js"
4
+ import {Entity} from "./entity.js"
5
+ import {IdCounter} from "../../tools/id-counter.js"
6
+ import {Components, EntityId, EntityData, UnknownComponents} from "./types.js"
7
+
8
+ export class World<Context, C extends Components> {
9
+ counter = new IdCounter()
10
+ on = sub<[EntityId, Entity<UnknownComponents<C>> | null]>()
11
+
12
+ #entities = new MapG<EntityId, Entity<Partial<C>>>()
13
+ #changed = new Set<EntityId>()
14
+
15
+ constructor(public context: Context, private systems: System[]) {}
16
+
17
+ /** iterate all entities */
18
+ all() {
19
+ return this.#entities.values()
20
+ }
21
+
22
+ /** iterate all entity ids */
23
+ ids() {
24
+ return this.#entities.keys()
25
+ }
26
+
27
+ /** get an entity by id (return null if not found) */
28
+ get<C2 extends Partial<C2>>(id: number) {
29
+ return (this.#entities.get(id) ?? null) as Entity<C2> | null
30
+ }
31
+
32
+ /** get an entity by id (throw an error if not found) */
33
+ require<C2 extends Partial<C2>>(id: number) {
34
+ return this.#entities.require(id) as unknown as C2
35
+ }
36
+
37
+ /** create, update, or delete an entity */
38
+ write<C2 extends Partial<C>>(id: number, components: C2 | null) {
39
+ if (components) {
40
+ const onChange = () => this.#changed.add(id)
41
+
42
+ // get or create entity
43
+ const entity = this.#entities.guarantee(
44
+ id,
45
+ () => new Entity(id, onChange),
46
+ ) as Entity<C2>
47
+
48
+ Entity.setComponents(entity, components)
49
+
50
+ // initialize the systems cache for this entity
51
+ for (const system of this.systems)
52
+ system.cache(id, entity)
53
+
54
+ // recognize that a change happened
55
+ this.on.pub(id, entity)
56
+ return entity
57
+ }
58
+ else {
59
+ // delete the entity
60
+ this.#entities.delete(id)
61
+
62
+ // update the cache
63
+ for (const system of this.systems)
64
+ system.cache(id, null)
65
+
66
+ // recognize that a change happened
67
+ this.on.pub(id, null)
68
+ return null
69
+ }
70
+ }
71
+
72
+ /** create a new entity */
73
+ create<C2 extends Partial<C>>(components: C2) {
74
+ const id = this.counter.next()
75
+ return this.write(id, components)!
76
+ }
77
+
78
+ /** update or create an entity */
79
+ update<C2 extends Partial<C>>(id: number, components: C2) {
80
+ return this.write(id, components) as Entity<C2>
81
+ }
82
+
83
+ /** delete an entity, dispatching relevant pubsubs */
84
+ delete(id: number) {
85
+ return this.write(id, null)
86
+ }
87
+
88
+ /** iterate all entity data */
89
+ *data() {
90
+ for (const entity of this.all())
91
+ yield entity.data()
92
+ }
93
+
94
+ /** delete everything */
95
+ clear() {
96
+ for (const id of [...this.#entities.keys()])
97
+ this.delete(id)
98
+ }
99
+
100
+ /** forget everything and start from a new state */
101
+ overwrite(data: EntityData<C>[]) {
102
+ for (const [id, components] of data)
103
+ this.write(id, components)
104
+ }
105
+
106
+ /** execute all systems (and maintain caching stuff) */
107
+ execute() {
108
+ // execute all systems
109
+ for (const system of this.systems) {
110
+ system.execute(this)
111
+
112
+ // update all system caches for changes
113
+ for (const id of this.#changed) {
114
+ const entity = this.get(id)
115
+ for (const system of this.systems)
116
+ system.cache(id, entity)
117
+ }
118
+ }
119
+
120
+ // dispatch all pent up changes
121
+ for (const id of this.#changed)
122
+ this.on.pub(id, this.get(id))
123
+
124
+ // clear changes
125
+ this.#changed.clear()
126
+ }
127
+
128
+ /** create a new downstream clone world that stays synchronized with this one */
129
+ replicate(
130
+ context: Context,
131
+ systems: System[],
132
+ ) {
133
+ const downstream = new World<Context, C>(context, systems)
134
+ const detach = this.on((id, entity) => {
135
+ downstream.write(id, deep.clone(entity?.components ?? null))
136
+ })
137
+ return [downstream, detach]
138
+ }
139
+ }
140
+
@@ -0,0 +1,27 @@
1
+
2
+ import {Science, test, expect} from "@e280/science"
3
+ import {setupHealthSituation} from "./situations/health.js"
4
+
5
+ export default Science.suite({
6
+ "systems can execute on the right entities": test(async() => {
7
+ const {world} = setupHealthSituation()
8
+ const warrior = world.create({health: 100, bleeding: 1})
9
+ const wizard = world.create({health: 100, mana: 0, manaRegen: 1})
10
+ expect(warrior.components.health).is(100)
11
+ expect(wizard.components.health).is(100)
12
+ world.execute()
13
+ expect(warrior.components.health).is(99)
14
+ expect(wizard.components.health).is(100)
15
+ }),
16
+
17
+ "warrior can bleed out, gets deleted": test(async() => {
18
+ const {world} = setupHealthSituation()
19
+ const warrior = world.create({health: 2, bleeding: 1})
20
+ expect(world.get(warrior.id)).ok()
21
+ world.execute()
22
+ expect(world.get(warrior.id)).ok()
23
+ world.execute()
24
+ expect(world.get(warrior.id)).not.ok()
25
+ }),
26
+ })
27
+
@@ -0,0 +1,40 @@
1
+
2
+ import {loop} from "@e280/stz"
3
+ import {Science, test, expect} from "@e280/science"
4
+
5
+ import {EasyHost} from "../../sugar/easy-host.js"
6
+ import {setupEurekaDemo} from "./situations/integration.js"
7
+ import {EurekaSimulator} from "../integration/simulator.js"
8
+
9
+ export default Science.suite({
10
+
11
+ "we host a game, we join it": test(async() => {
12
+ const host = new EasyHost({
13
+ hz: 10,
14
+ makeSimulator: () => {
15
+ const {world} = setupEurekaDemo()
16
+ return new EurekaSimulator(world)
17
+ },
18
+ })
19
+ const client = await host.localClient()
20
+
21
+ const {world} = host.session.simulator
22
+ const warrior = world.create({health: 100, bleeding: 1})
23
+
24
+ for (const _ of loop(50)) {
25
+ host.session.authority.tick()
26
+ client.speculator.tick()
27
+ }
28
+
29
+ expect(warrior.components.health).is(50)
30
+
31
+ // TODO wut?
32
+ // // FAILS HERE, component not found
33
+ // const clientWarrior = client.pastSimulator.world
34
+ // .require<typeof warrior.components>(warrior.id)
35
+ //
36
+ // expect(clientWarrior).ok()
37
+ // expect(clientWarrior.health).is(50)
38
+ }),
39
+ })
40
+
@@ -0,0 +1,43 @@
1
+
2
+ import {setupEureka} from "../../eureka.js"
3
+
4
+ class MyContext {}
5
+
6
+ type MyComponents = {
7
+ health: number
8
+ bleeding: number
9
+ mana: number
10
+ manaRegen: number
11
+ }
12
+
13
+ export function setupHealthSituation() {
14
+ const eureka = setupEureka<MyContext, MyComponents>()
15
+
16
+ const world = eureka.world(new MyContext(), [
17
+ eureka.system("health")
18
+ .select("health").andMaybe("bleeding")
19
+ .fn((entities, world) => {
20
+ for (const {id, components} of entities) {
21
+
22
+ // process bleeding
23
+ if (components.bleeding)
24
+ components.health -= components.bleeding
25
+
26
+ // process death
27
+ if (components.health <= 0)
28
+ world.delete(id)
29
+ }
30
+ }),
31
+
32
+ eureka.system("mana")
33
+ .select("mana").andMaybe("manaRegen")
34
+ .fn((entities, _world) => {
35
+ for (const {components} of entities)
36
+ if (components.manaRegen)
37
+ components.mana += components.manaRegen
38
+ }),
39
+ ])
40
+
41
+ return {world}
42
+ }
43
+