Compare commits
No commits in common. "478a0339d2e8d107571d5a6bc607ce256cb9a2e0" and "f37e67437e8061dddb05df6e0360d8d73d5067e1" have entirely different histories.
478a0339d2
...
f37e67437e
4 changed files with 47 additions and 144 deletions
91
README.md
91
README.md
|
@ -1,93 +1,2 @@
|
||||||
# GraphECS
|
# GraphECS
|
||||||
this is a prototype demonstrating representing underlying ecs data as a graph with a cypher-like query language
|
this is a prototype demonstrating representing underlying ecs data as a graph with a cypher-like query language
|
||||||
|
|
||||||
## Syntax
|
|
||||||
graphecs uses a modified subset of neo4j's [cypher](https://neo4j.com/docs/cypher-manual/current/introduction/) query language. neo4j is a graph database with a focus on relationships between data. it has many similar features to SQL-like languages but the ergonomics of relating data is significantly better.
|
|
||||||
|
|
||||||
### Querying entities
|
|
||||||
|
|
||||||
```cypher
|
|
||||||
MATCH (e)
|
|
||||||
RETURN e
|
|
||||||
```
|
|
||||||
|
|
||||||
this is a basic query to return every entity in current ecs world. `MATCH` clauses begin any expression that queries for data. `RETURN` defines what data will be returned by the query. an example of the output may look like this
|
|
||||||
|
|
||||||
```js
|
|
||||||
[
|
|
||||||
{ e: 0 },
|
|
||||||
{ e: 1 },
|
|
||||||
{ e: 2 },
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
```cypher
|
|
||||||
MATCH (e:Entity, :Player)
|
|
||||||
RETURN e
|
|
||||||
```
|
|
||||||
|
|
||||||
this query specifies two components to match for a single node. here, it explicitly identifies the `:Entity` component but this is optional -- binding a variable without any component label will be bound to the entity's id. additionally, it requires the node to also have the `Player` component. it doesn't bind that component to any variable, however. matching against multiple components in a single node acts like an AND operator -- in this case, `Entity AND Player`.
|
|
||||||
|
|
||||||
```cypher
|
|
||||||
MATCH (:Player)-[:Knows]->(n:NPC)
|
|
||||||
RETURN n
|
|
||||||
```
|
|
||||||
|
|
||||||
this query matches against a relationship: any `Player` which `Knows` an `NPC`. it will return a list of all NPCs which have this relationship with the player. in the prototype, edges are just entities with `from` and `to` properties
|
|
||||||
|
|
||||||
```js
|
|
||||||
const Item = defineComponent({ damage: Types.ui8 })
|
|
||||||
query('MATCH (item:Item) RETURN item.damage', engine)
|
|
||||||
```
|
|
||||||
|
|
||||||
in this example, we have an `Item` component with a single property: `damage`. it can be useful to only return a single property from a component. the result of this query may look like the following
|
|
||||||
|
|
||||||
```js
|
|
||||||
[
|
|
||||||
{ item: { damage: 10 } },
|
|
||||||
{ item: { damage: 15 } },
|
|
||||||
{ item: { damage: 20 } },
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
accessing nested data like this can get a bit unwieldy, but its possible to alias variables
|
|
||||||
|
|
||||||
```cypher
|
|
||||||
MATCH (item:Item)
|
|
||||||
RETURN item.damage AS damage
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
[
|
|
||||||
{ damage: 10 },
|
|
||||||
{ damage: 15 },
|
|
||||||
{ damage: 20 },
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Filtering queries
|
|
||||||
|
|
||||||
```cypher
|
|
||||||
MATCH (h:Health)
|
|
||||||
WHERE h.current < 10
|
|
||||||
RETURN h
|
|
||||||
```
|
|
||||||
|
|
||||||
it is also possible to filter results via the `WHERE` clause. the following operators are supported in graphecs: `> >= < <= = AND OR XOR ( )`
|
|
||||||
|
|
||||||
```cypher
|
|
||||||
MATCH (n)-[d:Damaged]->(e, h:Health)
|
|
||||||
WHERE d.damage > h.current AND h.current > 0
|
|
||||||
RETURN e
|
|
||||||
```
|
|
||||||
|
|
||||||
this would return every entity id that has a relationship that deals more damage than its remaining health and isn't already dead
|
|
||||||
|
|
||||||
### Selecting worlds
|
|
||||||
```cypher
|
|
||||||
USE ui
|
|
||||||
MATCH (button:Button)
|
|
||||||
WHERE button.type = 1
|
|
||||||
RETURN button
|
|
||||||
```
|
|
||||||
it's also possible to select different worlds with the `USE` clause
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { curry, is, last, mergeRight, mergeRightDeep, path } from '../fn.js'
|
import { curry, is, last, mergeRightDeep, path } from '../fn.js'
|
||||||
import { Stream } from '../stream.js'
|
import { Stream } from '../stream.js'
|
||||||
|
|
||||||
class Value {
|
class Value {
|
||||||
|
@ -99,7 +99,7 @@ export class Function {
|
||||||
const result = []
|
const result = []
|
||||||
let n = Math.min(this.length, stack.length)
|
let n = Math.min(this.length, stack.length)
|
||||||
|
|
||||||
while (n > 0) {
|
while(n > 0) {
|
||||||
result.push(stack.pop())
|
result.push(stack.pop())
|
||||||
n--
|
n--
|
||||||
}
|
}
|
||||||
|
@ -220,7 +220,8 @@ export class Filter {
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
passes(node) {
|
passes(nodes) {
|
||||||
|
return nodes.every(node => {
|
||||||
const values = this.values.map(v => v.from ? v.from(node.values) : v)
|
const values = this.values.map(v => v.from ? v.from(node.values) : v)
|
||||||
const stream = new Stream(values)
|
const stream = new Stream(values)
|
||||||
const stack = []
|
const stack = []
|
||||||
|
@ -235,6 +236,7 @@ export class Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
return stack[0]
|
return stack[0]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
12
src/query.js
12
src/query.js
|
@ -50,16 +50,14 @@ const queryRelationship = (query, world) => {
|
||||||
return node.relationship ? node.from : node
|
return node.relationship ? node.from : node
|
||||||
})
|
})
|
||||||
|
|
||||||
const [fc, tc] = [from, to].map(n => n.components.filter(notEntity).map(prop('type')))
|
const matchesFrom = matches(world, from.components.map(prop('type')), from.query(world))
|
||||||
const matchesFrom = matches(world, fc, from.query(world))
|
const matchesTo = matches(world, to.components.map(prop('type')))
|
||||||
const matchesTo = matches(world, tc)
|
|
||||||
|
|
||||||
const Edge = edge.components[0].type
|
const Edge = edge.components[0].type
|
||||||
const edges = edge.query(world)
|
const edges = edge.query(world)
|
||||||
const result = []
|
const result = []
|
||||||
for (let i = 0; i < edges.length; i++) {
|
for (let i = 0; i < edges.length; i++) {
|
||||||
const eid = edges[i]
|
const eid = edges[i]
|
||||||
// console.log(eid, Edge.from[eid], '->', Edge.to[eid], matchesFrom(Edge.from[eid]), matchesTo(Edge.to[eid]))
|
|
||||||
|
|
||||||
if (matchesFrom(Edge.from[eid]) && matchesTo(Edge.to[eid])) {
|
if (matchesFrom(Edge.from[eid]) && matchesTo(Edge.to[eid])) {
|
||||||
result.push(eid)
|
result.push(eid)
|
||||||
|
@ -144,7 +142,7 @@ const resolveValues = query => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return values.flat().reduce(mergeRightDeep)
|
return values.flat()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +151,7 @@ const filterValues = curry((filters, values) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const resolveReturns = curry((returnValues, values) => {
|
const resolveReturns = curry((returnValues, values) => {
|
||||||
return values.map(x => returnValues.from(x.values))
|
return values.map(x => x.map(y => returnValues.from(y.values)))
|
||||||
})
|
})
|
||||||
|
|
||||||
export const query = curry((input, { world, component }) => {
|
export const query = curry((input, { world, component }) => {
|
||||||
|
@ -165,7 +163,7 @@ export const query = curry((input, { world, component }) => {
|
||||||
const values = resolveValues(results)
|
const values = resolveValues(results)
|
||||||
const filtered = filterValues(filters, values)
|
const filtered = filterValues(filters, values)
|
||||||
const returns = resolveReturns(returnValues, filtered)
|
const returns = resolveReturns(returnValues, filtered)
|
||||||
return returns
|
return map(reduce(mergeRightDeep, {}), returns)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -115,37 +115,31 @@ describe('query', () => {
|
||||||
const [edge] = relate(world, player, Damaged, enemy)
|
const [edge] = relate(world, player, Damaged, enemy)
|
||||||
update(Damaged, { damage: 10 }, edge)
|
update(Damaged, { damage: 10 }, edge)
|
||||||
|
|
||||||
// assert.deepEqual(
|
|
||||||
// query('MATCH (e, h:Health) WHERE h.current < 30 RETURN e, h.max', engine).unwrap(),
|
|
||||||
// [{ e: 12, h: { max: 50 } }]
|
|
||||||
// )
|
|
||||||
|
|
||||||
// assert.deepEqual(
|
|
||||||
// query('MATCH (e, h:Health) WHERE e = 13 AND h.max = 50 OR h.current < 25 RETURN e, h.max', engine).unwrap(),
|
|
||||||
// [{ e: 13, h: { max: 50 } }]
|
|
||||||
// )
|
|
||||||
|
|
||||||
// assert.deepEqual(
|
|
||||||
// query('MATCH (e, h:Health) WHERE h.max = 50 OR h.current > 45 RETURN e, h.max AS maxHealth', engine).unwrap(),
|
|
||||||
// [
|
|
||||||
// { e: 12, maxHealth: 50 },
|
|
||||||
// { e: 13, maxHealth: 50 }
|
|
||||||
// ]
|
|
||||||
// )
|
|
||||||
// assert.deepEqual(
|
|
||||||
// query('MATCH (e, :Player, h:Health) WHERE (h.max = 50 OR h.current > 45) AND e = 12 RETURN e, h.max AS maxHealth', engine).unwrap(),
|
|
||||||
// [{ e: 12, maxHealth: 50 }]
|
|
||||||
// )
|
|
||||||
|
|
||||||
// assert.deepEqual(
|
|
||||||
// query('MATCH (:Player)-[d:Damaged]->(h:Health) RETURN h.current AS health, d.damage AS damage', engine).unwrap(),
|
|
||||||
// [{ damage: 10, health: 35 }]
|
|
||||||
// )
|
|
||||||
|
|
||||||
update(Damaged, { damage: 50 }, edge)
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
query('MATCH (n)-[d:Damaged]->(e, h:Health) WHERE d.damage > h.current AND h.current > 0 RETURN e', engine).unwrap(),
|
query('MATCH (e, h:Health) WHERE h.current < 30 RETURN e, h.max', engine).unwrap(),
|
||||||
[{ e: 13 }]
|
[{ e: 12, h: { max: 50 } }]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
query('MATCH (e, h:Health) WHERE e = 13 AND h.max = 50 OR h.current < 25 RETURN e, h.max', engine).unwrap(),
|
||||||
|
[{ e: 13, h: { max: 50 } }]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
query('MATCH (e, h:Health) WHERE h.max = 50 OR h.current > 45 RETURN e, h.max AS maxHealth', engine).unwrap(),
|
||||||
|
[
|
||||||
|
{ e: 12, maxHealth: 50 },
|
||||||
|
{ e: 13, maxHealth: 50 }
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert.deepEqual(
|
||||||
|
query('MATCH (e, :Player, h:Health) WHERE (h.max = 50 OR h.current > 45) AND e = 12 RETURN e, h.max AS maxHealth', engine).unwrap(),
|
||||||
|
[{ e: 12, maxHealth: 50 }]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
query('MATCH (:Player)-[d:Damaged]->(h:Health) RETURN h.current AS health, d.damage AS damage', engine).unwrap(),
|
||||||
|
[{ damage: 10, health: 35 }]
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue