Re-Direct is a top-down 2D action game built in 48 hours for the 2025 Kenney Jam, a game development hackathon. The game was co-developed with Henry Seefeldt, a fellow student at Texas A&M.
The core mechanic revolves around turning an enemy's own firepower against them: when an enemy bullet enters the player's grab radius, it becomes grabbable. Holding the grab button lets the player draw a circle with the mouse: the tighter and more complete the circle, the faster and larger the redirected shot becomes when released back at the nearest enemy.
The game was built with Pygame and structured to compile to WebAssembly via Pygbag, allowing it to run directly in the browser rather than requiring a local Python install.
The player redirecting an enemy bullet mid-combat.
Gameplay & Core Mechanic
The central skill-based mechanic, redirecting bullets by drawing a circle, required a way to evaluate how "circular" a freeform mouse path actually is, in real time, without relying on any external libraries.
The circularity check works by first finding the average center point of the drawn path, then measuring how much each point's distance from that center deviates from the average radius. A path with low variance, points sitting roughly equidistant from the center, reads as more circular; a jagged or elongated path reads as less circular. The path must also loop back near its starting point to be considered "closed."
utilities.py
def is_circle(path, tolerance=0.4):
if len(path) < 30:
return False # Not enough points to form a circle
xs, ys = zip(*path)
cx = sum(xs) / len(xs)
cy = sum(ys) / len(ys)
distances = [math.hypot(x - cx, y - cy) for x, y in path]
avg_radius = sum(distances) / len(distances)
radius_variance = sum((d - avg_radius) ** 2 for d in distances) / len(distances)
circularity = radius_variance / (avg_radius ** 2 + 1e-5)
start = pygame.Vector2(path[0])
end = pygame.Vector2(path[-1])
loop_closed = start.distance_to(end) < avg_radius * 0.5
return circularity < tolerance and loop_closed
def calculate_circularity(path):
if len(path) < 10:
return float('inf') # not enough data, worst possible score
xs, ys = zip(*path)
cx = sum(xs) / len(xs)
cy = sum(ys) / len(ys)
distances = [math.hypot(x - cx, y - cy) for x, y in path]
avg_radius = sum(distances) / len(distances)
radius_variance = sum((d - avg_radius) ** 2 for d in distances) / len(distances)
return radius_variance / (avg_radius ** 2 + 1e-5)
def circularity_to_accuracy(circularity, tolerance=0.4):
capped = min(circularity, tolerance * 2)
raw_score = 1.0 - (capped / (tolerance * 2))
accuracy = int(raw_score * 100)
return max(0, min(accuracy, 100)) / 100
Rather than treating the redirect as a binary success or failure, calculate_circularity() returns a continuous score, which circularity_to_accuracy() converts into a 0–1 accuracy value. That accuracy is then used to scale the launch speed and hitbox size of the redirected bullet: a clean circle produces a faster, larger, more dangerous shot, while a sloppy one produces a weak one.
The game loop itself was also restructured around Python's asyncio, which Pygbag requires in order to yield control back to the browser between frames, a necessary adaptation for running a Pygame project as WebAssembly rather than as a native desktop application.
redirect_game.py — async structure
import asyncio
async def main():
screen = pygame.display.set_mode((800, 800))
clock = pygame.time.Clock()
running = True
while running:
dt = clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# ... input handling, updates, and drawing ...
pygame.display.flip()
if not running:
pygame.quit()
exit()
await asyncio.sleep(0)
if __name__ == "__main__":
asyncio.run(main())
Assets & Sound Design
Alongside the code, I created the game's sprite work — character animations, enemy designs, bullets, and UI elements — as well as the background music and sound effects.
Consolidated sprite sheet: player, enemy, bullet, and UI assets, along with the pixel font (Eight-Bit Dragon) used throughout the game.
Original background music composed for Re-Direct.
Results & Reflections
If you've ever been in any sort of creative online space, you may have heard the mantra: "just make it exist first, you can make it good later." This project was a lesson in trusting that process. When designing under a time crunch, there's no room to focus on aesthetics or visuals; all of your effort has to go into core logic and functionality. That's especially true for a game, since what you see on the screen is the game.
The project's scope also changed constantly, probably more than ten times over the course of the 48 hours. At one point we wanted to include more than six shapes and a much more convoluted redirect mechanic, but the time limit forced us to pick and choose what we could realistically implement.