Skip to main content
edited tags
Link
Jamal
  • 35.2k
  • 13
  • 134
  • 238
Source Link
rookie
  • 1.2k
  • 3
  • 14
  • 32

Functional programming in Python: 2048 merge functions

I've written the core functions for a basic 2048 game. I know Python isn't a functional programming language, but I like the "functional style" (easier to maintain, simple to understand, more elegant), and have tried to use this rather than iteration, mutation, etc when I can.

From a functional perspective, what more can be done to improve my code? From an organizational perspective, would it be best to include these functions inside a "game client" class, or better to leave them in a separate module? Right now I have the separate, but coming from a OOP background, my feeling is that the helper functions should be in one class.

def merge(row):
    """ Returns a left merged row with zeros
    
    >>> merge([2, 2, 4, 4])
    [4, 8, 0, 0]
    >>> merge([0, 0, 4, 4])
    [8, 0, 0, 0]
    >>> merge([1, 2, 3, 4])
    [1, 2, 3, 4]
    """
    
    def inner(b, a=[]):
        """ 
        Helper for merge. If we're finished with the list,
        nothing to do; return the accumulator. Otherwise
        if we have more than one element, combine results of first
        with right if they match; skip over right and continue merge
        """
        
        if not b:
            return a
        x = b[0]
        if len(b) == 1:
            return inner(b[1:], a + [x])
        return inner(b[2:], a + [2*x]) if x == b[1] else inner(b[1:], a + [x])

    merged = inner([x for x in row if x != 0])
    return merged + [0]*(len(row)-len(merged))

def reverse(x):
    """ Returns a reversed list of x """
    return list(reversed(x))

def left(b):
    """ Returns a left merged board

    >>> merge([2, 2, 4, 0])
    [4, 4, 0, 0]
    """
    
    return [list(x) for x in map(merge, iter(b))]

def right(b):
    """ Returns a right merged board

    >>> reverse(merge(reverse([2, 2, 4, 0])))
    [0, 0, 4, 4]
    >>> reverse(merge(reverse([4, 4, 4, 4])))
    [0, 0, 8, 8]
    """
    
    t = map(reverse, iter(b))
    return [reverse(x) for x in map(merge, iter(t))]

def up(b):
    """ Returns an upward merged board
        NOTE: zip(*t) is transpose

    >>> b = [[2, 4, 0, 4],[2, 4, 4, 4],[2, 0, 0, 2],[2, 2, 0, 4]]
    >>> up(b)
    [[4, 8, 4, 8], [4, 2, 0, 2], [0, 0, 0, 4], [0, 0, 0, 0]]
    """
    
    t = left(zip(*b))
    return [list(x) for x in zip(*t)]

def down(b):
    """ Returns an upward merged board
        NOTE: zip(*t) is transpose

    >>> b = [[2, 4, 0, 4],[2, 4, 4, 4],[2, 0, 0, 2],[2, 2, 0, 4]]
    >>> down(b)
    [[0, 0, 0, 0], [0, 0, 0, 8], [4, 8, 0, 2], [4, 2, 4, 4]]
    """
    
    t = right(zip(*b))
    return [list(x) for x in zip(*t)]

def can_move(b):
    """ Returns the status (over/not over) of the game

    >>> b = [[1,2,3,4],[5,6,3,8],[1,2,3,4],[5,6,7,8]]
    >>> can_move(b)
    True
    >>> b = [[1,2,3,4],[5,6,7,8],[1,2,3,4],[5,6,7,8]]
    >>> can_move(b)
    False
    >>> b = [[1,2,3,4],[5,6,7,8],[1,2,3,4],[5,6,7,0]]
    >>> can_move(b)
    True
    """
    
    def inner(b):
        for row in b:
            for x, y in zip(row[:-1], row[1:]):
                if x == y or x == 0 or y == 0:
                    return True
        return False
    return inner(b) or inner(zip(*b))

Here's the client:

class Game2048:
    """
    A 2048 game client

    Merge like tiles to reach tile 2048
    """
    def __init__(self):
        self.b = [[0]*4 for i in range(4)]
        self._spawn(2)

    def _spawn(self, k):
        dist = [2]*9 + [4]
        rows = list(range(4))
        cols = list(range(4))
        
        random.shuffle(rows)
        random.shuffle(cols)
        count = 0
        for r, c in itertools.product(rows, cols):
            if count == k:
                return
            if self.b[r][c] == 0:
                self.b[r][c] = random.sample(dist, 1)[0]
                count += 1

    def pprint(self):
        print('\n'.join([''.join(['{:4}'.format(item) for item in row])
                         for row in self.b]))

    def play(self):
        while can_move(self.b):
            self.pprint()
            direc = input("Please enter a direction (w, a, s, d): ")
            
            if   direc == "w": self.b = up(self.b)
            elif direc == "s": self.b = down(self.b)
            elif direc == "a": self.b = left(self.b)
            elif direc == "d": self.b = right(self.b)
            else:
                continue 

            self._spawn(1)