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)