Coder Social home page Coder Social logo

What to do about conditionals about rpgdice HOT 11 CLOSED

mrfigg avatar mrfigg commented on August 24, 2024
What to do about conditionals

from rpgdice.

Comments (11)

Morgul avatar Morgul commented on August 24, 2024

I started to formalize the case ... of proposal, but I ended up realizing it doesn't work unless you can declare variables inside of the grammar, and I'd like to avoid that. Instead, I'm going to start with a more traditional proposal...

If/Then/Else

Conditional: Expression BooleanOp Expression [...BooleanOp]

if Conditional then Expression else Expression

Example:

if 3d6 + 1 < 3 then 2d6 + 1 else 3d8 + 12

A Conditional, for this discussion, always evaluates to a Boolean, and is the result of a BooleanOp (ex: and, or, <, >=, !=, etc.) A BooleanOp, on the other hand, is an operator that always returns a Boolean. They are not allowed outside of the conditional part of an if ... then ... else statement.

Some considerations: without making the else required, we would have to decide how the entire if ... then evaluates in the case the condition isn't met. Additionally, where in the grammar would we allow conditionals? Only top level? Anywhere? Things could get very complicated depending on what we decide.

Given these, I propose the following rules:

  • The else block must always be present.
  • The Conditional must always be a Boolean. (Casts are handled by the BooleanOp)
  • The Expression portion of the then and else blocks must always evaluate to a Number.
  • Any BooleanOps must always have well defined casting:
    • 0 -> false and true = !false. (Any number other than 0 is true.)
  • if ... then ... else must always be allowed where ever an Expression is allowed by the Grammar.
  • if ... then ... else must not be evaluated by Variable.

Examples

if attackBonus > 5 then
    2 * (3d8 + [Weapons.Longsword.Damage])
else
   3d8 + [Weapons.Longsword.Damage]
if if if if 1d6 > 1 then 2d6 else 3 then 1d6 +1 else -7 then 3(3(3d12 + 7) + 9) else 1d2 + 1 then 2 else 3
3d10 + [Attrs.StrMod] + if [Attrs.DexMod] > 2 then [Attrs.DexMod] else 0

Additional Thoughts

Seeing this on paper, I don't hate it nearly as much. I think, if we implement this, I would like to also support the ternary Condition ? Expression : Expression syntax, since it's more succinct, and should follow exactly the same rules as if ... then ... else.

from rpgdice.

Morgul avatar Morgul commented on August 24, 2024

Here's another proposal that, I think we could support without changes to the grammar.

Conditional Functions

Conditional: Expression BooleanOp Expression [...BooleanOp]
ThenExpr: Expression
ElseExpr: Expression

if(Condition, ThenExpr, ElseExpr)

Example:

if(3d6 + 1 < 3, 2d6 + 1, 3d8 + 12)

The Conditional portion would be cast to a Boolean, no matter what is passed in, meanwhile ThenExpr and ElseExpr would throw a TypeError if it doesn't return a Number.

A basic implementation of this could be:

function if(cond, thenExpr, elseExpr)
{
    cond = cond.eval();
    if(cond.value === 0)
    {
        return thenExpr.eval();
    } // end if

    return elseExpr.eval();
} // end if

I think that would work the way we want, and should be pretty easy to test with. While I'm not sold on how this feels as a part of the syntax, this could be tenable. I would still want to implement the different BooleanOps from above.

Examples

if(
    attackBonus > 5, 
    2 * (3d8 + [Weapons.Longsword.Damage]), 
    3d8 + [Weapons.Longsword.Damage]
)
if( if( if( if( 1d6 > 1, 2d6,  3), 1d6 +1, -7) 3(3(3d12 + 7) + 9), 1d2 + 1), 2, 3)
3d10 + [Attrs.StrMod] + if([Attrs.DexMod] > 2, [Attrs.DexMod], 0)

from rpgdice.

Morgul avatar Morgul commented on August 24, 2024

I've almost convinced myself of my first proposal, to be honest. The function version is ugly.

from rpgdice.

mrfigg avatar mrfigg commented on August 24, 2024

I'm in favor of your If/Then/Else proposal and am willing to help implement it as is, I'm just going to have to put some thought into how to implement the peg parse tree. I think it's going to require two different major branches, one simpler branch for handling only math and one expanded branch for handling both BooleanOps and math. Also, just to float the idea, should we only use ... ? ... : ... and not if ... then ... else as they are functionally the same, and the readme seems to indicate this module is going for a javascript-like syntax rather than an, I don't know..., python-like one? And we wouldn't have to worry about any confusion between the conditional and variables/functions. Just a thought/personal preference, feel free to ignore.

Alternatively... preemptive type coercion

There is also an alternative to implementing If/Then/Else as is that, while a bit unorthodox, would allow us to keep a unified parse tree. I'm not sure I am going to be able to explain this one well, so I apologize in advance. We could have every op that would return a boolean preemptively use type coercion to return a number instead, because it's the only value type we want to be dealing with anyway. Then every op that needs to test for a boolean would check if(value !== 0) for true or if(value === 0) for false. This means these ops won't break any math if they are used unexpectedly outside of a conditional.

Examples

Given that >= would be:

{
  greaterThanOrEqual:
  {
    symbol: '>=',
    evalValue: (left, right) =>
    {
      return left.value >= right.value ? 1 : 0;
    }
  }
}

And that ... ? ... : ... would be something along the lines of:

eval(scope, depth = 1)
{
  scope = defaultScope.buildDefaultScope(scope);

  this.condition = this.condition.eval(scope, depth + 1);
  this.then = this.then.eval(scope, depth + 1);
  this.else = this.else.eval(scope, depth + 1);

  if(this.condition.value !== 0)
  {
    this.value = this.then.value;
  }
  else
  {
    this.value = this.else.value;
  }
}

We can handle the expected use:
(1d20 + 5) >= enemyArmor ? 2d8 + 3 : 0

As well as:
max(1d100 - chancePenalty, 0) ? 2d8 + 3 : 0

And then even be able to handle someone doing something unexpected:
((1d20 + 5) >= enemyArmor) * (2d8 + 3)

I admit that last one is not very intuitive, but I'd argue the input for that last one isn't intuitive either, so it's not our fault. At least we're still able to handle it. And this should still feel very similar to javascript as you can do some weird things like true * 5 = 5, false * 5 = 0, and true + true + false = 2 already anyway.

I do feel this idea could still be pretty out there, so I understand if it's too far of a stretch.

from rpgdice.

mrfigg avatar mrfigg commented on August 24, 2024

Actually now that I've slept on it I realize it should be possible to leave the BooleanOps in the normal peg tree and to just let javascript handle type coercion natively. This would mean that a user could get a root expression with a boolean value if they did rpgdice.eval('1d20 + 5 >= enemyArmor', {...}), but that's what they're asking for.

Rereading your proposal I guess I just don't see the advantage of these two points;

  • The Conditional must always be a Boolean. (Casts are handled by the BooleanOp)
  • The Expression portion of the then and else blocks must always evaluate to a Number.

Ultimately I'm arguing in favour of the flexibility javascript type coercion allows.

from rpgdice.

Morgul avatar Morgul commented on August 24, 2024

Responding in order:

Ternary vs IfThenElse

I kinda want both, if ... then ... else and ... ? ... : ..., but for what might be a silly reason; I hate the look of multiline expressions with ternary:

attackBonus > 5 
    ? 2 * (3d8 + [Weapons.Longsword.Damage])
    : 3d8 + [Weapons.Longsword.Damage]

But, this is both not javscript, and involves more typing:

if attackBonus > 5 then 2 * (3d8 + [Weapons.Longsword.Damage]) else 3d8 + [Weapons.Longsword.Damage]

(In fact, I've lifted if ... then ... else ... from Elm; it's syntax is basically exactly what we wanted, it felt like.)

At the end of the day, ? might be more consistent; as long as you think it'll work for your use case, I do prefer limiting the nesting, or some of the crazier use cases like that.

Preemptive Type Coercion

This isn't as weird as you might have thought; one of the gotchas about everything here is that right now we only have one output type: Numbers. Booleans will need to have some sort of casting to them otherwise people can accidentally do illegal things that will (should) blow up with a TypeError. As an approach, this isn't terrible.

Javascript Coercion

Regarding my points:

  • The Conditional must always be a Boolean. (Casts are handled by the BooleanOp)
  • The Expression portion of the then and else blocks must always evaluate to a Number.

My reasoning goes like this: We only need Booleans to exist for conditionals. I suppose this should be relaxed to "The Conditional may be a Boolean." I'm totally find with javascript coercion here.

The part that I'm not cool with is rpgdice.eval('1d20 + 5 >= enemyArmor', {...}). That should, IMHO throw a TypeError with a helpful message, like, "Expression does not evaluate to a numeric." What I want is to prevent mixing Boolean and Numeric operations in awkward ways.

That being said, Javascript does seem to have some decent rules for this:

> true + 1
2
> false + 1
1
> 50 / true
50
> 50 / false
Infinity
> parseFloat(true)
NaN
> parseInt(true)
NaN
> true + 0
1
> 

I just don't want the end result of the evaluation to be true or false. It should be a number, always.

from rpgdice.

mrfigg avatar mrfigg commented on August 24, 2024

Ternary vs IfThenElse
It seems we both just have personal preferences on this one, both of which I think are valid, and neither of which should really get in the way of the other. So I say let's just implement both and let the user decide which they find most appealing.

Could you clarify what you mean by limiting the nesting? I believe they should also be right-to-left associative as per Operator Precedence.

No top level BooleanOps
Thinking on it not allowing top level booleans makes complete sense, but in order to accomplish this and truly protect against odd behavior I think we might be forced into going with preemptive type coercion. Otherwise what's to stop someone from trying to do this;

let expr = rpgdice.parse('1d20 + 5 >= enemyArmor ? 0 : 0');
expr = expr.condition;
expr.eval({...});

The condition has no way to know if it's been pulled out of its conditional. Or should we consider this to be beyond our control and let the user do what they want?

And with preemptive type coercion a user could still do rpgdice.eval('1d20 + 5 >= enemyArmor', {...}) if they really want, and still have the output be usable for their (probable) intended purpose and be a valid number.

from rpgdice.

Morgul avatar Morgul commented on August 24, 2024

Limited Nesting

All I meant was, it gets really ugly trying to do:

5 >= armorBonus ? 3 < armorBonus ? 9 > dexBonus ? 3d6 : 2d6 : 9 : 0

Which means the user won't try to do it too much. No actual enforcement requires, and it should definitely follow Operator Precedence.

For now, let's just go with ternary. We can always add if ... then ... else support later, if people start complaining about formatting nested ternaries.

No top level BooleanOps

Yeah, great point. I'm kinda sold on the preemptive type coercion. I want to think about it a little more before moving forward, but I think that's the direction I'd rather go. We then aren't introducing Booleans, rather, we're just saying that 5 < 3 === 0. I'm ok with that,I think.

from rpgdice.

mrfigg avatar mrfigg commented on August 24, 2024

Shall I go ahead and mark #7 ready for review, or should I wait for this to be finalized so it can be added in? I'm fine with either.

from rpgdice.

Morgul avatar Morgul commented on August 24, 2024

Let's go ahead and mark #7 for review, and put this in another merge.

from rpgdice.

Morgul avatar Morgul commented on August 24, 2024

This was closed by #10.

from rpgdice.

Related Issues (6)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.