Building a Pygame Foundation
Introduction
Welcome to Pygame Foundations for your first game! Over the next four units, we will build the visual and interactive core of a small arcade game: a Dodge Game where a player slides left and right to avoid falling obstacles. By the end of this learning path, that game will run right in a browser preview.
Every game, from a text adventure to a AAA blockbuster, needs three things:
- A display surface to draw into;
- A loop that repeats over and over, redrawing the scene;
- A clean shutdown when the player is done.
This first unit builds exactly that. When we finish, we will have an 800 × 600 game canvas painted a calm dark blue that stays active until the program receives a quit event or is stopped through the browser preview controls. Pygame is a Python library for making 2D games: It handles displays, drawing, input, and sound for us. It comes pre-installed in the CodeSignal environment, so we can start coding immediately (on a personal machine, pip install pygame does the job).
The supplied environment runs the game through PygBag inside a browser preview. Unlike native desktop Pygame, this preview may not provide an operating-system title bar or close X. We will still handle pygame.QUIT because it is standard, portable Pygame structure and is used when a supported host asks the game to close.
Imports, Constants, and the Program Skeleton
Let's start at the top of our file with two imports and a few constants.
Naming values once at the top keeps them easy to find and change: If we later want a bigger display, we edit one line instead of hunting for 800 scattered across the file. Constants are written in uppercase by convention, signaling "do not change me while the game runs."
The async def main() plus asyncio.run(main()) shape may look unusual for a game. Here is the practical reason: Browsers refuse to let any single piece of code hog the processor, so a browser-ready Pygame program must be written as an async function. We do not need deep knowledge of asyncio; we just need this pattern, plus one extra line inside the loop that we will meet shortly.
Starting Pygame and Creating the Display
Now, let's wake Pygame up and ask it for a display surface.
pygame.init()is the standard convenience call that initializes importedPygamemodules that require initialization, such as the display and font modules. Some utilities, includingpygame.Rectand portions of the timing API, do not depend on initialization in the same way.pygame.display.set_mode((WIDTH, HEIGHT))creates the display and returns the display surface: The canvas that everything gets drawn onto. Notice the double parentheses:set_mode()takes a singletuple,(WIDTH, HEIGHT).pygame.display.set_caption("Dodge Game")sets the display caption. In native desktop Pygame this commonly appears in the window's title bar; in a browser-hosted PygBag preview it may instead affect browser metadata or have no visible title-bar equivalent.
Running this alone makes the preview start and then end immediately, because main() reaches its end and the program exits. Nothing yet keeps it alive.
How Pygame Sees the Screen
Before we draw anything, we need to know how positions work, because Pygame does not use the graph paper layout from math class. The origin (0, 0) sits at the top-left corner: x grows to the right, and y grows downward.
| Corner | Coordinates |
|---|---|
| Top-left | (0, 0) |
| Top-right | (799, 0) |
| Bottom-left | (0, 599) |
| Bottom-right | (799, 599) |
| Center | (400, 300) |
Each pair of numbers describes one pixel. Since we count from zero, an 800-pixel-wide display has valid x values from 0 to 799. The flipped y axis actually helps us later: An obstacle falling down the screen simply has an increasing y value.
The Game Loop and Handling the Quit Event
To keep the game active, we need a loop that keeps running until the host asks it to stop. Each single pass through this loop is one frame.
Pygame records input and host actions into an event queue. pygame.event.get() hands us that stored list and empties the queue. We must do this every frame so the game continues processing events and remains responsive.
We check event.type == pygame.QUIT, which is a request to close the game, and respond by setting running = False. In native desktop Pygame, clicking the operating-system window's close X normally generates this event. The browser-hosted PygBag preview may not expose an equivalent close button, so you may instead stop the program with the controls supplied by the learning environment. Keeping the pygame.QUIT check still makes the game portable across supported hosts.
The running flag lets the current frame finish neatly before the loop ends, rather than yanking the program out mid-frame. Finally, await asyncio.sleep(0) is the "let the browser breathe" line: It briefly hands control back so the page stays responsive.
Drawing the Background and Updating the Display
Right now, the loop keeps the preview active but never paints anything into it. Let's fill the surface with our color.
Colors in Pygame are tuples of three integers from 0 to 255, in red-green-blue order: (255, 0, 0) is bright red, (255, 255, 255) is white, (0, 0, 0) is black, and our DARK_BLUE = (20, 28, 48) is a deep navy with just a little red and green mixed in.
screen.fill() repaints every pixel of the surface. We do this on every frame, not just once, because it wipes away the previous frame; without it, moving objects would smear trails behind them. Then pygame.display.flip() updates the full visible display with the drawing currently on the display surface. Depending on the display mode and backend, Pygame may use buffering as part of presentation, but flip() itself does not guarantee a double-buffer swap unless the configuration supports or requests one. This gives us one firm rule: **all drawing happens before flip().
Capping the Frame Rate with a Clock
Left alone, our loop runs as fast as the processor allows, perhaps thousands of frames per second. That burns battery for no visual benefit, and worse, it makes motion speeds depend on the machine: An obstacle moving 5 pixels per frame would crawl on a slow laptop and rocket across a fast one. A clock fixes this.
We create one pygame.time.Clock object before the loop starts, then call clock.tick(FPS) once per frame, after flip(). tick() measures how long the frame took and pauses for whatever time is left over. With FPS = 60, each frame gets roughly milliseconds. 60 frames per second is the common standard: It matches many displays and looks smooth to the eye. From the next unit onward, this steady rate is what makes "move 5 pixels per frame" a predictable speed.
Shutting Down Cleanly and the Complete Program
One line remains: When the loop ends, we tell Pygame to release the display and other initialized resources.
Here is the finished program, with every piece in its place:
The body of the loop follows an order we will reuse for the rest of this path: process events → update state → draw → flip → tick → yield. Every remaining unit simply plugs new code into these same slots. Two mistakes to watch for: Forgetting flip(), which leaves a blank or frozen display even though the code "works"; and indenting pygame.quit() inside the loop, which shuts Pygame down after a single frame.
Conclusion and Next Steps
We just assembled the skeleton that every Pygame project shares: pygame.init() to initialize modules that require it, set_mode() to create a display and hand us its surface, an event loop that watches for pygame.QUIT, screen.fill() to repaint the background, pygame.display.flip() to update the visible display, clock.tick(FPS) to keep a steady pace, and pygame.quit() to close down cleanly.
The best mental model is a heartbeat: The game loop pulses sixty times a second, and each pulse asks the same questions, "What did the player do? What changed? What should the screen look like now?" Everything we add later — shapes, movement, collisions, scoring, and sound — lives inside that rhythm.
In the practices ahead, we will build this file up from nothing: First the skeleton, then the loop that keeps the preview active, then the dark blue background and the frame-rate cap. Once that canvas is holding steady, the next unit gets colorful as we draw real rectangles and circles onto it, so let's head to the editor and get that display running!
