Dr. Mario virus placement

Thread in 'Research & Development' started by IoannesPaulus, 3 Aug 2012.

  1. Attached is the source code for my reverse-engineered C version of SNES Dr. Mario's virus randomizer. The code directly corresponds to how the original code worked, adapted to C. I haven't analyzed it closely, nor compared it to the NES randomizer; someone else can do that. The archive also includes an IPS patch, to be applied to an SMC format ROM, that disables the randomizer being run every frame; I had to disable it, so that my code could be verified to be correct against the original, and I'm including the patch so others can verify for themselves.

    Attached Files:

    Boxes_DC likes this.
  2. Thanks so much @nightmareci for your work here. It started all of my adventures into NES Dr. Mario disassembly, which are continuing (I need to organize everything and make a post somewhere). I've made a mod for the NES version that allows you to choose a seed value in the level select screen and play a specific seed, with 0000 and 0001 falling back to the default behavior since they're invalid with the LFSR. The seed that was used to generate the current bottle is also shown while playing so that you can go back and replay it if desired. The UI is a bit rough (it doesn't show that you've gone down to the seed selection), but pressing <LEFT> and <RIGHT> will change the digit that's selected and <SELECT> will change the digit that is currently selected (also not shown in the UI yet). I've included the patch here for all to try out. I streamed while playing a demo and giving instructions on how to use it here.

    We also have a Discord where we discuss all things Dr. Mario (mostly the NES version), with a special channel just for custom tools/hacks. https://discordapp.com/invite/7fqvK4n

    Attached Files:

  3. Thank you, I'll integrate that into my clone today! Also you're credited in the credits :)
  4. I'm running the program with all 65536 seeds for levels 20-29 to see if it locks up. Level 20 has no issues, but in Level 21 there are 12 separate seeds that cause a lockup in "GenVirus()"

    - 0x074E //Gets stuck on 85
    - 0x28b8 //Gets stuck on 87
    - 0x3cab //Gets stuck on 86
    - 0x3f3a //Gets stuck on 87
    - 0x513f //Gets stuck on 86
    - 0x9325 //Gets stuck on 86
    - 0x933c //Gets stuck on 87
    - 0x9d3e //Gets stuck on 87
    - 0xa06a //Gets stuck on 87
    - 0xad35 //Gets stuck on 87
    - 0xb867 //Gets stuck on 87
    - 0xe13f //Gets stuck on 87

    For seed 0x074E, I viewed the WIP bottle and all the remaining empty spaces were unusable (Placing a virus there of any colour would have broken a placement rule). I assume the other 11 seeds locked up for the same reason.

    I assume the higher the level, the more lockup-seeds will appear. Are you aware of any restrictions on the value of the seed? I see in main's "GenVirus()" loop there's a comment saying the seed is randomised some number of times or not at all. What determines the number of seed randomisations?

    In the mean time I'm going to try and dig into the rest of this code to understand how this new system works.

    EDIT: I see a problem with the code. The variable "numVirusGenAttempts" is always zero. If it did increment then eventually the function would exit (Although it wouldn't have all the viruses it could have had) and hence the softlock wouldn't happen. There might be more pieces missing, I'm still trying to understand some of the player variables. I'll update here if I find anything else.
    Last edited: 23 Jan 2020
  5. I've updated the archive in my previous post for the SNES randomizer, fixing the bug you noted, and another in ValidVirusPos, where it should return false when the first if-condition fails, and did some refactoring.

    The game runs RealRand (a function that Rand calls, with Rand doing nothing else) some random number of times every frame, 0 or more. The number of times isn't governed by an explicit algorithm; it just keeps running the randomizer after a frame has been updated, so the number of times RealRand is run is proportional to how many CPU cycles remain after a frame update.
    Last edited: 23 Jan 2020
  6. Thanks for that, its no longer softlocking. On level 23 I noticed only roughly 80% of the seeds were successful (The requested number of viruses are present). So I ran some more tests that showed something promising.

    - I set the level to 23 (96 viruses)
    - I run all seeds for level 23 "k" times
    - For the current value of "k" I call Rand() that many times before every GenVirus() call
    - I set k to go from 0 to 100 and got the number of successful seeds for each run

    My results are as attached, but I can tell you I had the most success with 15 calls of rand which produced 65013 successful seeds out of 65536 (Thats a 99.3% success rate) or 523 bad seeds. For levels 0 - 22, every seed is successful using this method.

    The catch is for lower levels, they look rather bad with most of the viruses one of two columns, so I recommend only doing these 15 Rand() calls if you are on level 21+ (Lv 21 is the first level that would normally have some bad seeds). I haven't tested this too much yet, but levels 21+ look good with this enhancement.

    In case my description wasn't clear, here's how you add the enhancement. In main() go to the 3rd for loop and add this code just before the GenVirus() call (Note the "k" stuff was just a part of the previous test, not the "better seeds" solution so don't worry about that):
    if(Players[Current_Player].virusLevel > 20){    //Only do this for later levels where its required
        for(j = 0; j < 15; j++){    //Runs 15 times
    To fix the remaining 523 bad seeds, you could add an array of all the seeds and if your initial seed is one of them then re-roll. Thanks again nightmareci for your research :D

    Attached Files:

  7. I'm not sure where else to say this, but this page has some issues https://tetris.wiki/Dr._Mario

    Specifically the rotation section. It doesn't explicitly state you can rotate both clockwise and anti-clockwise (That might be obvious to some, but I haven't played the real Dr M in ages so I didn't know) and then in its wall-kick examples it uses clockwise and anti-clockwise rotations interchangeably. For months I've been wondering why my vertical wall kick felt off and this is why.

    I wanted to edit this to make these things clear, however I can't make an account for the wiki, the CAPTCHA is literally broken (As seen below). Sometimes it did generate questions I could read, but even then it always said I was wrong even though I'm certain my answers were right. Can someone with an account fix this so others don't get confused in the future?

    (EDIT: I've been informed that particular question isn't bugged. The answer is "L" and apparently those are the tetris shape pieces...)
    Last edited: 11 Feb 2020
  8. Hello. I recently researched and answered a Stack Exchange question about the speed up sound effect in Dr. Mario. When I first read the Stack Exchange question, I suspected the sound effect likely indicated a speed up, but I wanted to confirm it. I found the Dr. Mario page on Tetris.Wiki helpful in my investigation because it confirmed someone had already found a table of speed amounts in the game. I investigated on my own and found more details about how Dr. Mario's speed up table and process works. I am considering updating the Tetris.Wiki article with my findings. I have drafted an update and asked for feedback.

    In my investigation I found the gravity table entries represent the maximum value allowed for a zero-based frame counter that counts how long the vitamin capsule has been on the current row. Each frame this counter is incremented and if the result is over the maximum value allowed, the counter is reset to zero and the vitamin capsule is dropped a row. For example, in the NTSC game, for the first value used for the LOW speed (index 15 in the table which contains value 39), the counter ranges from 0 to 39 and the vitamin capsule stays on each row for 40 frames.

    My investigation found the gravity table has indexes from 0 to 80 and index 80 contains value 0, meaning the fastest possible speed on HI is 1 frame per row. (The table entries just before that with the value of 1 have an effect of 2 frames per row.)

    Here is a method to confirm with FCEUX using the NTSC game (PRG+CHR CRC32 198C2F41) or the PAL game (PRG+CHR CRC32 9735D267). (In FCEUX, go to the Help menu and choose Message Log to see the PRG+CHR CRC32.)

    1. Go to the Tools menu, choose Cheats, and add the following cheats:

    falling capsule left half = yellow030100(blank)
    falling capsule right half = yellow030200(blank)

    2. Go to the Tools menu, choose RAM Watch, and add the following watches:

    AddressNotesData TypeData Size
    0090capsule countHexadecimal2 bytes
    008Anumber of speed-upsUnsigned1 byte
    0092frame counter for row dropUnsigned1 byte

    3. Start a 1-player game with VIRUS LEVEL 00 and SPEED HI.

    4. Hold the Tab key to run the game at turbo speed until you see the number of speed-ups is 48.

    5. Tap the \ key repeatedly to run the game one frame at a time. Notice that the capsule stays on each row for 2 frames, and the counter cycles between 0 and 1.

    6. Press the Pause key to unpause the game, then hold the Tab key to run the game at turbo speed until you see the number of speed-ups is 49.

    7. Tap the \ key repeatedly to run the game one frame at a time. Notice that the capsule stays on each row for 1 frame, and the counter remains at 0.

    You can use this method to confirm the other drop speeds, but be aware there is a 2 frame delay from when the counter is zero to when the capsule first appears or drops a row. That is, when you are checking the other speeds, the first frame of each row happens when you see the counter is 2 in the RAM Watch window. Even though they're not aligned, the amounts still match up (for example, when the counter ranges from 0 to 39, the capsule spends 40 frames on each row).

    (I got the falling capsule color addresses from romhacking.net's Data Crystal wiki page Dr. Mario:RAM map.)

    Edit 2020-05-19 22:29 -05:00: Added link to Dr. Mario talk page where I asked for feedback.
    Last edited: 20 May 2020
  9. @Bavi_H Great research! Just a few notes:
    • You're right that the frames per drop in the table on the wiki are off by one - I have an updated spreadsheet at
    • The gravity table suffers from a common overflow issue, the index isn't checked before selecting from the table. This means that values outside of the expected range can be chosen for the speed. If you can survive through the 1g (1 frame per drop) portion, it slows down again and the speed fluctuates as it's hitting unexpected code values.
    • While the speed increase happens every 10 capsules, it first occurs on the 9th capsule. I think this is a combination of 2 off by one errors, but I've yet to investigate further in the code: when the capsules are generated, the first one Dr. Mario throws is at index 1 (not 0), and the fact that it occurs on the 9th and not the 10th seems to be another off by one error (it's probably checking the current index instead of the last).
    • Instead of actually running through all of the capsules to test it, you can just update the base speeds at $238D/E/F for low/med/hi respectively and then it will take effect immediately. These can also be changed with game genie codes :)
    We have a Discord server that has a #tools-and-hacks channel that you'd probably find interesting, there's a link in a previous post of mine. We talk about stuff like this pretty regularly, I've just yet to get around to making it into some kind of wiki/videos.
  10. Yep. Those values match the NES NTSC values I gathered.

    I looked at the NES NTSC version (PRG+CHR CRC32 198C2F41) and NES PAL version (PRG+CHR CRC32 9735D267). (In FCEUX, go to the Help menu and choose Message Log to see the PRG+CHR CRC32.) The code that selects from the gravity table doesn't have a bounds check, but the code that increments the "number of speed ups" counter does have a bounds check. At memory address 8F32 in the NTSC version or memory address 8F3B in the PAL version:
    A5 8A  LDA $008A
    C9 31  CMP #$31   ; 49 decimal
    F0 0F  BEQ later
    E6 8A  INC $008A
    The index into the gravity table is the sum of
    • the "starting index", which is either 15, 25, or 31 (for LOW, MED, or HI), and
    • the "number of speed ups" counter, which ranges from 0 to 49.
    So the maximum index selected from the gravity table is 31 + 49 = 80.

    If you are modifying the "starting index" table to contain values over 31, then I can see how the code will select past the normal end of the gravity table. But during normal gameplay, it doesn't do this. When I tested as I described in my previous post (add a cheat to force the capsule to a single color, then run the emulator at turbo speed) the "number of speed ups" counter goes up to 49, then stops incrementing.

    It depends on what you are counting.
    • If you are counting the capsules that appear in Dr. Mario's throwing hand, then the first speed up happens when the 10th capsule appears in Dr. Mario's throwing hand.
    • If you are counting the capsules as they arrive in the bottle, then the first speed up happens when the 9th capsule arrives in the bottle.
    • If you are counting the capsules as they lock into place, then the first speed up happens a moment after the 8th capsule locks into place.
  11. Ah, right you are! I had seen mention of the 49 max speed before but didn't bother to verify it. In that case you can just pause and modify the RAM of the speed counter :)

    And yeah, I count the capsules as they arrive in the bottle. The player has actively moved 8 capsules at a certain speed, then on the 9th one it moves to the new increased speed.

Share This Page