Twenty Forty Eight

Tags: large, game, puzzle

Gabriele Cirulli, a web developer, invented the game 2048 in one weekend. It was inspired by Veewo Studios’ 1024 game, which in turn was inspired by Threes!, a game by the development team Sirvo. In 2048, you must merge numbers on a 4 × 4 board to clear them from the screen. Two 2s merge into a 4, two 4s merge into an 8, and so on. The game adds a new 2 to the board on each merging. The objective is to reach 2048 before the entire board fills up.

twenty_forty_eight.py
  1"""Twenty Forty-Eight, by Al Sweigart al@inventwithpython.com
  2A sliding tile game to combine exponentially-increasing numbers.
  3Inspired by Gabriele Cirulli's 2048, which is a clone of Veewo Studios'
  41024, which in turn is a clone of the Threes! game.
  5More info at https://en.wikipedia.org/wiki/2048_(video_game)
  6This code is available at https://nostarch.com/big-book-small-python-programming
  7Tags: large, game, puzzle"""
  8
  9import random, sys
 10
 11# Set up the constants:
 12BLANK = ''  # A value that represents a blank space on the board.
 13
 14
 15def main():
 16    print('''Twenty Forty-Eight, by Al Sweigart al@inventwithpython.com
 17
 18Slide all the tiles on the board in one of four directions. Tiles with
 19like numbers will combine into larger-numbered tiles. A new 2 tile is
 20added to the board on each move. You win if you can create a 2048 tile.
 21You lose if the board fills up the tiles before then.''')
 22    input('Press Enter to begin...')
 23
 24    gameBoard = getNewBoard()
 25
 26    while True:  # Main game loop.
 27        drawBoard(gameBoard)
 28        print('Score:', getScore(gameBoard))
 29        playerMove = askForPlayerMove()
 30        gameBoard = makeMove(gameBoard, playerMove)
 31        addTwoToBoard(gameBoard)
 32
 33        if isFull(gameBoard):
 34            drawBoard(gameBoard)
 35            print('Game Over - Thanks for playing!')
 36            sys.exit()
 37
 38
 39def getNewBoard():
 40    """Returns a new data structure that represents a board.
 41
 42    It's a dictionary with keys of (x, y) tuples and values of the tile
 43    at that space. The tile is either a power-of-two integer or BLANK.
 44    The coordinates are laid out as:
 45       X0 1 2 3
 46      Y+-+-+-+-+
 47      0| | | | |
 48       +-+-+-+-+
 49      1| | | | |
 50       +-+-+-+-+
 51      2| | | | |
 52       +-+-+-+-+
 53      3| | | | |
 54       +-+-+-+-+"""
 55
 56    newBoard = {}  # Contains the board data structure to be returned.
 57    # Loop over every possible space and set all the tiles to blank:
 58    for x in range(4):
 59        for y in range(4):
 60            newBoard[(x, y)] = BLANK
 61
 62    # Pick two random spaces for the two starting 2's:
 63    startingTwosPlaced = 0  # The number of starting spaces picked.
 64    while startingTwosPlaced < 2:  # Repeat for duplicate spaces.
 65        randomSpace = (random.randint(0, 3), random.randint(0, 3))
 66        # Make sure the randomly selected space isn't already taken:
 67        if newBoard[randomSpace] == BLANK:
 68            newBoard[randomSpace] = 2
 69            startingTwosPlaced = startingTwosPlaced + 1
 70
 71    return newBoard
 72
 73
 74def drawBoard(board):
 75    """Draws the board data structure on the screen."""
 76
 77    # Go through each possible space left to right, top to bottom, and
 78    # create a list of what each space's label should be.
 79    labels = []  # A list of strings for the number/blank for that tile.
 80    for y in range(4):
 81        for x in range(4):
 82            tile = board[(x, y)]  # Get the tile at this space.
 83            # Make sure the label is 5 spaces long:
 84            labelForThisTile = str(tile).center(5)
 85            labels.append(labelForThisTile)
 86
 87    # The {} are replaced with the label for that tile:
 88    print("""
 89+-----+-----+-----+-----+
 90|     |     |     |     |
 91|{}|{}|{}|{}|
 92|     |     |     |     |
 93+-----+-----+-----+-----+
 94|     |     |     |     |
 95|{}|{}|{}|{}|
 96|     |     |     |     |
 97+-----+-----+-----+-----+
 98|     |     |     |     |
 99|{}|{}|{}|{}|
100|     |     |     |     |
101+-----+-----+-----+-----+
102|     |     |     |     |
103|{}|{}|{}|{}|
104|     |     |     |     |
105+-----+-----+-----+-----+
106""".format(*labels))
107
108
109def getScore(board):
110    """Returns the sum of all the tiles on the board data structure."""
111    score = 0
112    # Loop over every space and add the tile to the score:
113    for x in range(4):
114        for y in range(4):
115            # Only add non-blank tiles to the score:
116            if board[(x, y)] != BLANK:
117                score = score + board[(x, y)]
118    return score
119
120
121def combineTilesInColumn(column):
122    """The column is a list of four tile. Index 0 is the "bottom" of
123    the column, and tiles are pulled "down" and combine if they are the
124    same. For example, combineTilesInColumn([2, BLANK, 2, BLANK])
125    returns [4, BLANK, BLANK, BLANK]."""
126
127    # Copy only the numbers (not blanks) from column to combinedTiles
128    combinedTiles = []  # A list of the non-blank tiles in column.
129    for i in range(4):
130        if column[i] != BLANK:
131            combinedTiles.append(column[i])
132
133    # Keep adding blanks until there are 4 tiles:
134    while len(combinedTiles) < 4:
135        combinedTiles.append(BLANK)
136
137    # Combine numbers if the one "above" it is the same, and double it.
138    for i in range(3):  # Skip index 3: it's the topmost space.
139        if combinedTiles[i] == combinedTiles[i + 1]:
140            combinedTiles[i] *= 2  # Double the number in the tile.
141            # Move the tiles above it down one space:
142            for aboveIndex in range(i + 1, 3):
143                combinedTiles[aboveIndex] = combinedTiles[aboveIndex + 1]
144            combinedTiles[3] = BLANK  # Topmost space is always BLANK.
145    return combinedTiles
146
147
148def makeMove(board, move):
149    """Carries out the move on the board.
150
151    The move argument is either 'W', 'A', 'S', or 'D' and the function
152    returns the resulting board data structure."""
153
154    # The board is split up into four columns, which are different
155    # depending on the direction of the move:
156    if move == 'W':
157        allColumnsSpaces = [[(0, 0), (0, 1), (0, 2), (0, 3)],
158                            [(1, 0), (1, 1), (1, 2), (1, 3)],
159                            [(2, 0), (2, 1), (2, 2), (2, 3)],
160                            [(3, 0), (3, 1), (3, 2), (3, 3)]]
161    elif move == 'A':
162        allColumnsSpaces = [[(0, 0), (1, 0), (2, 0), (3, 0)],
163                            [(0, 1), (1, 1), (2, 1), (3, 1)],
164                            [(0, 2), (1, 2), (2, 2), (3, 2)],
165                            [(0, 3), (1, 3), (2, 3), (3, 3)]]
166    elif move == 'S':
167        allColumnsSpaces = [[(0, 3), (0, 2), (0, 1), (0, 0)],
168                            [(1, 3), (1, 2), (1, 1), (1, 0)],
169                            [(2, 3), (2, 2), (2, 1), (2, 0)],
170                            [(3, 3), (3, 2), (3, 1), (3, 0)]]
171    elif move == 'D':
172        allColumnsSpaces = [[(3, 0), (2, 0), (1, 0), (0, 0)],
173                            [(3, 1), (2, 1), (1, 1), (0, 1)],
174                            [(3, 2), (2, 2), (1, 2), (0, 2)],
175                            [(3, 3), (2, 3), (1, 3), (0, 3)]]
176
177    # The board data structure after making the move:
178    boardAfterMove = {}
179    for columnSpaces in allColumnsSpaces:  # Loop over all 4 columns.
180        # Get the tiles of this column (The first tile is the "bottom"
181        # of the column):
182        firstTileSpace = columnSpaces[0]
183        secondTileSpace = columnSpaces[1]
184        thirdTileSpace = columnSpaces[2]
185        fourthTileSpace = columnSpaces[3]
186
187        firstTile = board[firstTileSpace]
188        secondTile = board[secondTileSpace]
189        thirdTile = board[thirdTileSpace]
190        fourthTile = board[fourthTileSpace]
191
192        # Form the column and combine the tiles in it:
193        column = [firstTile, secondTile, thirdTile, fourthTile]
194        combinedTilesColumn = combineTilesInColumn(column)
195
196        # Set up the new board data structure with the combined tiles:
197        boardAfterMove[firstTileSpace] = combinedTilesColumn[0]
198        boardAfterMove[secondTileSpace] = combinedTilesColumn[1]
199        boardAfterMove[thirdTileSpace] = combinedTilesColumn[2]
200        boardAfterMove[fourthTileSpace] = combinedTilesColumn[3]
201
202    return boardAfterMove
203
204
205def askForPlayerMove():
206    """Asks the player for the direction of their next move (or quit).
207
208    Ensures they enter a valid move: either 'W', 'A', 'S' or 'D'."""
209    print('Enter move: (WASD or Q to quit)')
210    while True:  # Keep looping until they enter a valid move.
211        move = input('> ').upper()
212        if move == 'Q':
213            # End the program:
214            print('Thanks for playing!')
215            sys.exit()
216
217        # Either return the valid move, or loop back and ask again:
218        if move in ('W', 'A', 'S', 'D'):
219            return move
220        else:
221            print('Enter one of "W", "A", "S", "D", or "Q".')
222
223
224def addTwoToBoard(board):
225    """Adds a new 2 tile randomly to the board."""
226    while True:
227        randomSpace = (random.randint(0, 3), random.randint(0, 3))
228        if board[randomSpace] == BLANK:
229            board[randomSpace] = 2
230            return  # Return after finding one non-blank tile.
231
232
233def isFull(board):
234    """Returns True if the board data structure has no blanks."""
235    # Loop over every space on the board:
236    for x in range(4):
237        for y in range(4):
238            # If a space is blank, return False:
239            if board[(x, y)] == BLANK:
240                return False
241    return True  # No space is blank, so return True.
242
243
244# If this program was run (instead of imported), run the game:
245if __name__ == '__main__':
246    try:
247        main()
248    except KeyboardInterrupt:
249        sys.exit()  # When Ctrl-C is pressed, end the program.

https://inventwithpython.com/bigbookpython/project79.html