• Welcome to the new COTI server. We've moved the Citizens to a new server. Please let us know in the COTI Website issue forum if you find any problems.

Emacs Lisp Dabbling

whartung

SOC-14 1K
I've been doing a bunch of little simulations recently, and I've been doing them in Emacs Lisp. Why Emacs Lisp?

When it comes to Lisp today, outside of a dedicated environment, emacs is the "go to" UI for many Lisps and Schemes.

Normally, when I Lisp dabble, I use CLISP, an editor, and the command line. This is because CLISP is pretty simple, and it offers command line history (like the bash shell does). Configuring emacs to integrate with other Lisps is straightforward, but I've just never bothered. It just over complicates things.

But I thought I'd simplify even more by using the native lisp within emacs (elisp) for little toy projects. ELisp is neither a Scheme nor a Common Lisp. It's its own beast with its own style. But it's close, and there's built in Common Lisp-esque routines one can use.

I've been doing it there because of the interactive nature of it. It has this interaction mode where it's just really easy to try things out and play with things. Kind of like the Smalltalk Workspace, but better (to me) because of the nature of Lisp vs Smalltalk.

I thought I'd share what I did for that "How many navigators" sim I did mentioned in another thread.

First, the core code.
Code:
(defun roll-dice (n)
  (let ((sum 0))
    (dotimes (i n)
      (setq sum (+ sum (+ (random 6) 1))))
    sum))

(cl-defstruct char str dex end int edu social terms status skills commissioned)

(defun has-dm (trait value dm)
  (if (>= trait value)
      dm
    0))

(defun check-roll (target &rest dms)
  (let* ((dm-total (reduce #'+ dms)))
    (>= (+ (roll-dice 2) dm-total) target)))

(defun roll-character ()
  (let ((c (make-char)))
    (setf (char-str c) (roll-dice 2))
    (setf (char-dex c) (roll-dice 2))
    (setf (char-end c) (roll-dice 2))
    (setf (char-int c) (roll-dice 2))
    (setf (char-edu c) (roll-dice 2))
    (setf (char-social c) (roll-dice 2))
    (setf (char-terms c) 0)
    (setf (char-status c) 'alive)
    (setf (char-skills c) 0)
    c))

(defun enlist-navy (c)
  (let* ((int-dm (has-dm (char-int c) 8 1))
         (edu-dm (has-dm (char-edu c) 9 2)))
    (check-roll 8 int-dm edu-dm)))

(defun navy-term (c)
  (let* ((surv-int-dm (has-dm (char-int c) 7 2))
         (survived (check-roll 5 surv-int-dm))
         (check-commission (check-roll 10 (has-dm (char-social c) 9 1)))
         (check-promotion (check-roll 8 (has-dm (char-edu c) 8 1)))
         (check-reenlist (if (< (char-terms c) 7)
                             (check-roll 6)
                           (check-roll 12)))
         (total-skills 0))
    (incf (char-terms c))
    (incf total-skills)
    (when (= (char-terms c) 1)
      (incf total-skills))
    (when (and (not (char-commissioned c)) check-commission)
      (incf total-skills)
      (setf (char-commissioned c) t))
    (when check-promotion
      (incf total-skills))
    (if survived
        (dotimes (i total-skills)
          (when (= (roll-dice 1) 1)
            (incf (char-skills c))))
      (setf (char-status c) 'dead))
    (and survived check-reenlist)))

Some really simple routines. Roll some dice, simple structure, check for a DM, and see if a roll passes.

Biggest shortcut here is that we simply roll to see if they got Navigation. Since this is on a single table, and only once, it's a trivial 1 in 6 chance of getting navigation. So, no skill lists, no table choices, simply for each possible skill a character gets during generation, they have a 1 in 6 chance of bumping navigation (which is stuffed in the char-skills slot of the structure).

enlist-navy returns true is a character enlisted. navy-term updates the character and returns true if they get another term.

And that's it.

But where is the rest? Where's the 10,000 sailors? Where's the analysis?

The rest isn't in the code file, it's in the workspace. Emacs can set a buffer to be in "lisp-interaction-mode". Essentially that means that you can type in lisp expressions, hit Ctrl-J, and evaluate them. It has other features (like displaying lisp function arguments while editing). But the fundamental part is that you can just click at the end of an expression and evaluate it. When you evaluate, it prints the result. And in Lisp, everything is an expression.
Code:
(roll-character)
#s(char 10 7 6 7 11 5 0 alive 0 nil)

Do that 3 times (put cursor at the end of the ')' and hit Ctrl-J).

You can see the character structure, the 6 attributes, number of terms (0), status (alive or dead), skill level of navigation, and whether they've been commissioned.
Code:
(roll-character)
#s(char 7 9 9 3 5 3 0 alive 0 nil)

#s(char 6 6 4 8 4 8 0 alive 0 nil)

#s(char 10 7 6 7 11 5 0 alive 0 nil)

You can see the results are simply inserted in to the page. When you want to clean up the output, kill them like you would lines in any editor.

Command line history is nice, but this is a bit nicer when the expressions get more involved.

Why not just type the expressions in to the source file? Because here, I get the output. I can actually enable the interaction mode in the source code, but then it clutters the code with the output. This segregates the "working with the clay" and the "making of the clay" parts for me. When I was doing the trading work, my "expression" was over 50 lines long, but it wasn't until very late that I finally transferred it over to the source file for posterity. This is one of the features of Lisp in this case, the expressions can be arbitrarily complex. I don't have to wrap things in a function, then call the function. I can just evaluate the expression (all 50 lines in that one case) and skip that whole step.

Let's create the navy.

Code:
(let ((enlisted nil))
  (dotimes (i 100)
    (let* ((c (roll-character))
           (enlist (enlist-navy c)))
      (when enlist
        (push c enlisted))))
  (setq navy enlisted)
  (length enlisted))^J
57  <-- 57 folks enlisted out of the hundred.

Simply, roll a character, see if it enlisted, if it did, add it (push it) to the enlisted list. At the end, we set navy to the enlisted list.

See, since it's a single expression, all of the internal variables "go away" once it's evaluated, so if I want to keep something, I need to escape it somehow. That's what setting the global navy does.

If I want 10 people, I just change the 100 to 10 and ^J it again.

Running a character term is just calling navy-term. As long as navy-term rolls true (t), we keep running them.
Code:
(let ((c (roll-character)))
  (while (navy-term c))
  c)^J
#s(char 7 2 2 8 10 7 1 dead 0 nil)
This guy lasted one term and accidentally stepped in front of an engine intake (or WAS IT an accident? Reports are mixed...). See that 1 before 'dead'? That's the number of terms. The while loop does the work. "While we can run another term, run another term."

So, let's run the entirety of the navy through.
Code:
(dolist (c navy)
  (while (navy-term c)))^J
nil <- this doesn't return anything, just run for side effects.
How many lived?
Code:
(let ((sum 0))
  (dolist (c navy)
    (when (eq 'dead (char-status c))
      (incf sum)))
  sum)^J
10 <- 10 folks died
Tells us how many are dead. We can also get all fancified and functional
Code:
(cl-count-if #'(lambda (c)
                 (eq (char-status c) 'dead)) navy)^J
10
I'm an imperative guy, so I typically fall for simple loops at first go. Why isn't this its own function? Well it could be, but it's already done, it costs the "same" to run as anything else (i.e. click at the end hit ^J). If I had to type that in over and over, like in the Listener, they, yea, I'd have made it a first class function.

If you noticed in the code, I don't check for the EDU 8+ criteria. "All" of the navy gets the rolls. So, I need to remove those unqualified folks. (If you have a really keen eye you'll notice you can get a commission and/or a promotion if you're already dead, but..that's not important enough to fix.)
Code:
(setf navy (cl-remove-if #'(lambda (c)
                             (< (char-edu c) 8)) navy))^J
.... <- this returns the new list with the folks removed, emacs ellides large list results in the buffer
(length navy)^J
31 <- 31 folks left after the culling

Anyway, let's see how our navigators panned out. Scan the sailors and bump the bucket for that level of Navigation skill
Code:
(let ((v (make-vector 10 0)))
  (dolist (c navy)
    (when (eq 'alive (char-status c))
      (incf (aref v (char-skills c)))))
  v)^J
[9 7 5 4 2 0 0 0 0 0]  <- 9 have 0 skill, 7 +1, 5 +2, 4 +3, 2 +4

This is all very iterative. It obviously doesn't start this way, and it's just evolved over time. The code becomes the main components that you just stitch together interactively, tweaking and tuning until you get the result you want. In the end, anything you think is worth saving, you copy over to the source file. Otherwise, it just Goes Away when you close the editor.

Anyway, I just wanted to share a "session" of me figuring out these silly little sims I've been posting on the past little bit. The interactivity of the environment, the short cuts I take to drill in the result.
 
Confound it, @whartung. I thought I could overcome the compulsion, but I could not....

lisp_cycles.png
 
It has been a few decades since I used Lisp; and, I used it irregularly then. I was surprised I could still read and comprehend. Thank you for sharing.
 
Hare to grasp that the early 2000s WERE "two decades ago".
hard to grasp that my son turned 21 last month...and was born in 2000. Time flies.

I had some Lisp back in college (mid 80s) for some AI classes. I never could get my head wrapped around it despite all the parentheses doing all that wrapping :)
 
I've been playing with Lisp since the mid-90s. My first "web app" was done with CGI and scheme. Boy you should have seen this brutal, awful hack I did to interface scheme to our back end database back in the day. /shiver But the demo went well! (y)

I wrote a declarative rule language in it once that compiled down into Yet Another imperative rule language (that I also wrote) that compiled down in to Java. That wasn't too bad. It was pretty amazing how fast that went to together to be honest. Boy did that save me a lot of time.

Lisp made that easy because I made the new rule language simple Lisp macros, so I didn't have to write a lexer, parser, or any of that other "language" stuff. That was my largest Lisp project. The engine was about 1800 lines, the rules were about 8000.

My High Guard combat code is about 1200 lines.

I don't use Lisp everywhere because if I want graphics, I just use Java. I love Java, Java is amazing. But Lisp is fun and easy.
 
Back
Top