Profilo di ragoo

Nome ragoo
Indirizzo email n/a
Messaggi1
  • Creare tetromini
    Forum >> Programmazione Python >> Videogames
    Ehila. Per qualche ragione nutro il forte sospetto di essere stato sostanzialmente (e probabilmente giustamente) ostracizzato (leggasi sfanculato) sul forum in lingua inglese, quindi mi sembrava giusto cogliere l'occasione per esserlo anche qui.



    Il problema che avevo era inizializzare tetronimi partendo da una serie di "dati grezzi". Prima di esaurire la disponibilita' dell'utenza, sono riuscito a sgraffignare una soluzione interessante, che costituisce un significativo miglioramento rispetto alla mia, ahem, implementazione:

    import math
    import random
     
    from pyglet.shapes import Rectangle
    from pyglet.math import Vec2
     
    import settings
     
     
    class Shape:
        """I know the details of a tetronimo shape.
       
        Creating an instance of Shape adds the shape to my dictionary.  Use
        get(id) to get the Shape instance that matches the id, or use random()
        go get a random shape.
        """
        dictionary = {}  # Lookup Shape by id.
        default_kick = (
            {3: ((-1, 0), (-1, -1), (0, 2), (-1, 2)), 1: ((1, 0), (1, -1), (0, 2), (1, 2))},
            {0: ((-1, 0), (-1, 1), (0, -2), (-1, -2)), 2: ((-1, 0), (-1, 1), (0, -2), (-1, -2))},
            {1: ((1, 0), (1, -1), (0, 2), (1, 2)), 3: ((-1, 0), (-1, -1), (0, 2), (-1, 2))},
            {2: ((1, 0), (1, 1), (0, -2), (1, -2)), 0: ((1, 0), (1, 1), (0, -2), (1, -2))},
        )
        default_pivot = (-1, -2)
       
        def __init__(self, id, color, minos, pivot=None, wall_kick=None):
            pivot = self.default_pivot if pivot is None else pivot
            wall_kick = self.default_kick if wall_kick is None else wall_kick
            self.id = id
            self.color = color
            self.minos = [Vec2(*mino) for mino in minos]
            self.pivot = Vec2(*pivot)
            self.wall_kick = wall_kick
            self.dictionaryid = self
       
        @classmethod
        def get(cls, key):
            """Lookup shape by id."""
            return cls.dictionarykey
       
        @classmethod
        def random(cls):
            """Return random shape."""
            return random.choice(list(cls.dictionary.values()))
       
       
    class Mino(Rectangle):
        cell_size = 10
       
        def __init__(self, grid_position, pivot, color, batch):
            self.grid_position = grid_position
            self.pivot = pivot
            x = self.grid_position.x * self.cell_size
            y = self.grid_position.y * self.cell_size
            super().__init__(x, y, self.cell_size, self.cell_size, color, batch)
       
        def rotate(self, angle):
            translated = self.grid_position - self.pivot
            rotated = translated.rotate(math.radians(angle))
            self.grid_position = Vec2(*map(round, rotated + self.pivot))
            self.update()
       
        def shift(self, vector):
            self.pivot += vector
            self.grid_position += vector
            self.update()
       
        def update(self):
            self.x = self.grid_position.x * self.width
            self.y = self.grid_position.y * self.height
       
       
    class Tetromino:
        """I am a tetronimo that the player is moving on the screen.
       
        Use random(location, rotation) to create a randomly shaped
        tetronimo.  location is the coordinates of the origin.  Rotation
        is 0..3.
       
        Use get(shape_id, location, rotation) to create a tetronimo
        of a specific shape.
        """
       
        @classmethod
        def random(cls, location, batch):
            """Create a random Tetronimo."""
            return cls(Shape.random(), location, batch)
       
        @classmethod
        def get(cls, shape_id, location, batch):
            """Create Tetronimo using shape with matching id."""
            return cls(Shape.get(shape_id), location, batch)
       
        def __init__(self, shape, location, batch):
            self.minos = [
                Mino(pos + location, shape.pivot + location, shape.color, batch) for pos in shape.minos
            ]
            self.wall_kicks = shape.wall_kick
            self.rotation = 0
            self.old_rotation = 0
     
        def get_positions(self):
            return (mino.grid_position for mino in self.minos)
     
        def get_wall_kicks(self):
            return self.wall_kicks[self.rotation][self.old_rotation]
                 
        def rotate(self, angle):
            """Rotate tetronimo."""
            self.old_rotation = self.rotation
            self.rotation = (self.rotation - int(math.copysign(1, angle))) % 4
            for mino in self.minos:
                mino.rotate(angle)
     
        def shift(self, vector):
            """Move in x and y direction."""
            for mino in self.minos:
                mino.shift(vector)
       
        
    # Define all the shapes
    Shape("I", settings.icolor, ((-2, -2), (-1, -2), (0, -2), (1, -2)),
        pivot=(-0.5, -2.5),
        wall_kick=(
            {3: ((1, 0), (-2, 0), (1, -2), (-2, 1)), 1: ((2, 0), (-1, 0), (2, 1), (-1, -2))},
            {0: ((-2, 0), (1, 0), (-2, -1), (1, 2)), 2: ((1, 0), (-2, 0), (1, -2), (-2, 1))},
            {1: ((-1, 0), (2, 0), (-1, 2), (2, -1)), 3: ((-2, 0), (1, 0), (-2, -1), (1, 2))},
            {2: ((2, 0), (-1, 0), (2, 1), (-1, -2)), 0: ((-1, 0), (2, 0), (-1, 2), (2, -1))}
        )
    )
    Shape("O", settings.ocolor, ((-1, -2), (0, -2), (-1, -1), (0, -1)), (-0.5, -1.5), [])
    Shape("J", settings.jcolor, ((-2, -2), (-1, -2), (0, -2), (-2, -1)))
    Shape("L", settings.lcolor, ((-2, -2), (-1, -2), (0, -2), (0, -1)))
    Shape("S", settings.scolor, ((-2, -2), (-1, -2), (-1, -1), (0, -1)))
    Shape("T", settings.tcolor, ((-2, -2), (-1, -2), (0, -2), (-1, -1)))
    Shape("Z", settings.zcolor, ((-1, -2), (0, -2), (-2, -1), (-1, -1)))
    Il problema e' che non mi piace inizializzare tutte queste Shape nel modulo, mi sembra un tantinello sciatto. Non sarebbe meglio incapsulare la logica in una classe deputata unicamente a questo scopo? Inoltre, non e' meglio utilizzare un Enumerator anziche' stringhe per le shape_id?




    Con queste modifiche, questo sarebbe il risultato:

    import math
    import random
    from enum import Enum
    
    from pyglet.shapes import Rectangle
    from pyglet.math import Vec2
    
    import settings
    
    
    class ShapeID(Enum):
        I = 0
        J = 1
        L = 2
        O = 3
        S = 4
        T = 5
        Z = 6
    
    
    class ShapeFactory:
    
        iminos = ((-2, -2), (-1, -2), (0, -2), (1, -2))
        jminos = ((-2, -2), (-1, -2), (0, -2), (-2, -1))
        lminos = ((-2, -2), (-1, -2), (0, -2), (0, -1))
        ominos = ((-1, -2), (0, -2), (-1, -1), (0, -1))
        sminos = ((-2, -2), (-1, -2), (-1, -1), (0, -1))
        tminos = ((-2, -2), (-1, -2), (0, -2), (-1, -1))
        zminos = ((-1, -2), (0, -2), (-2, -1), (-1, -1))
        pivot = (-1, -2)
        ipivot = (-0.5, -2.5)
        opivot = (-0.5, -1.5)
        kicks = (
                {
                    3: ((-1, 0), (-1, -1), (0, 2), (-1, 2)),
                    1: ((1, 0), (1, -1), (0, 2), (1, 2)),
                },
                {
                    0: ((-1, 0), (-1, 1), (0, -2), (-1, -2)),
                    2: ((-1, 0), (-1, 1), (0, -2), (-1, -2)),
                },
                {
                    1: ((1, 0), (1, -1), (0, 2), (1, 2)),
                    3: ((-1, 0), (-1, -1), (0, 2), (-1, 2)),
                },
                {
                    2: ((1, 0), (1, 1), (0, -2), (1, -2)),
                    0: ((1, 0), (1, 1), (0, -2), (1, -2)),
                },
            )
        ikicks = (
                {
                    3: ((1, 0), (-2, 0), (1, -2), (-2, 1)),
                    1: ((2, 0), (-1, 0), (2, 1), (-1, -2)),
                },
                {
                    0: ((-2, 0), (1, 0), (-2, -1), (1, 2)),
                    2: ((1, 0), (-2, 0), (1, -2), (-2, 1)),
                },
                {
                    1: ((-1, 0), (2, 0), (-1, 2), (2, -1)),
                    3: ((-2, 0), (1, 0), (-2, -1), (1, 2)),
                },
                {
                    2: ((2, 0), (-1, 0), (2, 1), (-1, -2)),
                    0: ((-1, 0), (2, 0), (-1, 2), (2, -1)),
                },
            )
        
        def __init__(self):
            self.shapes = {}
            
            self.register_shape(ShapeID.I, settings.icolor, self.iminos, self.ipivot, self.ikicks)
            self.register_shape(ShapeID.J, settings.jcolor, self.jminos, self.pivot, self.kicks)
            self.register_shape(ShapeID.L, settings.lcolor, self.lminos, self.pivot, self.kicks)
            self.register_shape(ShapeID.O, settings.ocolor, self.ominos, self.opivot)
            self.register_shape(ShapeID.S, settings.scolor, self.sminos, self.pivot, self.kicks)
            self.register_shape(ShapeID.T, settings.tcolor, self.tminos, self.pivot, self.kicks)
            self.register_shape(ShapeID.Z, settings.zcolor, self.zminos, self.pivot, self.kicks)
    
        def register_shape(self, shape_id, color, minos, pivot, kicks=None):
            """Register a new shape with the factory."""
            self.shapesshape id = Shape(color, minos, pivot, kicks)
    
        def get(self, shape_id):
            """Return a specific shape by ID."""
            if shape_id in self.shapes:
                return self.shapesshape id
            else:
                raise ValueError(f"Invalid shape ID: {shape_id}")
    
        def random(self):
            """Return a random shape."""
            shape_id = random.choice(tuple(self.shapes.keys()))
            return self.get(shape_id)
    
    
    class Shape:
        def __init__(self, color, minos, pivot, kicks=None):
            self.color = color
            self.minos = [Vec2(*mino) for mino in minos]
            self.pivot = Vec2(*pivot)
            self.kicks = kicks
      
      
    class Mino(Rectangle):
        cell_size = 10
      
        def __init__(self, grid_position, pivot, color, batch):
            self.grid_position = grid_position
            self.pivot = pivot
            x = self.grid_position.x * self.cell_size
            y = self.grid_position.y * self.cell_size
            super().__init__(x, y, self.cell_size, self.cell_size, color, batch)
      
        def rotate(self, angle):
            translated = self.grid_position - self.pivot
            rotated = translated.rotate(math.radians(angle))
            self.grid_position = Vec2(*map(round, rotated + self.pivot))
            self.update()
      
        def shift(self, vector):
            self.pivot += vector
            self.grid_position += vector
            self.update()
      
        def update(self):
            self.x = self.grid_position.x * self.width
            self.y = self.grid_position.y * self.height
      
      
    class Tetromino:
        shape_factory = ShapeFactory()
    
        def __init__(self, shape, location, batch):
            self.minos = [
                Mino(pos + location, shape.pivot + location, shape.color, batch) for pos in shape.minos
            ]
            self.wall_kicks = shape.kicks
            self.rotation = 0
            self.old_rotation = 0
    
        def get_positions(self):
            return (mino.grid_position for mino in self.minos)
    
        def get_wall_kicks(self):
            return self.wall_kicks[self.rotation][self.old_rotation]
                
        def rotate(self, angle):
            """Rotate tetromino."""
            self.old_rotation = self.rotation
            self.rotation = (self.rotation - int(math.copysign(1, angle))) % 4
            for mino in self.minos:
                mino.rotate(angle)
    
        def shift(self, vector):
            """Move in x and y direction."""
            for mino in self.minos:
                mino.shift(vector)
    
        @classmethod
        def get(cls, shape_id, location, batch):
            """Create Tetromino using shape with matching id."""
            shape = cls.shape_factory.get(shape_id)
            return cls(shape, location, batch)
    
        @classmethod
        def random(cls, location, batch):
            """Create a random Tetromino."""
            shape = cls.shape_factory.random()
            return cls(shape, location, batch)
    In questo modo continuo ad aderire al principio open-close (anche se non ne vedo un gran beneficio, dal momento che le forme in Tetris sono quelle), e come minimo il codice mi diventa piu' ordinato, e quindi leggibile. Per me almeno.




    Non so se ho davvero bisogno di quella classe Shape, adesso. E' solo un container di dati di cui la classe Tetromino necessita. Potrei farne a meno e trasformare ShapeFactory in un TetrominoFactory...