kiwis.dev

Overview

The Table Tennis stage in Wii Play bears the identifier "RPPnpScene".

ExcelBin

The scene's common archive contains the following ExcelBin (ver. 1) files:

leveltable.bin

This file contains configurations for the AI paddle's behavior. Each configuration ("level") consists of the following structure:

Offset Type Name Notes
0x0000 uint32 easyChance Probability of returning the ball in the direction considered 'easy' difficulty for the player.
0x0004 uint32 mediumChance Probability of returning the ball in the direction considered 'medium' difficulty for the player.
0x0008 uint32 hardChance Probability of returning the ball in the direction considered 'hard' difficulty for the player.
0x000C N/A Total size N/A

C++ data structure:

struct RPPnpLevelInfo {
    /* 0x00 */ u32 easyChance;
    /* 0x04 */ u32 mediumChance;
    /* 0x08 */ u32 hardChance;
};

Probabilities usually add up to 10, but this is not required. This is because a random number is chosen in the range of the sum of the probabilities.

rallyleveluptable.bin

This file contains configurations for how the game difficulty increases over time. Each configuration ("level up") consists of the following structure:

Offset Type Name Notes
0x0000 uint32 rally Minimum rally count to level up.
0x0004 uint32 level AI level to use (index into leveltable.bin).
0x0008 float power Speed at which the ball will move.
0x000C N/A Total size N/A

C++ data structure:

struct RPPnpLevelUpInfo {
    /* 0x00 */ u32 rally;
    /* 0x04 */ u32 level;
    /* 0x08 */ f32 power;
};

Interestingly, there exists a level up for rally 1000, which is impossible to achieve. The highest possible rally count is 999.

This 1000th rally is considerably easier than the previous level up (rallies 987-999). Below is a comparison of the last two level ups:

rally level power
987 12 1.94
1000 4 1.17

rallyleveluptable2.bin

This file contains the same structures seen in rallyleveluptable.bin, but is unused by the game's code.

Unlike the first binary, the level parameter is not used here, suggesting that AI was not involved in this mode.

Additionally, there are 100 level ups for each rally, increasing the ball speed from 0.65 (same starting speed for 1P) to 2.0.

It is likely that this file originally controlled the difficulty increase during rallies in the 2P mode; however, in the final revision of the game both 1P/2P use rallyleveluptable.bin:

void RPPnpBall::Calculate() {
    // . . . . .
        
    f32 speed;
    u32 level;

    // 1-player mode (rally with AI).
    if (RPPnpMain::GetInstance()->GetGameType() == EGameType_Rally) {
        RPPnpBinaryMgr::GetInstance()->GetLevelUpInfo1(mNumHit / 2, &speed, &level);
        RPPnpMain::GetInstance()->SetLevel(level);
        SetSpeed(speed);
    }
    // 2-player mode (VS). Ignores 'level' parameter.
    else /* EGameType_VS */ {
        RPPnpBinaryMgr::GetInstance()->GetLevelUpInfo1(mNumHit / 2, &speed, NULL);
        SetSpeed(speed);
    }
};

Algorithms

Below is an annotated decompilation of the algorithm used to determine the direction in which the AI paddle will hit the ball (virtual address 80229ba0 in the NTSC 1.1 version):

f32 RPPnpCtrlCpu::GetHitTarget(f32 playerX) const {
    /**
     * Calculate the range which is considered the "middle" of the table.
     *
     * The range of the whole table is roughly [-140, 140].
     * The range of the "middle" section ends up being [-90, 90].
     */
    f32 rangeX = RPPnpMain::GetInstance()->GetTable()->GetWidth();
    rangeX *= 27.0f;           // Model width is always 10.0f, so rangeX = 270.0f
    f32 minX = -rangeX / 3.0f; // -90.0f
    f32 maxX = rangeX / 3.0f;  //  90.0f

    /**
     * The AI randomly chooses between hitting the ball in one of three directions:
     * - Left
     * - Straight
     * - Right
     *
     * Based on where the player paddle is relative to the "middle" section,
     * the AI picks three possible directions based on how difficult they would
     * be for the player to return.
     */
    u32 dirEasy, dirMedium, dirHard;
    if (playerX < minX) {
        // Player paddle is near the left EDGE
        dirEasy = HitDirection_Left;
        dirMedium = HitDirection_Middle;
        dirHard = HitDirection_Right;
    } else if (playerX > maxX) {
        // Player paddle is near the right EDGE
        dirEasy = HitDirection_Right;
        dirMedium = HitDirection_Middle;
        dirHard = HitDirection_Left;
    } else if (playerX < 0.0f) {
        // Player paddle is near the left MIDDLE
        dirEasy = HitDirection_Middle;
        dirMedium = HitDirection_Left;
        dirHard = HitDirection_Right;
    } else { /* playerX > 0.0f */
        // Player paddle is near the right MIDDLE
        dirEasy = HitDirection_Middle;
        dirMedium = HitDirection_Right;
        dirHard = HitDirection_Left;
    }

    /**
     * The "leveltable.bin" file contains probabilities for the easy/medium/hard directions.
     * Addition is because it is a range (0 >= easy threshold > medium threshold > hard threshold).
     */
    const LevelInfo& info = RPPnpBinaryMgr::GetInstance()->GetLevelInfo(mLevel);
    u32 easyProb = info.easyChance;
    u32 mediumProb = easyProb + info.mediumChance;
    u32 x = RPUtlRandom::getU32(info.hardChance + mediumProb);

    /**
     * Determine hit direction based on where the random number falls in the probability range.
     */
    u32 hitDirection = x < easyProb ? dirEasy : (x < mediumProb ? dirMedium : dirHard);

    /**
     * Choose an X-coordinate to aim for based on the chosen hit direction.
     */
    f32 destX;
    switch(hitDirection) {
        case HitDirection_Left:
            destX = minX;
            break;
        case HitDirection_Middle:
            destX = 0.0f;
            break;
        case HitDirection_Right:
            destX = maxX;
            break;
    }

    return destX;
}

Below is a visual reference to the X-coordinate units described in the above code:

Diagram showing how the table is divided into areas