sensors
sense
and sense2
lispdef
helper action entitiesWe can define an action for our knowledgebase by adding an entity with a lisp source form using the leodef
macro which lets us specify whether this is intended as a lisp defun
or new action
.
This article follows from this one setting up and creating a knowledgebase and this one filling in some organism ontology attributes.
Writing actions in lisp like this makes their insides opaque to the Leonardo system itself, though the form itself being stored by the leonardo system is obviously available for analysis.
The idiom of containing lispdef
entities that contain source for concrete implementations of our ontology in our ontology, e.g. making it self-evidently useful and useable is quite a point of distinction for Sandewallâs Leonardo system (my openaccess academic bibliography here).
I am choosing to use the powerful macro package cl-series
here, which expands declarative expressions into an efficient, lazy traversal while clearly indicating and assigning error codes to any incorrect statements via its compilation-time static analysis.
This does not introduce any runtime dependencies to our ontologyâs concrete realization, while adding additional compilation-time checks and purely ANSI CL optimizations using a prolog approach. I consider this to ideally complement writing the lowest level new atomic actions for our knowledgebase.
Here let us implement a helper, sense
, in our sensors
entityfile
.
Same setup as always (refer first article). The attribute definitions being referred to are from this second article.
The way this and other articles work for me is Eduardoâs eev eepitching to slime buffers.
(setq eepitch-buffer-name âslime-repl clispâ)
sensors
and adding a lispdef
to itloadk organisms-kb
setk organisms-kb
loadk organisms
loadk sensors
put sense type lispdef
addmember (get sensors contents) sense
writefil sensors
loadk sensors
Alright, now we have an entity to contain a lisp form defining an action
cl-series
ready from the lisp side. (require :series)
To start with, letâs just make a list defun that prints the contents of the world
entityfile
. This is a helper lisp function that belongs to our knowledgebase.
Admittedly, I find this step annoying. Sandewall specifically leaves it to you how you physically write your lisp code.
In particular, we have to visit the sensors
entityfile
ourselves, which if you followed me will be in #p"~/leocommunity/Plantworld/demus/Organisms/sensors.leo"
.
sensors
entityfile
to begin with---------------------------------------------------------
-- sensors
[: type entityfile]
[: latest-written "2025-07-13/23:43.+12"]
[: contents <sensors sense>]
[: changed-since-archived t]
[: nullvalued {has-purpose has-author requires mustload removed-entities leos-extension has-profile overlay-on overlay-types overlay-own leos-use dont-display sections local-ents purpose author latest-archived-entity latest-rearchived}]
---------------------------------------------------------
-- sense
[: type lispdef]
[: latest-rearchived nil]
ooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
We are meant to add our leodef
or whatever in that sense
section.
sense
definition that just prints world
âs contents
in the sensors
entityfile
---------------------------------------------------------
-- sensors
[: type entityfile]
[: latest-written "2025-07-13/23:43.+12"]
[: contents <sensors sense>]
[: changed-since-archived t]
[: nullvalued {has-purpose has-author requires mustload removed-entities leos-extension has-profile overlay-on overlay-types overlay-own leos-use dont-display sections local-ents purpose author latest-archived-entity latest-rearchived}]
---------------------------------------------------------
-- sense
[: type lispdef]
[: latest-rearchived nil]
(leodef sense nil (organism)
(series::let* ((world
(series::scan
(cdadr
(get 'world 'contents)))))
(series::iterate
((thing world))
(print thing))))
ooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
Remember, cl-seriesâ mechanism is to replace forms like let*
with special entypoints into native-code-generating compilation-time static analysis that turn placeholder series
expressions into an efficient in-order traversal. We have to use the namespace manually, since seriesâ symbols would collide with the Leonardo system if we tried to series::install
it.
As you can see, the arguement currently gets ignored and we just iteratively print
the contents of world
. From the lisp view, sequences <elements of the sequence>
â the tagged list: (seq& (elements of the sequence))
. Hence the cdadr
. Making full use of the underlying lisp (and just the underlying lisp ; remember series expands to just lisp) in low-level, atomic new actions is idiomatic (though regretful, in that they are fairly opaque).
sense
lisp-sideses.070) loadk sensors
Load-ef: sensors at ../../../demus/Organisms/sensors.leo
ses.071)
...> . (1+ 1)
2
ses.072) . (sense 'foo)
coelacanth nil
ses.073)
Itâs the coelacanth
we made in the second article.
sense2
definitionput sense2 type lispdef
addmember (get sensors contents) sense2
writefil sensors
The last one was an indicative example of generating the native lisp at compile-time using the series macro package. Iâm imagining (allowing, itâs my simulation and I can just pick what I want a sensor
sense
to mean).
(Actually, it ended up loop
y rather than series; and itâs big but keep scrolling down and we will continue talking)
---------------------------------------------------------
-- sensors
[: type entityfile]
[: latest-written "2025-07-14/21:01.+12"]
[: contents <sensors sense sense2>]
[: changed-since-archived t]
[: nullvalued {has-purpose has-author requires mustload removed-entities leos-extension has-profile overlay-on overlay-types overlay-own leos-use dont-display sections local-ents purpose author latest-archived-entity latest-rearchived}]
---------------------------------------------------------
-- sense
[: type lispdef]
[: latest-rearchived nil]
(leodef sense nil (organism)
(series::let* ((world
(series::scan
(cdadr
(get 'world 'contents)))))
(series::iterate
((thing world))
(print thing))))
---------------------------------------------------------
-- sense2
[: type lispdef]
[: latest-rearchived nil]
(leodef sense2 nil (the-organism)
(let* ((scale (get the-organism 'sensor-scale))
(off-direction-counts
`((ene
0 ; current count
,(* scale 2 ) ; xmin
,(* scale 3 ) ; xmax
,(* scale 1 ) ; ymin
,(* scale 2 ) ; ymax
)
(nne 0
,(* scale 1) ,(* scale 2)
,(* scale 2) ,(* scale 3))
(nnw 0
,(* scale -1) 0
,(* scale 2) ,(* scale 3))
(sse 0
,(* scale 1) ,(* scale 2)
,(* scale -2) ,(* scale -1))
(ssw 0
,(* scale -1) 0
,(* scale -2) ,(* scale -1))
(ene 0
,(* scale 2) ,(* scale 3)
,(* scale 1) ,(* scale 2))
(ese 0
,(* scale 2) ,(* scale 3)
,(* scale -1) ,(* scale 0))
(wnw 0
,(* scale -2) ,(* scale -1)
,(* scale 1) ,(* scale 2))
(wsw 0
,(* scale -2) ,(* scale -1)
,(* scale -1) ,(* scale 0)))))
(flet
((choose-dir
(lastdir
openangle
results)
(let* ((directions
'(e ene ne nne n nnw nw wnw
w wsw sw ssw s sse se ese))
(idx (search `(,lastdir)
directions))
(valid
(loop :With len := (length directions)
:for n :from 1 :by 2
:for theta :from 22.5 :by 45 :below openangle
:nconc
(list
(nth (mod (+ idx n) len)
directions)
(nth (mod (- idx n) len)
directions))))
(score
(loop :for compass :in
'(e ne n nw w sw s se)
:for right :in
'(ene nne nnw wnw wsw ssw sse ese)
:for left :in
'(ese ene nne nnw wnw wsw ssw sse)
:collect
(let ((sum 0))
(when (member right valid)
(incf sum
(or (cadr
(assoc right results))
0)))
(when (member left valid)
(incf sum
(or (cadr
(assoc left results))
0)))
(list compass sum)))))
(loop :for (dir score) := score
:then
(if (> nscore score)
(values ndir nscore)
(values dir score))
:for (ndir nscore) :in (cdr score)
:finally
(setf
(get the-organism 'current-direction)
dir)))))
(loop
:with xo := (get the-organism 'x-position)
:with yo := (get the-organism 'y-position)
:for organism :in (cdadr (get 'world 'contents))
:for x := (get organism 'x-position)
:for y := (get organism 'y-position)
:do (print organism)
(loop :for
(dir cur xmin xmax ymin ymax)
:in off-direction-counts
:for dx := (- x xo)
:for dy := (- y yo)
:do (print (list dir cur
xmin '<= dx '< xmax '&
ymin '<= dy '< ymax))
:when (and (<= xmin dx)
(< dx xmax)
(<= ymin dy)
(< dy ymax))
:do (print "success")
(incf (cadr (assoc dir off-direction-counts))
(or
(cadadr
(find (get organism 'type)
(cadr
(get the-organism
'sensor-weights))
:key 'caadr))
0))
(return))
:finally (return
(values
(choose-dir
(get the-organism 'current-direction)
(get the-organism 'opening-angle)
off-direction-counts)))))))
ooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
By the way, I can put whatever I want after the oooooendofentities.
Well, I guess sense
is about three different actions smooshed into one, and I ended up using the loop facility instead of cl-series here.
I spent quite a lot of time inside sldb debugger looking at values in stackframes, as well as Leonardo system to get the huge function working. Sorry for the excuse.
The meaning is that in-my-universe, âthereâs one sense
action that just does all thisâ.
I guess it
(get world contents)
max
of adjacent readingsWhere (get world contents)
is thought to be sparse. I believe this spans Braitenbergâs Vehicles, and in Braitenbergian fashion, we could compose meta-vehicles, a sort of higher order animal with multiple sensors rules (different scales, opening angles, signal weightsâŠ).
sensor
put blackbird type organism
addmember (get world contents) blackbird
put snail type organism
addmember (get world contents) snail
. (sense 'blackbird)
loadk sensors
. (sense2 'blackbird)
obviously we didnât actually define anything yet. We see that the Leonardo systemâs CLE provides its own notions of returns and error resolution.
put blackbird current-direction e
put blackbird opening-angle 23
put blackbird sensor-scale 1
put blackbird sensor-weights <<organism 3>>
(I actually forgot how to do maps momentarily but w/e a sequence of sequences will do).
put snail y-position 0
put snail x-position 0
put coelacanth y-position 2
put coelacanth x-position 1
ses.146) put blackbird current-direction e
put: blackbird current-direction e
ses.147) . (sense2 'blackbird)
coelacanth
(ene 0 2 <= 1 < 3 & 1 <= 2 < 2)
(nne 0 1 <= 1 < 2 & 2 <= 2 < 3)
"success"
snail
(ene 0 2 <= 0 < 3 & 1 <= 0 < 2)
(nne 2 1 <= 0 < 2 & 2 <= 0 < 3)
(nnw 0 -1 <= 0 < 0 & 2 <= 0 < 3)
(sse 0 1 <= 0 < 2 & -2 <= 0 < -1)
(ssw 0 -1 <= 0 < 0 & -2 <= 0 < -1)
(ene 0 2 <= 0 < 3 & 1 <= 0 < 2)
(ese 0 2 <= 0 < 3 & -1 <= 0 < 0)
(wnw 0 -2 <= 0 < -1 & 1 <= 0 < 2)
(wsw 0 -2 <= 0 < -1 & -1 <= 0 < 0) nil
ses.148)
With my verbose output, we can see that the coelacanth is discovered by a sensor, but the snail, sitting immediately beneath the blackbird, is not.
It has occurred to me that a positive reading on a sensor facing ssw
cannot on its own under our sense2
choose between s
and sw
, since I sum the 2° reading into each adjacent 8-compass reading before max
ing. So to turn left with the minimum, 23° opening angle would require a positive reading to the left, and a negative reading to the right, or a 68° opening angle with stronger positive readings both on the left to pick up more than one sensor in order to break the tie that happens.
sense
was a better lisp-side verb than sense2
was, but on the whole sense2
basically wholly defines a simulation game world reflecting Braitenbergâs vehicles on its own.
We have seen that we can put lisp definitions (i.e. of type
lispdef
) in our entityfile
s, and use the leodef
macro to make them available in lisp as helper functions.
For all its faults, my triffid sense2
definition seems to reflect Braitenbergâs Vehicles quite well, and I am excited to explore its implications for our simulation game.
Talk on the Mastodon thread as always please. I am very happy to hear from people who also started using the Leonardo calculus, and everyone else as well.
You are most welcome to use and share this article and my articles where and how occurs to you.
screwlisp proposes kittens