Universal Map Generator

UMG is a scripting language dedicated to procedural generation of grid-based maps.

_images/screen1.png

It is based on a number of ‘generators’ that can be composed in a similar way to HTML or to how widgets are used to build UI applications.

When a UMG script is run, its output is a map in which each cell contains an ordered list of tokens. The interpretation of these tokens is up to the consumer of the map, typically they will represent some physical objects. Each time a script is run, it can generate a different, random map, with the degree of randomness dependent on the script’s logic.

The UMG software can also render an ASCII representation of the map, seen above, but it only serves as a quick preview of the output, with only the top token being shown.

Basics

The most basic UMG script fills the entire map with a single token:

Set("floor")

Note that the size of the map is not encoded in the script, but passed in to UMG when the script is run. Therefore the script can generate maps of various sizes, much like a HTML document adjusts itself to the size of the browser window.

Every script consists of a single top-level generator. In the above example, it is a single Set command, but a generator can also be formed by a number of sub-generators chained together, which are executed in that order.

{
  Set("floor")
  Set("wall")
  Remove("wall")
}

Many generators take sub-generators as arguments and execute them in sub-areas of the map. This script will surround a floor-filled level with a wall of width 1.

{
  Set("floor")
  Border(1, Set("wall"))
}

Here, Border is a generator that takes two arguments: the width of the border, and the generator that is executed on the border. So whatever map this script is executed on, it will place a wall on its outer-most cells.

It is possible to place a number of sub-generators of given sizes inside a map, which is useful for placing buildings or rooms.

Place(
  minSize = {3 3}
  maxSize = {6 6}
  generator = Border(1, Set("wood_wall"))
  count = 3
  minSpacing = 1
)

The above script will randomly place three wooden buildings with height and width between 3 and 6. Place always makes sure that its sub-generators don’t overlap.

Generators

None

Does nothing.

Set

Set("token1", "token2", "token3")

Sets given tokens. They are added to the back of the list of tokens at each cell.

Remove

Remove("token1", "token2", "token3")

Removes given tokens from all cells.

SetFront

SetFront("token")

Adds a given token to the front of the list of tokens.

Reset

Reset("token1", "token2", "token3")

Removes all previous tokens and sets given tokens.

Filter

Filter(Predicate, Generator, [GeneratorIfNot]) # Last argument is optional

Runs the given generator, but only on cells that satisfy Predicate. See Predicates

If GeneratorIfNot is present, it is run on all the rest of the cells.

Example:

Filter(On("floor"), Set("treasure"))

Margin

Margin(Width, BorderGenerator, InsideGenerator)
Margin([TOP, BOTTOM, LEFT or RIGHT], Width, BorderGenerator, InsideGenerator)

Takes two generators, one is run on the border of given width, the other on the rest of the area. With the Position argument present, the margin is present only on the given side.

Example:

Margin(TOP, 3, Set("water"),
  Margin(TOP, 10, Set("sand"), Set("grass"))

SplitH

SplitH(Ratio, LeftGenerator, RightGenerator)

Splits the area horizontally according to Ratio, and runs the corresponding generators.

SplitV

SplitV(Ratio, TopGenerator, BottomGenerator)

Splits the area vertically according to Ratio, and runs the corresponding generators.

Position

Position(
  [MIDDLE, LEFT_CENTER, RIGHT_CENTER, TOP_CENTER, BOTTOM_CENTER, MIDDLE_V, MIDDLE_H],
  Size,
  Generator,
  MinSize, # Optional. If Size is none, then it is randomly picked between MinSize and MaxSize
  MaxSize # Optional.
)

Places the given generator in the area in the chosen position. In the options MIDDLE_V and MIDDLE_H, the sub-area’s size is stretched vertically or horizontally to the whole area. In the other cases the size is fixed.

Example:

# Places a 10x10 lake in the middle
Position(MIDDLE, {10 10}, Set("lake"))

# Places a wall of width between 2 and 4 running vertically through the middle.
Position(
  MIDDLE_V,
  none,
  Set("wall"),
  {2 0},
  {4 0}
)

Place

Place(
  Size,
  Generator,
  Count,
  Predicate, # Optional.
  MinSize, # Optional. If Size is none, then it is randomly picked between MinSize and MaxSize
  MaxSize, # Optional.
  minSpacing # Optional.
)

Place(
  (---as above---),
  .
  .
  (---as above---)
)

Run the sub-generator at a given number of randomly picked areas that satisfy Predicate. The second version takes multiple sub-generators with identical arguments, enclosed in brackets.

Example:

# Fills the level with rock and places 3-7 rooms of size 5-10, a minimum of two tiles apart.
{
  Set("rock")
  Place(none, Reset("floor"), {3 7}, True, {5 5}, {10 10}, 2)
}
# Similar to the above, but also with an extra 4x4 lava lake.
Place(
  ({4 4}, Reset("lava"), 1, True)
  (none, Reset("floor"), {3 7}, True, {5 5}, {10 10}, 2)
)

NoiseMap

NoiseMap(
  (Lower, Upper, Generator),
  .
  .
  (Lower, Upper, Generator)
)

Generates a height map using Perlin noise and runs generators according to given altitude range. The total range spans from 0 to 1.

Example:

NoiseMap(
  (0.0, 0.3, Set("water")),
  (0.3, 0.4, Set("sand")),
  (0.4, 0.8, Set("grass")),
  (0.8, 1.0, Set("mountain))
)

Choose

Choose(
  [Probability1] Generator1,  # The probabilities are optional, defaults to uniform distribution
  .
  .
  [ProbabilityN] GeneratorN
)

Runs a randomly chosen generator among specified.

Example:

Choose(
  0.3 Set("water"),
  0.7 Set("lava")
)

Connect

Connect(
  ToConnectPredicate,
  Cost, EntryPredicate, EntryGenerator
)

Connect(
  ToConnectPredicate,
  (Cost1, EntryPredicate1, EntryGenerator1),
  .
  .
  (CostN, EntryPredicateN, EntryGeneratorN)
)

Runs a pathfinding algorithm to connect all of the grid cells specified by ToConnectPredicate. By default, the cost of traversing each cell is 1, but if the cell matches an EntryPredicateX, then it will be traversed with cost CostX, and EntryGeneratorX will be executed on that cell.

Example:

Fills the map with rock; places a 10x10 lake in the middle, and five 4x4 rooms; connects the rooms with cost of digging the rock equal to two, and cost of crossing the water equal to five.

{
  Set("rock")
  Position(MIDDLE, {10 10}, Set("water"))
  Place(5, Reset("floor"), Not On("water"), {4 4})
  Connect(On("floor"),
    (2, On("rock"), Reset("corridor")),
    (5, On("water"), Reset("bridge"))
  )
}

Repeat

Repeat(N, Generator)

Runs Generator N times.

FloodFill

FloodFill(Predicate, Generator)

Starting from cells matching Predicate, flood-fills the entire map, and runs Generator on the filled tiles.

Predicates

A predicate returns either true or false for a given cell.

On

On("token")

Returns true, if cell contains token.

Not

Not Predicate

Negates the result of Predicate.

True

Always returns true.

And

And(Predicate1, ..., PredicateN)

Returns true, if all sub-predicates return true.

Or

Or(Predicate1, ..., PredicateN)

Returns true, if at least one sub-predicate returns true.

Chance

Chance(Probability)

Returns true randomly, according to Probability.

Area

Area(Radius, Predicate, [MinCount]) # MinCount is optional, by default set to 1.

Returns true if at least MinCount cells within Radius satisfy Predicate.

Example:

Runs a cellular automaton algorithm according to http://www.roguebasin.com/index.php?title=Cellular_Automata_Method_for_Generating_Random_Cave-Like_Levels.

{
  Filter(Chance(0.45), Set("cell_added"))
  Repeat(5, {
    Filter(Area(1, On("cell_added"), 5), Set("cell_added2"))
    Filter(On("cell_added2"), Set("cell_added"), Remove("cell_added"))
    Remove("cell_added2")
  })
}

Macros

Macros are a useful way to organize and reuse code. You can find a short tutorial here: https://pastebin.com/tHDtvwzx.

Using UMG in KeeperRL

It is possible to use UMG to design new maps and level in KeeperRL. The game uses a mapping to translate from tokens to in-game level generator actions, such as placing furniture, items or creatures.

The generators are defined in the file random_layouts.txt, for example:

"village" {
  Set("grass")
  ...
}

"castle" {
  Margin(1, Set("castle_wall"), Set("castle_floor"))
  Place({1 1}, Choose(Set("sword"), Set("potion")), {5 10})
  ...
}

The mapping is defined in layout_mapping.txt, for example:

"default_mapping" {
  "grass" Place "GRASS"
  "castle_wall" Place "CASTLE_WALL"
  "castle_floor" Place "FLOOR_STONE1"
  "sword" Items 1 "Sword"
  "potion" Items 1 Potion Heal FLESH
}

You can use your layout for an enemy, by referencing it in enemies.txt. For example:

"ORC_VILLAGE"
{
  settlement = {
    type = RandomLayout { "village" {20 15} "default_mapping" }
    ...
  }
  ...
}

You can use multiple mappings to reskin your levels in various ways.

"super_level_mapping" inherit "default_mapping" {
  "castle_wall" Place "MY_SUPER_WALL"
  "sword" Items 1 "MySuperSword"
}
"SUPER_VILLAGE"
{
  settlement = {
    type = RandomLayout { "village" {20 15} "super_level_mapping" }
    ...
  }
  ...
}

Testing layouts using the command line

If you open cmd.exe on Windows or a terminal window on Mac or Linux, you can generate a chosen layout in KeeperRL and draw its ASCII representation. For example:

./keeper --layout_name "castle" --layout_size 20:15

Your tokens must be defined in the file data_free/glyphs.txt, otherwise they will not be rendered.

Example glyphs.txt:

"grass" ~ green
"castle_wall" # gray
"castle_floor" . gray
"sword" ) blue
"potion" ! red