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()