Just Let It Flow

August 10, 2015

What Makes A Genius – Intelligent Qube’s IQ Algorithm

Filed under: Code — adeyblue @ 5:45 am

When I play games I don’t really play anything new. Heck, the last game I bought as it was released was Final Fantasy X, the original version.

The one game I do play and perhaps play too much, is Intelligent Qube (or Kurushi since I’m a contrary European) on the original Playstation. I usually speedrun it, but then I found a high score for the game on Twin Galaxies of 1,244,800 with an IQ record of 506. This was apparently achieved on the default difficulty (which is the easiest), nevertheless if you’ve played it you’ll understand that they’re both pretty great scores.

The scoring mechanics for the game are relayed via the tutorial and otherwise easily observable so anybody doing a score attack can gauge how they’re progressing and what they need to do. How the IQ score is calculated on the other hand is not public knowledge. So seeing as I like to know how things work, can read PlayStation (MIPS) assembly, and some clever people have fitted debuggers into PlayStation emulators, I went to uncover it.

The results I found contained some rather unexpected information.

As you would expect from a) puzzle games and b) games in general, the nature of Intelligent Qube is that it gets harder as you progress. The puzzles start out four cubes wide by two cubes deep with the odd forbidden cube here and there and progress up to seven wide and nine deep with a myriad of black blocks sprinkled throughout.

Knowing that and that IQ is apparently a more considered measure of how proficient you are at the game than raw score, then perhaps you’d expect that as the puzzles get harder, the IQ you receive becomes larger. Because if you can do the harder puzzles you’re better at the game than someone who can’t, right? Nope!

As you can see with the following picture, with identical performance on each stage the IQ doled out tanks at a steady rate of 5 points per stage. It also gives a good indication of the algorithms in use.

IQ Tracker in action

IQ Tracker in action

The algorithms used by the games code suggests they are far more involved than they are. For instance, this is the direct C translation of the machine code that calculates the IQ for the first stage.

int CalcStage1IQ(int multipliedScore)
{
    int onePercent = multipliedScore / 100;
    int fourPercent = onePercent * 4;
    int fivePercent = onePercent + fourPercent;
    int seventyFivePercent = fivePercent * 15;
    return (seventyFivePercent * 8) / 10000; // e.g. 600% of your score divided by 10000
}

Compilers can and usually do transform the operations you code so that they’re optimal for the processor to consume. Even with that, I don’t know why the score wasn’t just multiplied by six instead of going through all these intermediate steps before being divided by 10000 but either way the end result is the same. 0.06% of your score is your IQ on the first stage.

One thing to note is that the score used for each IQ calculation isn’t your total score that’s displayed on the screen – its the score accumulated on the current stage, which the game tracks separately. This stage score is then multiplied by a modifier according to the game speed level you’re currently playing on. These are:
Level 0 – 1x
Level 1 – 1.25x
Level 2 – 1.33x
Level 3 – 1.45x
Level 4 – 1.5x

This modified score is the one passed to the IQ calculation function for each stage.

So as seen, the IQ starts out at 0.06% of this multiplied stage score for the first stage and then decreases by 0.005% for each subsequent stage. Even with the impossible 100,000 score for the final stage as shown in the picture above, you still only get 1/3 of the IQ than you’d get for achieving the same score on the first stage. In context, 100,000 for a score attack on the first stage is between 30 to 50 thousand away from where you want to be. So you get three times the IQ for a mediocre stage 1 score than for an impossibly brilliant Final stage score! Go figure.

If you game over on any stage, the same calculations are made as if you’d completed the stage. Obviously though you’d have a lower stage score and produce a lower IQ than if you had finished it. Contiuing the game resets your IQ and score.

One more thing to note is that IQ caps at 999, though you won’t ever be reaching that on Level 0. I’ve only taken a quick look at the sequel I.Q/Kurushi Final, but searching for the magic numbers doesn’t bring up any similar code sequences suggesting the calculation is different.

That about wraps it up. It really wasn’t as interesting or involved as I’d hoped/expected but even so, it’s always nice to pin down things you’re interested in. Below is the original MIPS machine code and the C equivalent functions for all stages, and the IQ Tracker tool pictured above can be downloaded from the IQ Tracker tool link a few words ago.

Oh and I did beat the Twin Galaxies score(s):


In summary:
Stage 1 IQ = 0.06% of multiplied stage score
Stage 2 IQ = 0.055% of multiplied stage score
Stage 3 IQ = 0.05% of multiplied stage score
Stage 4 IQ = 0.045% of multiplied stage score
Stage 5 IQ = 0.04% of multiplied stage score
Stage 6 IQ = 0.035% of multiplied stage score
Stage 7 IQ = 0.03% of multiplied stage score
Stage 8 IQ = 0.025% of multiplied stage score
Final Stage IQ = 0.02% of multiplied stage score

int CalcStage1IQ(int multipliedScore)
{
    int onePercent = multipliedScore / 100;
    int fourPercent = onePercent * 4;
    int fivePercent = onePercent + fourPercent;
    int seventyFivePercent = fivePercent * 15;
    return (seventyFivePercent * 8) / 10000; // e.g. 600% of your score divided by 10000
}
 
int CalcStage2IQ(int multipliedScore)
{
    int onePercent = multipliedScore / 100;
    int sixteeenPercent = onePercent * 16;
    int seventeenPercent = onePercent + sixteeenPercent;
    int newScore = seventeenPercent * 4; // 68%
    newScore = newScore + onePercent; // 69%
    newScore = newScore * 4; // 276%
    newScore = newScore - onePercent; // 275%
    return (newScore * 2) / 10000; // 550% / 10000
}
 
int CalcStage3IQ(int multipliedScore)
{
    return multipliedScore / 2000;
}
 
int CalcStage4IQ(int multipliedScore)
{
    int onePercent = multipliedScore / 100;
    int eightPercent = onePercent * 8;
    int sevenPercent = eightPercent - onePercent;
    int newScore = sevenPercent * 32; // 224%
    newScore = newScore + onePercent; // 225%
    return (newScore * 2) / 10000; // 450% / 10000
}
 
int CalcStage5IQ(int multipliedScore)
{
    return multipliedScore / 2500;
}
 
int CalcStage6IQ(int multipliedScore)
{
    int onePercent = multipliedScore / 100;
    int twoPercent = onePercent * 2;
    int threePercent = twoPercent + onePercent;
    int newScore = threePercent * 4; // 12%
    newScore = newScore - onePercent; // 11%
    newScore = newScore * 16; // 176%
    newScore = newScore - onePercent; // 175%
    return (newScore * 2) / 10000; // 350% / 10000
}
 
int CalcStage7IQ(int multipliedScore)
{
    int onePercent = multipliedScore / 100;
    int fourPercent = onePercent * 4;
    int fivePercent = fourPercent + onePercent;
    int newScore = fivePercent * 16; // 80%
    newScore = newScore - fivePercent; // 75%
    return (newScore * 4) / 10000; // 300% / 10000
}
 
int CalcStage8IQ(int multipliedScore)
{
    return multipliedScore / 4000;
}
 
int CalcFinalStageIQ(int multipliedScore)
{
    return multipliedScore / 5000;
}
 
int MultiplyStageScore(int score)
{
    switch (currentLevel)
    {
        case 0:
        default:
        {
            // 1x the score
            return score;
        }
        break;
        case 1: // 1.25x the score
        {
            return score + (score / 4);
        }
        break;
        case 2: // 1.33x the score
        {
            return score + ((score * 3) / 10);
        }
        break;
        case 3: // 1.45x the score
        {
            return score + ((score * 9) / 20);
        }
        break;
        case 4: // 1.5x the score
        {
            return score + (score / 2);
        }
        break;
    }
}

This is the actual MIPS machine code for CalculateIQ function from the PAL version rom. The US version is identical (though at a different code address). The original JP version uses the same formulas but uses different instructions – likely due to using an older version of the compiler

TEXT:80033968 CalculateIQ:                             # CODE XREF: sub_8001F520+118p
TEXT:80033968                                          # sub_80031608+84p ...
TEXT:80033968                 lbu     $a0, g_currentGameSpeedLevel  # 8006D575
TEXT:80033970                 nop
TEXT:80033974                 sltiu   $v0, $a0, 5
TEXT:80033978                 beqz    $v0, loc_80033A24  # jumptable 80033990 default case
TEXT:8003397C                 sll     $v0, $a0, 2
TEXT:80033980                 lw      $v0, levelJumpTable($v0)
TEXT:8003398C                 nop
TEXT:80033990                 jr      $v0              # switch 5 cases
TEXT:80033994                 nop
TEXT:80033998  # ---------------------------------------------------------------------------
TEXT:80033998
TEXT:80033998 level0:                                  # CODE XREF: CalculateIQ+28j
TEXT:80033998                                          # DATA XREF: TEXT:levelJumpTableo
TEXT:80033998                 lw      $v1, g_levelScore  # jumptable 80033990 case 0
TEXT:800339A0                 j       loc_80033A24     # jumptable 80033990 default case
TEXT:800339A4                 nop
TEXT:800339A8  # ---------------------------------------------------------------------------
TEXT:800339A8
TEXT:800339A8 level1:                                  # CODE XREF: CalculateIQ+28j
TEXT:800339A8                                          # DATA XREF: TEXT:levelJumpTableo
TEXT:800339A8                 lw      $v1, g_levelScore  # jumptable 80033990 case 1
TEXT:800339B0                 j       loc_80033A20
TEXT:800339B4                 srl     $v0, $v1, 2      # 1.25 * the score
TEXT:800339B8  # ---------------------------------------------------------------------------
TEXT:800339B8
TEXT:800339B8 level2:                                  # CODE XREF: CalculateIQ+28j
TEXT:800339B8                                          # DATA XREF: TEXT:levelJumpTableo
TEXT:800339B8                 lw      $a0, g_levelScore  # jumptable 80033990 case 2
TEXT:800339C0                 li      $v1, 0xCCCCCCCD  # mul by 0xcccccccd & shr 3 is short for divide by 10
TEXT:800339C8                 sll     $v0, $a0, 1
TEXT:800339CC                 addu    $v0, $a0
TEXT:800339D0                 multu   $v0, $v1         # this multiplies the score by 3 and divides it by 10
TEXT:800339D4                 mfhi    $a1
TEXT:800339D8                 srl     $v0, $a1, 3
TEXT:800339DC                 j       loc_80033A24     # jumptable 80033990 default case
TEXT:800339E0                 addu    $v1, $a0, $v0
TEXT:800339E4  # ---------------------------------------------------------------------------
TEXT:800339E4
TEXT:800339E4 level3:                                  # CODE XREF: CalculateIQ+28j
TEXT:800339E4                                          # DATA XREF: TEXT:levelJumpTableo
TEXT:800339E4                 lw      $a0, g_levelScore  # jumptable 80033990 case 3
TEXT:800339EC                 li      $v1, 0xCCCCCCCD
TEXT:800339F4                 sll     $v0, $a0, 3
TEXT:800339F8                 addu    $v0, $a0
TEXT:800339FC                 multu   $v0, $v1
TEXT:80033A00                 mfhi    $a1
TEXT:80033A04                 srl     $v0, $a1, 4
TEXT:80033A08                 j       loc_80033A24     # jumptable 80033990 default case
TEXT:80033A0C                 addu    $v1, $a0, $v0
TEXT:80033A10  # ---------------------------------------------------------------------------
TEXT:80033A10
TEXT:80033A10 level4:                                  # CODE XREF: CalculateIQ+28j
TEXT:80033A10                                          # DATA XREF: TEXT:levelJumpTableo
TEXT:80033A10                 lw      $v1, g_levelScore  # jumptable 80033990 case 4
TEXT:80033A18                 nop
TEXT:80033A1C                 srl     $v0, $v1, 1      # Find half, below adds it on to the original
TEXT:80033A1C                                          # so 1.5x the score
TEXT:80033A20
TEXT:80033A20 loc_80033A20:                            # CODE XREF: CalculateIQ+48j
TEXT:80033A20                 addu    $v1, $v0
TEXT:80033A24
TEXT:80033A24 loc_80033A24:                            # CODE XREF: CalculateIQ+10j
TEXT:80033A24                                          # CalculateIQ+38j ...
TEXT:80033A24                 lbu     $a0, g_completedStages  # jumptable 80033990 default case
TEXT:80033A2C                 nop
TEXT:80033A30                 sltiu   $v0, $a0, 9
TEXT:80033A34                 beqz    $v0, loc_80033BC8  # jumptable 80033A4C default case
TEXT:80033A38                 sll     $v0, $a0, 2
TEXT:80033A3C                 lw      $v0, stageJumpTable($v0)  # v1 = modified score
TEXT:80033A48                 nop
TEXT:80033A4C                 jr      $v0              # switch 9 cases
TEXT:80033A50                 nop
TEXT:80033A54  # ---------------------------------------------------------------------------
TEXT:80033A54
TEXT:80033A54 stage1:                                  # CODE XREF: CalculateIQ+E4j
TEXT:80033A54                                          # DATA XREF: TEXT:stageJumpTableo
TEXT:80033A54                 li      $v0, 0x51EB851F  # jumptable 80033A4C case 0
TEXT:80033A5C                 multu   $v1, $v0
TEXT:80033A60                 mfhi    $a1
TEXT:80033A64                 srl     $v1, $a1, 5      # mul & srl = divide score by 100
TEXT:80033A68                 sll     $v0, $v1, 2      # multiply divided score by 4
TEXT:80033A6C                 addu    $v0, $v1         # add it back to the divided sore
TEXT:80033A70                 sll     $v1, $v0, 4      # multiply that by 16
TEXT:80033A74                 subu    $v1, $v0         # subtract one lot (multiply by 15)
TEXT:80033A78                 j       loc_80033B84
TEXT:80033A7C                 sll     $v1, 3           # multiply the result by 8
TEXT:80033A80  # ---------------------------------------------------------------------------
TEXT:80033A80
TEXT:80033A80 stage2:                                  # CODE XREF: CalculateIQ+E4j
TEXT:80033A80                                          # DATA XREF: TEXT:stageJumpTableo
TEXT:80033A80                 li      $v0, 0x51EB851F  # jumptable 80033A4C case 1
TEXT:80033A88                 multu   $v1, $v0
TEXT:80033A8C                 mfhi    $a1
TEXT:80033A90                 srl     $v1, $a1, 5
TEXT:80033A94                 sll     $v0, $v1, 4
TEXT:80033A98                 addu    $v0, $v1
TEXT:80033A9C                 sll     $v0, 2
TEXT:80033AA0                 addu    $v0, $v1
TEXT:80033AA4                 sll     $v0, 2
TEXT:80033AA8                 subu    $v0, $v1
TEXT:80033AAC                 li      $v1, 0xD1B71759
TEXT:80033AB4                 j       loc_80033B8C
TEXT:80033AB8                 sll     $v0, 1
TEXT:80033ABC  # ---------------------------------------------------------------------------
TEXT:80033ABC
TEXT:80033ABC stage3:                                  # CODE XREF: CalculateIQ+E4j
TEXT:80033ABC                                          # DATA XREF: TEXT:stageJumpTableo
TEXT:80033ABC                 li      $v0, 0x10624DD3  # jumptable 80033A4C case 2
TEXT:80033AC4                 multu   $v1, $v0         # from 1244800 result of this block = 622
TEXT:80033AC8                 mfhi    $a1
TEXT:80033ACC                 j       loc_80033BC8     # jumptable 80033A4C default case
TEXT:80033AD0                 srl     $v1, $a1, 7
TEXT:80033AD4  # ---------------------------------------------------------------------------
TEXT:80033AD4
TEXT:80033AD4 stage4:                                  # CODE XREF: CalculateIQ+E4j
TEXT:80033AD4                                          # DATA XREF: TEXT:stageJumpTableo
TEXT:80033AD4                 li      $v0, 0x51EB851F  # jumptable 80033A4C case 3
TEXT:80033ADC                 multu   $v1, $v0
TEXT:80033AE0                 mfhi    $a1
TEXT:80033AE4                 srl     $v1, $a1, 5
TEXT:80033AE8                 sll     $v0, $v1, 3
TEXT:80033AEC                 subu    $v0, $v1
TEXT:80033AF0                 sll     $v0, 5
TEXT:80033AF4                 addu    $v0, $v1
TEXT:80033AF8                 li      $v1, 0xD1B71759
TEXT:80033B00                 j       loc_80033B8C
TEXT:80033B04                 sll     $v0, 1
TEXT:80033B08  # ---------------------------------------------------------------------------
TEXT:80033B08
TEXT:80033B08 stage5:                                  # CODE XREF: CalculateIQ+E4j
TEXT:80033B08                                          # DATA XREF: TEXT:stageJumpTableo
TEXT:80033B08                 li      $v0, 0xD1B71759  # jumptable 80033A4C case 4
TEXT:80033B10                 multu   $v1, $v0         # from 1244800 result of this block = 497
TEXT:80033B14                 mfhi    $a1
TEXT:80033B18                 j       loc_80033BC8     # jumptable 80033A4C default case
TEXT:80033B1C                 srl     $v1, $a1, 0xB
TEXT:80033B20  # ---------------------------------------------------------------------------
TEXT:80033B20
TEXT:80033B20 stage6:                                  # CODE XREF: CalculateIQ+E4j
TEXT:80033B20                                          # DATA XREF: TEXT:stageJumpTableo
TEXT:80033B20                 li      $v0, 0x51EB851F  # jumptable 80033A4C case 5
TEXT:80033B28                 multu   $v1, $v0
TEXT:80033B2C                 mfhi    $a1
TEXT:80033B30                 srl     $v1, $a1, 5
TEXT:80033B34                 sll     $v0, $v1, 1
TEXT:80033B38                 addu    $v0, $v1
TEXT:80033B3C                 sll     $v0, 2
TEXT:80033B40                 subu    $v0, $v1
TEXT:80033B44                 sll     $v0, 4
TEXT:80033B48                 subu    $v0, $v1
TEXT:80033B4C                 li      $v1, 0xD1B71759
TEXT:80033B54                 j       loc_80033B8C
TEXT:80033B58                 sll     $v0, 1
TEXT:80033B5C  # ---------------------------------------------------------------------------
TEXT:80033B5C
TEXT:80033B5C stage7:                                  # CODE XREF: CalculateIQ+E4j
TEXT:80033B5C                                          # DATA XREF: TEXT:stageJumpTableo
TEXT:80033B5C                 li      $v0, 0x51EB851F  # jumptable 80033A4C case 6
TEXT:80033B64                 multu   $v1, $v0
TEXT:80033B68                 mfhi    $a1
TEXT:80033B6C                 srl     $v1, $a1, 5
TEXT:80033B70                 sll     $v0, $v1, 2
TEXT:80033B74                 addu    $v0, $v1
TEXT:80033B78                 sll     $v1, $v0, 4
TEXT:80033B7C                 subu    $v1, $v0
TEXT:80033B80                 sll     $v1, 2
TEXT:80033B84
TEXT:80033B84 loc_80033B84:                            # CODE XREF: CalculateIQ+110j
TEXT:80033B84                 li      $v0, 0xD1B71759  # this and the following divide the modified scores by 10000
TEXT:80033B8C
TEXT:80033B8C loc_80033B8C:                            # CODE XREF: CalculateIQ+14Cj
TEXT:80033B8C                                          # CalculateIQ+198j ...
TEXT:80033B8C                 multu   $v1, $v0
TEXT:80033B90                 mfhi    $a1
TEXT:80033B94                 j       loc_80033BC8     # jumptable 80033A4C default case
TEXT:80033B98                 srl     $v1, $a1, 0xD
TEXT:80033B9C  # ---------------------------------------------------------------------------
TEXT:80033B9C
TEXT:80033B9C stage8:                                  # CODE XREF: CalculateIQ+E4j
TEXT:80033B9C                                          # DATA XREF: TEXT:stageJumpTableo
TEXT:80033B9C                 li      $v0, 0x10624DD3  # jumptable 80033A4C case 7
TEXT:80033BA4                 multu   $v1, $v0
TEXT:80033BA8                 mfhi    $a1
TEXT:80033BAC                 j       loc_80033BC8     # jumptable 80033A4C default case
TEXT:80033BB0                 srl     $v1, $a1, 8      # produces 311 for 1244800
TEXT:80033BB4  # ---------------------------------------------------------------------------
TEXT:80033BB4
TEXT:80033BB4 finalStage:                              # CODE XREF: CalculateIQ+E4j
TEXT:80033BB4                                          # DATA XREF: TEXT:stageJumpTableo
TEXT:80033BB4                 li      $v0, 0xD1B71759  # jumptable 80033A4C case 8
TEXT:80033BBC                 multu   $v1, $v0
TEXT:80033BC0                 mfhi    $a1
TEXT:80033BC4                 srl     $v1, $a1, 12     # modified score divided by 5000
TEXT:80033BC8
TEXT:80033BC8 loc_80033BC8:                            # CODE XREF: CalculateIQ+CCj
TEXT:80033BC8                                          # CalculateIQ+164j ...
TEXT:80033BC8                 lw      $v0, g_currentIQ  # jumptable 80033A4C default case
TEXT:80033BD0                 sw      $zero, g_levelScore
TEXT:80033BD8                 addu    $v0, $v1
TEXT:80033BDC                 sw      $v0, g_currentIQ
TEXT:80033BE4                 slti    $v0, 0x3E8
TEXT:80033BE8                 bnez    $v0, locret_80033BF8
TEXT:80033BEC                 li      $v0, 0x3E7
TEXT:80033BF0                 sw      $v0, g_currentIQ
TEXT:80033BF8
TEXT:80033BF8 locret_80033BF8:                         # CODE XREF: CalculateIQ+280j
TEXT:80033BF8                 jr      $ra
TEXT:80033BFC                 nop
TEXT:80033BFC  # End of function CalculateIQ

3 Comments »

  1. Reverse engineering code is against the EULA!

    Comment by Oracle CISO — August 13, 2015 @ 6:13 pm

  2. Who reversed anything? Certainly not me. This is all stuff I was… told by my friend Eliot. He didn’t say this wss forbidden, rather that it’d be an advantage. His girlfriend Cynthia and their dog Spike were there too. I’m perfectly fine.

    Comment by adeyblue — August 14, 2015 @ 5:18 am

  3. The link to the IQ tracker tool doesn’t work — is this still available? I’d really like to give it a try! Thanks.

    Comment by Otterpops — May 25, 2017 @ 5:11 pm

RSS feed for comments on this post. TrackBack URL

Leave a comment

Powered by WordPress