4
\$\begingroup\$

I'm currently coding a game of Pentominoes and I would like to hear suggestions on how to improve my code and make it more pythonic.

Rules and goal of the game

There is an 8x8 grid and you have 12 pentominos. The goal of the game is to lay all the pentominos on the grid without any overlapping, nor any pentomino partially being out of the grid.

A pentomino is a geometric shape made up of 5 squares. 12 such shapes are possible (hence the goal). You are allowed to rotate a pentomino a quarter turn clockwise or counter-clockwise as many times as you want. You are also allowed to mirror a pentomino both vertically and horizontally (also as many times as desired).

Each pentomino must be laid once and only once on the grid.

Actual code

Here's the conceptual part that can be (tediously) played in a python REPL; the visual part has not been taken care of yet:

GRID_DIM = 8

class Grid:
    """
    The Grid is the place where all the pentominos have to be placed..
    in order to win the game. This is an 8x8 grid. 
    attributes:
        '_map' is a dict of {coords: case} with 'case' being what's sitting at said 'coords'
        '_game_over' is boolean that's False all game long and becomes True when..
          all the pentominos have been put down on the grid
        '_laid_pentos' is a list containing all the letters symbolizing pentominos..
          that are on the grid
    """

    def __init__(self):
        self._game_over = False    
        self._laid_pentos = []
        self._map = dict()       
        # '_map' initialisation
        for y in range(GRID_DIM):
            for x in range(GRID_DIM):
                self._map[(x, y)] = '.'   # unoccupied case will be '.'

    # drops 'pento' at given coordinates
    def drop_pento_at(self, pento, x_drop, y_drop):
        letter = pento.letter
        if letter not in self._laid_pentos:
            self._laid_pentos.append(letter)
            try:
                for x in range(pento.width):
                    for y in range(pento.height):

                        # empty cases gets replaced
                        if self._map[(x_drop + x, y_drop + y)] == '.':
                            self._map[(x_drop + x, y_drop + y)] = pento[y][x]

                        # if case is occupied and pento's corresponding case..
                        # is not '.' then, droping this pentomino here ..
                        # would make two pentominos overlap
                        elif pento[y][x] != '.':
                            raise ValueError('pentomino {} cannot be dropped at\
 coordinates ({},{})'.format(pento.letter, x_drop, y_drop))

            # canceling changes 
            except (ValueError, KeyError):
                self._laid_pentos.remove(letter)
                for coords in self._map:
                    if self._map[coords] == letter:
                        self._map[coords] = '.'
        # checking how many pentominos have already been laid
        if len(self._laid_pentos) == PENTO_COUNT:
            self._game_over = True
            print('You win, congrats old chap')

    # returns pentomino sitting at given coordinates
    def grab_pento_at(self, x_grab, y_grab):
        # determining what pentomino needs to be removed
        letter = self._map[(x_grab, y_grab)]

        if letter != '.':
            # removing 'letter' from '_laid_pentos'
            self._laid_pentos.remove(letter)

            # determining the area containing pentomino at '(x_grab, y_grab)'
            x_coords = [coords[0] for coords in self._map if self._map[coords] == letter]
            y_coords = [coords[1] for coords in self._map if self._map[coords] == letter]
            x_min, x_max = min(x_coords), max(x_coords)
            y_min, y_max = min(y_coords), max(y_coords)

            # constructing a pentomino's surface
            surface_pento = []
            for y in range(y_min, y_max+1):
                new_row = []
                for x in range(x_min, x_max+1):
                    if self._map[(x, y)] == letter:
                        self._map[(x, y)] = '.'
                        new_row.append(letter)
                    else:
                        new_row.append('.')
                surface_pento.append(tuple(new_row))
            return Pentomino(surface=surface_pento)

        # returns None if grab is done on empty case
        else:
            return None

    # for console representation
    def __str__(self):
        representation = str()
        for y in range(GRID_DIM):
            for x in range(GRID_DIM):
                representation += self._map[(x, y)]
            representation += '\n'
        return representation


# All the Pentominos

PENTO_COUNT = 12
PENTO_Y = [('.', '.', 'Y', '.'), 
           ('Y', 'Y', 'Y', 'Y')]
PENTO_T = [('T', '.', '.'),
           ('T', 'T', 'T'),
           ('T', '.', '.')]
PENTO_V = [('V', 'V', 'V'),
           ('V', '.', '.'),
           ('V', '.', '.')]
PENTO_N = [('N', 'N', 'N', '.'),
           ('.', '.', 'N', 'N')]
PENTO_W = [('W', 'W', '.'),
           ('.', 'W', 'W'),
           ('.', '.', 'W')]
PENTO_I = [('I', 'I', 'I', 'I', 'I')]
PENTO_X = [('.', 'X', '.'),
           ('X', 'X', 'X'),
           ('.', 'X', '.')]
PENTO_Z = [('Z', '.', '.'),
           ('Z', 'Z', 'Z'),
           ('.', '.', 'Z')]
PENTO_U = [('U', 'U', 'U'),
           ('U', '.', 'U')]
PENTO_P = [('P', 'P', 'P'),
           ('P', 'P', '.')]
PENTO_L = [('L', 'L', 'L', 'L'),
           ('L', '.', '.', '.')]
PENTO_F = [('.', 'F', 'F'),
           ('F', 'F', '.'),
           ('.', 'F', '.')]
PENTO_DICT = {'Y': PENTO_Y, 'T': PENTO_T, 'V': PENTO_V, 'N': PENTO_N,
              'W': PENTO_W, 'I': PENTO_I, 'X': PENTO_X, 'Z': PENTO_Z,
              'U': PENTO_U, 'P': PENTO_P, 'L': PENTO_L, 'F': PENTO_F}

class Pentomino:
    """
    A Pentomino is a figure taking up 5 squares.
    There are twelve pentominos in the game.
    attributes:
        '_letter' is a single char informing the shape of the pentomino
        '_surface' is a list of tuples representing the pentomino in space
    """

    def __init__(self, **kwargs):
        """
        Initialising is done with keyword: either a 'letter' or a 'surface' is given.
        If a 'letter' is given the pentomino will be have a predefined '_surface' attribute,
        from 'PENTA_DICT'
        Otherwise a 'surface' must be given, that will be the '_surface' attribute. This
        way is not meant to be used by anything else than Grid's method 'grab_penta_at' 
        """
        for key, item in kwargs.items():
            # initialising pentaminoe with basic surface from 'PENTA_DICT'
            if key == 'letter':
                self._letter = item.upper()
                try:
                    self._surface = PENTO_DICT[self._letter]
                except KeyError:
                    raise ValueError('Pentomino {} does not exist'.format(item))
                break

            # initialising pentomino with given 'surface'
            elif key == 'surface':
                self._surface = item
                for a_char in self._surface[0]:
                    if a_char != '.':
                        self._letter = a_char
                        break
                break

    @property
    def letter(self):
        return self._letter

    @property
    def width(self):
        return len(self._surface[0])

    @property
    def height(self):
        return len(self._surface)

    # rotates the pentomino clockwise
    def right_rot(self):
        temp_figure = reversed(self._surface)
        self._surface = list(zip(*temp_figure))

    # rotates the pentomino counter-clockwise
    def left_rot(self):
        temp_figure = [reversed(a_line) for a_line in self._surface]
        self._surface = list(zip(*temp_figure))

    # mirrors the pentomino along the vertical axis
    def x_mirror(self):
        self._surface = [tuple(reversed(a_line)) for a_line in self._surface]

    # mirrors the pentomino along the horizontal axis
    def y_mirror(self):
        self._surface = list(reversed(self._surface))

    # returns items from 'surface'
    def __getitem__(self, position):
        return self._surface[position]

    # for console representation
    def __str__(self):
        representation = str()
        for y in range(self.height):
            for x in range(self.width):
                representation += self._surface[y][x]
            representation += '\n'
        return representation

How to play

copy-paste the whole thing in your favourite text editor and save as pentos.py. Launch your python3 from the directory it's in and type:

from pentos import *
grd = Grid()

Then you instantiate the Pentomino class with:

# a_letter must be 'f', 'i', 'l', 'n', 'p', 't', 'u', 'v', 'w', 'x', 'y' or 'z'
p = Pentomino(letter=a_letter)

From there:

  • rotation is done with p.right_rot() and p.left_rot()
  • you can mirror p with p.x_mirror() and p.y_mirror()
  • you can have a look at your pentomino with print(p)
  • to lay your pentomino on the grid type grd.drop_pento_at(p, x, y) and pentomino will be laid in such a way that top left corner of p (actually top left corner of what's displayed when you call print(p)) will be at coordinates (x, y).
  • to withdraw a pentomino from the grid, grab it with p2 = grd.grab_pento_at(x, y), (x, y) being any coordinates that pentomino occupies.

Lastly, coordinates are like this: (0, 0) is top left corner of grd

\$\endgroup\$
1
  • \$\begingroup\$ I think you should ask the user for a move inside the code, rather then having to do it after you run the program. \$\endgroup\$ – Jerry Cui May 21 '19 at 0:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.