import json import os import random from pathlib import Path import pygame CELL = 24 COLS, ROWS = 26, 22 HEADER = 76 WIDTH, HEIGHT = COLS * CELL, HEADER + ROWS * CELL STEP_TIME = 0.105 SCORE_FILE = Path.cwd() / "high_scores.json" LATEST_FILE = Path("/tmp/alpaca_score.json") BG = (168, 190, 118) GRID = (151, 175, 104) INK = (34, 53, 30) BODY = (55, 79, 38) PANEL = (190, 208, 150) MUTED = (82, 106, 57) DIRECTIONS = { pygame.K_UP: (0, -1), pygame.K_DOWN: (0, 1), pygame.K_LEFT: (-1, 0), pygame.K_RIGHT: (1, 0), } def load_scores(): try: with SCORE_FILE.open("r", encoding="utf-8") as handle: data = json.load(handle) if not isinstance(data, list): return [] entries = [] for entry in data: if not isinstance(entry, dict): continue name = entry.get("initials") score = entry.get("score") if isinstance(name, str) and type(score) is int and score >= 0: entries.append({ "initials": name.strip()[:12] or "ANON", "score": score, }) return sorted(entries, key=lambda item: item["score"], reverse=True)[:5] except (OSError, ValueError): return [] def write_json(path, data): temporary = path.with_name(path.name + ".tmp") with temporary.open("w", encoding="utf-8") as handle: json.dump(data, handle, indent=2) handle.write("\n") os.replace(temporary, path) class SnakeGame: def __init__(self): pygame.init() pygame.display.set_caption("Snake • Pocket Arcade") self.screen = pygame.display.set_mode((WIDTH, HEIGHT)) self.clock = pygame.time.Clock() self.font = pygame.font.Font(None, 28) self.small = pygame.font.Font(None, 22) self.large = pygame.font.Font(None, 54) self.high_scores = load_scores() self.state = "menu" self.running = True self.snake = [(12, 11), (11, 11), (10, 11)] self.food = (18, 11) self.score = 0 self.initials = "" self.saved = False self.save_message = "" self.result = "" def text(self, message, position, font=None, color=INK, center=False): image = (font or self.font).render(message, True, color) rect = image.get_rect() if center: rect.center = position else: rect.topleft = position self.screen.blit(image, rect) def new_game(self): self.snake = [(COLS // 2, ROWS // 2), (COLS // 2 - 1, ROWS // 2), (COLS // 2 - 2, ROWS // 2)] self.direction = (1, 0) self.turns = [] self.elapsed = 0.0 self.score = 0 self.initials = "" self.saved = False self.save_message = "" self.result = "" self.state = "playing" self.place_food() def place_food(self): occupied = set(self.snake) empty = [(x, y) for y in range(ROWS) for x in range(COLS) if (x, y) not in occupied] self.food = random.choice(empty) if empty else None def finish(self, result): self.result = result self.state = "entry" self.elapsed = 0.0 self.turns.clear() self.initials = "" def record_run(self): if self.saved: return entry = { "initials": self.initials.strip()[:12] or "ANON", "score": self.score, } self.high_scores.append(entry) self.high_scores.sort(key=lambda item: item["score"], reverse=True) self.high_scores = self.high_scores[:5] errors = [] for path, data in ( (SCORE_FILE, self.high_scores), (LATEST_FILE, entry), ): try: write_json(path, data) except OSError as error: errors.append(path.name) print(f"Could not save {path}: {error}") self.save_message = ( "Save failed: " + ", ".join(errors) if errors else "Score saved." ) self.saved = True self.state = "gameover" def queue_turn(self, direction): previous = self.turns[-1] if self.turns else self.direction if (len(self.turns) < 2 and direction != previous and direction != (-previous[0], -previous[1])): self.turns.append(direction) def step(self): if self.turns: self.direction = self.turns.pop(0) dx, dy = self.direction head = (self.snake[0][0] + dx, self.snake[0][1] + dy) eating = head == self.food occupied = self.snake if eating else self.snake[:-1] if not (0 <= head[0] < COLS and 0 <= head[1] < ROWS): self.finish("You hit the wall!") return if head in occupied: self.finish("You hit your own tail!") return self.snake.insert(0, head) if eating: self.score += 10 self.place_food() if self.food is None: self.finish("Perfect run! Grid cleared.") else: self.snake.pop() def handle_events(self): for event in pygame.event.get(): if event.type == pygame.QUIT: if self.state == "entry": self.record_run() self.running = False return if event.type != pygame.KEYDOWN: continue if self.state == "playing": if event.key in DIRECTIONS: self.queue_turn(DIRECTIONS[event.key]) elif self.state == "entry": if event.key == pygame.K_RETURN: self.record_run() elif event.key == pygame.K_BACKSPACE: self.initials = self.initials[:-1] elif len(self.initials) < 12: character = event.unicode if character and character.isascii() and ( character.isalnum() or character in " -_" ): self.initials += character.upper() elif self.state in ("menu", "gameover"): if event.key in (pygame.K_RETURN, pygame.K_SPACE): self.new_game() elif event.key == pygame.K_ESCAPE: self.running = False return def draw_board(self): self.screen.fill(BG) pygame.draw.rect(self.screen, INK, (0, 0, WIDTH, HEADER)) self.text("SNAKE", (18, 13), color=PANEL) self.text(f"SCORE {self.score:04d}", (180, 13), color=PANEL) self.text(f"LENGTH {len(self.snake)}", (390, 13), color=PANEL) best = self.high_scores[0]["score"] if self.high_scores else 0 self.text(f"BEST {best} | ARROWS TO STEER | +10 / FOOD", (18, 46), font=self.small, color=BG) for x in range(COLS + 1): pygame.draw.line(self.screen, GRID, (x * CELL, HEADER), (x * CELL, HEIGHT)) for y in range(ROWS + 1): pygame.draw.line(self.screen, GRID, (0, HEADER + y * CELL), (WIDTH, HEADER + y * CELL)) if self.food is not None: x, y = self.food center = (x * CELL + CELL // 2, HEADER + y * CELL + CELL // 2) pygame.draw.circle(self.screen, INK, center, 8) pygame.draw.rect(self.screen, BG, (center[0] - 2, center[1] - 2, 4, 4)) for index, (x, y) in enumerate(self.snake): rect = pygame.Rect(x * CELL + 2, HEADER + y * CELL + 2, CELL - 4, CELL - 4) pygame.draw.rect(self.screen, INK if index == 0 else BODY, rect) if index == 0: dx, dy = getattr(self, "direction", (1, 0)) cx, cy = rect.center for side in (-1, 1): eye = (cx + dx * 5 - dy * side * 4, cy + dy * 5 + dx * side * 4) pygame.draw.circle(self.screen, PANEL, eye, 2) def draw_scores(self, top): self.text("TOP 5 • HIGH SCORES", (WIDTH // 2, top), center=True) for index in range(5): y = top + 32 + index * 28 if index < len(self.high_scores): entry = self.high_scores[index] name, score = entry["initials"], str(entry["score"]) else: name, score = "---", "---" self.text(f"{index + 1}. {name}", (118, y), font=self.small) image = self.small.render(score, True, INK) self.screen.blit(image, (WIDTH - 118 - image.get_width(), y)) def draw_overlay(self): rect = pygame.Rect(64, HEADER + 24, WIDTH - 128, ROWS * CELL - 48) pygame.draw.rect(self.screen, PANEL, rect) pygame.draw.rect(self.screen, INK, rect, 3) cx = WIDTH // 2 top = rect.top if self.state == "menu": self.text("SNAKE", (cx, top + 43), self.large, center=True) self.text("Eat. Grow. Stay alive.", (cx, top + 88), center=True) self.text("Arrow keys steer. Walls and body are fatal.", (cx, top + 122), self.small, center=True) self.draw_scores(top + 168) self.text("ENTER / SPACE to start", (cx, top + 389), center=True) self.text("ESC to quit", (cx, top + 425), self.small, center=True) else: title = "GRID CLEARED!" if self.food is None else "GAME OVER" self.text(title, (cx, top + 38), self.large, center=True) self.text(self.result, (cx, top + 77), self.small, center=True) self.text(f"Score: {self.score} Length: {len(self.snake)}", (cx, top + 111), center=True) if self.state == "entry": self.text("Enter name / initials, then press ENTER:", (cx, top + 147), self.small, center=True) field = pygame.Rect(150, top + 165, WIDTH - 300, 37) pygame.draw.rect(self.screen, BG, field) pygame.draw.rect(self.screen, INK, field, 2) cursor = "_" if pygame.time.get_ticks() % 1000 < 500 else "" self.text(self.initials + cursor, field.center, center=True) else: self.text(self.save_message, (cx, top + 164), self.small, center=True) self.draw_scores(top + 233) if self.state == "entry": footer = "Blank name saves as ANON." else: footer = "ENTER: play again | ESC: quit" self.text(footer, (cx, top + 440), self.small, center=True) def run(self): while self.running: dt = min(self.clock.tick(60) / 1000.0, 0.25) self.handle_events() if not self.running: break if self.state == "playing": self.elapsed += dt while self.elapsed >= STEP_TIME and self.state == "playing": self.elapsed -= STEP_TIME self.step() self.draw_board() if self.state != "playing": self.draw_overlay() pygame.display.flip() pygame.quit() if __name__ == "__main__": SnakeGame().run()