class Game(object): """ A 2048 game """ __dirs = { keypress.UP: Board.UP, keypress.DOWN: Board.DOWN, keypress.LEFT: Board.LEFT, keypress.RIGHT: Board.RIGHT, } __clear = 'cls' if os.name == 'nt' else 'clear' def __init__(self, **kws): """ Create a new game. """ self.board = Board(**kws) self.score = 0 self.__colors = { 2: Fore.GREEN, 4: Fore.BLUE + Style.BRIGHT, 8: Fore.CYAN, 16: Fore.RED, 32: Fore.MAGENTA, 64: Fore.CYAN, 128: Fore.BLUE + Style.BRIGHT, 256: Fore.MAGENTA, 512: Fore.GREEN, 1024: Fore.RED, 2048: Fore.YELLOW, # just in case people set an higher goal they still have colors 4096: Fore.RED, 8192: Fore.CYAN, } def incScore(self, pts): """ update the current score by adding it the specified number of points """ self.score += pts def end(self): """ return True if the game is finished """ return not (self.board.won() or self.board.canMove()) def readMove(self): """ read and return a move to pass to a board """ k = keypress.getKey() return Game.__dirs.get(k) def loop(self): """ main game loop. returns the final score. """ try: while True: os.system(Game.__clear) print(self.__str__(margins={'left': 4, 'top': 4, 'bottom': 4})) if self.board.won() or not self.board.canMove(): break m = self.readMove() self.incScore(self.board.move(m)) except KeyboardInterrupt: return print('You won!' if self.board.won() else 'Game Over') return self.score def getCellStr(self, x, y): # TODO: refactor regarding issue #11 """ return a string representation of the cell located at x,y. """ c = self.board.getCell(x, y) if c == 0: return ' .' elif c == 1024: s = ' 1k' elif c == 2048: s = ' 2k' else: s = '%3d' % c return self.__colors.get(c, Fore.RESET) + s + Style.RESET_ALL def boardToString(self, margins={}): """ return a string representation of the current board. """ b = self.board rg = range(b.size()) left = ' '*margins.get('left', 0) s = '\n'.join( [left + ' '.join([self.getCellStr(x, y) for x in rg]) for y in rg]) return s def __str__(self, margins={}): b = self.boardToString(margins=margins) top = '\n'*margins.get('top', 0) bottom = '\n'*margins.get('bottom', 0) scores = ' \tScore: %5d\n' % (self.score) return top + b.replace('\n', scores, 1) + bottom
class Game(object): """ A 2048 game """ __dirs = { keypress.UP: Board.UP, keypress.DOWN: Board.DOWN, keypress.LEFT: Board.LEFT, keypress.RIGHT: Board.RIGHT, } __clear = 'cls' if os.name == 'nt' else 'clear' COLORS = { 2: Fore.GREEN, 4: Fore.BLUE, 8: Fore.CYAN, 16: Fore.RED, 32: Fore.MAGENTA, 64: Fore.CYAN, 128: Fore.BLUE, 256: Fore.MAGENTA, 512: Fore.GREEN, 1024: Fore.RED, 2048: Fore.YELLOW, } # see Game#adjustColors # these are color replacements for various modes __color_modes = { 'dark': { Fore.BLUE: Fore.WHITE, }, 'light': { Fore.YELLOW: Fore.BLACK, }, } SCORES_FILE = '%s/.term2048.scores' % os.path.expanduser('~') def __init__(self, scores_file=SCORES_FILE, colors=COLORS, mode=None, **kws): """ Create a new game. scores_file: file to use for the best score (default is ~/.term2048.scores) colors: dictionnary with colors to use for each tile mode: color mode. This adjust a few colors and can be 'dark' or 'light'. See the adjustColors functions for more info. other options are passed to the underlying Board object. """ self.board = Board(**kws) self.score = 0 self.scores_file = scores_file self.__colors = colors self.loadBestScore() self.adjustColors(mode) def adjustColors(self, mode='dark'): """ Change a few colors depending on the mode to use. The default mode doesn't assume anything and avoid using white & black colors. The dark mode use white and avoid dark blue while the light mode use black and avoid yellow, to give a few examples. """ rp = Game.__color_modes.get(mode, {}) for k, color in self.__colors.items(): self.__colors[k] = rp.get(color, color) def loadBestScore(self): """ load local best score from the default file """ if self.scores_file is None or not os.path.exists(self.scores_file): self.best_score = 0 return try: f = open(self.scores_file, 'r') self.best_score = int(f.readline(), 10) f.close() except: pass # fail silently def saveBestScore(self): """ save current best score in the default file """ if self.score > self.best_score: self.best_score = self.score try: f = open(self.scores_file, 'w') f.write(str(self.best_score)) f.close() except: pass # fail silently def end(self): """ return True if the game is finished """ return not (self.board.won() or self.board.canMove()) def readMove(self): """ read and return a move to pass to a board """ k = keypress.getArrowKey() return Game.__dirs.get(k) def loop(self): """ main game loop """ while True: os.system(Game.__clear) print(self.__str__(margins={'left':4, 'top':4, 'bottom':4})) if self.board.won() or not self.board.canMove(): break try: m = self.readMove() except KeyboardInterrupt: self.saveBestScore() return self.score += self.board.move(m) if self.score > self.best_score: self.best_score = self.score self.saveBestScore() print('You won!' if self.board.won() else 'Game Over') def getCellStr(self, x, y): """ return a string representation of the cell located at x,y. """ c = self.board.getCell(x, y) if c == 0: return ' .' if c == 1024: s = ' 1k' elif c == 2048: s = ' 2k' else: s = '%3d' % c return self.__colors.get(c, Fore.RESET) + s + Fore.RESET def boardToString(self, margins={}): """ return a string representation of the current board. """ b = self.board rg = xrange(b.size()) left = ' '*margins.get('left', 0) s = '\n'.join( [left + ' '.join([self.getCellStr(x, y) for x in rg]) for y in rg]) return s def __str__(self, margins={}): b = self.boardToString(margins=margins) top = '\n'*margins.get('top', 0) bottom = '\n'*margins.get('bottom', 0) scores = ' \tScore: %5d Best: %5d\n' % (self.score, self.best_score) return top + b.replace('\n', scores, 1) + bottom
class Game(object): """ A 2048 game """ __dirs = { keypress.UP: Board.UP, keypress.DOWN: Board.DOWN, keypress.LEFT: Board.LEFT, keypress.RIGHT: Board.RIGHT, keypress.SPACE: Board.PAUSE, } __is_windows = os.name == 'nt' COLORS = { 2: Fore.GREEN, 4: Fore.BLUE + Style.BRIGHT, 8: Fore.CYAN, 16: Fore.RED, # Don't use MAGENTA directly; it doesn't display well on Windows. # see https://github.com/bfontaine/term2048/issues/24 32: Fore.MAGENTA + Style.BRIGHT, 64: Fore.CYAN, 128: Fore.BLUE + Style.BRIGHT, 256: Fore.MAGENTA + Style.BRIGHT, 512: Fore.GREEN, 1024: Fore.RED, 2048: Fore.YELLOW, # just in case people set an higher goal they still have colors 4096: Fore.RED, 8192: Fore.CYAN, } # see Game#adjustColors # these are color replacements for various modes __color_modes = { 'dark': { Fore.BLUE: Fore.WHITE, Fore.BLUE + Style.BRIGHT: Fore.WHITE, }, 'light': { Fore.YELLOW: Fore.BLACK, }, } SCORES_FILE = '%s/.term2048.scores' % os.path.expanduser('~') STORE_FILE = '%s/.term2048.store' % os.path.expanduser('~') def __init__(self, scores_file=SCORES_FILE, colors=COLORS, store_file=STORE_FILE, clear_screen=True, mode=None, azmode=False, **kws): """ Create a new game. scores_file: file to use for the best score (default is ~/.term2048.scores) colors: dictionnary with colors to use for each tile store_file: file that stores game session's snapshot mode: color mode. This adjust a few colors and can be 'dark' or 'light'. See the adjustColors functions for more info. other options are passed to the underlying Board object. """ self.board = Board(**kws) self.score = 0 self.scores_file = scores_file self.store_file = store_file self.clear_screen = clear_screen self.__colors = colors self.__azmode = azmode self.loadBestScore() self.adjustColors(mode) def adjustColors(self, mode='dark'): """ Change a few colors depending on the mode to use. The default mode doesn't assume anything and avoid using white & black colors. The dark mode use white and avoid dark blue while the light mode use black and avoid yellow, to give a few examples. """ rp = Game.__color_modes.get(mode, {}) for k, color in self.__colors.items(): self.__colors[k] = rp.get(color, color) def loadBestScore(self): """ load local best score from the default file """ try: with open(self.scores_file, 'r') as f: self.best_score = int(f.readline(), 10) except: self.best_score = 0 return False return True def saveBestScore(self): """ save current best score in the default file """ if self.score > self.best_score: self.best_score = self.score try: with open(self.scores_file, 'w') as f: f.write(str(self.best_score)) except: return False return True def incScore(self, pts): """ update the current score by adding it the specified number of points """ self.score += pts if self.score > self.best_score: self.best_score = self.score def readMove(self): """ read and return a move to pass to a board """ k = keypress.getKey() return Game.__dirs.get(k) def store(self): """ save the current game session's score and data for further use """ size = self.board.SIZE cells = [] for i in range(size): for j in range(size): cells.append(str(self.board.getCell(j, i))) score_str = "%s\n%d" % (' '.join(cells), self.score) try: with open(self.store_file, 'w') as f: f.write(score_str) except: return False return True def restore(self): """ restore the saved game score and data """ size = self.board.SIZE try: with open(self.store_file, 'r') as f: lines = f.readlines() score_str = lines[0] self.score = int(lines[1]) except: return False score_str_list = score_str.split(' ') count = 0 for i in range(size): for j in range(size): value = score_str_list[count] self.board.setCell(j, i, int(value)) count += 1 return True def clearScreen(self): """Clear the console""" if self.clear_screen: os.system('cls' if self.__is_windows else 'clear') else: print('\n') def hideCursor(self): """ Hide the cursor. Don't forget to call ``showCursor`` to restore the normal shell behavior. This is a no-op if ``clear_screen`` is falsy. """ if not self.clear_screen: return if not self.__is_windows: sys.stdout.write('\033[?25l') def showCursor(self): """Show the cursor.""" if not self.__is_windows: sys.stdout.write('\033[?25h') def loop(self): """ main game loop. returns the final score. """ pause_key = self.board.PAUSE margins = {'left': 4, 'top': 4, 'bottom': 4} atexit.register(self.showCursor) try: self.hideCursor() while True: self.clearScreen() print(self.__str__(margins=margins)) if self.board.won() or not self.board.canMove(): break m = self.readMove() if (m == pause_key): self.saveBestScore() if self.store(): print("Game successfully saved. " "Resume it with `term2048 --resume`.") return self.score print("An error ocurred while saving your game.") return self.incScore(self.board.move(m)) except KeyboardInterrupt: self.saveBestScore() return self.saveBestScore() print('You won!' if self.board.won() else 'Game Over') return self.score def getCellStr(self, x, y): # TODO: refactor regarding issue #11 """ return a string representation of the cell located at x,y. """ c = self.board.getCell(x, y) if c == 0: return '.' if self.__azmode else ' .' elif self.__azmode: az = {} for i in range(1, int(math.log(self.board.goal(), 2))): az[2**i] = chr(i + 96) if c not in az: return '?' s = az[c] elif c == 1024: s = ' 1k' elif c == 2048: s = ' 2k' else: s = '%3d' % c return self.__colors.get(c, Fore.RESET) + s + Style.RESET_ALL def boardToString(self, margins={}): """ return a string representation of the current board. """ b = self.board rg = range(b.size()) left = ' ' * margins.get('left', 0) s = '\n'.join( [left + ' '.join([self.getCellStr(x, y) for x in rg]) for y in rg]) return s def __str__(self, margins={}): b = self.boardToString(margins=margins) top = '\n' * margins.get('top', 0) bottom = '\n' * margins.get('bottom', 0) scores = ' \tScore: %5d Best: %5d\n' % (self.score, self.best_score) return top + b.replace('\n', scores, 1) + bottom
class TestBoard(unittest.TestCase): def setUp(self): self.b = Board() # == init == # def test_init_dimensions(self): self.assertEqual(len(self.b.cells), Board.SIZE) self.assertEqual(len(self.b.cells[0]), Board.SIZE) if Board.SIZE > 1: self.assertEqual(len(self.b.cells[1]), Board.SIZE) def test_init_dimensions_1(self): b = Board(size=1) c = b.cells[0][0] self.assertTrue(c in [2, 4]) def test_init_dimensions_3_goal_4(self): b = Board(size=3, goal=4) self.assertEqual(b.size(), 3) def test_init_only_two_tiles(self): t = 0 for x in xrange(Board.SIZE): for y in xrange(Board.SIZE): c = self.b.cells[y][x] if not c == 0: t += 1 else: self.assertEqual(c, 0, 'board[%d][%d] should be 0' % (y, x)) self.assertEqual(t, 2) def test_init_not_won(self): self.assertFalse(self.b.won()) def test_init_not_filled(self): self.assertFalse(self.b.filled()) # == .size == # def test_size(self): s = 42 b = Board(size=s) self.assertEqual(b.size(), s) # == .won == # def test_won(self): self.b._Board__won = True self.assertTrue(self.b.won()) self.b._Board__won = False self.assertFalse(self.b.won()) # == .canMove == # def test_canMove_no_empty_cell(self): b = Board(size=1) b.setCell(0, 0, 42) self.assertFalse(b.canMove()) def test_canMove_empty_cell(self): b = Board(size=2) self.assertTrue(b.canMove()) def test_canMove_no_empty_cell_can_collapse(self): b = Board(size=2) b.cells = [ [2, 2], [4, 8] ] self.assertTrue(b.canMove()) # == .filled == # def test_filled(self): self.b.cells = [[1]*Board.SIZE for _ in xrange(Board.SIZE)] self.assertTrue(self.b.filled()) # == .addTile == # def test_addTile(self): b = Board(size=1) b.cells = [[0]] b.addTile(value=42) self.assertEqual(b.cells[0][0], 42) # == .getCell == # def test_getCell(self): x, y = 3, 1 v = 42 self.b.cells[y][x] = v self.assertEqual(self.b.getCell(x, y), v) # == .setCell == # def test_setCell(self): x, y = 2, 3 v = 42 self.b.setCell(x, y, v) self.assertEqual(self.b.cells[y][x], v) # == .getLine == # def test_getLine(self): b = Board(size=4) l = [42, 17, 12, 3] b.cells = [ [0]*4, l, [0]*4, [0]*4 ] self.assertSequenceEqual(b.getLine(1), l) # == .getCol == # def test_getCol(self): s = 4 b = Board(size=s) l = [42, 17, 12, 3] b.cells = [[l[i], 4, 1, 2] for i in xrange(s)] self.assertSequenceEqual(b.getCol(0), l) # == .setLine == # def test_setLine(self): i = 2 l = [1, 2, 3, 4] self.b.setLine(i, l) self.assertEqual(self.b.getLine(i), l) # == .setCol == # def test_setLine(self): i = 2 l = [1, 2, 3, 4] self.b.setCol(i, l) self.assertEqual(self.b.getCol(i), l) # == .getEmptyCells == # def test_getEmptyCells(self): self.assertEqual(len(self.b.getEmptyCells()), Board.SIZE**2 - 2) def test_getEmptyCells_filled(self): b = Board(size=1) b.setCell(0, 0, 42) self.assertSequenceEqual(b.getEmptyCells(), []) # == .move == # def test_move_filled(self): b = Board(size=1) b.setCell(0, 0, 42) b.move(Board.UP) self.assertSequenceEqual(b.cells, [[42]]) b.move(Board.LEFT) self.assertSequenceEqual(b.cells, [[42]]) b.move(Board.RIGHT) self.assertSequenceEqual(b.cells, [[42]]) b.move(Board.DOWN) self.assertSequenceEqual(b.cells, [[42]]) def test_move_add_tile_if_collapse(self): b = Board(size=2) b.cells = [[2, 0], [2, 0]] b.move(Board.UP) self.assertEqual(len([e for l in b.cells for e in l if e != 0]), 2) def test_move_add_tile_if_move(self): b = Board(size=2) b.cells = [[0, 0], [2, 0]] b.move(Board.UP) self.assertEqual(len([e for l in b.cells for e in l if e != 0]), 2) def test_move_dont_add_tile_if_nothing_move(self): b = Board(size=2) b.cells = [[2, 0], [0, 0]] b.move(Board.UP) self.assertEqual(len([e for l in b.cells for e in l if e != 0]), 1) # test for issue #1 def test_move_dont_add_tile_if_nothing_move2(self): b = Board() b.cells = [ [8, 4, 4, 2], [0, 2, 2, 0], [0]*4, [0]*4 ] self.assertEqual(b.move(Board.UP), 0) self.assertEqual(len([e for l in b.cells for e in l if e != 0]), 6) self.assertEqual(b.getLine(0), [8, 4, 4, 2]) self.assertEqual(b.getLine(1), [0, 2, 2, 0]) def test_move_collapse(self): b = Board(size=2) b.cells = [ [2, 2], [0, 0] ] b.move(Board.LEFT, add_tile=False) self.assertSequenceEqual(b.cells, [ [4, 0], [0, 0] ]) def test_move_collapse_triplet1(self): b = Board(size=3) b.setLine(0, [2, 2, 2]) b.move(Board.LEFT, add_tile=False) self.assertSequenceEqual(b.getLine(0), [4, 2, 0]) def test_move_collapse_triplet2(self): b = Board(size=3) b.setLine(0, [2, 2, 2]) b.move(Board.RIGHT, add_tile=False) self.assertSequenceEqual(b.getLine(0), [0, 2, 4]) def test_move_collapse_with_empty_cell_in_between(self): b = Board(size=3) b.setLine(0, [2, 0, 2]) b.move(Board.RIGHT, add_tile=False) self.assertSequenceEqual(b.getLine(0), [0, 0, 4]) def test_move_collapse_with_empty_cell_in_between2(self): b = Board(size=3) b.setLine(0, [2, 0, 2]) b.move(Board.LEFT, add_tile=False) self.assertSequenceEqual(b.getLine(0), [4, 0, 0]) def test_move_collapse_and_win(self): b = Board(size=2, goal=4) b.cells = [ [2, 2], [0, 0] ] b.move(Board.LEFT, add_tile=False) self.assertTrue(b.won()) def test_move_wrong_direction(self): self.assertEqual(self.b.move(42, add_tile=False), 0) self.assertEqual(self.b.move(None), 0) self.assertEqual(self.b.move("up"), 0) def test_move_collapse_chain_col(self): # from https://news.ycombinator.com/item?id=7398249 b = Board() b.setCol(0, [0, 2, 2, 4]) b.move(Board.DOWN, add_tile=False) self.assertSequenceEqual(b.getCol(0), [0, 0, 4, 4]) def test_move_collapse_chain_line(self): # from https://news.ycombinator.com/item?id=7398249 b = Board() b.cells = [ [0, 2, 2, 4], [0]*4, [0]*4, [0]*4 ] self.assertEqual(b.move(Board.RIGHT, add_tile=False), 4) self.assertSequenceEqual(b.getLine(0), [0, 0, 4, 4])
class Game(object): """ A 2048 game """ __dirs = { keypress.UP: Board.UP, keypress.DOWN: Board.DOWN, keypress.LEFT: Board.LEFT, keypress.RIGHT: Board.RIGHT, } __clear = 'cls' if os.name == 'nt' else 'clear' COLORS = { 2: Fore.GREEN, 4: Fore.BLUE + Style.BRIGHT, 8: Fore.CYAN, 16: Fore.RED, 32: Fore.MAGENTA, 64: Fore.CYAN, 128: Fore.BLUE + Style.BRIGHT, 256: Fore.MAGENTA, 512: Fore.GREEN, 1024: Fore.RED, 2048: Fore.YELLOW, # just in case people set an higher goal they still have colors 4096: Fore.MAGENTA, 8192: Fore.CYAN, } # see Game#adjustColors # these are color replacements for various modes __color_modes = { 'dark': { Fore.BLUE: Fore.WHITE, Fore.BLUE + Style.BRIGHT: Fore.WHITE, }, 'light': { Fore.YELLOW: Fore.BLACK, }, } SCORES_FILE = '%s/.term2048.scores' % os.path.expanduser('~') def __init__(self, scores_file=SCORES_FILE, colors=COLORS, clear_screen=True, mode=None, azmode=False, **kws): """ Create a new game. scores_file: file to use for the best score (default is ~/.term2048.scores) colors: dictionnary with colors to use for each tile mode: color mode. This adjust a few colors and can be 'dark' or 'light'. See the adjustColors functions for more info. other options are passed to the underlying Board object. """ self.board = Board(**kws) self.score = 0 self.scores_file = scores_file self.clear_screen = clear_screen self.__colors = colors self.__azmode = azmode self.loadBestScore() self.adjustColors(mode) def adjustColors(self, mode='dark'): """ Change a few colors depending on the mode to use. The default mode doesn't assume anything and avoid using white & black colors. The dark mode use white and avoid dark blue while the light mode use black and avoid yellow, to give a few examples. """ rp = Game.__color_modes.get(mode, {}) for k, color in self.__colors.items(): self.__colors[k] = rp.get(color, color) def loadBestScore(self): """ load local best score from the default file """ if self.scores_file is None or not os.path.exists(self.scores_file): self.best_score = 0 return try: f = open(self.scores_file, 'r') self.best_score = int(f.readline(), 10) f.close() except: pass # fail silently def saveBestScore(self): """ save current best score in the default file """ if self.score > self.best_score: self.best_score = self.score try: f = open(self.scores_file, 'w') f.write(str(self.best_score)) f.close() except: pass # fail silently def incScore(self, pts): """ update the current score by adding it the specified number of points """ self.score += pts if self.score > self.best_score: self.best_score = self.score def end(self): """ return True if the game is finished """ return not (self.board.won() or self.board.canMove()) def readMove(self): """ read and return a move to pass to a board """ k = keypress.getKey() return Game.__dirs.get(k) def loop(self): """ main game loop. returns the final score. """ try: while True: if self.clear_screen: os.system(Game.__clear) else: print("\n") print(self.__str__(margins={'left':4, 'top':4, 'bottom':4})) if self.board.won() or not self.board.canMove(): break m = self.readMove() self.incScore(self.board.move(m)) except KeyboardInterrupt: self.saveBestScore() return self.saveBestScore() print('You won!' if self.board.won() else 'Game Over') return self.score def loopAI(self,sleep_time=0.1): """ main game loop. returns the final score. """ try: while True: if self.clear_screen: os.system(Game.__clear) else: print("\n") print(self.__str__(margins={'left':4, 'top':4, 'bottom':4})) if self.board.won() or not self.board.canMove(): break m = AI.nextMove(self.board) self.incScore(self.board.move(m)) time.sleep(0.01) except KeyboardInterrupt: self.saveBestScore() return self.saveBestScore() print('You won!' if self.board.won() else 'Game Over') return self.score def getCellStr(self, x, y): # TODO: refactor regarding issue #11 """ return a string representation of the cell located at x,y. """ c = self.board.getCell(x, y) az = {} for i in range(1, int(math.log(self.board.goal(), 2))): az[2**i] = chr(i+96) if c==0 and self.__azmode: return '.' elif c == 0: return ' .' elif self.__azmode: if c not in az: return '?' s = az[c] elif c == 1024: s = ' 1k' elif c == 2048: s = ' 2k' elif c == 4096: s = ' 4k' elif c == 8192: s = ' 8k' else: s = '%3d' % c return self.__colors.get(c, Fore.RESET) + s + Style.RESET_ALL def boardToString(self, margins={}): """ return a string representation of the current board. """ b = self.board rg = range(b.size()) left = ' '*margins.get('left', 0) s = '\n'.join( [left + ' '.join([self.getCellStr(x, y) for x in rg]) for y in rg]) return s def __str__(self, margins={}): b = self.boardToString(margins=margins) top = '\n'*margins.get('top', 0) bottom = '\n'*margins.get('bottom', 0) scores = ' \tScore: %5d Best: %5d\n' % (self.score, self.best_score) return top + b.replace('\n', scores, 1) + bottom
class Game(object): """ A 2048 game """ __dirs = { keypress.UP: Board.UP, keypress.DOWN: Board.DOWN, keypress.LEFT: Board.LEFT, keypress.RIGHT: Board.RIGHT, } __clear = 'cls' if os.name == 'nt' else 'clear' COLORS = { 2: Fore.GREEN, 4: Fore.BLUE + Style.BRIGHT, 8: Fore.CYAN, 16: Fore.RED, 32: Fore.MAGENTA, 64: Fore.CYAN, 128: Fore.BLUE + Style.BRIGHT, 256: Fore.MAGENTA, 512: Fore.GREEN, 1024: Fore.RED, 2048: Fore.YELLOW, # just in case people set an higher goal they still have colors 4096: Fore.MAGENTA, 8192: Fore.CYAN, } # see Game#adjustColors # these are color replacements for various modes __color_modes = { 'dark': { Fore.BLUE: Fore.WHITE, Fore.BLUE + Style.BRIGHT: Fore.WHITE, }, 'light': { Fore.YELLOW: Fore.BLACK, }, } SCORES_FILE = '%s/.term2048.scores' % os.path.expanduser('~') def __init__(self, scores_file=SCORES_FILE, colors=COLORS, clear_screen=True, mode=None, azmode=False, **kws): """ Create a new game. scores_file: file to use for the best score (default is ~/.term2048.scores) colors: dictionnary with colors to use for each tile mode: color mode. This adjust a few colors and can be 'dark' or 'light'. See the adjustColors functions for more info. other options are passed to the underlying Board object. """ self.board = Board(**kws) self.score = 0 self.scores_file = scores_file self.clear_screen = clear_screen self.__colors = colors self.__azmode = azmode self.loadBestScore() self.adjustColors(mode) def adjustColors(self, mode='dark'): """ Change a few colors depending on the mode to use. The default mode doesn't assume anything and avoid using white & black colors. The dark mode use white and avoid dark blue while the light mode use black and avoid yellow, to give a few examples. """ rp = Game.__color_modes.get(mode, {}) for k, color in self.__colors.items(): self.__colors[k] = rp.get(color, color) def loadBestScore(self): """ load local best score from the default file """ if self.scores_file is None or not os.path.exists(self.scores_file): self.best_score = 0 return try: f = open(self.scores_file, 'r') self.best_score = int(f.readline(), 10) f.close() except: pass # fail silently def saveBestScore(self): """ save current best score in the default file """ if self.score > self.best_score: self.best_score = self.score try: f = open(self.scores_file, 'w') f.write(str(self.best_score)) f.close() except: pass # fail silently def incScore(self, pts): """ update the current score by adding it the specified number of points """ self.score += pts if self.score > self.best_score: self.best_score = self.score def end(self): """ return True if the game is finished """ return not (self.board.won() or self.board.canMove()) def readMove(self): """ read and return a move to pass to a board """ k = keypress.getKey() return Game.__dirs.get(k) def loop(self): """ main game loop. returns the final score. """ try: while True: if self.clear_screen: os.system(Game.__clear) else: print("\n") print(self.__str__(margins={'left': 4, 'top': 4, 'bottom': 4})) if self.board.won() or not self.board.canMove(): break m = self.readMove() self.incScore(self.board.move(m)) except KeyboardInterrupt: self.saveBestScore() return self.saveBestScore() print('You won!' if self.board.won() else 'Game Over') return self.score def loopAI(self, sleep_time=0.1): """ main game loop. returns the final score. """ try: while True: if self.clear_screen: os.system(Game.__clear) else: print("\n") print(self.__str__(margins={'left': 4, 'top': 4, 'bottom': 4})) if self.board.won() or not self.board.canMove(): break m = AI.nextMove(self.board) self.incScore(self.board.move(m)) time.sleep(0.01) except KeyboardInterrupt: self.saveBestScore() return self.saveBestScore() print('You won!' if self.board.won() else 'Game Over') return self.score def getCellStr(self, x, y): # TODO: refactor regarding issue #11 """ return a string representation of the cell located at x,y. """ c = self.board.getCell(x, y) az = {} for i in range(1, int(math.log(self.board.goal(), 2))): az[2**i] = chr(i + 96) if c == 0 and self.__azmode: return '.' elif c == 0: return ' .' elif self.__azmode: if c not in az: return '?' s = az[c] elif c == 1024: s = ' 1k' elif c == 2048: s = ' 2k' elif c == 4096: s = ' 4k' elif c == 8192: s = ' 8k' else: s = '%3d' % c return self.__colors.get(c, Fore.RESET) + s + Style.RESET_ALL def boardToString(self, margins={}): """ return a string representation of the current board. """ b = self.board rg = range(b.size()) left = ' ' * margins.get('left', 0) s = '\n'.join( [left + ' '.join([self.getCellStr(x, y) for x in rg]) for y in rg]) return s def __str__(self, margins={}): b = self.boardToString(margins=margins) top = '\n' * margins.get('top', 0) bottom = '\n' * margins.get('bottom', 0) scores = ' \tScore: %5d Best: %5d\n' % (self.score, self.best_score) return top + b.replace('\n', scores, 1) + bottom
class Game(object): """ A 2048 game """ __dirs = { keypress.UP: Board.UP, keypress.DOWN: Board.DOWN, keypress.LEFT: Board.LEFT, keypress.RIGHT: Board.RIGHT, keypress.SPACE: Board.PAUSE, } __is_windows = os.name == 'nt' COLORS = { 2: Fore.GREEN, 4: Fore.BLUE + Style.BRIGHT, 8: Fore.CYAN, 16: Fore.RED, # Don't use MAGENTA directly; it doesn't display well on Windows. # see https://github.com/bfontaine/term2048/issues/24 32: Fore.MAGENTA + Style.BRIGHT, 64: Fore.CYAN, 128: Fore.BLUE + Style.BRIGHT, 256: Fore.MAGENTA + Style.BRIGHT, 512: Fore.GREEN, 1024: Fore.RED, 2048: Fore.YELLOW, # just in case people set an higher goal they still have colors 4096: Fore.RED, 8192: Fore.CYAN, } # see Game#adjustColors # these are color replacements for various modes __color_modes = { 'dark': { Fore.BLUE: Fore.WHITE, Fore.BLUE + Style.BRIGHT: Fore.WHITE, }, 'light': { Fore.YELLOW: Fore.BLACK, }, } SCORES_FILE = '%s/.term2048.scores' % os.path.expanduser('~') STORE_FILE = '%s/.term2048.store' % os.path.expanduser('~') def __init__(self, scores_file=SCORES_FILE, colors=None, store_file=STORE_FILE, clear_screen=True, mode=None, azmode=False, **kws): """ Create a new game. scores_file: file to use for the best score (default is ~/.term2048.scores) colors: dictionnary with colors to use for each tile store_file: file that stores game session's snapshot mode: color mode. This adjust a few colors and can be 'dark' or 'light'. See the adjustColors functions for more info. other options are passed to the underlying Board object. """ self.board = Board(**kws) self.score = 0 self.scores_file = scores_file self.store_file = store_file self.clear_screen = clear_screen self.best_score = 0 self.__colors = colors or self.COLORS self.__azmode = azmode self.loadBestScore() self.adjustColors(mode) def adjustColors(self, mode='dark'): """ Change a few colors depending on the mode to use. The default mode doesn't assume anything and avoid using white & black colors. The dark mode use white and avoid dark blue while the light mode use black and avoid yellow, to give a few examples. """ rp = Game.__color_modes.get(mode, {}) for k, color in self.__colors.items(): self.__colors[k] = rp.get(color, color) def loadBestScore(self): """ load local best score from the default file """ try: with open(self.scores_file, 'r') as f: self.best_score = int(f.readline(), 10) except: return False return True def saveBestScore(self): """ save current best score in the default file """ if self.score > self.best_score: self.best_score = self.score try: with open(self.scores_file, 'w') as f: f.write(str(self.best_score)) except: return False return True def incScore(self, pts): """ update the current score by adding it the specified number of points """ self.score += pts if self.score > self.best_score: self.best_score = self.score def readMove(self): """ read and return a move to pass to a board """ k = keypress.getKey() return Game.__dirs.get(k) def store(self): """ save the current game session's score and data for further use """ size = self.board.SIZE cells = [] for i in range(size): for j in range(size): cells.append(str(self.board.getCell(j, i))) score_str = "%s\n%d" % (' '.join(cells), self.score) try: with open(self.store_file, 'w') as f: f.write(score_str) except: return False return True def restore(self): """ restore the saved game score and data """ size = self.board.SIZE try: with open(self.store_file, 'r') as f: lines = f.readlines() score_str = lines[0] self.score = int(lines[1]) except: return False score_str_list = score_str.split(' ') count = 0 for i in range(size): for j in range(size): value = score_str_list[count] self.board.setCell(j, i, int(value)) count += 1 return True def clearScreen(self): """Clear the console""" if self.clear_screen: os.system('cls' if self.__is_windows else 'clear') else: print('\n') def hideCursor(self): """ Hide the cursor. Don't forget to call ``showCursor`` to restore the normal shell behavior. This is a no-op if ``clear_screen`` is falsy. """ if not self.clear_screen: return if not self.__is_windows: sys.stdout.write('\033[?25l') def showCursor(self): """Show the cursor.""" if not self.__is_windows: sys.stdout.write('\033[?25h') def loop(self): """ main game loop. returns the final score. """ pause_key = self.board.PAUSE margins = {'left': 4, 'top': 4, 'bottom': 4} atexit.register(self.showCursor) try: self.hideCursor() while True: self.clearScreen() print(self.__str__(margins=margins)) if self.board.won() or not self.board.canMove(): break m = self.readMove() if m == pause_key: self.saveBestScore() if self.store(): print("Game successfully saved. " "Resume it with `term2048 --resume`.") return self.score print("An error ocurred while saving your game.") return None self.incScore(self.board.move(m)) except KeyboardInterrupt: self.saveBestScore() return None self.saveBestScore() print('You won!' if self.board.won() else 'Game Over') return self.score def getCellStr(self, x, y): # TODO: refactor regarding issue #11 """ return a string representation of the cell located at x,y. """ c = self.board.getCell(x, y) if c == 0: return '.' if self.__azmode else ' .' elif self.__azmode: az = {} for i in range(1, int(math.log(self.board.goal(), 2))): az[2 ** i] = chr(i + 96) if c not in az: return '?' s = az[c] elif c == 1024: s = ' 1k' elif c == 2048: s = ' 2k' else: s = '%3d' % c return self.__colors.get(c, Fore.RESET) + s + Style.RESET_ALL def boardToString(self, margins=None): """ return a string representation of the current board. """ if margins is None: margins = {} b = self.board rg = range(b.size()) left = ' '*margins.get('left', 0) s = '\n'.join( [left + ' '.join([self.getCellStr(x, y) for x in rg]) for y in rg]) return s def __str__(self, margins=None): if margins is None: margins = {} b = self.boardToString(margins=margins) top = '\n'*margins.get('top', 0) bottom = '\n'*margins.get('bottom', 0) scores = ' \tScore: %5d Best: %5d\n' % (self.score, self.best_score) return top + b.replace('\n', scores, 1) + bottom
class Game(object): """ A 2048 game """ __dirs = { keypress.UP: Board.UP, keypress.DOWN: Board.DOWN, keypress.LEFT: Board.LEFT, keypress.RIGHT: Board.RIGHT, keypress.SPACE: Board.PAUSE, } __clear = "cls" if os.name == "nt" else "clear" COLORS = { 2: Fore.GREEN, 4: Fore.BLUE + Style.BRIGHT, 8: Fore.CYAN, 16: Fore.RED, 32: Fore.MAGENTA, 64: Fore.CYAN, 128: Fore.BLUE + Style.BRIGHT, 256: Fore.MAGENTA, 512: Fore.GREEN, 1024: Fore.RED, 2048: Fore.YELLOW, # just in case people set an higher goal they still have colors 4096: Fore.RED, 8192: Fore.CYAN, } # see Game#adjustColors # these are color replacements for various modes __color_modes = { "dark": {Fore.BLUE: Fore.WHITE, Fore.BLUE + Style.BRIGHT: Fore.WHITE}, "light": {Fore.YELLOW: Fore.BLACK}, } SCORES_FILE = "%s/.term2048.scores" % os.path.expanduser("~") STORE_FILE = "%s/.term2048.store" % os.path.expanduser("~") def __init__( self, scores_file=SCORES_FILE, colors=COLORS, store_file=STORE_FILE, clear_screen=True, mode=None, azmode=False, **kws ): """ Create a new game. scores_file: file to use for the best score (default is ~/.term2048.scores) colors: dictionnary with colors to use for each tile store_file: file that stores game session's snapshot mode: color mode. This adjust a few colors and can be 'dark' or 'light'. See the adjustColors functions for more info. other options are passed to the underlying Board object. """ self.board = Board(**kws) self.score = 0 self.scores_file = scores_file self.store_file = store_file self.clear_screen = clear_screen self.__colors = colors self.__azmode = azmode self.loadBestScore() self.adjustColors(mode) def adjustColors(self, mode="dark"): """ Change a few colors depending on the mode to use. The default mode doesn't assume anything and avoid using white & black colors. The dark mode use white and avoid dark blue while the light mode use black and avoid yellow, to give a few examples. """ rp = Game.__color_modes.get(mode, {}) for k, color in self.__colors.items(): self.__colors[k] = rp.get(color, color) def loadBestScore(self): """ load local best score from the default file """ try: with open(self.scores_file, "r") as f: self.best_score = int(f.readline(), 10) except: self.best_score = 0 return False return True def saveBestScore(self): """ save current best score in the default file """ if self.score > self.best_score: self.best_score = self.score try: with open(self.scores_file, "w") as f: f.write(str(self.best_score)) except: return False return True def incScore(self, pts): """ update the current score by adding it the specified number of points """ self.score += pts if self.score > self.best_score: self.best_score = self.score def readMove(self): """ read and return a move to pass to a board """ k = keypress.getKey() return Game.__dirs.get(k) def store(self): """ save the current game session's score and data for further use """ size = self.board.SIZE cells = [] for i in range(size): for j in range(size): cells.append(str(self.board.getCell(j, i))) score_str = "%s\n%d" % (" ".join(cells), self.score) try: with open(self.store_file, "w") as f: f.write(score_str) except: return False return True def restore(self): """ restore the saved game score and data """ size = self.board.SIZE try: with open(self.store_file, "r") as f: lines = f.readlines() score_str = lines[0] self.score = int(lines[1]) except: return False score_str_list = score_str.split(" ") count = 0 for i in range(size): for j in range(size): value = score_str_list[count] self.board.setCell(j, i, int(value)) count += 1 return True def loop(self): """ main game loop. returns the final score. """ pause_key = self.board.PAUSE margins = {"left": 4, "top": 4, "bottom": 4} try: while True: if self.clear_screen: os.system(Game.__clear) else: print("\n") print(self.__str__(margins=margins)) if self.board.won() or not self.board.canMove(): break m = self.readMove() if m == pause_key: self.saveBestScore() if self.store(): print("Game successfully saved. " "Resume it with `term2048 --resume`.") return self.score print("An error ocurred while saving your game.") return self.incScore(self.board.move(m)) except KeyboardInterrupt: self.saveBestScore() return self.saveBestScore() print("You won!" if self.board.won() else "Game Over") return self.score def getCellStr(self, x, y): # TODO: refactor regarding issue #11 """ return a string representation of the cell located at x,y. """ c = self.board.getCell(x, y) if c == 0: return "." if self.__azmode else " ." elif self.__azmode: az = {} for i in range(1, int(math.log(self.board.goal(), 2))): az[2 ** i] = chr(i + 96) if c not in az: return "?" s = az[c] elif c == 1024: s = " 1k" elif c == 2048: s = " 2k" else: s = "%3d" % c return self.__colors.get(c, Fore.RESET) + s + Style.RESET_ALL def boardToString(self, margins={}): """ return a string representation of the current board. """ b = self.board rg = range(b.size()) left = " " * margins.get("left", 0) s = "\n".join([left + " ".join([self.getCellStr(x, y) for x in rg]) for y in rg]) return s def __str__(self, margins={}): b = self.boardToString(margins=margins) top = "\n" * margins.get("top", 0) bottom = "\n" * margins.get("bottom", 0) scores = " \tScore: %5d Best: %5d\n" % (self.score, self.best_score) return top + b.replace("\n", scores, 1) + bottom
class TestBoard(unittest.TestCase): def setUp(self): self.b = Board() # == init == # def test_init_dimensions(self): self.assertEqual(len(self.b.cells), Board.SIZE) self.assertEqual(len(self.b.cells[0]), Board.SIZE) if Board.SIZE > 1: self.assertEqual(len(self.b.cells[1]), Board.SIZE) def test_init_dimensions_1(self): b = Board(size=1) c = b.cells[0][0] self.assertTrue(c in [2, 4]) def test_init_dimensions_3_goal_4(self): b = Board(size=3, goal=4) self.assertEqual(b.size(), 3) def test_init_only_two_tiles(self): t = 0 for x in xrange(Board.SIZE): for y in xrange(Board.SIZE): c = self.b.cells[y][x] if not c == 0: t += 1 else: self.assertEqual(c, 0, 'board[%d][%d] should be 0' % (y, x)) self.assertEqual(t, 2) def test_init_not_won(self): self.assertFalse(self.b.won()) def test_init_not_filled(self): self.assertFalse(self.b.filled()) # == .size == # def test_size(self): s = 42 b = Board(size=s) self.assertEqual(b.size(), s) # == .goal == # def test_goal(self): g = 17 b = Board(goal=g) self.assertEqual(b.goal(), g) # == .won == # def test_won(self): self.b._Board__won = True self.assertTrue(self.b.won()) self.b._Board__won = False self.assertFalse(self.b.won()) # == .canMove == # def test_canMove_no_empty_cell(self): b = Board(size=1) b.setCell(0, 0, 42) self.assertFalse(b.canMove()) def test_canMove_empty_cell(self): b = Board(size=2) self.assertTrue(b.canMove()) def test_canMove_no_empty_cell_can_collapse(self): b = Board(size=2) b.cells = [ [2, 2], [4, 8] ] self.assertTrue(b.canMove()) # == .filled == # def test_filled(self): self.b.cells = [[1]*Board.SIZE for _ in xrange(Board.SIZE)] self.assertTrue(self.b.filled()) # == .addTile == # def test_addTile(self): b = Board(size=1) b.cells = [[0]] b.addTile(value=42) self.assertEqual(b.cells[0][0], 42) # == .getCell == # def test_getCell(self): x, y = 3, 1 v = 42 self.b.cells[y][x] = v self.assertEqual(self.b.getCell(x, y), v) # == .setCell == # def test_setCell(self): x, y = 2, 3 v = 42 self.b.setCell(x, y, v) self.assertEqual(self.b.cells[y][x], v) # == .getLine == # def test_getLine(self): b = Board(size=4) l = [42, 17, 12, 3] b.cells = [ [0]*4, l, [0]*4, [0]*4 ] self.assertSequenceEqual(b.getLine(1), l) # == .getCol == # def test_getCol(self): s = 4 b = Board(size=s) l = [42, 17, 12, 3] b.cells = [[l[i], 4, 1, 2] for i in xrange(s)] self.assertSequenceEqual(b.getCol(0), l) # == .setLine == # def test_setLine(self): i = 2 l = [1, 2, 3, 4] self.b.setLine(i, l) self.assertEqual(self.b.getLine(i), l) # == .setCol == # def test_setCol(self): i = 2 l = [1, 2, 3, 4] self.b.setCol(i, l) self.assertEqual(self.b.getCol(i), l) # == .getEmptyCells == # def test_getEmptyCells(self): self.assertEqual(len(self.b.getEmptyCells()), Board.SIZE**2 - 2) def test_getEmptyCells_filled(self): b = Board(size=1) b.setCell(0, 0, 42) self.assertSequenceEqual(b.getEmptyCells(), []) # == .move == # def test_move_filled(self): b = Board(size=1) b.setCell(0, 0, 42) b.move(Board.UP) self.assertSequenceEqual(b.cells, [[42]]) b.move(Board.LEFT) self.assertSequenceEqual(b.cells, [[42]]) b.move(Board.RIGHT) self.assertSequenceEqual(b.cells, [[42]]) b.move(Board.DOWN) self.assertSequenceEqual(b.cells, [[42]]) def test_move_add_tile_if_collapse(self): b = Board(size=2) b.cells = [[2, 0], [2, 0]] b.move(Board.UP) self.assertEqual(len([e for l in b.cells for e in l if e != 0]), 2) def test_move_add_tile_if_move(self): b = Board(size=2) b.cells = [[0, 0], [2, 0]] b.move(Board.UP) self.assertEqual(len([e for l in b.cells for e in l if e != 0]), 2) def test_move_dont_add_tile_if_nothing_move(self): b = Board(size=2) b.cells = [[2, 0], [0, 0]] b.move(Board.UP) self.assertEqual(len([e for l in b.cells for e in l if e != 0]), 1) # test for issue #1 def test_move_dont_add_tile_if_nothing_move2(self): b = Board() b.cells = [ [8, 4, 4, 2], [0, 2, 2, 0], [0]*4, [0]*4 ] self.assertEqual(b.move(Board.UP), 0) self.assertEqual(len([e for l in b.cells for e in l if e != 0]), 6) self.assertEqual(b.getLine(0), [8, 4, 4, 2]) self.assertEqual(b.getLine(1), [0, 2, 2, 0]) def test_move_collapse(self): b = Board(size=2) b.cells = [ [2, 2], [0, 0] ] b.move(Board.LEFT, add_tile=False) self.assertSequenceEqual(b.cells, [ [4, 0], [0, 0] ]) def test_move_collapse_triplet1(self): b = Board(size=3) b.setLine(0, [2, 2, 2]) b.move(Board.LEFT, add_tile=False) self.assertSequenceEqual(b.getLine(0), [4, 2, 0]) def test_move_collapse_triplet2(self): b = Board(size=3) b.setLine(0, [2, 2, 2]) b.move(Board.RIGHT, add_tile=False) self.assertSequenceEqual(b.getLine(0), [0, 2, 4]) def test_move_collapse_with_empty_cell_in_between(self): b = Board(size=3) b.setLine(0, [2, 0, 2]) b.move(Board.RIGHT, add_tile=False) self.assertSequenceEqual(b.getLine(0), [0, 0, 4]) def test_move_collapse_with_empty_cell_in_between2(self): b = Board(size=3) b.setLine(0, [2, 0, 2]) b.move(Board.LEFT, add_tile=False) self.assertSequenceEqual(b.getLine(0), [4, 0, 0]) def test_move_collapse_and_win(self): b = Board(size=2, goal=4) b.cells = [ [2, 2], [0, 0] ] b.move(Board.LEFT, add_tile=False) self.assertTrue(b.won()) def test_move_wrong_direction(self): self.assertEqual(self.b.move(42, add_tile=False), 0) self.assertEqual(self.b.move(None), 0) self.assertEqual(self.b.move("up"), 0) # tests for weird-collapse-bug reported on HN (issue #2) # see: https://news.ycombinator.com/item?id=7398249 def test_move_collapse_chain_col(self): b = Board() b.setCol(0, [0, 2, 2, 4]) b.move(Board.DOWN, add_tile=False) self.assertSequenceEqual(b.getCol(0), [0, 0, 4, 4]) def test_move_collapse_chain_line_right(self): b = Board() b.cells = [ [0, 2, 2, 4], [0]*4, [0]*4, [0]*4 ] self.assertEqual(b.move(Board.RIGHT, add_tile=False), 4) self.assertSequenceEqual(b.getLine(0), [0, 0, 4, 4]) def test_move_collapse_chain_line_right2(self): b = Board() b.cells = [ [0, 4, 2, 2], [0]*4, [0]*4, [0]*4 ] self.assertEqual(b.move(Board.RIGHT, add_tile=False), 4) self.assertSequenceEqual(b.getLine(0), [0, 0, 4, 4]) def test_move_collapse_chain_line_left(self): b = Board() b.cells = [ [0, 2, 2, 4], [0]*4, [0]*4, [0]*4 ] self.assertEqual(b.move(Board.LEFT, add_tile=False), 4) self.assertSequenceEqual(b.getLine(0), [4, 4, 0, 0]) def test_move_collapse_chain_four_same_tiles(self): b = Board() b.cells = [ [2, 2, 2, 2], [0]*4, [0]*4, [0]*4 ] self.assertEqual(b.move(Board.LEFT, add_tile=False), 8) self.assertSequenceEqual(b.getLine(0), [4, 4, 0, 0])