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.
ADDENDUM: I had written values somewhere I meant to write lisp in sense2, fixed now.
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 loopy 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 sense2 (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 score2) := (car score)
:then
(if (> nscore score2)
(list ndir nscore)
(list dir score2))
: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))
(print (find (get organism 'type)
(cadr
(get the-organism
'sensor-weights))
:key 'caadr))
(print off-direction-counts)
(return))
:finally
(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âŠ).
sensorput 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 maxing. 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 entityfiles, 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