Lacking Natural Simplicity

Random musings on books, code, and tabletop games.

The Lord of the Rings Adventure Game from Iron Crown Enterprises

Last edited: 2022-12-26 22:43:46 EST

I got an interesting game yesterday, and finished reading it today: the 1991 Lord of the Rings Adventure Game from Iron Crown Enterprises, which was the first holder of the Tolkien franchise for RPGs.

ICE got its start with Rolemaster, which started as a detailed add-on combat system for D&D and developed into a very detailed RPG of its own, based on open-ended d100 rolls: high is good and on very high or low rolls you roll again and add the new roll for high or subtracted for low, continuing infinitely in either direction.

ICE then got the Tolkien franchise and developed Middle-Earth Role Playing (MERP) as a slightly simplified RPG for those who wanted to play in Middle Earth, but did not want all the complexities of the full Rolemaster system. The adventures were written in such a way that you could play them either with Rolemaster or MERP, and had some short guidelines to help the GM adapt them for other games. They had a lot of success and their Middle Earth products were extensive and highly regarded.

Lots of folk used them with other RPG systems. (I bought several for the ideas.)

Later in their run, ICE wanted to tap the larger Tolkien readership and so wrote the Lord of the Ring Adventure Game as a much simpler game that they hoped would draw interested readers and the viewers of the animated Hobbit and Lord of the Rings.

(I have fond memories of the animated Hobbit, though I knew it only through reading the graphic novel adaptation that used art from the movie when I was at my uncle and aunt's place in New York one summer. I finally saw it, probably on TV some years later.)

Anyway, the Lord of the Rings Adventure Game (LOR hereafter, as seems to be customary) comes in a box with a 32 page rulebook, a 64 page adventure with 6 pre-generated characters, a 32 pamphlet of 4 pages of backstory for the 6 pregens and area maps for the adventure, several poster sized maps, including a nice color one of Middle Earth, and (missing in my copy, as the seller had noted in the description) 2 six-sided dice and a sheet of standup cardboard figures of people and creatures and the pregens.

Characters have 12 stats: 6 capabilities (Strength, Agility, Intelligence, Movement, and Endurance) and 6 skills (Defense, Melee Offensive Bonus, Ranged Offensive Bonus, General, Subterfuge, Perception, and Magical). The value of each of these is called a bonus, and can be less than zero.

You pick one of 9 character types (Hobbit Scout, Elf Scout, Human Warrior, Dwarf Warrior, Elf Warrior, Human Ranger, Half-elf Ranger, Human Bard, and Elf Bard), which sets your equipment and capability values and starting skill values, and then you have six +1 bonuses to add to your skills as you choose, no more than +3 to any one skill. If you have +1 or more in magic you get two spells per +1 bonus. Some of your equipment gives bonuses to your stats. I'll note that your character type doesn't restrict what you can do: if you want your Warrior to be able to use magic, put some of your starting skill bonuses into Magical! And they don't really have much to do with what your characters occupation is: someone who takes one of the Warrior character types can be a merchant, someone who takes one of the Scout character types can be a healer. It is all in how you assign your starting skill bonuses.

Your character type sets your Endurance (basically your hit points), with the dwarf warrior having the most at 60 points and the elf bard the least at 30 points.

There are 15 spells, all with a fairly reasonable balance between keeping with the magic seen in the novels, which is to say not tremendously powerful, and what one would expect from a fantasy RPG. The spells are Strength, Shield, Speed, Balance, Camouflage, Concentration, Item Analysis, Clairvoyance, Healing, Luck, Protection from Magic, Sustenance, Calm, Charm Animal, and Fire Bolt. Magic items typically add a plus to a stat, or let you do something you couldn’t before.

Maneuvers are how you use your stats. Some, like climbing a tree have a set difficulty. Others the GM sets the difficulty, from Routine (4) to Absurd (18).

You roll 2d6 plus a stat bonus versus a target number or an opponent’s roll. Meet or exceed an unopposed roll to succeed, while you can tie on opposed rolls. In combat you take the attacker’s Offensive Bonus minus the defenders Defensive Bonus and roll 2d6 and look the result up in a chart to see how much damage is done and whether the defender is knocked unconscious or killed outright.

Activities are things that are normally automatically successful if you have the equipment and time (tying up a captive, setting up camp, digging a ditch), but turn into GM moderated maneuvers if you don’t have the time or equipment (digging a ditch to hide in before the opponents you want to ambush show up).

There are 14 action sequences. These things like combat (one of the action sequences) where there is a general sequence you follow to do something, from sneaking through a town at night to tracking through a wilderness or ambushing an enemy. I like how these are written up as a standard sequence of things to do and consider, just like how combat is done in most system.

These action sequences can be adjusted by the GM for circumstances and they encourage you to make up your own. You might make one up for when someone is wanting to convince a crowd of something. They are multi-step procedures for doing something. I think they would be quite useful.

You get Experience Points (EP) for successful maneuvers depending on how hard the target number was, for every point of damage inflicted, with unconsciousness and killed results worth more, and for every point of damage the caster of a spell takes to cast the spell. (Every spell costs the caster Damage to cast!). You also get EP for good ideas and for the group accomplishing significant goals.

The included adventure is a mix of programmed sequences to help the new GM and players learn the system, where the choices the players make determines what section you turn to next, and the sections are broken up into action sequences and the results determine which section the players go to next, GM notes on how to run or adjust things, and descriptions of what happens.

I do notice that each of the pregens has a special ability: finding lost items or people, eidetic memory, knowing if any creature within a 20 foot radius is aligned with forces of darkness, healing wounds faster, being unusually skilled at bargaining and negotiation, and always knowing which way is north and can follow known routes perfectly. But there is no rule for assigning these to characters. I imagine that if the GM wanted to have special abilities for other characters the GM and the players should come up with them.

The rules encourage you to use figures or counters to represent characters in combat, and movement is given in inches, to be measured on the map, if you draw one, or on the table if you just set out the figures at the right distances. You could easily use a battle map with a grid, if you have one. Movement at a walk is 50 feet plus 10 feet multiplied by the characters Movement bonus.

Anyway, I like it. I hope to run it this year for Christmas, if all goes well.

There is, of course, a Wikipedia page about it, but it is even briefer than this post.

There is a compatible game, The Middle-Earth Adventure Game (MEAG), that was designed by Brian Gross and J.R. Gracen. I knew of it before I got the LotRAG, it seems to be a generalization and expansion, and thus a little more complicated, but it doesn't look too complicated. I'll read it and report what I think of it.

Maintaining the old ada-mode.el formerly distributed with Emacs

I wrote a post about using the old ada-mode.el that used to be distributed with Emacs because I couldn't get the newer, separate package version to work for me.

Well, this morning when I pulled up an Ada file in Emacs version 28 there were two problems:

  1. The information to invoke ada-mode on Ada files was not in the auto-mode-alist variable in Emacs. That was easy enough to fix: add .ada, .ads, and .adb to auto-mode-alist (and .gpr, too, since Ada mode works for gprbuild files as well):

    (cl-loop for ext in '("\\.gpr$" "\\.ada$" "\\.ads$" "\\.adb$")
      do (add-to-list 'auto-mode-alist (cons ext 'ada-mode)))
    

    That was easy enough.

  2. When I tried to do any indentation emacs reported an error, with the error message “End position is smaller than start position”. Eventually I tracked this down to a call to parse-partial-sexp in ada-in-open-paren-p. It turns out that somewhere after Emacs version 27.2 was released the Emacs developers added a check to parse-partial-sexp to ensure that the FROM argument (which indicates where in the buffer to start parsing) was less than the TO argument (which indicates where in the buffer to stop parsing).

Drat. Drat. Drat. Well, looking at the code it was clear that since ada-in-open-paren-p is explicitly searching backwards that TO would always be smaller than FROM. So I could just transpose the s-expressions that found those two values. I tried it, and it worked!

At that point I realized that I had committed to maintain the old version of ada-mode, at least for myself, and that I'd already talked about it on my blog and it was small step from there to setting up a GitHub repository with the old code, adding an issue describing the problem, adding a commit with the fix, and then writing this blog post.

Somewhere, someone is laughing and enjoying the schadenfreude. Maybe this will help someone else.

And since I already have a GitHub repository, I ought to document the first problem and since it is a documentation problem, put a mention in the README.

Demonicity, a Tri-Stat boxed game by Dyskami

I finished reading Dyskami Publishing’s Demonicity tonight (PDF). I liked the document design and found it easy to read, except the “Demon City” two page spread which I found a pain to read because of the body font. I liked the artwork; it was not particularly anime or manga like, unlike BESM Fourth Edition. There were a few typos, missing words, and other minor mistakes, but there was only one that hindered comprehension, and that one I figured out eventually from the context. The rules are easy to understand.

The setting of Demonicity is the titular “Demon City”, a city that is the location of an attack by demons from an different world/dimension. It is obviously inspired by their earlier licensed RPG, Demon City Shinjuku, which I quite liked.

Demonicity has a very interesting stripped down version of the Tri-Stat system that makes character generation and play much simpler. In some ways it reminds me of BESM First Edition, though it is even simpler. The three Stats are Body, Mind, and Soul, of course, so that’s the same. There are also Attributes, which are the special things a character can do. Stats can be determined randomly or assigned by the player, and they and Attributes always cost points. And there are some Derived Values: Combat, Health, Sanity.

You roll 2d6 and add a stat or a derived value (like Combat), plus optionally an Attribue’s level, and compare the result to a target number or an opposed roll. Things that make something easier are Edges: you get an extra die or two on your dice roll, and you keep the highest two. Things that make something harder, like trying to use two weapons at once without training, gives you Obstacles: an extra die or two on your dice roll, but you keep the two lowest rolls. Edges cancel out Obstacles and vice versa. Combat is theater of the mind.

Demoicity’s version of BESM’s Attributes are quite stripped down. There are only nine: Adaptability, Damage, Demonicity, Fighting, Knacks, Movement, Resistance, Resources, and Skills. These are bought in levels as usual, but associated with each are a number of Foci (as many as one has levels in the Attribute), which gives the character a special ability. Demonicity is the Attribute that allows a character to do most of the more impressive supernatural things, for instance. A few of its Foci are Healing, Meld (mere with inanimate objects), and Shadow Form. The Fighting Attribute makes a character more proficient at combat than their Stats would suggest, and it’s Foci specify which kinds of Fighting you are better at (Melee or Ranged), or makes it easier to do certain things in combat (Blind Fighting, Two Weapons). Generally, an Attribute’s level adds to dice rolls for things associated with it, while having an appropriate Foci means that you get an Edge. After every session characters get a few points to increase their Stats and Attributes.

The rulebook is only 32 pages long and has a thicker card stock cover. It is accompanied by six one page adventures with related art on the back and by six pre-generated characters with an image of the character on the back, all on card stock. There are also four six-sided dice. The box everything comes in seems sturdy. In general the production values are quite good.

People who are confident with their improvisation skills can probably run the adventures easily, but those who like more details will probably want to do a bit more preparation. The adventures appear designed to be easily linked into a six episode mini-campaign, or can form the backbone of a longer campaign if the GM writes their own adventures.

I like the inclusion of the pre-generated characters and the one page adventures. They allow a gaming group to pick the game and start playing with little or no preparation. And the simplified rules make it easy to learn the game, and to create characters if the gaming group doesn't want to use the pre-generated characters. I think this makes it an excellent pick-up-and-play game.

I would like to have seen something like the adventure generators from various Savage Worlds setting books or Toon and its supplements. They allow the GM who is stuck for ideas to come up with something quickly when inspiration is lacking. But it would have been difficult to fit in the constraints of the product. Perhaps on another sheet of card stock, since I can’t see deleting anything from the rulebook? BESM and Tri-Stat generally have very little procedural support for coming up with NPCs, scenes, adventures, etc., and I think an adventure generator would be a useful addition. Of course, having the example adventures to give an idea of what Demonicity adventures would be like is very useful.

All in all, I quite like Demonicity and hope to play it soon. My only reservations are about the price of this game and the other two Tri-Stat boxed games, Pixies and Wyrms. Forty-five retail dollars Canadian for the physical product seems like a lot. The PDF seems more reasonable at 15 dollars.

Splitting Strings on a Delimiter in the Ada Programming Language

Last edited: 2022-12-06 13:30:34 EST

When I did a search for “splitting strings on a delimiter in the Ada programming language” recently I did not get many useful results. Eventually I stumbled over GNAT.String_Split which is an instantiation of the generic package GNAT.Array_Split. I also finally found GNATCOLL.Strings_Impl and GNATCOLL.Strings, its default instantiation, which looks especially interesting, contains a split implementation, and which seems to be designed to be a more efficient string implementation than than Ada.Strings.Unbounded.

However, those are all a little complicated, so it might be appropriate to show a simpler implementation.

The String type in Ada is a array of characters. Once declared, a String variable always has the same length. That means that all the strings in an array of strings have to be the same length. However, an access (Ada's version of a pointer) to a String can point to a string of any length, so for this version we'll return an array of pointers to String.

Operations on String are defined in Ada.Strings.Fixed.

with Ada.Strings; use Ada.Strings;
with Ada.Strings.Fixed; use Ada.Strings.Fixed;
with Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure split_fixed is
   -- Ada.Text_IO contains a type, Count, that would conflict with
   -- the function Ada.Strings.Fixed.Count, so don't "use Ada.Text_IO;"
   -- instead, make a package the gives it a shorter name, and use all its
   -- procedures with that as the prefix.
   package ATIO renames Ada.Text_IO;
   type String_Ptr is access String;
   type Vector is array (Natural range <>) of String_Ptr;

   -- Allocate a new String in a storage pool, initializing it to S, and
   -- returning an access to it (a pointer).
   function "+" (Source : in String) return String_Ptr  is
      SP : String_Ptr := new String'(Source);
   begin
      return SP;
   end "+";

   function Split (S: String; Pattern: String) return Vector is
      Start: Positive := 1;
      Position: Natural;
      Num_Parts: Natural := Count (S, Pattern) + 1;
      V : Vector (1.. Num_Parts);
      I : Natural := 0;
   begin
      while Start <= S'Length loop
         Position := Index (S, Pattern, Start);
         exit when Position = 0;
         I := I + 1;
         V (I) := +S(Start..Position-1);
          -- The pattern can be longer than one character.
         Start := Position + Pattern'Length;
      end loop;
      I := I + 1;
      V (I) := +S(Start..S'Last);

      return V;
   end Split;

   procedure Print_Vector (Label: String; S: String; V: Vector) is
      N: Natural := 0;
   begin
      ATIO.Put_Line (Label & ": """ & S & """");
      for I in V'First .. V'Last loop
         N := N + 1;
         ATIO.Put ("    Part "); Put (N, 0); ATIO.Put (": """);
         ATIO.Put (V(I).all);
         ATIO.Put_Line ("""");
      end loop;
   end Print_Vector;

   S1: String := "Hello, World!|I am fine!|How are you?";
   V1: Vector := Split (S1, "|");
   S2: String := "";                    --  Empty string.
   V2: Vector := Split (S2, "|");
   S3: String := "|";                   --  Just one  of pattern.
   V3: Vector := Split (S3, "|");
   S4: String := "||";                  --  Just two of pattern.
   V4: Vector := Split (S4, "|");
   S5: String := "one";                 --  Just one part.
   V5: Vector := Split (S5, "|");
   -- The delimiter doesn't have to be one character.
   S6: String := "foo<=>bar";
   V6: Vector := Split (S6, "<=>");

begin
   Print_Vector ("S1", S1, V1);
   Print_Vector ("S2", S2, V2);
   Print_Vector ("S3", S3, V3);
   Print_Vector ("S4", S4, V4);
   Print_Vector ("S5", S5, V5);
   Print_Vector ("S6", S6, V6);
end split_fixed;

Here's the output:

S1: "Hello, World!|I am fine!|How are you?"
    Part 1: "Hello, World!"
    Part 2: "I am fine!"
    Part 3: "How are you?"
S2: ""
    Part 1: ""
S3: "|"
    Part 1: ""
    Part 2: ""
S4: "||"
    Part 1: ""
    Part 2: ""
    Part 3: ""
S5: "one"
    Part 1: "one"
S6: "foo<=>bar"
    Part 1: "foo"
    Part 2: "bar"

The Bounded_String type in Ada has a maximum capacity and a current length. You instantiate a new package for each different maximum capacity that you want, producing a different type for each. You can assign any string smaller than or equal to the maximum length, and the current length is recorded.

Operations on Bounded_String are defined in Ada.Strings.Bounded.

with Ada.Strings; use Ada.Strings;
with Ada.Strings.Bounded; use Ada.Strings.Bounded;
with Ada.Text_IO.Bounded_IO;
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure split_bounded is
   package B_String is new
     Ada.Strings.Bounded.Generic_Bounded_Length (Max => 128);
   use B_String;
   package B_String_IO is new Bounded_IO (B_String); use B_String_IO;

   type Vector is array (Natural range <>) of Bounded_String;

   function Split (S: Bounded_String; Pattern: String)
                  return Vector is
      Start: Positive := 1;
      Position: Natural;
      Num_Parts: Natural := B_String.Count (S, Pattern) + 1;
      V : Vector (1 .. Num_Parts);
      I : Natural := 0;
   begin
      while Start <= Length (S) loop
         Position := Index (S, Pattern, Start);
         exit when Position = 0;
         I := I + 1;
         V (I) := Bounded_Slice (S, Start, Position - 1);
          -- The pattern can be longer than one character.
         Start := Position + Pattern'Length;
      end loop;
      I := I + 1;
      V (I) := Bounded_Slice (S, Start, Length (S));

      return V;
   end Split;

   procedure Print_Vector (Label: String; S: Bounded_String; V: Vector) is
      N : Natural := 0;
   begin
      Put_Line (label & ": """ & S & """");
      for I in V'First .. V'Last loop
         N := N + 1;
         Put ("    Part "); Put (N, 0); Put (": """); Put (V(I));
         Put_Line ("""");
      end loop;
   end Print_Vector;

   S1: Bounded_String := To_Bounded_String ("Hello, World!|I am fine!|How are you?");
   V1: Vector := Split (S1, "|");
   S2: Bounded_String := To_Bounded_String ("");      --  Empty string.
   V2: Vector := Split (S2, "|");
   S3: Bounded_String := To_Bounded_String ("|");     --  Just one  of pattern.
   V3: Vector := Split (S3, "|");
   S4: Bounded_String := To_Bounded_String ("||");    --  Just two of pattern.
   V4: Vector := Split (S4, "|");
   S5: Bounded_String := To_Bounded_String ("one");   --  Just one part.
   V5: Vector := Split (S5, "|");
   -- The delimiter doesn't have to be one character.
   S6: Bounded_String := To_Bounded_String ("foo<=>bar");
   V6: Vector := Split (S6, "<=>");

begin
   Print_Vector ("S1", S1, V1);
   Print_Vector ("S2", S2, V2);
   Print_Vector ("S3", S3, V3);
   Print_Vector ("S4", S4, V4);
   Print_Vector ("S5", S5, V5);
   Print_Vector ("S6", S6, V6);
end split_bounded;

Here's the output:

S1: "Hello, World!|I am fine!|How are you?"
    Part 1: "Hello, World!"
    Part 2: "I am fine!"
    Part 3: "How are you?"
S2: ""
    Part 1: ""
S3: "|"
    Part 1: ""
    Part 2: ""
S4: "||"
    Part 1: ""
    Part 2: ""
    Part 3: ""
S5: "one"
    Part 1: "one"
S6: "foo<=>bar"
    Part 1: "foo"
    Part 2: "bar"

The Unbounded_String type in Ada grows dynamically as needed, but is not as time efficient as fixed strings or bounded strings. For this version, we'll use Ada.Containers.Vectors for a dynamically expending vector, rather than a fixed size vector.

Operations on Unbounded_String are defined in Ada.Strings.Unbounded.

with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_Io;
with Ada.Text_IO.Unbounded_IO; use Ada.Text_IO.Unbounded_IO;
with Ada.Containers.Vectors;
procedure split_unbounded is
   package Unbounded_String_Vectors is new
     Ada.Containers.Vectors (Natural, Unbounded_String);
   use Unbounded_String_Vectors;

   function "+" (Source : in String)
                return Unbounded_String renames To_Unbounded_String;
   subtype UBS_Vector is Unbounded_String_Vectors.Vector;

   function Split (S: Unbounded_String; Pattern: String)
                  return UBS_Vector is
      Start: Positive := 1;
      Position: Natural;
      Num_Parts: Natural := 0;
      V : UBS_Vector;
   begin
      while Start <= Length (S) loop
         Position := Index (S, Pattern, Start);
         exit when Position = 0;
         Append (V, Unbounded_Slice (S, Start, Position - 1));
          -- The pattern can be longer than one character.
         Start := Position + Pattern'Length;
      end loop;
      Num_Parts := Num_Parts + 1;
      Append (V, Unbounded_Slice (S, Start, Length (S)));

      return V;
   end Split;

   procedure Print_UBS_Vector (Label: String;
                               S: Unbounded_String;
                               V: UBS_Vector) is
      N : Natural := 0;
   begin
      Put_Line (Label & ": """ & to_string (s) & """");
      for I in V.First_Index .. V.Last_Index loop
         N := N + 1;
         Put ("    Part "); Put (N, 0); Put (": """); Put (V(I));
         Put_Line ("""");
      end loop;
   end Print_UBS_Vector;

   S1: Unbounded_String := +"Hello, World!|I am fine!|How are you?";
   V1: UBS_Vector := Split (S1, "|");
   S2: Unbounded_String := +"";         --  Empty string.
   V2: UBS_Vector := Split (S2, "|");
   S3: Unbounded_String := +"|";        --  Just one  of pattern.
   V3: UBS_Vector := Split (S3, "|");
   S4: Unbounded_String := +"||";       --  Just two of pattern.
   V4: UBS_Vector := Split (S4, "|");
   S5: Unbounded_String := +"one";      --  Just one part.
   V5: UBS_Vector := Split (S5, "|");
   -- The delimiter doesn't have to be one character.
   S6: Unbounded_String := +"foo<=>bar";
   V6: UBS_Vector := Split (S6, "<=>");

begin
   Print_UBS_Vector ("S1", S1, V1);
   Print_UBS_Vector ("S2", S2, V2);
   Print_UBS_Vector ("S3", S3, V3);
   Print_UBS_Vector ("S4", S4, V4);
   Print_UBS_Vector ("S5", S5, V5);
   Print_UBS_Vector ("S6", S6, V6);
end split_unbounded;

Here's the output:

S1: "Hello, World!|I am fine!|How are you?"
    Part 1: "Hello, World!"
    Part 2: "I am fine!"
    Part 3: "How are you?"
S2: ""
    Part 1: ""
S3: "|"
    Part 1: ""
    Part 2: ""
S4: "||"
    Part 1: ""
    Part 2: ""
    Part 3: ""
S5: "one"
    Part 1: "one"
S6: "foo<=>bar"
    Part 1: "foo"
    Part 2: "bar"