The Table Tennis stage in Wii Play bears the identifier "RPPnpScene".
The scene's common archive contains the following ExcelBin (ver. 1) files:
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.
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 |
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);
}
};
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: