Final Fantasy V - Quadzerker Steals

󰃭 2025-03-28

Four Job Fiesta

Every summer there is a charity event called Final Fantasy V Four Job Fiesta. The basic premise is that you sign up to do a run and you are assigned 4 jobs at random, these are the only jobs you can use. If you get lucky and get at least one good job you’re set and you can cruise to a “Triple Crown” (beating the game as well as the two super bosses, Omega and Shinryu). If you’re unlucky the game will be much harder than usual.

If you are extremely unlucky (or are a masochist) and get 4 Berserkers the game becomes extremely challenging.

The gimmick of the Berserker Job in FF5 is that you do not control them. They do one thing and one thing only, attack enemies at random until either the enemies die or you die. In a normal run with 3 other jobs Berserker can be pretty useful and powerful. In a run with 4 Berserkers (a Quadzerker run), they are a huge pain in the ass. They can hit pretty hard and will chew threw many bosses and random encounters, but there are a handful of bosses that are extremely punishing. The details of a basic Quadzerker run aren’t the purpose of this article, but they’ll be included at the bottom for those interested.

The Challenge

During FJF 2020, another user on a forum proposed an additional challenge for the fiesta:

$20 up to a maximum of $200 for each person who can show me,

Post one screenshot showing your classes. You cannot use a thief at any time during this challenge. Post one screenshot showing your item list containing the following items:

  1. Titan’s Glove
  2. Genji Armor
  3. Kornago Gourd (or 2 if you have a beastmaster)
  4. 3 Gold Hairpins

(Reminder: There is a gold hairpin in Barrier Tower, and one in the Pyramid, don’t forget to pick them up.)

This is somewhat tricky challenge, but not terribly difficult for most parties. In World 3 you can obtain a Thief Knife in the Phantom Village which gives the wielder a 50% chance of doing a Mug (Attack + Steal) when attacking. Once you have that you just have to know which enemies have these items.

  1. Titan’s Glove - This is a rare steal from Azulmagia in the Castle portion of the final dungeon.
  2. Genji Armor - This is a rare steal from Gilgamesh when he appears during the fight with Necrophobe just before the final battle.
  3. Kornago Gourd - This is a rare steal from Omniscient in Fork Tower. This one is a bit more tricky than the others because Omniscient’s gimmick is that if you attack with physical attacks he will reset the battle. This can be avoided by muting him, but mute does not last very long.
  4. 3 Gold Hairpins - Two of these are just in treasure chests. The third is a rare steal from Gogo in the sunken Walse Tower. This one is tricky as well because the intended way of doing this fight is to do nothing. Attacking Gogo in any way causes brutal counterattacks, and if you hit him too many times he goes berserk. If you do nothing (which mimics him doing nothing), he congratulates you and leaves.

The Run

My philosophy for challenge runs like this is that I won’t use hacks or cheat codes to make the game easier, but I will automate what I can and I will use save states to save scum. My goal is to see if something is technically possible with favorable RNG within the parameters of the game itself. In speedrunning terms this is more akin to a TAS than a legitimate run.

I didn’t begin the run with this challenge in mind, at the beginning it was a pretty normal Quadzerker run, did grinding to 42 for the Sandworm, coasted until right before The Void in World 3.

At this point, I went to the Pyramid and went to the last room that has slidey-stairs, I abused speed up extensively by walking up the slidey-stairs, getting pushed back down, over and over. With Up held down and pushing A it just turboed through fights. One of the enemies you can fight in that room has an Elixir that can be stolen, and I was stealing them at a faster rate than I was getting hit, so once I got low on HP I just elixired everyone and kept going. This got me up to 79, I got bored and decided to go kill some stuff. That’s when I got the crazy idea to see if I could steal from Omniscient.

Heist #1: Kornago Gourd from Omniscient

The first question to be answered was if it was even possible to kill Omniscient with 3 Berserkers (the gimmick of Fork Tower is that you have to split your party into two groups, I split them into a group of 1 and a group of 3 to give myself the best chances for Omniscient) if one of them is using the Thief’s Knife. As stated before, Omniscient will cast Return and reset the fight if he is ever attacked with a physical attack while he isn’t muted and obviously with Berserkers you can’t cast spells. Luckily, there is another piece of equipment in the game that makes this challenge doable, the Mage Masher dagger is equippable by Berserkers and has a 33% chance to Silence on a hit. I set up my party and held down fast forward on the emulator until I successfully killed Omniscient. At this point I knew it was theoretically possible to do, but it was going to take a long time since I had to get perfect silence procs and also pull off a rare steal.

The Thief Knife proc’s a Mug 50% of the time. Steal is successful 40% of the time. The rare steal for Omniscient happens 3.91% of the time. Less than a 1% chance to get the rare steal with each swing of the knife. Combined with the 33% chance to mute him with the Mage Masher while also chewing through a beefy 16,999 HP and the odds were not in my favor.

Omniscient

I did some more experimenting and killed Omni a few times, then I just got completely stuck in a run. I held down fast forward on the emulator for around 20 minutes and Omni just wouldn’t die, so I wasn’t sure if I had just gotten incredibly lucky on my first few attempts or incredibly unlucky on the this attempt. I did a few more tries, and kept getting stuck without a kill. At this point I realized this was going to take something more than just holding down fast forward and hoping.

I had played around with the memory viewer in VBA a bit when I was fighting the Sandworm over and over, mostly out of curiosity and boredom, I reset after playing around because I wanted to get a legit kill but I knew I could edit the memory and do things like manually silence Omni, or even kill him (Credit to samurai goroh for mapping out the memory locations for things, I couldn’t have done it without him). But again, I wanted to get a “legit” kill without manipulating the memory.

So I started researching emulators that support scripting and found VBA-RR which supports Lua. I looked at the functions it supported, I could read memory and load a save state, so I started experimenting with scripting out my task.

First, I needed to be able to detect what I had stolen, I sold all of my Potions (the common steal) and emptied out the first slot in my inventory. Then in battle memory address 0x0201C360 would tell me what item is in the first slot. 00 for empty, E0 for a potion, and C5 for the Kornago Gourd. So I could tell whether I was successful stealing, so my first attempt at scripting the fight was to save the state just before the fight, go into the fight, reset if I stole a potion, and keep going until I killed Omni (and reset if I killed Omni without the gourd).

save = savestate.create()
savestate.save(save)
while true do
    if(memory.readbyte(0x0201C360) == 0xE0) then
        savestate.load(save)
    end
    if(memory.readbyte(0x0201C360) == 0xC5) then
        vba.print("Seen it!")
    end

The problem I ran into next was I once again kept getting stuck in neverending fights, after watching what was happening for a while I realized that the fight could get stuck in a loop. Since there is no human input a berserker fight will play out exactly the same every time if all the initial memory state is the same, and something in the RNG for the Omniscient fight can cause the memory state to loop between Returns. So I needed to do something about this. I decided to add a timer to each save state load, if the attempt went more than a certain amount of time I would reload and try again automatically.

save = savestate.create()
savestate.save(save)
while true do
    if(memory.readbyte(0x0201C360) == 0xE0) then
        savestate.load(save)
    end
    if(memory.readbyte(0x0201C360) == 0xC5) then
        vba.print("Seen it!")
    end
    if(os.clock() - reload_time > 90) then
        savestate.load(save)
    end

I also added some randomness to the start of each attempt, I would wait a random number of frames before initiating the fight to try and find the perfect memory setup to pull off The Perfect Crime.

function reload(reason)
   reload_time = os.clock()
   savestate.load(save)
   rng = math.random(2400)
   # Wait a random amount of frames
   while(rng > 0) do
     vba.frameadvance()
     rng = rng - 1
   end
   i = 0
   # Initiate the battle again
   while(i < 200) do
     joypad.set(1, {up=true})
     vba.frameadvance()
     i = i+1
   end
end

The final piece of the puzzle was removing the automatic restart if I stole a potion. I was never clearing the fight with the potion reset in place. The first steal in an Omniscient fight is almost entirely irrelevant, the chances of killing him with no returns is very small, so there is no point in resetting just because of one bad steal.

At this point I had run a few hundred attempts with various versions of the script, but I was confident that over a long enough timeframe it would work, I just didn’t know how long. I added some logging for stats and set it up to run overnight. About half an hour later I was curious so I checked in on the progress and I turned my monitor on just in time to see “Stole Kornago Gourd!” followed by a series of successful silences and the kill. Absolutely perfect timing.

The final script used was

save = savestate.create()
savestate.save(save)
seen = false
reloads = -1
clears = 0
seen_count = 0
reload_time = os.clock()
potions = 0
have_potion = false

function reload(reason)
   vba.print(reason)
   reloads = reloads + 1
   reload_time = os.clock()
   savestate.load(save)
   rng = math.random(2400)
   while(rng > 0) do
     vba.message("R: " .. reloads  .. " S: " .. seen_count .. " C: " .. clears .. " P: " .. potions)
     vba.frameadvance()
     rng = rng - 1
   end
   i = 0
   while(i < 200) do
     joypad.set(1, {up=true})
     vba.message("R: " .. reloads  .. " S: " .. seen_count .. " C: " .. clears .. " P: " .. potions)
     vba.frameadvance()
     i = i+1
   end
end

reload("Start")

while true do
   vba.message("R: " .. reloads  .. " S: " .. seen_count .. " C: " .. clears .. " P: " .. potions)
   if(memory.readbyte(0x0201C360) == 224) then
      if(have_potion == false) then
        potions = potions + 1
        have_potion = true
      end
      seen = false
   end
   if(memory.readbyte(0x0201C360) == 197 and seen == false) then
      vba.print("Seen it!")
      vba.print(seen_count)
      seen = true
      have_potion = false
      seen_count = seen_count + 1
   end
   if(memory.readbyte(0x0201C360) == 0) then
      seen = false
      have_potion = false
   end
   if(memory.readwordsigned(0x0201F056) == 16999 and memory.readwordsigned(0x0201F054) == 0) then
     clears = clears + 1
     vba.print("Cleared!")
     vba.print(clears)
     if(seen == true) then
       vba.print("GOT IT!")
       break
     end
     if(seen == false) then
       reload("Didn't steal the thing")
     end
   end
   if(os.clock() - reload_time > 90) then
      reload("Timed out")
   end
   vba.frameadvance()
end

vba.print("Done")

Heist #2: Gold Hairpin from Gogo

Stealing the Kornago Gourd from Omniscient was not easy, but it was very simple. The method was known going in and it was just a matter of finding the right RNG to succeed.

Gogo was a puzzle. Even ignoring stealing from him, how do 4 berserkers get through the Gogo fight? If you hit him he hits back for anywhere from 3000 to 9999 damage and if you hit him too many times he goes berserk repeatedly triple casts Meteor on the party until you are dead.

The first thing I tried was a straight DPS race. I could pretty reliably get him to the point where he was triple casting, so I thought maybe the same strategy as Omni would work. Give someone the Thief’s knife, load up everyone else with Mage Mashers, and just pound him. Unfortunately Gogo has 47,714 HP compared to Omniscient’s 16,999 so it would take a lot more silences to get through his HP. He also has 99 Magic Evasion compared to Omniscient’s 0, so silence is a lot harder to land. A DPS race was not going to work.

Then I thought maybe if my attacks did 0 he wouldn’t counter attack. I could stick weak weapons on 3 characters and the thief’s knife on another, the idea being the counter attacks would kill my thief knife-r, then the other 3 characters would hit for 0 and he would consider that the same as doing nothing.

0 HP attacks do not count as doing nothing. This wasn’t going to work either.

However, I did notice that misses DID count as doing nothing, so I went back out of Walse tower, hunted down something that could blind me, and then tried again. I kept the 3 other characters unarmed so that we would be less likely to push Gogo into his Meteor enrage before I could steal, I successfully got through him a few times, but the success rate was very low because the unarmed attacks were still hitting too often.

Then I remembered that axes and hammers have terrible accuracy, so I stuck a hammer on the other 3 characters and my successful clear rate went way up. Then it was just a matter of doing it enough times to get the rare steal.

I was really surprised when Butz whacked him at the end but he still gave me the clear a few seconds later. I guess there’s just a certain amount of time you have to be doing nothing and occasional attacks don’t reset it? Whatever, I’ll take it!

I ended up scripting out this fight too, although once the hammer accuracy part clicked it really wasn’t even needed. Here’s the script anyway.

save = savestate.create()
savestate.save(save)
hairpin = false
reloads = -1
clears = 0
hairpin_count = 0
reload_time = os.clock()
leather = 0

function reload(reason)
   vba.print(reason)
   reloads = reloads + 1
   reload_time = os.clock()
   hairpin = false
   savestate.load(save)
   rng = math.random(300)
   while(rng > 0) do
     vba.message("R: " .. reloads  .. " H: " .. hairpin_count .. " C: " .. clears .. " L: " .. leather)
     vba.frameadvance()
     rng = rng - 1
   end
   i = 0
   while(i < 30) do
     joypad.set(1, {A=true})
     vba.message("R: " .. reloads  .. " H: " .. hairpin_count .. " C: " .. clears .. " L: " .. leather)
     vba.frameadvance()
     i = i+1
   end
   i = 0
   while(i < 120) do
     vba.frameadvance()
     i = i+1
   end
   i = 0
   while(i < 30) do
     joypad.set(1, {A=true})
     vba.message("R: " .. reloads  .. " H: " .. hairpin_count .. " C: " .. clears .. " L: " .. leather)
     vba.frameadvance()
     i = i+1
   end
end

reload("Start")

while true do
   in_fight = memory.readwordsigned(0x0201F056) == -17822
   butz_dead = memory.readwordsigned(0x0201EE04) == 0
   lenna_dead = memory.readwordsigned(0x0201EE98) == 0
   krile_dead = memory.readwordsigned(0x0201EF2C) == 0
   faris_dead = memory.readwordsigned(0x0201EFC0) == 0
   gogo_dead = memory.readwordsigned(0x0201F054) == 0
   vba.message((in_fight and 't' or 'f').. " " .. (butz_dead and 't' or 'f') .. " " .. (lenna_dead and 't' or 'f').. " " .. (krile_dead and 't' or 'f').. " " .. (faris_dead and 't' or 'f').. " " .. (gogo_dead and 't' or 'f'))
   if(memory.readbyte(0x0201C36A) == 5) then
      leather = leather + 1
      reload("Stole leather")
   end
   if(memory.readbyte(0x0201C360) == 148 and hairpin == false) then
      vba.print("Seen it!")
      hairpin = true
      hairpin_count = hairpin_count + 1
   end
   if(in_fight and gogo_dead) then
     clears = clears + 1
     vba.print("Cleared!")
     vba.print(clears)
     if(hairpin == true) then
       vba.print("GOT IT!")
       vba.pause()
       break
     end
     if(hairpin == false) then
       reload("Didn't steal the thing")
     end
   end
   if(in_fight and krile_dead and hairpin == false) then
     reload("Krile died")
   end
   if(in_fight and butz_dead and lenna_dead and krile_dead and faris_dead) then
     reload("Wrecked")
   end
   vba.frameadvance()
end

vba.print("Done")

The Rest

Stealing from Azulmagia and Gilgamesh were considerably easier. They each took a few tries, but eventually I got lucky and got their steals.

I finished leveling up to 99, defeated Shinryu and Exdeath, and finished the trickiest Four Job Fiesta I had done up to that point. One day I want to determine if it is possible, even with perfect RNG, to defeat Omega with a Quadzerker party, but that’s a story for another time.

Additional Info

This is the rest of the information about a Quadzerker run. It’s a pretty standard Quadzerker experience.

Quadzerker Basics

A basic Quadzerker run is not difficult, just extremely time consuming because there is no strategy, all you can do is improve your levels or improve your gear. The beginning of the game goes relatively normally, albeit with some additional grinding to be able to handle bosses. But that all changes when you reach The Desert in World 1.

The Sandworm

The Sandworm is a boss at the beginning of the Desert, the gimmick for this boss is that there are 3 holes and the Sandworm will move between them. If you target the correct hole you damage the Sandworm. If you target an empty hole you will get a Gravity counter which reduces a random characters HP by half. The Sandworm itself attacks with Quicksand which damages the whole party and inflicts the Sap condition (HP slow ticks down). Not terribly difficult unless your characters are all attacking holes at random.

In a speedrun you reach the Sandworm at level 13. In a casual run you might reach the Sandworm around level 20 if you do a decent amount of grinding. In order to have a shot at beating the Sandworm in a Quadzerker run you need to be level 40+. This is extremely time consuming since the options for grinding in World 1 are very limited.

After the Sandworm

The good news is, after hours upon hours of grinding to get past the Sandworm you can cruise through World 2 and most of World 3. The bad news is that Neo-Exdeath is very hard and you my as well level up to 99 before trying to give yourself the best chance.

The Grind

I went to the Pyramid of Moore and went to the last room which has slidey-stairs that push you back down if you try to climb up them, I abused the emulator’s fast forward feature extensively by walking up the slidey-stairs, getting pushed back down, over and over. With Up held down and pushing A it just turboed through fights. One of the enemies you can fight in that room has an Elixir that can be stolen, and I was stealing them at a faster rate than I was getting hit, so once I got low on HP I just elixired everyone and kept going. This got me up to 79, I got bored and decided to go kill some stuff. That was the point where the Heist began.