if, else if, nested if, "block" ifs: a tutorial

Doc requests, organization, and submissions

Moderators: dorpond, trevor, Azhrei

User avatar
Mathemagician
Dragon
Posts: 666
Joined: Tue May 22, 2007 2:27 pm

if, else if, nested if, "block" ifs: a tutorial

Post by Mathemagician »

Important: In build 1.3.b38, the == operation is nonfunctional, but has been fixed for b39. Also, in 1.3.b38, strings are left mangled, losing their first and last character. This has also been fixed for b39, I understand.

I think this is a macro trick that will be of general interest to the community, and I didn't want to clog up the announcements thread any more (where people might not see it anyway).

With build 1.3.b38, we were given the ability to use an "if" command. If you are familiar with other programming languages, you should recognize "if" as a ternary operator: if(condition,then,else). This is VERY different from
if ( condition)
{ do stuff}
else
{do other stuff},

Before we get to some examples, we should remind ourselves of the basic boolean things. There's a HUGE difference in MapTool between "=" and "==". "=" is the assignment operator, you're familiar with that for putting numbers into variable names. "x=2" stores 2 into x. "==" on the other hand, asks a question, "does this equal that?" That is, "x==2" asks "is x equal to 2?"

Similarly, "x>2" asks "is x greater than 2?"

You cannot put assignments (=) in the "then" or "else" portions of "if(condition,then,else)" So you have to do the assignment before the word if.

So if wanted to emulate the code:

Code: Select all

if (x>y)
   x = x*x
else
   x = x-1
You CANNOT write:

Code: Select all

{if(x>y, x=x*x, x=x-1)}

You would write instead:

Code: Select all

{x = if( x > y, x*x, x-1)}

Notice that the condition is x > y, and if that is true (we want to use the "then" portion) x = x*x, but if x <= y, we should use the else portion, so x = x-1. So if x is 2, and y is 1, we will find that after this line is run, x = 4, y = 1. However, if x = 1, y = 2, after this code is run, x = 0, y = 2.

Sometimes, you want your code to "do nothing" in the case of the else:

So if you wanted to emulate this code:

Code: Select all

if(x>y)
  x = x*x
You would write:

Code: Select all

{x= if(x > y, x*x, x)}
The rationale being that if x<= y, we don't change x, that is: x=x.

Let's now graduate to a slightly more sophisticated code:
Suppose we want to emulate a code like this, where we change x in the "if" but change y in the "else":

Code: Select all

if (x > y)
   x = x*x
else
   y = y*y
There are of course, many ways to approach this, but looking forward to more complicated codes, I'll propose this method. We'll have a variable "Control" which will tell us which part of the if we're in. This will become useful later, I feel.

Code: Select all

{Control = if(x > y, 1 , 0)}
{x = if(Control==1, x*x, x)}
{y = if(Control==0, y*y, y)}
So Control is set to 1 if x> y, or Control is 0 if x <= y.
Any lines having if(Control==1,...) can be thought of as "inside" the if block.
Any lines having if(Control==0,...) can be thought of as "inside" the else block.

This approach allows us to cleanly approach "block if" codes.

Suppose we now want to do:

Code: Select all

if (x > y)
   x = x-1
   y = x*x
else
   y = y+2
   x = y
I propose the following macro code:

Code: Select all

{Control = if(x>y,1,0)}
{x = if(Control==1,x-1,x)}
{y = if(Control==1,x*x,y)}
{y = if(Control==0,y+2,y)}
{x = if(Control==0,y,x)}
The repeated (Control==1) tells us "okay, this line of code is only if x >y", notice how the false conditions are always keeping x=x, or y=y.

Now we can move up one more level of sophistication, we can do nested-ifs by using two different "control variables".

Suppose we want to emulate this code:

Code: Select all

if (x>y)
   x = x-1
   if (x>y)
      y = x*x
   else
      y = y+1
else
    y = y*2
I propose this solution:

Code: Select all

{Control1=if(x>y,1,0)}
{x = if(Control1==1,x+1,x)}
{Control2=if((Control1==1)&&(x>y),1,0)}
{y = if((Control1==1)&&(Control2==1), x*x,y)}
{y = if((Control1==1)&&(Control2==0), y+1,y)}
{y = if(Control1==0,y*2,y)}
The repeated Control1s are very important here. Remember && means "and," which means BOTH have to be true. (true && true = true, true&&false = false). So Control1==1 means we're inside the first if statement, and then by adding &&(Control2==1) it means we're inside the nested-if, while &&(Control2==0) means we are inside the nested else (my god, I typed nested elf...too much D&D).

One very very very important thing to be careful about:

It is tempting to collapse these two lines

Code: Select all

{y = if((Control1==1)&&(Control2==1), x*x,y)}
{y = if((Control1==1)&&(Control2==0), y+1,y)}
to one line:

Code: Select all

{y= if((Control1==1)&&(Control2==1),x*x,y+1)}
But there is a very significant error with this! If Control1==0, (that is we never should have seen the nested if!) you'll update y, when you never should have. So I think it is best to use these (Control==1) (Control==0)s to identify different blocks of code.

So now let's tackle the problem of "else-if." While it's possible to think of "else-if" as an if statement nested in an else statement, that can get messy. As always, I think an example is the easiest way to illuminate my proposed technique:

Suppose we want to emulate a "switch" styled else-if:

Code: Select all

if (x==3)
   x = x*2
else if (x==5)
  x = x*x
else if (x==7)
  x = x-2
else
  x = -1
We'll approach this by using just one control variable:

Code: Select all

{Control=if(x==3,1,0)}
{Control=if((Control==0)&&(x==5),2,0)}
{Control=if((Control==0)&&(x==7),3,0)}
The above code sets Control to 1 if x is 3, Control to 2 if x is 5, control to 3 if x is 7, and all other values of x will set control to 0. Now, we can use these different values of Control to coordinate what "else if" block we are in.

Code: Select all

{x=if(Control==1,x*2,x)}
{x=if(Control==2,x*x,x)}
{x=if(Control==3,x-2,x)}
{x=if(Control==0,-1,x)}
I should note that using variables like "Control" across multiple lines is much safer than repeating the condition statement, because every line of the code is always executed, unlike traditional programming where some lines get skipped. For instance:

Code: Select all

if (x>y)
  x = x*x
else
  y = y*2
It would be INCORRECT to implement the above code as:

Code: Select all

{x = if(x>y, x*x,x)}
{y = if(x<=y,y*2,y)}
Because what if x = .5, y = .3?
The first line asks, "is .5 > .3?" it is, so the new value of x is x = .5*.5 = .25 . Ahh, but now, we come to the second line, which "has no knowledge" that the first line ran (and so according to the code we are trying to emulate, this line "shouldn't be run"). Even though the condition for the second line is the logical opposite of the first one, since x has been updated by our code, they are no longer "exclusive." That is, now .25 < .3 is certainly true, so we change y, even though we weren't supposed to. This is why you see this "Control" variable all over my code. It may also be that I'm a control freak?

Anyway, the correct way to implement that code, again, would be:

Code: Select all

{Control=if(x>y,1,0)}
{x=if(Control==1,x*x,x)}
{y=if(Control==0,y*2,y)}
Remember if you use this approach to if-then-else/etc, your if statement should (almost always) "do nothing" to the variable in the event that the condition is false.


I have one more trick to share. Recently it was discovered (by BigO, I think...correct me if I'm wrong), that a cute way of hiding output is to enclose things in <!-- {code here} --> . We'll be using this trick to show how one might produce output inside of if-blocks, without displaying every line of possible output :). Due to an oddity in the parser, you'll be seeing this sort of nonsense a lot "'"+variable+"'". Do not be alarmed. That's a quotation mark, apostrophe, quotation mark. It lets us tell Maptool to store variable as a string (or if you wrote "Stuff", it treats that as a string). Please note that:
OutputString = "'"+OutputString+"'" is the string equivalent of "x=x"

So let's say we want to emulate this code:

Code: Select all

if (x>y)
   Hello Cruel World!
else if (x==y)
   Hello well balanced World!
else
   Goodbye Cruel World!
I would write:

Code: Select all

<!-- {Control = if(x>y,1,0)}
{Control = if((Control==0)&&(x==y),2,0)}

{OutputString=' '}
{OutputString=if(Control==1,"'"+"Hello Cruel World"+"'", "'"+OutputString+"'")}
{OutputString=if(Control==2,"'"+"Hello well balanced World" +"'","'"+OutputString+"'")} 
{OutputString=if(Control==0,"'"+"Goodbye Cruel World"+"'", "'"+OutputString+"'")}
 -->{OutputString}
Please remember, if you are quoting any of this code, to click on the "disable HTML in this post" check box.


Your questions and comments are encouraged!
Last edited by Mathemagician on Tue Jul 29, 2008 6:50 pm, edited 6 times in total.

User avatar
UntoldGlory
Great Wyrm
Posts: 1649
Joined: Sun Mar 16, 2008 8:12 pm

Post by UntoldGlory »

Very well illustrated examples! This post is going to save me a LOT of banging my head. Can we maybe get this stickied? At least till the parser changes again?

User avatar
RPTroll
TheBard
Posts: 3159
Joined: Tue Mar 21, 2006 7:26 pm
Location: Austin, Tx
Contact:

Post by RPTroll »

You might want to put this into the docs as well changing it as needed for newbies.

User avatar
jfrazierjr
Deity
Posts: 5176
Joined: Tue Sep 11, 2007 7:31 pm

Post by jfrazierjr »

UntoldGlory wrote:Very well illustrated examples! This post is going to save me a LOT of banging my head. Can we maybe get this stickied? At least till the parser changes again?
You mean next week? :roll:
I save all my Campaign Files to DropBox. Not only can I access a campaign file from pretty much any OS that will run Maptool(Win,OSX, linux), but each file is versioned, so if something goes crazy wild, I can always roll back to a previous version of the same file.

Get your Dropbox 2GB via my referral link, and as a bonus, I get an extra 250 MB of space. Even if you don't don't use my link, I still enthusiastically recommend Dropbox..

User avatar
Mathemagician
Dragon
Posts: 666
Joined: Tue May 22, 2007 2:27 pm

Post by Mathemagician »

Updated the original post to include else-ifs, and a (oh so slightly) better introduction.

User avatar
BigO
Dragon
Posts: 558
Joined: Mon Jul 28, 2008 12:23 pm
Location: Oshkosh, WI
Contact:

Post by BigO »

Some important caveats about the examples in the parent post (and forgive me if i get something wrong, I don't have maptool in front of me):

1) Note that in the build mentioned, 1.3.b38, the "==" evaluation is broken, so statments like:

Code: Select all

if(Control==0,y,x)
Will not work. If I understand correctly, this has already been fixed and will be in the next build.

2) A lot of strings found in macros are getting chopped. A string of "Hello World" might display as "ello Worl". To get around this just pad the string to be " Hello World " so that it cuts the whitespace. This is also a known issue that is due to be fixed in the next build.

3) Also note that the parent post didn't include any controls for output formatting, which is fine but can be confusing if you don't know how it normally works. Pretty much any time an assignment happens, or a variable name is used there is going to be something written to the screen. For example, if you did this:

Code: Select all

{x = 5}
{y = 6}
{x = x + y}
{x}
And were expecting the x at the end to just spit out the value of 11, then you'd be surprised when you saw these results:

Code: Select all

5
6
11
11
What's worse is if you had these on the same line. Doing: {x = 5}{y = 6}{x = x + y}{x} would get you this:

Code: Select all

561111
There's more than one way to get around this. The most common thing I do is use an html comment like so:

Code: Select all

<!-- {x=1d20} -->
After doing that you would have a variable named x that you could work with and the assignment of it would not have shown in the chat window (it WOULD be in the html behind the scenes though, so players can still find it if they export the chat and know how to read html).

Another thing I do a lot is put an empty string in the 'else' of an if statement, and I'll illustrate that:

Code: Select all

<!-- {x=1d20} -->{if(x>15, " Hit! ", ""}
With this code you would see Hit! output if the roll was 16 or higher, but nothing else would show up if it's lower.

A lot more macro code is going in the next build so we should revisit this then.

[EDIT: parent post has been updated and made most of this post obsolete :)]

--O
Last edited by BigO on Tue Jul 29, 2008 6:32 pm, edited 1 time in total.

User avatar
Mathemagician
Dragon
Posts: 666
Joined: Tue May 22, 2007 2:27 pm

Post by Mathemagician »

One more update including { } around my codes, and including output inside if-then-elses. Thanks BigO (even if I made the update while you were making your post, I appreciate it!). Good point about the ==s and strings being broken.

User avatar
Full Bleed
Demigod
Posts: 4736
Joined: Sun Feb 25, 2007 11:53 am
Location: FL

Post by Full Bleed »

Mathemagician, thanks for taking this kind of tutorial on. MT could use a lot more of this sort of thing.

I hope you expand this to include some actual example of Macros in use (like BigO was doing) as I think people will be able to follow things even better then.

The more system independent you can stay in your examples the more useful it will be.

Ultimately, I think it will be a lot easier for people to edit existing, relatively generic macros to suit their own needs than it will be for them to build from scratch.

User avatar
BigO
Dragon
Posts: 558
Joined: Mon Jul 28, 2008 12:23 pm
Location: Oshkosh, WI
Contact:

Post by BigO »

Full Bleed wrote:Mathemagician, thanks for taking this kind of tutorial on. MT could use a lot more of this sort of thing.

I hope you expand this to include some actual example of Macros in use (like BigO was doing) as I think people will be able to follow things even better then.

The more system independent you can stay in your examples the more useful it will be.

Ultimately, I think it will be a lot easier for people to edit existing, relatively generic macros to suit their own needs than it will be for them to build from scratch.
Yeah, I agree that there is a big need for that and I definitely want to help with the effort. I would have started already but the macro code is currently EXTREMELY volatile (meaning it's changing almost daily). I'm going to wait until the biggest bugs are fixed and the biggest features are done, which at this rate will be next week. :)

Another thing I want to do when it settles down is have a thread where people share macros with each other. Build a repository of sorts.

--O

User avatar
TK
Giant
Posts: 162
Joined: Fri Jun 27, 2008 12:02 am

Re: if, else if, nested if, "block" ifs: a tutoria

Post by TK »

Mathemagician wrote: My gripe with the above code is that it produces a lot of blank lines... and then the one line of desired output.
Assuming that Maptools parses comments correctly, could you not simply make the following modification?

Code: Select all

<!-- {Control = if(x>y,1,0)} 
 {Control = if((Control==0)&&(x==y),2,0)} 

 {OutputString=' '} 
 {OutputString=if(Control==1,"'"+"Hello Cruel World"+"'", "'"+OutputString+"'")} 
 {OutputString=if(Control==2,"'"+"Hello well balanced World" +"'","'"+OutputString+"'")} 
 {OutputString=if(Control==0,"'"+"Goodbye Cruel World"+"'", "'"+OutputString+"'")} -->

{OutputString}
Essentially, just making 1 big comment rather than several smaller comments separated by new line characters?

/* No Comment */

User avatar
BigO
Dragon
Posts: 558
Joined: Mon Jul 28, 2008 12:23 pm
Location: Oshkosh, WI
Contact:

Re: if, else if, nested if, "block" ifs: a tutoria

Post by BigO »

Mathemagician wrote:

Code: Select all

<!-- {Control = if(x>y,1,0)} -->
<!-- {Control = if((Control==0)&&(x==y),2,0)} -->

<!-- {OutputString=' '} -->
<!-- {OutputString=if(Control==1,"'"+"Hello Cruel World"+"'", "'"+OutputString+"'")} -->
<!-- {OutputString=if(Control==2,"'"+"Hello well balanced World" +"'","'"+OutputString+"'")} -->
<!-- {OutputString=if(Control==0,"'"+"Goodbye Cruel World"+"'", "'"+OutputString+"'")} -->

{OutputString}
My gripe with the above code is that it produces a lot of blank lines... and then the one line of desired output.
Please remember, if you are quoting any of this code, to click on the "disable HTML in this post" check box.
It doesn't have to, just use less comments:

Code: Select all

<!-- {Control = if(x>y,1,0)}
{Control = if((Control==0)&&(x==y),2,0)}

{OutputString=' '}
{OutputString=if(Control==1,"'"+"Hello Cruel World"+"'", "'"+OutputString+"'")}
{OutputString=if(Control==2,"'"+"Hello well balanced World" +"'","'"+OutputString+"'")}
{OutputString=if(Control==0,"'"+"Goodbye Cruel World"+"'", "'"+OutputString+"'")} -->
What was probably happening is that on each of those new lines the chat parser was auto inserting a <br> tag, but with all of that in one comment it wouldn't matter.

--O

User avatar
Mathemagician
Dragon
Posts: 666
Joined: Tue May 22, 2007 2:27 pm

Post by Mathemagician »

Thanks TK, BigO. Fixed it up ;)

User avatar
UntoldGlory
Great Wyrm
Posts: 1649
Joined: Sun Mar 16, 2008 8:12 pm

Post by UntoldGlory »

Okay, lemme see if I've got this in a real life example. It doesn't work right now but I think that's cause of the == bug.


Bear in mind that "WeaponUsed" is a property I'm adding to my sheet, and my players will just update it to be weapon 1 (primary on hand), weapon 2 (ranged), or weapon 3 (secondary or off hand).

Code: Select all

<!-- {Attack = 0}
{Attack = if(WeaponUsed==1,d20 + StrBonus + LevelBonus + Weapon1Bonus + MiscAttBonus,Attack)}
{Attack = if(WeaponUsed==2,d20 + StrBonus + LevelBonus + Weapon2Bonus + MiscAttBonus,Attack)}
{Attack = if(WeaponUsed==3,d20 + StrBonus + LevelBonus + Weapon3Bonus + MiscAttBonus,Attack)} --!>

<b> Cleave -  Fighter Attack 1</b>
<b> At-Will,  Martial, Weapon</b>
<b>Standard Action</b>, Melee weapon
<b>Target:</b> One creature
<b>Attack:</b> Str vs. AC
<b>Attack</b>: {Attack}
<b>Hit:</b> [Weapon1Damage + StrBonus + Weapon1DamBonus + MiscDamBonus], and an enemy adjacent to you other than the target takes damage equal to [StrBonus]

Thanks guys!

User avatar
BigO
Dragon
Posts: 558
Joined: Mon Jul 28, 2008 12:23 pm
Location: Oshkosh, WI
Contact:

Post by BigO »

UntoldGlory wrote:

Code: Select all

<!-- {Attack = 0}
{Attack = if(WeaponUsed==1,d20 + StrBonus + LevelBonus + Weapon1Bonus + MiscAttBonus,Attack)}
{Attack = if(WeaponUsed==2,d20 + StrBonus + LevelBonus + Weapon2Bonus + MiscAttBonus,Attack)}
{Attack = if(WeaponUsed==3,d20 + StrBonus + LevelBonus + Weapon3Bonus + MiscAttBonus,Attack)} --!>
The == is a problem, yes, but there's something else too. You're trying to roll a die inside the IF statement, and that doesn't work. You'd have to do something like this (and I'll put in a work around for the == too):

Code: Select all

{AttackRoll = d20}{Attack = AttackRoll + if(WeaponUsed<2 && WeaponUsed>0, StrBonus + LevelBonus + Weapon1Bonus + MiscAttBonus,0)}
Etc.

--O

User avatar
Mathemagician
Dragon
Posts: 666
Joined: Tue May 22, 2007 2:27 pm

Post by Mathemagician »

BigO wrote:
UntoldGlory wrote:

Code: Select all

<Attack>
The == is a problem, yes, but there's something else too. You're trying to roll a die inside the IF statement, and that doesn't work. You'd have to do something like this (and I'll put in a work around for the == too):

Code: Select all

{AttackRoll = d20}{Attack = AttackRoll + if(WeaponUsed<2>0, StrBonus + LevelBonus + Weapon1Bonus + MiscAttBonus,0)}
Etc.

--O
What do you mean rolling a die inside an if statement doesn't work?

{if(1>0,2d20+20,1d4-10)} and the like seem to work just fine..

Post Reply

Return to “Documentation Requests/Discussion”