import pygame import json import os import sys pygame.init() TILE = 40 COLS = 15 ROWS = 15 WIDTH = TILE * COLS HEIGHT = TILE * ROWS FPS = 60 SCREEN = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("Crossy Hopper") CLOCK = pygame.time.Clock() WHITE = (255, 255, 255) BLACK = (0, 0, 0) GREEN = (60, 180, 75) GRAY = (90, 90, 90) BLUE = (40, 90, 200) YELLOW = (240, 220, 60) RED = (220, 60, 60) DARK = (30, 30, 40) LIGHT = (200, 200, 210) HIGH_SCORE_FILE = "high_scores.json" TMP_SCORE_FILE = "/tmp/alpaca_score.json" FONT = pygame.font.SysFont("consolas", 20) BIG_FONT = pygame.font.SysFont("consolas", 32, bold=True) SMALL_FONT = pygame.font.SysFont("consolas", 16) def load_high_scores(): if os.path.exists(HIGH_SCORE_FILE): try: with open(HIGH_SCORE_FILE, "r") as f: data = json.load(f) if isinstance(data, list): return data[:5] except Exception: pass return [] def save_high_scores(scores): try: with open(HIGH_SCORE_FILE, "w") as f: json.dump(scores[:5], f) except Exception: pass def write_tmp_score(initials, score): try: with open(TMP_SCORE_FILE, "w") as f: json.dump({"initials": initials, "score": score}, f) except Exception: pass def add_high_score(scores, initials, score): scores.append({"name": initials, "score": score}) scores.sort(key=lambda x: x["score"], reverse=True) return scores[:5] class Lane: def __init__(self, row, kind, speed, direction, difficulty): self.row = row self.kind = kind # 'road' or 'water' self.speed = speed self.direction = direction self.obstacles = [] spacing = TILE * 4 count = (WIDTH // spacing) + 3 for i in range(count): x = i * spacing + (row * 17) % spacing w = TILE * 2 if kind == "road" else TILE * 3 self.obstacles.append([x, w]) def update(self, dt): for ob in self.obstacles: ob[0] += self.speed * self.direction * dt for ob in self.obstacles: if self.direction > 0 and ob[0] > WIDTH + TILE: ob[0] = -ob[1] - TILE * 2 elif self.direction < 0 and ob[0] + ob[1] < -TILE: ob[0] = WIDTH + TILE * 2 def draw(self, surface, y): if self.kind == "road": pygame.draw.rect(surface, GRAY, (0, y, WIDTH, TILE)) for ob in self.obstacles: pygame.draw.rect(surface, RED, (ob[0], y + 6, ob[1], TILE - 12), border_radius=4) else: pygame.draw.rect(surface, BLUE, (0, y, WIDTH, TILE)) for ob in self.obstacles: pygame.draw.rect(surface, (140, 90, 40), (ob[0], y + 8, ob[1], TILE - 16), border_radius=6) class Game: def __init__(self): self.reset() def reset(self): self.score = 0 self.player_col = COLS // 2 self.player_row = ROWS - 2 self.lanes = {} self.next_row = ROWS - 3 self.generate_lanes(ROWS - 3) self.state = "play" self.initials = "" self.high_scores = load_high_scores() self.death_timer = 0 def generate_lanes(self, up_to_row): while self.next_row >= 0: r = self.next_row difficulty = 1.0 + self.score * 0.05 if r % 2 == 0: kind = "road" speed = (60 + self.score * 4) * (1 if r % 4 == 0 else -1) else: kind = "water" speed = (40 + self.score * 3) * (1 if r % 4 == 1 else -1) self.lanes[r] = Lane(r, kind, speed, 1 if speed > 0 else -1, difficulty) self.next_row -= 1 def hop(self, dc, dr): if self.state != "play": return new_col = self.player_col + dc new_row = self.player_row + dr if 0 <= new_col < COLS and 0 <= new_row < ROWS: self.player_col = new_col self.player_row = new_row if dr < 0: self.score += 1 if self.player_row < 3: self.shift_world() def shift_world(self): new_lanes = {} for r, lane in self.lanes.items(): new_lanes[r + 1] = lane lane.row = r + 1 self.lanes = new_lanes self.player_row += 1 self.next_row = 0 difficulty = 1.0 + self.score * 0.05 r = 0 if r % 2 == 0: kind = "road" speed = (60 + self.score * 4) * (1 if r % 4 == 0 else -1) else: kind = "water" speed = (40 + self.score * 3) * (1 if r % 4 == 1 else -1) self.lanes[r] = Lane(r, kind, speed, 1 if speed > 0 else -1, difficulty) def update(self, dt): if self.state == "play": for lane in self.lanes.values(): lane.update(dt) self.check_collision() elif self.state == "dead": self.death_timer += dt if self.death_timer > 0.5: self.state = "entry" def check_collision(self): lane = self.lanes.get(self.player_row) if lane is None: return px = self.player_col * TILE + TILE * 0.15 pw = TILE * 0.7 for ob in lane.obstacles: ox, ow = ob if px < ox + ow and px + pw > ox: if lane.kind == "road": self.state = "dead" self.death_timer = 0 return else: return if lane.kind == "water": self.state = "dead" self.death_timer = 0 def draw(self, surface): surface.fill(DARK) for r in range(ROWS): y = r * TILE lane = self.lanes.get(r) if lane: lane.draw(surface, y) else: pygame.draw.rect(surface, GREEN, (0, y, WIDTH, TILE)) px = self.player_col * TILE + TILE * 0.15 py = self.player_row * TILE + TILE * 0.15 color = YELLOW if self.state != "dead" else RED pygame.draw.rect(surface, color, (px, py, TILE * 0.7, TILE * 0.7), border_radius=6) score_surf = FONT.render(f"Score: {self.score}", True, WHITE) surface.blit(score_surf, (10, 10)) hs = self.high_scores[0]["score"] if self.high_scores else 0 hs_surf = FONT.render(f"Best: {hs}", True, WHITE) surface.blit(hs_surf, (WIDTH - hs_surf.get_width() - 10, 10)) def draw_game_over(self, surface): surface.fill(DARK) title = BIG_FONT.render("GAME OVER", True, RED) surface.blit(title, (WIDTH // 2 - title.get_width() // 2, 40)) sc = FONT.render(f"Score: {self.score}", True, WHITE) surface.blit(sc, (WIDTH // 2 - sc.get_width() // 2, 90)) if self.state == "entry": prompt = FONT.render("Enter initials (A-Z), ENTER to save:", True, WHITE) surface.blit(prompt, (WIDTH // 2 - prompt.get_width() // 2, 130)) box = pygame.Rect(WIDTH // 2 - 60, 160, 120, 40) pygame.draw.rect(surface, WHITE, box, 2) txt = BIG_FONT.render(self.initials + "_", True, YELLOW) surface.blit(txt, (WIDTH // 2 - txt.get_width() // 2, 165)) board_title = FONT.render("HIGH SCORES", True, YELLOW) surface.blit(board_title, (WIDTH // 2 - board_title.get_width() // 2, 220)) for i, entry in enumerate(self.high_scores): line = SMALL_FONT.render(f"{i+1}. {entry['name']} {entry['score']}", True, WHITE) surface.blit(line, (WIDTH // 2 - line.get_width() // 2, 250 + i * 22)) hint = SMALL_FONT.render("Press R to restart, ESC to quit", True, LIGHT) surface.blit(hint, (WIDTH // 2 - hint.get_width() // 2, HEIGHT - 30)) def finish_run(self): name = self.initials if self.initials else "AAA" self.high_scores = add_high_score(self.high_scores, name, self.score) save_high_scores(self.high_scores) write_tmp_score(name, self.score) def main(): game = Game() running = True while running: dt = CLOCK.tick(FPS) / 1000.0 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if game.state == "play": if event.key == pygame.K_UP: game.hop(0, -1) elif event.key == pygame.K_DOWN: game.hop(0, 1) elif event.key == pygame.K_LEFT: game.hop(-1, 0) elif event.key == pygame.K_RIGHT: game.hop(1, 0) elif event.key == pygame.K_ESCAPE: running = False elif game.state == "entry": if event.key == pygame.K_RETURN: game.finish_run() game.state = "gameover" elif event.key == pygame.K_BACKSPACE: game.initials = game.initials[:-1] elif event.key == pygame.K_ESCAPE: running = False else: ch = event.unicode.upper() if ch.isalpha() and len(game.initials) < 3: game.initials += ch elif game.state == "gameover": if event.key == pygame.K_r: game.reset() elif event.key == pygame.K_ESCAPE: running = False game.update(dt) if game.state in ("play", "dead"): game.draw(SCREEN) else: game.draw_game_over(SCREEN) pygame.display.flip() pygame.quit() sys.exit() if __name__ == "__main__": main()