• 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.
  • We, the systems administration staff, apologize for this unexpected outage of the boards. We have resolved the root cause of the problem and there should be no further disruptions.

Random Character Generator

whartung

SOC-14 5K
So, over in the https://www.travellerrpg.com/threads/skill-frequency.44657/ thread I mentioned I concocted a random character generator to find out what the skill frequency is.

The wrote it in Emacs Lisp. There's no "UI", Emacs is the UI, just using the *scratch* buffer.

It's reasonably complete, it does mustering out, aging, etc.

Prints out characters that look like this:
Code:
UPP: 986559 Service: Army Rank: Captain 
Age: 26 Terms: 2 Money: 10000
Skills:
blade-cbt-1 brawling-2 low-psg-1 mid-psg-1 
rifle-1 vehicle-2

I know these are pretty much a dime-a-dozen, and I'm sure I could have picked a more obscure platform to write the code in, but if there's interest, I'll share the code.

If not, no harm, no foul.
 
I'm always interested in Traveller character generators. It looks like it is a Classic Traveller generator, no advanced chargen? Does it include S4 careers?
 
I found that looking at code in languages that I don't know encouraged me to either learn that language, or sometimes to see the one(s) I know in a new light. Obscure is a plus; post the code!
 
Emacs is a great OS but a lousy editor I've heard :)

I sill write code in vi, even in for c# & ruby, languages currently using. just easier for me and for most stuff a lot faster. Plugins for the IDEs I use. it does throw people off when we pair program on my machine.

and one of my previous bosses used emacs.

and I am always interested in looking at code.
 
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.
 

Attachments

Last bit, here's the core routine for running a character through a single term.

Code:
(defun service-term (char service)
  "runs a character through a single term of service"
  (let ((skills 1)
        (survived (success char service 'survival 'survival-dms))
        (commissioned (success char service 'commission 'commission-dms))
        (promoted (success char service 'promotion 'promotion-dms)))
    (cl-incf (char-terms char) 1)
    (when survived
      (when (eq (char-terms char) 1)
        (cl-incf skills))
      (dprint (list "comm" (char-commissioned char) (char-terms char) (char-drafted char)))
      (when (and (not (char-commissioned char))
                 (not (and (= (char-terms char) 1) (char-drafted char))))
        (when commissioned
          (dprint "commissioned")
          (cl-incf skills)
          (setf (char-rank char) 1)
          (setf (char-commissioned char) t)))
      (when (char-commissioned char)
        (when promoted
          (dprint "promoted")
          (cl-incf skills)
          (when (< (char-rank char) 6)
            (cl-incf (char-rank char) 1)
            (let ((rank-skill (assoc (char-rank char) (service-rank-skills service))))
              (when rank-skill
                (add-skill char (cadr rank-skill)))))))
      (when (> (service-skills-per-term service) 0)
        (setf skills (service-skills-per-term service)))
      (let ((skill-tables (list (service-personal-development service)
                                (service-service-skills service)
                                (service-advanced-education service)
                                (service-advanced-education2 service)))
            (total-tables (if (>= (char-edu char) 8) 4 3)))
        (dprint (list 'skills skills))
        (dotimes (i skills)
          (let* ((skill-table (elt skill-tables (random total-tables)))
                 (skill (elt skill-table (random 6))))
            (add-skill char skill))))
      (cl-incf (char-age char) 4))
    (when (not survived)
      (setf (char-status char) 'dead))))

This is pretty straightforward. It's pretty much just running down the page of the rules in the book. It runs them through commission and promotion.

The "assoc" function find entries in an association list, a common construct with lists in Lisp. This code uses those in lieu of, say, a hashmap typically done in other languages. And I use that because its simple. Lists are a native construct in Lisp, vs a hashtable. The DMs are lists, the skills are lists, etc.

Are lists efficient? Not necessarily. But at these sizes, and this code, that's not a concern. On my machine I can create 100K characters in less than 10s. "fast enough". Some things could be better represented as vectors, or hashmaps. But this is simple hacking. Not worth that extra effort.

Same reason I use symbols everywhere. For example, here, I use 'dead. "dead" is a symbol. 'dead turns into (quote dead). Quote is a special form in lisp that does not evaluate its argument (i.e. it does not try to look up the value for "dead"). I use "dead" as a symbol, instead creating a string with quotes.

Why do that? Symbols are singletons. There's only one instance of a symbol in memory. Every place I refer to "dead", its the same symbol.

In contrast, if I use a string, "dead" (boy thats confusing), those are all different instances.

Consider:

Code:
(assoc 'dex '((str 1) (dex 2)))
(assoc "dex" '(("str" 1) ("dex" 2)))

Because of how assoc works (that is, its default behavior), its use the "eq" function for comparison, which stands for "equality". In C, it's the same as "==". It means "these are the same thing". The exact same thing. It's comparing pointers to memory. Recall that there's only one instance of a symbol, so the first 'dex symbol is the same thing as the second 'dex. They refer to the same element in memory.

Whereas in the second line, the first "dex" is an instance of a string at location XYZ in memory, while the second "dex" is another instance, at location QED. When assoc compares XYZ with QED, it will find them different (and thus assoc fails). You can pass an optional test function to assoc to deal with this, but why fight that. The symbols are just fine.

So, its natural in Lisp to use symbols where most other languages would use strings.

Finally, these are scattered about.
Code:
      (dprint (list "comm" (char-commissioned char) (char-terms char) (char-drafted char)))

That's a debug print I have in there. I have a little function at the top:
Code:
(defun dprint (f)
  (when debug-flag
    (print f)))

Why do I send lists to the print function? print only takes one argument. Wrapping everything in a list makes a single argument to print. Yes, I'm that lazy.

You'll need to run:

Code:
(setf debug-flag nil)

Or the code will give you an error.

You can turn debug on with:
Code:
(setf debug-flag t)

That's it for now.

Feel free to ask questions if you have any problems.
 
I never could get my head wrapped around Lisp. But thanks for the code - cool to see how people do things like that.

(I did have 2 classes in college that needed to use Lisp (believe it or not I had an AI class in the 80s!) the other part was more robotics, and I could write hex code easily enough. But Lisp and I just did not click. I'd rather write in assembly or even binary)
 
Back
Top