テトリス Python+Cocos2d

tetris.py

"""Tetris Cocos2d."""

import random
import cocos
from cocos.director import director
import pyglet


def arena_to_pixel(ax, ay):
    """Convert arena coord to pixel coord."""
    px = ax * (Cell.WIDTH + Cell.SPACING)
    py = ay * (Cell.HEIGHT + Cell.SPACING)
    return (px, py)


class Cell(cocos.sprite.Sprite):
    """Cell component."""

    WIDTH = 32
    HEIGHT = 32
    SPACING = 1
    IMAGE = pyglet.resource.image('square.png')

    def __init__(self, acoord, color):
        """Ctor."""
        super(Cell, self).__init__(Cell.IMAGE, color=color, anchor=(0, 0))
        self.scale_x = Cell.WIDTH / Cell.IMAGE.width
        self.scale_y = Cell.HEIGHT / Cell.IMAGE.height
        self.set_arena_position(acoord[0], acoord[1])

    def set_arena_position(self, ax, ay):
        """Set position in arena coord."""
        self.ax = ax
        self.ay = ay
        self.position = arena_to_pixel(ax, ay)

    def get_arena_position(self):
        """Get position in arena coord."""
        return self.ax, self.ay


class Piece(cocos.cocosnode.CocosNode):
    """Piece consits with four cells."""

    PATTERN = [
        [(0, 0), (1, 0), (1, 1), (2, 1)],  # S
        [(1, 0), (2, 0), (0, 1), (1, 1)],  # Z
        [(0, 0), (1, 0), (0, 1), (0, 2)],  # L
        [(0, 0), (1, 0), (1, 1), (1, 2)],  # J
        [(1, 0), (0, 1), (1, 1), (2, 1)],  # T
        [(0, 0), (1, 0), (0, 1), (1, 1)],  # O
        [(0, 0), (1, 0), (2, 0), (3, 0)],  # I
    ]

    def __init__(self, pattern, position, drop_interval):
        """Ctor."""
        super(Piece, self).__init__()

        self.ax = 0
        self.ay = 0
        self.drop_interval = drop_interval
        self.drop_time = drop_interval
        self.alive = True

        for p in pattern:
            c = Cell(p, (255, 255, 255))
            self.add(c)

        self.set_arena_position(position[0], position[1])
        self.clear_input()

    def update(self, dt):
        """Update frame."""
        # handle rotation
        self.rotate(self.rotation_input)
        if self._is_overlapping():
            # cancel
            self.rotate(-self.rotation_input)

        # move left or right
        self.move(self.move_input_dx, self.move_input_dy)
        if self._is_overlapping():
            # cancel
            self.move(-self.move_input_dx, -self.move_input_dy)

        # free fall
        self.drop_time -= dt
        if self.drop_time <= 0:
            self.move(0, -1)
            self.drop_time = self.drop_interval
            if self._is_overlapping():
                self.move(0, 1)  # cancel drop
                self._detach_cells()
                self.alive = False

        self.clear_input()

    def set_arena_position(self, ax, ay):
        """Set position in arena coord."""
        self.ax = ax
        self.ay = ay
        self.position = arena_to_pixel(self.ax, self.ay)

    def move(self, dx, dy):
        """Move by offset."""
        self.set_arena_position(self.ax + dx, self.ay + dy)

    def rotate(self, rotation):
        """Rotate left (-1) or right (+1)."""
        if rotation != -1 and rotation != 1:
            return

        for c in self.get_children():
            cx, cy = c.get_arena_position()
            new_x = +rotation * cy
            new_y = -rotation * cx
            c.set_arena_position(new_x, new_y)

    def set_drop_interval(self, drop_interval):
        """Set drop intenval."""
        self.drop_interval = drop_interval

    def push_move_input(self, dx, dy):
        """Push move value for next update."""
        self.move_input_dx += dx
        self.move_input_dy += dy

    def push_rotation_input(self, rotation):
        """Push rotation value for next update."""
        # -1 is counter clock-wise (left)
        # +1 is clock-wise (right)
        # otherwise is no rotation
        self.rotation_input = rotation

    def clear_input(self):
        """Clear input values."""
        self.move_input_dx = 0
        self.move_input_dy = 0
        self.rotation_input = 0

    def _detach_cells(self):
        """Detach child cells and pass to parent."""
        for cell in self.get_children():
            (ax, ay) = cell.get_arena_position()
            ax += self.ax
            ay += self.ay
            cell.set_arena_position(ax, ay)
            self.remove(cell)
            self.parent.add(cell)

    def _is_overlapping(self):
        """Check if overlapping something."""
        # check for left or right wall
        if self.left() < 0 or self.right() >= Arena.WIDTH:
            return True

        # check landing on bottom
        if self.bottom() < 0:
            return True

        # check for cells
        arena_cells = [
            c for c in self.parent.get_children() if isinstance(c, Cell)
        ]
        for pcell in self.get_children():
            px, py = pcell.get_arena_position()
            px += self.ax
            py += self.ay
            for acell in arena_cells:
                ax, ay = acell.get_arena_position()
                if px == ax and py == ay:
                    return True

        return False

    def is_alive(self):
        """Return True if this piece is alive (not landing on)."""
        return self.alive

    def left(self):
        """Return left most cell position x."""
        children = self.get_children()
        if len(children) > 0:
            return self.ax + min([c.get_arena_position()[0] for c in children])
        else:
            return self.ax

    def right(self):
        """Return right most cell position x."""
        children = self.get_children()
        if len(children) > 0:
            return self.ax + max([c.get_arena_position()[0] for c in children])
        else:
            return self.ax

    def bottom(self):
        """Return down most cell position .y."""
        children = self.get_children()
        if len(children) > 0:
            return self.ay + min([c.get_arena_position()[1] for c in children])
        else:
            return self.ay


class Arena(cocos.cocosnode.CocosNode):
    """Arena where cells put on."""

    WIDTH = 10
    HEIGHT = 20

    def __init__(self, acoord):
        """Ctor."""
        super(Arena, self).__init__()
        self.set_arena_position(acoord[0], acoord[1])

    def set_arena_position(self, ax, ay):
        """Set position in arena coord."""
        self.ax = ax
        self.ay = ay
        self.position = arena_to_pixel(self.ax, self.ay)


class Game(cocos.layer.Layer):
    """Game player layer."""

    is_event_handler = True

    def __init__(self):
        """Ctor."""
        super(Game, self).__init__()
        self._setup_fence()

        self.arena = Arena((1, 1))
        self.add(self.arena)

        self._respawn_piece()

        self.schedule(self.update)

    def update(self, dt):
        """Update frame."""
        if self.piece.is_alive():
            self.piece.update(dt)
        else:
            self.piece.kill()
            self._cleanup_perfect_lines()
            self._respawn_piece()

    def on_key_press(self, key, modifiers):
        """Handle key press events."""
        dx = 0
        if key == pyglet.window.key.LEFT:
            dx = -1
        if key == pyglet.window.key.RIGHT:
            dx = +1
        self.piece.push_move_input(dx, 0)

        rot = 0
        if key == pyglet.window.key.UP:
            rot = -1
        if key == pyglet.window.key.DOWN:
            rot = +1
        self.piece.push_rotation_input(rot)

        if key == pyglet.window.key.SPACE:
            self.piece.set_drop_interval(0)

    def _cleanup_perfect_lines(self):
        """Clean up cells on perfect line."""
        line_cell_count = [0] * Arena.HEIGHT
        cells = [c for c in self.arena.get_children() if isinstance(c, Cell)]
        for cell in cells:
            ay = cell.get_arena_position()[1]
            if ay < Arena.HEIGHT:
                line_cell_count[ay] += 1

        for linum in range(Arena.HEIGHT - 1, -1, -1):
            if line_cell_count[linum] == Arena.WIDTH:
                removing_cells = []
                for cell in cells:
                    ax, ay = cell.get_arena_position()
                    if ay > linum:
                        cell.set_arena_position(ax, ay - 1)
                    elif ay == linum:
                        removing_cells.append(cell)
                for cell in removing_cells:
                    cells.remove(cell)
                    cell.kill()

    def _respawn_piece(self):
        """Respawn new piece."""
        self.piece = Piece(random.choice(Piece.PATTERN),
                           position=(Arena.WIDTH // 2, Arena.HEIGHT),
                           drop_interval=0.5)
        self.arena.add(self.piece)

    def _setup_fence(self):
        """Set up left and right walls, bottom floor."""
        fence_color = (88, 88, 188)
        for i in range(Arena.HEIGHT + 1):
            left = Cell((0, i), fence_color)
            right = Cell((Arena.WIDTH + 1, i), fence_color)
            self.add(left)
            self.add(right)

        for i in range(1, Arena.WIDTH + 1):
            bottom = Cell((i, 0), fence_color)
            self.add(bottom)


game_width = (Arena.WIDTH + 2) * (Cell.WIDTH + Cell.SPACING) - Cell.SPACING
game_height = (Arena.HEIGHT + 1) * (Cell.HEIGHT + Cell.SPACING) - Cell.SPACING

director.init(width=game_width,
              height=game_height,
              caption="Tetris Cocos2d",
              autoscale=False)
scene = cocos.scene.Scene(Game())
director.run(scene)