Skip to content

Recovery

Tom Sherman edited this page Mar 27, 2017 · 25 revisions

Previous Section: DI

Contents

Overview

Marth

Falcon

Falco

Fox

This is by far the most complicated portion of the AI. Here we will create recovery flowcharts for Falco, Fox, Marth, and Falcon. There will be a generic flowchart for recovering close to the stage (double jumping back).

Background Knowledge

switch statement

custom inputs

Point math

General Recovery

The main logic for recovery looks like:

Logic recoveryLogic = 
{
    {&recoverySituation, .arg1.u = 2},
    {&recover, .arg1.p = &cpuPlayer}
};


void recover(AI* ai)
{
    setGlobalVariables(ai);

    if (!closeRecovery(ai))
    {
        switch (rInfo.character)
        {
            case FALCO_ID:
            case FOX_ID:
                spacieRecovery(ai);
                break;
            case MARTH_ID:
                marthRecovery(ai);
                break;
            case FALCON_ID:
                falconRecovery(ai);
                break;
        }
    }
}

Whenever the AI is in a "recovery situation" (offstage and not in hitstun) it calls the recover function. This function first sets up some global variables for all functions related to recovery to use. Then it checks if it is close enough to double jump back - this is character independent. If it is not able to jump back then it calls a character specific recovery.

The global variables are stored in a struct called rInfo:

typedef struct
{
    Point ledge;
    Point coords;
    float abs_x;
    float dist;
    bool leftSide;
    float stageDir;
    u32 jumps;
    u32 character;
    s32 horizJump, vertJump;
    s32 charHeight;

} RecoveryInfo;

RecoveryInfo rInfo = {0};

It is very important to initialize this variable since it is not static. It contains all of the relevant information for recovering. Even though all this information is already available, it is worth taking a little extra space in the beginning to have the info readily available for all recovery functions. Here it the function that updates it:

void setGlobalVariables(AI* ai)
{
    rInfo.ledge.x = _gameState.stage.ledge.x;
    rInfo.ledge.y = _gameState.stage.ledge.y;

    rInfo.coords.x = _gameState.playerData[ai->port]->coordinates.x;
    rInfo.coords.y = _gameState.playerData[ai->port]->coordinates.y;
    rInfo.abs_x = fabs(rInfo.coords.x);
    rInfo.dist = rInfo.abs_x - rInfo.ledge.x;

    rInfo.leftSide = rInfo.coords.x < 0;
    rInfo.stageDir = rInfo.leftSide ? 0.f : 180.f;

    rInfo.jumps = 2 - (u32) _gameState.playerData[ai->port]->jumpsUsed;

    rInfo.character = CHAR_SELECT(ai->port);

    rInfo.horizJump = _dj_horizontal[rInfo.character];
    rInfo.vertJump = _dj_vertical[rInfo.character];
    rInfo.charHeight = _char_height[rInfo.character];
}

Next we define some helper functions:

void addCleanUpLogic(AI* ai)
{
    addLogic(ai, &onLedgeLogic);    
    addLogic(ai, &resetOnHitLogic);
    addLogic(ai, &resetOnWaitLogic);
    addLogic(ai, &resetOnDeathLogic);
    addLogic(ai, &resetOnStageLogic);
}

void doubleJump(AI* ai, float target)
{
    setGlobalVariables(ai);
    float dist = fmax(rInfo.dist, target - 30.f);
    dist = fmin(dist, target + 30.f);

    float dir = (dist - (target - 30.f)) * 180.f / 60.f;
    dir = rInfo.leftSide ? 180.f - dir : dir;

    SET_DJ_DIR(dir);
    addMove(ai, &_mv_doubleJump);
}

addCleanUpLogic contains most of the logic we need to call after adding a move to the AI when its recovering. There are basically 5 different ways that the final recovery move could end. The AI grabs ledge, lands on stage, is in the wait action state (randall), gets hit again, or dies.

doubleJump is an extremely useful function. It calculates the angle of the control stick needed to get close to the target x-coordinate. This prevents the AI from jumping forward underneath the stage, and allows for more precise positioning with the double jump.

The logic we will use for this kind of recovery looks like:

Logic doubleJumpRecoveryLogic = 
{
    {&belowHeight, .arg1.u = 2},
    {&doubleJumpRecovery, .arg1.p = &cpuPlayer}
};

void doubleJumpRecovery(AI* ai)
{
    doubleJump(ai, 0.f);
    
    resetAfterFrameLogic.condition.arg1.u = CURRENT_FRAME + 40;
    addLogic(ai, &resetAfterFrameLogic);
    addCleanUpLogic(ai);
}

Once player 2 drops below the height (not set yet) in doubleJumpRecoveryLogic, doubleJumpRecovery will be called and the AI will jump towards the stage. We need additional logic besides addCleanUpLogic because there is a possiblility this attempt is not successful (e.g. human player takes the ledge) and in that case we need to react and try recovering again. So if 40 frames go by the AI resets and will call the original recover function again.

All we need to do now is figure the height the AI should jump at.

#define CANT_CLOSE_RECOVER  \
    rInfo.jumps < 1 || \
    rInfo.dist > rInfo.horizJump || \
    rInfo.coords.y < -(rInfo.vertJump + rInfo.charHeight - rInfo.ledge.y)

#define JUMP_TO_PLATFORM \
    chance(0.5f) && \
    rInfo.jumps > 0 && \
    rInfo.abs_x < _gameState.stage.side.right + rInfo.horizJump && \
    rInfo.coords.y > _gameState.stage.side.height - rInfo.vertJump

static bool closeRecovery(AI* ai)
{
    if (CANT_CLOSE_RECOVER)
    {
        return false;
    }
    else if (JUMP_TO_PLATFORM)
    {
        doubleJumpRecoveryLogic.condition.arg2.f = 
            _gameState.stage.side.height - rInfo.vertJump;

    }
    else //jump to ledge
    {
        doubleJumpRecoveryLogic.condition.arg2.f = 
            -(rInfo.vertJump + rInfo.charHeight - rInfo.ledge.y);
    }

    ...
}

The macro definitions are simply to help with readability. First we check if the AI has a jump and is close enough to the ledge, otherwise we have to call a character specific recovery. Next we see if the AI is close enought to a platform, and in that case there's a 50% chance the AI will jump for it. In this case we set the second argument of the condition function in doubleJumpRecoveryLogic. This is the height at which doubleJumpRecovery is called. Finally, if the previous two if statements are false we simply jump to the ledge.

The next part of this function looks like:

if (JUMP_TO_PLATFORM || rInfo.dist > 20.f)
{
    SET_HOLD_DIR(rInfo.stageDir);
    addMove(ai, &_mv_holdDirection);
}

This simply tells the AI to hold in while it falls. Except we dont want to hold in if we are close to the stage and trying to go for the ledge. If the AI does hold in it might go underneath battlefield or make itself especially vulnerable.

See the full recovery here

Falcon Recovery

This is the simplest of all three recovery flowcharts. Basically there are 3 options falcon needs to decide between: double jump, falcon kick, up-b. Obviously it can be more complicated than this, but for now this is a good starting point. His recovery structure looks like:

#define KICK_HEIGHT 50.f
#define KICK_DIST   50.f

void falconRecovery(AI* ai)
{
    if (rInfo.jumps > 0) //double jump
    {
        ...
    }
    else if (rInfo.coords.y > KICK_HEIGHT && rInfo.dist > KICK_DIST) //falcon kick
    {
        ...
    }
    else //up b
    {
        ...
    }
    ...
}

Falcon will always double jump if he can, then if he is high and far away from the stage he will falcon kick, and finally he will up-b towards the stage. Here is what each part looks like:

if (rInfo.jumps > 0)
{
    doubleJump(ai, 40.f);
    resetAfterFrameLogic.condition.arg1.u = CURRENT_FRAME + 20;
    addLogic(ai, &resetAfterFrameLogic);
}

Falcon always aims to be roughly 40 away from the stage, since his up-b can cover this distance easily and it prevents him from being too vulnerable after the double jump. 20 frames after inputting this jump, the AI will reset and start the recovery tree from the top level recover function.

The falcon kick is essentially the same as the double jump except we wait longer before resetting.

else if (rInfo.coords.y > KICK_HEIGHT && rInfo.dist > KICK_DIST)
{
    addMove(ai, &_mv_downB);
    resetAfterFrameLogic.condition.arg1.u = CURRENT_FRAME + 60;
    addLogic(ai, &resetAfterFrameLogic);
}

Finally we have the up-b

else 
{
    float dir = rInfo.dist > 30.f ? rInfo.stageDir : 90.f;
    if (rInfo.dist < 0.f) {dir = rInfo.stageDir + 180.f;}

    SET_UP_B_DIR(dir);
    addMove(ai, &_mv_upB);
}

The only trick here is figuring out what direction to hold after the up-b. If falcon is under the stage the AI will hold back to do a reverse up-b. If falcon is far enough from the stage it will hold in, otherwise the AI will hold straight up.

See the full recovery here

Marth Recovery

Marth's recovery is slightly more complex than Falcon's. Falcon could decide between his 3 options independently, but Marth needs to decide on a multiple move strategy ahead of time (e.g. double jump and up-b are not independent of each other). Here is the basic structure:

#define SIDE_B_HEIGHT      0.f
#define SIDE_B_DIST       70.f
#define SWEET_SPOT_PROB  0.55f

void marthRecovery(AI* ai)
{
    if (rInfo.coords.y > SIDE_B_HEIGHT && rInfo.dist > SIDE_B_DIST) //side-b
    {
        ...
    }
    else if (chance(SWEET_SPOT_PROB) || rInfo.dist > 50.f) // up-b to ledge
    {
        ...
    }
    else // up-b to the stage
    {
        ...
    }
    addCleanUpLogic(ai);
}

His side-b is a little tricky - if Marth just inputs side-b he doesn't gain much distance. Marth needs to hold a direction first and then side-b helps him float along with the momentum. For this reason, we need to define a custom move for his side-b.

static RawInput raw_marthSideB[2] = 
{
    {OVERWRITE, 0, NO_FLAGS},
    {OVERWRITE, 15, NO_FLAGS}
};
static Move mv_marthSideB = {.inputs = raw_marthSideB, .size = 2};

static void marthSideB(AI* ai)
{
    raw_marthSideB[0].controller =
        FULL_STICK | STICK_ANGLE(rInfo.stageDir);
    raw_marthSideB[1].controller = 
        B_BUTTON | FULL_STICK | STICK_ANGLE(rInfo.stageDir);
                
    addMove(ai, &mv_marthSideB);

    resetAfterFrameLogic.condition.arg1.u = CURRENT_FRAME + 40;
    addLogic(ai, &resetAfterFrameLogic);
}

This way Marth will hold a direction before hitting side-b. Since we need to call addCleanUpLogic (marth could be hit out of side-b, grab ledge, etc...) it is also neccesary to put a hard reset in place. Otherwise, Marth will wait until one of the cleanup logic rules is true. Thus, 40 frames after the side-b the AI will reset and call the top-level recover.

Next we define some Logic for double jump and up-b.

Logic marthUpBLogic = 
{
    {&belowHeight, .arg1.u = 2},
    {&marthUpB, .arg1.p = &cpuPlayer}
};

static float jumpDist = 0.f;
static float upBdist = 0.f;
static float upBheight = 0.f;

void marthDoubleJump(AI* ai)
{
    if (rInfo.jumps > 0) {doubleJump(ai, jumpDist);}
    marthUpBLogic.condition.arg2.f = upBheight;
    addLogic(ai, &marthUpBLogic);
    addCleanUpLogic(ai);
}

void marthUpB(AI* ai)
{
    setGlobalVariables(ai);
    float dir = rInfo.dist > upBdist + 25.f ? rInfo.stageDir : 90.f;
    SET_UP_B_DIR(dir);
    addMove(ai, &_mv_upB);
    addCleanUpLogic(ai);
}

jumpDist, upBdist, and upBheight are all set by marthRecovery, since they depend on the particular type of recovery strategy being used. marthDoubleJump tells the AI to double jump and queues up an up-b at a upBheight. marthUpB is very similar to Falcon's up-b.

Finally let's fill in the details for marthRecovery

else if (chance(SWEET_SPOT_PROB) || rInfo.dist > 50.f)
{
    jumpDist = 25.f;
    marthDoubleJumpLogic.condition.arg2.f = -60.f;

    upBdist = 0.f;
    upBheight = -63.f;
    marthUpBLogic.condition.function = &belowHeight;            

    addLogic(ai, &marthDoubleJumpLogic);
}

If marth is going for a sweet spot we want to jump about 25 away from the stage, and we use our jump at height -60. We want Marth to up-b to the ledge (upBdist = 0.f) and sweet spot the height (upBheight = -63.f). Finally we need to specify that Marth should wait until he's below -63 to trigger the upB (marthUpBLogic.condition.function = &belowHeight).

else
{
    jumpDist = 13.f;
    upBdist = -20.f;
    upBheight = -45.f;

    if (rInfo.jumps > 0)
    {
        marthDoubleJumpLogic.condition.arg2.f = -65.f;
        marthUpBLogic.condition.function = &aboveHeight;
        addLogic(ai, &marthDoubleJumpLogic);
    }
    else
    {
        marthUpBLogic.condition.arg2.f = upBheight;
        marthUpBLogic.condition.function = &belowHeight;            
        addLogic(ai, &marthUpBLogic);         
    }
}

If Marth if going for the stage and has a jump, we want marth to use the jump at -65 and then the upB at -45, however without a jump, as soon as Marth drops below -45 we want him to use the upB.

See the full recovery here

Spacie Recovery

The basic structure of spacieRecovery looks like:

if (...) // able to do illusion
{
    illusionRecovery(ai, illusionLength);
}
else if (rInfo.jumps > 0)
{
    doubleJump(ai, 40.f);

    resetAfterFrameLogic.condition.arg1.u = CURRENT_FRAME + 20;
    addLogic(ai, &resetAfterFrameLogic);
    addCleanUpLogic(ai);
}
else
{
    fireRecovery(ai);
}

Thus the recovery flowchart is roughly - do immediate side-b if possible (with some random chance), otherwise use the double jump. Without the double jump available randomly choose between side-b and up-b.

Here are the details for deciding on the side-b recovery.

float illusionLength = rInfo.character == FALCO_ID ? 85.f : 95.f;
float illusionProb = 0.3f + (rInfo.jumps > 0 ? 0.2f : 0.f)
    + (rInfo.character == FALCO_ID ? 0.1f : 0.f);

if (rInfo.coords.y > rInfo.ledge.y - 13.f && illusionLength > rInfo.dist
    && chance(illusionProb))
{
    illusionRecovery(ai, illusionLength);
}

First let's look at illusionProb. Here's what that formula works out to:

FALCO FOX
Jump 0.6 0.5
No Jump 0.4 0.3

Since Falco's firebird is worse, we always want a higher probability of using side-b. Also, since immediate side-b is better than jump + side-b, we want a higher probability to doing that. These values give reasonable behavior. The if statement just makes sure the AI is close enough and high enough to use side-b, then calls chance to see if the AI should side-b based on the above probability.

Now let's look at the details of illusionRecovery. This function decides which height the AI will side-b at. Either 1) to the side platform, 2) to the ledge, 3) at a random height.

float u = rand();

if (_gameState.stage.side.height > 1.f
    && rInfo.coords.y > _gameState.stage.side.height
    && rInfo.abs_x - _gameState.stage.side.left > length
    && u < 0.5f)
{
    illusionRecoveryLogic.condition.arg2.f =
        _gameState.stage.side.height - 1.f;        
}

That if statement checks that the side platform exists and the AI is in range. Finally, it uses u < 0.5f to add some randomness to the decision.

Next the AI calculates the height it should side-b at to hit the ledge. Here we add a new feature: cpu level dependent decisions. Hitting a perfect side-b sweet spot is pretty difficuly, so we will have the cpu make some error to the height calculation based on the level that is set for the cpu.

else if (u < 0.8f)
{
    u32 maxError = 18 - 2 * _gameState.playerData[ai->port]->aiLevel;

    illusionRecoveryLogic.condition.arg2.f = rInfo.ledge.y - 15.f
        + uniform(0.f, (float) maxError);
}

A level 9 cpu will make no error and a level 1 cpu will make an error between (0, 16). This makes it easier to edgeguard lower level cpu's.

Finally, the ai will side-b at a random height 20% of the time:

else
{
    illusionRecoveryLogic.condition.arg2.f = 
        uniform(rInfo.ledge.y, rInfo.coords.y);
}

Next let's let's look at the details of fireRecovery. This function decides the height that the AI should begin the up-b.

float fireLength = rInfo.character == FALCO_ID ? 70.f : 90.f;

if (fireLength < rInfo.dist)
{
    fireRecoveryLogic.condition.arg2.f = 30.f;
}
else 
{
    float maxHeight = sqrt(SQUARE(fireLength) - SQUARE(rInfo.dist));
    maxHeight = fmax(maxHeight - 10.f, 1.f);
    fireRecoveryLogic.condition.arg2.f = uniform(-maxHeight, maxHeight);
}

First we check if the AI is even in range of the stage, if its not the AI falls to around 30 and up-b's from there. Otherwise it calculates the maximum height it could hit the ledge from using simple trigonometry. It then decides on a height randomly within the possible bounds.

After the AI decides on a height, it must pick the angle.

Point ledge_pt = {rInfo.ledge.x, rInfo.ledge.y - 10.f};
Point coord_pt = {rInfo.abs_x, rInfo.coords.y};
float ledgeAngle = angle(coord_pt, ledge_pt);

float fireLength = rInfo.character == FALCO_ID ? 70.f : 90.f;
float highAngle = reflectAngle(acos(rInfo.dist / fireLength));

float ang = chance(0.4f) ? ledgeAngle : uniform(ledgeAngle, highAngle);

First the minimum (aka ledge) angle and the maximum angle are calculated. Then the AI picks the ledge angle with a 40% chance and otherwise chooses randomly between the two bounds.

Next we need to clean the angle because of the dead zones in the controller.

if (ang >  73.f && ang <  85.f) {ang = 73.f;}
if (ang < 180.f && ang > 163.f) {ang = 163.f;}
if (ang > 253.f && ang < 265.f) {ang = 253.f;}

and if the AI is too far, we automatically do a 45 degree angle.

if (rInfo.dist > fireLength) {ang = 135.f;}

Note that the entire recovery is calculated as if the AI is on the right side of the stage. We need to reflect the angle if the AI is on the left side:

static float reflectAngle(float ang)
{
    ang = 180.f - ang;
    return ang < 0.f ? ang + 360.f : ang;
}

void spacieFire(AI* ai)
{
    ...

    if (rInfo.dist > fireLength) {ang = 135.f;}

    ...
}

That last thing that needs to be done is resetting the control stick immediately after the firebird/fox starts to move, otherwise the spacie might fast fall past the ledge.

postFireLogic.condition.arg1.u = CURRENT_FRAME + 45;
addLogic(ai, &postFireLogic);
addCleanUpLogic(ai);

See the full recovery here

Ledge Option

For now let's make things simple. Whenever the AI is on the ledge it performs a ledgedash.

Logic onLedgeLogic = 
{
    {&actionStateEq, .arg1.u = 2, .arg2.u = _AS_CliffWait},
    {&ledgeOption, .arg1.p = &cpuPlayer}
};   

void ledgeOption(AI* ai)
{
    setGlobalVariables(ai);
    float ang = rInfo.stageDir > 90.f ? 200.f : 340.f;
    SET_LEDGEDASH_ANGLE(ang);
    addMove(ai, &_mv_ledgeDash);
}

Possible Expansions

  • make cpu ledgedashes depend on level (lower level = less frame perfect)
  • add more ledge options
  • add ledge teching (could be more general than just teching on recovery attempts)

Creating the Program

The full code is here. Run the standard wiimake command to build the iso.

Next Section

Now that we have all the major pieces, we can put the finishing touches on the AI.

Next Section: Defensive AI