Tutorial 4.1 (Rules) - Aerll/rpp GitHub Wiki

About rules

Rules allow us to give the automapper a list of requirements, that the tile needs to meet, in order to be placed. Simply put, it's nothing more than a test. If the test passes, the tile is placed. If not, automapper moves on to the next rule.

Automappers execute all of the rules in order. There's no such thing, such as skipping a rule or choosing one rule out of many. Everything will be executed for every tile on the map, no matter what.

It's a really simple method, which does the job, but unfortunately it comes with a big drawback, it may lead to many problems and unexpected results. In r++, these are easier to deal with and some are even solved for you, but you can still fall for some traps. We will look at some of those in another part.

Insert

Insert is the body of a rule in r++. This is where we can tell the automapper what to place and when.

The structure of every rule is as follows:

  • index to insert
  • list of tests

And optionally:

  • chance
  • no default test

Let's start with the simplest rule we can make in r++:

Insert(1);

We've already seen it, but if you run this automapper, you'll notice something weird. It won't place tile 1 everywhere, but only on the tiles that aren't empty.

Why is that? By default, if we don't provide any test for position [0, 0], it will be created for us. Therefore, the above line is an equivalent of:

Insert(1).If(
    IndexAt([0, 0]).IsFull()
);

We can prevent r++ from creating this test by writing:

Insert(1).NoDefaultPosRule();

If you were to run this automapper, it would fill the entire map.

Another thing we can add to the rule is a chance. This will tell the automapper to place a tile with some probability, so that even when our test passes, the tile might not be placed.

Note: rules with probabilities shown in this chapter, won't work as you'd expect them to. This is to keep things simple. We will talk about how to use them correctly later on.

In r++ there are 2 methods supported. We can either uniformly distribute our tiles or use a percentage chance of our choice. Here's an example of both:

Insert(1, 2, 3).Roll(); // <- uniform distribution
Insert(1, 2, 3).Chance(1, 30, 69); // <- 1% for 1, 30% for 2, 69% for 3

Important thing to note, is that you can give any amount of values to Chance and they don't necessarily need to add up to 100. In case they exceed 100, they will be normalized. Also if you give it less values than the tiles, it will keep rolling back to the beginning of the list. Here's an example:

// less values
Insert(1, 2, 3).Chance(10); // <- 10% for 1, 2 and 3
Insert(1, 2, 3, 4, 5).Chance(5, 10); // <- 5% for 1, 10% for 2, 5% for 3, 10% for 4, 5% for 5

// values exceeding 100
Insert(1, 2, 3).Chance(40, 80, 80); // <- results in 20% for 1, 40% for 2 and 3

All of the functions mentioned above can be freely combined and swapped around, with an exception for Insert:

Insert(1, 2, 3).Chance(10).NoDefaultPosRule();
Insert(1, 2, 3).NoDefaultPosRule().Chance(10);

Both of these generate the same rule.

Important thing to note is that when we insert more tiles, everything including tests, chance and no default test, applies to each tile. For example:

Insert(1, 2, 3).If(
    IndexAt([0, 0]).IsFull()
).Chance(10).NoDefaultPosRule();

Will generate 3 separate rules for tiles 1, 2 and 3. They will all share the same test, probability and default test won't be created for any of them.

Another thing we can do, is we can simply insert a tile at a specific position:

Insert(1).At([0, 5]);

This will generate a special test, which places a tile 1 at position [0, 5]. Keep in mind, that it can't be combined with other tests. If you try to, they will be simply ignored.

Tests

Now that we have the optional things out of the way, we can focus on the most important part of every rule, which is If. This is where we're gonna be writing all of our tests. Tests are created using IndexAt and it looks like this:

Insert(1).If(
    IndexAt([0, 0]).Is(5).IsNot(5.V)
);

Here the automapper will test, whether a tile at position [0, 0] is 5, no matter its rotation. Then it will test, whether it's not 5 with V rotation. Therefore every tile 5, except for the ones with V rotation, will be replaced. This test will run for every tile on the map.

Positions given to IndexAt are relative to the automapped tile. What does this mean? Imagine you pick a random tile on the map. Position [0, 0] refers to that tile, [-1, 0] refers to the tile on the left, [1, 0] refers to the tile on the right, and so on.

IndexAt should only be used inside If. You can write as many of them as you want. They are separated with either , or and. We could write the above rule like this:

Insert(1).If(
    IndexAt([0, 0]).Is(5),
    IndexAt([0, 0]).IsNot(5.V)
);

Or even like:

Insert(1).If(
    IndexAt([0, 0]).Is(5)
and IndexAt([0, 0]).IsNot(5.V)
);

But in the end, it's up to your preference.

r++ provides plenty of such tests, for almost everything you will ever need. We'll talk about them more in the next part.

Masks

Now let's go back to Roll and Chance for a second. I mentioned before, that the examples above won't work. This is because of the way these functions work. If we write:

Insert(1, 2, 3).Chance(20, 30, 50);

You may think, that each rule will end up with exactly 20%, 30% and 50%, but it's not the case. Let's take the last value under the radar. Here Chance needs to compensate for the fact, that the previous 2 tiles already replaced half of the tiles (20% + 30% is 50%). This means it has only 50% of the tiles left to work with. Therefore, to fill a half of the whole (where there's only half left), this rule needs a 100% chance.
Now because we rely on a default test, we will never get to see tiles 1 and 2. After all, the default test will pass for all of our rules, meaning that the last rule with 100% chance will be always the one chosen.

To fix this, we need to use a mask, which is just simply any tile, that we don't use. In r++ we can use g:mask, which represents a tile 255.

Note: in r++, tile 255 is reserved for masks. You shouldn't use it for anything else.

Let's fix our rule then:

Insert(g:mask);
Insert(1, 2, 3).If(
    IndexAt([0, 0]).Is(g:mask)
).Chance(20, 30, 50);

Now because we check for a mask, we ensure that only one rule will pass the test and it will work as expected.

Example

This time we will create some more useful automapper. It will replace every tile with a random silver 1x1 unhook.

Let's start by writing down all of the indices:

int silver1 = 1;
int silver2 = 2;
int silver3 = 16;
int silver4 = 17;
int silver5 = 18;
int silver6 = 32;
int silver7 = 33;
int silver8 = 34;

For randomization we will utilize Roll, to get a nice uniform distribution. First we need to fill everything with a mask:

Insert(g:mask);

Finally we randomize our tiles over the mask we just placed:

Insert(silver1, silver2, silver3, silver4, silver5, silver6, silver7, silver8).If(
    IndexAt([0, 0]).Is(g:mask)
).Roll();

Full code

#include "base.r"
#output "generic_unhookable.rules"

int silver1 = 1;
int silver2 = 2;
int silver3 = 16;
int silver4 = 17;
int silver5 = 18;
int silver6 = 32;
int silver7 = 33;
int silver8 = 34;

AutoMapper("Rolling Stones");
NewRun();

Insert(g:mask);
Insert(silver1, silver2, silver3, silver4, silver5, silver6, silver7, silver8).If(
    IndexAt([0, 0]).Is(g:mask)
).Roll();
⚠️ **GitHub.com Fallback** ⚠️