@benev/archimedes 0.1.0-3 → 0.1.0-5

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 (66) hide show
  1. package/README.md +80 -10
  2. package/package.json +4 -4
  3. package/s/ecs/index.ts +6 -5
  4. package/s/ecs/parts/apply-delta.ts +37 -0
  5. package/s/ecs/parts/change.ts +39 -0
  6. package/s/ecs/parts/entities.ts +98 -0
  7. package/s/ecs/parts/execute-systems.ts +21 -0
  8. package/s/ecs/parts/lifecycle.ts +29 -0
  9. package/s/ecs/parts/types.ts +28 -0
  10. package/s/ecs/test/setup-example.ts +50 -0
  11. package/s/ecs/test.ts +114 -61
  12. package/x/ecs/index.d.ts +6 -4
  13. package/x/ecs/index.js +6 -4
  14. package/x/ecs/index.js.map +1 -1
  15. package/x/ecs/parts/apply-delta.d.ts +3 -0
  16. package/x/ecs/parts/apply-delta.js +34 -0
  17. package/x/ecs/parts/apply-delta.js.map +1 -0
  18. package/x/ecs/parts/change.d.ts +15 -0
  19. package/x/ecs/parts/change.js +35 -0
  20. package/x/ecs/parts/change.js.map +1 -0
  21. package/x/ecs/parts/entities.d.ts +11 -0
  22. package/x/ecs/parts/entities.js +70 -0
  23. package/x/ecs/parts/entities.js.map +1 -0
  24. package/x/ecs/parts/execute-systems.d.ts +3 -0
  25. package/x/ecs/parts/execute-systems.js +14 -0
  26. package/x/ecs/parts/execute-systems.js.map +1 -0
  27. package/x/ecs/parts/lifecycle.d.ts +2 -0
  28. package/x/ecs/parts/lifecycle.js +20 -0
  29. package/x/ecs/parts/lifecycle.js.map +1 -0
  30. package/x/ecs/parts/types.d.ts +23 -0
  31. package/x/ecs/parts/types.js +9 -0
  32. package/x/ecs/parts/types.js.map +1 -0
  33. package/x/ecs/test/setup-example.d.ts +22 -0
  34. package/x/ecs/test/setup-example.js +33 -0
  35. package/x/ecs/test/setup-example.js.map +1 -0
  36. package/x/ecs/test.d.ts +6 -2
  37. package/x/ecs/test.js +111 -62
  38. package/x/ecs/test.js.map +1 -1
  39. package/s/ecs/parts/changers.ts +0 -20
  40. package/s/ecs/parts/world.ts +0 -65
  41. package/s/ecs/test/setup-example-world.ts +0 -42
  42. package/s/ecs/types.ts +0 -24
  43. package/s/ecs/utils/apply-change.ts +0 -32
  44. package/s/ecs/utils/is-match.ts +0 -7
  45. package/s/ecs/utils/optimizer.ts +0 -38
  46. package/x/ecs/parts/changers.d.ts +0 -5
  47. package/x/ecs/parts/changers.js +0 -15
  48. package/x/ecs/parts/changers.js.map +0 -1
  49. package/x/ecs/parts/world.d.ts +0 -11
  50. package/x/ecs/parts/world.js +0 -58
  51. package/x/ecs/parts/world.js.map +0 -1
  52. package/x/ecs/test/setup-example-world.d.ts +0 -10
  53. package/x/ecs/test/setup-example-world.js +0 -31
  54. package/x/ecs/test/setup-example-world.js.map +0 -1
  55. package/x/ecs/types.d.ts +0 -21
  56. package/x/ecs/types.js +0 -6
  57. package/x/ecs/types.js.map +0 -1
  58. package/x/ecs/utils/apply-change.d.ts +0 -2
  59. package/x/ecs/utils/apply-change.js +0 -30
  60. package/x/ecs/utils/apply-change.js.map +0 -1
  61. package/x/ecs/utils/is-match.d.ts +0 -2
  62. package/x/ecs/utils/is-match.js +0 -4
  63. package/x/ecs/utils/is-match.js.map +0 -1
  64. package/x/ecs/utils/optimizer.d.ts +0 -9
  65. package/x/ecs/utils/optimizer.js +0 -37
  66. package/x/ecs/utils/optimizer.js.map +0 -1
package/README.md CHANGED
@@ -1,19 +1,89 @@
1
1
 
2
- ![](https://i.imgur.com/JNCvW1J.png)
2
+ ![](https://i.imgur.com/DYcrs49.png)
3
3
 
4
- # 🏛️ archimedes netlogic engine
4
+ # 🌀 archimedes, netlogic for multiplayer web games
5
5
 
6
- > [***"do not disturb my circles!"***](https://en.wikipedia.org/wiki/noli_turbare_circulos_meos!)
6
+ > [***"do not disturb my circles!"***](https://en.wikipedia.org/wiki/noli_turbare_circulos_meos!)
7
7
  >     — *archimedes, c. 212 bc*
8
8
 
9
- ## rollforward netcode for web games
9
+ ```bash
10
+ npm install @benev/archimedes
11
+ ```
10
12
 
11
- 🔮 **automatic networking**
12
- you just code your game naively as though it's singleplayer, archimedes handles the multiplayer.
13
+ - 🧩 [**#ecs,**](#ecs) entities, components, systems.
14
+ - 🔮 [**#sim,**](#sim) code like it's single-player, archimedes makes it multiplayer.
15
+ - 🌎 [**#net,**](#net) whole-world rollforward, everything is clientside predicted for insta-feels.
13
16
 
14
- 🌎 **whole-world rollforward**
15
- clientside prediction applies to the whole simulation. all effects of player inputs feel instant.
16
17
 
17
- 🧩 **ecs architecture**
18
- program your simulation with entities, components, and systems.
18
+
19
+ <br/><a id="ecs"></a>
20
+
21
+ ## 🧩 ecs — entities, components, systems
22
+
23
+ ```ts
24
+ import {Entities, asSystems, makeId, executeSystems} from "@benev/archimedes"
25
+ ```
26
+
27
+ 1. ***define components.*** json-friendly data that entities could have.
28
+ ```ts
29
+ export type MyComponents = {
30
+ health: number
31
+ bleed: number
32
+ }
33
+ ```
34
+ 1. ***create entities map.*** indexed for speedy-fast lookups.
35
+ ```ts
36
+ export const entities = new Entities<MyComponents>()
37
+ ```
38
+ 1. ***define systems.*** select entities by components. formal changes.
39
+ ```ts
40
+ const systems = asSystems<MyComponents>(
41
+ function bleeding(entities, change) {
42
+ for (const [id, components] of entities.select("health", "bleed")) {
43
+ if (components.bleed > 0) {
44
+ const health = components.health - components.bleed
45
+ change.merge(id, {health})
46
+ }
47
+ }
48
+ },
49
+
50
+ function death(entities, change) {
51
+ for (const [id, components] of entities.select("health")) {
52
+ if (components.health <= 0)
53
+ change.delete(id)
54
+ }
55
+ },
56
+ )
57
+ ```
58
+ 1. ***manually insert your first entity.***
59
+ ```ts
60
+ const wizardId = makeId()
61
+ entities.set(wizardId, {health: 100, bleed: 2})
62
+
63
+ console.log(entities.get(wizardId)?.health)
64
+ // 100
65
+ ```
66
+ 1. ***execute systems to simulate each tick.***
67
+ ```ts
68
+ executeSystems(entities, systems)
69
+
70
+ console.log(entities.get(wizardId)?.health)
71
+ // 98
72
+ ```
73
+
74
+
75
+
76
+ <br/><a id="sim"></a>
77
+
78
+ ## 🔮 sim — networkable simulation architecture
79
+
80
+ *coming soon*
81
+
82
+
83
+
84
+ <br/><a id="net"></a>
85
+
86
+ ## 🌎 net — connect and run multiplayer games
87
+
88
+ *coming soon*
19
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benev/archimedes",
3
- "version": "0.1.0-3",
3
+ "version": "0.1.0-5",
4
4
  "description": "game ecs with auto networking",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,20 +10,20 @@
10
10
  "s"
11
11
  ],
12
12
  "scripts": {
13
- "build": "octo-s 'rm -rf x' 'mkdir x' 'tsc'",
13
+ "build": "rm -rf x && mkdir x && tsc",
14
14
  "dev": "octo 'node --watch x/test.js' 'tsc -w'",
15
15
  "test": "node x/test.js",
16
16
  "count": "find s -path '*/_archive' -prune -o -name '*.ts' -exec wc -l {} +"
17
17
  },
18
18
  "dependencies": {
19
19
  "@e280/renraku": "^0.5.7",
20
- "@e280/stz": "^0.2.27",
20
+ "@e280/stz": "^0.2.29",
21
21
  "@msgpack/msgpack": "^3.1.3",
22
22
  "sparrow-rtc": "^0.2.15"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@e280/octo": "^0.1.0",
26
- "@e280/science": "^0.1.9",
26
+ "@e280/science": "^0.1.10",
27
27
  "npm-run-all": "^4.1.5",
28
28
  "typescript": "^6.0.2"
29
29
  },
package/s/ecs/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
 
2
- export * from "./parts/changers.js"
3
- export * as change from "./parts/changers.js"
2
+ export * from "./parts/apply-delta.js"
3
+ export * from "./parts/change.js"
4
+ export * from "./parts/entities.js"
5
+ export * from "./parts/execute-systems.js"
6
+ export * from "./parts/lifecycle.js"
4
7
  export * from "./parts/make-id.js"
5
- export * from "./parts/world.js"
6
-
7
- export * from "./types.js"
8
+ export * from "./parts/types.js"
8
9
 
@@ -0,0 +1,37 @@
1
+
2
+ import {Entities} from "./entities.js"
3
+ import {DeltaSet, Delta, Components, DeltaKind, DeltaMerge, DeltaDrop} from "./types.js"
4
+
5
+ export function applyDelta<C extends Components>(entities: Entities<C>, delta: Delta<C>) {
6
+ switch (delta[0]) {
7
+ case DeltaKind.Set: return applySet<C>(entities, <DeltaSet<C>>delta)
8
+ case DeltaKind.Merge: return applyMerge<C>(entities, <DeltaMerge<C>>delta)
9
+ case DeltaKind.Drop: return applyDrop<C>(entities, <DeltaDrop<C>>delta)
10
+ default: throw new Error(`unknown delta kind "${delta[0]}"`)
11
+ }
12
+ }
13
+
14
+ function applySet<C extends Components>(entities: Entities<C>, [, id, components]: DeltaSet<C>) {
15
+ if (components) entities.set(id, components as Partial<C>)
16
+ else entities.delete(id)
17
+ return id
18
+ }
19
+
20
+ function applyMerge<C extends Components>(entities: Entities<C>, [, id, patch]: DeltaMerge<C>) {
21
+ const components = entities.get(id)
22
+ if (!components)
23
+ return id
24
+ Object.assign(components, patch)
25
+ entities.set(id, components)
26
+ return id
27
+ }
28
+
29
+ function applyDrop<C extends Components>(entities: Entities<C>, [, id, keys]: DeltaDrop<C>) {
30
+ const components = entities.get(id)
31
+ if (!components)
32
+ return id
33
+ for (const key of keys) delete components[key]
34
+ entities.set(id, components)
35
+ return id
36
+ }
37
+
@@ -0,0 +1,39 @@
1
+
2
+ import {makeId} from "./make-id.js"
3
+ import {Components, Id, DeltaKind, Delta} from "./types.js"
4
+
5
+ export class Change<C extends Components> {
6
+ constructor(private commit: (delta: Delta<C>) => void) {}
7
+
8
+ /** create a new entity with the given components */
9
+ create(components: Partial<C>) {
10
+ const id = makeId()
11
+ this.commit([DeltaKind.Set, id, components])
12
+ return id
13
+ }
14
+
15
+ /** overwrite a whole entity to the given components */
16
+ set(id: Id, components: Partial<C>) {
17
+ this.commit([DeltaKind.Set, id, components])
18
+ return id
19
+ }
20
+
21
+ /** remove the entity */
22
+ delete(id: Id) {
23
+ this.commit([DeltaKind.Set, id])
24
+ return id
25
+ }
26
+
27
+ /** update or add the given components onto the entity */
28
+ merge(id: Id, components: Partial<C>) {
29
+ this.commit([DeltaKind.Merge, id, components])
30
+ return id
31
+ }
32
+
33
+ /** delete specific components off the entity */
34
+ drop(id: Id, ...keys: (keyof C)[]) {
35
+ this.commit([DeltaKind.Drop, id, keys])
36
+ return id
37
+ }
38
+ }
39
+
@@ -0,0 +1,98 @@
1
+
2
+ import {GMap} from "@e280/stz"
3
+ import {Components, Id, Select} from "./types.js"
4
+
5
+ export class Entities<C extends Components> extends GMap<Id, Partial<C>> {
6
+ #index = new GMap<Set<keyof C>, GMap<Id, Partial<C>>>()
7
+
8
+ set(id: Id, components: Partial<C>) {
9
+ super.set(id, components)
10
+ for (const [set, entities] of this.#index) {
11
+ if (componentsSatisfySet(components, set))
12
+ entities.set(id, components)
13
+ else
14
+ entities.delete(id)
15
+ }
16
+ return this
17
+ }
18
+
19
+ delete(id: Id) {
20
+ const didDelete = super.delete(id)
21
+ if (!didDelete)
22
+ return false
23
+
24
+ for (const entities of this.#index.values())
25
+ entities.delete(id)
26
+
27
+ return true
28
+ }
29
+
30
+ clear() {
31
+ super.clear()
32
+ for (const entities of this.#index.values())
33
+ entities.clear()
34
+ }
35
+
36
+ *select<K extends keyof C>(...componentKeys: K[]): Iterable<[Id, Select<C, K>]> {
37
+ const cached = this.#getCache(componentKeys)
38
+ if (cached)
39
+ yield* cached
40
+ else
41
+ yield* this.#makeCache(componentKeys)
42
+ }
43
+
44
+ readonly() {
45
+ return this as EntitiesReadonly<C>
46
+ }
47
+
48
+ #getCache<K extends keyof C>(componentKeys: K[]) {
49
+ for (const set of this.#index.keys()) {
50
+ if (setHasSameValuesAsArray(set, componentKeys))
51
+ return this.#index.require(set) as GMap<Id, Select<C, K>>
52
+ }
53
+ }
54
+
55
+ *#makeCache<K extends keyof C>(componentKeys: K[]) {
56
+ const set = new Set(componentKeys)
57
+ const entities = new GMap<Id, Partial<C>>()
58
+ this.#index.set(set, entities)
59
+
60
+ for (const entity of this) {
61
+ const [id, components] = entity
62
+
63
+ if (componentsSatisfyKeys(components, componentKeys)) {
64
+ entities.set(id, components)
65
+ yield entity as [Id, Select<C, K>]
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ export type EntitiesReadonly<C extends Components> = Pick<Entities<Readonly<C>>, (
72
+ | "has"
73
+ | "get"
74
+ | "require"
75
+ | "keys"
76
+ | "values"
77
+ | "entries"
78
+ | "select"
79
+ | typeof Symbol.iterator
80
+ )>
81
+
82
+ function componentsSatisfySet(components: Components, set: Set<PropertyKey>) {
83
+ for (const key of set)
84
+ if (!(key in components))
85
+ return false
86
+ return true
87
+ }
88
+
89
+ function setHasSameValuesAsArray(set: Set<unknown>, keys: unknown[]) {
90
+ if (set.size !== keys.length)
91
+ return false
92
+ return keys.every(key => set.has(key))
93
+ }
94
+
95
+ function componentsSatisfyKeys(components: object, keys: PropertyKey[]) {
96
+ return keys.every(key => key in components)
97
+ }
98
+
@@ -0,0 +1,21 @@
1
+
2
+ import {Change} from "./change.js"
3
+ import {Entities} from "./entities.js"
4
+ import {applyDelta} from "./apply-delta.js"
5
+ import {Delta, Components, System} from "./types.js"
6
+
7
+ export function executeSystems<C extends Components>(entities: Entities<C>, systems: System<C>[]) {
8
+ const entitiesReadonly = entities.readonly()
9
+ const deltas: Delta<C>[] = []
10
+
11
+ const change = new Change<C>(delta => {
12
+ applyDelta(entities, delta)
13
+ deltas.push(delta)
14
+ })
15
+
16
+ for (const system of systems)
17
+ system(entitiesReadonly, change)
18
+
19
+ return deltas
20
+ }
21
+
@@ -0,0 +1,29 @@
1
+
2
+ import {GMap} from "@e280/stz"
3
+ import {Components, Id, LifecycleCallbacks, LifecycleEnter, System} from "./types.js"
4
+
5
+ export function lifecycle<C extends Components, K extends keyof C>(
6
+ componentKeys: K[],
7
+ enter: LifecycleEnter<C, K>
8
+ ): System<C> {
9
+
10
+ const alive = new GMap<Id, LifecycleCallbacks<C, K>>()
11
+
12
+ return (entities, commit) => {
13
+
14
+ // add fresh entities
15
+ for (const [id, components] of entities.select(...componentKeys)) {
16
+ const callbacks = alive.guarantee(id, () => enter(id, components, commit))
17
+ callbacks.tick(id, components)
18
+ }
19
+
20
+ // delete stale entities
21
+ const currentIds = new Set([...entities.select(...componentKeys)].map(([id]) => id))
22
+ for (const [id, callbacks] of alive) {
23
+ if (currentIds.has(id)) continue
24
+ alive.delete(id)
25
+ callbacks.exit(id)
26
+ }
27
+ }
28
+ }
29
+
@@ -0,0 +1,28 @@
1
+
2
+ import {Change} from "./change.js"
3
+ import {EntitiesReadonly} from "./entities.js"
4
+
5
+ export type Id = string
6
+ export type Components = Record<string, unknown>
7
+ export type AsComponents<C extends Components> = C
8
+ export type Select<C extends Components, K extends keyof C> = Pick<C, K> & Partial<C>
9
+
10
+ export enum DeltaKind {Set, Merge, Drop}
11
+ export type DeltaSet<C extends Components> = [kind: DeltaKind.Set, id: Id, components?: Partial<C>]
12
+ export type DeltaMerge<C extends Components> = [kind: DeltaKind.Merge, id: Id, patch: Partial<C>]
13
+ export type DeltaDrop<C extends Components> = [kind: DeltaKind.Drop, id: Id, keys: (keyof C)[]]
14
+ export type Delta<C extends Components> = DeltaSet<C> | DeltaMerge<C> | DeltaDrop<C>
15
+
16
+ export type System<C extends Components> = (entities: EntitiesReadonly<C>, change: Change<C>) => void
17
+ export const asSystem = <C extends Components>(system: System<C>) => system
18
+ export const asSystems = <C extends Components>(...systems: System<C>[]) => systems
19
+
20
+ export type LifecycleCallbacks<C extends Components, K extends keyof C> = {
21
+ tick: (id: Id, components: Select<C, K>) => void
22
+ exit: (id: Id) => void
23
+ }
24
+
25
+ export type LifecycleEnter<C extends Components, K extends keyof C> = (
26
+ (id: Id, components: Select<C, K>, change: Change<C>) => LifecycleCallbacks<C, K>
27
+ )
28
+
@@ -0,0 +1,50 @@
1
+
2
+ import {Change} from "../parts/change.js"
3
+ import {asSystems} from "../parts/types.js"
4
+ import {Entities} from "../parts/entities.js"
5
+ import {applyDelta} from "../parts/apply-delta.js"
6
+
7
+ export function setupExample() {
8
+ type MyComponents = {
9
+ health: number
10
+ bleed: number
11
+ mana: number
12
+ manaRegen: number
13
+ }
14
+
15
+ const systems = asSystems<MyComponents>(
16
+ function manaRegen(entities, change) {
17
+ for (const [id, components] of entities.select("mana", "manaRegen")) {
18
+ if (components.manaRegen !== 0) {
19
+ const mana = components.mana + components.manaRegen
20
+ change.merge(id, {mana})
21
+ }
22
+ }
23
+ },
24
+
25
+ function bleeding(entities, change) {
26
+ for (const [id, components] of entities.select("health", "bleed")) {
27
+ if (components.bleed >= 0) {
28
+ const health = components.health - components.bleed
29
+ const bleed = components.bleed - 1
30
+ change.merge(id, {health, bleed})
31
+ }
32
+ if (components.bleed <= 0)
33
+ change.drop(id, "bleed")
34
+ }
35
+ },
36
+
37
+ function death(entities, change) {
38
+ for (const [id, components] of entities.select("health")) {
39
+ if (components.health <= 0)
40
+ change.delete(id)
41
+ }
42
+ },
43
+ )
44
+
45
+ const entities = new Entities<MyComponents>()
46
+ const change = new Change<MyComponents>(delta => applyDelta(entities, delta))
47
+
48
+ return {systems, entities, change}
49
+ }
50
+