Tetris on the Nintendo Entertainment System. We are all familiar with this all-time classic where you arrange these 7 iconic pieces into neat lines. Except this run only seems to know about a single one of them.
Game objectives
- Emulator used: BizHawk 2.3.1
- Main objective: Get the best score possible of 999,999 points
- Real main objective: Achieving the maximum score possible while absolutely deny the existence of randomness by getting the same piece over and over
- Actual real main objective: Make a dumb joke for April fools day
What is "square%"
As you may notice quickly, the whole point of this run was to get a whole 999,999-point run while not ever seeing a piece that is not the square piece.
You may be wondering why I went with the square piece (also called the O piece) for this run. Here are the two main reasons:
- The O piece is kind of a joke in the Tetris community, so since this run is an April fools I was content with it. (not main reason)
- It was the only piece I was able to do this with from the start.
You see, I am only able to manipulate what piece is picked by the RNG by delaying when the piece is picked. In game this is not much of a problem, since I only need to manipulate one piece at a time - the one that goes in the NEXT box. For the start though, I need to manipulate two pieces - The one that's in the NEXT box and the one that spawns on the field as the first piece. I seemed to only get the same sequences, but one of them was an O piece on the field and another in the NEXT box, so I went with it.
About the randomiser
With the help of this wonderful article which goes in depth about every detail of the NES version of Tetris, I was able to create a Lua script to predict what the next piece is gonna be if it's picked in an arbitrary number of frames.
The general idea is that the RNG seed gives out a 16-bit number, from which we take the lower 3 bits to get a number between 0 and 7.
If that number is 7 or if it corresponds to the same piece as the one that was dealt before, get a new number from the RNG and get a number between 0 and 6 using modulo.
It then deal the piece that corresponds to the number it had.
The article goes in more detail, but the point is that the game keeps track of the previous piece and tries not to have repeats in the sequence of pieces it gives you, Which is the exact opposite of what we want. Luckily though, since the RNG is called at least once per frame, even when the game is paused, we can simply pause and wait for a good seed to come up.
How the run goes
Step 1: Before scoring a line
I figured the best strategy for this would be to start by stacking as high as I could, since this would minimise the time it takes for pieces to drop if they have to cover less distance.
I also figured starting on level 19 (by holding A while selecting level 9) would also help, since the level acts as a multiplier on the score, and level 19 is the fastest I can start on, with gravity pulling down the piece every 2 frames (this is as fast as softdrop).
Step 2: Clearing lines
And it was at this moment I realised the terrible truth: with O pieces only, I am stuck clearing doubles, so this is going to take a while.
It would take so much of a while, in fact, that I decided I would not do it manually, but instead just run a script that would bruteforce RNG for me on every piece drop, and even record the inputs on its own.
The algorithm looks like this:
-- timings for how far to look ahead for good RNG
-- timingrow3 is greater than timingrow4 because it's possible to stack higher on the side opposite to the well
-- I now realise while writing these comments that I have been calling columns "rows" all this time but hey it didn't prevent it from working
timingrow3 = 28
timingrow4 = 30
timingrow5 = 6
while true do
prevpiececount = memory.read_u8(0x001A)
prevlinecount = memory.read_u8(0x0050)
curplaced = {false, false, false, false} -- keep track of placed blocks
for curplacing = 1, 4 do
if not (
(predictPiece(timingrow4) and not curplaced[4]) -- didn't place a piece in columns 7-8 and the RNG will give an O after placing a piece there
or (predictPiece(timingrow3) and not curplaced[3]) -- didn't place pieces in columns 1-2, 3-4 and 5-6 and the RNG will give an O after placing a piece there
)
then
joypad.set({["P1 Start"] = true})
emu.frameadvance()
joypad.set({["P1 Start"] = false})
emu.frameadvance()
while not (
(predictPiece(timingrow4) and not curplaced[4]) -- pause and wait for the first opportunity
or (predictPiece(timingrow3) and not curplaced[3])
or (predictPiece(timingrow5) and curplacing == 5) -- oops this line is a leftover from a different attempt
)
do
emu.frameadvance()
end
joypad.set({["P1 Start"] = true})
end
emu.frameadvance()
joypad.set({["P1 Start"] = false})
if predictPiece(timingrow4-1) and not curplaced[4] then -- if you can put a piece in columns 7-8 do it
curplaced[4] = true
joypad.set({["P1 Right"] = true})
emu.frameadvance()
joypad.set({["P1 Right"] = false})
emu.frameadvance()
joypad.set({["P1 Right"] = true})
emu.frameadvance()
joypad.set({["P1 Right"] = false})
emu.frameadvance()
else
if not curplaced[2] then -- else place the piece as far left as you can
joypad.set({["P1 Left"] = true})
emu.frameadvance()
joypad.set({["P1 Left"] = false})
emu.frameadvance()
joypad.set({["P1 Left"] = true})
emu.frameadvance()
joypad.set({["P1 Left"] = false})
emu.frameadvance()
if not curplaced[1] then
joypad.set({["P1 Left"] = true})
emu.frameadvance()
joypad.set({["P1 Left"] = false})
emu.frameadvance()
joypad.set({["P1 Left"] = true})
emu.frameadvance()
joypad.set({["P1 Left"] = false})
emu.frameadvance()
curplaced[1] = true
else
curplaced[2] = true
end
else
curplaced[3] = true
end
end
while prevpiececount == memory.read_u8(0x001A) do -- while the piece count hasn't updated cheese softdrop points
joypad.set({["P1 Down"] = true})
emu.frameadvance()
end
joypad.set({["P1 Down"] = false})
prevpiececount = memory.read_u8(0x001A)
end
-- part about clearing the line
-- move far right
joypad.set({["P1 Down"] = false})
joypad.set({["P1 Right"] = true})
emu.frameadvance()
joypad.set({["P1 Right"] = false})
emu.frameadvance()
joypad.set({["P1 Right"] = true})
emu.frameadvance()
joypad.set({["P1 Right"] = false})
emu.frameadvance()
joypad.set({["P1 Right"] = true})
emu.frameadvance()
joypad.set({["P1 Right"] = false})
emu.frameadvance()
joypad.set({["P1 Right"] = true})
emu.frameadvance()
joypad.set({["P1 Right"] = false})
emu.frameadvance()
while prevlinecount == memory.read_u8(0x0050) do -- cheese softdrop points until you clear the line
joypad.set({["P1 Down"] = true})
emu.frameadvance()
end
joypad.set({["P1 Down"] = false})
joypad.set({["P1 Start"] = true})
emu.frameadvance()
joypad.set({["P1 Start"] = false})
emu.frameadvance()
while not predictPiece(timingrow5) do -- pause and wait for good RNG to give you an O piece after you unpause
emu.frameadvance()
end
joypad.set({["P1 Start"] = true})
emu.frameadvance()
joypad.set({["P1 Start"] = false})
while prevpiececount == memory.read_u8(0x001A) do -- wait for next piece to spawn
emu.frameadvance()
end
-- update variables and start again
prevpieceline = memory.read_u8(0x0050)
prevpiececount = memory.read_u8(0x001A)
end
After a bit of tweaking to get the values right, I set this running and watched as it started clearing lines by itself, until 230 lines where it hit the transition into level 29.
Step 3: Getting in and playing level 29+
If you are familiar with high-level gameplay of Nintendo Tetris, you may have heard of level 29 being called "The Killscreen". From this level onwards, the gravity pulls the blocks down every frame, forcing the game into a speed that is unsustainable for most human players. Unless you can tap the D-pad more than 3 times a second, which is what very few people like Joseph Saelee manage to do.
But since this is Tool-Assisted, we can tap the D-pad 15 times a second easily. I did have to lower my stack though to accommodate to the new highest height I could build, and then ran my script again, with adjusted timing values:
timingrow3 = 25 -- 28
timingrow4 = 25 -- 30
timingrow5 = 6
I found it strange that placing a block two cells lower on the field didn't change how long it takes between drops (both take 25 frames), but I didn't question it and just let it run while I went away from my computer to cook some burgers for my family.
I then came back to my computer, only to find that my script had been playing for way longer than necessary, to the point where the O piece count in the statistics box showed U29 (3,029 pieces, while the final TAS ends after 1,396).
Step 4: Ending this stupid thing
Since I didn't check up on my script to stop it when I should have, I went back to end it at the right moment, downstacking all the lines I currently had on my screen, and topping out manually, after nearly fourteen hundred piece drops. I then went went into the name entry and entered an appropriate name, ending the TAS after pressing start to go back to the level select screen.
Possible improvements
- Coming up with a better script, of course.
- The start of the game can definitely be improved. I didn't use the fact that I could create multiple options by stacking all columns at once on different heights, leaving more possibility to place a piece without pausing and get another O-piece in the NEXT box.
- The transition to level 29 may have been better if I downstacked the whole field and upstacked again with the faster drop speed. I didn't compare and assumed keeping the blocks high before the transition made up for it, but I may be wrong.
Am I serious with this
Yes and no.
No because, of course, this is an April fools submission, and I thought I was being hilarious. In reality, this probably was not nearly as funny as I thought it was, and this submission is probably gonna get rejected right away, because this is the most repetitive game of Tetris, and it goes on for 20 minutes.
Yes because while I was doing this, I learnt more about how to use Lua scripting applied to Tool-Assisted Speedrunning. I also learnt and understood more about how NES Tetris works, especially on the randomiser, and I am still happy with the fact that I was able to accurately predict the next pieces.
If you read this far, I respect your dedication, and I must thank you for the attention you put towards this ridiculous submission. Have a wonderful first of April.
Masterjun: Should have used the Left+Down+Right trick to make less use of pausing! Neat goal, unfortunately unwatchable. Rejected due to the Tetris community thinking the letter O is a square.