On ecs
Another attempt at describing what I see:
An Entity is an Object. In traditional programming languages you would define a Struct or a Class. In a factbase, you virtually crush the Object molecule in an Atomizer which spits out the same info but at a very fine grain (triplets). The fine grained description is fugly for humans to read, but makes it easier for stupid machines to deal with. You don’t (even) get “pointers” like in C (which is pretty low level), you get “relations”. Each field in an Object becomes a set of very fine grained triplets, so a “pointer”, contained in an Object, becomes a triplet which says something like (father id1 id57)
, where id1
is the name of the Object, father
is the name of the field (now expressed as a relation) and id57
is the name of the target.
In some hypothetical traditional language, you might say
Entity id1 {
...
father : Entity
...
}
id1.father = id57
The traditional description is more human-readable, but, contains a lot of pesky syntax - which helps human readability but hinders machine automatability.
The triple method contains no (or very little) syntax, but is harder for humans to read and to reason about.
Bizarrely, so-called “computer science” has become more about human readability than about stupid machine readability.
Your example might be expressed, in traditional terms, as:
class Entity {
position: PositionVector
model: Geometry
about: string
}
var player = Entity{position:[0,0,0], model:<...>, about:"seeker of lightly used gym socks"}
var treasure1 = Entity{position:[10,3,0],model:<...>,about:"A pair of lightly used gym socks"}
var treasure2 = Entity{position:[9,-5,0], model:<...>,about:"Dictionary of mythical animals, middle pages missing."}
whereas, in a factbase, the above might be normalized to:
(entity 'player nil)
(position 'player '(0 0 0))
(model 'player 'm57)
(about 'player "seeker of lightly used gym socks")
(entity 'treasure1 nil)
(position 'treasure1 '(10 3 0))
(model 'treasure1 'm171)
(about 'treasure1 "A pair of lightly used gym socks")
(entity 'treasure2 nil)
(position 'treasure2 '(9 -5 0))
(model 'treasure2 'm99)
(about 'treasure2 "Dictionary of mythical animals, middle pages missing.")
and the “algorithm” for drawing the world becomes a statement, not a recipe:
drawWorld :-
(entity ?X nil)
(position ?X ?POS)
(model ?X ?M)
draw(?M ?POS).
The engine only needs to pattern-match triplets, and create “holes” (placeholders) for the ? thingies. The engine might use brute-force backtracking or something wildly more complicated, but, you don’t care. Once the engine finds 3 matches as specified above, it fills in the holes and calls draw(...)
. It does this matching over and over again until it has touched every triplet in the factbase.
Dumb algorithm: When the engine finds one match, e.g. (enity player nil)
, it tries for the second match (position 'player ?POS)
starting from the top of the factbase again (duh, but often “fast enough”).
PROLOG is basically this kind of thing with nuances added to allow specifying negative matches (“must not be ???”) and cut-off points. PROLOG does it kinda like above, using backtracking. MiniKanren figured out how to get the same results without needing to use backtracking (kinda feed-forward, successive filtering based on the assumption that wasting memory is OK so that it can keep a list of “matched thus far”, whereas PROLOG was designed in the days of memory-fear, so it was considered better to waste machine time doing backtracking whilst conserving memory)
N.B. glossed over: the triplet version also needs to define commands for “define a pattern-matching rule” and “call some external procedure”. Not exactly rocket science. In Lisp, say, a rule is a name and a list of triplets-to-be-matched. A call might be a list where the first item is some special symbol and the rest is the list specifying the call with parameters that might be filled-in holes. If you get this right, then these thingies fit neatly into the factbase and the factbase engine knows what to do when it hits them - enter Nils Holm.
Advice
There are at least 2 tasks here:
- design the game at a “high level”
- design and test the code that executes the game as specified in point (1).
I find it a lot easier to keep these activities separate. Go into a Flow State and do (1), then, go into a different Flow State and do (2). By the time you finish (1), you will probably already know what to do in (2). The PROLOG-crazies think that (1) is way more important than (2), so they give you a syntax for doing (1) and tack on syntactic baubles to do (2) as an after-thought, in the unfortunate belief that it is easy to write (2)-code using (1)-like patterns. The General Purpose Programming Language crazies see the world in reverse - i.e. they see (2) as way more important than (1) and give you syntax for doing (2) at the expense of hobbling what you want to say in (1). In the best of all worlds, you would have 2 completely different syntaxes, or, you would use Lisp macros to do (1) and then use Lisp and macro expansions to do (2). Or, if you were ultra-extreme, like me, then you would invent a DSL for (1) and write (1) in that DSL. Then, you would use OhmJS to transpile the (1)-code into Lisp and run it.
And, then, if you were really-really-extreme, you would use diagrams of boxes and arrows to do (1) and use layers of OhmJS to transpile the diagrams into Lisp and PROLOG and …
ECS in Nils Holm’s PROLOG
Using stock code from Nils Holm’s PROLOG Control in 6 Slides , I tacked this factbase onto the end:
;;; for Jos'h
(define db '(
((entity player nil))
((position player (0 0 0)))
((model player m57))
((about player "seeker of lightly used gym socks"))
((entity treasure1 nil))
((position treasure1 (10 3 0)))
((model treasure1 m171))
((about treasure1 "A pair of lightly used gym socks"))
((entity treasure2 nil))
((position treasure2 (9 -5 0)))
((model treasure2 m99))
((about treasure2 "Dictionary of mythical animals, middle pages missing."))
))
Then, I set up a goal which is to find ?POS and ?M for every entity..
(define goals '(
(entity (? X) nil)
(position (? X) (? POS))
(model (? X) (? M))
))
and executed the search:
$ mit-scheme
mit-scheme
MIT/GNU Scheme running under OS X
Type `^C' (control-C) followed by `H' to obtain information about interrupts.
Copyright (C) 2022 Massachusetts Institute of Technology
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.
Image saved on Saturday January 7, 2023 at 8:36:21 PM
Release 12.1 || SF || LIAR/x86-64
1 ]=> (load "josh")
(load "josh")
;Loading "josh.scm"... done
;Value: goals
1 ]=> (prove6 '() goals db empty 1 '())
(prove6 '() goals db empty 1 '())
m = m57
pos = (0 0 0)
x = player
m = m171
pos = (10 3 0)
x = treasure1
m = m99
pos = (9 -5 0)
x = treasure2
;Value: #t
Then, I refined the factbase to contain a drawInfo
rule
;;; for Jos'h - again, but with a Rule for drawInfo
(define db '(
((entity player nil))
((position player (0 0 0)))
((model player m57))
((about player "seeker of lightly used gym socks"))
((entity treasure1 nil))
((position treasure1 (10 3 0)))
((model treasure1 m171))
((about treasure1 "A pair of lightly used gym socks"))
((entity treasure2 nil))
((position treasure2 (9 -5 0)))
((model treasure2 m99))
((about treasure2 "Dictionary of mythical animals, middle pages missing."))
((drawInfo (? entity) (? position) (? model))
(entity (? entity) nil)
(position (? entity) (? position))
(model (? entity) (? model)))
))
(define goals '((drawInfo (? entity) (? position) (? model))))
(define (basic-test)
(prove6 '() basic-goals basic-db empty 1 '()))
(define (test)
(prove6 '() goals db empty 1 '()))
and ran everything again
$ mit-scheme
mit-scheme
MIT/GNU Scheme running under OS X
Type `^C' (control-C) followed by `H' to obtain information about interrupts.
Copyright (C) 2022 Massachusetts Institute of Technology
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.
Image saved on Saturday January 7, 2023 at 8:36:21 PM
Release 12.1 || SF || LIAR/x86-64
1 ]=> (load "josh")
(load "josh")
;Loading "josh.scm"... done
;Value: test
1 ]=> (basic-test)
(basic-test)
m = m57
pos = (0 0 0)
x = player
m = m171
pos = (10 3 0)
x = treasure1
m = m99
pos = (9 -5 0)
x = treasure2
;Value: #t
1 ]=> (test)
(test)
model = m57
position = (0 0 0)
entity = player
model = m171
position = (10 3 0)
entity = treasure1
model = m99
position = (9 -5 0)
entity = treasure2
;Value: #t
1 ]=>
(see 1 ]=> (test)
above)
N.B. everything seems to be neatly ordered, but, that’s just a coincidence caused by the simplicity of this example.
Appendix - Adding Lisp Call-Outs
Apparently, I ported Holm’s code to Common Lisp and added the (:lisp …) command.
I hacked on prove6
and renamed it to prove-helper
. (Actually, I augmented prove
and moved most of the matching logic to a different function called prove-helper
). I can revisit and explain this more, if this interests you (note that the textual documentation leaves a lot to be desired, sigh :-).
Appendix - See Also
References
https://guitarvydas.github.io/2004/01/06/References.html
Blog
Blog
obsidian blogs (see blogs that begin with a date 202x-xx-xx-)
Videos
videos - programming simplicity playlist
Books (WIP)
Pamphlets
Discord
Programming Simplicity all welcome, I invite more discussion of these topics, esp. regarding Drawware and 0D
@paul_tarvydas