juego
def init(self, x, y, w, h): self.rect = pygame.Rect(x, y, w, h)
def draw(self, surface, camera): x = self.rect.x - camera.x y = self.rect.y - camera.y
pygame.draw.rect(surface, YELLOW_WALL, (x, y, self.rect.w, self.rect.h))
pygame.draw.rect(surface, YELLOW_LIGHT, (x, y, self.rect.w, 5))
pygame.draw.rect(surface, (70, 60, 35), (x, y + self.rect.h - 4, self.rect.w, 4))
# Líneas sutiles para simular papel pintado / paneles.
for lx in range(x + 24, x + self.rect.w, 48):
pygame.draw.line(surface, (130, 115, 55), (lx, y + 6), (lx, y + self.rect.h - 6), 1)
def __init__(self, x, y):
self.rect = pygame.Rect(x, y, 22, 22)
self.collected = False
self.float_phase = random.random() * math.tau
def draw(self, surface, camera, player_center): if self.collected: return
# Las notas están "ocultas": solo se muestran claramente si estás cerca.
if distance(self.rect.center, player_center) > 430:
return
x = self.rect.x - camera.x
y = self.rect.y - camera.y + math.sin(pygame.time.get_ticks() * 0.005 + self.float_phase) * 3
pygame.draw.rect(surface, (235, 230, 190), (x, y, self.rect.w, self.rect.h))
pygame.draw.rect(surface, (70, 55, 30), (x, y, self.rect.w, self.rect.h), 2)
pygame.draw.line(surface, (100, 80, 45), (x + 5, y + 8), (x + 17, y + 8), 1)
pygame.draw.line(surface, (100, 80, 45), (x + 5, y + 13), (x + 15, y + 13), 1)def init(self, x, y, color, name="Actor"): self.pos = pygame.Vector2(x, y) self.rect = pygame.Rect(x, y, 30, 48)
self.vel = pygame.Vector2(0, 0)
self.color = color
self.name = name
self.health = 100
self.alive = True
self.on_ground = False
self.on_wall = 0 # -1 pared izquierda, 1 pared derecha
self.jumps_left = 1
self.facing = 1
self.control_lock = 0.0
self.hit_flash = 0.0def center(self): return pygame.Vector2(self.rect.center)
def damage(self, amount): if not self.alive: return
self.health -= amount
self.hit_flash = 0.12
if self.health <= 0:
self.health = 0
self.alive = Falsedef jump(self): if not self.alive: return
# Salto normal
if self.on_ground:
self.vel.y = -JUMP_POWER
self.on_ground = False
self.jumps_left = 1
# Wall jump: empuja en dirección contraria a la pared.
elif self.on_wall != 0:
self.vel.y = -JUMP_POWER * 0.92
self.vel.x = -self.on_wall * WALL_JUMP_PUSH
self.jumps_left = 1
self.control_lock = 0.16
# Doble salto
elif self.jumps_left > 0:
self.vel.y = -DOUBLE_JUMP_POWER
self.jumps_left -= 1def detect_wall(self, platforms): self.on_wall = 0
left_probe = self.rect.move(-2, 0)
right_probe = self.rect.move(2, 0)
for platform in platforms:
if left_probe.colliderect(platform.rect):
self.on_wall = -1
return
if right_probe.colliderect(platform.rect):
self.on_wall = 1
returndef apply_physics(self, platforms): if not self.alive: return
self.on_ground = False
# Movimiento horizontal
self.pos.x += self.vel.x
self.rect.x = round(self.pos.x)
for platform in platforms:
if self.rect.colliderect(platform.rect):
if self.vel.x > 0:
self.rect.right = platform.rect.left
self.on_wall = 1
elif self.vel.x < 0:
self.rect.left = platform.rect.right
self.on_wall = -1
self.pos.x = self.rect.x
self.vel.x = 0
self.detect_wall(platforms)
# Gravedad y wall slide
self.vel.y = min(MAX_FALL_SPEED, self.vel.y + GRAVITY)
if self.on_wall != 0 and self.vel.y > WALL_SLIDE_SPEED:
self.vel.y = WALL_SLIDE_SPEED
# Movimiento vertical
self.pos.y += self.vel.y
self.rect.y = round(self.pos.y)
for platform in platforms:
if self.rect.colliderect(platform.rect):
if self.vel.y > 0:
self.rect.bottom = platform.rect.top
self.on_ground = True
self.jumps_left = 1
elif self.vel.y < 0:
self.rect.top = platform.rect.bottom
self.pos.y = self.rect.y
self.vel.y = 0
# Límites del mundo
if self.rect.left < 0:
self.rect.left = 0
self.pos.x = self.rect.x
self.vel.x = 0
if self.rect.right > WORLD_WIDTH:
self.rect.right = WORLD_WIDTH
self.pos.x = self.rect.x
self.vel.x = 0
if self.rect.top < 0:
self.rect.top = 0
self.pos.y = self.rect.y
self.vel.y = 0
if self.rect.bottom > WORLD_HEIGHT:
self.rect.bottom = WORLD_HEIGHT
self.pos.y = self.rect.y
self.vel.y = 0
self.on_ground = True
self.jumps_left = 1def draw(self, surface, camera): if not self.alive: return
x = self.rect.x - camera.x
y = self.rect.y - camera.y
color = self.color
if self.hit_flash > 0:
color = (255, 70, 70)
pygame.draw.rect(surface, color, (x, y, self.rect.w, self.rect.h), border_radius=4)
# Ojo / dirección.
eye_x = x + 20 if self.facing > 0 else x + 7
pygame.draw.circle(surface, (10, 10, 20), (int(eye_x), int(y + 14)), 4)
# Indicador visual de wall slide.
if self.on_wall != 0 and not self.on_ground:
spark_x = x - 5 if self.on_wall < 0 else x + self.rect.w + 2
pygame.draw.line(surface, (255, 235, 120), (spark_x, y + 12), (spark_x, y + 32), 2)
def update(self, keys, platforms, dt):
if not self.alive:
return
if self.hit_flash > 0:
self.hit_flash -= dt
if self.control_lock > 0:
self.control_lock -= dt
else:
direction = 0
if keys[pygame.K_a] or keys[pygame.K_LEFT]:
direction -= 1
if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
direction += 1
running = keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]
speed = RUN_SPEED if running else WALK_SPEED
if direction != 0:
self.facing = direction
self.vel.x = direction * speed
else:
self.vel.x *= 0.78
if abs(self.vel.x) < 0.1:
self.vel.x = 0
self.apply_physics(platforms)
def __init__(self, x, y, bot_id):
super().__init__(x, y, BOT_COLOR, f"Bot {bot_id}")
self.jump_cooldown = random.uniform(0.2, 0.8)
self.wander_timer = 0
self.wander_dir = random.choice([-1, 1])def update(self, game, dt): if not self.alive: return
if self.hit_flash > 0:
self.hit_flash -= dt
self.jump_cooldown -= dt
self.wander_timer -= dt
center = self.center()
zone_center = game.zone_center
zone_distance = center.distance_to(zone_center)
entity_distance = center.distance_to(game.entity.center())
direction = 0
# Prioridad 1: si está fuera o cerca del borde, corre hacia la zona segura.
if zone_distance > game.zone_radius * 0.72:
direction = sign(zone_center.x - center.x)
# Prioridad 2: si la Entidad está cerca, intenta alejarse.
if entity_distance < 330:
direction = sign(center.x - game.entity.center().x)
# Si no hay amenaza clara, deambula.
if direction == 0:
if self.wander_timer <= 0:
self.wander_timer = random.uniform(1.2, 3.0)
self.wander_dir = random.choice([-1, 1])
direction = self.wander_dir
self.facing = direction if direction != 0 else self.facing
target_speed = RUN_SPEED * 0.92 if zone_distance > game.zone_radius else WALK_SPEED
self.vel.x += direction * 0.55
self.vel.x = clamp(self.vel.x, -target_speed, target_speed)
# Detección básica de obstáculo / precipicio.
ahead = self.rect.move(direction * 22, 0)
obstacle_ahead = any(ahead.colliderect(p.rect) for p in game.platforms)
foot_probe = pygame.Rect(
self.rect.centerx + direction * 38 - 5,
self.rect.bottom + 5,
10,
60
)
ground_ahead = any(foot_probe.colliderect(p.rect) for p in game.platforms)
must_jump = obstacle_ahead or not ground_ahead
# Si el centro de la zona está bastante más alto, intenta subir.
if zone_center.y < center.y - 120:
must_jump = True
if must_jump and self.jump_cooldown <= 0:
self.jump()
self.jump_cooldown = random.uniform(0.35, 0.75)
if self.on_wall != 0 and self.jump_cooldown <= 0:
self.jump()
self.jump_cooldown = random.uniform(0.45, 0.85)
self.apply_physics(game.platforms)
def __init__(self, x, y):
self.pos = pygame.Vector2(x, y)
self.rect = pygame.Rect(x, y, 42, 70)
self.target = None
self.retarget_timer = 0
self.phase = random.random() * math.taudef center(self): return pygame.Vector2(self.rect.center)
def update(self, game, dt): alive_targets = [game.player] + [bot for bot in game.bots if bot.alive] alive_targets = [actor for actor in alive_targets if actor.alive]
if not alive_targets:
return
self.retarget_timer -= dt
if self.target not in alive_targets or self.retarget_timer <= 0:
self.target = random.choice(alive_targets)
self.retarget_timer = random.uniform(2.0, 5.0)
target_center = self.target.center()
to_target = target_center - self.center()
if to_target.length() > 0:
to_target = to_target.normalize()
# Movimiento flotante e irregular.
speed = 125 + math.sin(pygame.time.get_ticks() * 0.002 + self.phase) * 35
self.pos += to_target * speed * dt
self.rect.x = round(self.pos.x)
self.rect.y = round(self.pos.y)
# Daño por contacto.
for actor in alive_targets:
if self.rect.colliderect(actor.rect):
actor.damage(35 * dt)def draw(self, surface, camera): x = self.rect.x - camera.x y = self.rect.y - camera.y
jitter = random.randint(-2, 2)
# Sombra/glitch.
pygame.draw.rect(surface, (90, 0, 90), (x + jitter, y, self.rect.w, self.rect.h), 2)
pygame.draw.rect(surface, ENTITY_COLOR, (x, y, self.rect.w, self.rect.h), border_radius=8)
# Ojos mínimos.
pygame.draw.circle(surface, (255, 40, 40), (int(x + 13), int(y + 20)), 4)
pygame.draw.circle(surface, (255, 40, 40), (int(x + 29), int(y + 20)), 4)
def follow(self, target_rect):
target_x = target_rect.centerx - SCREEN_WIDTH // 2
target_y = target_rect.centery - SCREEN_HEIGHT // 2
self.x += (target_x - self.x) * 0.10
self.y += (target_y - self.y) * 0.10
self.x = clamp(self.x, 0, WORLD_WIDTH - SCREEN_WIDTH)
self.y = clamp(self.y, 0, WORLD_HEIGHT - SCREEN_HEIGHT)
self.camera = Camera()
self.running = True
self.flashlight_mask = self.create_flashlight_mask(300)
self.reset()def reset(self): self.state = "menu"
self.platforms = []
self.notes = []
self.bots = []
self.zone_elapsed = 0
self.zone_start_center = pygame.Vector2(WORLD_WIDTH / 2, WORLD_HEIGHT / 2)
self.zone_center = self.zone_start_center.copy()
margin = 600
self.zone_target = pygame.Vector2(
random.randint(margin, WORLD_WIDTH - margin),
random.randint(margin, WORLD_HEIGHT - margin)
)
self.zone_radius = ZONE_START_RADIUS
self.generate_map()
self.player = Player(160, WORLD_HEIGHT - 160)
spawn_platforms = [p for p in self.platforms if p.rect.w > 120]
random.shuffle(spawn_platforms)
for i in range(5):
platform = spawn_platforms[i + 1]
x = random.randint(platform.rect.left + 20, platform.rect.right - 60)
y = platform.rect.top - 52
self.bots.append(Bot(x, y, i + 1))
self.entity = Entity(WORLD_WIDTH // 2, WORLD_HEIGHT // 2)
self.message = "Recoge notas, sobrevive a la niebla y evita a la Entidad."
self.game_result = ""def generate_map(self): """ Generación semi-aleatoria de plataformas. Simula un nivel laberíntico de oficinas abandonadas. """
rng = random.Random()
# Suelo base.
self.platforms.append(Platform(0, WORLD_HEIGHT - 60, WORLD_WIDTH, 80))
# Capas de plataformas.
y = WORLD_HEIGHT - 260
while y > 320:
x = 0
while x < WORLD_WIDTH:
segment_width = rng.randint(180, 520)
gap = rng.randint(80, 230)
if rng.random() > 0.16:
self.platforms.append(Platform(x, y, segment_width, 24))
# Paredes verticales para wall slide / wall jump.
if rng.random() < 0.38:
wall_h = rng.randint(90, 220)
wall_x = x + rng.randint(20, max(25, segment_width - 45))
self.platforms.append(Platform(wall_x, y - wall_h, 28, wall_h))
x += segment_width + gap
y -= rng.randint(185, 245)
# Algunas columnas altas para reforzar el parkour.
for _ in range(22):
x = rng.randint(250, WORLD_WIDTH - 350)
y = rng.randint(420, WORLD_HEIGHT - 420)
h = rng.randint(100, 280)
self.platforms.append(Platform(x, y, 34, h))
# Notas ocultas sobre plataformas aleatorias.
candidates = [p for p in self.platforms if p.rect.w > 100 and p.rect.y < WORLD_HEIGHT - 90]
random.shuffle(candidates)
for platform in candidates[:14]:
nx = random.randint(platform.rect.left + 20, platform.rect.right - 40)
ny = platform.rect.top - 30
self.notes.append(Note(nx, ny))
# Luces de fondo parpadeantes.
self.background_lights = []
for ly in range(160, WORLD_HEIGHT - 120, 260):
for lx in range(120, WORLD_WIDTH - 120, 520):
if rng.random() < 0.75:
self.background_lights.append((
lx + rng.randint(-45, 45),
ly + rng.randint(-18, 18),
rng.random() * math.tau
))def create_flashlight_mask(self, radius): """ Crea una máscara radial para simular linterna. En un juego final puedes cambiar esto por un shader o sprite de luz. """
size = radius * 2
mask = pygame.Surface((size, size), pygame.SRCALPHA)
for y in range(size):
for x in range(size):
dx = x - radius
dy = y - radius
dist = math.sqrt(dx * dx + dy * dy)
if dist < radius:
strength = 1.0 - dist / radius
alpha_subtract = int(240 * (strength ** 1.7))
mask.set_at((x, y), (0, 0, 0, alpha_subtract))
else:
mask.set_at((x, y), (0, 0, 0, 0))
return maskdef handle_events(self): for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
self.running = False
if self.state == "menu":
if event.key in (pygame.K_RETURN, pygame.K_SPACE):
self.state = "playing"
elif self.state == "playing":
if event.key in (pygame.K_SPACE, pygame.K_w, pygame.K_UP):
self.player.jump()
elif self.state == "gameover":
if event.key == pygame.K_r:
self.reset()
# Soporte simple para prototipo táctil: click/tap = saltar o empezar.
if event.type == pygame.MOUSEBUTTONDOWN:
if self.state == "menu":
self.state = "playing"
elif self.state == "playing":
self.player.jump()
elif self.state == "gameover":
self.reset()def update_zone(self, dt): self.zone_elapsed += dt
t = clamp(self.zone_elapsed / ZONE_CLOSE_TIME, 0, 1)
smooth = t * t * (3 - 2 * t)
self.zone_center = self.zone_start_center.lerp(self.zone_target, smooth)
self.zone_radius = ZONE_START_RADIUS + (ZONE_FINAL_RADIUS - ZONE_START_RADIUS) * smooth
actors = [self.player] + self.bots
for actor in actors:
if not actor.alive:
continue
d = actor.center().distance_to(self.zone_center)
if d > self.zone_radius:
extra = (d - self.zone_radius) * 0.006
actor.damage((ZONE_DAMAGE_PER_SECOND + extra) * dt)def update_notes(self): for note in self.notes: if not note.collected and self.player.rect.colliderect(note.rect): note.collected = True self.player.mystery_points += 1 self.message = f"Nota encontrada: misterio +1 ({self.player.mystery_points})"
def update(self, dt): if self.state != "playing": return
keys = pygame.key.get_pressed()
self.player.update(keys, self.platforms, dt)
for bot in self.bots:
bot.update(self, dt)
self.entity.update(self, dt)
self.update_zone(dt)
self.update_notes()
self.camera.follow(self.player.rect)
alive_actors = [actor for actor in [self.player] + self.bots if actor.alive]
if not self.player.alive:
self.state = "gameover"
self.game_result = "Has sido eliminado en los Backrooms."
elif len(alive_actors) == 1 and alive_actors[0] == self.player:
self.state = "gameover"
self.game_result = "Sobreviviste. Pero la salida aún no existe."def draw_background(self): screen.fill((142, 126, 58))
cam_x = int(self.camera.x)
cam_y = int(self.camera.y)
# Patrón de paredes amarillas.
for x in range(-cam_x % 120, SCREEN_WIDTH, 120):
pygame.draw.line(screen, (120, 105, 50), (x, 0), (x, SCREEN_HEIGHT), 1)
for y in range(-cam_y % 90, SCREEN_HEIGHT, 90):
pygame.draw.line(screen, (175, 155, 70), (0, y), (SCREEN_WIDTH, y), 1)
# Luces fluorescentes parpadeantes.
time = pygame.time.get_ticks() * 0.005
for lx, ly, phase in self.background_lights:
sx = lx - self.camera.x
sy = ly - self.camera.y
if -120 < sx < SCREEN_WIDTH + 120 and -80 < sy < SCREEN_HEIGHT + 80:
intensity = 90 + int(math.sin(time + phase) * 45)
intensity = clamp(intensity, 35, 140)
light_surface = pygame.Surface((110, 34), pygame.SRCALPHA)
light_surface.fill((255, 245, 165, intensity))
pygame.draw.rect(light_surface, (250, 235, 160, 230), (12, 12, 86, 6))
screen.blit(light_surface, (sx - 55, sy - 17))def draw_zone(self): center_screen = ( int(self.zone_center.x - self.camera.x), int(self.zone_center.y - self.camera.y) )
radius = int(self.zone_radius)
# Borde de zona segura.
if radius < 3000:
pygame.draw.circle(screen, (130, 0, 180), center_screen, radius, 5)
pygame.draw.circle(screen, (220, 40, 255), center_screen, radius, 1)
# Si el jugador está fuera, la pantalla se contamina.
if self.player.center().distance_to(self.zone_center) > self.zone_radius:
fog = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
pulse = 75 + int(math.sin(pygame.time.get_ticks() * 0.012) * 35)
fog.fill((35, 0, 45, pulse))
screen.blit(fog, (0, 0))def draw_world(self): self.draw_background() self.draw_zone()
view = pygame.Rect(self.camera.x, self.camera.y, SCREEN_WIDTH, SCREEN_HEIGHT).inflate(200, 200)
for platform in self.platforms:
if platform.rect.colliderect(view):
platform.draw(screen, self.camera)
for note in self.notes:
note.draw(screen, self.camera, self.player.center())
for bot in self.bots:
bot.draw(screen, self.camera)
self.player.draw(screen, self.camera)
self.entity.draw(screen, self.camera)def draw_flashlight(self): darkness = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
flicker = 225 + random.randint(-8, 12)
darkness.fill((0, 0, 0, flicker))
player_screen_center = (
int(self.player.rect.Source: pygame/pygame