diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..b95bb76 --- /dev/null +++ b/constants.py @@ -0,0 +1,74 @@ +# 游戏常量定义 +import pygame + +# 屏幕设置 +SCREEN_WIDTH = 800 +SCREEN_HEIGHT = 600 +FPS = 60 + +# 颜色定义 +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) +BLUE = (0, 128, 255) +RED = (255, 0, 0) +GREEN = (0, 255, 0) +YELLOW = (255, 255, 0) +PURPLE = (128, 0, 128) +ORANGE = (255, 165, 0) +GRAY = (128, 128, 128) +LIGHT_GRAY = (200, 200, 200) +DARK_GRAY = (64, 64, 64) + +# 玩家颜色 +PLAYER_COLOR = BLUE +SHADOW_COLOR = (100, 100, 255, 150) # 半透明蓝色 + +# 机关颜色 +BUTTON_COLOR = RED +BUTTON_ACTIVE_COLOR = GREEN +DOOR_COLOR = PURPLE +LASER_COLOR = (255, 50, 50) +BOX_COLOR = ORANGE + +# 墙壁颜色 +WALL_COLOR = DARK_GRAY +FLOOR_COLOR = LIGHT_GRAY + +# 网格设置 +TILE_SIZE = 40 +GRID_WIDTH = 20 +GRID_HEIGHT = 15 + +# 玩家设置 +PLAYER_SPEED = 4 # 像素每帧 +MAX_REWIND_FRAMES = 300 # 最大回溯帧数 (5秒 @ 60FPS) +MAX_REWIND_COUNT = 5 # 最大回溯次数 + +# 时间回溯设置 +REWIND_KEY = pygame.K_r +RESTART_KEY = pygame.K_ESCAPE +MOVE_KEYS = { + 'up': pygame.K_UP, + 'down': pygame.K_DOWN, + 'left': pygame.K_LEFT, + 'right': pygame.K_RIGHT +} + +# 动画设置 +TRAIL_ALPHA_START = 200 +TRAIL_ALPHA_END = 50 +TRAIL_LENGTH = 30 + +# 关卡设置 +MAX_LEVELS = 5 + +# 门类型 +DOOR_NORMAL = 0 +DOOR_DELAYED = 1 +DOOR_ONE_WAY = 2 + +# 方向 +UP = (0, -1) +DOWN = (0, 1) +LEFT = (-1, 0) +RIGHT = (1, 0) diff --git a/game.py b/game.py new file mode 100644 index 0000000..82aafad --- /dev/null +++ b/game.py @@ -0,0 +1,117 @@ +import pygame +from constants import * +from player import Player +from recorder import StateRecorder +from level import LevelManager +from ui import UIManager + +class Game: + def __init__(self, screen): + self.screen = screen + self.current_level = 0 + self.level_manager = LevelManager() + self.ui_manager = UIManager(screen) + + self.reset() + + def reset(self): + level_data = self.level_manager.get_level(self.current_level) + self.recorder = StateRecorder() + self.player = Player( + level_data['player_start'][0], + level_data['player_start'][1], + self.recorder + ) + + self.level_manager.load_level(self.current_level, self.recorder) + self.rewind_count = MAX_REWIND_COUNT + self.is_rewinding = False + self.rewind_progress = 0 + self.game_won = False + + def handle_keydown(self, key): + if self.game_won: + if key == pygame.K_SPACE: + self.next_level() + return + + if key == REWIND_KEY and self.rewind_count > 0 and not self.is_rewinding: + self.start_rewind() + + if key == RESTART_KEY: + self.reset() + + if key in MOVE_KEYS.values(): + self.player.handle_keydown(key) + + def handle_keyup(self, key): + if key in MOVE_KEYS.values(): + self.player.handle_keyup(key) + + def start_rewind(self): + self.is_rewinding = True + self.rewind_progress = 0 + self.recorder.freeze_current_state() + self.player.freeze_for_rewind() + + def update(self, dt): + if self.game_won: + return + + if self.is_rewinding: + self.rewind_progress += dt + rewind_speed = 1.0 / 1.5 # 1.5秒完成回溯 + + progress = min(1.0, self.rewind_progress * rewind_speed) + self.recorder.update_rewind(progress) + self.player.update_rewind(progress) + + if progress >= 1.0: + self.finish_rewind() + else: + self.player.update(dt, self.level_manager) + self.level_manager.update(dt, self.player, self.recorder.get_shadows()) + + if self.level_manager.check_win(self.player): + self.game_won = True + + state = self.player.get_state() + self.recorder.record_state(state) + + def finish_rewind(self): + self.is_rewinding = False + self.rewind_count -= 1 + self.recorder.create_shadow_from_rewind() + self.player.release_from_rewind() + self.level_manager.on_rewind_finish() + + def next_level(self): + self.current_level += 1 + if self.current_level >= self.level_manager.get_level_count(): + self.current_level = 0 + self.reset() + + def draw(self): + self.screen.fill(FLOOR_COLOR) + + self.level_manager.draw(self.screen) + self.recorder.draw_shadows(self.screen) + self.player.draw(self.screen) + + if self.is_rewinding: + self.draw_rewind_overlay() + + self.ui_manager.draw( + self.current_level, + self.rewind_count, + self.recorder.get_recorded_frames(), + MAX_REWIND_FRAMES, + self.game_won, + self.is_rewinding, + self.rewind_progress + ) + + def draw_rewind_overlay(self): + overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) + overlay.fill((0, 0, 100, 30)) + self.screen.blit(overlay, (0, 0)) diff --git a/level.py b/level.py new file mode 100644 index 0000000..a33847c --- /dev/null +++ b/level.py @@ -0,0 +1,282 @@ +import pygame +from constants import * +from mechanisms import Button, Door, LaserEmitter, Box, Goal + +class LevelManager: + def __init__(self): + self.levels = self._create_levels() + self.current_level_data = None + + self.walls = [] + self.doors = [] + self.buttons = [] + self.lasers = [] + self.boxes = [] + self.goals = [] + + def _create_levels(self): + return [ + { + 'name': '教程关卡 1: 基础移动', + 'player_start': (2, 2), + 'walls': self._create_rect_walls(1), + 'buttons': [], + 'doors': [], + 'lasers': [], + 'boxes': [], + 'goals': [(15, 7)], + 'hints': '使用方向键移动,到达绿色目标点' + }, + { + 'name': '教程关卡 2: 推箱子', + 'player_start': (3, 5), + 'walls': self._create_rect_walls(2) + [ + (8, 4), (8, 5), (8, 6), (8, 7), + (12, 4), (12, 5), (12, 6), (12, 7), + (9, 3), (10, 3), (11, 3), + (9, 8), (10, 8), (11, 8) + ], + 'buttons': [ + {'pos': (10, 6), + 'target_doors': [0], + 'laser_targets': []} + ], + 'doors': [ + {'pos': (15, 6), + 'type': DOOR_NORMAL} + ], + 'lasers': [], + 'boxes': [(6, 5)], + 'goals': [(17, 6)], + 'hints': '推动箱子到按钮上开门' + }, + { + 'name': '关卡 3: 时间回溯基础', + 'player_start': (3, 3), + 'walls': self._create_rect_walls(3) + [ + (8, 2), (8, 3), (8, 4), (8, 5), (8, 6), (8, 7), (8, 8), + (14, 2), (14, 3), (14, 4), (14, 5), (14, 6), (14, 7), (14, 8) + ], + 'buttons': [ + {'pos': (5, 3), + 'target_doors': [0], + 'laser_targets': []}, + {'pos': (11, 7), + 'target_doors': [0], + 'laser_targets': []} + ], + 'doors': [ + {'pos': (17, 5), + 'type': DOOR_NORMAL} + ], + 'lasers': [], + 'boxes': [], + 'goals': [(18, 5)], + 'hints': '按 R 键回溯时间,与影子会重演你的动作!两个按钮需要同时按下' + }, + { + 'name': '关卡 4: 延迟门', + 'player_start': (2, 5), + 'walls': self._create_rect_walls(4) + [ + (10, 3), (10, 4), (10, 5), (10, 6), (10, 7), + (15, 3), (15, 4), (15, 5), (15, 6), (15, 7) + ], + 'buttons': [ + {'pos': (6, 5), + 'target_doors': [0], + 'laser_targets': []} + ], + 'doors': [ + {'pos': (18, 5), + 'type': DOOR_DELAYED, + 'delay': 2.0} + ], + 'lasers': [], + 'boxes': [], + 'goals': [(18, 5)], + 'hints': '黄色标记的门是延迟门,需要时间才能打开。利用时间回溯快速通过!' + }, + { + 'name': '关卡 5: 激光陷阱', + 'player_start': (2, 7), + 'walls': self._create_rect_walls(5) + [ + (5, 3), (5, 4), (5, 6), (5, 7), (5, 8), + (12, 3), (12, 4), (12, 6), (12, 7), (12, 8) + ], + 'buttons': [ + {'pos': (8, 3), + 'target_doors': [], + 'laser_targets': [0]} + ], + 'doors': [ + {'pos': (17, 5), + 'type': DOOR_NORMAL} + ], + 'lasers': [ + {'pos': (5, 5), + 'direction': RIGHT, + 'target_doors': [0]} + ], + 'boxes': [(3, 3)], + 'goals': [(18, 5)], + 'hints': '激光会阻挡前进!用箱子或影子来挡住激光,或者按下按钮关闭它' + } + ] + + def _create_rect_walls(self, level_num): + walls = [] + for x in range(GRID_WIDTH): + walls.append((x, 0)) + walls.append((x, GRID_HEIGHT - 1)) + for y in range(1, GRID_HEIGHT - 1): + walls.append((0, y)) + walls.append((GRID_WIDTH - 1, y)) + return walls + + def get_level_count(self): + return len(self.levels) + + def get_level(self, index): + return self.levels[index] + + def load_level(self, level_index, recorder): + level_data = self.levels[level_index] + self.current_level_data = level_data + + self.walls = level_data['walls'].copy() + + self.doors = [] + for door_data in level_data['doors']: + door = Door( + door_data['pos'][0], + door_data['pos'][1], + door_type=door_data.get('type', DOOR_NORMAL), + delay_time=door_data.get('delay', 0) + ) + self.doors.append(door) + + self.buttons = [] + for button_data in level_data['buttons']: + button = Button( + button_data['pos'][0], + button_data['pos'][1], + button_data['target_doors'], + button_data['laser_targets'] + ) + self.buttons.append(button) + + self.lasers = [] + for laser_data in level_data['lasers']: + laser = LaserEmitter( + laser_data['pos'][0], + laser_data['pos'][1], + laser_data['direction'], + laser_data.get('target_doors', []) + ) + self.lasers.append(laser) + + self.boxes = [] + for box_pos in level_data['boxes']: + box = Box(box_pos[0], box_pos[1]) + self.boxes.append(box) + + self.goals = [] + for goal_pos in level_data['goals']: + goal = Goal(goal_pos[0], goal_pos[1]) + self.goals.append(goal) + + def update(self, dt, player, shadows): + for button in self.buttons: + button.update(player, shadows, self.boxes) + + for i, button in enumerate(self.buttons): + for door_idx in button.target_doors: + if door_idx < len(self.doors): + if button.is_pressed: + self.doors[door_idx].open() + else: + self.doors[door_idx].close() + + for laser in self.lasers: + laser.update(self.walls, self.doors, player, shadows, self.boxes) + + for i, laser in enumerate(self.lasers): + for door_idx in laser.target_doors: + if door_idx < len(self.doors): + if laser.is_blocked: + self.doors[door_idx].open() + else: + self.doors[door_idx].close() + + for button in self.buttons: + for laser_idx in button.laser_targets: + if laser_idx < len(self.lasers): + pass + + for door in self.doors: + door.update(dt) + + for box in self.boxes: + box.update(dt) + + for goal in self.goals: + goal.update(dt) + + def is_wall(self, grid_x, grid_y): + return (grid_x, grid_y) in self.walls + + def is_closed_door(self, grid_x, grid_y): + for door in self.doors: + if door.grid_x == grid_x and door.grid_y == grid_y: + return door.is_blocking() + return False + + def get_box_at(self, grid_x, grid_y): + for box in self.boxes: + if box.grid_x == grid_x and box.grid_y == grid_y: + return box + return None + + def can_box_move_to(self, grid_x, grid_y): + if (grid_x, grid_y) in self.walls: + return False + + for door in self.doors: + if door.grid_x == grid_x and door.grid_y == grid_y and door.is_blocking(): + return False + + for box in self.boxes: + if box.grid_x == grid_x and box.grid_y == grid_y: + return False + + return True + + def check_win(self, player): + for goal in self.goals: + if player.grid_x == goal.grid_x and player.grid_y == goal.grid_y: + return True + return False + + def on_rewind_finish(self): + pass + + def draw(self, screen): + for wall in self.walls: + x = wall[0] * TILE_SIZE + y = wall[1] * TILE_SIZE + pygame.draw.rect(screen, WALL_COLOR, (x, y, TILE_SIZE, TILE_SIZE)) + + for goal in self.goals: + goal.draw(screen) + + for button in self.buttons: + button.draw(screen) + + for door in self.doors: + door.draw(screen) + + for laser in self.lasers: + laser.draw(screen) + + for box in self.boxes: + box.draw(screen) diff --git a/main.py b/main.py new file mode 100644 index 0000000..e94cb5a --- /dev/null +++ b/main.py @@ -0,0 +1,35 @@ +import pygame +import sys +from constants import * +from game import Game + +def main(): + pygame.init() + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + pygame.display.set_caption("时间回溯解谜游戏") + clock = pygame.time.Clock() + + game = Game(screen) + + 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: + game.handle_keydown(event.key) + elif event.type == pygame.KEYUP: + game.handle_keyup(event.key) + + game.update(dt) + game.draw() + + pygame.display.flip() + + pygame.quit() + sys.exit() + +if __name__ == "__main__": + main() diff --git a/mechanisms.py b/mechanisms.py new file mode 100644 index 0000000..3f524f6 --- /dev/null +++ b/mechanisms.py @@ -0,0 +1,345 @@ +import pygame +import math +from constants import * + +class Button: + def __init__(self, grid_x, grid_y, target_doors, laser_targets=None): + self.grid_x = grid_x + self.grid_y = grid_y + self.target_doors = target_doors + self.laser_targets = laser_targets or [] + self.is_pressed = False + self.was_pressed = False + + def update(self, player, shadows, boxes): + current_pressed = self._check_activation(player, shadows, boxes) + self.was_pressed = self.is_pressed + self.is_pressed = current_pressed + + def _check_activation(self, player, shadows, boxes): + if player.grid_x == self.grid_x and player.grid_y == self.grid_y: + return True + + for shadow in shadows: + if shadow.is_active() and shadow.grid_x == self.grid_x and shadow.grid_y == self.grid_y: + return True + + for box in boxes: + if box.grid_x == self.grid_x and box.grid_y == self.grid_y: + return True + + return False + + def just_pressed(self): + return self.is_pressed and not self.was_pressed + + def just_released(self): + return not self.is_pressed and self.was_pressed + + def draw(self, screen): + x = self.grid_x * TILE_SIZE + y = self.grid_y * TILE_SIZE + + color = BUTTON_ACTIVE_COLOR if self.is_pressed else BUTTON_COLOR + + button_rect = pygame.Rect( + x + 8, + y + TILE_SIZE - 12, + TILE_SIZE - 16, + 8 + ) + pygame.draw.rect(screen, color, button_rect) + + if self.is_pressed: + glow_rect = pygame.Rect( + x + 4, + y + TILE_SIZE - 16, + TILE_SIZE - 8, + 12 + ) + glow_surface = pygame.Surface((TILE_SIZE - 8, 12), pygame.SRCALPHA) + pygame.draw.rect(glow_surface, (0, 255, 0, 50), glow_surface.get_rect()) + screen.blit(glow_surface, (x + 4, y + TILE_SIZE - 16)) + + +class Door: + def __init__(self, grid_x, grid_y, door_type=DOOR_NORMAL, delay_time=0, one_way_direction=None): + self.grid_x = grid_x + self.grid_y = grid_y + self.door_type = door_type + self.is_open = False + self.is_locked = False + + self.delay_time = delay_time + self.delay_timer = 0 + self.is_delaying = False + + self.one_way_direction = one_way_direction + + self.open_progress = 0 + self.animation_speed = 5.0 + + def open(self): + if self.is_locked: + return + + if self.door_type == DOOR_DELAYED: + if not self.is_delaying: + self.is_delaying = True + self.delay_timer = 0 + else: + self.is_open = True + + def close(self): + if self.is_locked: + return + + self.is_open = False + self.is_delaying = False + + def update(self, dt): + if self.door_type == DOOR_DELAYED and self.is_delaying: + self.delay_timer += dt + if self.delay_timer >= self.delay_time: + self.is_open = True + self.is_delaying = False + + if self.is_open and self.open_progress < 1.0: + self.open_progress = min(1.0, self.open_progress + dt * self.animation_speed) + elif not self.is_open and self.open_progress > 0: + self.open_progress = max(0.0, self.open_progress - dt * self.animation_speed) + + def is_blocking(self): + return not self.is_open or self.open_progress < 0.9 + + def can_pass_from_direction(self, direction): + if self.door_type != DOOR_ONE_WAY: + return not self.is_blocking() + + if self.one_way_direction is None: + return not self.is_blocking() + + return direction == self.one_way_direction + + def draw(self, screen): + x = self.grid_x * TILE_SIZE + y = self.grid_y * TILE_SIZE + + if self.is_open and self.open_progress >= 1.0: + pygame.draw.rect(screen, FLOOR_COLOR, (x, y, TILE_SIZE, TILE_SIZE)) + return + + open_offset = int(self.open_progress * (TILE_SIZE - 4)) + + door_surface = pygame.Surface((TILE_SIZE, TILE_SIZE), pygame.SRCALPHA) + + left_door = pygame.Rect(2, 2, TILE_SIZE // 2 - 2 - open_offset // 2, TILE_SIZE - 4) + right_door = pygame.Rect( + TILE_SIZE // 2 + open_offset // 2, + 2, + TILE_SIZE // 2 - 2 - open_offset // 2, + TILE_SIZE - 4 + ) + + door_color = DOOR_COLOR if not self.is_locked else (128, 128, 128) + pygame.draw.rect(door_surface, door_color, left_door) + pygame.draw.rect(door_surface, door_color, right_door) + + if self.door_type == DOOR_DELAYED: + pygame.draw.circle(door_surface, YELLOW, (TILE_SIZE // 2, TILE_SIZE // 2), 6) + elif self.door_type == DOOR_ONE_WAY: + arrow_center = (TILE_SIZE // 2, TILE_SIZE // 2) + pygame.draw.polygon(door_surface, GREEN, [ + (arrow_center[0], arrow_center[1] - 8), + (arrow_center[0] - 6, arrow_center[1] + 4), + (arrow_center[0] + 6, arrow_center[1] + 4) + ]) + + screen.blit(door_surface, (x, y)) + + pygame.draw.rect(screen, DARK_GRAY, (x, y, TILE_SIZE, TILE_SIZE), 2) + + +class LaserEmitter: + def __init__(self, grid_x, grid_y, direction, target_doors=None): + self.grid_x = grid_x + self.grid_y = grid_y + self.direction = direction + self.target_doors = target_doors or [] + self.is_blocked = False + self.laser_end_x = grid_x + self.laser_end_y = grid_y + + def update(self, walls, doors, player, shadows, boxes): + self.is_blocked = False + current_x = self.grid_x + current_y = self.grid_y + + while True: + next_x = current_x + self.direction[0] + next_y = current_y + self.direction[1] + + if next_x < 0 or next_x >= GRID_WIDTH or next_y < 0 or next_y >= GRID_HEIGHT: + self.laser_end_x = current_x + self.laser_end_y = current_y + break + + if (next_x, next_y) in walls: + self.laser_end_x = current_x + self.laser_end_y = current_y + break + + for door in doors: + if door.grid_x == next_x and door.grid_y == next_y and door.is_blocking(): + self.laser_end_x = current_x + self.laser_end_y = current_y + break + else: + if (player.grid_x == next_x and player.grid_y == next_y): + self.is_blocked = True + self.laser_end_x = next_x + self.laser_end_y = next_y + break + + for shadow in shadows: + if shadow.is_active() and shadow.grid_x == next_x and shadow.grid_y == next_y: + self.is_blocked = True + self.laser_end_x = next_x + self.laser_end_y = next_y + break + else: + for box in boxes: + if box.grid_x == next_x and box.grid_y == next_y: + self.is_blocked = True + self.laser_end_x = next_x + self.laser_end_y = next_y + break + else: + current_x = next_x + current_y = next_y + continue + break + break + + def draw(self, screen): + x = self.grid_x * TILE_SIZE + y = self.grid_y * TILE_SIZE + + emitter_rect = pygame.Rect(x + 8, y + 8, TILE_SIZE - 16, TILE_SIZE - 16) + pygame.draw.rect(screen, RED, emitter_rect) + + direction_arrow = { + UP: (TILE_SIZE // 2, 4), + DOWN: (TILE_SIZE // 2, TILE_SIZE - 4), + LEFT: (4, TILE_SIZE // 2), + RIGHT: (TILE_SIZE - 4, TILE_SIZE // 2) + } + + pygame.draw.circle(screen, YELLOW, (x + direction_arrow[self.direction][0], + y + direction_arrow[self.direction][1]), 3) + + self.draw_laser_beam(screen) + + def draw_laser_beam(self, screen): + start_x = self.grid_x * TILE_SIZE + TILE_SIZE // 2 + start_y = self.grid_y * TILE_SIZE + TILE_SIZE // 2 + end_x = self.laser_end_x * TILE_SIZE + TILE_SIZE // 2 + end_y = self.laser_end_y * TILE_SIZE + TILE_SIZE // 2 + + if self.direction[0] != 0: + end_x += self.direction[0] * (TILE_SIZE // 2) + if self.direction[1] != 0: + end_y += self.direction[1] * (TILE_SIZE // 2) + + laser_surface = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) + pygame.draw.line(laser_surface, (255, 50, 50, 150), (start_x, start_y), (end_x, end_y), 6) + pygame.draw.line(laser_surface, (255, 200, 200, 200), (start_x, start_y), (end_x, end_y), 2) + screen.blit(laser_surface, (0, 0)) + + +class Box: + def __init__(self, grid_x, grid_y): + self.grid_x = grid_x + self.grid_y = grid_y + self.x = grid_x * TILE_SIZE + self.y = grid_y * TILE_SIZE + self.target_x = self.x + self.target_y = self.y + self.moving = False + + def move(self, direction): + self.grid_x += direction[0] + self.grid_y += direction[1] + self.target_x = self.grid_x * TILE_SIZE + self.target_y = self.grid_y * TILE_SIZE + self.moving = True + + def update(self, dt): + if not self.moving: + return + + move_amount = PLAYER_SPEED * 60 * dt + + if self.x < self.target_x: + self.x = min(self.x + move_amount, self.target_x) + elif self.x > self.target_x: + self.x = max(self.x - move_amount, self.target_x) + + if self.y < self.target_y: + self.y = min(self.y + move_amount, self.target_y) + elif self.y > self.target_y: + self.y = max(self.y - move_amount, self.target_y) + + if abs(self.x - self.target_x) < 1 and abs(self.y - self.target_y) < 1: + self.x = self.target_x + self.y = self.target_y + self.moving = False + + def draw(self, screen): + box_rect = pygame.Rect( + self.x + 4, + self.y + 4, + TILE_SIZE - 8, + TILE_SIZE - 8 + ) + pygame.draw.rect(screen, BOX_COLOR, box_rect) + pygame.draw.rect(screen, DARK_GRAY, box_rect, 2) + + cross_size = TILE_SIZE // 3 + center_x = self.x + TILE_SIZE // 2 + center_y = self.y + TILE_SIZE // 2 + + pygame.draw.line(screen, DARK_GRAY, + (center_x - cross_size, center_y - cross_size), + (center_x + cross_size, center_y + cross_size), 3) + pygame.draw.line(screen, DARK_GRAY, + (center_x + cross_size, center_y - cross_size), + (center_x - cross_size, center_y + cross_size), 3) + + +class Goal: + def __init__(self, grid_x, grid_y): + self.grid_x = grid_x + self.grid_y = grid_y + self.animation_timer = 0 + + def update(self, dt): + self.animation_timer += dt + + def draw(self, screen): + x = self.grid_x * TILE_SIZE + y = self.grid_y * TILE_SIZE + + pulse = (math.sin(self.animation_timer * 3) + 1) / 2 + size = int(TILE_SIZE // 2 + pulse * 6) + + center_x = x + TILE_SIZE // 2 + center_y = y + TILE_SIZE // 2 + + glow_surface = pygame.Surface((TILE_SIZE, TILE_SIZE), pygame.SRCALPHA) + pygame.draw.circle(glow_surface, (0, 255, 0, 50 + int(pulse * 50)), + (TILE_SIZE // 2, TILE_SIZE // 2), size + 4) + screen.blit(glow_surface, (x, y)) + + pygame.draw.circle(screen, GREEN, (center_x, center_y), size // 2) + pygame.draw.circle(screen, YELLOW, (center_x, center_y), size // 4) diff --git a/player.py b/player.py new file mode 100644 index 0000000..99c4e6d --- /dev/null +++ b/player.py @@ -0,0 +1,185 @@ +import pygame +from constants import * + +class Player: + def __init__(self, grid_x, grid_y, recorder): + self.grid_x = grid_x + self.grid_y = grid_y + self.x = grid_x * TILE_SIZE + self.y = grid_y * TILE_SIZE + self.target_x = self.x + self.target_y = self.y + self.recorder = recorder + + self.direction = RIGHT + self.moving = False + self.keys_pressed = set() + + self.is_frozen = False + self.frozen_x = 0 + self.frozen_y = 0 + + self.trail = [] + + def handle_keydown(self, key): + if self.is_frozen: + return + self.keys_pressed.add(key) + + def handle_keyup(self, key): + if key in self.keys_pressed: + self.keys_pressed.remove(key) + + def update(self, dt, level_manager): + if self.is_frozen: + return + + self.handle_movement(level_manager) + self.update_position(dt) + self.update_trail() + + def handle_movement(self, level_manager): + if MOVE_KEYS['up'] in self.keys_pressed and not self.moving: + self.try_move(UP, level_manager) + elif MOVE_KEYS['down'] in self.keys_pressed and not self.moving: + self.try_move(DOWN, level_manager) + elif MOVE_KEYS['left'] in self.keys_pressed and not self.moving: + self.try_move(LEFT, level_manager) + elif MOVE_KEYS['right'] in self.keys_pressed and not self.moving: + self.try_move(RIGHT, level_manager) + + def try_move(self, direction, level_manager): + new_grid_x = self.grid_x + direction[0] + new_grid_y = self.grid_y + direction[1] + + if level_manager.is_wall(new_grid_x, new_grid_y): + return + + if level_manager.is_closed_door(new_grid_x, new_grid_y): + return + + box = level_manager.get_box_at(new_grid_x, new_grid_y) + if box: + box_new_x = new_grid_x + direction[0] + box_new_y = new_grid_y + direction[1] + + if level_manager.can_box_move_to(box_new_x, box_new_y): + box.move(direction) + else: + return + + self.direction = direction + self.start_move(new_grid_x, new_grid_y) + + def start_move(self, new_grid_x, new_grid_y): + self.grid_x = new_grid_x + self.grid_y = new_grid_y + self.target_x = new_grid_x * TILE_SIZE + self.target_y = new_grid_y * TILE_SIZE + self.moving = True + + def update_position(self, dt): + if not self.moving: + return + + move_amount = PLAYER_SPEED * 60 * dt + + if self.x < self.target_x: + self.x = min(self.x + move_amount, self.target_x) + elif self.x > self.target_x: + self.x = max(self.x - move_amount, self.target_x) + + if self.y < self.target_y: + self.y = min(self.y + move_amount, self.target_y) + elif self.y > self.target_y: + self.y = max(self.y - move_amount, self.target_y) + + if abs(self.x - self.target_x) < 1 and abs(self.y - self.target_y) < 1: + self.x = self.target_x + self.y = self.target_y + self.moving = False + + def update_trail(self): + self.trail.append((int(self.x + TILE_SIZE // 2), int(self.y + TILE_SIZE // 2))) + if len(self.trail) > TRAIL_LENGTH: + self.trail.pop(0) + + def get_state(self): + return { + 'x': self.x, + 'y': self.y, + 'grid_x': self.grid_x, + 'grid_y': self.grid_y, + 'direction': self.direction, + 'moving': self.moving, + 'target_x': self.target_x, + 'target_y': self.target_y + } + + def set_state(self, state): + self.x = state['x'] + self.y = state['y'] + self.grid_x = state['grid_x'] + self.grid_y = state['grid_y'] + self.direction = state['direction'] + self.moving = state['moving'] + self.target_x = state['target_x'] + self.target_y = state['target_y'] + + def freeze_for_rewind(self): + self.is_frozen = True + self.frozen_x = self.x + self.frozen_y = self.y + + def release_from_rewind(self): + self.is_frozen = False + self.trail = [] + + def update_rewind(self, progress): + pass + + def draw(self, screen): + if self.is_frozen: + return + + self.draw_trail(screen) + + player_rect = pygame.Rect( + self.x + 4, + self.y + 4, + TILE_SIZE - 8, + TILE_SIZE - 8 + ) + pygame.draw.rect(screen, PLAYER_COLOR, player_rect) + + center_x = self.x + TILE_SIZE // 2 + center_y = self.y + TILE_SIZE // 2 + eye_offset = 6 + + if self.direction == RIGHT: + eye_x = center_x + eye_offset + elif self.direction == LEFT: + eye_x = center_x - eye_offset + else: + eye_x = center_x + + if self.direction == DOWN: + eye_y = center_y + eye_offset + elif self.direction == UP: + eye_y = center_y - eye_offset + else: + eye_y = center_y + + pygame.draw.circle(screen, WHITE, (eye_x, eye_y), 4) + pygame.draw.circle(screen, BLACK, (eye_x, eye_y), 2) + + def draw_trail(self, screen): + trail_length = len(self.trail) + for i, pos in enumerate(self.trail): + alpha = int(TRAIL_ALPHA_START - (TRAIL_ALPHA_START - TRAIL_ALPHA_END) * (i / trail_length)) + size = int(4 + 2 * (i / trail_length)) + + trail_surface = pygame.Surface((size * 2, size * 2), pygame.SRCALPHA) + pygame.draw.circle(trail_surface, (PLAYER_COLOR[0], PLAYER_COLOR[1], PLAYER_COLOR[2], alpha), + (size, size), size) + screen.blit(trail_surface, (pos[0] - size, pos[1] - size)) diff --git a/recorder.py b/recorder.py new file mode 100644 index 0000000..51f3749 --- /dev/null +++ b/recorder.py @@ -0,0 +1,44 @@ +import pygame +from constants import * +from shadow import Shadow + +class StateRecorder: + def __init__(self): + self.states = [] + self.shadows = [] + self.current_shadow = None + self.frozen_states = None + self.rewind_start_index = 0 + + def record_state(self, state): + self.states.append(state.copy()) + if len(self.states) > MAX_REWIND_FRAMES: + self.states.pop(0) + + def freeze_current_state(self): + self.frozen_states = self.states.copy() + self.rewind_start_index = 0 + + def update_rewind(self, progress): + pass + + def create_shadow_from_rewind(self): + if self.frozen_states and len(self.frozen_states) > 0: + shadow = Shadow(self.frozen_states.copy()) + self.shadows.append(shadow) + self.current_shadow = shadow + self.frozen_states = None + + def get_shadows(self): + return self.shadows + + def get_recorded_frames(self): + return len(self.states) + + def update(self, dt): + for shadow in self.shadows: + shadow.update(dt) + + def draw_shadows(self, screen): + for shadow in self.shadows: + shadow.draw(screen) diff --git a/shadow.py b/shadow.py new file mode 100644 index 0000000..d7a8e9f --- /dev/null +++ b/shadow.py @@ -0,0 +1,113 @@ +import pygame +from constants import * + +class Shadow: + def __init__(self, recorded_states): + self.recorded_states = recorded_states + self.current_frame = 0 + self.max_frames = len(recorded_states) + self.is_playing = True + self.loop_count = 0 + + if self.max_frames > 0: + initial_state = recorded_states[0] + self.x = initial_state['x'] + self.y = initial_state['y'] + self.grid_x = initial_state['grid_x'] + self.grid_y = initial_state['grid_y'] + self.direction = initial_state['direction'] + else: + self.x = 0 + self.y = 0 + self.grid_x = 0 + self.grid_y = 0 + self.direction = RIGHT + + self.frame_timer = 0 + self.frame_duration = 1.0 / 60.0 + + def update(self, dt): + if not self.is_playing: + return + + self.frame_timer += dt + + while self.frame_timer >= self.frame_duration and self.current_frame < self.max_frames: + self.advance_frame() + self.frame_timer -= self.frame_duration + + def advance_frame(self): + if self.current_frame >= self.max_frames: + self.is_playing = False + return + + state = self.recorded_states[self.current_frame] + self.x = state['x'] + self.y = state['y'] + self.grid_x = state['grid_x'] + self.grid_y = state['grid_y'] + self.direction = state['direction'] + + self.current_frame += 1 + + if self.current_frame >= self.max_frames: + self.is_playing = False + self.loop_count += 1 + + def get_current_grid_position(self): + return (self.grid_x, self.grid_y) + + def is_active(self): + return self.is_playing or self.current_frame > 0 + + def draw(self, screen): + if not self.is_playing and self.current_frame >= self.max_frames: + return + + self.draw_trail(screen) + + shadow_surface = pygame.Surface((TILE_SIZE, TILE_SIZE), pygame.SRCALPHA) + + shadow_rect = pygame.Rect(4, 4, TILE_SIZE - 8, TILE_SIZE - 8) + pygame.draw.rect(shadow_surface, (100, 100, 255, 180), shadow_rect) + + center_x = TILE_SIZE // 2 + center_y = TILE_SIZE // 2 + eye_offset = 6 + + if self.direction == RIGHT: + eye_x = center_x + eye_offset + elif self.direction == LEFT: + eye_x = center_x - eye_offset + else: + eye_x = center_x + + if self.direction == DOWN: + eye_y = center_y + eye_offset + elif self.direction == UP: + eye_y = center_y - eye_offset + else: + eye_y = center_y + + pygame.draw.circle(shadow_surface, (255, 255, 255, 180), (eye_x, eye_y), 4) + + screen.blit(shadow_surface, (int(self.x), int(self.y))) + + def draw_trail(self, screen): + trail_length = min(self.current_frame, TRAIL_LENGTH) + start_frame = max(0, self.current_frame - trail_length) + + for i, frame_idx in enumerate(range(start_frame, self.current_frame)): + if frame_idx >= len(self.recorded_states): + continue + + state = self.recorded_states[frame_idx] + pos_x = int(state['x'] + TILE_SIZE // 2) + pos_y = int(state['y'] + TILE_SIZE // 2) + + alpha = int(50 + 100 * (i / trail_length)) if trail_length > 0 else 50 + size = int(3 + 3 * (i / trail_length)) if trail_length > 0 else 3 + + trail_surface = pygame.Surface((size * 2, size * 2), pygame.SRCALPHA) + pygame.draw.circle(trail_surface, (100, 100, 255, alpha), (size, size), size) + screen.blit(trail_surface, (pos_x - size, pos_y - size)) diff --git a/ui.py b/ui.py new file mode 100644 index 0000000..25bcc5d --- /dev/null +++ b/ui.py @@ -0,0 +1,115 @@ +import pygame +from constants import * + +class UIManager: + def __init__(self, screen): + self.screen = screen + self.font_large = pygame.font.Font(None, 48) + self.font_medium = pygame.font.Font(None, 36) + self.font_small = pygame.font.Font(None, 24) + + def draw(self, level, rewind_count, recorded_frames, max_frames, game_won, is_rewinding, rewind_progress): + self.draw_level_info(level) + self.draw_rewind_info(rewind_count, recorded_frames, max_frames) + + if is_rewinding: + self.draw_rewind_progress(rewind_progress) + + if game_won: + self.draw_win_screen() + + self.draw_controls_hint() + + def draw_level_info(self, level): + text = self.font_medium.render(f"关卡 {level + 1}", True, BLACK) + text_rect = text.get_rect() + text_rect.topright = (SCREEN_WIDTH - 20, 10) + self.screen.blit(text, text_rect) + + def draw_rewind_info(self, rewind_count, recorded_frames, max_frames): + count_text = self.font_small.render(f"回溯次数: {rewind_count}/{MAX_REWIND_COUNT}", True, BLACK) + count_rect = count_text.get_rect() + count_rect.topleft = (20, 10) + self.screen.blit(count_text, count_rect) + + bar_width = 200 + bar_height = 15 + bar_x = 20 + bar_y = 40 + + pygame.draw.rect(self.screen, DARK_GRAY, (bar_x, bar_y, bar_width, bar_height)) + + progress = recorded_frames / max_frames if max_frames > 0 else 0 + fill_width = int(bar_width * progress) + + if progress > 0.3: + bar_color = GREEN + elif progress > 0.1: + bar_color = YELLOW + else: + bar_color = RED + + pygame.draw.rect(self.screen, bar_color, (bar_x, bar_y, fill_width, bar_height)) + pygame.draw.rect(self.screen, BLACK, (bar_x, bar_y, bar_width, bar_height), 2) + + frame_text = self.font_small.render(f"记录帧: {recorded_frames}", True, BLACK) + frame_rect = frame_text.get_rect() + frame_rect.topleft = (bar_x + bar_width + 10, bar_y) + self.screen.blit(frame_text, frame_rect) + + def draw_rewind_progress(self, progress): + overlay_width = 300 + overlay_height = 60 + overlay_x = (SCREEN_WIDTH - overlay_width) // 2 + overlay_y = (SCREEN_HEIGHT - overlay_height) // 2 - 100 + + overlay_surface = pygame.Surface((overlay_width, overlay_height)) + overlay_surface.fill(DARK_GRAY) + overlay_surface.set_alpha(200) + self.screen.blit(overlay_surface, (overlay_x, overlay_y)) + + text = self.font_medium.render("时间回溯中...", True, BLUE) + text_rect = text.get_rect() + text_rect.centerx = overlay_x + overlay_width // 2 + text_rect.y = overlay_y + 10 + self.screen.blit(text, text_rect) + + bar_width = 250 + bar_height = 10 + bar_x = overlay_x + (overlay_width - bar_width) // 2 + bar_y = overlay_y + 40 + + pygame.draw.rect(self.screen, BLACK, (bar_x, bar_y, bar_width, bar_height)) + + fill_width = int(bar_width * min(1.0, progress)) + pygame.draw.rect(self.screen, BLUE, (bar_x, bar_y, fill_width, bar_height)) + + def draw_win_screen(self): + overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) + overlay.fill((0, 0, 0, 150)) + self.screen.blit(overlay, (0, 0)) + + win_text = self.font_large.render("关卡完成!", True, GREEN) + win_rect = win_text.get_rect() + win_rect.center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - 50) + self.screen.blit(win_text, win_rect) + + hint_text = self.font_medium.render("按空格键进入下一关", True, WHITE) + hint_rect = hint_text.get_rect() + hint_rect.center = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 20) + self.screen.blit(hint_text, hint_rect) + + def draw_controls_hint(self): + hints = [ + "方向键: 移动", + f"R: 时间回溯", + "ESC: 重置关卡" + ] + + y_offset = SCREEN_HEIGHT - 30 * len(hints) - 10 + + for i, hint in enumerate(hints): + text = self.font_small.render(hint, True, DARK_GRAY) + text_rect = text.get_rect() + text_rect.bottomleft = (20, y_offset + 30 * (i + 1)) + self.screen.blit(text, text_rect)