Source

fungus / fungus_tenica.py

The default branch has multiple heads

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# encoding: utf-8

"""Simple Tetris-clone

Design:

- Static Background. 
- Bricks moving top down
- Simple keyboard control
- Groups of bricks (same controls as bricks/simple Sprites)
- Turning them
- Collision + sticking = new group
- Checking the bottom group on "full row" + dieing row = everything above becomes a new falling group, but doesn't get keyboard focus.
- dieing animation
- More UI (movement changes)
- getting faster (this needs a good representation of the position, like a grid, or it needs good bounding boxes. Coordinates are substandard, because all relevand movement happens on a grid (movement between grid nodes can be seen as pure graphical effect).

"""

#### Call the correct fungus_game when called from the command line or clicked in a GUI ####

if __name__ == "__main__": 
    # Call this Scene via fungus_game
    # For this to work, the main scene inside the module MUST be the class with the name "Scene"
    from fungus_core import call_this_scene
    # pass the commandline args
    from sys import argv
    call_this_scene(__file__, argv)


#### Imports ####

from fungus_core import Sprite
from fungus_scene import BaseScene
from pyglet.window import key
from time import time, sleep

#### API definitions ####

### A "Scene method not implemented" Exception class. 

class MethodNotImplemented(Exception):
    """A warning to display if any necessary scripting function isn't implemented."""
    def __init__(self, func, implementation = None):
        self.func = func
        self.implementation = implementation

    def __str__(self):
        if self.implementation is None:
            return "The method " + str(self.func) + " must be implemented."
        else:
            return "The method " + str(self.func) + " must be implemented." + "\nThe simplest way is to just add the following lines to your class:" + self.implementation

#### An example scene ####

### Things needed for the scene

BG_IMAGE = "tenica_background.png"
BRICK_RED = "tenica_red.png"
BRICK_BLUE = "tenica_blue.png"
BRICK_GREEN = "tenica_green.png"
BRICK_PINK = "tenica_pink.png"
# TODO: The bricks are too big. should be about 2/3rd size. 
BRICK_SIZE = 24
LOWER_BORDER = 94

### The Scene itself. 

class Group(object):
    """A group of bricks which move together."""
    def __init__(self, core, x, y, kind="stick", brick_image=BRICK_RED):
        from random import choice
        brick_image = choice([BRICK_RED, BRICK_BLUE, BRICK_GREEN, BRICK_PINK])
        self.kind = kind
        self._x = x
        self._y = y
        self._dy = 0
        self.center = core.sprite(brick_image, x=x, y=y)
        self.center.vecy = 0
        self.center.vecx = 0
        self.others = [self.center]
        if self.kind == "square":
            # one to the left
            brick = core.sprite(brick_image, x=x-BRICK_SIZE, y=y)
            brick.vecx = -BRICK_SIZE
            brick.vecy = 0
            self.others.append(brick)
            # one below
            brick = core.sprite(brick_image, x=x, y=y-BRICK_SIZE)
            brick.vecx = 0
            brick.vecy = -BRICK_SIZE
            self.others.append(brick)
            # and one left below
            brick = core.sprite(brick_image, x=x-BRICK_SIZE, y=y-BRICK_SIZE)
            brick.vecx = -BRICK_SIZE
            brick.vecy = -BRICK_SIZE
            self.others.append(brick)
        if self.kind == "stick":
            # three below
            for i in range(1, 4):
                brick = core.sprite(brick_image, x=x, y=y-BRICK_SIZE*i)
                brick.vecx = 0
                brick.vecy = -BRICK_SIZE*i
                self.others.append(brick)
                

    @property
    def height_bottom(self):
        return abs(min((o.vecy for o in self.others)))

    @property
    def width_left(self):
        return abs(min((o.vecx for o in self.others))) + BRICK_SIZE

    @property
    def width_right(self):
        return abs(max((o.vecx for o in self.others)))

    def _reposition_all(self):
        """reposition all parts (after turning)."""
        for brick in self.others:
            brick.x = self._x + brick.vecx
            brick.y = self._y + brick.vecy

    @property
    def x(self):
        return self._x
    @x.setter
    def x(self, x):
        for brick in self.others:
            brick.x = x + brick.vecx
        self._x = x

    @property
    def y(self):
        return self._y
    @y.setter
    def y(self, y):
        for brick in self.others:
            brick.y = y + brick.vecy
        self._y = y        

    @property
    def dy(self):
        return self._dy
    @dy.setter
    def dy(self, dy):
        for brick in self.others:
            brick.dy = dy
        self._dy = dy

    def turn(self):
        for brick in self.others:
            tmp = brick.vecx
            brick.vecx = -brick.vecy
            brick.vecy = tmp
        self._reposition_all()

    def update(self):
        """Propagate update to all contents."""
        self._y += self._dy
        for o in self.others:
            o.update()

    def blit(self):
        """Propagate blit to all contents."""
        for o in self.others:
            o.blit()

class BrickProvider(object):
    """Provides bricks/groups, time based."""
    def __init__(self, delay=30):
        """@param delay: The maximum time in seconds till the next brick comes."""
        self.delay = delay
        self.last_brick_time = 0

    def check_for_brick(self, core):
        """check if a new brick is due.

        @return: None or brick/group
        """
        if time() - self.last_brick_time >= self.delay:
            brick = Group(core, x=core.win.width/2, y=core.win.height - BRICK_SIZE)
            brick.dy = -3
            brick.bound = False
            self.last_brick_time = time()
            return brick
        

class Scene(BaseScene): 
    """A dummy scene - mostly just the Scene API."""
    def __init__(self, core, *args, **kwds): 
        """Initialize the scene with a core object for basic functions."""
        
        ## Get the necessary attributes for any scene. 
        # This gets the 'visible', 'colliding' and 'overlay' lists 
        # as well as the scene switch 'switch_to_scene' 
        # which can be assigned a scene to switch to. 
        super(Scene, self).__init__(core, *args, **kwds)

        #: creates new bricks at the top.
        self.provider = BrickProvider()

        # for fixed FPS
        self.last_update_time = time()
        self.target_fps = 25

        ## Tests - not necessary for every scene. 
        # Add a blob to the visible items.
        self.back = self.core.sprite(BG_IMAGE, x=0, y=0)
        # self.back = self.core.sprite(background, x=0, y=0,
        #                              update_func=self.blob_update)

        # background 
        self.visible.append(self.back)
        # and the first brick, controlled via the keyboard
        self.actor = self.provider.check_for_brick(self.core)
        self.visible.append(self.actor)
        self.actor.update()



    def collision_check(self, brick):
        """Check for collisions with the ground or the bound bricks."""
        if brick.y - brick.height_bottom - LOWER_BORDER <= 0:
            return True
        # make sure we only do (expensive) collision checking if a collision is plausible.
        if self.colliding[1:] and brick.y - brick.height_bottom > max((o.y for o in self.colliding if o is not brick)) + BRICK_SIZE*4:
            return False
        # for optimization: only consider other bricks in the same or neighboring rows.
        brick_lower_border = min((o.y for o in brick.others))
        for other in (o for o in self.colliding if o is not brick):
            other_upper_border = max((m.y for m in other.others))
            if other_upper_border < brick_lower_border - BRICK_SIZE:
                continue
            other_lower_border = min((m.y for m in other.others))
            if other_lower_border - BRICK_SIZE > brick_lower_border: 
                continue
            for part in (o for o in brick.others if o.y - BRICK_SIZE <= other_upper_border):
                for prt in (o for o in other.others if o.y >= part.y - BRICK_SIZE and o.y -BRICK_SIZE <= part.y):
                    if self.core.overlaps_rectangle(part, prt):
                        return True
        return False

    def check_row(self, brick, kill = []):
        """check, if the remaining items form a complete row to the right."""
        kill.append(brick)
        if brick.x == self.core.win.width/2.0 + 5*BRICK_SIZE:
            return kill
        for group in self.colliding:
            next_brick = [b for b in group.others if b.y == brick.y and b.x == brick.x + BRICK_SIZE]
            if next_brick:
                return self.check_row(next_brick[0], kill=kill)

    def kill_full_rows(self):
        """remove all bricks inside full rows."""
        kill_bricks =[]
        for group in (g for g in self.colliding if g.x - g.width_left == self.core.win.width/2.0 - 5*BRICK_SIZE):
            # just look at all bricks on the left side and then check if they form a full row. 
            for brick in [b for b in group.others if b.x - BRICK_SIZE == self.core.win.width/2.0 - 5*BRICK_SIZE]:
                kill_bricks.extend(self.check_row(brick))
        print kill_bricks
        for brick in kill_bricks:
            for group in self.colliding:
                if brick in group.others:
                    group.others.remove(brick)
                    

    def collide(self, brick):
        """Check for collisions with the ground or the bound bricks."""
        if self.collision_check(brick):
            # faster moving bricks have to be at the same height as slower ones.
            deviation_from_raster = (brick.y - LOWER_BORDER)%BRICK_SIZE
            if deviation_from_raster and deviation_from_raster < 5: 
                brick.y -= deviation_from_raster
            elif deviation_from_raster > 5:
                brick.y -= deviation_from_raster - BRICK_SIZE
            brick.dy = 0
            brick.bound = True
            # set the next brick as actor
            if brick is not self.visible[-1]: 
                self.actor = self.visible[-1]
            else:
                # if we don't yet have another brick, we need one
                self.actor = None
                self.provider.last_brick_time = 0
            self.colliding.append(brick)
            self.kill_full_rows()

    def update(self): 
        """Update the stats of all scene objects. 

Don't blit them, though. That's done by the Game itself.

To show something, add it to the self.visible list. 
To add a collider, add it to the self.colliding list. 
To add an overlay sprite, add it to the self.overlay list. 
"""
        for sprite in self.visible:
            sprite.update()
        brick = self.provider.check_for_brick(self.core)
        if brick is not None:
            self.visible.append(brick)
            if self.collision_check(brick):
                print "Game Over"
        if self.actor is None:
            self.actor = brick
        # collision checking for all moving bricks.
        for bri in (br for br in self.visible[1:] if not br.bound):
            self.collide(bri)
        # fix FPS (=variable delay)
        t = time()
        if t - self.last_update_time < 1.0/self.target_fps:
            sleep(t - self.last_update_time)
        self.last_update_time = t


    def on_key_press(self, symbol, modifiers):
        """Forwarded keyboard input."""
        # Use the escape key as a means to exit the game.
        if symbol == key.ESCAPE:
            self.core.win.has_exit = True
        # control keys
        elif self.actor is not None and symbol == key.RIGHT:
            self.actor.x += BRICK_SIZE
            # if we collide, undo the move
            if self.collision_check(self.actor) or self.actor.x + self.actor.width_right > self.core.win.width/2.0 + 5*BRICK_SIZE:
                self.actor.x -= BRICK_SIZE
        elif self.actor is not None and symbol == key.LEFT:
            self.actor.x -= BRICK_SIZE
            # if we collide, undo the move
            if self.collision_check(self.actor) or self.actor.x - self.actor.width_left < self.core.win.width/2.0 - 5*BRICK_SIZE:
                self.actor.x += BRICK_SIZE            
        elif self.actor is not None and symbol == key.DOWN:
            self.actor.dy -= 6
        elif self.actor is not None and symbol == key.UP:
            self.actor.turn()
            # if the turn causes a collision, undo it.
            if self.collision_check(self.actor) or self.actor.x - self.actor.width_left < self.core.win.width/2.0 - 5*BRICK_SIZE or self.actor.x + self.actor.width_right > self.core.win.width/2.0 + 5*BRICK_SIZE:
                for i in range(3):
                    self.actor.turn()
        else:
            pass

    def on_key_release(self, symbol, modifiers):
        """Forwarded keyboard input."""
        # releasing up slows the brick down again
        if self.actor is not None and symbol == key.DOWN:
            self.actor.dy = -3