2048 in Python
Posted on October 20, 2015
Tags:
A simple implementation of the game 2048 in Python, using ncurses.
It supports different “bases” (other than 2) as well as colors, and uses a kind of Python-y functional style.
Minus comments, the whole thing is 70 lines.
#-----------------------------functional-helpers-------------------------#
from functools import reduce, partial
def compose(*funcs):
"""
Mathematical function composition.
compose(h, g, f)(x) => h(g(f(x)))
"""
return reduce(lambda a,e: lambda x: a(e(x)), funcs, lambda x: x)
#-----------------------------------base---------------------------------#
# The base determines three things:
# - The number of squares which need to be in a row to coalesce (= base)
# - The length of the side of the board (= base^2)
# - The number added to a random blank box on the board at the beginning
# of every turn. (The seed) (90% of the time, the number added will be
# the base, but 10% of the time, it will be the square of the base)
# - The number of seeds added at evey turn (= 2^(base - 2))
#
# Normal 2048 has a base of 2.
= int(input("Choose a base. (2 for normal 2048)\n> "))
base
#-----------------------------------rand---------------------------------#
def addn(board):
"""
Inserts n seeds into random, empty positions in board. Returns board.
n = 2^(base - 2)
The seed is equal to base 90% of the time. 10% of the time, it is
equal to the square of the base.
"""
from random import randrange, sample
= range(base**2)
inds = [(y,x) for y in inds for x in inds if not board[y][x]]
empties for y,x in sample(empties,2**(base-2)):
= base if randrange(10) else base**2
board[y][x] return board
#----------------------------------squish--------------------------------#
from itertools import count, groupby, starmap
def squish(row):
"""
Returns a list, the same length as row, with the contents
"squished" by the rules of 2048.
Boxes are coalesced by adding their values together.
Boxes will be coalesced iff:
- They are adjacent, or there are only empty boxes between them.
- The total number of boxes is equal to the base.
- All the values of the boxes are equal.
For base 2:
[2][2][ ][ ] -> [4][ ][ ][ ]
[2][2][2][2] -> [4][4][ ][ ]
[4][ ][4][2] -> [8][2][ ][ ]
[4][2][4][2] -> [4][2][4][2]
For base 3:
[3][ ][ ][3][ ][ ][3][ ][ ] -> [9][ ][ ][ ][ ][ ][ ][ ][ ]
[3][3][3][3][3][3][3][3][3] -> [9][9][9][ ][ ][ ][ ][ ][ ]
[3][3][3][9][9][ ][ ][ ][ ] -> [9][9][9][ ][ ][ ][ ][ ][ ]
Keyword arguments:
row -- A list, containing a combination of numbers and None
(representing empty boxes)
"""
= []
r for n,x in starmap(lambda n, a: (n, sum(map(bool,a))),
filter(bool, row))):
groupby(+= ([n*base] * (x//base)) + ([n] * (x%base))
r return r + ([None] * (base**2 - len(r)))
#----------------------------matrix-manipulation-------------------------#
# Transposes an iterable of iterables
# [[1, 2], -> [[1, 3],
# [3, 4]] [2, 4]]
def transpose(l): return [list(x) for x in zip(*l)]
# Flips horizontally an iterable of lists
# [[1, 2], -> [[2, 1],
# [3, 4]] [4, 3]]
= partial(map, reversed)
flip
# transforms an iterable of iterables into a list of lists
= compose(list, partial(map, list))
thunk
#----------------------------------moves---------------------------------#
# The move functions take a board as their argument, and return the board
# "squished" in a given direction.
= compose(thunk, partial(map, squish), thunk)
moveLeft = compose(thunk, flip, moveLeft, flip)
moveRight = compose(transpose, moveLeft, transpose)
moveUp = compose(transpose, moveRight, transpose)
moveDown
#-------------------------------curses-init------------------------------#
try:
import curses
= curses.initscr()
screen # Don't print pressed keys
curses.noecho() # Don't wait for enter
curses.cbreak() True)
screen.keypad(False) # Hide cursor
curses.curs_set(
#----------------------------------keymap--------------------------------#
# A map from the arrow keys to the movement functions
= {curses.KEY_RIGHT: moveRight,
moves
curses.KEY_LEFT : moveLeft ,
curses.KEY_UP : moveUp ,
curses.KEY_DOWN : moveDown }
#----------------------------------color---------------------------------#
curses.start_color()
curses.use_default_colors()1, curses.COLOR_WHITE, -1) # Border color
curses.init_pair(
def colorfac():
"""Initializes a color pair and returns it (skips black)"""
for i,c in zip(count(2),(c for c in count(1) if c!=curses.COLOR_BLACK)):
-1)
curses.init_pair(i, c, yield curses.color_pair(i)
= colorfac()
colorgen
from collections import defaultdict
# A cache of colors, with the keys corresponding to numbers on the board.
= defaultdict(lambda: next(colorgen))
colors
#---------------------------printing-the-board---------------------------#
= max(11 - base*2, 3) # box width
size
def printBoard(board):
def line(b,c): return b + b.join([c*(size)]*len(board)) + b
= line("+","-"), line("|"," ")
border, gap = "\n" + "\n".join([gap]*((size-2)//4)) if size > 5 else ""
pad 0, 0, border, curses.color_pair(1))
screen.addstr(for row in board:
+ "\n|", curses.color_pair(1))
screen.addstr(pad for e in row:
if e: screen.addstr(str(e).center(size), colors[e])
else: screen.addstr(" " * size)
"|", curses.color_pair(1))
screen.addstr(+ "\n" + border, curses.color_pair(1))
screen.addstr(pad
#----------------------------------board---------------------------------#
# The board is a list of n lists, each of length n, where n is the base
# squared. Empty boxes are represented by None. The starting board has
# one seed.
= addn([[None for _ in range(base**2)] for _ in range(base**2)])
board
printBoard(board)
#----------------------------------game-loop-----------------------------#
# The main game loop. Continues until there are not enough empty spaces
# on the board, or "q" is pressed.
for char in filter(moves.__contains__, iter(screen.getch, ord("q"))):
= moves[char](board)
moved if sum(not n for r in moved for n in r) < 2**(base-2): break
if moved != board: board = addn(moved)
printBoard(board)
#--------------------------------clean-up--------------------------------#
finally:
# Wait for enter
curses.nocbreak() 0) # Stop arrow-key handling
screen.keypad(# Print all keyboard input
curses.echo() True) # Show cursor
curses.curs_set(# Return to normal prompt curses.endwin()