Gauntlet II Fight Power
One of the major gameplay changes in Gauntlet II from the original Gauntlet is that each player has the ability to change the character assigned to a coin slot. In Gauntlet, the characters are fixed to each coin slot, so there is only red warrior (Thor), blue valkyrie (Thyra), yellow wizard (Merlin), and green elf (Questor). In Gauntlet II, the colors are still assigned to fixed coin slots, but you can have a red wizard, blue warrior, yellow elf, green valkyrie, and so on.
This adds a lot of variability to the game that wasn't available in the original, but the addition of this feature was not perfect. Long after this game was released in arcades, information was published on the internet indicating that the fight power of the characters never worked quite correctly in Gauntlet II.
"Play Green Valkyrie if you want to cheat and have Warrior's original fight
power without the powerup (2-3x normal power). Red, blue and yellow Valk have
2x fight power (which is default for her)."
"Being able to select your character, regardless of joystick, in Gauntlet 2,
is a bit bugged. Only the GREEN player is given the extra 'half' bonus for
fighting ability. In other words, playing Red, Blue, or Yellow warrior only
gives you 2x normal fight power, the same as Valkyrie. Green warrior has the
proper fight power. The same goes for Elf: Red, blue or green gives elf the
same fight power as wizard."
"Likewise, playing both Valkyrie and Wizard on the green player gives them an
extra half level, so Valkyrie's fight power becomes equal to Warrior, and
Wizard's become equal to Elf's. If only that applied to shot power..."
The Gauntlet II instructions are not clear how fight power is supposed to operate.

The Gauntlet I instructions, on the other hand, provide exact details:

Thor and Thyra have the same base power of 2. Merlin and Questor have the same base power of 1. Thor and Questor have variable damage that can increase their power up to +1 over their base. Thyra and Merlin are fixed at their base powers.
I have long wanted to investigate this issue and make an attempt to fix it, and I finally got around to digging in.
player MOB
To begin, I started with the Gauntlet source code from MAME, which reveals these memory locations and layout for the motion object data.
902000-903FFF R/W xxxxxxxx xxxxxxxx Motion object RAM (1024 entries x 4 words)
R/W -xxxxxxx xxxxxxxx (0: Tile index)
R/W xxxxxxxx x------- (1024: X position)
R/W -------- ----xxxx (1024: Palette select)
R/W xxxxxxxx x------- (2048: Y position)
R/W -------- -x------ (2048: Horizontal flip)
R/W -------- --xxx--- (2048: Number of X tiles - 1)
R/W -------- -----xxx (2048: Number of Y tiles - 1)
R/W ------xx xxxxxxxx (3072: Link to next object)
To begin debugging, I launched a mame debug session and coined up a player on level 1. I froze the game at a VBLANK and took a snapshot of the motion object RAM.
> dump mob.txt,902000,2000
I moved the player up the Y direction, froze the game at the next VBLANK, and took another snapshot. The MAME source documentation explains that the Y positions of motion objects are the upper 9 bits of 16 bit words stored in an 1024 element array beginning at address $903000 (0x902000 + 0x1000). By comparing the two snapshots, I was able to find one motion object that had a diff in the Y buffer memory between the two frames.
903130: 0000 0000 0000 0000 D912 0000 0000 0000
|
|
|
903130: 0000 0000 0000 0000 DA12 0000 0000 0000
|
This gave me the information needed to locate my player mob offset.
0x903138 - 0x903000 = 0x138
0x902000 + 0x138 = 0x902138 # P1 tile
0x902800 + 0x138 = 0x902938 # P1 x position * 128 + palette
0x903000 + 0x138 = 0x903138 # P1 y position * 128 + tile info
0x903800 + 0x138 = 0x903938 # list link
Now, I set a watchpoint on the tile address for my mob
> wpset 902138,2,w Stopped at watchpoint 5 writing 15D8 to 00902138 (PC=4AC2A)
D0 00000000 D1 00000030 D2 00000138 D3 00000018 A0 00902000 A1 00058A4A A3 009049A4 A4 009048E8 | 04AC10 move.w (A3,D0.w), D1 # D1 = *(A3 + D0) 04AC14 move.w (A4,D0.w), D3 # D3 = *(A4 + D0) 04AC18 lsl.w #3, D3 # D3 = D3 * 8 04AC1A add.w D3, D1 # D1 = D3 + D1 04AC1C add.w D1, D1 # D1 = D1 + D1 04AC1E lea $58a4a.l, A1 # A1 = 00058a4a (program ROM) 04AC24 lea $902000.l, A0 # A0 = 00902000 (MOB base) *04AC2A move.w (A1,D1.w), (A0,D2.w) # *(A0 + D2) = *(A1 + D1) |
D2 contains the offset for the P1 sprite. From there, I tracked down the line of code where D2 was set, revealing the address where the player mob is stored.
04AB0C: lea $9049ac.l, A0
04AB12: lea $9048c8.l, A1
*04AB18: move.w (A1,D0.w), D2
04AB1C: beq $4ac30
04AB20: add.w D2, D2
$9048c8 is an array of mob ids for the players indexed by coin slot. Note that the values stored in this array are ids, not address offsets. The selected id is converted to an offset on line $04AB20 by multiplying D2*2.
kill routine
I now had a consistent memory address to find my player mob between restarts of the game. Next, I needed a strategy to identity the offset of an enemy mob. I lured a grunt to a similar X location as my character, as close to the edge of the maze as I could, paused in debugger, and found my mob index using $9048c8. I looked up my X position and used that to find the mob offset of the grunt next to me.
The offset for my player mob in this run was 37E, so the X position was stored at $902800 + 37E = $902B7E.
902B70: 0000 0000 0000 8000 0000 0000 0000 0C3F 902B80: 8000 0000 0000 0000 0000 0000 0000 0000 902B90: 0000 0000 0000 0000 0000 0000 0000 0000 902BA0: 0000 0000 0000 0000 0000 0000 0000 0000 902BB0: 0000 0000 0000 0000 0000 0000 0000 0B1B
As described previously, X is stored multiplied by 128, so to find the actual X position, the value in memory needs to be shifted right 7 bits.
0C3F >> 7 = 0x18
Not too far away in the memory buffer, there was a similar value at $902BBE.
0B1B >> 7 = 0x16 0x902BBE - 0x902800 = 3BE
Grunt offset identified, I now set a watchpoint on its tile address.
> wpset 9023BE,2,w
I began the fight animation and stepped frame by frame until this happened.
Stopped at watchpoint 6 writing 0000 to 009023BE (PC=5DEBE)
Here is the code where it stopped, with my comments added.
D0 00000000 D2 000003BE A2 00902000 A3 00902800 A4 00903000 A5 00903800 A6 00904066 | *05DEBC moveq #$0, D0 # set D0 = 0 05DEBE move.w D0, (A2,D2.w) # set (902000 + mob offset) = 0 05DEC2 move.w D0, (A3,D2.w) # set (902800 + mob offset) = 0 05DEC6 move.w D0, (A4,D2.w) # set (903000 + mob offset) = 0 05DECA move.w D0, (A5,D2.w) # set (903800 + mob offset) = 0 05DECE move.w D0, (A6,D2.w) # set (904066 + mob offset) = 0 05DED2 rts # return from subroutine. |
This clears all the data for the specified mob from memory. It's the mob kill code!
The subroutine return at $05DED2 returns to $05DE08, which was just a wrapper function starting at $05DDDA for managing the stack and setting the buffer addresses. The subroutine on the stack above that started at $052192, and containts the meat of the fight code.
0425A6: jsr $52192.l # jump to fight subroutine 052354: bsr $5ddda.l # branch to kill subroutine wrapper 05DE02: bsr $5de44 # branch to kill code 05DED2: rts # return to wrapper 05DE08: rts # return to fight subroutine 05287A: rts # return to caller
damage routine
My strategy now was to use diffs of cpu traces to find where the code branched from frames where the player injured the monster vs the frame where the mob was killed. My suspicion was that the code that performs the damage logic would immediately precede the the code that determines whether the monster died. I set a breakpoint at $052192 and used a wizard to engage a higher level grunt. As soon as the debugger paused at that breakpoint, I turned CPU trace on.
> trace red_wizard_fight_grunt_a.tr,maincpu
After enabling tracing, I ran the debugger to the next VBLANK. Once the debugger paused at the end of the frame, I turned CPU trace off again.
> trace off,maincpu
I repeated these steps until the monster died. Then I compared the diffs. The first divergence that I found was after a conditional check at $0523d6 that determined whether to branch to $052462.
0523CE: move.w D3, D0
0523D0: add.w D0, D0
0523D2: tst.w (A3,D0.w)
0523D6: beq $52462
052462: cmpi.w #$1, $904bf2.l
05246A: bne $52492
05246C: move.w D3, D0
05246E: add.w D0, D0
| → |
0523CE: move.w D3, D0
0523D0: add.w D0, D0
0523D2: tst.w (A3,D0.w)
0523D6: beq $52462
0523DA: move.w D2, D0
0523DC: ext.l D0
0523DE: move.l D0, -(A7)
0523E0: clr.l -(A7)
|
I studied this code for a while and determined this wasn't the branch I was looking for. When the code branched to $052462, it didn't do much before it returned back out of the subroutine. The address it checks at A3 is $9049ac, and is indexed based off player slot. I think this code is checking to determine if the animation has reached the frame where the action should occur.
I decided that I was only interested in frames where the trace went through the $0523DA path. I changed my breakpoint location to $0523DA, found a new grunt and repeated the process of collecting traces for each frame of the fight process that broke in the debugger until the next monster died. I compared new diffs between the frames, and this time it diverged at the conditional branch on $052438.
I stepped through this series of instructions numerous times with different characters and player colors and confirmed this was the code I was looking for. Here is the snippet of code with my comments added to decipher what's happening.
# D0 = always 0 with red slot, sometimes 1 with green # D2 = enemy mob index # D4 = selected player character (warrior:0, valkyrie:1, wizard:2, elf:3) 0523FC: move.w D0, D1 # save fight bonus to D1 # lookup fight power from table at $5b7d4 based on player character 0523FE: move.w D4, D0 052400: add.w D0, D0 # D0 = player character offset 052402: movea.l #$5b7d4, A0 # 05B7D4 0002 0002 0001 0001 # add fight power to bonus to calculate damage and store in D1 052408: add.w (A0,D0.w), D1 # D1 += (warrior:2 valkyrie:2 wizard:1 elf:1) # get the enemy palette value from the mob buffer 05240C: move.w D2, D0 05240E: add.w D0, D0 # D0 = enemy mob offset 052410: movea.l #$902800, A2 # mob buffer for mob_x*128 + mob_palette # subtract damage from the enemy palette value 052416: sub.w D1, (A2,D0.w) # mob_palette[mob_index] -= D1 05241A: move.w (A2,D0.w), D0 05241E: andi.w #$f, D0 # D0 = new mob_palette value 052422: move.w D5, D1 052424: ext.l D1 # determine if the monster was killed 052426: movea.l #$5864c, A0 # minimum palette value look up table 05242C: moveq #$0, D4 05242E: move.b (A0,D1.l), D4 # D4 = minimum_palette[mob_type] 052432: sub.w D4, D0 # subtract minimum from mob_palette 052434: addq.w #2, D0 # subtract 1 052436: subq.w #3, D0 052438: bcs $5243e # if result is negative, branch to kill routine
The lookup table at $05B7D4 confirms that both warrior and valkyrie are tuned to a fight power of 2, and both elf and wizard have fight power of 1. This value is being calculated correctly based on the character and not the coin slot.
My test runs through this code also confirmed that only the green player has a chance to receive a +1 bonus, exactly as described in the bug report. The potential +1 bonus is stored in the D0 register. So, I now needed to figure out how D0 was calculated. Scrolling backwards in the trace brought me to this set of instructions:
0523E4: move.w D3, D0 # D0 = (red:0, blue:1, yellow:2, green:3) 0523E6: add.w D0, D0 0523E8: movea.l #$5b7e4, A2 # 05B7E4 0000 0000 0000 0002 0523EE: move.w (A2,D0.w), D1 # D1 = 2 if player is green, else D1 = 0 0523F2: ext.l D1 0523F4: move.l D1, -(A7) # push D1 to stack 0523F6: jsr $5fc4e.l # jump to subroutine 05FC4E: lea $904bfc.l, A0 05FC54: bra $5fc26 05FC26: moveq #$0, D0 # D0 = 0 05FC28: move.w ($6,A7), D0 # D0 = retrieve D1 from stack 05FC2C: move.w (A0), D1 # D1 = value at $904bfc 05FC2E: muls.w #$3619, D1 # D1 *= 0x3619 05FC32: addi.w #$5d35, D1 # D1 += 0x5d35 05FC36: move.w D1, (A0) # value at $904bfc = D1 05FC38: muls.w D0, D1 # D1 *= D0 05FC3A: swap D0 # swap upper and lower bytes of D0 05FC3C: asr.l #1, D0 # D0 = D0 / 2 05FC3E: add.l D1, D0 # D0 = D0 + D1 05FC40: swap D0 # swap upper and lower bytes of D0 05FC42: ext.l D0 # convert D0 to long 05FC44: rts # return from subroutine
The subroutine at $05fc4e appears to be a random number generator with the state stored at $904bfc. A lookup table at $05B7E4 is used to determine the upper limit of the random value. The lookup table is indexed by the coin slot, and only the green player can get the bonus, because the green slot is the only non-zero entry in that table.
fixes
There are two issues that need to be fixed:
- The offset used to fetch from $05B7E4 needs to be character indexed, not by color slot
- The warrior entry at $05B7E4 needs to be adjusted to match elf
Fortunately, these were both incredibly easy to solve.
To fix the offset to be based on player character instead of player coin slot, the change needs to be performed on operation $0523E4. The operand D3 needs to be changed to something else. D4 already contains the player character, as it's used just a few instructions later at $0523FE to lookup the base fight power. So, I can just change D3 to D4 as follows.
0523E4: move.w D3, D0
0523E6: add.w D0, D0
0523E8: movea.l #$5b7e4, A2
0523EE: move.w (A2,D0.w), D1
0523F2: ext.l D1
0523F4: move.l D1, -(A7)
0523F6: jsr $5fc4e.l
0523FC: move.w D0, D1
0523FE: move.w D4, D0
052400: add.w D0, D0
052402: movea.l #$5b7d4, A0
052408: add.w (A0,D0.w), D1
| → |
0523E4: move.w D4, D0
0523E6: add.w D0, D0
0523E8: movea.l #$5b7e4, A2
0523EE: move.w (A2,D0.w), D1
0523F2: ext.l D1
0523F4: move.l D1, -(A7)
0523F6: jsr $5fc4e.l
0523FC: move.w D0, D1
0523FE: move.w D4, D0
052400: add.w D0, D0
052402: movea.l #$5b7d4, A0
052408: add.w (A0,D0.w), D1
|
For the second issue, the table at $05B7E4 just needed to be updated to allow warrior to also get the random bonus.
05B7E4: 0000 0000 0000 0002
|
|
|
05B7E4: 0002 0000 0000 0002
|
ROM updates
To actually make the changes, I needed to figure out which ROMs stored the data. This is the MAME specfication for this address range.
ROM_LOAD16_BYTE( "136043-1121.6a", 0x058000, 0x004000 ROM_CONTINUE( 0x050000, 0x004000 ROM_LOAD16_BYTE( "136043-1122.6b", 0x058001, 0x004000 ROM_CONTINUE( 0x050001, 0x004000
These are 32 kb ROMs divided into two 16 kb regions. The 2 byte words making up the memory are interleaved with 1 byte coming from each ROM. 6a stores the even (high) bytes, and 6b stores the odd (low) bytes.
136043-1121.6a first 16k = even bytes in the range 058000 - 5FFFE 136043-1122.6a second 16k = even bytes in the range 050000 - 57FFE 136043-1122.6b first 16k = odd bytes in the range 058001 - 5FFFF 136043-1122.6b second 16k = odd bytes in the range 050001 - 57FFF
The first byte that needed to be changed was $0523E5, which is the low byte in the instruction for choosing the lookup offset. Since this is an odd address, it is stored in ROM 6b.
$0523E5 - $050001 = $23E4 $23E4 / 2 = $11F2 $11F2 + $4000 = $51F2
51F0: A794 0340 7C05 E432 00C1 01B9 054E 0004
|
|
|
51F0: A794 0440 7C05 E432 00C1 01B9 054E 0004
|
The second byte to change was $05B7E5, which is the low byte for the word containing the warrior's bonus in the lookup table. Since this is also an odd address, it also resides in 6b.
$05B7E5 - $058001 = $37E4 $37E4 / 2 = $1BF2
1BF0: 0202 0000 0002 0302 0000 0403 0001 0102
|
|
|
1BF0: 0202 0200 0002 0302 0000 0403 0001 0102
|
I booted up the new ROMs in MAME and confirmed in the debugger that the bug was now fixed.
> bpset 05240C
Here is red warrior delivering 3 damage just like he did in his Gauntlet I days!
checksum error
There was one more thing that needed to be fixed though. Because I altered the ROMs, the game displayed this at boot before it proceeded to the splash screen.

To fix this, I reverted to the original 6b ROM, turned on CPU trace and stepped through frames until the splash screen was displayed. Then, I restarted MAME with my modified ROM, turned on CPU trace and stepped frames until the error was displayed. Once I had the two traces recorded, I loaded them into a diff tool.
000860: addq.w #1, D3
(loops for 131072 instructions)
000870: cmpi.b #-$1, D0
000874: beq $87a
00087A: cmpi.b #-$1, D1
00087E: beq $884
000884: cmpa.l A0, A1
000886: bgt $84e
000888: bra $834
000834: movea.l (A6)+, A0
000836: move.l (A6)+, D0
000838: beq $8ec
0008EC: tst.w D5
0008EE: beq $946
| → |
000860: addq.w #1, D3
(loops for 131072 instructions)
000870: cmpi.b #-$1, D0
000874: beq $87a
00087A: cmpi.b #-$1, D1
00087E: beq $884
000880: bsr $d7a
000D7A: move.w #$0, $803120.l
000D82: movem.l D0-D4/A0-A1/A6, -(A7)
000D86: move.l D0, D2
000D88: move.l D1, D3
000D8A: subq.l #2, A0
000D8C: move.l A0, D5
000D8E: moveq #$f, D4
|
The low byte of D1 should equal -1 (0xFF) at line $087A or it calls the subroutine at $0D7A to report the error.
D3 00000001 D4 00007FFF A0 00050000 | 00084E move.l A0, D0 # D0 = A0 ($50000) 000850 lsr.l #6, D0 # D0 = D0 / 64 ( $1400) 000852 lsr.l #6, D0 # D0 = D0 / 64 ( $50) 000854 move.w D0, D1 # D1 = D0 ( $50) 000856 addq.w #1, D1 # D1 += 1 ( $51) 000858 move.w D0, $803100.l # *($803100) = D0 # D2 = number of bytes to read - 1 00085E move.l D4, D2 # D2 = D4 ( $7FFF) 000860 addq.w #1, D3 # D3 += 1 ( 2) # begin loop 000862 add.b (A0)+, D0 # D0 = *(A0); A0++; 000864 add.b (A0)+, D1 # D1 = *(A0); A1++; 000866 move.w D0, $803100.l # *($803100) = D0 # decrement D2 and branch back to $0862 00086C dbra D2, $862 # end loop 000870 cmpi.b #-$1, D0 000874 beq $87a 000876 bsr $d7a 00087A cmpi.b #-$1, D1 00087E beq $884 000880 bsr $d7a |
Basically, this code starts by initializing D0 and D1 with a value derived from the base memory address and then loops through and adds the values of all the bytes in the 6a and 6b ROM address space to either D0, if it's an even address, or D1, if it's an odd address. After it is done summing up all the data in the ROM, it verifies that the low byte of the total equals FF.
This implies that there's a byte stored somewhere in the ROM itself with the sole purpose of ensuring this sum computes to the correct value. The very end of the ROM data would be a logical place to store this byte, so I looked up the value at $5FFFF in the memory window.
05FFA0 FFF8 6D00 FE6A 7000 4CEE 20FC FFDA 4E5E
05FFB0 4E75 FFFF FFFF FFFF FFFF FFFF FFFF FFFF
05FFC0 FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF
05FFD0 FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF
05FFE0 FFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFF
05FFF0 FFFF FFFF FFFF FFFF FFFF FFFF FFFF E19E
It appears that the useful data in the ROM ends at $5FFB2, and the remaining data is set to FF, until the very last two bytes which hold the checksum bytes for 6a at $5FFFE and 6b at $5FFFF. I just needed to figure out what the new value should be that replaces 9E. To do that, I had to know what the value of D1 is right before it performs the final addition.
> bpset 0864, a0 == 5ffff
D1 = 64, so the value of the byte at $5FFFF needs to be the difference between FF and 64.
FF - 64 = 9B
The original value at $5FFFF is 9E, and 9E - 9B = 3, which is also the difference of the 2 bytes changed for the fix!
I also needed to calculate the byte offset in the ROM using the same formula I used to make the fixes.
$05FFFF - $058001 = $7FFE $7FFE / 2 = $3FFF
I changed the value at $3FFF from 9E to 9B.
3FF0: FFFF FFFF FFFF FFFF FFFF FFFF FFFF FF9E
|
|
|
3FF0: FFFF FFFF FFFF FFFF FFFF FFFF FFFF FF9B
|
I booted the game with the new ROM, and no more error!
summary
I am impressed at the accuracy of the bug description; everything I encountered confirmed the game behaves exactly as stated in the report. The issue was also easier to solve than I anticipated, thanks to the robust debugging tools within MAME. I was also lucky that I wasn't forced into making any changes that modified instruction offsets, as that would have added complexity. The fix can be described by altering just 3 bytes in ROM "136043-1122.6b"
- Replace 00 at 1bf2 with 02
- Replace 9E at 3fff with 9B
- Replace 03 at 51f2 with 04


