view mamba/snake.py @ 501:ed752233f1aa

Clamp distance moved per update to shorter side of tile.
author Simon Cross <hodgestar@gmail.com>
date Sat, 17 Sep 2011 23:41:01 +0200
parents 1e299187884b
children 3fdbde59dc8a
line wrap: on
line source

"""The player snake object."""

import random

import pygame
from pygame.sprite import Group, spritecollide

from mamba.constants import TILE_SIZE, UP, DOWN, LEFT, RIGHT
from mamba.sprites import BaseSprite, tile_sizify
from mamba.engine import SnakeDiedEvent, LevelCompletedEvent
from mamba.sound import load_sound, play_sound
from mamba import mutators


INITIAL_SEGMENT_COUNT = 4


class Snake(object):

    # don't move more than one tile at once
    MAX_DISTANCE = min(TILE_SIZE)

    def __init__(self, tile_pos, orientation):
        load_sound('crash', 'crash.ogg')
        self.segments = self.create_segments(tile_pos, orientation)
        self.pending_segments = []  # segments waiting to be added
        self.segment_group = Group()
        self.segment_group.add(*reversed(self.segments))
        self.set_orientation(orientation)
        self.speed = 120.0  # pixel / s
        self.frac_ds = 0.0
        self.mutation = None
        self.coiled = True
        self._orientation_changes = []

    head = property(fget=lambda self: self.segments[0])
    tail = property(fget=lambda self: self.segments[-1])

    def create_segments(self, tile_pos, orientation):
        segments = []
        for cls in [Head] + [Body] * INITIAL_SEGMENT_COUNT + [Tail]:
            segments.append(cls(tile_pos, orientation))
        return segments

    def add_segment(self, segment=None):
        if segment is None:
            segment = Body((0, 0), UP)
            segment.set_colour(self.head.colour)
        self.pending_segments.append(segment)

    def remove_segment(self, segment=None):
        if segment is None:
            if len(self.segments) < 3:
                return
            segment = self.segments[-2]
        try:
            idx = self.segments.index(segment)
        except IndexError:
            return
        self.shiftup_segments(idx, segment.get_tile_state())
        del self.segments[idx]
        segment.kill()

    def shiftup_segments(self, idx, state):
        for segment in self.segments[idx:]:
            next_state = segment.get_tile_state()
            segment.shift_tile(state)
            state = next_state

    def draw(self, surface):
        self.segment_group.draw(surface)

    def update(self, dt, world):
        ds = dt * self.speed + self.frac_ds
        ds = min(ds, self.MAX_DISTANCE)
        ds, self.frac_ds = divmod(ds, 1)
        ds = int(ds)
        tile_state = self.head.get_tile_state()
        shifted, ds = self.head.shift_head(ds)
        if shifted:
            self.coiled = False
            self.head.shifted_tile()
            self._pop_orientation_queue()
            if self.pending_segments:
                new_segment = self.pending_segments.pop(0)
                self.segments.insert(1, new_segment)
                self.segment_group.add(new_segment)
                new_segment.shift_tile(tile_state)
                for segment in self.segments[2:]:
                    segment.shift_tile(segment.get_tile_state())
                # Fix for segment being mis-coloured when the
                # snake passes over two consequetive paint
                # splashes and then eats a lot of large rats
                # Warning: Don't completely understand why this
                #          works.
                ds = 0
            else:
                self.shiftup_segments(1, tile_state)

        self.check_self_crash()
        self.check_on_screen(world)
        for segment in self.segments:
            segment.shift_pixels(ds)
            world.interact(segment)

    def send_new_direction(self, orientation):
        # Filter illegal & noop orientation changes
        if self._orientation_changes:
            tip = self._orientation_changes[-1]
        else:
            tip = self.head.orientation
        if ((0 == orientation[0] == tip[0])
            or (0 == orientation[1] == tip[1])):
            return

        self._orientation_changes.append(orientation)
        # Cap queue length:
        self._orientation_changes = self._orientation_changes[:3]

    def force_new_direction(self, orientation):
        self._orientation_changes.insert(0, orientation)

    def _pop_orientation_queue(self):
        while self._orientation_changes:
            orientation = self._orientation_changes.pop(0)
            if ((0 == orientation[0] == self.head.orientation[0])
                or (0 == orientation[1] == self.head.orientation[1])):
                continue
            self.set_orientation(orientation)
            break

    def set_orientation(self, orientation):
        self.orientation = orientation
        self.head.set_orientation(orientation)

    def check_self_crash(self):
        if self.coiled:
            return
        collides = spritecollide(self.head, self.segment_group, False)
        if [s for s in collides if s not in self.segments[:2]]:
            self.crash('You hit yourself!')

    def check_on_screen(self, world):
        if self.coiled:
            return
        world_rect = pygame.Rect((0, 0), world.level.get_size())
        if not world_rect.contains(self.head.rect):
            self.crash('You left the arena!')

    def crash(self, reason='You hit something'):
        play_sound('crash')
        SnakeDiedEvent.post(reason)

    def exit_level(self):
        LevelCompletedEvent.post()

    def mutate(self, mutation):
        self.mutation = mutation
        self.tail.show_mutation(mutation)

    def can_swim(self):
        return self.mutation == 'amphibious'

    def flame_retardant(self):
        return self.mutation == 'flame-retardant'

    def adjust_speed(self, delta):
        self.speed += delta
        self.speed = max(self.speed, 60)
        self.speed = min(self.speed, 180)


class Segment(BaseSprite):

    GREEN = mutators.SNAKE_GREEN
    BLUE = mutators.BLUE
    RED = mutators.RED
    YELLOW = mutators.YELLOW

    _detail_mutators = ()

    is_head = False

    def __init__(self, image_name, tile_pos, orientation):
        super(Segment, self).__init__()
        self.set_base_image(image_name)
        self.colour = self.GREEN
        self.orientation = orientation

        self.make_images()
        self.update_image()
        self.set_tile_pos(tile_pos)
        self.on_tiles = []

    def filter_collisions(self, group):
        collide = []
        tiles = spritecollide(self, group, False)
        for tile in tiles:
            if tile not in self.on_tiles:
                collide.append(tile)
        self.on_tiles = tiles
        return collide

    def set_base_image(self, image_name):
        self._base_image = "/".join(["snake", image_name])

    def make_images(self):
        self._images = {}
        for orientation, muts in [
            (RIGHT, (mutators.RIGHT,)),
            (LEFT, (mutators.LEFT,)),
            (UP, (mutators.UP,)),
            (DOWN, (mutators.DOWN,)),
            ]:
            all_muts = (self.colour,) + self._detail_mutators + muts
            self._images[orientation] = self.load_image(self._base_image,
                    all_muts)

    def update_image(self):
        self.image = self._images[self.orientation]

    def set_orientation(self, orientation):
        self.orientation = orientation
        self.update_image()

    def set_colour(self, colour_overlay):
        self.colour = colour_overlay
        self.make_images()
        self.update_image()

    def get_tile_state(self):
        return self.tile_pos, self.orientation

    def get_distance(self):
        rx, ry = self.rect.topleft
        x, y = tile_sizify(self.tile_pos)
        return max(abs(rx - x), abs(ry - y))

    def shift_tile(self, tile_state):
        """Shift this segment to the tile the other one was on.

        Also reset the position to be the center of the tile.
        """
        tile_pos, orientation = tile_state
        self.set_tile_pos(tile_pos)
        self.set_orientation(orientation)

    def shift_tile_and_pixels(self, tile_state):
        ds = self.get_distance()
        self.shift_tile(tile_state)
        self.shift_pixels(ds)

    def shifted_tile(self):
        pass

    def shift_pixels(self, distance):
        """Shift this segment a number of pixels."""
        dx, dy = self.orientation
        dx, dy = distance * dx, distance * dy
        self.rect = self.rect.move(dx, dy)

    def shift_head(self, ds):
        """Shift the head a number of pixels in the direction of it
        orientation.
        """
        dx, dy = self.orientation
        TX, TY = TILE_SIZE
        rx, ry = self.rect.left, self.rect.top
        tx, ty = self.tile_pos
        # WARNING: Tri-state logic ahead
        # (Tri-state logic is the mamba's natural habitat)
        if dx != 0:
            newdx = rx + ds * dx
            newtx = newdx / TX
            if newtx > tx:
                self.set_tile_pos((tx + 1, ty))
                ds = newdx - self.rect.left
                return True, ds
            elif newtx < tx - 1:
                self.set_tile_pos((tx - 1, ty))
                ds = self.rect.left - newdx
                return True, ds
        else:
            newdy = ry + ds * dy
            newty = newdy / TY
            if newty > ty:
                self.set_tile_pos((tx, ty + 1))
                ds = newdy - self.rect.top
                return True, ds
            elif newty < ty - 1:
                self.set_tile_pos((tx, ty - 1))
                ds = self.rect.top - newdy
                return True, ds
        return False, ds


class Head(Segment):
    CLOSED = "snake-head"
    OPEN = "snake-head-mouth-open-r"
    EYE = mutators.Overlay("snake/snake-head-eye-r")
    TONGUE = mutators.Overlay("snake/snake-head-tongue-r")

    is_head = True

    def __init__(self, tile_pos, orientation):
        self._detail_mutators = (self.EYE,)
        super(Head, self).__init__(self.CLOSED, tile_pos, orientation)

    def mouth_open(self):
        self.set_base_image(self.OPEN)
        self.make_images()
        self.update_image()

    def mouth_close(self):
        self.set_base_image(self.CLOSED)
        self.make_images()
        self.update_image()

    def tongue_out(self):
        self._detail_mutators = (self.EYE, self.TONGUE)
        self.make_images()
        self.update_image()

    def tongue_in(self):
        self._detail_mutators = (self.EYE,)
        self.make_images()
        self.update_image()

    def shifted_tile(self):
        if random.random() < 0.02:
            self.mouth_open()
            self.tongue_out()
        else:
            self.mouth_close()
            self.tongue_in()


class Body(Segment):
    def __init__(self, tile_pos, orientation):
        super(Body, self).__init__("snake-body", tile_pos, orientation)


class Tail(Segment):
    INDICATORS = {
        "flame-retardant": mutators.Overlay("snake/snake-tail-fire-r"),
        "amphibious": mutators.Overlay("snake/snake-tail-puddle-r"),
    }

    def __init__(self, tile_pos, orientation):
        super(Tail, self).__init__("snake-tail-r", tile_pos, orientation)

    def show_mutation(self, mutation):
        self._detail_mutators = (self.INDICATORS[mutation],)
        self.make_images()
        self.update_image()