2
\$\begingroup\$

There are 4 player character slots and 8 enemy slots. I'm using a combat system that's similar to ATB from FF4; You select a move, there's a waiting period depending upon the move, during which other moves can happen, and then it is executed. The problem is some moves counter other moves and this changes targeting.

Example:

Enemy A is targeting Player 1 with Stampede. Player 2 decides to use a sweep attack on Enemy A to stop it. If nothing else happens, this causes Player 2 to become the new target for any damage dealt by Enemy A's Stampede (he's jumping in there and breaking up the attack, if damage is dealt, it's to him now).

Here's the problem: If Player 2 is being countered then he can't counter Enemy A, unless whoever is countering Player 2 is also being countered, in which case he can, unless someone is countering the ... and I'm not sure the cases ever end, even with just the 12 participants.

I need a working theory on how to logic through it smoothly in a pile of code that isn't Mount Everest. How do I narrow the results until I figure out the exact result?

Using GML if it matters.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ To iterate is human, to recurse divine. \$\endgroup\$ – MichaelHouse Mar 1 '16 at 18:25
  • 1
    \$\begingroup\$ @Byte56 But to recurse, you have to curse first. Which is human as well. \$\endgroup\$ – Vaillancourt Mar 1 '16 at 18:27
  • 1
    \$\begingroup\$ @AlexandreVaillancourt Recursion can often cause cursing. But I think recursion would be the best way to solve this particular problem. \$\endgroup\$ – MichaelHouse Mar 1 '16 at 18:32
3
\$\begingroup\$

This is a really simple problem if you stop thinking about what happens to everything at once, and deconstruct it: moves can either happen, or be countered. Then all you have to do is figure out where the sequence begins, set it off there, and let everything resolve each other. It's like dominoes.

Games like Magic the Gathering models this as a stack. Whenever a player plays a move, the opponent can play another move to counter it, but nothing happens yet, it all goes on a stack - literally a stack of moves/cards, if that's easier to visualise. The first player can then play something to counter that, and so on and so on, building the stack up until someone stops. Then the stack is "unwound", from the top to the bottom. The topmost move works because it isn't countered, but the one below it is countered so it doesn't work. Now you're left with a smaller stack, but it works the same way: the topmost move in the stack works; the one below it doesn't because it is countered. That's it. You don't need to worry about how big the stack is, which moves are countered by the counter of the counter of the whatever. It's just these questions:

  • Is this move countered? (yes/no)
  • What happens if this move is, or isn't countered? (this if countered, or that if not countered)
  • (Logical) stack of moves (add moves to the top, resolve them from top to bottom)

Example

Let's try an example game, and we'll build up the stack.

Enemy A attacks Player 1 with Stampede

Imagine we add this to a stack now:

|                                        |
| Enemy A attacks Player 1 with Stampede |
+----------------------------------------+
  • Is this move countered? (no, because it's the topmost item on the stack)
  • What happens? (Player 1 totally gets their ass stampeded)
  • What happens to the stack? (it becomes empty)

So far so good, easy. Now let's play Player 2's counter...

Player 2 sweep attacks Enemy A

Now our stack grows:

|                                        |
| Player 2 sweep attacks Enemy A         |
| Enemy A attacks Player 1 with Stampede |
+----------------------------------------+

We resolve these moves from the top to the bottom. So let's do Player 2's sweep first:

  • Countered? (no, it's top of stack)
  • What happens? (Enemy A's attack gets countered by Player 2)
  • Stack? (1 move left)

Let's pause here. The sweep attack you've defined isn't a simple counter. It changes the target of the countered move to the countering player. This means that, in addition to tracking whether moves are countered, you also need to track which player/enemy did the countering. Then we are left with this stack:

|                                        |
| Enemy A attacks Player 2 with Stampede |  <-- different target!
+----------------------------------------+

And keep going...

  • Countered? (yes, by Player 2, this means the target changes to Player 2)
  • What happens? (Player 1 2 totally gets their ass stampeded)
  • Stack? (empty)

There's some trickiness going on with the counter just then, but notice that the way the stack works doesn't change. Now let's imagine something countering Player 2's sweep:

Enemy B steals Player 2's broom

...because sweeping is done using brooms right? Yeah...

|                                        |
| Enemy B steals Player 2's broom        |
| Player 2 sweep attacks Enemy A         |
| Enemy A attacks Player 1 with Stampede |
+----------------------------------------+

You should know how the stack works now. The steal succeeds, Player 2's sweep gets cancelled because they don't have a broom.

|                                        |
|              *poof!*                   |
|          *poof! cancelled!*            |
| Enemy A attacks Player 1 with Stampede |
+----------------------------------------+

The result: Player 2 loses their broom, Player 1 totally gets their ass stampeded.

Player 3 chastises Enemy B

Stealing is bad and is easily prevented by chastisement. Our stack is starting to get pretty tall, but no worries, it works all the same.

|                                        |
| Player 3 chastises Enemy B             |
| Enemy B steals Player 2's broom        |
| Player 2 sweep attacks Enemy A         |
| Enemy A attacks Player 1 with Stampede |
+----------------------------------------+

The chastisement works!

|                                        |
|              *poof!*                   |
|      *poof! Enemy B feels guilty*      |
| Player 2 sweep attacks Enemy A         |
| Enemy A attacks Player 1 with Stampede |
+----------------------------------------+

Player 2 can now do the sweep:

|                                        |
|              *poof!*                   |
| Enemy A attacks Player 2 with Stampede |  <-- different target!
+----------------------------------------+

The result: Enemy B feels guilty, Player 2 totally gets their ass stampeded.

What you need

As you can see, it doesn't matter whether a move is countered because there's a move countering the move countering the move countering itself... it's only a binary choice: is it countered or not. You use a stack to track which moves counter which.

GML has the ds_stack data structure.

And, to handle complex counters like the sweep move (which modifies the moves that it counters), track which player did the countering.

\$\endgroup\$
3
  • \$\begingroup\$ I am familiar with MTG/the stack and that really is helping me visualize. I believe my main issue was a loop is created if A counters 1, who is countered by 2, who is countered by B, who is countered by 1 (from above). I'm gonna put some more time into this with your examples, but at the end of the day I think I need to disregard 1's attack if he's the original character being targeted in order. If I do it like a stack, Character 1 would get his counter through at the top and Enemy A would get the shaft, even though he's the one currently attacking. \$\endgroup\$ – Nogusielkt Mar 2 '16 at 21:55
  • \$\begingroup\$ @Nogusielkt the only way a loop can form is if Player 1's original move somehow counters the last enemy move. But even then it's fine, just choose the first move to resolve (i.e. ignore its counter), then follow the loop around until all moves are resolved. In a turn-based game like MtG it starts from the last move added, but in an ATB game where there's a certain time between committing to a move and resolving, it's just the first move to finish that delay. You'll have a lot of counter moves with missing targets, and you'll need to figure out what happens then. \$\endgroup\$ – congusbongus Mar 2 '16 at 23:06
  • \$\begingroup\$ Yeah, perhaps one important thing I forgot to mention: After you select a move, but before it would be put on the stack, it is in a "ready" state. The first move that is put on the stack will call all "ready" moves to the stack in order to resolve them all at once. Despite it being ATB, timing is adjusted to smooth the counters before and after the action is resolved. PS. The broom cracked me up. \$\endgroup\$ – Nogusielkt Mar 3 '16 at 2:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.