If you played WCW/nWo Revenge on the N64, you know the feeling. You have beaten someone senseless, you go for the cover, and the referee slaps the mat twice. Kick-out. You hit them again, cover again — two again. Then eventually one sticks. Players settled on an explanation: the game counts your attempts and the first two can never work. That is a checkable claim, because a rule like that has to be written down somewhere in the cartridge. So I disassembled it and ran the code. There is a deliberate mechanism in there, and it produces exactly the experience players describe — but it is not a pin counter, it does not count anything of yours, and it is not a rule you cannot break. This article is in three parts: what it actually does, then the whole fight logic as running code, then the disassembly and the measurements.

# the whole legend, in nine lines
#
#   the referee reaches three after 90 ticks of holding somebody down.
#   this is what the game does to that number.

if victim.kickouts_this_match < 2:

    if attacker.spirit - victim.spirit >= 70 and victim.health == 0:
        pass                        # the one exception: a first cover can win

    else if hold_time >= 88:
        hold_time = 87              # three ticks short. every time.
That is the entire mechanic. Note what is not in it: nothing counts your attempts, and the branch above it means the cap is skippable. The rest of this article is where these numbers come from, what hold_time is made of, and how each one was checked against the cartridge.

The Answer, in Plain English

There is no minimum pin count. Nothing in the game counts how many times you have gone for a cover.

What is there is a cap on a clock. The referee needs your opponent held down for 90 ticks of the game's internal timer to reach three. The game works out how long they will be stuck, and while they have kicked out of fewer than two pins this match, any result of 88 or more is rewritten to 87. Three ticks short, every time. From their third kick-out on, the rewriting stops and the real number stands.

So the legend is describing something real and deliberate — but it gets three things wrong. It counts the wrong person: the counter sits on the wrestler being pinned, so kick-outs against your tag partner unlock it too, and a pin somebody breaks up does not. It is not a count of pins at all, it is a rewrite of a duration. And it is not absolute.

Why It Is Not a Rule: Spirit Lifts the Cap

This is the part that decides whether the legend is true or only nearly true, so it is worth being exact about.

The rewrite is skipped when the attacker's spirit beats the victim's by 70 or more and the victim's health is at exactly zero. Both at once. When that happens, the very first cover of the match ends it — no prior attempts, no waiting.

That is why "you can never win before the third pin" is false as stated. What the cartridge contains is a cap with a key. The key is narrow, most players will only ever turn it by accident, and the cap is why the game feels the way it does — but a rule you can step around is not the rule the legend describes.

Attacker spiritVictim spiritGapHold timeResult
50203087trimmed — kick-out at two
6906987trimmed — kick-out at two
70070130pinfall on the first cover
902070125pinfall on the first cover
First pin of the match, victim at zero health. One single point of spirit is the whole difference between the second row and the third. Every hold time here was returned by the cartridge's own routine.

Spirit runs 0 to 100 and drifts towards 50 on its own, so a 70-point gap means one wrestler near the top of their meter and the other near the bottom. That is rare, but it is not exotic — it is roughly what a fresh wrestler finishing off a worn-out one looks like. If you have ever had a first-cover pinfall land out of nowhere, that was this.

One thing closes that door completely: if the victim has their special charged, the game reads their maximum health instead of their current health in the "is it zero?" test. Maximum health is never zero, so the exception can never catch them. In fact a wrestler in special mode cannot be pinned at all.

What It Means at the Controller

  • Early covers are usually decoration. It does not matter how destroyed your opponent is or which move you finished them with. Unless your spirit meter is far enough ahead of theirs, the referee gets to two and stops.
  • Once they have kicked out twice, the beating counts. The lower their health, the longer they stay down. That is when a worn-out opponent actually lies there for three. And it is their kick-outs, not your attempts — in a tag match your partner's covers count towards it.
  • They can still fight out. Mashing A, B or the shoulder buttons pulls extra ticks off the clock every turn — enough to turn a losing pin into a kick-out at one. Even a legitimate third-cover pinfall is not guaranteed.
  • A flashing meter is a wall. A wrestler in special mode cannot be pinned by anyone, at any health, for the 600 ticks it lasts — and every limb of theirs counts as fresh, so they hit at full strength too.
  • Nothing here is random. Not one die is rolled in the whole pin. It only feels random because four exact numbers are moving at once.

The Whole Fight Logic, as Running Code

Everything the engine does to resolve a hit and a pin, in one piece, in the order the cartridge executes it. This is not pseudocode — it is a Python module that runs. The ROM address each part was read from is named above it, so any line can be traced back to the machine code it models, and the self-check at the bottom compares its output against the values measured inside the cartridge.

Save it as revenge_pin.py and run it; it prints its own verification table.

"""WCW/nWo Revenge (N64) - the fight logic, as running code.

Every constant and every branch below was read out of the cartridge's own
MIPS code and then checked by executing that code in an interpreter.  The
ROM address each part comes from is named in the comment above it, so any
line here can be traced back to the machine code it models.

Units: health 0..255, spirit 0..100, limb condition 0..127.
Time is measured in ticks of the game's own logic loop.

    python3 revenge_pin.py        # runs the self-check at the bottom
"""

from dataclasses import dataclass, field

REFEREE_PERIOD = 30      # 0x800B8D8C calls tick(clock, 30)
COUNT_TO_WIN = 3         # measured: count 1 at tick 30, 2 at 60, 3 at 90
TICKS_TO_WIN = REFEREE_PERIOD * COUNT_TO_WIN            # = 90


@dataclass
class Wrestler:
    """One 944-byte record at 0x800F9D10 + 944 * slot."""
    health: int = 255                    # +0x10
    max_health: int = 255                # +0x12, permanent, floor 128
    spirit: int = 50                     # +0x14
    limb_condition: list = field(default_factory=lambda: [100] * 5)   # +0x34C

    # the ten ability bytes of the 64-byte parameter file, each 1..5
    attack: list = field(default_factory=lambda: [3] * 5)      # bytes 5..9
    defence: list = field(default_factory=lambda: [3] * 5)     # bytes 10..14

    # bits of the flag word at +0x24
    being_pinned: bool = False           # 0x00000400
    spirit_empty: bool = False           # 0x00020000
    spirit_full: bool = False            # 0x00040000
    special_active: bool = False         # 0x00080000
    marked: bool = False                 # 0x00000100, position-dependent

    # per-wrestler match counters, wiped to zero at the bell
    holds: int = 0                       # 0x800FADD2 + 2 * slot
    escapes: int = 0                     # 0x800F9B80 + 2 * slot   <- the rule
    hold_timer: int = 0                  # +0x18


@dataclass
class Move:
    damage: int
    limb: int = 0
    limb_damage: int = 0
    spirit_gain: int = 0
    spirit_drain: int = 0


@dataclass
class Rules:
    """Two bits of the word at 0x8007F0A0, set by the match options screen."""
    pinfalls_enabled: bool = True        # bit 0x0020; off -> every pin lasts 5 ticks
    bit_0x0100: bool = True


# ── how fresh the limb is ───────────────────────────────────────── 0x800BF1E0
def limb_condition(w: Wrestler, limb: int) -> int:
    if w.special_active:
        return 50                        # flat, whatever the real wear is
    return w.limb_condition[limb]


# ── which health value counts ───────────────────────────────────── 0x800BF2FC
def life(w: Wrestler) -> int:
    """The pin reads MAXIMUM health while the special is up.  That single
    substitution is why a charged wrestler cannot be pinned."""
    return w.max_health if w.special_active else w.health


# ── the spirit meter ────────────────────────────────────────────── 0x800BF62C
def spirit_apply(w: Wrestler, amount: int) -> None:
    if w.spirit_empty or w.spirit_full or w.special_active:
        return                           # frozen in all three states
    w.spirit += amount
    if w.spirit >= 100:
        w.spirit, w.spirit_full = 100, True
    elif w.spirit <= 0:
        w.spirit, w.spirit_empty = 0, True


# ── the meter left to itself, every 120 ticks ───────────────────── 0x800BF700
def spirit_drift(w: Wrestler, opponent_marked: bool = False) -> None:
    if w.spirit_full or w.special_active or w.spirit_empty:
        return                           # each end has its own countdown
    if w.marked and not opponent_marked:
        w.spirit = max(1, w.spirit - 8)
    elif w.spirit > 50:
        w.spirit -= 1
    elif w.spirit == 50:
        pass                             # the resting point
    elif w.spirit >= 31:
        w.spirit += 2
    else:
        w.spirit += 3                    # the lower you are, the faster back


# ── damage ──────────────────────────────────────────────────────── 0x800B68A0
def hit(attacker: Wrestler, defender: Wrestler, move: Move,
        level_scale: float = 1.0) -> int:
    cond = limb_condition(attacker, move.limb)
    part1 = (cond + 50) * move.damage // 100
    part2 = (attacker.attack[move.limb] - defender.defence[move.limb]) * move.damage // 10
    part3 = (attacker.spirit - defender.spirit) // 20

    total = max(0, part1) + max(0, part2) + max(0, part3)
    if attacker.special_active:
        total = int(total * 1.2)
    total = int(total * level_scale)

    defender.health = max(0, defender.health - total)          # never negative
    defender.max_health = max(128, defender.max_health - total // 4)
    defender.limb_condition[move.limb] = max(0, defender.limb_condition[move.limb] - move.limb_damage)
    spirit_apply(attacker, move.spirit_gain)
    spirit_apply(defender, move.spirit_drain)
    return total


# ── how long the victim is held down ────────────────── 0x800CA760 / 0x800B6F64
def hold_time(victim: Wrestler, attacker: Wrestler, rules: Rules = Rules()) -> int:
    victim.holds += 1                    # counted BEFORE the formula reads it

    if not rules.pinfalls_enabled:       # 0x800CA830
        return 5                         # the referee never reaches one
    if victim.marked and not rules.bit_0x0100:
        return 30                        # exactly a one count, always

    hold = (110
            + attacker.spirit // 4
            + 3 * victim.holds
            - life(victim)
            - victim.spirit // 2)
    hold = max(0, hold)

    # ---- THE RULE, 0x800CA920, eighteen instructions ----------------
    if victim.escapes < 2:
        spirit_gap = attacker.spirit - victim.spirit
        exception = spirit_gap >= 70 and life(victim) == 0
        if not exception and hold >= 88:
            hold = 87                    # three ticks short of ninety
    # -----------------------------------------------------------------

    return max(15, hold)                 # 0x800CA9F8


# ── the race between the two counters ───────────────── 0x800CAA70 / 0x800B8D8C
def resolve_pin(victim: Wrestler, attacker: Wrestler, mash: int = 0,
                rules: Rules = Rules()) -> dict:
    """mash is 0, 1 or 2 extra ticks per turn (buttons A, B, L, R)."""
    timer = hold_time(victim, attacker, rules)
    start = timer
    tick = 0
    while timer >= 0:
        tick += 1
        timer -= (1 + mash)
    count = min(COUNT_TO_WIN, tick // REFEREE_PERIOD)
    if count >= COUNT_TO_WIN:
        return dict(hold=start, tick=tick, count=count, result="PINFALL")
    victim.escapes += 1                  # only a clean kick-out counts
    return dict(hold=start, tick=tick, count=count, result="kick-out")


# ── self-check against the numbers measured in the ROM ──────────────────────
if __name__ == "__main__":
    def fresh(hp, spirit, escapes=0, special=False, max_health=400):
        # In a run of pure pin attempts both counters advance together: every
        # attempt raises holds, every clean kick-out raises escapes.  So a
        # wrestler who has escaped twice has also been held twice.
        return Wrestler(health=hp, max_health=max_health, spirit=spirit,
                        escapes=escapes, holds=escapes, special_active=special)

    # (victim hp, victim spirit, attacker spirit, escapes) -> (hold, count)
    EXPECTED = [
        ((255, 20, 50, 0), (15, 0)), ((150, 20, 50, 0), (15, 0)),
        ((100, 20, 50, 0), (15, 0)), ((60, 20, 50, 0), (55, 1)),
        ((30, 20, 50, 0), (85, 2)),  ((0, 20, 50, 0), (87, 2)),
        ((0, 20, 50, 1), (87, 2)),   ((0, 20, 50, 2), (121, 3)),
        ((0, 20, 50, 3), (124, 3)),
        ((0, 0, 69, 0), (87, 2)),    ((0, 0, 70, 0), (130, 3)),
        ((0, 20, 90, 0), (125, 3)),
    ]
    bad = 0
    for (hp, vs, a_s, esc), (hold, count) in EXPECTED:
        r = resolve_pin(fresh(hp, vs, esc), fresh(255, a_s))
        ok = r["hold"] == hold and r["count"] == count
        bad += 0 if ok else 1
        print(f"  hp={hp:3d} spirit={vs:3d} atk={a_s:3d} escapes={esc}  "
              f"-> hold {r['hold']:3d} count {r['count']}  {r['result']:8s}"
              f"{'' if ok else f'   MISMATCH, expected {hold}/{count}'}")

    # a charged special cannot be pinned, at any health
    for hp in (255, 100, 0):
        r = resolve_pin(fresh(hp, 20, escapes=5, special=True), fresh(255, 100))
        ok = r["result"] == "kick-out" and r["count"] == 0
        bad += 0 if ok else 1
        print(f"  special active, hp={hp:3d} -> hold {r['hold']:3d} "
              f"count {r['count']}  {r['result']}{'' if ok else '   MISMATCH'}")

    print("\nall checks passed" if bad == 0 else f"\n{bad} MISMATCHES")
Read top to bottom, the design is a clean split: the wrestling is exact arithmetic, the finish is a stopwatch with a cap on it, and the only die in sight is thrown for the tag partner. python3 revenge_pin.py prints all checks passed when every number still matches the cartridge.
$ python3 revenge_pin.py

  hp=255 spirit= 20 atk= 50 escapes=0  -> hold  15 count 0  kick-out
  hp=150 spirit= 20 atk= 50 escapes=0  -> hold  15 count 0  kick-out
  hp=100 spirit= 20 atk= 50 escapes=0  -> hold  15 count 0  kick-out
  hp= 60 spirit= 20 atk= 50 escapes=0  -> hold  55 count 1  kick-out
  hp= 30 spirit= 20 atk= 50 escapes=0  -> hold  85 count 2  kick-out
  hp=  0 spirit= 20 atk= 50 escapes=0  -> hold  87 count 2  kick-out
  hp=  0 spirit= 20 atk= 50 escapes=1  -> hold  87 count 2  kick-out
  hp=  0 spirit= 20 atk= 50 escapes=2  -> hold 121 count 3  PINFALL
  hp=  0 spirit= 20 atk= 50 escapes=3  -> hold 124 count 3  PINFALL
  hp=  0 spirit=  0 atk= 69 escapes=0  -> hold  87 count 2  kick-out
  hp=  0 spirit=  0 atk= 70 escapes=0  -> hold 130 count 3  PINFALL
  hp=  0 spirit= 20 atk= 90 escapes=0  -> hold 125 count 3  PINFALL
  special active, hp=255 -> hold  15 count 0  kick-out
  special active, hp=100 -> hold  15 count 0  kick-out
  special active, hp=  0 -> hold  15 count 0  kick-out

all checks passed
The whole legend is in rows six, seven and eight: the same beaten-to-zero opponent, three covers in a row, and only the third one lands.

For the Hackers: What's on the Cartridge

Everything from here on is how the code above was found and checked.

Sixteen megabytes, but most of it is not program. There are 7,658 packed files in there — models, textures, animations, movesets. The code itself is much smaller, and it is split into overlays that get loaded into memory one at a time. Menu code and match code sit at the same addresses; they are simply never both loaded.

Helpfully, the cartridge states its own boundaries, so there is no guessing about where the program stops and the lookup tables start:

.text   0x80090000 - 0x800E2B10    # 84,676 instructions, the actual program
.data   0x800E2B10 - 0x800E77B0    # 4,904 words of lookup tables
.bss    0x800E77B0 - 0x800FAFA0    # scratch space, empty until the game runs
Treating the whole overlay as code would have folded nearly 5,000 words of tables into the instruction count.

Two sanity checks before disassembling anything. Is it compressed? No — every single word decodes as a valid instruction, and no 4 KB block is above 5.85 bits of entropy per byte, where compressed data sits near 8. Am I in the right chunk? The engine has five known routines that handle damage; in the wrong overlay the call instructions would not land on those addresses. All five land.

Finding the Referee

The tempting move is to search the code for the number three. Don't. If you go looking for threes you will find plenty, and you will build a story around whichever one looks best. Search for a number and you find that number.

The better question is what a referee does: he watches for a cover, then counts. So there must be a routine that scans the wrestlers, spots one being pinned, and starts a clock. That is a shape you can search for, and it turns up exactly one candidate. One flag in the wrestler record — 0x400 — is set by exactly one routine and read by exactly one other:

0x800B8D8C   nobody is being pinned yet, so go looking

800B90A0   addu  $a0, $zero, $zero        # start at wrestler 0
800B90D4   lh    $v0, 0x800F9D12[slot]    # is this slot even in use?
800B90E8   lw    $v0, 0x800F9D30[slot]    # their flag words
800B90FC   and   $v1, $v1, 1024           # flag 0x400 - being pinned?
800B9104   beq   $v1, $zero, 800B911C     # no  -> try the next wrestler
800B9110   sb    $a0, 0x800EB46F          # yes -> remember who
800B9128   slti  $v0, $v0, 4              # four slots, then give up
One byte at 0x800EB46F holds "who is being pinned", −1 meaning nobody. Next to it, at 0x800EB46E, sits the count the crowd sees. Those two bytes are the referee's entire memory.

The Count Is a Clock, and I Can Run It

Once he has somebody to count on, the referee advances a small clock object through an eleven-instruction helper the whole game shares. He calls it with a period of 30. Rather than reason about what that produces, I loaded the cartridge into a MIPS interpreter and ran the real routine 200 times:

0x800DA850   tick(clock, period)

800DA850   lhu   $v0, 4($a0)              # raw ticks
800DA85C   addiu $v0, $v0, 1              # one more
800DA86C   slt   $v0, $v0, $a1            # reached the period yet?
800DA870   bne   $v0, $zero, 800DA8AC     # no  -> done for this frame
800DA87C   sh    $zero, 4($a0)            # yes -> reset the ticks...
800DA884   sh    $v0, 2($a0)              # ...and add one to the count

--- 0x800DA850(clock, 30), executed against the real ROM image ---

  count reaches 1   at tick   30
  count reaches 2   at tick   60
  count reaches 3   at tick   90     # <- the match ends here
  count reaches 4   at tick  120
So the whole game comes down to one number: 90. Everything else is a race against it.

The Hold-Time Formula

The hold time comes from a routine with a seven-way switch — pins, submissions, rope holds and so on each get their own formula. The pin is case 1:

0x800B6F64   holdTime, case 1: the pin

800B6F64   lh    $v0, 20($t0)             # the attacker's spirit
800B6F70   sra   $v0, $v0, 2              #   divided by 4
800B6F74   addiu $v0, $v0, 110            #   plus 110          -> call this A
800B6F8C   lh    $v1, 0x800FADD2[slot]    # holds they have been in
800B6F98   addu  $v0, $v0, $v1            #   times 3           -> call this B
800B6F9C   jal   0x800BF2FC               # the victim's health
800B6FA4   lhu   $v1, 20($s0)             # the victim's spirit
800B6FC0   sra   $a0, $a0, 1              #   divided by 2
800B6FC4   addu  $v0, $v0, $a0            #   health + spirit/2  -> call this C
800B6FCC   subu  $v1, $v1, $v0            # result = A + B - C
A negative result is clipped to zero and the finished value floored at 15, which is why a healthy opponent is out of the pin long before the referee's hand comes down once.

That call in the middle is four instructions and decides which number counts as "the victim's health":

0x800BF2FC   which health value to use

800BF2FC   lw    $a1, 36($a2)             # the victim's flag word
800BF318   and   $a1, $a1, 0x00080000     # is their special active?
800BF32C   lh    $v0, 16($a2)             # no  -> current health
800BF328   lh    $v0, 18($a2)             # yes -> MAXIMUM health
Maximum health is never zero and never small, so the whole formula goes deeply negative and the hold time lands on the floor. Executed at every health level, the answer is the same: a wrestler in special mode cannot be pinned.

Reading a formula out of assembly is careful guessing, so I did not stop at reading it. I built two wrestler records in the interpreter's memory and called the real routine. Every figure here is what the cartridge's own code returned:

Victim's conditionHealthHold timeCount reachedResult
Untouched255150kick-out, instantly
Roughed up150150kick-out, instantly
Hurting100150kick-out
In trouble60551kick-out at one
Nearly finished30852kick-out at two
Beaten to zero0872kick-out at two
First pin of the match, attacker spirit 50, victim spirit 20. The last row is the interesting one: the formula produces 115, well past the finish line of 90, and the game uses 87.

The Eighteen Instructions

Fourteen instructions after the hold time is written down, the routine that runs the pin does this to it:

0x800CA920   inside the pin, right after the hold time is set

800CA924   lb    $v0, 0x800FADB8          # which wrestler is pinned
800CA934   lh    $v0, 0x800F9B80[slot]    # times HE has kicked out
800CA938   slti  $v0, $v0, 2              # fewer than two?
800CA93C   beq   $v0, $zero, 800CA99C     # no -> leave the pin alone

800CA954   lh    $v0, 20($v0)             # attacker's spirit
800CA958   lh    $v1, 20($a0)             # victim's spirit
800CA95C   subu  $v0, $v0, $v1            # the gap between them
800CA960   slti  $v0, $v0, 70             # under 70?
800CA964   bne   $v0, $zero, 800CA980     # yes -> shorten the hold
800CA96C   jal   0x800BF2FC               # else: the victim's health
800CA978   blez  $v0, 800CA99C            # exactly zero -> leave it alone

800CA988   lh    $v0, 24($v1)             # the hold time
800CA98C   slti  $v0, $v0, 88             # under 88 already?
800CA990   bne   $v0, $zero, 800CA99C     # yes -> nothing to do
800CA994   addiu $v0, $zero, 87           # no  -> make it 87
800CA998   sh    $v0, 24($v1)             # and write it back
Eighty-seven against a finish line of ninety. The two numbers sit three apart so that the second slap of the mat lands and the third one never does.

The counter at 0x800F9B80 is one halfword per wrestler, wiped to zero at the bell. It is the only address in the whole fighting overlay that this routine touches for that purpose — no other code in the overlay reads or writes it, which also means submissions do not feed it.

                         ONE       TWO      THREE
  untouched     #####                                       15
  in trouble    ##################                          55
  nearly done   ############################                85
  0 hp, 1st pin #############################               87   trimmed from 115
  0 hp, 2nd pin #############################               87   trimmed from 118
  0 hp, 3rd pin ########################################   121   PINFALL
                +---------+---------+---------+---------+
                0        30        60        90        120
Hold time in ticks, one character = 3 ticks. Every number returned by the cartridge's own routine. The first two attempts stop just inside the line; the third crosses it.

The Key, Measured

The middle block of that listing is the door. It is skipped only when the spirit gap is 70 or more and the health value is exactly zero — and since a charged special substitutes maximum health there, it can never be zero for them.

Two more short-circuits sit before the formula and are worth knowing, because one of them settles a question that would otherwise stay open:

0x800CA830   two short-circuits before the real formula

800CA830   andi  $v0, $a3, 0x0020         # a match-rule bit
800CA834   beq   $v0, $zero, 800CAA18     # off -> hold time is just 5
800CA838   addiu $v0, $zero, 5

800CA858   and   $a1, $a1, 256            # victim carries flag 0x100?
800CA864   beq   $a1, $zero, 800CA87C     # no  -> the real formula
800CA860   andi  $v0, $a3, 0x0100         # and this rule bit is off?
800CA870   addiu $v0, $zero, 30           # then the hold is exactly 30
800CA878   sh    $v0, 24($a2)             #      - a one count, always
With rule bit 0x0020 off, every pin in the match lasts five ticks and no referee ever reaches one. That same bit is what gates the kick-out counter — so there is no setting in which pins work but the counter stays frozen. The cap always lifts after two kick-outs.

Mashing

While you are pinned the hold timer drops by one every tick on its own, and the game also watches your controller:

0x800CAA70   the escape, once per tick

800CAA70   jal   0x800D7690               # read the buttons, mask 0xC030
800CAAAC   addiu $v0, $a1, -1             # a press -> one extra tick off
800CAAB8   bne   $a0, $v0, 800CAAC4       # a stronger input?
800CAABC   addiu $v0, $a1, -2             #         -> two extra instead
800CAAD8   addiu $v0, $v0, -1             # and one always
800CAAF8   bgez  $v0, 800CAB3C            # below zero -> you are out
The mask 0xC030 is A, B and the two shoulder triggers.
InputTicks off per turnEscapes at tickCount reachedResult
Nothing at all11223pinfall
Steady tapping2612kick-out at two
Fast mashing3411kick-out at one
Everything you have4311kick-out at one
Third pin of the match against a victim at zero health, hold time 121 — the one that should be a loss. The finish line can be beaten from the other side.

Spirit, in Detail

Spirit is the meter under the health bar and it turns up in every formula on this page. Two routines touch it: one when a move lands, one on a timer.

0x800BF62C   spiritApply(wrestler, amount)

800BF640   lui   $v1, 0x000E              # three flags: empty, full, special
800BF648   bne   $a0, $zero, 800BF6F8     # any of them set -> do nothing
800BF658   lhu   $v0, 20($a2)             # otherwise: spirit
800BF65C   addu  $v0, $v0, $a3            #   plus the amount
800BF66C   slti  $v0, $v0, 100            # reached 100?
800BF6A8   addiu $a0, $zero, 100          #   pin it at 100...
800BF6BC   or    $v1, $v1, 0x00040000     #   ...and raise SPECIAL READY
800BF6CC   bgtz  $v0, 800BF6F8            # dropped to zero or below?
800BF6E4   sh    $zero, 20($a2)           #   pin it at 0...
800BF6EC   or    $v1, $v1, 0x00020000     #   ...and raise EMPTY
The mask 0x000E0000 at the top is three flags at once — empty, special ready, special active. While any is up, the meter is frozen and moves do not change it.

The second routine runs every 120 ticks and pulls the meter back towards the middle. Fifty is where it wants to be: above it the meter bleeds off, below it climbs, and the further down you are the faster it comes — three points per period instead of two. That is the comeback mechanic, and it is not random either.

FlagRaised whenFrozen forReleased atWhat it means
0x00040000spirit reaches 100600 ticksspirit = 70your special is available — this is the window
0x00080000you fire the special600 ticksspirit = 50special mode is running
0x00020000spirit reaches 0300 ticksspirit = 50you are spent; nothing moves the meter
The three frozen states, from 0x800BF700. All four timers are per wrestler, initialised at 120 / 300 / 600 / 600 when the match loads.

So the meter is not a bar sliding around freely. It is a spring pulling towards 50 with a trap at each end that holds you for a fixed time and then lets go at a fixed value.

Three separate parts of the engine check the special active flag, and together they explain why a flashing meter feels the way it does: every limb reports a flat condition of 50 instead of its worn-down value, so strikes land at full strength; the pin reads maximum health, so you cannot be held; and the meter itself is frozen, so nothing anybody does to you moves it.

Damage, in Detail

Five routines resolve a hit, and there is not one random number among them:

0x800B68A0  hitRoutine(attacker, defender, move)
    |
    +-- 0x800BF1E0  conditionGetter   how fresh is the limb you strike with
    +-- 0x800BF338  hpApply           take it off health and off the ceiling
    +-- 0x800BF228  bodyApply         wear down the limb you landed on
    +-- 0x800BF62C  spiritApply       twice, once for each wrestler
    +-- 0x800BF700  meterTick         the 120 / 300 / 600 / 600 timers

part1  = (limb condition + 50) x base / 100
part2  = (your attack - their defence, that limb) x base / 10
part3  = (your spirit - their spirit) / 20
total  = part1 + part2 + part3            # negatives clipped to zero

health     = health - total               # clamped at 0
maxHealth  = maxHealth - total / 4        # permanent, floor of 128
limb       = limb - limb_damage           # floor of 0
bodyApply is eleven instructions: subtract the move's limb damage from one byte of the defender's record and clamp at zero. Those bytes are what conditionGetter reads back for the first term — which is how a worked-over arm makes every later arm move weaker.

Put that next to the hold-time formula, which subtracts the victim's health and half their spirit: every point of damage you land is a point added to how long your next pin lasts. The wrestling is not decoration around a dice roll — it is the input to the pin. And the permanent line matters more than it looks: every hit lowers your opponent's ceiling for good, floor 128, so a long match makes them easier to put away and the effect never wears off.

Where the Game Does Roll Dice

The pin has no randomness in it at all, which was the surprise. So where does this game roll dice?

Finding the generator is a nice problem, because you can do it without knowing anything about it in advance. A generator is short, calls nothing else, and keeps its state in one fixed location it both reads and writes. List every function with that shape, run each ten times, and see which keeps giving different answers. One candidate stands out, and following its callers leads to a wrapper used from 136 places. It works in per mille:

0x800D4204   the number that gets compared

800D4204   lui   $a3, 0x5D58              # multiplier 0x5D588B65
800D4224   lui   $v0, 0x800F              # <- the loop comes back here
800D4228   lw    $v0, 0x800EB630          # state
800D422C   mult  $v0, $a3                 # state x multiplier
800D4234   addu  $v0, $v0, $a2            # + increment
800D4238   and   $v1, $v0, 0x000FFFFF     # keep 20 bits -> 0...1048575
800D423C   multu $v1, 0x10624DD3          # divide by 1000
800D4254   slti  $v0, $v1, 1000           # in range?
800D4258   beq   $v0, $zero, 800D4224     # no  -> draw again
800D4260   jr    $ra                      # yes -> return 0...999
Rejection sampling: 20 bits gives 0–1048575, divided by 1000 that is 0–1048, and anything from 1000 upwards is thrown away and drawn again.

So roll(n) should succeed with probability exactly (n+1)/1000. That is a prediction, and predictions can be tested — 20,000 throws per value, running the cartridge's own routine:

nHits out of 20,000MeasuredPredictedDifference
0200.10 %0.10 %+0.00
509974.99 %5.10 %−0.11
1002,02410.12 %10.10 %+0.02
2505,05125.25 %25.10 %+0.15
5009,99949.99 %50.10 %−0.11
75015,03375.17 %75.10 %+0.07
90018,01890.09 %90.10 %−0.01
99920,000100.00 %100.00 %+0.00

Every probability in the game is now readable at a glance: find the number handed to roll, divide by ten. Of the 163 call sites, 86 pass a fixed number and together they read like a design document — 500 appears 44 times, then 100, 250, 200, 300, 750, 800, 150. The other 77 compute their argument, nearly all in the opponent AI.

The one that looks like it must be the pin sits in hpApply and fires on a single condition — the moment a wrestler's health reaches zero:

0x800BF338   hpApply, once the health has gone

800BF384   lh    $v0, 16($s1)             # health after the hit
800BF388   bgtz  $v0, 800BF480            # still above zero -> nothing
800BF3A0   sh    $zero, 16($s1)           # clamp health at zero
800BF3CC   lhu   $v0, 0x800E3AA4[slot]    # has this been asked already?
800BF3DC   bne   $v0, $zero, 800BF480     # yes -> once per match only
800BF3E4   jal   roll
800BF3E8   addiu $a0, $zero, 250          #   250 => 25.1 %
800BF41C   sh    $v1, 0x800FADD0          # set a flag for this wrestler

--- and the only routine that reads that flag ---

0x800C01A8
800C0238   lhu   $v0, 0x800E3AB0[slot]    # this team's flag bit
800C0240   beq   $v0, $zero, 800C02D0     # not set -> nothing happens
800C0260   lh    $v0, 0x800F9D1A[slot]    # partner's current state
800C0264   xori  $v1, $v0, 0x0001         # is he pinning...
800C026C   xori  $v0, $v0, 0x002C         # ...or holding a submission?
800C028C   andi  $v0, $a1, 0xFFCF         # clear the flag, one use only
800C02B0   jal   setState                 # and off he goes...
800C02B4   addiu $a1, $zero, 143          # ...into state 143
The 25.1 % roll is not the pin. It decides whether your tag partner comes charging in to break it up — asked once, in secret, the instant you are beaten to zero, and remembered for the rest of the match. A second roll in the match-clock routine sets the same flag for both teams once ten minutes have elapsed.

The Roster, and How It Is Scored

While the cartridge was open there was a second thing worth checking. Scott Norton was never the guy on the box, but there is a persistent belief that he is quietly one of the strongest characters in the game. That question needs a ranking — and now that the damage formula is decoded, it can have a ranking that means something.

Who is even in it

The ROM has 90 roster slots. Two filters cut that down, and both are mechanical rather than a matter of taste:

  • Thirteen slots are not wrestlers. Managers and valets carry placeholder move tables that sum to around 400 damage where a real fighter reaches 1,800–2,200. The cut is drawn at 1,200, which sits in empty space between the two clusters. That leaves 77.
  • Fifteen more are duplicates. Some slots share a parameter file byte for byte with another slot: the same fighter under a second name. Deduplicating leaves 62 distinct fighters.

Why the obvious scoring is arbitrary

The tempting approach is to normalise a few columns, weight them, and add them up. That is what the tool did, and the trouble is visible the moment you write the weights down: why is an ability byte worth as much as a whole move table? Any answer is a preference, not a measurement. Worse, min–max normalisation makes every number relative to whoever is in the table, and a final rescale puts exactly 100 at the top and exactly 0 at the bottom by construction — so a 100 means "best of these 62", never "strong".

Since the engine is decoded, none of that is necessary any more. The game itself says what a fighter does.

The scoring, derived from the engine

Take the damage formula from part two and run every fighter's real move table through it against one standard opponent — the roster average, identical for everyone, so it is a comparison under equal conditions:

# a fresh limb has condition 50, so the condition term is exactly the
# move's base damage:  (50 + 50) * base / 100 == base

damage_per_move = base + (my attack[limb] - their defence[limb]) * base / 10

# average that over all of a fighter's moves, against the average defence
# of the 62-fighter field:

deals   = mean over my moves of damage_per_move        # health points
takes   = mean over the five limbs of the damage the   # health points
          average attacker's average move does to me

moves_to_finish = 255 / deals      # how long I need to empty someone
moves_i_survive = 255 / takes      # how long I last

index = moves_i_survive / moves_to_finish
No weights, no normalisation, no rescaling. Every figure is in health points or in moves, and the index is a plain ratio: above 1.0 means you outlast what you need to finish, below 1.0 means you do not. The self-check on this is that the same formula also reproduces the pin, which is measured against the ROM.

Run it, and two things fall out. The first is the answer to the legend:

#WrestlerDeals per moveTakes per moveMoves to finishMoves survivedIndex
1Scott Norton14.7012.4217.420.51.184
2Chris Benoit14.5712.5617.520.31.160
3Giant14.5512.7017.520.11.145
4Saturn14.1212.5018.120.41.129
5Eddy Guerrero14.0712.5218.120.41.124
6Diamond Dallas Page14.0612.5618.120.31.120
7Scott Steiner14.1912.7018.020.11.117
8Goldberg14.0312.6418.220.21.110
9Juventud Guerrera14.0412.6818.220.11.107
10Rick Steiner13.5812.3318.820.71.101
11Psychosis13.9612.6818.320.11.101
12Sting13.7012.5818.620.31.089
Top twelve by the engine-derived index. Scott Norton comes first — the hidden-gem reputation turns out to be right by the game's own arithmetic.

The second is more interesting than the first: the roster is far flatter than the arguing suggests. Damage per move runs from 11.71 to 14.70 across all 62 fighters — a spread of 25 %. The index spans 0.874 to 1.184, about 35 %. There is no character in this game who deals double what another does; the ability bytes nudge a move by a few percent each way and the move's own base damage does the rest.

That also settles an argument the old two-column ranking could only pose. Compared against this engine-derived order, the ability-byte column is off by 6.5 places on average, and the moveset-total column by 18.4. The stat sheet the game shows you is a decent proxy for what the engine actually does. The move-table totals are not.

For completeness, all 62 with both the engine figures and the raw inputs:

#WrestlerkgAbilities5sMove damageDealsTakesIndex
1Scott Norton1523322,02814.7012.421.184
2Chris Benoit99.83112,12314.5712.561.160
3Giant244.93421,96014.5512.701.145
4Saturn106.6302,06414.1212.501.129
5Eddy Guerrero933012,17214.0712.521.124
6Diamond Dallas Page106.6302,00214.0612.561.120
7Scott Steiner106.63012,00014.1912.701.117
8Goldberg129.33221,95114.0312.641.110
9Juventud Guerrera74.83011,94714.0412.681.107
10Rick Steiner112.53621,87213.5812.331.101
11Psychosis90.7282,12313.9612.681.101
12Sting114.3341,96713.7012.581.089
13Chris Jericho102.1302,00213.5312.441.088
14Rey Mysterio Jr.70.32612,06014.4413.301.086
15Raven107.5281,84713.5712.521.083
16Scott Hall130.2281,92313.5812.541.083
17Lex Luger122.53021,91213.6412.601.083
18Wrath134.7272,09813.6512.681.077
19Booker T1173211,85113.2812.371.074
20AKI man109.8251,98213.5912.861.057
21Kevin Nash166.52811,88813.8013.091.055
22Buff Bagwell114.32611,96913.4912.811.053
23Dean Malenko98302,03313.1512.561.047
24Hollywood Hogan124.73111,86713.3212.741.045
25Curt Hennig118.82811,90813.1012.541.045
26Brian Adams146.1271,95413.1212.561.045
27La Parka105.2292,01613.2812.741.043
28Reese163.3241,86413.1712.661.040
29Ultimo Dragon79.43012,07612.9812.641.027
30Disco Inferno108.9281,89212.6612.441.018
31Bret Hart106.1291,81112.6612.471.015
32Roddy Piper103291,76512.4812.401.007
33Kanyon114.3251,97912.7412.681.005
34Glacier111.12411,82413.0013.030.998
35Meng140.6271,86012.8112.840.998
36Konnan113.4251,99812.7612.840.994
37Executioner117.9192,02513.7613.890.991
38Dr. Frank127221,98313.2013.580.972
39Han Zo Mon94.8222,08213.1413.540.970
40Barbarian136.1251,90312.4512.840.970
41Macho Man Randy Savage104.3281,83712.5612.980.967
42Maya Inca Boy119.7202,04012.4712.910.966
43Fit Finley104.8271,82012.4312.870.966
44Jim Neidhart123.8241,94012.3712.840.963
45Alex Wright98.9241,89512.3012.810.960
46Sick Boy113.4201,93812.5813.150.957
47Jekel122.9181,95513.0113.610.956
48Yugi Nagata104.3251,82812.2012.790.954
49Kidman83.9172,00713.0113.650.953
50Kim Chee159.7221,88012.5513.280.945
51Chavo Guerrero Jr.89.8191,94212.6113.400.941
52Van Hammer124.7201,99012.4513.400.929
53British Bulldog114.8211,91212.1213.150.921
54Shogun109.8201,86312.5713.650.921
55Stevie Ray132.4201,82312.2213.280.920
56Hawk Hana119.7191,93212.5613.820.909
57Ming Chee119.7211,94411.9613.360.895
58Lodi99.8171,86812.1613.650.891
59Riggs101.6171,86812.1613.650.891
60Brickowski109.8201,95012.1913.860.880
61Dake Ken104.8191,88211.9513.610.878
62Larry Zbysko109.3181,77311.7113.400.874
All 62 fighters, ordered by the engine-derived index. Abilities is the sum of the ten parameter bytes, 5s how many of them are maxed, Move damage the total across all 279 moves. Deals and Takes are health points per move against the average opponent. Computed from the ROM; nothing here is hand-entered.

Norton's parameter file reads 3 5 5 1 2 for offence and 3 4 4 2 4 for defence — 33 points, and two maxed offence values, which only four fighters have: Giant, Norton, Luger and Goldberg. He pairs that with a move table near the top of the field, and the combination is what puts him first. Hollywood Hogan, for the record, lands 24th.

The Tool This Came Out Of

All of this was done with the AKI Inspector, a tool I am building for exactly this: reading the six wrestling games AKI made for the Nintendo 64 — WCW vs. nWo World Tour, Virtual Pro-Wrestling 64, WCW/nWo Revenge, WWF WrestleMania 2000, Virtual Pro-Wrestling 2 and WWF No Mercy. One HTML file that runs in the browser; you bring your own ROM and it never leaves your machine.

It shows rosters, parameters, movesets, damage tables, groups, titles, rule sets, models and the raw bytes of every file in the ROM — plus a MIPS disassembler and interpreter for the game code itself. Every claim it makes about a data format carries a status: proven against the ROM, probable, or merely segmented. That habit is what made this investigation possible, and the investigation fed straight back into the tool.

  • A memory map that refuses to guess. Boot code and the fighting overlay overlap in RAM, and getting that boundary wrong produces disassembly that looks perfectly clean and is entirely wrong. The mapping now lives in one place and will not run unless the known call targets check out.
  • An interpreter in the loop. Every formula in this article was executed against the real memory image before it was written down. One address derived by hand was off by sixteen bytes; the interpreter caught it and re-reading would not have, because the wrong address also sits in uninitialised memory and returns a well-behaved zero.
  • Two kinds of evidence kept apart. The scoring above is the tool's, and the reason it has two columns rather than one blended figure is this article.

What This Does Not Say

  • The cap is on pinfalls, not on winning. Submissions, count-outs, the time-limit decision and knock-outs run through other routines — 0x800B955C and 0x800B8484 resolve three further defeat flags, each with its own reason code. The counter at 0x800F9B80 is read in exactly one place in the whole overlay, inside the pin, so no comparable cap exists elsewhere; but I did not decode the submission's own escape formula, and a match can still be won without a single successful pin.
  • Two match-option bits appear in the pin. Their menu captions live in an overlay that is not loaded alongside the match code, so I can name their effects but not their labels. The one that matters is settled by its effect: with it off, every pin lasts five ticks.
  • A tick is a turn of the game loop, not a second. The 30/60/90 figures are exact relative to each other because both counters share that clock; this article makes no claim about how many seconds a three count takes on a real console.

So what is actually true

The legend was right that something deliberate is stopping early pins, and right about how it feels. It was wrong about what it is. There is no minimum pin count: nothing counts your attempts. There is a counter at 0x800F9B80 that counts the other wrestler's kick-outs, a comparison against two, and a hold time rewritten to 87 when the referee needs 90. And it is skipped outright on a 70-point spirit lead against an opponent at zero health — which makes it a cap with a key rather than a rule. Most players will only ever turn that key by accident, which is exactly why the legend survived twenty-five years in the shape it did.

What is nicer than being right is what sits around it. There is no randomness in the pin at all. The damage is exact arithmetic, every point traceable to a stat, a limb and a move. The spirit meter is a spring pulling towards fifty with a trap at each end. The finish is a stopwatch. And the only coin flip in the neighbourhood is not about the pin — it decides whether your tag partner comes running.

Which is a rather good way to build a wrestling game. Real matches do not end because a bar hit zero. They end on a two count that everybody in the building thought was three — and that, it turns out, is exactly what the cartridge was written to produce.

Methodology

Everything ran against a dump of my own cartridge, through the AKI Inspector's disassembly harness. Nothing rests on reading assembly alone: the hold-time formula, the referee's clock and the random number generator were all executed inside a MIPS interpreter against the cartridge's real memory image, and every number printed above is what the cartridge's own code returned. The roster is ranked by running each fighter's real move table through the decoded damage formula against the roster-average opponent, so every figure in it is health points or moves rather than a weighted score; bin/staerke.js in the tool reproduces the table from a ROM. The Python module in part two carries a self-check that compares its output against those measurements; if a number ever stops matching, running it says so. No ROM data is reproduced here.

ROMWCW/nWo Revenge (USA)
Size16,777,216 bytes
Product codeNW2E
SHA-25666c137d326565c6f31f992daba8f67c0aee7f025a142dd249d27019708014b60
Fighting codeoverlay 1, RAM 0x80090000
Instructions searched84,676 (.text only)
Roster90 slots, 77 with a real move table, 62 distinct fighters
ArchitectureMIPS R4300i, big-endian