← Arcade ๐Ÿ† Players My player card

Retro Maelstrom

โ˜… Retro Maelstrom โ˜…
Retro Maelstrom gameplay screenshot
๐Ÿ“ฆ Game code, download & prompt info
๐Ÿ–ฅ๏ธ Desktop app (python) โ€” play it live above, right in your browser, or download it below
  • Model: openrouter:stealth/union-alpha
  • Test: Retro: 3D Asteroids (pygame + PyOpenGL)
  • Category: retrogames
  • Kind: code ยท scored as score (higher is better)
  • Run: โ€”
  • Benchmark score: 98.0
  • Benchmark date: 2026-09-17
  • Published: 2026-09-25
Original prompt
Write a complete, runnable Python program using `pygame` and `PyOpenGL` that renders a 3D Asteroids scene. Create the window with `pygame.display.set_mode((800,600), pygame.OPENGL | pygame.DOUBLEBUF)`, set up a perspective projection, and draw several rotating 3D asteroids (use `glBegin(GL_TRIANGLES)` / `glVertex3f` from `from OpenGL.GL import *`) drifting and rotating in 3D space, plus a small 3D ship. Rotate the camera/scene slightly each frame. Handle the QUIT event. Use only primitive geometry - no external models. Design note (interpret freely): Maelstrom was a 3D-rendered Asteroids upgrade - thrust and rotate a ship in open space, blast asteroids that split into fragments, and collect power-up 'goodies' (triple/long shots) while dodging an indestructible steel asteroid and enemy saucers. It proved fast arcade action could run on a desktop computer. SCORING & PERSISTENT HIGH-SCORE BOARD (REQUIRED): The game MUST award points for gameplay actions and maintain a persistent high-score table. When a run finishes, prompt the player to enter their name/initials and record it with the score. Save the top 5 scores (name + score) to a local JSON file in the working directory (e.g. high_scores.json), and also write the latest finished run as {"initials": name, "score": number} to /tmp/alpaca_score.json (overwrite it every run) so the arcade scoreboard can pick it up automatically, load and display them on startup and on the game-over screen, and write a new entry whenever a run finishes. Scores must survive restarting the program. Every new game MUST start with the score reset to 0 - never carry over the previous game's score into the next session. The program must run top-to-bottom and open a visible 3D window. OUTPUT REQUIREMENT: Respond with ONLY the raw, runnable source code in a single fenced code block, with the first line starting at column 0 (no leading indentation). Do not include any thinking, explanation, summary, or prose before or after the code, and keep comments inside the code minimal.
โฌ‡ Download game.py
import json
import math
import os
import random
import pygame
from OpenGL.GL import *
from OpenGL.GLU import gluPerspective

WIDTH, HEIGHT = 800, 600
FIELD_X, FIELD_Y = 23.0, 16.0
HIGH_SCORE_FILE = "high_scores.json"
LATEST_SCORE_FILE = "/tmp/alpaca_score.json"


def load_scores(path):
    try:
        with open(path, "r", encoding="utf-8") as f:
            data = json.load(f)
        if not isinstance(data, list):
            return []
        result = []
        for item in data:
            if not isinstance(item, dict):
                continue
            name = item.get("initials", item.get("name", ""))
            score = item.get("score")
            if isinstance(name, str) and isinstance(score, int) and score >= 0:
                result.append({"initials": name[:12], "score": score})
        return sorted(result, key=lambda item: item["score"], reverse=True)[:5]
    except (OSError, ValueError, TypeError):
        return []


def load_latest():
    try:
        with open(LATEST_SCORE_FILE, "r", encoding="utf-8") as f:
            item = json.load(f)
        if (isinstance(item, dict)
                and isinstance(item.get("initials"), str)
                and isinstance(item.get("score"), int)):
            return item
    except (OSError, ValueError, TypeError):
        pass
    return None


def write_json(path, data):
    temporary = path + ".tmp"
    with open(temporary, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2)
        f.flush()
        os.fsync(f.fileno())
    os.replace(temporary, path)


def normalize(v):
    length = math.sqrt(sum(x * x for x in v))
    return tuple(x / length for x in v) if length else (0, 0, 1)


def triangle(a, b, c):
    u = tuple(b[i] - a[i] for i in range(3))
    v = tuple(c[i] - a[i] for i in range(3))
    n = normalize((u[1] * v[2] - u[2] * v[1],
                   u[2] * v[0] - u[0] * v[2],
                   u[0] * v[1] - u[1] * v[0]))
    glNormal3f(*n)
    glVertex3f(*a)
    glVertex3f(*b)
    glVertex3f(*c)


def make_mesh(rough=False):
    top, bottom = (0, 0, 1), (0, 0, -1)
    ring = [(1, 0, 0), (0, 1, 0), (-1, 0, 0), (0, -1, 0)]
    faces = []
    for i in range(4):
        a, b = ring[i], ring[(i + 1) % 4]
        faces.extend([(top, a, b), (bottom, b, a)])

    refined = []
    for a, b, c in faces:
        ab = normalize(tuple(a[i] + b[i] for i in range(3)))
        bc = normalize(tuple(b[i] + c[i] for i in range(3)))
        ca = normalize(tuple(c[i] + a[i] for i in range(3)))
        refined.extend([(a, ab, ca), (ab, b, bc), (ca, bc, c), (ab, bc, ca)])

    vertices = {}
    mesh = []
    for face in refined:
        result = []
        for vertex in face:
            if vertex not in vertices:
                factor = random.uniform(0.78, 1.18) if rough else 1.0
                vertices[vertex] = tuple(x * factor for x in vertex)
            result.append(vertices[vertex])
        mesh.append(tuple(result))
    return mesh


def draw_mesh(mesh):
    glBegin(GL_TRIANGLES)
    for face in mesh:
        triangle(*face)
    glEnd()


def wrap(value, bound):
    return (value + bound) % (2 * bound) - bound


def distance_squared(a, b):
    dx = wrap(a[0] - b[0], FIELD_X)
    dy = wrap(a[1] - b[1], FIELD_Y)
    return dx * dx + dy * dy


class Asteroid:
    def __init__(self, size=3, position=None, velocity=None):
        self.size = size
        self.radius = {1: 0.62, 2: 1.12, 3: 1.95}[size]
        self.position = list(position or (
            random.uniform(-FIELD_X, FIELD_X),
            random.uniform(-FIELD_Y, FIELD_Y),
            random.uniform(-0.9, 0.9)))
        speed = random.uniform(1.3, 2.6) * (1.5 - size * 0.15)
        angle = random.uniform(0, math.tau)
        self.velocity = list(velocity or (
            math.cos(angle) * speed,
            math.sin(angle) * speed,
            random.uniform(-0.22, 0.22)))
        self.rotation = [random.uniform(0, 360) for _ in range(3)]
        self.spin = [random.uniform(-38, 38) for _ in range(3)]
        self.mesh = make_mesh(True)
        shade = random.uniform(0.5, 0.78)
        self.color = (shade, shade * 0.82, shade * 0.64)

    def update(self, dt):
        for i in range(3):
            self.position[i] += self.velocity[i] * dt
            self.rotation[i] += self.spin[i] * dt
        self.position[0] = wrap(self.position[0], FIELD_X)
        self.position[1] = wrap(self.position[1], FIELD_Y)
        if abs(self.position[2]) > 1.0:
            self.velocity[2] = -math.copysign(abs(self.velocity[2]), self.position[2])

    def draw(self):
        glPushMatrix()
        glTranslatef(*self.position)
        for angle, axis in zip(self.rotation, ((1, 0, 0), (0, 1, 0), (0, 0, 1))):
            glRotatef(angle, *axis)
        glScalef(self.radius, self.radius, self.radius)
        glColor3f(*self.color)
        draw_mesh(self.mesh)
        glPopMatrix()


class Game:
    def __init__(self):
        pygame.init()
        pygame.display.set_mode((800, 600), pygame.OPENGL | pygame.DOUBLEBUF)
        pygame.display.set_caption("Maelstrom Drift - 3D Asteroids")
        self.clock = pygame.time.Clock()
        self.fonts = {
            "small": pygame.font.Font(None, 23),
            "normal": pygame.font.Font(None, 29),
            "large": pygame.font.Font(None, 58),
        }
        self.text_cache = {}
        self.scores = load_scores(HIGH_SCORE_FILE)
        self.latest = load_latest()
        self.state = "title"
        self.running = True
        self.quit_after_entry = False
        self.initials = ""
        self.save_message = ""
        self.score = 0
        self.lives = 3
        self.wave = 0
        self.time = 0
        self.ship_position = [0.0, 0.0, 0.0]
        self.ship_velocity = [0.0, 0.0]
        self.ship_angle = 0.0
        self.invulnerable = 0
        self.cooldown = 0
        self.thrusting = False
        self.bullets = []
        self.asteroids = [Asteroid() for _ in range(9)]
        self.bullet_mesh = make_mesh()
        self.stars = [
            (random.uniform(-65, 65), random.uniform(-45, 45),
             random.uniform(-35, -9), random.uniform(0.35, 1))
            for _ in range(260)
        ]
        glClearColor(0.008, 0.012, 0.03, 1)
        glEnable(GL_DEPTH_TEST)
        glEnable(GL_NORMALIZE)
        glEnable(GL_LIGHTING)
        glEnable(GL_LIGHT0)
        glEnable(GL_COLOR_MATERIAL)
        glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
        glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.95, 0.95, 1.0, 1))
        glLightModelfv(GL_LIGHT_MODEL_AMBIENT, (0.24, 0.25, 0.32, 1))
        glShadeModel(GL_FLAT)
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1)

    def new_game(self):
        self.score = 0
        self.lives = 3
        self.wave = 0
        self.bullets = []
        self.asteroids = []
        self.initials = ""
        self.save_message = ""
        self.quit_after_entry = False
        self.reset_ship()
        self.next_wave()
        self.state = "playing"

    def reset_ship(self):
        self.ship_position = [0.0, 0.0, 0.0]
        self.ship_velocity = [0.0, 0.0]
        self.ship_angle = 0.0
        self.invulnerable = 3.0
        self.cooldown = 0.25
        self.thrusting = False

    def next_wave(self):
        self.wave += 1
        for _ in range(min(4 + self.wave, 15)):
            asteroid = Asteroid()
            while distance_squared(asteroid.position, self.ship_position) < 64:
                asteroid.position[0] = random.uniform(-FIELD_X, FIELD_X)
                asteroid.position[1] = random.uniform(-FIELD_Y, FIELD_Y)
            self.asteroids.append(asteroid)

    def finish_run(self):
        if self.state != "playing":
            return
        self.state = "entry"
        self.thrusting = False
        self.initials = ""
        pygame.key.start_text_input()

    def record_run(self):
        if self.state != "entry":
            return
        name = self.initials.strip() or "AAA"
        entry = {"initials": name, "score": int(self.score)}
        self.latest = entry
        self.scores.append(entry)
        self.scores = sorted(
            self.scores, key=lambda item: item["score"], reverse=True)[:5]
        errors = []
        for path, data in ((HIGH_SCORE_FILE, self.scores),
                           (LATEST_SCORE_FILE, entry)):
            try:
                write_json(path, data)
            except OSError as exc:
                errors.append(os.path.basename(path))
                print("Could not save", path, ":", exc)
        self.save_message = (
            "Save failed: " + ", ".join(errors) if errors else "Score saved.")
        pygame.key.stop_text_input()
        self.state = "gameover"
        if self.quit_after_entry:
            self.running = False

    def events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                if self.state == "playing":
                    self.quit_after_entry = True
                    self.finish_run()
                elif self.state == "entry":
                    self.quit_after_entry = True
                    self.record_run()
                else:
                    self.running = False
            elif event.type == pygame.TEXTINPUT and self.state == "entry":
                clean = "".join(c for c in event.text.upper()
                                if c.isascii() and (c.isalnum() or c in "_-"))
                self.initials = (self.initials + clean)[:12]
            elif event.type == pygame.KEYDOWN:
                if self.state == "entry":
                    if event.key == pygame.K_BACKSPACE:
                        self.initials = self.initials[:-1]
                    elif event.key in (pygame.K_RETURN, pygame.K_KP_ENTER):
                        self.record_run()
                    elif event.key == pygame.K_ESCAPE:
                        self.record_run()
                elif self.state in ("title", "gameover"):
                    if event.key in (pygame.K_RETURN, pygame.K_KP_ENTER):
                        self.new_game()
                    elif event.key == pygame.K_ESCAPE:
                        self.running = False
                elif event.key == pygame.K_ESCAPE:
                    self.finish_run()

    def shoot(self):
        angle = math.radians(self.ship_angle)
        direction = (-math.sin(angle), math.cos(angle))
        self.bullets.append({
            "position": [
                self.ship_position[0] + direction[0] * 1.25,
                self.ship_position[1] + direction[1] * 1.25,
                0.0],
            "velocity": [
                self.ship_velocity[0] + direction[0] * 28,
                self.ship_velocity[1] + direction[1] * 28],
            "life": 1.35,
        })
        self.cooldown = 0.16

    def update(self, dt):
        self.time += dt
        if self.state in ("entry", "gameover"):
            return
        for asteroid in self.asteroids:
            asteroid.update(dt)
        if self.state != "playing":
            return

        keys = pygame.key.get_pressed()
        turn = int(keys[pygame.K_LEFT] or keys[pygame.K_a])
        turn -= int(keys[pygame.K_RIGHT] or keys[pygame.K_d])
        self.ship_angle += turn * 205 * dt
        self.thrusting = bool(keys[pygame.K_UP] or keys[pygame.K_w])
        if self.thrusting:
            angle = math.radians(self.ship_angle)
            self.ship_velocity[0] -= math.sin(angle) * 13 * dt
            self.ship_velocity[1] += math.cos(angle) * 13 * dt
        speed = math.hypot(*self.ship_velocity)
        if speed > 15:
            self.ship_velocity = [v * 15 / speed for v in self.ship_velocity]
        for i, bound in enumerate((FIELD_X, FIELD_Y)):
            self.ship_velocity[i] *= math.exp(-0.16 * dt)
            self.ship_position[i] = wrap(
                self.ship_position[i] + self.ship_velocity[i] * dt, bound)
        self.invulnerable = max(0, self.invulnerable - dt)
        self.cooldown -= dt
        if keys[pygame.K_SPACE] and self.cooldown <= 0:
            self.shoot()

        surviving_bullets = []
        for bullet in self.bullets:
            bullet["life"] -= dt
            if bullet["life"] <= 0:
                continue
            for i, bound in enumerate((FIELD_X, FIELD_Y)):
                bullet["position"][i] = wrap(
                    bullet["position"][i] + bullet["velocity"][i] * dt, bound)
            hit = next((a for a in self.asteroids
                        if distance_squared(a.position, bullet["position"])
                        < (a.radius + 0.18) ** 2), None)
            if hit is None:
                surviving_bullets.append(bullet)
                continue
            self.score += {3: 20, 2: 50, 1: 100}[hit.size]
            self.asteroids.remove(hit)
            if hit.size > 1:
                angle = random.uniform(0, math.tau)
                for sign in (-1, 1):
                    dx, dy = math.cos(angle) * sign, math.sin(angle) * sign
                    position = [hit.position[0] + dx * 0.4,
                                hit.position[1] + dy * 0.4, hit.position[2]]
                    velocity = [hit.velocity[0] + dx * 2.8,
                                hit.velocity[1] + dy * 2.8,
                                random.uniform(-0.2, 0.2)]
                    self.asteroids.append(Asteroid(hit.size - 1, position, velocity))
        self.bullets = surviving_bullets

        if self.invulnerable <= 0:
            for asteroid in self.asteroids:
                if distance_squared(asteroid.position, self.ship_position) < (
                        asteroid.radius + 0.55) ** 2:
                    self.lives -= 1
                    if self.lives <= 0:
                        self.finish_run()
                    else:
                        self.reset_ship()
                    break
        if self.state == "playing" and not self.asteroids:
            self.score += 250 * self.wave
            self.next_wave()

    def draw_ship(self):
        if self.state == "playing" and self.invulnerable > 0:
            if int(self.time * 12) % 2:
                return
        glPushMatrix()
        glTranslatef(*self.ship_position)
        glRotatef(self.ship_angle, 0, 0, 1)
        nose = (0, 1.15, 0)
        left = (-0.78, -0.72, -0.18)
        right = (0.78, -0.72, -0.18)
        ridge = (0, -0.25, 0.42)
        underside = (0, -0.4, -0.32)
        glBegin(GL_TRIANGLES)
        glColor3f(0.3, 0.8, 1)
        triangle(nose, left, ridge)
        glColor3f(0.8, 0.9, 1)
        triangle(nose, ridge, right)
        glColor3f(0.16, 0.35, 0.55)
        triangle(nose, underside, left)
        triangle(nose, right, underside)
        triangle(left, right, ridge)
        triangle(left, underside, right)
        if self.thrusting:
            flame = (0, -1.6 - 0.35 * math.sin(self.time * 55), 0)
            glColor3f(1, 0.4, 0.04)
            triangle((-0.3, -0.65, 0), flame, (0, -0.65, 0.25))
            glColor3f(1, 0.85, 0.1)
            triangle((0, -0.65, 0.25), flame, (0.3, -0.65, 0))
        glEnd()
        glPopMatrix()

    def text(self, value, x, y, size="normal", color=(225, 237, 255), center=False):
        key = (value, size, color)
        if key not in self.text_cache:
            if len(self.text_cache) > 256:
                self.text_cache.clear()
            surface = self.fonts[size].render(value, True, color)
            self.text_cache[key] = (
                surface.get_width(), surface.get_height(),
                pygame.image.tostring(surface, "RGBA", True))
        width, height, pixels = self.text_cache[key]
        if center:
            x -= width // 2
        glRasterPos2f(x, HEIGHT - y - height)
        glDrawPixels(width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels)

    def panel(self, bottom=545):
        glColor4f(0.015, 0.025, 0.06, 0.9)
        glBegin(GL_QUADS)
        glVertex2f(95, HEIGHT - 65)
        glVertex2f(705, HEIGHT - 65)
        glVertex2f(705, HEIGHT - bottom)
        glVertex2f(95, HEIGHT - bottom)
        glEnd()

    def score_table(self, y):
        self.text("PERSISTENT TOP 5", 400, y, color=(255, 208, 105), center=True)
        for i in range(5):
            if i < len(self.scores):
                item = self.scores[i]
                name, score = item["initials"], str(item["score"])
            else:
                name, score = "---", "---"
            self.text(f"{i + 1}.  {name}", 220, y + 37 + i * 29)
            self.text(score, 520, y + 37 + i * 29)

    def draw_ui(self):
        glDisable(GL_LIGHTING)
        glDisable(GL_DEPTH_TEST)
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glMatrixMode(GL_PROJECTION)
        glPushMatrix()
        glLoadIdentity()
        glOrtho(0, WIDTH, 0, HEIGHT, -1, 1)
        glMatrixMode(GL_MODELVIEW)
        glPushMatrix()
        glLoadIdentity()

        if self.state == "playing":
            self.text(f"SCORE {self.score}", 18, 15)
            self.text(f"LIVES {self.lives}", 325, 15)
            self.text(f"WAVE {self.wave}", 660, 15)
            self.text("Arrows/WAD: fly   SPACE: fire   ESC: finish run",
                      400, 573, "small", center=True)
        elif self.state == "entry":
            self.panel(470)
            self.text("RUN FINISHED", 400, 96, "large", center=True)
            self.text(f"Final score: {self.score}", 400, 175, center=True)
            self.text("Enter your name / initials (12 characters)", 400, 239,
                      center=True)
            cursor = "_" if int(self.time * 2) % 2 == 0 else " "
            self.text(self.initials + cursor, 400, 290, "large",
                      (255, 208, 105), True)
            self.text("ENTER saves   Backspace edits   Blank name becomes AAA",
                      400, 375, "small", center=True)
            if self.quit_after_entry:
                self.text("Your score will be saved before closing.",
                          400, 415, "small", center=True)
        else:
            self.panel()
            title = "MAELSTROM DRIFT" if self.state == "title" else "GAME OVER"
            self.text(title, 400, 86, "large", center=True)
            subtitle = ("Primitive geometry. Endless space. Three lives."
                        if self.state == "title" else
                        f"Final score: {self.score}   {self.save_message}")
            self.text(subtitle, 400, 148, "small", center=True)
            self.score_table(198)
            if self.latest:
                self.text(
                    f"Latest run: {self.latest['initials'][:12]}  "
                    f"{self.latest['score']}",
                    400, 399, "small", center=True)
            self.text("ENTER: new game    ESC: quit", 400, 440,
                      color=(120, 230, 255), center=True)
            self.text("Left/Right or A/D: rotate   Up/W: thrust   Space: fire",
                      400, 484, "small", center=True)
            self.text("Rocks: 20 / 50 / 100 pts   Clear wave: 250 x wave",
                      400, 510, "small", center=True)

        glPopMatrix()
        glMatrixMode(GL_PROJECTION)
        glPopMatrix()
        glMatrixMode(GL_MODELVIEW)
        glDisable(GL_BLEND)
        glEnable(GL_DEPTH_TEST)
        glEnable(GL_LIGHTING)

    def draw(self):
        glViewport(0, 0, WIDTH, HEIGHT)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(50, WIDTH / HEIGHT, 0.1, 160)
        glMatrixMode(GL_MODELVIEW)
        glLoadIdentity()
        glTranslatef(0, 0, -49)
        glRotatef(8 + math.sin(self.time * 0.23) * 3, 1, 0, 0)
        glRotatef(math.sin(self.time * 0.19) * 5, 0, 1, 0)
        glRotatef(math.sin(self.time * 0.13) * 3, 0, 0, 1)
        glLightfv(GL_LIGHT0, GL_POSITION, (-12, 18, 24, 1))

        glDisable(GL_LIGHTING)
        glPointSize(2)
        glBegin(GL_POINTS)
        for x, y, z, brightness in self.stars:
            glColor3f(brightness * 0.8, brightness * 0.88, brightness)
            glVertex3f(x, y, z)
        glEnd()
        glEnable(GL_LIGHTING)

        for asteroid in self.asteroids:
            asteroid.draw()
        if self.lives > 0 or self.state == "title":
            self.draw_ship()

        glDisable(GL_LIGHTING)
        glColor3f(0.5, 1, 0.85)
        for bullet in self.bullets:
            glPushMatrix()
            glTranslatef(*bullet["position"])
            glScalef(0.15, 0.15, 0.3)
            draw_mesh(self.bullet_mesh)
            glPopMatrix()
        glEnable(GL_LIGHTING)
        self.draw_ui()
        pygame.display.flip()

    def run(self):
        try:
            while self.running:
                dt = min(self.clock.tick(60) / 1000.0, 0.033)
                self.events()
                if not self.running:
                    break
                self.update(dt)
                self.draw()
        finally:
            pygame.quit()


if __name__ == "__main__":
    Game().run()

๐ŸŽฎ Press โ–ถ Play above to run it live in your browser, or download it below and run locally (pip install pygame), then enter your score by hand