我探索俄罗斯方块世界的旅程
My quest to see all of Tetris

原始链接: https://antithesis.com/blog/2026/tetris-quest/

2026年,Antithesis团队着手挑战“通关”《俄罗斯方块》,目标是达到“重生”——即不断升级,直到关卡计数器从255回滚至0。尽管他们此前已经突破了臭名昭著的29关“死亡屏障”,但随着人类纪录的不断演进(特别是青少年使用“滚动”技巧),促使该团队立志挑战更高目标。 团队利用了其独特的模糊测试平台,该平台通过随机输入和“目标函数”来演化游戏策略。通过奖励“方块准备度”和高效的消行方式,他们稳步提升关卡。然而,在160关时,他们撞上了一堵无法解释的墙。经过数月的困扰,作者发现了一个自身导致的程序错误:一个行计数函数错误地解析了内存值,导致软件将1600行视为负向进度。 在进行了简单的代码修复后,团队成功达到了255关,见证了重生,并体验了定义该游戏最深处未知代码的混乱故障群。最终,该项目成为了一个技术隐喻:如同复杂的软件,游戏也存在隐藏且充满漏洞的“死角”,只有当你远远偏离预期路径时,这些问题才会显现出来。

Hacker News 最新 | 过往 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 我观看所有俄罗斯方块玩法的探索 (antithesis.com) 11 点,作者:wwilson,2 小时前 | 隐藏 | 过往 | 收藏 | 1 条评论 | 帮助 jdw64 1 分钟前 [–] 我本以为会看到类似 DQN 的东西,但实际上看到了一种新方法,这很有趣。通常在制作小型 AI 演示或进行实践练习时,经常会用到俄罗斯方块。在 NES 版俄罗斯方块中,如果输入相同,结果也相同,因此你可以存储所有输入并重现特定时刻。状态就像是一个图,这使得模糊测试成为可能。很有意思。 回复 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

Early in 2026 I was sweating. A few years before, I had made the statement that Antithesis had “beaten” Tetris. We have a tradition that we name conference rooms after games that we have beaten, and since 2021 there has been a room in the office named Tetris.

However, my statement of victory had been overtaken by events. As we shall see, teenage gamers had learned how to extend human performance into parts of the game our product had not reached. I made it my side quest to use my free time to catch up to the teenagers.

The goal was “rebirth” — running through all the levels of Tetris until the level counter rolled over from 255 back to 0. When this happens, the game returns to the speed and color scheme of level 0 and starts the climb through the levels again. If we could see all of Tetris, surely we could claim we had beaten it.

Unfortunately, our exploration was stuck on level 160. I was about to join a new department, and I knew once that happened I would not have time to work on Tetris, maybe ever again. Yet there I was, stuck.

But I am getting ahead of myself. I want to say up front that this does have a happy ending, despite a most exasperating resolution of the “stuck” situation. I should start the story at the beginning.

If you just want to see what we did in the end, and the weird glitches we found on the way, you can jump straight to Rebirth!

Back in the early days of Antithesis, new engineers would go through an initiation rite: learn our system by using it to play and hopefully beat a game from the iconic 1980s Nintendo Entertainment System. Over time we’ve tackled a lot of classics, including Mario, Zelda and Metroid. When I joined in 2019, I chose Arkanoid, because it was my second favorite NES game. My favorite game, Tetris, had already been tried.1

I had become semi-addicted to Tetris in the 90s when I was flying back from visiting a customer in Europe. The then-revolutionary in-flight entertainment system had a version you could play at your seat. I enjoyed using spatial reasoning to figure out how to rotate and position a piece mid-flight. Seeing completed rows vaporize as my score increased gave me an immediate sense of accomplishment.

Of course, Tetris doesn’t let you stay comfortable for long. Just when you get in the groove, the pieces drop faster. If you make a mistake, like covering up an empty spot with a new piece, you’re now playing defense and having to trade off future health for not crashing out right now. There is no such thing as a finish line: you simply keep playing until you lose. No one gets out clean. (Maybe this is all a metaphor for something.)

When we had tried before to beat Tetris we had made progress, but we had not managed to get very far in the game. But the idea of beating Tetris stuck with me, and became a (very) long-running side project. But what does “beating” even mean?

Normally at Antithesis, “beating” a game means that we discover a set of user inputs that play the game to a winning conclusion. As Tetris has no win state, defining exactly should count as beating the game is a bit slippery. Our understanding of this evolved along the way (with some help from the teenage gamers).

Antithesis plays Tetris in an unusual way, so first I’ll explain how that works. We don’t do the typical kind of tool-assisted speedrun that crafts a set of perfect user inputs from first principles. Nor do we create a robot that knows how to play the game, watches the screen and reacts.

Instead, we run smallish trials of random inputs at known starting game states, evaluate the effect each trial has on the game, and remember game situations that we deem to be interesting with respect to a goal.

We take advantage of the fact that what happens in a NES game is completely determined by the user inputs it receives, which allows us to be stingy about taking game snapshots. As long as we save every input we have sent to the game, we can recreate an arbitrary game state by starting from the closest snapshot in the state’s history and feeding it the inputs needed to get us to the exact game moment we want.2

In a typical setup for a test run, one Antithesis fuzzer feeds 32 emulated NES sessions, each loaded with the game we are fuzzing. We host each session with the fceux emulator, and use fceux’s Lua integration to create a harness script that runs the emulator in headless mode.3

Inside the fuzzer, we have a component called a “tactic” which generates input for the game we are testing. A second “strategy” component then observes what the result of the input is and guides the fuzzer for what to do next. (See our previous post on beating Gradius for more on this.)

To see how this all fits together, it helps to zoom into a single frame. An NES game cycles at a rate of 60 screen updates per second. Each cycle has the following actions:

  • Our harness receives an input byte from the fuzzer from an input pipe.
  • The harness reads the input byte and sends it to the waiting emulator.
  • The emulator processes the input byte, modifying game memory, and generating a new game screen, and then returns to the harness.
  • The harness interrogates the emulator and captures values of specific game memory locations and optionally the new game screen. The results get put on an output pipe that goes back to the fuzzer to process asynchronously.
  • The harness loops back to process the next input.

The fuzzer is asynchronous with respect to this loop. It pre-generates batches of inputs for the harnesses, and observes the returns from the harnesses as they arrive.

With that machinery in place, beating a game comes down to two things:

  • Finding the right tactic (algorithm for generating inputs)
  • Finding the right strategy to guide the fuzzer to winning the game.

For Tetris, a game with no end, our first thought was that beating it would mean to do as well as the best humans could do. The thing we failed to take into account was all the other humans.

My coworker Alex Pshenichkin had already tried getting the fuzzer to play Tetris. He’d figured out the foundations of how to get the current piece layout by reading emulator memory. He used this to calculate our first goodness measure, based on some research he found about playing Tetris.

This version took a long time to play because the tactic that generated input created basically random inputs.4 When we took the results of our runs and converted them into a movie, watching the pieces drop was like watching snowflakes caught in a gentle breeze. They would slowly meander down the screen, undulating back and forth, and eventually settle in a random place.

Our strategy measured the board state at each frame, and explored in parallel many diverse patterns of board layout. Combined with the random tactic, this gave us an approach where we were all-in on randomness. In the time we devoted to running this, we never could clear more than a few lines.

Alex did amazing work getting the core setup working, but his real job pulled at his time, and he went on to be the key person on the deterministic hypervisor at the core of our product. Tetris would have to sit on the shelf until a global pandemic brought it front and center again.5

In 2020, while we were stuck at home due to COVID, I started work on Tetris as a side project. I wanted to make our tactics and strategy behave more like the best humans did.

Our aim was to beat the game for good by getting past level 29, the point that traditionally defeated human gamers. At level 29 the speed that pieces drop suddenly doubles, from 2 frames per row to 1 frame per row, so that a piece falls the full height of the playfield in about a third of a second. At this speed, the standard method of holding down left or right can’t move a piece all the way from the spawn point to the far edge before it reaches the bottom. So for decades, level 29 was considered unsurvivable: a “kill screen” where the game effectively ended itself no matter how good you were.

My new tactic generated input bytes specifically for Tetris. It mimicked what the top players did when encountering a new piece — first rotate, then move left or right to a particular column, then go down-down-down until the piece was placed.

I coupled this with a revamped strategy. I took the game board evaluation function from the first attempt, and converted it to an objective function that was designed to run when a new piece was ready to drop. The objective function took into account how tall the stack of pieces was (smaller is better), how far down the board the piece was when we locked it (deeper is better), and how many “bubbles” we left in the assembled mass of pieces, where a bubble is a trapped empty square we can’t fill. More bubbles, more troubles.

The strategy we used ran the objective function when game memory told us there was a new piece. With each new piece, we also determined what “strategy bucket” we had reached. A bucket is defined by a tuple made up of:

(number of pieces seen so far in the game, column the last piece was placed into).

In each bucket, we stored its score from the objective function and a game state we could use to return to that point in the game.

When the strategy chose a bucket to explore, the fuzzer loaded the saved game state into an emulator. From there, it invoked the tactic to generate random Tetris moves. We were constantly jumping back in time to interesting game states and trying again. When we landed back in a bucket we had already filled, the objective function decided whether to keep the state we had, or replace it with the newly-discovered one. Across thousands of runs, each bucket slowly accumulated the best game state we had found for each (piece count, column) combination.

This objective function naturally goes up as we clear more and more lines: the only way to satisfy it is to keep going a little further. As we played more and more games we would improve the game states we saved for each game position.

This same strategy had worked well for other NES games. Tetris, however, was different. We ended up making progress, but stalled out around level 15. The breakthrough came from my coworker Josh Reagan, who was building a new strategy with our co-founder Dave Scherer: a “graph-aware” strategy. Their insight was to stop treating the buckets as a disconnected pile and instead wire them into a graph: each bucket becomes a node, each edge carries the reward for getting from one node to the next. The strategy searches for the highest-reward path to each node on the way to the deepest one. The objective function was perfect to hand out those rewards. Josh switched us over, and kicked off an overnight run. We would know more in the morning.6

We got to level 41, well past the level 29 “kill screen”. When we got back to the office we named a conference room “Tetris”. We were done!

As you can probably tell from the length of the remaining blog post, we were in fact very much not done.

We thought we’d beaten human gamers, but the definition of what that meant was changing out from under us. We happened to be trying to beat Tetris at exactly the time that gamers were blowing right past the old limits.

Even in 2020, the human state of the art had advanced beyond level 29. Weirdly, it was very close to where we had landed, with level 38 reached for the first time by teenage gamer Eric Tolt.

The breakthrough that made this possible was more advanced button pressing techniques. Instead of holding down left or right, you tap them extremely rapidly instead. The first technique developed, hypertapping, relied on using arm muscle tremors to vibrate the button. This was what was used by Tolt in 2020. However, hypertapping caused a lot of physical strain and so stayed pretty niche.

Around the same time, a new technique, rolling, was developed. Rolling involved placing one finger on left or right while drumming the fingers of the other hand against the back of the controller, pushing the button down repeatedly. This was much less physically demanding, and importantly, even faster. Gamers started switching to it and smashing more records.

In 2023, the 13-year-old gamer Willis Gibson, known as “Blue Scuti”, used the rolling technique to reach level 157, and became the first human player ever to crash the game. As the game plays on into uncharted territory, strange bugs start to appear that cause the game to reset, freeze or just go black.

This was about the time that Antithesis came out of stealth. When our blog launched people asked “Hey, we beat Tetris, right? You should do a blog post.” I had to sheepishly admit, “well, there’s this 13-year-old who can outplay us… and please don’t ask about the conference room.”

Then in late 2024, the 16-year-old Michael Artiaga (“dogplayingtetris”, or just “Dog”) achieved the unthinkable. Playing by hand on a modified cartridge that let him get past the crash triggers, he got to level 255 and then advanced one more level, rolling the level counter back to 0. When this happens, the colors, piece speed, and scoring increments all go back to where they are at game start. This achievement, known as “rebirth”, had been seen in tool-assisted runs, but this was the first time that a human had reached it.

What makes this feat particularly impressive is all the bugs he had to navigate around in the high levels. A particularly bad one at level 235 stops the level counter from incrementing after the normal 10 lines; instead, Artiaga had to clear a ridiculous 810 lines. To make things even worse, he hit an unrelated color palette glitch that turned all the pieces dark green, making them very hard to see on the black background.

The teenagers had moved the goalposts again. We had climbed a mountain only to see that there was a bigger mountain. We had to get to rebirth.

April 2025: I had a window between projects, and the marketing people were asking for the Tetris post again.

With the help of my coworker Peter Stiglitz, I’d been researching the way humans played Tetris. I expected that we would need some counter-measures against the glitches in the game that were known to happen in deep levels.

Peter found that some of the glitches required clearing multiple lines at a time in certain places in the game. Also we had not yet maxed the score. The humans in the Tetris world championships competed on score, and could occasionally reach 999999, the maximum score possible in the original version. Our strategy did not care about the score as much, so as part of matching the best humans and getting to rebirth, I thought we should also max the score on the way. Thus “Project 999999” was born.

Looking at the humans, I saw that the competitive players tried to clear four lines with one piece. You get a bigger bonus the more lines you clear at once. The biggest bonus, called a “Tetris” in the game, is to clear four at once. The only piece that can reach four lines at the same time is the straight “I” piece.7 To score a Tetris, a player must build a solid structure four tetrominoes high and completely filled in, except for an empty well one square wide. Then the player must maintain this until an I piece appears, rotate the I to a vertical position and drop it in the well. This will clear all four lines and earn the Tetris bonus.

This led me to create an addition to the objective function. I wrote code that would evaluate a Tetris board and score it on Tetris readiness. A deeper well meant a higher readiness score.8

The objective function we landed on

This is the objective function that would (eventually) carry us to Tetris victory:

score=1(Level+50)BubbleCount2+10all piecesPieceDepth+30LinesCleared+50TetrisReadiness+GameScore\begin{aligned} \text{score} &= -1 \cdot (\text{Level} + 50) \cdot \text{BubbleCount}^2 \\ &+ 10 \cdot \sum_{\text{all pieces}} \text{PieceDepth} \\ &+ 30 \cdot \text{LinesCleared} \\ &+ 50 \cdot \text{TetrisReadiness} \\ &+ \text{GameScore} \end{aligned}
联系我们 contact @ memedata.com