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.
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 spirit | Victim spirit | Gap | Hold time | Result |
|---|---|---|---|---|
| 50 | 20 | 30 | 87 | trimmed — kick-out at two |
| 69 | 0 | 69 | 87 | trimmed — kick-out at two |
| 70 | 0 | 70 | 130 | pinfall on the first cover |
| 90 | 20 | 70 | 125 | pinfall on the first cover |
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")
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
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
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
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
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
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
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 condition | Health | Hold time | Count reached | Result |
|---|---|---|---|---|
| Untouched | 255 | 15 | 0 | kick-out, instantly |
| Roughed up | 150 | 15 | 0 | kick-out, instantly |
| Hurting | 100 | 15 | 0 | kick-out |
| In trouble | 60 | 55 | 1 | kick-out at one |
| Nearly finished | 30 | 85 | 2 | kick-out at two |
| Beaten to zero | 0 | 87 | 2 | kick-out at two |
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
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
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
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
0xC030 is A, B and the two shoulder triggers.| Input | Ticks off per turn | Escapes at tick | Count reached | Result |
|---|---|---|---|---|
| Nothing at all | 1 | 122 | 3 | pinfall |
| Steady tapping | 2 | 61 | 2 | kick-out at two |
| Fast mashing | 3 | 41 | 1 | kick-out at one |
| Everything you have | 4 | 31 | 1 | kick-out at one |
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
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.
| Flag | Raised when | Frozen for | Released at | What it means |
|---|---|---|---|---|
0x00040000 | spirit reaches 100 | 600 ticks | spirit = 70 | your special is available — this is the window |
0x00080000 | you fire the special | 600 ticks | spirit = 50 | special mode is running |
0x00020000 | spirit reaches 0 | 300 ticks | spirit = 50 | you are spent; nothing moves the meter |
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
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:
| n | Hits out of 20,000 | Measured | Predicted | Difference |
|---|---|---|---|---|
| 0 | 20 | 0.10 % | 0.10 % | +0.00 |
| 50 | 997 | 4.99 % | 5.10 % | −0.11 |
| 100 | 2,024 | 10.12 % | 10.10 % | +0.02 |
| 250 | 5,051 | 25.25 % | 25.10 % | +0.15 |
| 500 | 9,999 | 49.99 % | 50.10 % | −0.11 |
| 750 | 15,033 | 75.17 % | 75.10 % | +0.07 |
| 900 | 18,018 | 90.09 % | 90.10 % | −0.01 |
| 999 | 20,000 | 100.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 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
Run it, and two things fall out. The first is the answer to the legend:
| # | Wrestler | Deals per move | Takes per move | Moves to finish | Moves survived | Index |
|---|---|---|---|---|---|---|
| 1 | Scott Norton | 14.70 | 12.42 | 17.4 | 20.5 | 1.184 |
| 2 | Chris Benoit | 14.57 | 12.56 | 17.5 | 20.3 | 1.160 |
| 3 | Giant | 14.55 | 12.70 | 17.5 | 20.1 | 1.145 |
| 4 | Saturn | 14.12 | 12.50 | 18.1 | 20.4 | 1.129 |
| 5 | Eddy Guerrero | 14.07 | 12.52 | 18.1 | 20.4 | 1.124 |
| 6 | Diamond Dallas Page | 14.06 | 12.56 | 18.1 | 20.3 | 1.120 |
| 7 | Scott Steiner | 14.19 | 12.70 | 18.0 | 20.1 | 1.117 |
| 8 | Goldberg | 14.03 | 12.64 | 18.2 | 20.2 | 1.110 |
| 9 | Juventud Guerrera | 14.04 | 12.68 | 18.2 | 20.1 | 1.107 |
| 10 | Rick Steiner | 13.58 | 12.33 | 18.8 | 20.7 | 1.101 |
| 11 | Psychosis | 13.96 | 12.68 | 18.3 | 20.1 | 1.101 |
| 12 | Sting | 13.70 | 12.58 | 18.6 | 20.3 | 1.089 |
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:
| # | Wrestler | kg | Abilities | 5s | Move damage | Deals | Takes | Index |
|---|---|---|---|---|---|---|---|---|
| 1 | Scott Norton | 152 | 33 | 2 | 2,028 | 14.70 | 12.42 | 1.184 |
| 2 | Chris Benoit | 99.8 | 31 | 1 | 2,123 | 14.57 | 12.56 | 1.160 |
| 3 | Giant | 244.9 | 34 | 2 | 1,960 | 14.55 | 12.70 | 1.145 |
| 4 | Saturn | 106.6 | 30 | 2,064 | 14.12 | 12.50 | 1.129 | |
| 5 | Eddy Guerrero | 93 | 30 | 1 | 2,172 | 14.07 | 12.52 | 1.124 |
| 6 | Diamond Dallas Page | 106.6 | 30 | 2,002 | 14.06 | 12.56 | 1.120 | |
| 7 | Scott Steiner | 106.6 | 30 | 1 | 2,000 | 14.19 | 12.70 | 1.117 |
| 8 | Goldberg | 129.3 | 32 | 2 | 1,951 | 14.03 | 12.64 | 1.110 |
| 9 | Juventud Guerrera | 74.8 | 30 | 1 | 1,947 | 14.04 | 12.68 | 1.107 |
| 10 | Rick Steiner | 112.5 | 36 | 2 | 1,872 | 13.58 | 12.33 | 1.101 |
| 11 | Psychosis | 90.7 | 28 | 2,123 | 13.96 | 12.68 | 1.101 | |
| 12 | Sting | 114.3 | 34 | 1,967 | 13.70 | 12.58 | 1.089 | |
| 13 | Chris Jericho | 102.1 | 30 | 2,002 | 13.53 | 12.44 | 1.088 | |
| 14 | Rey Mysterio Jr. | 70.3 | 26 | 1 | 2,060 | 14.44 | 13.30 | 1.086 |
| 15 | Raven | 107.5 | 28 | 1,847 | 13.57 | 12.52 | 1.083 | |
| 16 | Scott Hall | 130.2 | 28 | 1,923 | 13.58 | 12.54 | 1.083 | |
| 17 | Lex Luger | 122.5 | 30 | 2 | 1,912 | 13.64 | 12.60 | 1.083 |
| 18 | Wrath | 134.7 | 27 | 2,098 | 13.65 | 12.68 | 1.077 | |
| 19 | Booker T | 117 | 32 | 1 | 1,851 | 13.28 | 12.37 | 1.074 |
| 20 | AKI man | 109.8 | 25 | 1,982 | 13.59 | 12.86 | 1.057 | |
| 21 | Kevin Nash | 166.5 | 28 | 1 | 1,888 | 13.80 | 13.09 | 1.055 |
| 22 | Buff Bagwell | 114.3 | 26 | 1 | 1,969 | 13.49 | 12.81 | 1.053 |
| 23 | Dean Malenko | 98 | 30 | 2,033 | 13.15 | 12.56 | 1.047 | |
| 24 | Hollywood Hogan | 124.7 | 31 | 1 | 1,867 | 13.32 | 12.74 | 1.045 |
| 25 | Curt Hennig | 118.8 | 28 | 1 | 1,908 | 13.10 | 12.54 | 1.045 |
| 26 | Brian Adams | 146.1 | 27 | 1,954 | 13.12 | 12.56 | 1.045 | |
| 27 | La Parka | 105.2 | 29 | 2,016 | 13.28 | 12.74 | 1.043 | |
| 28 | Reese | 163.3 | 24 | 1,864 | 13.17 | 12.66 | 1.040 | |
| 29 | Ultimo Dragon | 79.4 | 30 | 1 | 2,076 | 12.98 | 12.64 | 1.027 |
| 30 | Disco Inferno | 108.9 | 28 | 1,892 | 12.66 | 12.44 | 1.018 | |
| 31 | Bret Hart | 106.1 | 29 | 1,811 | 12.66 | 12.47 | 1.015 | |
| 32 | Roddy Piper | 103 | 29 | 1,765 | 12.48 | 12.40 | 1.007 | |
| 33 | Kanyon | 114.3 | 25 | 1,979 | 12.74 | 12.68 | 1.005 | |
| 34 | Glacier | 111.1 | 24 | 1 | 1,824 | 13.00 | 13.03 | 0.998 |
| 35 | Meng | 140.6 | 27 | 1,860 | 12.81 | 12.84 | 0.998 | |
| 36 | Konnan | 113.4 | 25 | 1,998 | 12.76 | 12.84 | 0.994 | |
| 37 | Executioner | 117.9 | 19 | 2,025 | 13.76 | 13.89 | 0.991 | |
| 38 | Dr. Frank | 127 | 22 | 1,983 | 13.20 | 13.58 | 0.972 | |
| 39 | Han Zo Mon | 94.8 | 22 | 2,082 | 13.14 | 13.54 | 0.970 | |
| 40 | Barbarian | 136.1 | 25 | 1,903 | 12.45 | 12.84 | 0.970 | |
| 41 | Macho Man Randy Savage | 104.3 | 28 | 1,837 | 12.56 | 12.98 | 0.967 | |
| 42 | Maya Inca Boy | 119.7 | 20 | 2,040 | 12.47 | 12.91 | 0.966 | |
| 43 | Fit Finley | 104.8 | 27 | 1,820 | 12.43 | 12.87 | 0.966 | |
| 44 | Jim Neidhart | 123.8 | 24 | 1,940 | 12.37 | 12.84 | 0.963 | |
| 45 | Alex Wright | 98.9 | 24 | 1,895 | 12.30 | 12.81 | 0.960 | |
| 46 | Sick Boy | 113.4 | 20 | 1,938 | 12.58 | 13.15 | 0.957 | |
| 47 | Jekel | 122.9 | 18 | 1,955 | 13.01 | 13.61 | 0.956 | |
| 48 | Yugi Nagata | 104.3 | 25 | 1,828 | 12.20 | 12.79 | 0.954 | |
| 49 | Kidman | 83.9 | 17 | 2,007 | 13.01 | 13.65 | 0.953 | |
| 50 | Kim Chee | 159.7 | 22 | 1,880 | 12.55 | 13.28 | 0.945 | |
| 51 | Chavo Guerrero Jr. | 89.8 | 19 | 1,942 | 12.61 | 13.40 | 0.941 | |
| 52 | Van Hammer | 124.7 | 20 | 1,990 | 12.45 | 13.40 | 0.929 | |
| 53 | British Bulldog | 114.8 | 21 | 1,912 | 12.12 | 13.15 | 0.921 | |
| 54 | Shogun | 109.8 | 20 | 1,863 | 12.57 | 13.65 | 0.921 | |
| 55 | Stevie Ray | 132.4 | 20 | 1,823 | 12.22 | 13.28 | 0.920 | |
| 56 | Hawk Hana | 119.7 | 19 | 1,932 | 12.56 | 13.82 | 0.909 | |
| 57 | Ming Chee | 119.7 | 21 | 1,944 | 11.96 | 13.36 | 0.895 | |
| 58 | Lodi | 99.8 | 17 | 1,868 | 12.16 | 13.65 | 0.891 | |
| 59 | Riggs | 101.6 | 17 | 1,868 | 12.16 | 13.65 | 0.891 | |
| 60 | Brickowski | 109.8 | 20 | 1,950 | 12.19 | 13.86 | 0.880 | |
| 61 | Dake Ken | 104.8 | 19 | 1,882 | 11.95 | 13.61 | 0.878 | |
| 62 | Larry Zbysko | 109.3 | 18 | 1,773 | 11.71 | 13.40 | 0.874 |
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 —
0x800B955Cand0x800B8484resolve three further defeat flags, each with its own reason code. The counter at0x800F9B80is 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.
| ROM | WCW/nWo Revenge (USA) |
| Size | 16,777,216 bytes |
| Product code | NW2E |
| SHA-256 | 66c137d326565c6f31f992daba8f67c0aee7f025a142dd249d27019708014b60 |
| Fighting code | overlay 1, RAM 0x80090000 |
| Instructions searched | 84,676 (.text only) |
| Roster | 90 slots, 77 with a real move table, 62 distinct fighters |
| Architecture | MIPS R4300i, big-endian |