← Arcade ๐Ÿ† Players My player card

Retro Space Invaders

โ˜… Retro Space Invaders โ˜…
Retro Space Invaders gameplay screenshot
๐Ÿ“ฆ Game code, download & prompt info
๐Ÿ–ฅ๏ธ Desktop app (python) โ€” play it live above, right in your browser, or download it below
  • Model: openrouter:deepseek/deepseek-v4.1-flash
  • Test: Retro: Space Invaders (playable, pygame UI)
  • Category: retrogames
  • Kind: code ยท scored as score (higher is better)
  • Run: โ€”
  • Benchmark score: 72.0
  • Benchmark date: 2026-09-16
  • Published: 2026-09-25
Original prompt
YOU ARE BEING TESTED: This is a benchmark test โ€” your code will be automatically run in a sandbox, displayed live on screen, and scored on whether it launches, plays correctly, and meets every requirement below. Write clean, working, creative code and give it your best effort. The arcade will display your game live for others to play and rate. Write a complete, runnable Python program using `pygame` that implements a playable Space Invaders-inspired arcade game. Open a window, draw a formation/grid of alien enemies that march side to side and drop down at the edge, a player ship at the bottom moved with LEFT/RIGHT arrows that fires bullets with SPACE, and bullets that destroy aliens on contact. Display the score. Include a game loop handling QUIT. Use only primitive shapes - no external assets. MOBILE RESPONSIVE (REQUIRED): The game must scale and work properly on mobile phone screens (minimum 375x667 portrait). Use responsive layout, visible touch controls alongside keyboard/mouse controls, and scale the game area and UI elements so they remain playable and readable on small screens.

CREATIVITY & MULTI-WAVE GAMEPLAY (REQUIRED โ€” DO NOT LIMIT TO ONE WAVE): The game must NOT end after a single wave. When all aliens are cleared, automatically spawn the next wave with escalating difficulty (faster march speed, extra rows, new formation, tighter spacing, or a new movement pattern) so the game can continue indefinitely as an arcade loop. You are encouraged to be creative โ€” add your own visual style, colors, animations, particle explosions, bonus UFOs, destructible bunkers, shields, power-ups, or other flourishes while keeping the core Space Invaders feel recognizable. Interpret the design freely rather than copying one exact layout. Design note (interpret freely): the original's defining trait is waves of 11-column alien grids marching down in unison, accelerating as fewer remain, while the player hides behind destructible bunkers. Capture that escalating march-and-accelerate tension that made it the first blockbuster arcade hit, but expand it into a full multi-wave arcade experience. 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. CODE HYGIENE (background habit, keep your own design): before finishing, briefly self-review for everyday logic slips - initialize every attribute you use, keep collision and interaction rules symmetric unless you intend otherwise, consume or remove a projectile once it hits so one shot cannot trigger multiple effects, check special keys before generic text input in event branches, keep every screen/state reachable and returnable with no dead branches, and guard file I/O on both load and save. Apply quietly without narrowing your creative choices. The program must run top-to-bottom and open a visible window. AUDIO: The game must include sound effects (laser fire, enemy explosions, power-up pickups, wave transitions) and background music that plays during gameplay. Sound must work in the games native audio system (pygame.mixer for Python, Web Audio API for HTML/JS, equivalent for Go/Rust/C++).

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. Before submitting, lint or compile-check your code in its respective language and fix any syntax, import, or runtime errors so it actually executes.
โฌ‡ Download game.py
import array
import json
import math
import os
import random
import sys

import pygame

SR = 22050
BS = 4
VH = 640

EV_FINGERDOWN = getattr(pygame, 'FINGERDOWN', -1)
EV_FINGERUP = getattr(pygame, 'FINGERUP', -1)
EV_FINGERMOTION = getattr(pygame, 'FINGERMOTION', -1)

ALIEN_COLORS = [(255, 90, 190), (95, 240, 230), (255, 205, 80)]
ALIEN_PATTERNS = [
    [["..XX..", ".XXXX.", "XXXXXX", "X.XX.X", "X....X"],
     ["..XX..", ".XXXX.", "XXXXXX", "X.XX.X", ".X..X."]],
    [["XXXXXX", "X.XX.X", "XXXXXX", "X....X", ".X..X."],
     ["XXXXXX", "X.XX.X", "XXXXXX", ".X..X.", "X....X"]],
    [["..XX..", "XXXXXX", "XX..XX", "XXXXXX", "..XX.."],
     ["..XX..", "XXXXXX", "XX..XX", "XXXXXX", ".X..X."]],
]
LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"


def clamp(v, lo, hi):
    return lo if v < lo else (hi if v > hi else v)


def make_font(size, bold=True):
    try:
        return pygame.font.SysFont("consolas,couriernew,dejavusansmono,monospace", size, bold=bold)
    except Exception:
        return pygame.font.Font(None, size)


def _sound_from(buf):
    a = array.array('h')
    for v in buf:
        iv = int(clamp(v, -1.0, 1.0) * 32000)
        a.append(iv)
        a.append(iv)
    return pygame.mixer.Sound(buffer=a.tobytes())


def buf_laser():
    d = 0.13
    n = int(SR * d)
    out = []
    f0, f1 = 1400.0, 320.0
    for i in range(n):
        t = i / SR
        ph = 2 * math.pi * (f0 * t + (f1 - f0) * t * t / (2 * d))
        s = 1.0 if math.sin(ph) > 0 else -1.0
        out.append(s * (1.0 - t / d) * 0.30)
    return out


def buf_boom():
    d = 0.34
    n = int(SR * d)
    out = []
    prev = 0.0
    for i in range(n):
        t = i / SR
        env = (1.0 - t / d) ** 2
        nz = random.uniform(-1.0, 1.0)
        prev = prev * 0.55 + nz * 0.45
        rum = math.sin(2 * math.pi * 70.0 * t) * (1.0 - t / d) ** 3
        out.append((prev * 0.75 + rum * 0.5) * env * 0.55)
    return out


def buf_hit():
    d = 0.8
    n = int(SR * d)
    out = []
    prev = 0.0
    for i in range(n):
        t = i / SR
        env = (1.0 - t / d) ** 1.6
        nz = random.uniform(-1.0, 1.0)
        prev = prev * 0.7 + nz * 0.3
        f = 320.0 * (1.0 - t / d) + 40.0
        tone = math.sin(2 * math.pi * f * t)
        out.append((prev * 0.5 + tone * 0.6) * env * 0.55)
    return out


def buf_power():
    out = []
    for f in (523.25, 659.25, 783.99, 1046.5):
        d = 0.09
        n = int(SR * d)
        for i in range(n):
            t = i / SR
            ph = 2 * math.pi * f * t
            s = 1.0 if math.sin(ph) > 0 else -1.0
            out.append(s * (1.0 - t / d) * 0.22)
    return out


def buf_wave():
    d = 0.75
    n = int(SR * d)
    out = []
    for i in range(n):
        t = i / SR
        f = 180.0 + 620.0 * (t / d)
        ph = 2 * math.pi * (180.0 * t + 620.0 * t * t / (2 * d))
        s = math.sin(ph) + 0.45 * math.sin(2.0 * ph)
        env = math.sin(math.pi * t / d)
        out.append(s * env * 0.24)
    return out


def buf_gameover():
    d = 1.1
    n = int(SR * d)
    out = []
    for i in range(n):
        t = i / SR
        f = 520.0 * (1.0 - t / d) + 60.0
        ph = 2 * math.pi * f * t
        s = 1.0 if math.sin(ph) > 0 else -1.0
        env = (1.0 - t / d) ** 1.2
        out.append(s * env * 0.22)
    return out


def buf_tick():
    d = 0.05
    n = int(SR * d)
    out = []
    for i in range(n):
        t = i / SR
        out.append(math.sin(2 * math.pi * 150.0 * t) * (1.0 - t / d) * 0.13)
    return out


def buf_music():
    bpm = 126.0
    beat = 60.0 / bpm
    bars = 2
    n = int(SR * beat * 4 * bars)
    buf = [0.0] * n
    roots = [110.0, 98.0]
    arp = [0, 3, 7, 12, 7, 3, 10, 7]

    def add(start, dur, freq, amp, kind):
        i0 = int(start * SR)
        i1 = min(n, int((start + dur) * SR))
        if i1 <= i0:
            return
        ln = i1 - i0
        for i in range(i0, i1):
            t = (i - i0) / SR
            env = min(1.0, t * 90.0) * ((1.0 - (i - i0) / ln) ** 1.3)
            ph = 2 * math.pi * freq * t
            if kind == 'sq':
                s = 1.0 if math.sin(ph) > 0 else -1.0
            elif kind == 'saw':
                s = 2.0 * ((freq * t) % 1.0) - 1.0
            else:
                s = math.sin(ph)
            buf[i] += s * amp * env

    for b in range(bars):
        t0 = b * beat * 4
        root = roots[b]
        for k in range(4):
            add(t0 + k * beat, beat * 0.85, root * (1.0 if k % 2 == 0 else 1.5), 0.22, 'sq')
        for k in range(8):
            add(t0 + k * beat * 0.5, beat * 0.42, root * 2.0 * (2.0 ** (arp[k] / 12.0)), 0.10, 'saw')
        for k in range(4):
            add(t0 + k * beat, 0.10, 60.0, 0.35, 'sin')
    m = max(0.001, max(abs(v) for v in buf))
    sc = 0.7 / m
    return [v * sc for v in buf]


class Game:
    def __init__(self):
        pygame.mixer.pre_init(SR, -16, 2, 512)
        pygame.init()
        if pygame.mixer.get_init() is None:
            try:
                pygame.mixer.init(SR, -16, 2, 512)
            except pygame.error:
                pass
        self.audio = pygame.mixer.get_init() is not None
        self.sounds = {}
        self.music_snd = None
        self.music_ch = None
        if self.audio:
            try:
                pygame.mixer.set_num_channels(24)
                pygame.mixer.set_reserved(1)
                self.music_ch = pygame.mixer.Channel(0)
                self._build_audio()
            except Exception:
                self.audio = False
                self.music_ch = None

        info = pygame.display.Info()
        dw = getattr(info, 'current_w', 0) or 400
        dh = getattr(info, 'current_h', 0) or 700
        self.win_w = int(clamp(dw, 340, 470))
        self.win_h = int(clamp(dh, 520, 830))
        self.screen = pygame.display.set_mode((self.win_w, self.win_h), pygame.RESIZABLE)
        pygame.display.set_caption("NEON INVADERS")
        self.VH = VH
        self.VW = int(clamp(self.VH * self.win_w / max(1.0, float(self.win_h)), 340, 620))
        self._layout()

        self.font_s = make_font(14)
        self.font_m = make_font(19)
        self.font_l = make_font(28)
        self.font_xl = make_font(38)

        self.time = 0.0
        self.state = 'MENU'
        self.state_time = 0.0
        self.go_delay = 0.0
        self.shake = 0.0
        self.particles = []
        self.pointers = {}
        self.btn_left = False
        self.btn_right = False
        self.btn_fire = False
        self.drag_target = None
        self.entry = ""
        self.high_scores = self.load_scores()
        self.stars = [[random.uniform(0, self.VW), random.uniform(0, self.VH),
                       random.uniform(6, 34), random.choice([1, 1, 1, 2])] for _ in range(64)]
        self.alien_surfs = []
        self.init_play_state()

    # ---------------- audio ----------------
    def _build_audio(self):
        self.sounds['laser'] = _sound_from(buf_laser())
        self.sounds['boom'] = _sound_from(buf_boom())
        self.sounds['hit'] = _sound_from(buf_hit())
        self.sounds['power'] = _sound_from(buf_power())
        self.sounds['wave'] = _sound_from(buf_wave())
        self.sounds['gameover'] = _sound_from(buf_gameover())
        self.sounds['tick'] = _sound_from(buf_tick())
        self.music_snd = _sound_from(buf_music())
        for k, v in (('laser', 0.35), ('boom', 0.5), ('hit', 0.6), ('power', 0.5),
                     ('wave', 0.5), ('gameover', 0.5), ('tick', 0.3)):
            self.sounds[k].set_volume(v)

    def play_sound(self, name):
        if not self.audio:
            return
        s = self.sounds.get(name)
        if s is not None:
            try:
                s.play()
            except Exception:
                pass

    def play_music(self):
        if not self.audio or self.music_snd is None or self.music_ch is None:
            return
        try:
            self.music_ch.set_volume(0.38)
            if not self.music_ch.get_busy():
                self.music_ch.play(self.music_snd, loops=-1)
        except Exception:
            pass

    def stop_music(self):
        if not self.audio or self.music_ch is None:
            return
        try:
            self.music_ch.stop()
        except Exception:
            pass

    # ---------------- layout ----------------
    def _layout(self):
        self.scale = min(self.win_w / float(self.VW), self.win_h / float(self.VH))
        self.ox = (self.win_w - self.VW * self.scale) / 2.0
        self.oy = (self.win_h - self.VH * self.scale) / 2.0
        self.surf = pygame.Surface((self.VW, self.VH))

    def to_virtual(self, px, py):
        return ((px - self.ox) / self.scale, (py - self.oy) / self.scale)

    # ---------------- scores ----------------
    def load_scores(self):
        data = []
        try:
            if os.path.exists('high_scores.json'):
                with open('high_scores.json', 'r') as f:
                    raw = json.load(f)
                if isinstance(raw, list):
                    for e in raw:
                        if isinstance(e, dict) and 'score' in e:
                            data.append({'name': str(e.get('name', 'AAA'))[:3], 'score': int(e['score'])})
        except Exception:
            data = []
        data.sort(key=lambda z: -z['score'])
        return data[:5]

    def save_score(self, initials, score):
        name = (initials or 'AAA').upper()[:3]
        entry = {'name': name, 'score': int(score)}
        self.high_scores.append(entry)
        self.high_scores.sort(key=lambda z: -z['score'])
        self.high_scores = self.high_scores[:5]
        try:
            with open('high_scores.json', 'w') as f:
                json.dump(self.high_scores, f)
        except Exception:
            pass
        try:
            with open('/tmp/alpaca_score.json', 'w') as f:
                json.dump({'initials': entry['name'], 'score': entry['score']}, f)
        except Exception:
            pass

    # ---------------- state ----------------
    def init_play_state(self):
        self.score = 0
        self.lives = 3
        self.wave = 0
        self.player = pygame.Rect(0, 0, 32, 18)
        self.px = self.VW / 2.0
        self.player.y = self.VH - 138
        self.controls_top = self.VH - 104
        self.pbullets = []
        self.ebullets = []
        self.aliens = []
        self.aliens_alive = 0
        self.aliens_total = 1
        self.march_dir = 1
        self.march_timer = 0.0
        self.step_x = 8.0
        self.drop_y = 14.0
        self.anim_frame = 0
        self.bob_amp = 0.0
        self.bunkers = []
        self.ufo = None
        self.ufo_timer = 12.0
        self.powerups = []
        self.power = {'rapid': 0.0, 'triple': 0.0, 'shield': 0.0}
        self.invuln = 0.0
        self.fire_cd = 0.0
        self.enemy_fire_timer = 1.2
        self.intro_timer = 0.0
        self.max_ebullets = 4
        self.build_alien_surfs(22, 18)
        self.make_bunkers()

    def build_alien_surfs(self, aw, ah):
        aw = max(8, int(aw))
        ah = max(8, int(ah))
        self.alien_surfs = []
        for t in range(3):
            frames = []
            for f in range(2):
                s = pygame.Surface((aw, ah), pygame.SRCALPHA)
                pat = ALIEN_PATTERNS[t][f]
                cw = aw / 6.0
                ch = ah / 5.0
                col = ALIEN_COLORS[t]
                for ry in range(5):
                    for cx in range(6):
                        if pat[ry][cx] == 'X':
                            pygame.draw.rect(s, col, (int(cx * cw), int(ry * ch),
                                                      max(1, int(cw) + 1), max(1, int(ch) + 1)))
                frames.append(s)
            self.alien_surfs.append(frames)

    def make_bunkers(self):
        self.bunkers = []
        bw, bh = 12, 7
        for i in range(3):
            cx = self.VW * (0.2 + 0.3 * i)
            x = cx - bw * BS / 2.0
            y = self.player.y - 62
            cells = [[1] * bw for _ in range(bh)]
            for r in range(bh):
                for c in range(bw):
                    if r == 0 and (c < 2 or c > bw - 3):
                        cells[r][c] = 0
                    if r >= bh - 2 and 4 <= c <= bw - 5:
                        cells[r][c] = 0
            self.bunkers.append({'x': x, 'y': y, 'cells': cells, 'bw': bw, 'bh': bh, 'surf': None})

    def start_game(self):
        self.score = 0
        self.lives = 3
        self.power = {'rapid': 0.0, 'triple': 0.0, 'shield': 0.0}
        self.particles = []
        self.invuln = 0.0
        self.entry = ""
        self.px = self.VW / 2.0
        self.spawn_wave(1)
        self.state = 'PLAYING'
        self.state_time = 0.0
        self.play_music()

    def spawn_wave(self, wave):
        self.wave = wave
        cols = 11
        rows = min(3 + wave, 7)
        pattern = (wave - 1) % 3
        tight = max(0.70, 1.0 - 0.035 * (wave - 1))
        base_cell = (self.VW - 44) / float(cols)
        cell_w = base_cell * tight
        aw = max(9, int(cell_w * 0.80))
        ah = max(9, int(aw * 0.85))
        cell_h = ah + max(5.0, 10.0 - wave * 0.5)
        gw = cols * cell_w
        gx = (self.VW - gw) / 2.0
        gy = 92.0
        self.build_alien_surfs(aw, ah)
        self.aliens = []
        for r in range(rows):
            for c in range(cols):
                x = gx + c * cell_w + (cell_w - aw) / 2.0
                if pattern == 1 and r % 2 == 1:
                    x += cell_w * 0.30
                y = gy + r * cell_h
                atype = 0 if r == 0 else (1 if r < 3 else 2)
                self.aliens.append({'x': x, 'y': y, 'w': aw, 'h': ah, 'type': atype,
                                    'col': c, 'row': r, 'alive': True,
                                    'phase': c * 0.55 + r * 0.4})
        self.aliens_total = max(1, len(self.aliens))
        self.aliens_alive = len(self.aliens)
        self.march_dir = 1
        self.march_timer = 0.55
        self.step_x = max(5.0, 9.0 - wave * 0.25)
        self.drop_y = 10.0 + wave * 1.5
        self.bob_amp = 7.0 if pattern == 2 else 0.0
        self.anim_frame = 0
        self.pbullets = []
        self.ebullets = []
        self.powerups = []
        self.ufo = None
        self.ufo_timer = random.uniform(10.0, 18.0)
        self.enemy_fire_timer = 1.4
        self.max_ebullets = min(8, 3 + wave)
        self.make_bunkers()
        self.intro_timer = 1.4

    def next_wave(self):
        self.score += 100 * self.wave
        self.play_sound('wave')
        self.spawn_wave(self.wave + 1)

    def game_over(self):
        self.state = 'GAMEOVER'
        self.state_time = 0.0
        self.go_delay = 0.9
        self.entry = ""
        self.pointers.clear()
        self.stop_music()
        self.play_sound('gameover')

    # ---------------- events ----------------
    def handle_events(self):
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                self.quit_game()
            elif e.type == pygame.VIDEORESIZE:
                self.win_w = max(260, e.w)
                self.win_h = max(340, e.h)
                self.screen = pygame.display.set_mode((self.win_w, self.win_h), pygame.RESIZABLE)
                self._layout()
            elif e.type == pygame.KEYDOWN:
                self.on_keydown(e)
            elif e.type == pygame.TEXTINPUT:
                if self.state == 'GAMEOVER':
                    for ch in e.text:
                        if ch.isalnum():
                            self.add_initial(ch)
            elif e.type == pygame.MOUSEBUTTONDOWN:
                vx, vy = self.to_virtual(e.pos[0], e.pos[1])
                self.on_pointer_down('mouse', vx, vy)
            elif e.type == pygame.MOUSEBUTTONUP:
                self.pointers.pop('mouse', None)
                self.update_pointer_state()
            elif e.type == pygame.MOUSEMOTION:
                if 'mouse' in self.pointers:
                    vx, vy = self.to_virtual(e.pos[0], e.pos[1])
                    p = self.pointers['mouse']
                    p['x'] = vx
                    p['y'] = vy
                    self.update_pointer_state()
            elif e.type == EV_FINGERDOWN:
                vx = e.x * self.win_w
                vy = e.y * self.win_h
                self.on_pointer_down(e.finger_id, vx, vy)
            elif e.type == EV_FINGERUP:
                self.pointers.pop(e.finger_id, None)
                self.update_pointer_state()
            elif e.type == EV_FINGERMOTION:
                if e.finger_id in self.pointers:
                    p = self.pointers[e.finger_id]
                    p['x'] = e.x * self.win_w
                    p['y'] = e.y * self.win_h
                    self.update_pointer_state()

    def on_keydown(self, e):
        if e.key == pygame.K_ESCAPE:
            if self.state == 'PLAYING':
                self.stop_music()
                self.state = 'MENU'
                self.state_time = 0.0
            elif self.state == 'GAMEOVER':
                self.state = 'MENU'
                self.state_time = 0.0
            return
        if self.state == 'MENU':
            if e.key in (pygame.K_SPACE, pygame.K_RETURN, pygame.K_KP_ENTER):
                self.start_game()
            return
        if self.state == 'GAMEOVER':
            if e.key == pygame.K_RETURN or e.key == pygame.K_KP_ENTER:
                self.submit_score()
            elif e.key == pygame.K_BACKSPACE:
                self.entry = self.entry[:-1]
            elif pygame.K_a <= e.key <= pygame.K_z:
                self.add_initial(chr(e.key))
            return

    def on_pointer_down(self, pid, vx, vy):
        if self.state == 'PLAYING':
            kind = self.button_at(vx, vy)
            if kind is None and vy < self.controls_top and vy > 0:
                kind = 'drag'
            if kind is None:
                return
            self.pointers[pid] = {'kind': kind, 'x': vx, 'y': vy}
            self.update_pointer_state()
        elif self.state == 'MENU':
            if self.state_time > 0.35:
                self.start_game()
        elif self.state == 'GAMEOVER':
            self.handle_gameover_click(vx, vy)

    def button_at(self, vx, vy):
        if vy < self.controls_top:
            return None
        y = self.VH - 52
        for cx, r, name in ((self.VW * 0.16, 34, 'left'),
                            (self.VW * 0.42, 34, 'right'),
                            (self.VW * 0.80, 40, 'fire')):
            if (vx - cx) ** 2 + (vy - y) ** 2 <= r * r:
                return name
        return None

    def update_pointer_state(self):
        self.btn_left = any(p['kind'] == 'left' for p in self.pointers.values())
        self.btn_right = any(p['kind'] == 'right' for p in self.pointers.values())
        self.btn_fire = any(p['kind'] in ('fire', 'drag') for p in self.pointers.values())
        xs = [p['x'] for p in self.pointers.values() if p['kind'] == 'drag']
        self.drag_target = (sum(xs) / len(xs)) if xs else None

    def grid_geom(self):
        return 14.0, 268.0, (self.VW - 28) / 7.0, 32.0

    def handle_gameover_click(self, vx, vy):
        if self.go_delay > 0:
            return
        gx, gy, cw, ch = self.grid_geom()
        if gx <= vx < gx + cw * 7 and gy <= vy < gy + ch * 4:
            c = int((vx - gx) // cw)
            r = int((vy - gy) // ch)
            idx = r * 7 + c
            if idx < 26:
                self.add_initial(LETTERS[idx])
            elif idx == 26:
                self.entry = self.entry[:-1]
            else:
                self.submit_score()

    def add_initial(self, ch):
        ch = ch.upper()
        if ch.isalpha() and len(self.entry) < 3:
            self.entry += ch

    def submit_score(self):
        self.save_score(self.entry or 'AAA', self.score)
        self.entry = ""
        self.state = 'MENU'
        self.state_time = 0.0
        self.pointers.clear()
        self.update_pointer_state()

    def quit_game(self):
        self.stop_music()
        pygame.quit()
        sys.exit(0)

    # ---------------- update ----------------
    def update(self, dt):
        self.time += dt
        self.state_time += dt
        if self.go_delay > 0:
            self.go_delay = max(0.0, self.go_delay - dt)
        if self.shake > 0:
            self.shake = max(0.0, self.shake - dt * 2.5)
        self.update_stars(dt)
        self.update_particles(dt)
        if self.state == 'PLAYING':
            if self.intro_timer > 0:
                self.intro_timer -= dt
            else:
                self.update_play(dt)

    def update_stars(self, dt):
        for s in self.stars:
            s[1] += s[2] * dt
            if s[1] > self.VH + 2:
                s[1] = -2
                s[0] = random.uniform(0, self.VW)

    def spawn_particles(self, x, y, count, color, speed):
        if len(self.particles) > 420:
            return
        for _ in range(count):
            a = random.uniform(0, math.pi * 2)
            sp = random.uniform(0.25, 1.0) * speed
            self.particles.append({'x': x, 'y': y,
                                   'vx': math.cos(a) * sp, 'vy': math.sin(a) * sp,
                                   'life': random.uniform(0.3, 0.75),
                                   'max': 0.75, 'color': color,
                                   'size': random.choice([2, 2, 3])})

    def update_particles(self, dt):
        for p in self.particles[:]:
            p['x'] += p['vx'] * dt
            p['y'] += p['vy'] * dt
            p['vy'] += 250 * dt
            p['life'] -= dt
            if p['life'] <= 0:
                self.particles.remove(p)

    def alien_rect(self, a):
        y = a['y']
        if self.bob_amp:
            y += math.sin(self.time * 2.8 + a['phase']) * self.bob_amp
        return pygame.Rect(int(a['x']), int(y), a['w'], a['h'])

    def update_play(self, dt):
        for k in self.power:
            if self.power[k] > 0:
                self.power[k] = max(0.0, self.power[k] - dt)
        if self.invuln > 0:
            self.invuln = max(0.0, self.invuln - dt)
        if self.fire_cd > 0:
            self.fire_cd -= dt

        keys = pygame.key.get_pressed()
        d = 0
        if keys[pygame.K_LEFT] or keys[pygame.K_a] or self.btn_left:
            d -= 1
        if keys[pygame.K_RIGHT] or keys[pygame.K_d] or self.btn_right:
            d += 1
        sp = 270.0
        if d:
            self.px += d * sp * dt
        if self.drag_target is not None:
            diff = self.drag_target - self.px
            if abs(diff) > 3:
                self.px += clamp(diff, -sp * dt, sp * dt)
        self.px = clamp(self.px, 16.0, self.VW - 16.0)
        self.player.x = int(self.px - self.player.w / 2.0)

        if (keys[pygame.K_SPACE] or self.btn_fire) and self.fire_cd <= 0:
            self.fire()

        for b in self.pbullets[:]:
            b['x'] += b['vx'] * dt
            b['y'] += b['vy'] * dt
            r = pygame.Rect(int(b['x']), int(b['y']), b['w'], b['h'])
            if r.bottom < 0 or r.right < -20 or r.left > self.VW + 20:
                self.pbullets.remove(b)
                continue
            if self.hit_bunker(r, 1):
                self.spawn_particles(r.centerx, r.centery, 4, (110, 210, 140), 70)
                self.pbullets.remove(b)
                continue
            hit = False
            for a in self.aliens:
                if a['alive'] and r.colliderect(self.alien_rect(a)):
                    self.kill_alien(a)
                    hit = True
                    break
            if hit:
                self.pbullets.remove(b)
                continue
            if self.ufo is not None and r.colliderect(self.ufo['rect']):
                self.hit_ufo()
                self.pbullets.remove(b)

        for b in self.ebullets[:]:
            b['x'] += b['vx'] * dt
            b['y'] += b['vy'] * dt
            r = pygame.Rect(int(b['x']), int(b['y']), b['w'], b['h'])
            if r.top > self.VH:
                self.ebullets.remove(b)
                continue
            if self.hit_bunker(r, 1):
                self.spawn_particles(r.centerx, r.centery, 4, (200, 110, 160), 70)
                self.ebullets.remove(b)
                continue
            if r.colliderect(self.player) and self.invuln <= 0:
                self.ebullets.remove(b)
                self.player_hit()

        if self.aliens_alive <= 0:
            self.next_wave()
            return

        self.march_timer -= dt
        if self.march_timer <= 0:
            ratio = self.aliens_alive / float(self.aliens_total)
            speed = (1.0 + 0.16 * (self.wave - 1)) * (1.0 + 3.4 * (1.0 - ratio))
            self.march_timer = max(0.045, 0.60 / speed)
            self.march_step()

        self.enemy_fire_timer -= dt
        if self.enemy_fire_timer <= 0 and len(self.ebullets) < self.max_ebullets:
            self.enemy_fire()
            self.enemy_fire_timer = random.uniform(0.45, 1.25) / (1.0 + 0.1 * (self.wave - 1))

        if self.ufo is None:
            self.ufo_timer -= dt
            if self.ufo_timer <= 0:
                dirn = random.choice([-1, 1])
                w, h = 46, 18
                x = -w if dirn > 0 else self.VW
                self.ufo = {'rect': pygame.Rect(int(x), 74, w, h), 'vx': dirn * 95.0}
        else:
            self.ufo['rect'].x += self.ufo['vx'] * dt
            if self.ufo['rect'].right < -20 or self.ufo['rect'].left > self.VW + 20:
                self.ufo = None
                self.ufo_timer = random.uniform(14.0, 26.0)

        for p in self.powerups[:]:
            p['y'] += p['vy'] * dt
            p['t'] += dt
            r = pygame.Rect(int(p['x'] - 11), int(p['y'] - 11), 22, 22)
            if p['y'] > self.VH + 20:
                self.powerups.remove(p)
                continue
            if r.colliderect(self.player):
                self.apply_power(p['kind'])
                self.powerups.remove(p)

        self.aliens_vs_bunkers()

    def fire(self):
        self.fire_cd = 0.13 if self.power['rapid'] > 0 else 0.30
        cx = self.player.centerx
        top = self.player.top
        if self.power['triple'] > 0:
            self.pbullets.append({'x': cx - 8.0, 'y': top - 8.0, 'w': 4, 'h': 10,
                                  'vx': -150.0, 'vy': -560.0})
            self.pbullets.append({'x': cx - 2.0, 'y': top - 12.0, 'w': 4, 'h': 12,
                                  'vx': 0.0, 'vy': -640.0})
            self.pbullets.append({'x': cx + 4.0, 'y': top - 8.0, 'w': 4, 'h': 10,
                                  'vx': 150.0, 'vy': -560.0})
        else:
            self.pbullets.append({'x': cx - 2.0, 'y': top - 12.0, 'w': 4, 'h': 12,
                                  'vx': 0.0, 'vy': -640.0})
        self.play_sound('laser')

    def march_step(self):
        alive = [a for a in self.aliens if a['alive']]
        if not alive:
            return
        dx = self.march_dir * self.step_x
        minx = min(a['x'] for a in alive) + dx
        maxx = max(a['x'] + a['w'] for a in alive) + dx
        if minx < 10 or maxx > self.VW - 10:
            self.march_dir *= -1
            for a in alive:
                a['y'] += self.drop_y
        else:
            for a in alive:
                a['x'] += dx
        self.anim_frame ^= 1
        self.play_sound('tick')
        for a in alive:
            if self.alien_rect(a).bottom > self.player.y - 4:
                self.game_over()
                return

    def enemy_fire(self):
        alive = [a for a in self.aliens if a['alive']]
        if not alive:
            return
        cols = {}
        for a in alive:
            if a['col'] not in cols or a['y'] > cols[a['col']]['y']:
                cols[a['col']] = a
        a = random.choice(list(cols.values()))
        r = self.alien_rect(a)
        self.ebullets.append({'x': r.centerx - 2.0, 'y': float(r.bottom), 'w': 4, 'h': 10,
                              'vx': 0.0, 'vy': 190.0 + self.wave * 14.0})

    def kill_alien(self, a):
        a['alive'] = False
        self.aliens_alive -= 1
        self.score += (30, 20, 10)[a['type']] + self.wave * 2
        col = ALIEN_COLORS[a['type']]
        self.spawn_particles(a['x'] + a['w'] / 2.0, a['y'] + a['h'] / 2.0, 14, col, 160)
        self.play_sound('boom')
        if random.random() < 0.07:
            self.spawn_powerup(a['x'] + a['w'] / 2.0, a['y'] + a['h'] / 2.0)

    def hit_ufo(self):
        pts = random.choice([50, 100, 150, 200, 300])
        self.score += pts
        r = self.ufo['rect']
        self.spawn_particles(r.centerx, r.centery, 26, (255, 240, 140), 200)
        self.play_sound('boom')
        self.ufo = None
        self.ufo_timer = random.uniform(14.0, 26.0)

    def spawn_powerup(self, x, y):
        kinds = ['rapid', 'triple', 'shield', 'life']
        k = random.choices(kinds, weights=[3, 3, 3, 1])[0]
        self.powerups.append({'x': x, 'y': y, 'kind': k, 'vy': 70.0, 't': 0.0})

    def apply_power(self, kind):
        if kind == 'life':
            self.lives = min(6, self.lives + 1)
        else:
            self.power[kind] = 10.0
        self.score += 50
        self.play_sound('power')
        self.spawn_particles(self.player.centerx, self.player.centery - 10, 16, (130, 255, 190), 160)

    def hit_bunker(self, rect, radius):
        for b in self.bunkers:
            full = pygame.Rect(int(b['x']), int(b['y']), b['bw'] * BS, b['bh'] * BS)
            if not rect.colliderect(full):
                continue
            hit = False
            for r in range(b['bh']):
                for c in range(b['bw']):
                    if not b['cells'][r][c]:
                        continue
                    cr = pygame.Rect(int(b['x']) + c * BS, int(b['y']) + r * BS, BS, BS)
                    if cr.colliderect(rect):
                        for dr in range(-radius, radius + 1):
                            for dc in range(-radius, radius + 1):
                                rr, cc = r + dr, c + dc
                                if 0 <= rr < b['bh'] and 0 <= cc < b['bw'] and random.random() < 0.75:
                                    b['cells'][rr][cc] = 0
                        hit = True
            if hit:
                b['surf'] = None
                return True
        return False

    def aliens_vs_bunkers(self):
        limit = self.player.y - 90
        for a in self.aliens:
            if not a['alive']:
                continue
            ar = self.alien_rect(a)
            if ar.bottom < limit:
                continue
            self.hit_bunker(ar, 2)

    def player_hit(self):
        if self.invuln > 0:
            return
        if self.power['shield'] > 0:
            self.power['shield'] = 0.0
            self.spawn_particles(self.player.centerx, self.player.centery, 18, (90, 220, 255), 190)
            self.play_sound('power')
            self.ebullets = []
            self.invuln = 1.2
            self.shake = 0.5
            return
        self.lives -= 1
        self.shake = 1.0
        self.spawn_particles(self.player.centerx, self.player.centery, 30, (255, 140, 60), 220)
        self.play_sound('hit')
        self.ebullets = []
        self.power = {'rapid': 0.0, 'triple': 0.0, 'shield': 0.0}
        if self.lives <= 0:
            self.game_over()
        else:
            self.invuln = 2.2
            self.px = self.VW / 2.0

    # ---------------- drawing ----------------
    def draw(self):
        self.surf.fill((6, 6, 20))
        self.draw_stars()
        if self.state == 'MENU':
            self.draw_menu()
        elif self.state == 'PLAYING':
            self.draw_play()
        else:
            self.draw_gameover()

        sw = max(1, int(self.VW * self.scale))
        sh = max(1, int(self.VH * self.scale))
        sx = self.ox + random.uniform(-4, 4) * self.shake * self.scale
        sy = self.oy + random.uniform(-4, 4) * self.shake * self.scale
        self.screen.fill((2, 2, 8))
        scaled = pygame.transform.scale(self.surf, (sw, sh))
        self.screen.blit(scaled, (int(sx), int(sy)))
        pygame.display.flip()

    def draw_stars(self):
        for s in self.stars:
            b = 90 if s[3] == 1 else 160
            col = (b, b, min(255, b + 60))
            pygame.draw.rect(self.surf, col, (int(s[0]), int(s[1]), s[3], s[3]))

    def draw_play(self):
        for b in self.bunkers:
            if b['surf'] is None:
                s = pygame.Surface((b['bw'] * BS, b['bh'] * BS), pygame.SRCALPHA)
                for r in range(b['bh']):
                    for c in range(b['bw']):
                        if b['cells'][r][c]:
                            pygame.draw.rect(s, (70, 220, 130), (c * BS, r * BS, BS, BS))
                b['surf'] = s
            self.surf.blit(b['surf'], (int(b['x']), int(b['y'])))

        for a in self.aliens:
            if not a['alive']:
                continue
            r = self.alien_rect(a)
            self.surf.blit(self.alien_surfs[a['type']][self.anim_frame & 1], (r.x, r.y))

        if self.ufo is not None:
            r = self.ufo['rect']
            pygame.draw.ellipse(self.surf, (200, 90, 255), r)
            pygame.draw.ellipse(self.surf, (255, 210, 255), (r.x + 13, r.y - 7, 20, 14))
            pygame.draw.ellipse(self.surf, (255, 255, 255), (r.x + 18, r.y - 3, 10, 7))

        for p in self.powerups:
            col = {'rapid': (255, 220, 90), 'triple': (255, 120, 220),
                   'shield': (90, 200, 255), 'life': (120, 255, 150)}[p['kind']]
            sz = 11 + math.sin(p['t'] * 6) * 2
            pts = [(int(p['x']), int(p['y'] - sz)), (int(p['x'] + sz), int(p['y'])),
                   (int(p['x']), int(p['y'] + sz)), (int(p['x'] - sz), int(p['y']))]
            pygame.draw.polygon(self.surf, col, pts)
            pygame.draw.polygon(self.surf, (255, 255, 255), pts, 1)

        for b in self.pbullets:
            pygame.draw.rect(self.surf, (140, 255, 255), (int(b['x']), int(b['y']), b['w'], b['h']))
        for b in self.ebullets:
            pygame.draw.rect(self.surf, (255, 120, 200), (int(b['x']), int(b['y']), b['w'], b['h']))

        self.draw_player()

        for p in self.particles:
            f = clamp(p['life'] / p['max'], 0.0, 1.0)
            col = (int(p['color'][0] * f), int(p['color'][1] * f), int(p['color'][2] * f))
            pygame.draw.rect(self.surf, col, (int(p['x']), int(p['y']), p['size'], p['size']))

        self.draw_hud()
        self.draw_controls()

        if self.intro_timer > 0:
            t = self.font_xl.render("WAVE %d" % self.wave, True, (255, 240, 120))
            self.surf.blit(t, (int(self.VW / 2 - t.get_width() / 2), int(self.VH / 2 - 60)))
            if self.wave > 1:
                s = self.font_s.render("SPEED UP", True, (255, 140, 200))
                self.surf.blit(s, (int(self.VW / 2 - s.get_width() / 2), int(self.VH / 2 - 16)))

    def draw_player(self):
        if self.invuln > 0 and int(self.time * 14) % 2 == 0:
            return
        p = self.player
        cx = p.centerx
        pts = [(cx, p.y), (cx + 7, p.y + 7), (cx + 15, p.y + 10), (cx + 15, p.y + 17),
               (cx - 15, p.y + 17), (cx - 15, p.y + 10), (cx - 7, p.y + 7)]
        pygame.draw.polygon(self.surf, (90, 240, 255), pts)
        pygame.draw.polygon(self.surf, (235, 255, 255), pts, 1)
        pygame.draw.rect(self.surf, (220, 255, 255), (cx - 2, p.y - 5, 4, 6))
        if self.power['shield'] > 0:
            pygame.draw.circle(self.surf, (90, 190, 255), (cx, p.y + 8), 27, 2)

    def draw_hud(self):
        txt = self.font_m.render("SCORE %06d" % self.score, True, (255, 255, 255))
        self.surf.blit(txt, (10, 8))
        hs = self.high_scores[0]['score'] if self.high_scores else 0
        h = self.font_s.render("HI %d" % hs, True, (150, 155, 210))
        self.surf.blit(h, (10, 32))
        wt = self.font_m.render("WAVE %d" % max(1, self.wave), True, (255, 210, 90))
        self.surf.blit(wt, (int(self.VW / 2 - wt.get_width() / 2), 8))
        for i in range(self.lives):
            x = self.VW - 20 - i * 20
            pygame.draw.polygon(self.surf, (90, 240, 255), [(x, 14), (x + 8, 28), (x - 8, 28)])
        pw = []
        if self.power['rapid'] > 0:
            pw.append(('R', (255, 220, 90)))
        if self.power['triple'] > 0:
            pw.append(('T', (255, 120, 220)))
        if self.power['shield'] > 0:
            pw.append(('S', (90, 200, 255)))
        for i, (ch, col) in enumerate(pw):
            t = self.font_s.render(ch, True, col)
            self.surf.blit(t, (int(self.VW / 2 - 30) + i * 20, 32))

    def draw_button(self, cx, cy, r, label, active):
        fill = (46, 62, 108) if active else (26, 32, 58)
        edge = (150, 210, 255) if active else (90, 120, 180)
        pygame.draw.circle(self.surf, fill, (int(cx), int(cy)), int(r))
        pygame.draw.circle(self.surf, edge, (int(cx), int(cy)), int(r), 2)
        if label == 'FIRE':
            t = self.font_s.render("FIRE", True, edge)
            self.surf.blit(t, (int(cx - t.get_width() / 2), int(cy - t.get_height() / 2)))
        elif label == 'L':
            pygame.draw.polygon(self.surf, edge, [(cx + 8, cy - 12), (cx + 8, cy + 12), (cx - 10, cy)])
        else:
            pygame.draw.polygon(self.surf, edge, [(cx - 8, cy - 12), (cx - 8, cy + 12), (cx + 10, cy)])

    def draw_controls(self):
        y = self.VH - 52
        self.draw_button(self.VW * 0.16, y, 34, 'L', self.btn_left)
        self.draw_button(self.VW * 0.42, y, 34, 'R', self.btn_right)
        self.draw_button(self.VW * 0.80, y, 40, 'FIRE', self.btn_fire)

    def draw_scores(self, y, title=True):
        cx = self.VW / 2
        if title:
            t = self.font_m.render("HIGH SCORES", True, (255, 210, 90))
            self.surf.blit(t, (int(cx - t.get_width() / 2), y))
            y += 26
        if not self.high_scores:
            t = self.font_s.render("NO SCORES YET", True, (120, 130, 175))
            self.surf.blit(t, (int(cx - t.get_width() / 2), y))
            return
        for i, e in enumerate(self.high_scores[:5]):
            txt = "%d. %-3s %6d" % (i + 1, str(e.get('name', 'AAA'))[:3], int(e.get('score', 0)))
            col = (255, 255, 255) if i == 0 else (170, 190, 225)
            t = self.font_m.render(txt, True, col)
            self.surf.blit(t, (int(cx - t.get_width() / 2), y + i * 22))

    def draw_menu(self):
        cx = self.VW / 2
        t = self.font_xl.render("NEON", True, (255, 80, 190))
        self.surf.blit(t, (int(cx - t.get_width() / 2), 58))
        t2 = self.font_xl.render("INVADERS", True, (80, 240, 255))
        self.surf.blit(t2, (int(cx - t2.get_width() / 2), 100))

        if self.alien_surfs:
            aw = self.alien_surfs[0][0].get_width()
            total = 6 * aw + 5 * 14
            start = cx - total / 2.0
            for i in range(6):
                f = (int(self.time * 3) + i) % 2
                s = self.alien_surfs[i % 3][f]
                self.surf.blit(s, (int(start + i * (aw + 14)), 172))

        self.draw_scores(240)
        if int(self.time * 2) % 2 == 0:
            p = self.font_m.render("PRESS SPACE / TAP TO START", True, (255, 255, 255))
            self.surf.blit(p, (int(cx - p.get_width() / 2), 452))
        hints = ["ARROWS / A-D  MOVE", "SPACE  FIRE", "TOUCH PADS ON MOBILE", "ESC  BACK TO MENU"]
        for i, h in enumerate(hints):
            t = self.font_s.render(h, True, (130, 145, 190))
            self.surf.blit(t, (int(cx - t.get_width() / 2), 500 + i * 20))

    def draw_gameover(self):
        cx = self.VW / 2
        t = self.font_l.render("GAME OVER", True, (255, 90, 120))
        self.surf.blit(t, (int(cx - t.get_width() / 2), 58))
        s = self.font_m.render("SCORE %06d" % self.score, True, (255, 255, 255))
        self.surf.blit(s, (int(cx - s.get_width() / 2), 108))
        w = self.font_m.render("REACHED WAVE %d" % max(1, self.wave), True, (255, 210, 90))
        self.surf.blit(w, (int(cx - w.get_width() / 2), 134))

        e = self.font_m.render("ENTER INITIALS", True, (150, 230, 255))
        self.surf.blit(e, (int(cx - e.get_width() / 2), 176))

        bw, bh = 36, 38
        total = 3 * bw + 2 * 8
        bx = cx - total / 2.0
        for i in range(3):
            r = pygame.Rect(int(bx + i * (bw + 8)), 206, bw, bh)
            pygame.draw.rect(self.surf, (24, 30, 56), r)
            pygame.draw.rect(self.surf, (90, 200, 255), r, 2)
            ch = self.entry[i] if i < len(self.entry) else "_"
            ct = self.font_l.render(ch, True, (255, 255, 255))
            self.surf.blit(ct, (int(r.centerx - ct.get_width() / 2), int(r.centery - ct.get_height() / 2)))

        gx, gy, cw, ch = self.grid_geom()
        for idx in range(28):
            r = idx // 7
            c = idx % 7
            rx = int(gx + c * cw)
            ry = int(gy + r * ch)
            rect = pygame.Rect(rx + 2, ry + 2, int(cw) - 4, int(ch) - 4)
            if idx < 26:
                label = LETTERS[idx]
                col = (36, 46, 82)
                edge = (110, 150, 210)
            elif idx == 26:
                label = "DEL"
                col = (70, 34, 44)
                edge = (220, 110, 140)
            else:
                label = "OK"
                col = (26, 66, 50)
                edge = (110, 240, 160)
            pygame.draw.rect(self.surf, col, rect)
            pygame.draw.rect(self.surf, edge, rect, 1)
            f = self.font_m if idx < 26 else self.font_s
            lt = f.render(label, True, (235, 240, 255))
            self.surf.blit(lt, (int(rect.centerx - lt.get_width() / 2),
                                int(rect.centery - lt.get_height() / 2)))

        self.draw_scores(420)
        hint = self.font_s.render("TYPE OR TAP A-Z, THEN OK", True, (150, 165, 205))
        self.surf.blit(hint, (int(cx - hint.get_width() / 2), 566))

    # ---------------- main loop ----------------
    def run(self):
        clock = pygame.time.Clock()
        while True:
            dt = min(clock.tick(60) / 1000.0, 0.05)
            self.handle_events()
            self.update(dt)
            self.draw()


def main():
    game = Game()
    game.run()


if __name__ == '__main__':
    main()

๐ŸŽฎ 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