ludic/docs/language/query/query-nearest.md
Orkuncakilkaya b25dc328a2
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m11s
commit-lint / conventional-commits (push) Successful in 4s
docs / build-and-deploy (push) Successful in 18s
feat(stdlib): add Query.* — ECS spatial queries over the reflection ABI (#42)
Completes the half of #24 that was explicitly deferred as blocked: entity-space
queries to sit alongside the grid-space Grid.*/pathfinding that shipped in
07e5a20. Query.* answers questions about the live entities that carry a
property, built directly on the EV2 reflection ABI (world_query_next/world_get):

  - Query.count(prop) -> int                 how many live entities carry prop
  - Query.first(prop) -> int                 the lowest-id bearer, or -1
  - Query.nearest(prop, pos, xf, yf, x, y)   the bearer closest to (x,y), or -1
  - Query.within(prop, pos, x, y, r, xf, yf) -> []int   every bearer within r

prop is a property id (World.prop_id); the spatial forms read a position from a
coordinate property `pos` at two int field ids (World.field_id), so `prop` can be
a discriminating tag distinct from the position component ("nearest Enemy"), or
the same id to query the coordinate component itself. Distances are exact squared
integers (no sqrt), ties break to the lower entity id, and `within` returns
entities in ascending id order — so every answer is deterministic and replay-safe.

The engine (runtime/native/query.ludic, ~55 lines of Ludic, C-free) is a linear
scan over the entity table — ample for the entity counts Ludic targets, the same
reasoning as the grid pathfinder's open set; a bucketed/quadtree index is a
future optimisation, not a correctness need. It is spliced on demand when the
parser sees Query.* (g_uses_query), which also force-emits the reflection ABI so
a Query program needs no @events of its own (previously the ABI required them).

examples/library/query.ludic asserts 18 cases over five entities at known
positions (count/first with a component filter, nearest with a separate tag vs
position property, within radii incl. r=0 and the empty-property case), wired
into x test (now 63 passed). Docs: a Query section + 4 per-symbol pages,
inventory/coverage green. Seed reseeded; the C-free bootstrap fixpoint holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-31 13:19:25 +03:00

1.7 KiB

id name category kind tokens sig tip order ns member
query-nearest Query.nearest query namespace-method Query.nearest Query.nearest(prop, pos, x_field, y_field, x, y) -> int The bearer of prop closest to a point, by squared distance. 3 Query nearest

Returns the live entity carrying prop whose position is closest to (x, y), or -1 if none. Position comes from property pos at its integer fields x_field and y_field — pass the same id for prop and pos to query the coordinate component itself, or a separate tag for prop to mean "nearest Enemy". Distance is compared by exact squared integer distance (no square root), and ties go to the lower entity id, so the result is fully deterministic. The go-to for target acquisition, "interact with the closest thing", or nearest-neighbour AI.

Parameters:

  • prop — the property that filters which entities are considered
  • pos — the property holding the coordinates
  • x_field, y_field — field ids of the x / y coordinate within pos
  • x, y — the point to measure from
program Demo {
  property Position { col: int = 0, row: int = 0 }
  property Enemy { hp: int = 0 }
  model Slime { Position, Enemy }
  entry {
    spawn Slime { Position { col: 2, row: 0 }, Enemy { hp: 3 } }
    spawn Slime { Position { col: 9, row: 0 }, Enemy { hp: 3 } }
    let E  = World.prop_id("Enemy")
    let P  = World.prop_id("Position")
    let cx = World.field_id(P, "col")
    let cy = World.field_id(P, "row")
    let e  = Query.nearest(E, P, cx, cy, 0, 0)   # the enemy at col 2
    print(World.get(e, P, cx))                    # 2
  }
}