adding multiple component matching

This commit is contained in:
Rowan 2024-11-25 02:34:24 -06:00
parent 3f1686d084
commit 6224cce8b7
8 changed files with 130 additions and 87 deletions

View file

@ -56,7 +56,7 @@ export const some = curry((predicate, v) => v.some(predicate))
export const all = curry((predicates, v) => every(applyTo(v), predicates))
export const any = curry((predicates, v) => some(applyTo(v), predicates))
export const isOk = value => !is(Result, value) || value.isOk()
export const eq = curry((a, b) => a === b)
// classes
export const construct = Type => args => new Type(...args)
@ -121,7 +121,10 @@ export const mergeRightDeep = curry((a, b) =>
export const mergeLeftDeep = flip(mergeRightDeep)
export const prop = curry((key, obj) => obj && key && obj[key])
export const propEq = curry((key, val, obj) => pipe(prop(key), eq(val))(obj))
export const hasProp = curry((key, obj) => isNil(prop(key, obj)))
export const path = curry((key, obj) => reduce(flip(prop), obj, makePath(key)))
export const pathEq = curry((key, val, obj) => pipe(path(key), eq(val))(obj))
export const mergeLeft = curry((a, b) => ({ ...b, ...a }))
export const mergeRight = flip(mergeLeft)
export const keys = Object.keys

View file

@ -127,6 +127,8 @@ export const many = curry((parser, state) => {
return result
})
export const many1 = parser => seq(parser, many(parser))
export const skip = curry((parser, state) => {
const [parsed] = state
const result = parser(state)

View file

@ -25,6 +25,7 @@ export const Symbol = Object.freeze({
Comma: char(','),
Hyphen: char('-'),
Newline: char('\n'),
Tab: char('\t'),
Period: char('.'),
Plus: char('+'),
Space: char(' '),
@ -36,7 +37,7 @@ export const collect = parser => map(join(''), parser)
export const quoted = collect(seq(skip(Symbol.Quote), until(Symbol.Quote), skip(Symbol.Quote)))
export const number = seq(digit, many(digit))
export const signed = seq(maybe(any(Symbol.Plus, Symbol.Hyphen)), number)
export const ws = skip(many(any(Symbol.Newline, Symbol.Space)))
export const ws = skip(many(any(Symbol.Newline, Symbol.Space, Symbol.Tab)))
export const identifier = map(([x]) => new Identifier(x), collect(seq(alpha, many(alphanumeric))))
const float = map(([n]) => parseFloat(n, 10), collect(seq(signed, Symbol.Period, number)))

View file

@ -1,6 +1,6 @@
import { identifier, literal, Symbol, ws } from './common.js'
import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge, Relationship } from './types.js'
import { construct, curry, filter, is } from '../fn.js'
import { Node, Edge, KeyValuePair, Label, Name, Direction, DirectedEdge, Relationship, Component } from './types.js'
import { construct, curry, is } from '../fn.js'
import { many, maybe, map, seq, skip, between, noCaseString, separated, list, any } from '../parser.js'
const { Bracket, Colon, Comma, Hyphen } = Symbol
@ -24,25 +24,29 @@ const kvp = map(
construct(KeyValuePair),
separated(name, trim(Colon), literal)
)
export const kvps = list(trim(Comma), kvp)
export const properties = bracketed(kvps, Bracket.Curly)
const id = seq(maybe(name), label)
const id = seq(maybe(name), maybe(label))
const makeObj = Ctr => params => {
const makeObj = Type => params => {
const [name, label] = is(Name, params[0]) ? [params[0], params[1]] : [undefined, params[0]]
const properties = !name ? params.slice(1) : params.slice(2)
return new Ctr(name, label, properties)
return new Type(name, label, properties)
}
const component = map(makeObj(Component), seq(id, maybe(properties)))
const components = list(trim(Comma), component)
export const node = map(
makeObj(Node),
bracketed(seq(id, maybe(properties)), Bracket.Round)
x => new Node(x),
bracketed(components, Bracket.Round)
)
export const edge = map(
makeObj(Edge),
bracketed(seq(id, maybe(properties)), Bracket.Square)
x => new Edge(x),
bracketed(components, Bracket.Square)
)
const arrowRight = map(() => Direction.Right, seq(Hyphen, Bracket.Angle.Right))

View file

@ -75,11 +75,14 @@ export class ObjectPath extends Identifier {
}
equals(value) {
if (is(ObjectPath, value)) {
return value != null
&& is(ObjectPath, value)
&& this.path.length === value.path.length
&& super.equals(value)
&& this.path.every((x, i) => x === value.path[i])
} else {
return super.equals(value)
}
}
}
@ -92,31 +95,38 @@ class GraphObject {
}
}
export class Node extends GraphObject {
export class Component extends GraphObject {
constructor(name, label, properties) {
super(name, label, properties)
}
}
export class Node {
constructor(components) {
this.components = components
}
}
export const Direction = Object.freeze({
Left: 0,
Right: 1
})
export class Edge extends GraphObject {
constructor(name, label, properties) {
super(name, label, properties)
export class Edge {
constructor(components) {
this.components = components
}
}
export class DirectedEdge extends Edge {
constructor(name, label, direction, properties) {
super(name, label, properties)
constructor(components, direction) {
super(components)
this.direction = direction
}
static fromEdge(edge, direction) {
return new DirectedEdge(edge.name, edge.label, direction, edge.properties)
return new DirectedEdge(edge.components, direction)
}
}
@ -139,7 +149,7 @@ export class ReturnValues {
}
includes(value) {
return this.find(value.equals.bind(value))
return this.find(x => x.equals(value))
}
}

View file

@ -1,9 +1,19 @@
import { defineQuery, hasComponent } from 'bitecs'
import { query as q } from './query-parser/index.js'
import { parseAll } from './parser.js'
import { curry, of, is, map, when, assoc, isNil, path, pipe, always, assocPath, prop } from './fn.js'
import { curry, of, is, map, path, pipe, always, assocPath, prop, identity } from './fn.js'
import { Relationship } from './query-parser/types.js'
const nodeComponent = component => node => {
const label = node.label?.value
const name = node.name?.value
const type = label && component[node.label.value] || identity
return { name, label, type }
}
const notEntity = ({ label }) => label != null && label !== 'Entity'
const prepareQuery = (query, component) => {
const { match, returnValues } = query
const relationship = is(Relationship, match)
@ -14,18 +24,20 @@ const prepareQuery = (query, component) => {
return { from, to, edge, relationship, schema: query }
} else {
const label = match.label.value
const name = match.name.value
const type = component[match.label.value]
return { name, label, type, relationship, query: defineQuery([type]), schema: query }
const components = match.components.map(nodeComponent(component))
const types = components.filter(notEntity).map(prop('type'))
return { components, relationship, query: defineQuery(types), schema: query }
}
}
const matches = (world, type, query, set = new Set()) => entity => {
const hasComponents = (world, types, entity) => (
types.every(type => hasComponent(world, type, entity))
)
const matches = (world, types, set = new Set()) => entity => {
if (set.has(entity)) {
return true
} else if (hasComponent(world, type, entity) && query.includes(entity)) {
} else if (hasComponents(world, types, entity)) {
set.add(entity)
return true
} else {
@ -35,20 +47,27 @@ const matches = (world, type, query, set = new Set()) => entity => {
const queryRelationship = (query, world) => {
const [from, edge, to] = ['from', 'edge', 'to'].map(name => {
let node = query[name]
node = node.relationship ? node.from : node
return { ...node, results: node.query(world) }
const node = query[name]
return node.relationship ? node.from : node
})
const matchesFrom = matches(world, from.type, from.results)
const matchesTo = matches(world, to.type, to.results)
const Edge = edge.type
const matchesFrom = matches(world, from.components.map(prop('type')))
const matchesTo = matches(world, to.components.map(prop('type')))
const Edge = edge.components[0].type
// FIXME: filter these to make sure the from components match the previous edge's to components
const edges = edge.query(world)
console.log('59', edges)
const result = []
for (let i = 0; i < edge.results.length; i++) {
const eid = edge.results[i]
for (let i = 0; i < edges.length; i++) {
const eid = edges[i]
if (matchesFrom(Edge.from[eid])) {
console.log('65', eid, from.components.map(({ name, label }) => ({ name, label })))
if (matchesTo(Edge.to[eid])) {
console.log('67', eid, to.components.map(({ name, label }) => ({ name, label })))
}
}
if (matchesFrom(Edge.from[eid]) && matchesTo(Edge.to[eid])) {
result.push(eid)
}
@ -65,19 +84,22 @@ const executeQuery = curry((query, world) => {
}
const edges = queryRelationship(query, world)
console.log('80', query.from.components, edges)
if (query.to.relationship) {
return edges.reduce((acc, eid) => {
const next = assocPath(
'from.query',
always(of(query.edge.type.to[eid])),
always(of(query.edge.components[0].type.to[eid])),
query.to
)
return executeQuery(next, world)
const a = executeQuery(next, world)
.map(child => [eid, ...child])
.map(([l, r]) => [{ entity: l, query }, r])
.concat(acc)
//.concat(acc)
console.log(a)
return a.concat(acc)
}, [])
} else {
return assembleQuery(query, edges)
@ -85,14 +107,16 @@ const executeQuery = curry((query, world) => {
})
const isTag = type => (
Object.getOwnPropertySymbols(type).find(
type && Object.getOwnPropertySymbols(type).find(
(s) => s.description === 'tagStore'
)
) || false
)
const getValue = (type, entity) => {
if (isTag(type)) {
return {}
} else if (typeof type === 'function') {
return type(entity)
} else {
return Object.fromEntries(
Object.entries(type).map(([k, v]) => [k, v[entity]])
@ -101,16 +125,16 @@ const getValue = (type, entity) => {
}
const resolveNode = curry((node, obj) => {
const name = path('query.schema.match.name', node)
const returns = path('query.schema.returnValues', node)
const { entity, query: { type } } = node
const value = getValue(type, entity)
return returns.includes(name) ? { ...obj, [name.value]: value } : obj
const { entity, query: { components } } = node
const values = components.filter(({ name }) => returns.includes(name))
.map(({ name, type }) => ({ [name]: getValue(type, entity) }))
return Object.assign(obj, ...values)
})
const resolveRelationship = edges => {
const { entity, query } = edges
const Edge = query.edge.type
const Edge = query.edge.components[0].type
const to = query.to?.from ?? query.to
return pipe(
@ -138,8 +162,11 @@ export const query = curry((input, { world, component }) => {
return parseAll(q, input).map(({ use, ...rest }) => {
const useWorld = world[use?.value ?? 'default']
const preparedQuery = prepareQuery(rest, component)
//console.log('prepared', preparedQuery)
const results = executeQuery(preparedQuery, useWorld)
//console.log('results', results)
const returns = resolveReturns(results)
//console.log('returns', returns)
return returns
})
})

View file

@ -1,10 +1,7 @@
export class Result {
#value
#error
constructor(value, error) {
this.#value = value
this.#error = error
this.value = value
this.error = error
}
static Ok(value) {
@ -16,7 +13,7 @@ export class Result {
}
isOk() {
return this.#error === null
return this.error === null
}
isErr() {
@ -25,7 +22,7 @@ export class Result {
map(fn) {
if (this.isOk()) {
return Result.Ok(fn(this.#value))
return Result.Ok(fn(this.value))
} else {
return this
}
@ -33,7 +30,7 @@ export class Result {
flatMap(fn) {
if (this.isOk()) {
return fn(this.#value)
return fn(this.value)
} else {
return this
}
@ -41,14 +38,14 @@ export class Result {
unwrap() {
if (this.isOk()) {
return this.#value
return this.value
} else {
throw new ResultError(`failed to unwrap result: ${this.#error.message}`, this)
throw new ResultError(`failed to unwrap result: ${this.error.message}`, this)
}
}
unwrapOr(value) {
return this.isOk() ? this.#value : value
return this.isOk() ? this.value : value
}
}

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, it } from 'node:test'
import { before, describe, it } from 'node:test'
import { addComponent, addEntity, createWorld, defineComponent, Types } from 'bitecs'
import assert from './assert.js'
import { query } from '../src/query.js'
@ -23,7 +23,7 @@ const relate = (world, a, type, ...b) => {
}
describe('query', () => {
beforeEach(() => {
before(() => {
const world = { default: createWorld() }
const component = {
Entity: identity,
@ -53,18 +53,18 @@ describe('query', () => {
relate(world, b, Knows, a) // 9
relate(world, c, Knows, player) // 10
assert.deepEqual(
query('MATCH (player:Player) RETURN player', engine).unwrap(),
[{ player: {} }]
)
//assert.deepEqual(
// query('MATCH (player:Player) RETURN player', engine).unwrap(),
// [{ player: {} }]
//)
assert.deepEqual(
query('MATCH (player:Player)-[e1:Knows]->(a:NPC) RETURN player, e1, a', engine).unwrap(),
[
{ player: {}, e1: { from: 0, to: 1 }, a: {} },
{ player: {}, e1: { from: 0, to: 3 }, a: {} }
]
)
//assert.deepEqual(
// query('MATCH (player:Player)-[e1:Knows]->(a:NPC) RETURN player, e1, a', engine).unwrap(),
// [
// { player: {}, e1: { from: 0, to: 1 }, a: {} },
// { player: {}, e1: { from: 0, to: 3 }, a: {} }
// ]
//)
assert.deepEqual(
query('MATCH (player:Player)-[e1:Knows]->(a:NPC)-[e2:Knows]->(b:NPC) RETURN player, e1, a, e2, b', engine).unwrap(),
@ -75,18 +75,17 @@ describe('query', () => {
)
})
it('should match multiple components', () => {
const world = engine.world.default
const { Player, Health } = engine.component
const player = create(world, Player, Health)
//it('should match multiple components', () => {
// const world = engine.world.default
// const { Player, Health } = engine.component
// const player = create(world, Player, Health)
Health.max[player] = 50
Health.current[player] = 25
assert.deepEqual(
query('MATCH (h:Health) return h.max', engine).unwrap(),
[{ h: { current: 25, max: 50 } }]
)
})
// Health.max[player] = 50
// Health.current[player] = 25
// assert.deepEqual(
// query('MATCH (e:Entity, h:Health) return e, h.max', engine).unwrap(),
// [{ e: 11, h: { current: 25, max: 50 } }]
// )
//})
})