While the technical teardowns in Architecture Field Notes take significant time to produce, I want a more frequent outlet for architectural reflections. Welcome to Architect’s Arcade. This series uses simple game mechanics to isolate and explain the core principles behind high performance systems. Consider this the tactical DLC to our main quest. Shall we play?
If you plan to build a Tetris engine, the core requirement looks easy at first. The block must drop every second, and the game must respond to your keyboard inputs instantly.
To observe this in action, we strip Tetris to its skeleton. We do not need complex shapes or rotations. We only need a single 1x1 pixel in the terminal. By isolating this, we can see exactly where the logic fails.
If you are used to writing s…
While the technical teardowns in Architecture Field Notes take significant time to produce, I want a more frequent outlet for architectural reflections. Welcome to Architect’s Arcade. This series uses simple game mechanics to isolate and explain the core principles behind high performance systems. Consider this the tactical DLC to our main quest. Shall we play?
If you plan to build a Tetris engine, the core requirement looks easy at first. The block must drop every second, and the game must respond to your keyboard inputs instantly.
To observe this in action, we strip Tetris to its skeleton. We do not need complex shapes or rotations. We only need a single 1x1 pixel in the terminal. By isolating this, we can see exactly where the logic fails.
If you are used to writing sequential scripts, your first instinct is likely a while loop with a time.sleep(1) call. It is the most honest implementation of the requirement, mirroring the human rhythm of the game. However, the moment you run this code, your application goes dark.
import curses
import time
def main(stdscr):
curses.curs_set(0)
y, x = 0, 10
while True:
stdscr.erase()
stdscr.addstr(y, x, "[]")
stdscr.refresh()
key = stdscr.getch()
if key == ord('q'):
break
elif key == curses.KEY_LEFT:
x -= 1
elif key == curses.KEY_RIGHT:
x += 1
y += 1
time.sleep(1)
if __name__ == "__main__":
curses.wrapper(main)
In this loop, the program is trapped in two ways. First, it stops at stdscr.getch() and refuses to move the block until you press something. Second, even if you do press a key, it immediately hits time.sleep(1) and refuses to listen to your keyboard again for a full second.
I call this the periodicity paradox. It is the logical assumption that because a task is periodic, the process itself should be periodic too. By attempting to be an efficient waiter, you have accidentally sacrificed the system’s ability to sense the world. To fix this, we have to stop waiting and start listening.
If we cannot simply use time.sleep(1) to track time, we need a different way to manage our tasks. We have to change our mental model from a sequential script to a multitasking system.
Think of the difference between a phone support agent and a live chat agent. A phone agent is locked into a single conversation. If the customer goes to find a serial number, the agent sits in silence. They are connected, but their productivity is zero. A live chat agent handles multiple customers at once. They do not wait for one person to type. Instead, they scan their windows at a high frequency, check the status of one, reply to another, and keep the work moving.
The Tetris engine needs to act like that chat agent. Rather than letting a single timer own the process, we introduce a dispatcher. This is the core of what we commonly call an event loop.
The dispatcher does not sleep for a full second. It sits in a tight loop and asks two questions as fast as possible:
Did the user press a key? 1.
Is it time to move the block?
By separating the gravity logic from the input logic, the game remains responsive. We are no longer trapping the execution flow in a timer. We are simply checking the state of the game many times per second.
To move our 1x1 pixel without freezing the system, we implement a breathing loop. The logic relies on keeping the loop running while manually tracking the passage of time. The magic happens by flipping the terminal into nonblocking mode with stdscr.nodelay(True). This single line tells the system that if the user has not pressed a key, stdscr.getch() should not wait. It returns a null value immediately so the loop can continue and check the clock.
import curses
import time
def main(stdscr):
curses.curs_set(0)
# Turn off blocking input
stdscr.nodelay(True)
stdscr.timeout(0)
y, x = 0, 10
last_drop = time.time()
tick_rate = 1.0
while True:
now = time.time()
stdscr.erase()
stdscr.addstr(y, x, “[]”)
stdscr.refresh()
# 1. Non blocking input polling
# Runs hundreds of times per second.
key = stdscr.getch()
if key == ord('q'):
break
elif key == curses.KEY_LEFT:
x -= 1
elif key == curses.KEY_RIGHT:
x += 1
# 2. Track time manually with
# jitter correction. Handle gravity
# only when the clock tells us to.
if now - last_drop >= tick_rate:
y += 1
# Align to expected timeline
# not the current “now”
last_drop += tick_rate
# 3. CPU Relief: Polite Polling
# Sleep just enough to save the CPU
# not enough to miss an input.
time.sleep(0.01)
if __name__ == “__main__”:
curses.wrapper(main)
This loop functions because of two critical mechanisms. The first is jitter correction. Notice that we do not reset last_drop to the current time after a move. Alternatively, we increment it by the fixed tick_rate. If your rendering logic takes 20 milliseconds, a naive reset like last_drop = time.time() would cause the clock to drift. Over several minutes, your game would become progressively slower because each second actually lasts 1.02 seconds. By adding to the previous timestamp, we ensure the heartbeat remains consistent even if individual frames are slightly delayed.
The second mechanism is polite polling. The tiny time.sleep(0.01) is our way of saving the CPU. Without it, the loop would run at the maximum frequency of your processor. It would peg a CPU core at 100% just to move a single pixel. We want the dispatcher to be responsive, but not a resource hog that steals resources from other processes.
The natural counter argument to a single loop is multithreading. Since the time.sleep command blocks the thread, why not create a separate thread for gravity and keep the main thread for input? In complicated game engines, this approach is common for background tasks like physics simulation or asset loading. But for the core logic of a 1x1 pixel game, it is a common trap.
When you have two threads moving the same object, you enter the territory of race conditions. Imagine the gravity thread increments the vertical position at the exact microsecond the input thread tries to move it sideways. To prevent data corruption, you must introduce a lock. Every time a thread wants to touch the position data, it must acquire the lock. If the gravity thread is busy, the input thread must wait.
The irony is clear. By introducing locks to manage concurrency, you have effectively reinvented blocking but with significantly more complex code. For systems with a shared state, the single threaded dispatcher remains the superior design. It is predictable, atomic by nature, and avoids the mental tax of debugging deadlocks. By keeping the logic in one loop, we ensure that every change to our world happens in a clean order without losing the ability to detect keyboard input.
The loop we built for our 1x1 pixel game is a functional engine. But even with all the mechanisms above, we are still relying on active polling. We wake up the CPU 100 times per second just to ask if anything happened. This is the architectural equivalent of a daily standup that repeats every weekday. Even if no work is done, everyone is exhausted from the constant checking. For a server handling hundreds of thousands of concurrent network connections, this creates a massive performance drain.
High performance systems like Nginx move away from this meeting fatigue. They rely on operating system primitives like epoll on Linux or kqueue on macOS (https://nginx.org/en/docs/events.html). These systems function like an emergency pager. In lieu of checking every connection for updates, they register their interests with the kernel once and then remain silent. They effectively tell the operating system not to wake me up until a network packet actually arrives or a specific timeout is reached.
This shift from constant status checks to as-needed notifications is how modern architecture scales. By removing the busy wait of polling, a system can remain quiet and consume almost zero resources until the real work arrives.
The 1x1 pixel taught us the mechanics of a dispatcher. The operating system teaches us the efficiency of interrupts. We have moved from a program that waits for time, to a program that is truly event driven.
We used the skeleton of Tetris to understand the mechanics of a non-blocking event loop. The arcade has a lot more to offer. In the upcoming installments of this journey, we will use familiar games to isolate the foundations of modern engineering:
Civilization and the Tech Tree: We will deconstruct the logic of Directed Acyclic Graphs (DAGs). This is the same engine that allows Maven to manage library versions and ensures Excel to calculate complicated formulas efficiently.
Monopoly and the Banker’s Lie: We will examine why a single bank balance in a database is a dangerous illusion. By exploring event sourcing, we will see why financial systems use immutable logs to track every transaction rather than just storing a final number.
Zelda and the Chemistry Engine: We will explore the power of decoupled architecture through a rule based chemistry engine. This mirrors the logic of smart home automation, where independent devices react to their environment without being hardcoded together, preventing a tangled mess of code.
The arcade is active. This 1x1 pixel was the starting point. It is time to move to the next level.