8
\$\begingroup\$

Tetromino class:

class Tetromino(object):
    def __init__(self, board, matrix, type, color, x=0, y=None, updateinterval=FRAMERATE, queue=0):
        self.matrix = matrix
        self.type = type
        self.board = board
        self.color = color
        self.updateinterval = updateinterval
        self.time_until_update = self.updateinterval
        self.draw_required = True
        self.update_required = True
        self.sped_up = False
        self.x = x
        self.y = y
        self.queue = queue
        self.level = 1

        if y == None:
            self.y = -(len(self.matrix))

    ## Hackety hack
    def forBlock(self, func, boolean=False):
        for y in xrange(len(self.matrix)):
            for x in xrange(len(self.matrix[y])):
                if self.matrix[y][x] and func(self.x + x, self.y + y, self.matrix) and boolean:
                    return True

    def draw(self):
        def drawBlock(x, y, _):
            self.board.drawCube(x, y, self.color)
        self.forBlock(drawBlock)

    def insert(self):
        def insert(x, y, _):
            self.board.blocks[(x, y)] = self.color

        if self.y < 0:
            ## XXX: GAME OVER
            self.board.update_required = False

        self.forBlock(insert)
        self.board.checkTetris()
        self.update_required = False

    def update(self):
        self.time_until_update -= 1
        if self.time_until_update <= 0:
            self.moveDiagonal(1)
            self.time_until_update = self.updateinterval

    def drop(self):
        while self.update_required:
            self.moveDiagonal(1)

    def checkBlockCollision(self):
        def colliding(x, y, _):
            return self.board.blocks.get((x, y))
        return self.forBlock(colliding, boolean=True)

    def checkWallCollision(self, xp, yp):
        for y in xrange(len(self.matrix)):
            for x in xrange(len(self.matrix[y])):
                ## Some of the functions need to know which edge the collision happened on,
                ## otherwise the result can be treated like a boolean.
                if self.matrix[y][x]:
                    if yp+y > self.board.height-1:
                        return "bottom"
                    if xp+x > self.board.width-1:
                        return "right"
                    if xp+x < 0:
                        return "left"

    ## Move diagonally, if possible
    def moveDiagonal(self, direction):
        self.y += direction
        if self.checkBlockCollision():
            self.y -= direction
            self.insert()
        if self.checkWallCollision(self.x, self.y) == "bottom":
            self.y -= direction
            self.insert()

    ## Move horizontally, if possible
    def moveHorizontal(self, direction):
        self.x += direction
        if self.checkBlockCollision():
            self.x -= direction
        if self.checkWallCollision(self.x, self.y):
            self.x -= direction

    ## Rotate if possible
    def rotate(self, direction):
        last_matrix = self.matrix
        self.matrix = rot90(self.matrix)
        if self.checkWallCollision(self.x, self.y) or self.checkBlockCollision():
            self.matrix = last_matrix

    ## It makes the game WAAY to easy, but i kind of always wondered "what if"
    def flip(self):
        flip(self.matrix)
        if self.checkWallCollision(self.x, self.y) or self.checkBlockCollision():
            flip(self.matrix)

    def eventHandler(self, events):
        for event in events:
            if event.type == KEYUP:
                if event.key == keymap["game"]["speed_up"] and self.sped_up:
                    self.sped_up = False
                    self.updateinterval *= 10
                    self.time_until_update = self.updateinterval

            if event.type == KEYDOWN:
                if event.key == keymap["game"]["rotate_right"]:
                    self.rotate(1)
                elif event.key == keymap["game"]["rotate_left"]:
                    self.rotate(-1)
                elif event.key == keymap["game"]["reverse"]:
                    self.flip()

                elif event.key == keymap["game"]["move_right"]:
                    self.moveHorizontal(1)
                elif event.key == keymap["game"]["move_left"]:
                    self.moveHorizontal(-1)

                elif event.key == keymap["game"]["drop_down"]:
                    self.drop()

                elif event.key == keymap["game"]["speed_up"]:
                    self.sped_up = True
                    self.updateinterval /= 10
                    self.time_until_update = self.updateinterval

Game class:

class Game(object):
    def __init__(self, _id, caption="", mouse_visible=True, bgcolor=(0x22,0x22,0x22), screen=None, ticktime=FRAMERATE,
                 width=SCREEN_WIDTH, height=SCREEN_HEIGHT, x=SCREEN_WIDTH, y=SCREEN_HEIGHT, sound_enabled=False, soundtrack=None):
        self.caption = caption
        self.mouse_visible = mouse_visible
        self.bgcolor = bgcolor
        self.screen = screen
        self.ticktime = ticktime
        self.batch = {}
        self.drawqueue = []
        self.ret = 0
        self.windows = {}
        self.height = y
        self.width = x
        self.events = None
        self.id = _id
        self.soundtrack = soundtrack
        self.sound_enabled = sound_enabled
        self.playing = ""

        self.setup()

    def stopMusic(self):
        self.playing = ""
        Pygame.mixer.music.stop()

    ## TODO: The call/quit model currently fails here, I'll just have to save the music's "progress."
    def playMusic(self, path, loops=1):
        try:
            if not self.sound_enabled:
                Log.warning("Attempted to play music in `{}' where sound has been disabled".format(self.id))
            Pygame.mixer.music.load(path)
            Pygame.mixer.music.play(loops)
            Log.log("Playing sountrack `{}'".format(path))
            self.playing = path
        except:
            Log.error("Unable to play music file: `{}'".format(path))

    def getJob(self, name):
        return self.batch[name]

    def addJob(self, name, obj):
        self.batch[name] = obj
        self.drawqueue.append(name)

    ## Why not just call Sys.exit(), why create a separate method for this?
    ## Because finishing of can get more complex as this program develops.
    def quit(self):
        Sys.exit()

    ## We just "exploit" the stack to create things like pause menus or other "contexts"
    ## that take over the screen.
    def call(self, obj, **kwargs):
        game = obj(screen=self.screen, **kwargs)
        ret = game.run()

        self.setup()

        if ret and self.id != ret:
            self.quitGame(ret)

    def quitGame(self, *args):
        if args:
            self.ret = args[0]
        if self.playing:
            self.stopMusic()
        self.running = None

    def setup(self):
        Pygame.init()
        Pygame.display.set_caption(self.caption)
        Pygame.mouse.set_visible(int(self.mouse_visible))
        if not Pygame.mixer.get_init() and self.sound_enabled:
            Log.log("Initializing mixer")
            Pygame.mixer.init()
        if self.soundtrack and self.sound_enabled and not self.playing:
            self.playMusic(self.soundtrack, loops=-1)
        if not self.screen:
            self.screen = Pygame.display.set_mode((self.width, self.height), DISPLAY_OPTIONS)
        self.screen.fill(self.bgcolor)
        Pygame.display.flip()
        self.clock = Pygame.time.Clock()

    def eventHandler(self, events):
        pass

    def run(self):
        if not hasattr(self, "running") or not hasattr(self, "eventHandler"):
            raise GameError("Game has not been properly initialized")

        while self.running:
            self.clock.tick(self.ticktime)
            self.screen.fill(self.bgcolor)
            self.events = Pygame.event.get()
            queue = sorted(self.batch, key=lambda obj: self.batch[obj].queue)
            for obj in queue:
                obj = self.getJob(obj)
                if obj.update_required:
                    obj.update()
                if obj.draw_required:
                    obj.draw()

                ## Context is love, context is life.
                obj.eventHandler(self.events)
            Pygame.display.flip()
            self.eventHandler(self.events)
            if self.running:
                self.running()

        return self.ret
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Why not just let the code speak for itself? Instead of a long explanation of the design, put the code to be reviewed in the question. (You've included some code, but not nearly enough to constitute a Code Review question, I think.) \$\endgroup\$ Commented May 15, 2014 at 9:04

1 Answer 1

4
\$\begingroup\$

I have a few comments on your OOP design.

Your Tetromino class is way too full of functionality, or in other words, bloated. On one hand, it stores the basic info of location, type etc, but at the same time it is responsible for events, drawing, and collision detection. That is a violation of the Single Responsibility Principle which states that each component should be responsible for 1 task.

Therefore, the Tetromino class should just have a list of the 4 coordinates, and a color. We then subclass Tetromino for all the types of tetrominoes: SquareTetromino, LineTetromino, and etc. which will have convenient initializers for generating the correct coordinates given a single coordinate (such as the upper left corner).

Then, we add a TetrominoController which handles moving the Tetrominos as needed and so has a list of the tetrominos and the dimensions of the board. This class does the bound checking.

Lastly we should have a TetrominoRenderer that interfaces with the TetrominoController to draw the board (the controller will notify the renderer when positions change and so on).

The Game class should interact with the TetrominoController to dictate updates, and it should initialize the renderer (but not do anything else, as the renderer should get notifications from the controller).

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.