Skip to content

Instantly share code, notes, and snippets.

@Kulbear
Last active July 28, 2019 21:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kulbear/fbc7d19a5f694e5c8d94520dda0612d1 to your computer and use it in GitHub Desktop.
Save Kulbear/fbc7d19a5f694e5c8d94520dda0612d1 to your computer and use it in GitHub Desktop.
2048
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Authors: Ji Yang <jyang7@ualberta.ca>
# License: MIT
# Version: 1.0.0
# Last Updated: May 14, 2017
import random
import sys
from functools import reduce
class Game2048:
"""The game 2048.
Each instance is a unique 2048 game that can be used for training our model later on.
Parameters
----------
upper_bound : int, optional (default=20)
This value will be used for generating a mapping for beautiful print of game boards.
If 20, we will generate a map(dict) contains key-value pair from '2': '2' to '524288': '524288'.
This value has to be greater than 10 in order to have a minimal map for a 2048 game.
Attributes
----------
row : int
The number of rows for the game board.
col : int
The number of columns for the game board.
board : list of lists
The game board in form of [[row1], [row2], [row3], [row4]] with `row` is set to 4.
mapping : dict
The game value map for printing.
score : int
The game score.
moves : list
Valid moves(0 for UP, 1 for DOWN, 2 for LEFT, 3 for RIGHT).
end : bool
Whether the game is ended.
"""
def __init__(self, upper_bound=20):
assert upper_bound > 10
self.row = 4
self.col = 4
self.board = self._generate_board()
self.board = [
[2, 0, 2, 8],
[4, 32, 8, 2],
[2, 8, 64, 16],
[8, 2, 32, 4]
]
self.score = 0
self.end = False
self._moves = [0, 1, 2, 3]
self._mapping = self._generate_mapping(upper_bound)
# self._fill_random_empty_tile()
# self._fill_random_empty_tile()
def _is_empty_tile(self, tile):
"""Return whether the given tile is empty"""
return tile == 0
def _add_score(self, new):
"""Set the score by adding the new score to the current."""
self.score += new
def _generate_board(self):
"""Generate a empty game board."""
return [[0 for _ in range(self.col)] for _ in range(self.row)]
def _generate_mapping(self, upper_bound):
"""Generate a game value map for printing."""
# TODO: support customized mapping (not a feature for AI)
mapping = {str(2 ** power): str(2 ** power) for power in range(1, upper_bound)}
return reduce(lambda x, y: dict(x, **y), ({'0': '0'}, mapping))
def _get_empty_tiles(self):
"""Get coordinates of all empty tiles(in format of [col, row])"""
empty_tiles = []
for y in range(self.row):
for x in range(self.col):
if self._is_empty_tile(self.board[y][x]):
empty_tiles.append([y, x])
return empty_tiles
def _get_num_empty_tiles(self):
"""Get the number of empty tiles remain on the board"""
return len(self._get_empty_tiles())
def _fill_random_empty_tile(self):
"""Randomly fill an empty tile with 2 or 4, prob 90% and 10%, respectively"""
empty_tiles = self._get_empty_tiles()
if empty_tiles:
[i, j] = random.choice(empty_tiles)
self.board[i][j] = 4 if random.random() > 0.9 else 2
def _is_mergeable(self):
"""Return whether there exists tiles are mergeable"""
def is_adjacent_equal(arr):
"""Return whether there exists adjacent tiles have an identical value"""
if len(arr) is 1:
return True
for idx in range(len(arr) - 1):
if arr[idx] == arr[idx + 1]:
return True
return False
def check_all_rows_mergeable():
"""Check mergebility for each row"""
for row_idx in range(self.row):
row = []
for i in self.board[row_idx]:
if i != 0:
row.append(i)
return is_adjacent_equal(row)
def check_all_columns_mergeable():
"""Check mergebility for each column"""
for col_idx in range(self.col):
col = []
for i in self.board:
if i[col_idx] != 0:
col.append(i[col_idx])
return is_adjacent_equal(col)
if self._get_num_empty_tiles() != 0:
return True
return check_all_rows_mergeable() or check_all_columns_mergeable()
def _merge(self, arr, direction):
"""Merge tiles in rows and columns by given direction
Parameters
----------
arr : list
A list represents a row/column. For example, [2, 2, 0, 0] could represent a row/column.
direction: bool
The direction of merging.
If True, merge a row to the left or merge a column to the top.
If False, merge a row to the right or merge a column to the bottom.
Return
----------
tuple
The first element is the merged row/column.
The second element represents whether the merge does change the board.
"""
def squeeze():
"""Get rid of 0s in the row/column"""
nonlocal store, arr
for i in arr:
if i != 0:
store.append(i)
store = []
squeeze()
# If there is only 1 non-zero tiles, no need to do further work
if len(store) is 1:
result = [0 for _ in range(len(arr))]
# To the left/top
if direction:
result[0] = store[0]
# To the right/bottom
else:
result[-1] = store[0]
return result, not arr == result
# Reverse if we are performing a right/downward merge
if not direction:
store = store[::-1]
# Handle special cases where we have [A, A, B, B] in a row/column
# A and B could be identical
if len(store) is 4 and store[0] == store[1] and store[2] == store[3]:
store = [store[0] * 2, store[2] * 2]
else:
# Merge identical neighbors
for idx in range(len(store) - 1):
if store[idx] == store[idx + 1]:
store[idx] *= 2
self._add_score(store[idx + 1] * 2)
store.pop(idx + 1)
break
# Reverse it back if we are performing a right/downward merge
if not direction:
store = store[::-1]
# Keep a length of 4 for a row/column
while len(store) < len(arr):
store.append(0) if direction else store.insert(0, 0)
return store, not arr == store
def _horizontally_merge(self, direction):
"""Merge all rows"""
# Set a flag to tell whether we need to fill an empty tile
should_fill = False
for i in range(0, len(self.board)):
self.board[i], changed = self._merge(self.board[i], direction)
# If there is any row has been changed after a merge
if changed:
should_fill = True
# Fill an empty tile if this merge changes the game state
if should_fill:
self._fill_random_empty_tile()
def _vertically_merge(self, direction):
"""Merge all columns"""
def update_column_by_index(idx, col):
"""Update all columns after merging"""
for row_idx in range(len(self.board)):
self.board[row_idx][idx] = col[row_idx]
# Set a flag to tell whether we need to fill an empty tile
should_fill = False
for col_idx in range(self.col):
# Construct columns
column = [i[col_idx] for i in self.board]
merged_column, changed = self._merge(column, direction)
# If there is any column has been changed after a merge
if changed:
should_fill = True
update_column_by_index(col_idx, merged_column)
if should_fill:
self._fill_random_empty_tile()
def perform_move(self, move):
"""Perform a move on the game board"""
assert move in self._moves
# 0 for UP, 1 for DOWN, 2 for LEFT, 3 for RIGHT
if move == 0:
self._horizontally_merge(True)
elif move == 1:
self._horizontally_merge(False)
elif move == 2:
self._vertically_merge(True)
elif move == 3:
self._vertically_merge(False)
def save_game_state(self):
"""Save the game info we need for further statistics"""
# TODO: Should save the current game score, the highest tile value achieved
pass
def is_lost(self):
"""Return True if the game is ended"""
return not self._is_mergeable()
def print_game(self):
"""Print the game board"""
for row in self.board:
print(row)
def beautiful_print(self):
"""Print the game board gracefully"""
for row in self.board:
print('{}-'.format('------' * self.col))
for entry in row:
print('|', end='')
print(self._mapping[str(entry)].center(4, ' '), end=' ')
print('|')
print('{}-'.format('------' * self.col))
print('Score: {}\nEmpty Tiles: {}'.format(
self.score, self._get_num_empty_tiles()))
print('{}-'.format('------' * self.col))
def request_move():
"""ONLY for human players"""
direction = input('Enter a direction: ')
if direction.upper() == 'Q':
print('Force quit, bye bye~')
sys.exit(0)
else:
if direction.upper() == 'A':
game.perform_move(0)
elif direction.upper() == 'D':
game.perform_move(1)
elif direction.upper() == 'W':
game.perform_move(2)
elif direction.upper() == 'S':
game.perform_move(3)
if __name__ == '__main__':
global game
game = Game2048()
while True:
game.print_game()
if game.is_lost():
print('Game ended.')
break
else:
request_move()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment