WebAssembly (Wasm) enables near-native performance for applications running in web browsers. Combined with tools like pygbag, it allows Python code—specifically games built with Pygame—to run directly in the browser without requirring users to install Python locally.
Why Pygbag?
While alternatives like Pyodide exist, they often lack support for complex libraries such as Pygame or suffer from slow startup times. pygbag fills this gap by packaging Pygame-based applications into WebAssembly modules. It leverages Emscripten under the hood but abstracts away much of the complexity, providing a streamlined workflow for deevlopers.
Building a Snake Game
Start by implementing a basic Snake game using Pygame. The following example creates a playable version where the player controls a red circle that grows when consuming white food items:
import pygame
import random
pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
running = True
dt = 0
head_pos = [pygame.Vector2(screen.get_width() / 2, screen.get_height() / 2)]
speed = 300
segment_radius = 30
food_location = pygame.Vector2(
random.randint(0, screen.get_width()),
random.randint(0, screen.get_height())
)
food_size = 15
movement = pygame.Vector2(0, 0)
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill("black")
keys = pygame.key.get_pressed()
if keys[pygame.K_w] or keys[pygame.K_UP]:
movement = pygame.Vector2(0, -1)
if keys[pygame.K_s] or keys[pygame.K_DOWN]:
movement = pygame.Vector2(0, 1)
if keys[pygame.K_a] or keys[pygame.K_LEFT]:
movement = pygame.Vector2(-1, 0)
if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
movement = pygame.Vector2(1, 0)
head_pos[0] += movement * speed * dt
if head_pos[0].distance_to(food_location) < segment_radius + food_size:
head_pos.append(head_pos[-1].copy())
food_location = pygame.Vector2(
random.randint(0, screen.get_width()),
random.randint(0, screen.get_height())
)
for i in range(len(head_pos) - 1, 0, -1):
head_pos[i] = head_pos[i - 1].copy()
pygame.draw.circle(screen, "white", food_location, food_size)
for pos in head_pos:
pygame.draw.circle(screen, "red", pos, segment_radius)
pygame.display.flip()
dt = clock.tick(60) / 1000
pygame.quit()
Adapting for Pygbag
To run this game in the browser via pygbag, wrap the main loop in an asynchronous function and use asyncio.sleep(0) to yield control back to the browser evant loop. Save the file as main.py in your project directory:
import pygame
import random
import asyncio
pygame.init()
screen = pygame.display.set_mode((1280, 720))
clock = pygame.time.Clock()
running = True
dt = 0
head_pos = [pygame.Vector2(screen.get_width() / 2, screen.get_height() / 2)]
speed = 300
segment_radius = 30
food_location = pygame.Vector2(
random.randint(0, screen.get_width()),
random.randint(0, screen.get_height())
)
food_size = 15
movement = pygame.Vector2(0, 0)
async def game_loop():
global running, dt, head_pos, food_location, movement
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill("black")
keys = pygame.key.get_pressed()
if keys[pygame.K_w] or keys[pygame.K_UP]:
movement = pygame.Vector2(0, -1)
if keys[pygame.K_s] or keys[pygame.K_DOWN]:
movement = pygame.Vector2(0, 1)
if keys[pygame.K_a] or keys[pygame.K_LEFT]:
movement = pygame.Vector2(-1, 0)
if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
movement = pygame.Vector2(1, 0)
head_pos[0] += movement * speed * dt
if head_pos[0].distance_to(food_location) < segment_radius + food_size:
head_pos.append(head_pos[-1].copy())
food_location = pygame.Vector2(
random.randint(0, screen.get_width()),
random.randint(0, screen.get_height())
)
for i in range(len(head_pos) - 1, 0, -1):
head_pos[i] = head_pos[i - 1].copy()
pygame.draw.circle(screen, "white", food_location, food_size)
for pos in head_pos:
pygame.draw.circle(screen, "red", pos, segment_radius)
pygame.display.flip()
dt = clock.tick(60) / 1000
await asyncio.sleep(0)
pygame.quit()
if __name__ == "__main__":
asyncio.run(game_loop())
Deployment with Pygbag
Install pygbag using pip:
pip install pygbag
From the parent directory of your game folder (e.g., if your game is in snake_game/, run from the directory containing snake_game/), execute:
python -m pygbag snake_game
This command starts a local development server at http://localhost:8000/. After a brief loading period, the game will appear in the browser, fully interactive and powered by WebAssembly.