So my issue is with the centipede class where the first segment is the head and then subsequent segments are the body. The subsequent segments take the position of the one in front so you get the basic look of a snake game. Problem is that they don't stay consistent distance apart (SEGMENT SIZE), and tend to overlap or space a bit too much and sometimes second segment is on top of head segment about half way down the screen. The other issue is when the centipede is hit by a bullet (in the Game events section). It splits into a front and back section where the front continues on as normal, just shorter, and the back segment becomes its own centipede with new head. Problem is it mostly works, but I get rogue segments that are not following the rest of the centipede properly or don't turn into a head if the only segment left (you can see from the debug red lines of segment pieces suppose to be together). Any suggestion on how to fix or make moving and splitting more concise? import pygame as pg import os import sys import random SCREEN_WIDTH = 600 SCREEN_HEIGHT = 800 SEGMENT_SIZE = 20 class Player(pg.sprite.Sprite): def __init__(self, game, level, pos): super().__init__() self.game = game self.level = level self.image = pg.Surface((SEGMENT_SIZE, SEGMENT_SIZE), pg.SRCALPHA) self.image.fill((255, 0, 0)) self.rect = self.image.get_rect(center=pos) self.speed = 300 # Adjusted speed to work with delta time (in pixels per second) self.can_shoot = True # Track whether the player can shoot a bullet self.mask = pg.mask.from_surface(self.image) def update(self, delta_time): # Initialize movement vector movement = pg.math.Vector2(0, 0) # Handle keyboard input keys = pg.key.get_pressed() if keys[pg.K_LEFT] and self.rect.left > 5: movement.x -= 1 if keys[pg.K_RIGHT] and self.rect.right <= (SCREEN_WIDTH - 5): movement.x += 1 if keys[pg.K_UP] and self.rect.top > (SCREEN_HEIGHT - (SEGMENT_SIZE * 6)): movement.y -= 1 if keys[pg.K_DOWN] and self.rect.bottom < (SCREEN_HEIGHT - SEGMENT_SIZE): movement.y += 1 if keys[pg.K_SPACE] and self.can_shoot: self.can_shoot = False self.game.bullets.add(Bullet(self.game, self.rect.midtop)) # Handle joystick input for joystick in self.game.joysticks: hat_pos = joystick.get_hat(0) movement.x += hat_pos[0] movement.y -= hat_pos[1] for joystick in self.game.joysticks: if joystick.get_button(0): if self.can_shoot: self.game.bullets.add(Bullet(self, self.rect.midtop)) self.can_shoot = False # Normalize diagonal movement if movement.length() > 0: movement = movement.normalize() # Apply movement self.rect.x += int(movement.x * self.speed * delta_time) self.rect.y += int(movement.y * self.speed * delta_time) # Enforce screen boundaries if self.rect.left < 5: self.rect.left = 5 if self.rect.right > (SCREEN_WIDTH - 5): self.rect.right = (SCREEN_WIDTH - 5) if self.rect.y > (SCREEN_HEIGHT - SEGMENT_SIZE): self.rect.y = (SCREEN_HEIGHT - SEGMENT_SIZE) if self.rect.y < (SCREEN_HEIGHT - (SEGMENT_SIZE * 6)): self.rect.y = (SCREEN_HEIGHT - (SEGMENT_SIZE * 6)) def draw(self, screen): screen.blit(self.image, self.rect) class Bullet(pg.sprite.Sprite): def __init__(self, game, pos): super().__init__() self.game = game self.image = pg.Surface((4, 10)) self.image.fill((25, 255, 255)) self.rect = self.image.get_rect(center=pos) self.speed = 1250 # Pixels per second def update(self, delta_time): self.rect.y -= int(self.speed * delta_time) class Centipede(pg.sprite.Sprite): def __init__(self, game, level, x, y, num_segments, base_speed): super().__init__() self.game = game self.level = level self.base_speed = base_speed self.segments = [] # Tracks the individual segments of the centipede for movement and collision self.new_centipede_created = False self.surface = pg.Surface((20, 20), pg.SRCALPHA) # Create all centipede segments for i in range(num_segments): segment_x = x - i * SEGMENT_SIZE is_head = (i == 0) segment = { "rect": pg.Rect(segment_x, y, SEGMENT_SIZE, SEGMENT_SIZE), "speed": base_speed, "x_direction": 1, "y_direction": SEGMENT_SIZE, # Controls vertical movement "move_down_next_frame": False, "move_up_next_frame": False, # Added for upward movement logic "poisoned": False, "is_head": is_head, "current_frame": 0, "current_frame_poisoned": 0, "last_update_time": pg.time.get_ticks(), "down_time": pg.time.get_ticks() } self.segments.append(segment) def update_speed(self): """Updates the speed of the centipede based on the number of segments.""" if len(self.segments) == 1: speed = self.base_speed + 2 elif 2 <= len(self.segments) < 6: speed = self.base_speed + 1 else: speed = self.base_speed for segment in self.segments: segment["speed"] = speed def update(self, mushrooms, delta_time): """Updates the movement and behavior of the centipede.""" self.update_speed() for i, segment in enumerate(self.segments): now = pg.time.get_ticks() # Manage animation frames if not segment["poisoned"]: if segment["move_down_next_frame"]: segment["rect"].y += segment["y_direction"] segment["move_down_next_frame"] = False else: segment["rect"].y += 5 # Reverse y_direction logic if segment["y_direction"] < 0: # Moving up segment["poisoned"] = False # Reset poisoned status when moving up if segment["rect"].y <= SCREEN_HEIGHT - 120: for mushroom in mushrooms: if segment["rect"].colliderect(mushroom.rect): segment["y_direction"] *= -1 # Reverse to move down when hitting a mushroom segment["move_up_next_frame"] = True break if segment["rect"].left <= 0 or segment["rect"].right >= SCREEN_WIDTH: segment["y_direction"] *= -1 # Reverse to move down when hitting screen edges segment["move_up_next_frame"] = True elif segment["rect"].y < 0: # Prevent going off-screen vertically segment["y_direction"] *= -1 elif segment["rect"].y >= SCREEN_HEIGHT - SEGMENT_SIZE * 2: # At the bottom edge segment["y_direction"] *= -1 # Reverse to upward movement # **Per-Frame Downward Movement ("move_down_next_frame")** if segment["move_down_next_frame"]: # Move downwards based on this flag segment["rect"].y += segment["y_direction"] segment["move_down_next_frame"] = False # Horizontal movement logic if not segment["poisoned"]: segment["rect"].x += segment["speed"] * segment["x_direction"] else: down_now = pg.time.get_ticks() if down_now - segment["down_time"] > 40: # Reverse horizontal direction at intervals while poisoned segment["down_time"] = down_now segment["x_direction"] *= -1 # Flip direction segment["rect"].x += segment["speed"] * segment["x_direction"] # Screen edge collisions if segment["rect"].left <= 0 and segment["x_direction"] == -1: segment["x_direction"] = 1 segment["move_down_next_frame"] = True elif segment["rect"].right >= SCREEN_WIDTH and segment["x_direction"] == 1: segment["x_direction"] = -1 segment["move_down_next_frame"] = True # Maintain spacing between segments if i > 0: prev_segment = self.segments[i - 1] if segment["x_direction"] == prev_segment["x_direction"]: if segment["x_direction"] == 1: segment["rect"].x = max(segment["rect"].x, prev_segment["rect"].x - SEGMENT_SIZE) else: segment["rect"].x = min(segment["rect"].x, prev_segment["rect"].x + SEGMENT_SIZE) def draw(self, screen): """Draws each centipede segment on the screen.""" for segment in self.segments: is_head = segment["is_head"] color = (255, 255, 255) if is_head else (255, 255, 0) self.surface.fill((0, 0, 0, 0)) # Clear surface pg.draw.circle(self.surface, color, (10, 10), 10) # Draw circle on surface screen.blit(self.surface, segment["rect"].topleft) # debuging, checking body vs head alignment for i in range(1, len(self.segments)): pg.draw.line(screen, (255, 0, 0), self.segments[i - 1]["rect"].center, self.segments["rect"].center,2) class Mushroom(pg.sprite.Sprite): def __init__(self, game, level, x, y): super().__init__() self.game = game self.level = level self.full_height = SEGMENT_SIZE self.image = pg.Surface((20, 20)) self.image.fill((0, 255, 0)) self.health = 0 # Mushrooms take 3 hits to destroy (start at 0 and move up to 3) self.rect = self.image.get_rect(topleft=(x, y)) self.is_poisoned = False self.mask = pg.mask.from_surface(self.image) def update_mushroom(self): if not self.is_poisoned: self.image.fill((0, 255, 0)) else: self.image.fill((201, 137, 50)) def take_hit(self): # Increase health when hit and update mushroom sprite self.health += 1 if self.health < 4: self.update_mushroom() else: self.kill() class Game: def __init__(self): self.round = 0 self.music_round = 0 self.base_centipede_speed = 2 self.total_centipedes = 1 self.length = 13 self.loners = -1 self.life = 2 self.previous_score = 0 self.sprite_level = 0 pg.init() pg.joystick.init() self.joysticks = [pg.joystick.Joystick(x) for x in range(pg.joystick.get_count())] pg.key.set_repeat(10, 50) pg.display.set_caption("CENTIPEDE") os.environ['SDL_VIDEO_WINDOW_POS'] = '350, 40' self.VIRTUAL_WIDTH = 600 self.VIRTUAL_HEIGHT = 800 # Get display resolution (e.g., full screen or windowed) self.SCREEN_WIDTH, self.SCREEN_HEIGHT = 660, 880 # Replace with desired resolution self.scale_x = self.SCREEN_WIDTH / self.VIRTUAL_WIDTH self.scale_y = self.SCREEN_HEIGHT / self.VIRTUAL_HEIGHT # Initialize display and virtual surfaces self.screen = pg.display.set_mode((self.SCREEN_WIDTH, self.SCREEN_HEIGHT), pg.RESIZABLE) self.virtual_surface = pg.Surface((self.VIRTUAL_WIDTH, self.VIRTUAL_HEIGHT)) self.clock = pg.time.Clock() self.font = pg.font.Font(None, 36) self.paused = False self.start_of_game = True self.last_update = pg.time.get_ticks() self.load_data() def is_position_valid(self, x, y, mushrooms): """Check if the position (x, y) is valid (not overlapping/touching existing mushrooms).""" for mushroom in mushrooms: if mushroom.rect.colliderect(pg.Rect(x, y, SEGMENT_SIZE, SEGMENT_SIZE)): return False return True def advance_level(self): self.round += 1 self.sprite_level += 1 if self.sprite_level > 14: self.sprite_level = 1 self.sprite_round = "level{}".format(self.sprite_level) if self.round > 1: for mushroom in self.mushrooms: mushroom.kill() self.mushrooms.add(Mushroom(self, self.sprite_round, mushroom.rect.x, mushroom.rect.y)) mushroom.update_mushroom() self.base_centipede_speed += 1 if self.base_centipede_speed > 4: self.base_centipede_speed = 4 self.length -= 1 self.loners += 1 if self.length < 0: self.length = 12 self.loners = 0 def lose_life(self): self.life -= 1 if self.life < 0: self.quit() # self.game_over_screen() else: self.round -= 1 self.length += 1 self.loners -= 1 self.base_centipede_speed -= 1 self.sprite_level -= 1 self.end_round_mushroom_cleanup() def load_data(self): # load sprite images self.dim_screen = pg.Surface(self.screen.get_size()).convert_alpha() self.dim_screen.fill((0, 0, 0, 120)) def adjusted_mouse_position(self, mouse_pos): """Scale mouse position based on the virtual resolution.""" scaled_x = mouse_pos[0] / self.scale_x scaled_y = mouse_pos[1] / self.scale_y return scaled_x, scaled_y def new(self): self.bullets = pg.sprite.Group() self.centipedes = pg.sprite.Group() # if first round if self.start_of_game: self.mushrooms = pg.sprite.Group() # Adding a few initial mushrooms ensuring no overlaps while len(self.mushrooms) < 40: x = random.randint(20, SCREEN_WIDTH - SEGMENT_SIZE) if x % 20 != 0: x = 20 * round(x/20) y = random.randint(40, SCREEN_HEIGHT - (SEGMENT_SIZE * 8)) if y % 20 != 0: y = 20 * round(y/20) if self.is_position_valid(x, y, self.mushrooms): self.mushrooms.add(Mushroom(self, 'level1', x, y)) self.advance_level() self.last_update_time = pg.time.get_ticks() self.player = Player(self, self.sprite_round, (SCREEN_WIDTH // 2, SCREEN_HEIGHT - 50)) # Centipede initialization if self.round % 5 == 0: self.total_centipedes += 1 if self.total_centipedes > 4: self.total_centipedes = 1 self.base_centipede_speed += 1 for i in range(self.total_centipedes): x = random.randrange(SCREEN_WIDTH // 2, SCREEN_WIDTH - SEGMENT_SIZE, 20) y = random.randrange(-60, -40, 20) self.centipedes.add(Centipede(self, self.sprite_round, x, y, self.length, self.base_centipede_speed)) if self.loners > 0: for _ in range(self.loners): x = random.randrange(SCREEN_WIDTH // 2, SCREEN_WIDTH - SEGMENT_SIZE, 20) y = random.randrange(-60, -40, 20) self.centipedes.add(Centipede(self, self.sprite_round, x, y, 1, self.base_centipede_speed)) self.start_of_game = False self.run() def run(self): self.playing = True while self.playing: self.mx, self.my = pg.mouse.get_pos() self.dt = self.clock.tick(60) / 1000 self.events() self.update() self.draw() def quit(self): pg.quit() sys.exit() def update(self): if not self.paused: if not getattr(self, "enemies_paused", False): # Update player self.player.update(self.dt) for mushroom in self.mushrooms: if self.player.rect.colliderect(mushroom.rect): # Adjust player position to prevent overlap if self.player.rect.right > mushroom.rect.left and self.player.rect.left < mushroom.rect.left: self.player.rect.right = mushroom.rect.left elif self.player.rect.left < mushroom.rect.right and self.player.rect.right > mushroom.rect.right: self.player.rect.left = mushroom.rect.right if self.player.rect.bottom > mushroom.rect.top and self.player.rect.top < mushroom.rect.top: self.player.rect.bottom = mushroom.rect.top elif self.player.rect.top < mushroom.rect.bottom and self.player.rect.bottom > mushroom.rect.bottom: self.player.rect.top = mushroom.rect.bottom # Update bullets with delta time for bullet in self.bullets: bullet.update(self.dt) # Update centipedes, mushrooms self.mushrooms.update() self.centipedes.update(self.mushrooms, self.dt) def draw(self): pg.display.set_caption(f"FPS: {self.clock.get_fps():.2f}" + " " + f"Delta: {self.dt * 1000:.2f}") # Clear virtual surface self.virtual_surface.fill((0, 0, 0)) # Draw everything on the virtual surface self.player.draw(self.virtual_surface) self.bullets.draw(self.virtual_surface) self.mushrooms.draw(self.virtual_surface) for centipede in self.centipedes: centipede.draw(self.virtual_surface) # Game pause overlay if self.paused: self.virtual_surface.blit(self.dim_screen, (0, 0)) # Scale virtual surface to the actual screen size scaled_surface = pg.transform.scale(self.virtual_surface, (self.SCREEN_WIDTH, self.SCREEN_HEIGHT)) self.screen.blit(scaled_surface, (0, 0)) # Calculate the largest fit while maintaining aspect ratio scale = min(self.SCREEN_WIDTH / self.VIRTUAL_WIDTH, self.SCREEN_HEIGHT / self.VIRTUAL_HEIGHT) scaled_width = int(self.VIRTUAL_WIDTH * scale) scaled_height = int(self.VIRTUAL_HEIGHT * scale) offset_x = (self.SCREEN_WIDTH - scaled_width) // 2 offset_y = (self.SCREEN_HEIGHT - scaled_height) // 2 # Scale virtual surface and add black bars scaled_surface = pg.transform.scale(self.virtual_surface, (scaled_width, scaled_height)) self.screen.fill((0, 0, 0)) # Black bars self.screen.blit(scaled_surface, (offset_x, offset_y)) pg.display.flip() def events(self): for event in pg.event.get(): if event.type == pg.QUIT: self.quit() if event.type == pg.KEYDOWN: if event.key == pg.K_ESCAPE: self.quit() if event.type == pg.VIDEORESIZE: # Adjust physical screen dimensions self.SCREEN_WIDTH, self.SCREEN_HEIGHT = event.size self.scale_x = self.SCREEN_WIDTH / self.VIRTUAL_WIDTH self.scale_y = self.SCREEN_HEIGHT / self.VIRTUAL_HEIGHT self.screen = pg.display.set_mode((self.SCREEN_WIDTH, self.SCREEN_HEIGHT), pg.RESIZABLE) if event.type == pg. KEYUP: if event.key == pg.K_p: self.paused = not self.paused if event.type == pg.JOYBUTTONDOWN: # 0=A, 1=B, 2=X, 3=Y, 4=FTL, 5=FTR, 6=Back, 7=Start if event.button == 7: self.paused = not self.paused # Check if mouse touches the sprite mouse_pos = pg.mouse.get_pos() mouse_scaled = self.adjusted_mouse_position(mouse_pos) for mushroom in self.mushrooms: if mushroom.rect.collidepoint(mouse_scaled): mushroom.is_poisoned = True mushroom.update_mushroom() if self.playing and not self.centipedes: self.end_round_mushroom_cleanup() # Check for bullet going off-screen for bullet in self.bullets: if bullet.rect.bottom < 0: bullet.kill() self.player.can_shoot = True break # Check for bullet collisions with mushrooms for bullet in self.bullets: for mushroom in self.mushrooms: if bullet.rect.colliderect(mushroom.rect): mushroom.take_hit() bullet.kill() self.player.can_shoot = True break # Check for bullet collision with centipede for bullet in self.bullets: for centipede in self.centipedes: # Iterate safely over centipedes for i, segment in enumerate(centipede.segments[:]): # Track index for referencing front segment if bullet.rect.colliderect(segment["rect"]): # Remove the bullet after collision bullet.kill() if segment["x_direction"] == 1: if self.is_position_valid(segment["rect"].x + 20, segment["rect"].y, self.mushrooms): self.mushrooms.add(Mushroom(self, self.sprite_round, segment["rect"].x + 20, segment["rect"].y)) else: if self.is_position_valid(segment["rect"].x - 20, segment["rect"].y, self.mushrooms): self.mushrooms.add(Mushroom(self, self.sprite_round, segment["rect"].x - 20, segment["rect"].y)) # Determine the index where the segment was hit hit_index = centipede.segments.index(segment) # Split the centipede into two groups front_segments = centipede.segments[:hit_index] # Segments before the hit back_segments = centipede.segments[hit_index + 1:] # Segments after the hit # Remove the hit segment completely centipede.segments.pop(hit_index) # Avoid rogue behavior # Handle the front segments if front_segments: # Assign the first remaining segment as the head front_segments[0]["is_head"] = True # Ensure it retains the correct direction front_segments[0]["x_direction"] = segment["x_direction"] front_segments[0]["y_direction"] = segment["y_direction"] centipede.segments = front_segments # Update the original centipede else: # Remove the centipede if there are no front segments self.centipedes.remove(centipede) # Handle the back segments if back_segments: # Assign the new head for the back segment group back_segments[0]["is_head"] = True # Ensure it retains the correct direction back_segments[0]["x_direction"] = segment["x_direction"] back_segments[0]["y_direction"] = segment["y_direction"] # Create a new centipede for the back group new_centipede = Centipede( self, self.sprite_round, back_segments[0]["rect"].x, back_segments[0]["rect"].y, len(back_segments), centipede.base_speed # Retain the original speed ) new_centipede.segments = back_segments # Assign new split segments self.centipedes.add(new_centipede) # Allow player shoot again self.player.can_shoot = True break # Check for player collision with centipede segments for centipede in self.centipedes: for segment in centipede.segments: if self.player.rect.colliderect(segment["rect"]): self.lose_life() # Reduce player's life # Handle collisions between centipede and mushrooms for centipede in self.centipedes: for segment in centipede.segments: for mushroom in self.mushrooms: if segment["rect"].colliderect(mushroom.rect): if not mushroom.is_poisoned: if not segment["poisoned"]: if segment["x_direction"] == 1 and segment["rect"].right >= mushroom.rect.left: segment["x_direction"] = -1 segment["move_down_next_frame"] = True elif segment["x_direction"] == -1 and segment["rect"].left <= mushroom.rect.right: segment["x_direction"] = 1 segment["move_down_next_frame"] = True else: segment["poisoned"] = True def end_round_mushroom_cleanup(self): # Step 1: Freeze enemy updates self.enemies_paused = True # Step 2: Gather all objects to remove (poisoned mushrooms and enemies) poisoned_mushrooms = [m for m in self.mushrooms if m.is_poisoned] # Combine all elements into a single list for cleanup objects_to_remove = poisoned_mushrooms # Step 3: Process each object with a delay for obj in objects_to_remove: if isinstance(obj, pg.sprite.Sprite): obj.kill() self.draw() pg.display.flip() pg.time.delay(200) # delay between removing # Restore hit mushrooms back to full hit_mushrooms = [m for m in self.mushrooms if m.health > 0] for mushroom in hit_mushrooms: mushroom.health = 0 mushroom.update_mushroom() self.draw() pg.display.flip() pg.time.delay(200) # delay between restoring self.enemies_paused = False self.new() if __name__ == '__main__': g = Game() g.new() Continue reading...