โ
Retro Space Invaders โ
๐ฆ Game code, download & prompt info
๐ฅ๏ธ Desktop app (python) โ play it live above, right in your browser, or download it below
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.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