The revision 2 ROMs have some significant bugs that are exploited during world record score attempts, particularly in "marathon" games. Revision 2 is effectively the "standard" version, even though the less-common revision 3 software is more correct.
Changes between rev 2 and rev 3:
The easiest way to see the bugs in practice is to set the appropriate wave number or score value using an emulator. This video demonstrates how to jump to wave 255 in MAME, using the debugger.
It's possible to identify a rev 3 ROM by looking for an "eclipse" on the "MISSILE COMMAND" title page. In order to make room for the wave-reset patch, the code that draws the title explosions was rewritten. The original version erased all 20 existing explosions, then drew 20 new ones. Any explosions that overlap would simply blend together. The new version erases and redraws each explosion individually, so if a new explosion in slot #1 overlaps an older explosion in slot #2, then part or all of the new explosion in slot #1 will disappear when slot #2 is erased. It can be hard to spot with the colors cycling, but if you watch carefully you will often see an explosion with a bite taken out of it.
If you see an eclipse, you know you have a revision 3 ROM set. There's also a minor change to the sizes of explosions, but that's even harder to spot.
The number of ICBMs per wave was increased for waves 17-19+. The details can be found on the wave guide page.
This doesn't address a bug. It just makes the game harder.
The code that awards bonus cities doesn't increment the count as you play. Instead, at the end of each wave, it figures out the total number of bonus cities your score represents, and then subtracts the number of bonus cities awarded in the past. For example, if your score is 805,000, and bonus cities are awarded every 10,000 points, it would conclude that you have earned 80 bonus cities.
The code is trying to divide the player's score by the bonus city score, but the 6502 isn't very good at division, so it just subtracts the bonus city score (10,000) from the player's score (805,000) until the value goes negative, incrementing a counter each time. The subtraction is done in in BCD (binary-coded decimal) mode, with the score held in three bytes. In this example the bytes are $80 $50 $00, from which we repeatedly subtract $01 $00 $00.
The problem occurs when the player's score reaches a level where the first subtraction leaves a value of $80 or higher in the high byte. In our example, if the score were 810,000 ($81 $00 $00) and we subtracted 10,000 ($01 $00 $00) we would be left with $800,000 ($80 $00 $00), which is a negative number as far as the 6502 is concerned. The subtraction loop exits immediately, which effectively means that the game has decided that you've earned a grand total of zero bonus cities.
Now for the fun part: the game subtracts the previously-awarded bonus cities from zero. Assuming we finished an earlier wave with a score between 800K and 810K, we should have a total of 80 cities awarded. 0 - 80 = -80, unless you're using 8-bit unsigned math, in which case it's equal to 256 - 80 = 176. So the game awards 176 bonus cities in one shot.
The player's total bonus city count is also stored in an 8-bit value, which rolls over at 256. If the player had lost exactly 6 cities, they would have a total of 80 cities (live + banked bonus), and the addition of 176 more would bring the total to 256... which would roll over to 0, ending the game.
After awarding the huge pile of cities, the previously-awarded bonus city count is set to zero. When the next wave ends, the subtraction loop still ends early, with a result of zero. When the previous count is subtracted from the current count, 0 - 0 = 0, so no bonus cities are awarded. This situation persists until the score rolls over at 1,000,000 points.
The bug was fixed by rewriting the routine at $6040. The new code mostly fit in the same space as the original, though the code right before it was modified slightly to make a CLD-then-RTS accessible.
There are multiple problems associated with high wave numbers:
The game counts waves from 1, not 0, so wave 1 is $01, wave 255 is $ff, and wave 256 uses the unexpected value $00.
The multiplier is computed by taking the current wave number, adding one, and dividing by two. If the result is greater than 6, it's capped at 6. The problem happens on wave 255 and 256, when ((wave+1)/2) evalutes to zero. Zero is less than 6, so it isn't capped, but when it's used as a loop counter it runs for 256 iterations rather than zero. This causes the base score to be set to 25*256 instead of 25*6. (See $5b9e.)
A wave ends after a certain number of ICBMs and smart bombs have been destroyed. The missile and bomb counts are read out of a table, using the wave number as an index. The wave number starts at 1 and is capped at 19, but wave 256 acts like wave 0. The values stored in ROM where the wave 0 values would live happen to be zero, so wave 256 ends after zero missiles and bombs have been destroyed. (See $5b31.)
At wave 257, the wave counter has rolled over completely and is back to 1. The game difficulty resets to match.
As mentioned on the main page, the game has a mechanism that blocks
the creation of new ICBMs / bombs when the altitude of the highest ICBM
is above a certain level. This starts at 200 and drops on each wave,
using the formula 202 - 2 * wave_number
. The threshold is
capped to go no lower than 180.
At wave 102 the result of the calculation becomes negative, which in unsigned 2's complement math makes it very large. This effectively disables the restriction, because the altitude limiter is off the top of the screen. The desired behavior gradually reasserts itself, returning to the wave 1 behavior at wave 129, but breaks again at wave 230.
The bug causes a noticeable change in difficulty at wave 102.
The fix is simple: when the wave counter reaches 40, roll it back to 20. This works because the difficulty is the same at level 19+, and the color scheme rotates through a 20-wave sequence.
The only trick was finding a place to put the patch. The developers needed 13 bytes, so they rewrote the title explosion animation code and dropped it in there ($6300).
For reference, here are the raw diffs for the only file that changed,
035822-02.kl1
vs. 035822-03e.kl1
:
4,9c4,9 < 00000030: b9da 0165 d399 da01 85d3 d8a9 ff85 d460 ...e...........` < 00000040: a5f4 2970 c970 f039 4a4a 4aa8 a900 858d ..)p.p.9JJJ..... < 00000050: a6b9 bdd8 0185 98bd da01 8599 f8a5 9838 ...............8 < 00000060: f982 6085 98a5 99f9 8360 8599 3002 e68d ..`......`..0... < 00000070: 10eb d8a5 8d38 f5c3 1875 c095 c0a5 8d95 .....8...u...... < 00000080: c360 0001 2001 4001 5001 8001 0002 8000 .`.. .@.P....... --- > 00000030: b9da 0165 d399 da01 85d3 a9ff 85d4 d860 ...e...........` > 00000040: a5f4 2970 4a4a 4a4a a8b9 7b60 8599 f0ef ..)pJJJJ..{`.... > 00000050: a6b9 bdd8 014a 4a4a 4a85 98bd da01 0a0a .....JJJJ....... > 00000060: 0a0a 0598 f838 f5c3 38e5 9990 d1f6 c048 .....8..8......H > 00000070: b5c3 1865 9995 c368 4c68 6010 1214 1518 ...e...hLh`..... > 00000080: 2008 0001 2001 4001 5001 8001 0002 8000 ... .@.P....... 11c11 < 000000a0: 1012 14d0 e0c0 08a0 6040 2010 0a06 0402 ........`@ ..... --- > 000000a0: 1113 16d0 e0c0 08a0 6040 2010 0a06 0402 ........`@ ..... 44,49c44,49 < 000002b0: fdfc fba9 0085 9aa2 13bd 6e01 f014 859c ..........n..... < 000002c0: bd39 0185 9bbd c201 85b5 2071 5ea9 009d .9........ q^... < 000002d0: 6e01 ca10 e4a9 fc85 eca2 13ad 0a40 2907 n............@). < 000002e0: 0903 9dc2 0185 9aad 0a40 9d39 0185 9bad .........@.9.... < 000002f0: 0a40 c9d0 b005 c938 b003 38b0 f29d 6e01 .@.....8..8...n. < 00000300: 859c a900 85b5 2071 5eca 10cf 6020 4d63 ...... q^...` Mc --- > 000002b0: fdfc fba2 13a9 0085 9abd 6e01 f006 bdc2 ..........n..... > 000002c0: 0120 f162 a9fc 85ec ad0a 4029 0709 049d . .b......@).... > 000002d0: c201 859a ad0a 409d 3901 ad0a 40c9 d0b0 ......@.9...@... > 000002e0: f9c9 3890 f59d 6e01 a900 20f1 62ca 10c5 ..8...n... .b... > 000002f0: 6085 b5bd 6e01 859c bd39 0185 9b4c 715e `...n....9...Lq^ > 00000300: a5a7 c928 9002 a914 85a7 4c86 6920 4d63 ...(......L.i Mc 86c86 < 00000550: 00a5 9881 0de6 0d88 10e7 605e a980 8509 ..........`^.... --- > 00000550: 00a5 9881 0de6 0d88 10e7 6016 a980 8509 ..........`..... 115c115 < 00000720: ca10 fb20 8669 207c 6820 5e67 2021 69a5 ... .i |h ^g !i. --- > 00000720: ca10 fb20 0063 207c 6820 5e67 2021 69a5 ... .c |h ^g !i.
Source code for revision 2. Compare to the main disassembly listing.
603a: d8 cld ;rev3: CLD moved down for code sharing 603b: a9 ff lda #$ff 603d: 85 d4 sta score_dirty_flag 603f: 60 :Return rts ; ; Awards bonus cities based on score. ; ; Player scores are stored as a 3-byte BCD value: ABC,DEF. Bonus cities are ; awarded every XY,000 points. ; ; It's possible to earn multiple bonus cities on one wave. ; ; On entry: ; X-reg=player number (0/1) ; ]bonus_count .var $8d {addr/1} ]score_md .var $98 {addr/1} ]score_hi .var $99 {addr/1} 6040: a5 f4 CheckBonusCity lda r8_irq_inv ;get R8 switches 6042: 29 70 and #%01110000 ;mask to get bonus city bits 6044: c9 70 cmp #%01110000 ;are bonus cities disabled? 6046: f0 39 beq :Return ;yes, bail 6048: 4a lsr A ;right-shift to get 0-7 6049: 4a lsr A 604a: 4a lsr A 604b: a8 tay ;copy to Y-reg to use as index 604c: a9 00 lda #$00 604e: 85 8d sta ]bonus_count ;init bonus count to zero ; 6050: a6 b9 ldx cur_plyr_num ;get current player number (0/1) 6052: bd d8 01 lda cur_score_md,x ;get middle byte of score 6055: 85 98 sta ]score_md ;copy to ZP 6057: bd da 01 lda cur_score_hi,x ;get high byte of score 605a: 85 99 sta ]score_hi ;copy to ZP 605c: f8 sed ;enable decimal mode ; Divide the player's score by the bonus city score. 605d: a5 98 :DivLoop lda ]score_md ;get middle byte of score 605f: 38 sec 6060: f9 82 60 sbc bonus_city_bcd,y ;subtract middle byte of bonus city score 6063: 85 98 sta ]score_md ;save result 6065: a5 99 lda ]score_hi ;repeat for high byte 6067: f9 83 60 sbc bonus_city_bcd+1,y 606a: 85 99 sta ]score_hi ; Here's the bug: if, after subtracting the bonus city points, the high byte of ; the player's score is >= $80 (i.e. 800,000 points), we branch. This is ; because, in 2's complement math, $80 is a negative number. 606c: 30 02 bmi :DivDone ;branch if negative 606e: e6 8d inc ]bonus_count ;add one bonus city 6070: 10 eb :DivDone bpl :DivLoop ; 6072: d8 cld ;disable decimal mode 6073: a5 8d lda ]bonus_count ;get the bonus count 6075: 38 sec ; Here's the effect of the bug: we exited the divide early, so bonus_count is ; zero. We now subtract the number of bonus cities we previously awarded, ; e.g. at 800K points we awarded the 80th bonus city. In mod 256 arithmetic 0- ; 80=176, so we give 176 cities this round. Because we then set the previously- ; awarded value to zero, future score increases award no cities until the score ; rolls over at 1M points. 6076: f5 c3 sbc bonus_award_sc,x ;subtract cities previously awarded to this player 6078: 18 clc ; to get the number of cities to award now 6079: 75 c0 adc num_cities,x ;add the new bonus cities to their collection 607b: 95 c0 sta num_cities,x 607d: a5 8d lda ]bonus_count ;save the updated bonus-city count 607f: 95 c3 sta bonus_award_sc,x ; to the per-player state 6081: 60 :Return rts
[...]
6090: 0c 0f 12 0c+ icbm_count_tbl .bulk $0c,$0f,$12,$0c,$10,$0e,$11,$0a,$0d,$10,$13,$0c,$0e,$10,$12,$0e + $10,$12,$14
[...]
62b3: a9 00 DrawTitleExpl lda #$00 62b5: 85 9a sta ]end_radius ;prep to erase (shrink to zero) 62b7: a2 13 ldx #19 ;20 explosions 62b9: bd 6e 01 :EraseLoop lda expl_ypos,x ;is there an explosion in this slot? 62bc: f0 14 beq :NextE ;no, branch 62be: 85 9c sta ]yc ;set Y coord arg 62c0: bd 39 01 lda expl_xpos,x ;get X coord 62c3: 85 9b sta ]xc ;set arg 62c5: bd c2 01 lda expl_radius_idx_arr,x ;get radius 62c8: 85 b5 sta ]start_radius ;set as start radius 62ca: 20 71 5e jsr DrawExplosion ;call draw routine to erase it 62cd: a9 00 lda #$00 62cf: 9d 6e 01 sta expl_ypos,x ;clear explosion entry 62d2: ca :NextE dex ;move to next entry 62d3: 10 e4 bpl :EraseLoop ;loop until done ; 62d5: a9 fc lda #%11111100 ;cycle all colors except 0/1 (background) 62d7: 85 ec sta color_cyc_mask 62d9: a2 13 ldx #19 ;20 explosions 62db: ad 0a 40 :DrawLoop lda POKEY_RANDOM ;get random number 62de: 29 07 and #%00000111 ;reduce to 0-7 62e0: 09 03 ora #%00000011 ;make it 3 or 7 62e2: 9d c2 01 sta expl_radius_idx_arr,x ;set radius in table 62e5: 85 9a sta ]end_radius ;set argument 62e7: ad 0a 40 lda POKEY_RANDOM ;get random number 62ea: 9d 39 01 sta expl_xpos,x ;set as X coordinate in table 62ed: 85 9b sta ]xc ; and in argument 62ef: ad 0a 40 :Retry lda POKEY_RANDOM ;get random number 62f2: c9 d0 cmp #208 62f4: b0 05 bcs :BcsRetry ;too high, try again 62f6: c9 38 cmp #56 62f8: b0 03 bcs :DoDraw ;high enough, branch 62fa: 38 sec 62fb: b0 f2 :BcsRetry bcs :Retry ;(always) 62fd: 9d 6e 01 :DoDraw sta expl_ypos,x ;save Y coord in table 6300: 85 9c sta ]yc ; and in argument 6302: a9 00 lda #$00 6304: 85 b5 sta ]start_radius ;set start radius to zero 6306: 20 71 5e jsr DrawExplosion ;draw the explosion 6309: ca dex ;have we drawn all 20? 630a: 10 cf bpl :DrawLoop ;no, branch 630c: 60 rts
[...]
655b: 5e .dd1 $5e ;checksum byte
[...]
6723: 20 86 69 jsr ClearScreen ;clear entire screen
Copyright 2021 by Andy McFadden