Ok, I'll drop this here.
It's about 900 lines, but includes LBB 1 and S4.
For stuff like this, there's a couple of reasons I like using Lisp.
First, there's no I/O. Meaning, since we that REPL (Read Eval Print Loop), the Lisp listener, I don't have to spend any effort on input or output routines.
Rolling up a character is pretty simple. Throwing it up on a GUI or anything like that, that's Work. The REPL gives me all that for "Free" as long as I'm content to work the way it works, which I am.
I can not compare the Lisp REPL to something like Python, simply because I don't know Python, and have no real interest in it. Yes, the whitespace nature of Python is off-putting to me, but also the entire environment is off-putting to me. With all of its dependencies, etc. Clearly folks get by, but I don't see a need.
I tried to use one of the Notebook things? Jupiter? Maybe? It uses something called "Anaconda". I download half the internet into a complicated install and I still couldn't get it to work.
Second, I really like kabob-case-for-identifiers. I like it more than using underscores_for_identifiers, or even camelCase for them. It's just easier to type, easier to read.
As for emacs, I'm simply using this because it has a built in Lisp (I have external ones, I do like Common Lisp more than eLisp). I've been using emacs for over 30 years. It has almost 2000 keybinds out of the box. I know, uh, like, 3 of them. I rely on its automatic parentheses matching and its auto-indenting for Lisp (both of which are important to usability with Lisp). That said, I've done a lot of work with just raw vi, and it works for me.
Emacs has endless plugins to support Lisp, navigating S-expresssions, and all that. I don't use any of those (I do use Slime for Common Lisp). I'm sure I'm less efficient, but my big complaint for emacs is all of its Meta this, Control, Meta-Control-Shift-Left Foot Pedal the other. Which is why I don't know any of the keybinds. They're all uncomfortable for me. But still, I find value just using this ][ much of emacs to get by.
So, onto the code.
To use it, "simply" visit the file in Emacs (Ctrl-X F), then you can Evaluate Buffer (M-X evaluate-buffer). Then you can then head over to the *scratch* buffer.
Then you can do something like this:
Code:
(setf services (create-services))
(print-char (random-character services))
That'll print out a random character from the LBB 6 services.
The system works on the meta data stored in Service structure.
Code:
(cl-defstruct service name enlistment enlistment-dms survival survival-dms
commission commission-dms promotion promotion-dms
reenlist skills-per-term
ranks personal-development service-skills
advanced-education advanced-education2
mustering-out mustering-out-cash
auto-skills rank-skills rank-names soc-is-rank)
and each service has a respective "create-<service>" function. Such as create-army, create-marines, create-flyers, create-hunters.
create-services just creates the LBB 6
Code:
(defun create-services ()
(list (create-navy) (create-marines) (create-army)
(create-scouts) (create-merchants) (create-other)))
And random-character takes a list of services to pick from.
Characters are structures also (structures are like records or structs in C).
Code:
(cl-defstruct char str dex end int edu soc terms status skills commissioned
rank rank-name age service drafted)
Here's the Navy:
Code:
(defun create-navy ()
(let ((service (make-service)))
(setf (service-name service) "Navy")
(setf (service-enlistment service) 8)
(setf (service-enlistment-dms service) '((int 8 1) (edu 9 2)))
(setf (service-survival service) 5)
(setf (service-survival-dms service) '((int 7 2)))
(setf (service-commission service) 10)
(setf (service-commission-dms service) '((soc 9 1)))
(setf (service-promotion service) 8)
(setf (service-promotion-dms service) '((edu 8 1)))
(setf (service-reenlist service) 6)
(setf (service-skills-per-term service) 0)
(setf (service-personal-development service)
'(str dex end int edu soc))
(setf (service-service-skills service)
'(ships-boat vacc-suit fwd-obsrv gunnery blade-cbt gun-cbt))
(setf (service-advanced-education service)
'(vacc-suit mechanical electronics engineering gunnery jack-o-t))
(setf (service-advanced-education2 service)
'(medical navigation engineering computer pilot admin))
(setf (service-mustering-out service)
'(low-psg int (edu 2) blade travellers-aid high-psg (soc 2)))
(setf (service-mustering-out-cash service)
'(1000 5000 5000 10000 20000 50000 50000))
(setf (service-rank-skills service) '((5 soc) (6 soc)))
(setf (service-rank-names service) '("Ensign" "Lieutenant" "Lt. Commander"
"Commander" "Captain" "Admiral"))
service))
Here's the highlights of how this works. Since the process is mostly identical across services, its mostly in the data.
Take a note of the DMs and how they're represented.
Code:
(setf (service-enlistment-dms service) '((int 8 1) (edu 9 2)))
Here we have 2 DMs, a DM +1 for INT >= 8, and a DM +2 for EDU >= 9. If the the value is negative (i.e. (edu -9 2)), then the DM is for <= the absolute value of the number (so, EDU <= 9). Finally, another DM, the name of a slot in the structure. Structures are made of slots (service-name is the name slot for a service). This is used for Belters, where we use the "terms" slot as the DM.
Next, we have the skills and such.
Code:
'(low-psg int (edu 2) blade travellers-aid high-psg (soc 2)))
Each symbol is a skill or attribute or something else. If it's an attribute (str, dex, int, etc.), then we bump the attribute on the character. Otherwise, we just add item to the list of skills. If the item is already there, we bump it up. Also, note things like "(edu 2)", instead of just adding one, this adds 2. This also works for things like "(soc -1)".
It does not distinguish between "things" and skills, so the skills could easily say something like "vacc-suit-2 low-psg-2". Its simply not worth the effort to distinguish at this level.
Let's take a look at how some of this works.
Here's the code to collect the DMs.
Code:
(defun collect-dms (c dms)
(let ((tot-dm 0))
(dolist (dm dms)
(if (consp dm)
(let* ((attribute (cl-first dm))
(rating (cl-second dm))
(amount (cl-third dm))
(attribute-value (cl-struct-slot-value 'char attribute c)))
(if (> rating 0)
(when (>= attribute-value rating)
(cl-incf tot-dm amount))
(when (<= attribute-value (abs rating))
(cl-incf tot-dm amount))))
(let ((attribute (cl-struct-slot-value 'char dm c)))
(when attribute
(cl-incf tot-dm attribute)))))
tot-dm))
The parameters are the character, and a list of DMs. The secret sauce here is "cl-struct-slot-value". This is a reflective bit. Given the name of an attribute, we can get the value of that slot from a structure. Normally, you use (structure-name structure) to get the value. This is more dynamic.
So, this code uses "dolist" to iterate across the list of DMs. A DM is either a list "(soc 9 1)" or a single item "(terms)". "consp" determines if that element is a list or not. If its a list, we break it apart with cl-first, -second, -third. Then we look up the value from character using the secret sauce. If the dm is not a list, then we look up the attribute directly.
Idiom here. First, "let*". Lisp uses "let" and "let*" to introduce bindings. it's a lexical binding, meaning the bindings are only valid inside the let. The difference between "let" and "let*" is that with "let", you can not access the bindings with the binding section. With "let*" you can. In this case, we use the 'attribute' value in the 'attribute-value' binding.
Next, we have "if" but also "when". Note that everything in Lisp is an expression. so (if cond val1 val2) is much like C's (and others) trinary operation "cond ? val1 : val2". With "if", the "else" clause is optional. (if cond val1) and (when cond val1) are identical. However, I do not find that "if" distinguishes well between the true and false sections. You can see here, the second "when" is indented a little less than the first. That can blur out when things get complicated. A "when" has no else, so there's no chance of a getting it messed up. (the counter part to "when" is "unless").
cl-incf is essentially (setf place (+ place amount)). But it's more subtle than that because of how setf works. Think of it like "place++" or "place += amount".
Next item:
Code:
(defun success (char service step-target-name step-dms-name)
(let ((dms 0))
(when step-dms-name
(setf dms (collect-dms char
(cl-struct-slot-value 'service step-dms-name service))))
(let ((target (cl-struct-slot-value 'service step-target-name service))
(roll (roll-dice 2)))
(>= (+ roll dms) target))))
This one is nice too. Is uses the secret sauce again, and its used like this:
Code:
(survived (success char service 'survival 'survival-dms))
We have the name of the target, the name of the DMs, and use the cl-struct-slot-value to pull those out of the service.
Continue next post.