995 words
~5min read

Actions and Selections with Card Lang

December 14th 2018
< 1K views

When the events we set up in part 1 of exploring the card language fire it would be quite nice if they could actually do something! This is where actions get their chance to shine. There are a ton of different actions a card can take but we’ll start out with just a few example ones like doing damage to pieces (Hit), healing them, or drawing a card. Just as we can have multiple events on a card we should also be able to have multiple actions for each event. We’ll also need a way to pass arguments to these actions to tell them which pieces to hit, what player draws the card, and so on.

So without further intro, lets add some more tokens to the language after the event tokens from before.

//Targets are who the actions affect

// player targets
(PLAYER|OPPONENT)
  return 'target'

// piece selectors
(ENEMY|FRIENDLY|CHARACTER|MINION|HERO)
  return 'target'

// actions
(Hit|Heal|DrawCard)
  return 'action'

//numbers
(\-?[0-9]+)
  return 'number'

//text
(\'(.*?)\')
  return 'text'

//additional syntax
'|'    return '|'
'&'    return '&'
'-'    return '-'

These should all be pretty straightforward as we’re just adding tokens for targets and actions as well as matching numbers and text to use for action arguments.

The grammar is where the fun begins here. We’ll modify the pEvent from last time to include the actionList but the rest are new. Starting from the top, we have a few lists so we can have multiple actions per event (with actionlist and actionargs), and multiple arguments per action (arguments and argument_item). For demo purposes the only argument types supported here are either the possibleRandSelector we’ll get into below or a simple number.

/* events */
pEvent
  : event'{'actionlist'}'
   { $$ = { event: $1, actions: $3 } }
;

/* actionlist is all the actions for each event */
actionlist
  : actionlist actionargs
     { $$ = $actionlist; $$.push($actionargs); }
  | actionargs
     { $$ = [$actionargs]; }
;

/* actionargs is the basic syntax for all the actions with function like arguments */
actionargs
  : action'('arguments')'
  {{ $$ =
    { action: $1, args: $3 }
  }}
;

arguments
  : arguments ',' argument_item
     { $$ = $arguments; $$.push($argument_item); }
  | argument_item
     { $$ = [$argument_item]; }
;

argument_item
  : possibleRandSelector -> $1
  | pNumber -> $1
;

possibleRandSelector
  : selector
  | targetExpr
     { $$ = { left: $1}; }
  | random'('targetExpr')'
     { $$ = { random: true, selector: { left: $3} }; }
  | random'('selector')'
     { $$ = { random: true, selector: $3 }; }
;

targetExpr
  : target -> $1
;

selector
  : selector operator targetExpr
     { $$ = { left: $1, op: $2, right: $3 }; }
  | targetExpr operator targetExpr
     { $$ = { left: $1, op: $2, right: $3 }; }
;

operator
  : '&' //intersection
  | '|' //union
  | '-' //difference
;

pNumber
  : number -> parseInt($1)
;

Now with this grammar we can implement a spell that says “Deal 1 damage to all enemies” with this text:

playSpell{
  Hit(ENEMY,1)
}

Which parses into this result:

[
  {
    "event": "playSpell",
    "actions": [
      {
        "action": "Hit",
        "args": [
          {
            "left": "ENEMY"
          },
          1
        ]
      }
    ]
  }
]

Screenshot-2017-12-12-14.06.51

To select which pieces on the board we want to target we really need something that operates on sets and can also do things like pick a random piece. This is where possibleRandSelector comes into play. The selector is set up so you can create expressions that will do set operations on the targets and then optionally wrap random() around that expression to select one random piece out of the set.

To heal your own hero you would then use:

playSpell{
  Heal(FRIENDLY & HERO,1)
}

To get:

[
  {
    "event": "playSpell",
    "actions": [
      {
        "action": "Heal",
        "args": [
          {
            "left": "FRIENDLY",
            "op": "&",
            "right": "HERO"
          },
          1
        ]
      }
    ]
  }
]

Or to do an arbitrarily complex selection like “All enemy characters who aren’t minions, or any hero” you can use this, which is both nonsensical and possible:

playSpell{
  Hit(ENEMY & CHARACTER - MINION | HERO, 1)
}
[
  {
    "event": "playSpell",
    "actions": [
      {
        "action": "Hit",
        "args": [
          {
            "left": {
              "left": {
                "left": "ENEMY",
                "op": "&",
                "right": "CHARACTER"
              },
              "op": "-",
              "right": "MINION"
            },
            "op": "|",
            "right": "HERO"
          },
          1
        ]
      }
    ]
  }
]

To give a little teaser of how the language is run in the game (which we’ll go into more in the next part of the series) here’s some code from the piece selector which is responsible for taking the piece state that holds all the pieces on the board and filtering them according to the selector it gets. To do this it recurses down the left sides of the expressions until it gets to a string which means it’s something to filter the pieces on. Then as the recursion is climbing its way back up the stack it will check to see if there’s a right side to the expression and an operator to combine the filtered pieces.

export default class PieceSelector{
  constructor(allPieces, pieceSelectorParams){
    this.allPieces = allPieces;
    this.controllingPlayerId = pieceSelectorParams.controllingPlayerId;
  }

  Select(selector){
    //base case
    if(typeof selector == 'string'){
      switch(selector){
        case 'CHARACTER':
          return this.allPieces;
          break;
        case 'FRIENDLY':
          return this.allPieces.filter(p => p.playerId == this.controllingPlayerId);
          break;
        case 'ENEMY':
          return this.allPieces.filter(p => p.playerId != this.controllingPlayerId);
          break;
        case 'MINION':
          return this.allPieces.filter(p => p.isMinion);
          break;
        case 'HERO':
          return this.allPieces.filter(p => p.isHero);
          break;
        default:
          throw 'Invalid piece type selector ' + selector;
      }
    }

    if(!selector.left){
      throw 'Selector must have left hand side selector';
    }

    //ordinary case of recursing the piece selections
    let leftResult = this.Select(selector.left);

    if(selector.op && selector.right){
      let rightResult = this.Select(selector.right);

      switch(selector.op){
        case '|':
          return Union(leftResult, rightResult, (a,b) => a.id === b.id);
          break;
        case '&':
          return Intersection(leftResult, rightResult, (a,b) => a.id === b.id);
          break;
        case '-':
          return Difference(leftResult, rightResult, (a,b) => a.id === b.id);
          break;
      }

    }else{
      return leftResult;
    }
  }
}

And that right there is the fundamentals of card lang! Lists of events trigger the lists of actions which take arguments and select pieces to run the actions on. Tune in next time to find out more about how the server triggers and runs the actions!

Want the inside scoop?

Sign up and be the first to see new posts

No spam, just the inside scoop and $10 off any photo print!