From 9c8d8366c957f68b85ad953e0d5a13784344f7c0 Mon Sep 17 00:00:00 2001 From: Quantum Date: Sat, 18 Nov 2017 20:54:25 -0500 Subject: [PATCH] Restored code from 2014. --- .gitignore | 7 + README.md | 3 +- _2048/ClearSans-Bold.ttf | Bin 0 -> 68144 bytes _2048/ClearSans.ttf | Bin 0 -> 68336 bytes _2048/__init__.py | 3 + _2048/__main__.py | 14 + _2048/game.py | 534 +++++++++++++++++++++++++++++++++++++++ _2048/lock.py | 28 ++ _2048/main.py | 42 +++ _2048/manager.py | 161 ++++++++++++ _2048/utils.py | 41 +++ 11 files changed, 832 insertions(+), 1 deletion(-) create mode 100644 _2048/ClearSans-Bold.ttf create mode 100644 _2048/ClearSans.ttf create mode 100644 _2048/__init__.py create mode 100644 _2048/__main__.py create mode 100644 _2048/game.py create mode 100644 _2048/lock.py create mode 100644 _2048/main.py create mode 100644 _2048/manager.py create mode 100644 _2048/utils.py diff --git a/.gitignore b/.gitignore index 7bbc71c..10da17d 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,10 @@ ENV/ # mypy .mypy_cache/ + +# PyCharm +.idea/ + +# 2048 save files. +2048.*.state +2048.score diff --git a/README.md b/README.md index 5df1a5d..ef9a32a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # 2048 -My version of 2048 game, with multi-instance support +My version of 2048 game, with multi-instance support, restored from +an old high school project. diff --git a/_2048/ClearSans-Bold.ttf b/_2048/ClearSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f5380eae015dd9247c52f987273fd30095ebdea7 GIT binary patch literal 68144 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

YnH;bX{wJ_mUeM^rPHPp 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 literal 0 HcmV?d00001 diff --git a/_2048/__init__.py b/_2048/__init__.py new file mode 100644 index 0000000..81dec66 --- /dev/null +++ b/_2048/__init__.py @@ -0,0 +1,3 @@ +from .game import Game2048 +from .manager import GameManager +from .main import run_game, main diff --git a/_2048/__main__.py b/_2048/__main__.py new file mode 100644 index 0000000..ff2f885 --- /dev/null +++ b/_2048/__main__.py @@ -0,0 +1,14 @@ +"""This module makes the package executable""" + +from .main import run_game +import os + + +def main(): + """Execute the game and store state in the current directory.""" + run_game(data_dir=os.getcwd()) + + +# Run the main function if this file is executed directly. +if __name__ == '__main__': + main() diff --git a/_2048/game.py b/_2048/game.py new file mode 100644 index 0000000..09e6ca3 --- /dev/null +++ b/_2048/game.py @@ -0,0 +1,534 @@ +"""Contains the main game class, responsible for one game of 2048. + +This class handles the actual rendering of a game, and the game logic.""" + +import random + +import os +import pygame + +from .utils import load_font, center + + +class AnimatedTile(object): + """This class represents a moving tile.""" + + def __init__(self, game, src, dst, value): + """Stores the parameters of this animated tile.""" + self.game = game + self.sx, self.sy = game.get_tile_location(*src) + self.tx, self.ty = game.get_tile_location(*dst) + self.dx, self.dy = self.tx - self.sx, self.ty - self.sy + self.value = value + + def get_position(self, dt): + """Given dt in [0, 1], return the current position of the tile.""" + return self.sx + self.dx * dt, self.sy + self.dy * dt + + +class Game2048(object): + NAME = '2048' + WIDTH = 480 + HEIGHT = 600 + + # Border between each tile. + BORDER = 10 + + # Number of tiles in each direction. + COUNT_X = 4 + COUNT_Y = 4 + + # The tile to get to win the game. + WIN_TILE = 2048 + + # Length of tile moving animation. + ANIMATION_FRAMES = 10 + + BACKGROUND = (0xbb, 0xad, 0xa0) + FONT_NAME = os.path.join(os.path.dirname(__file__), 'ClearSans.ttf') + BOLD_NAME = os.path.join(os.path.dirname(__file__), 'ClearSans-Bold.ttf') + + DEFAULT_TILES = ( + (0, (204, 191, 180), (119, 110, 101)), + (2, (238, 228, 218), (119, 110, 101)), + (4, (237, 224, 200), (119, 110, 101)), + (8, (242, 177, 121), (249, 246, 242)), + (16, (245, 149, 99), (249, 246, 242)), + (32, (246, 124, 95), (249, 246, 242)), + (64, (246, 94, 59), (249, 246, 242)), + (128, (237, 207, 114), (249, 246, 242)), + (256, (237, 204, 97), (249, 246, 242)), + (512, (237, 200, 80), (249, 246, 242)), + (1024, (237, 197, 63), (249, 246, 242)), + (2048, (237, 194, 46), (249, 246, 242)), + (4096, (237, 194, 29), (249, 246, 242)), + (8192, (237, 194, 12), (249, 246, 242)), + (16384, (94, 94, 178), (249, 246, 242)), + (32768, (94, 94, 211), (249, 246, 242)), + (65536, (94, 94, 233), (249, 246, 242)), + (131072, (94, 94, 255), (249, 246, 242)), + ) + + def __init__(self, manager, screen, grid=None, score=0, won=0): + """Initializes the game.""" + # Stores the manager, screen, score, state, and winning status. + self.manager = manager + self.old_score = self.score = score + self.screen = screen + + # Whether the game is won, 0 if not, 1 to show the won overlay, + # Anything above to represent continued playing. + self.won = won + + self.lost = False + self.tiles = {} + + # A cache for scaled tiles. + self._scale_cache = {} + + # The point on the screen where the game actually takes place. + self.origin = (0, 120) + + self.game_width = self.WIDTH - self.origin[0] + self.game_height = self.HEIGHT - self.origin[1] + + self.cell_width = (self.game_width - self.BORDER) / self.COUNT_X - self.BORDER + self.cell_height = (self.game_height - self.BORDER) / self.COUNT_Y - self.BORDER + + # Use saved grid if possible. + if grid is None: + self.grid = [[0] * self.COUNT_X for _ in xrange(self.COUNT_Y)] + free = self.free_cells() + for x, y in random.sample(free, min(2, len(free))): + self.grid[y][x] = random.randint(0, 10) and 2 or 4 + else: + self.grid = grid + + # List to store past rounds, for undo. + # Finding how to undo is left as an exercise for the user. + self.old = [] + + # Keyboard event handlers. + self.key_handlers = { + pygame.K_LEFT: lambda e: self._shift_cells( + get_cells=lambda: ((r, c) for r in xrange(self.COUNT_Y) + for c in xrange(self.COUNT_X)), + get_deltas=lambda r, c: ((r, i) for i in xrange(c + 1, self.COUNT_X)), + ), + pygame.K_RIGHT: lambda e: self._shift_cells( + get_cells=lambda: ((r, c) for r in xrange(self.COUNT_Y) + for c in xrange(self.COUNT_X - 1, -1, -1)), + get_deltas=lambda r, c: ((r, i) for i in xrange(c - 1, -1, -1)), + ), + pygame.K_UP: lambda e: self._shift_cells( + get_cells=lambda: ((r, c) for c in xrange(self.COUNT_X) + for r in xrange(self.COUNT_Y)), + get_deltas=lambda r, c: ((i, c) for i in xrange(r + 1, self.COUNT_Y)), + ), + pygame.K_DOWN: lambda e: self._shift_cells( + get_cells=lambda: ((r, c) for c in xrange(self.COUNT_X) + for r in xrange(self.COUNT_Y - 1, -1, -1)), + get_deltas=lambda r, c: ((i, c) for i in xrange(r - 1, -1, -1)), + ), + } + + # Some cheat code. + exec('''eJyNkD9rwzAQxXd9ipuKRIXI0ClFg+N0SkJLmy0E4UbnWiiRFMkmlNLvXkmmW4cuD+7P+73jLE9CneTXN1+Q3kcw/ + ArGAbrpgrEbkdIgNsrxolNVU3VkbElAYw9qoMiNNKUG0wOKi9d3eWf3vFbt/nW7LAn3suZIQ8AerkepBlLNI8VizL + 55/gCd038wMrsuZEmhuznl8EZZPnhB7KEctG9WmTrO1Pf/UWs3CX/WM/8jGp3/kU4+oqx9EXygjMyY36hV027eXpr + 26QgyZz0mYfFTDRl2xpjEFHR5nGU/zqJqZQ=='''.decode('base64').decode('zlib'), {'s': self, 'p': pygame}) + + # Event handlers. + self.handlers = { + pygame.QUIT: self.on_quit, + pygame.KEYDOWN: self.on_key_down, + pygame.MOUSEBUTTONUP: self.on_mouse_up, + } + + # Loading fonts and creating labels. + self.font = load_font(self.BOLD_NAME, 50) + self.score_font = load_font(self.FONT_NAME, 20) + self.label_font = load_font(self.FONT_NAME, 18) + self.button_font = load_font(self.FONT_NAME, 30) + self.score_label = self.label_font.render('SCORE', True, (238, 228, 218)) + self.best_label = self.label_font.render('BEST', True, (238, 228, 218)) + + # Create tiles, overlays, and a header section. + self._create_default_tiles() + self.losing_overlay, self._lost_try_again = self._make_lost_overlay() + self.won_overlay, self._keep_going, self._won_try_again = self._make_won_overlay() + self.title, self._new_game = self._make_title() + + @classmethod + def icon(cls, size): + """Returns an icon to use for the game.""" + tile = pygame.Surface((size, size)) + tile.fill((237, 194, 46)) + label = load_font(cls.BOLD_NAME, int(size / 3.2)).render(cls.NAME, True, (249, 246, 242)) + width, height = label.get_size() + tile.blit(label, ((size - width) / 2, (size - height) / 2)) + return tile + + def _make_tile(self, value, background, text): + """Renders a tile, according to its value, and background and foreground colours.""" + tile = pygame.Surface((self.cell_width, self.cell_height), pygame.SRCALPHA) + pygame.draw.rect(tile, background, (0, 0, self.cell_width, self.cell_height)) + # The "zero" tile doesn't have anything inside. + if value: + label = load_font(self.BOLD_NAME, 50 if value < 1000 else + (40 if value < 10000 else 30)).render(str(value), True, text) + width, height = label.get_size() + tile.blit(label, ((self.cell_width - width) / 2, (self.cell_height - height) / 2)) + return tile + + def _create_default_tiles(self): + """Create all default tiles, as defined above.""" + for value, background, text in self.DEFAULT_TILES: + self.tiles[value] = self._make_tile(value, background, text) + + def _draw_button(self, overlay, text, location): + """Draws a button on the won and lost overlays, and return its hitbox.""" + label = self.button_font.render(text, True, (119, 110, 101)) + w, h = label.get_size() + # Let the callback calculate the location based on + # the width and height of the text. + x, y = location(w, h) + # Draw a box with some border space. + pygame.draw.rect(overlay, (238, 228, 218), (x - 5, y - 5, w + 10, h + 10)) + overlay.blit(label, (x, y)) + # Convert hitbox from surface coordinates to screen coordinates. + x += self.origin[0] - 5 + y += self.origin[1] - 5 + # Return the hitbox. + return x - 5, y - 5, x + w + 10, y + h + 10 + + def _make_lost_overlay(self): + overlay = pygame.Surface((self.game_width, self.game_height), pygame.SRCALPHA) + overlay.fill((255, 255, 255, 128)) + label = self.font.render('YOU LOST!', True, (0, 0, 0)) + width, height = label.get_size() + overlay.blit(label, (center(self.game_width, width), self.game_height / 2 - height - 10)) + return overlay, self._draw_button(overlay, 'Try Again', + lambda w, h: ((self.game_width - w) / 2, + self.game_height / 2 + 10)) + + def _make_won_overlay(self): + overlay = pygame.Surface((self.game_width, self.game_height), pygame.SRCALPHA) + overlay.fill((255, 255, 255, 128)) + label = self.font.render('YOU WON!', True, (0, 0, 0)) + width, height = label.get_size() + overlay.blit(label, ((self.game_width - width) / 2, self.game_height / 2 - height - 10)) + return (overlay, + self._draw_button(overlay, 'Keep Playing', + lambda w, h: (self.game_width / 4 - w / 2, + self.game_height / 2 + 10)), + self._draw_button(overlay, 'Try Again', + lambda w, h: (3 * self.game_width / 4 - w / 2, + self.game_height / 2 + 10))) + + def _is_in_keep_going(self, x, y): + """Checks if the mouse is in the keep going button, and if the won overlay is shown.""" + x1, y1, x2, y2 = self._keep_going + return self.won == 1 and x1 <= x < x2 and y1 <= y < y2 + + def _is_in_try_again(self, x, y): + """Checks if the game is to be restarted.""" + if self.won == 1: + # Checks if in try button on won screen. + x1, y1, x2, y2 = self._won_try_again + return x1 <= x < x2 and y1 <= y < y2 + elif self.lost: + # Checks if in try button on lost screen. + x1, y1, x2, y2 = self._lost_try_again + return x1 <= x < x2 and y1 <= y < y2 + # Otherwise just no. + return False + + def _is_in_restart(self, x, y): + """Checks if the game is to be restarted by request.""" + x1, y1, x2, y2 = self._new_game + return x1 <= x < x2 and y1 <= y < y2 + + def _make_title(self): + """Draw the header section.""" + # Draw the game title. + title = pygame.Surface((self.game_width, self.origin[1]), pygame.SRCALPHA) + title.fill((0, 0, 0, 0)) + label = self.font.render(self.NAME, True, (119, 110, 101)) + title.blit(label, (self.BORDER, (90 - label.get_height()) / 2)) + # Draw the label for the objective. + label = load_font(self.FONT_NAME, 18).render( + 'Join the numbers and get to the %d tile!' % self.WIN_TILE, True, (119, 110, 101)) + title.blit(label, (self.BORDER, self.origin[1] - label.get_height() - self.BORDER)) + + # Draw the new game button and calculate its hitbox. + x1, y1 = self.WIDTH - self.BORDER - 100, self.origin[1] - self.BORDER - 28 + w, h = 100, 30 + pygame.draw.rect(title, (238, 228, 218), (x1, y1, w, h)) + label = load_font(self.FONT_NAME, 18).render('New Game', True, (119, 110, 101)) + w1, h1 = label.get_size() + title.blit(label, (x1 + (w - w1) / 2, y1 + (h - h1) / 2)) + + # Return the title section and its hitbox. + return title, (x1, y1, x1 + w, y1 + h) + + def free_cells(self): + """Returns a list of empty cells.""" + return [(x, y) + for x in xrange(self.COUNT_X) + for y in xrange(self.COUNT_Y) + if not self.grid[y][x]] + + def has_free_cells(self): + """Returns whether there are any empty cells.""" + return any(cell == 0 for row in self.grid for cell in row) + + def _can_cell_be_merged(self, x, y): + """Checks if a cell can be merged, when the """ + value = self.grid[y][x] + if y > 0 and self.grid[y - 1][x] == value: # Cell above + return True + if y < self.COUNT_Y - 1 and self.grid[y + 1][x] == value: # Cell below + return True + if x > 0 and self.grid[y][x - 1] == value: # Left + return True + if x < self.COUNT_X - 1 and self.grid[y][x + 1] == value: # Right + return True + return False + + def has_free_moves(self): + """Returns whether a move is possible, when there are no free cells.""" + return any(self._can_cell_be_merged(x, y) + for x in xrange(self.COUNT_X) + for y in xrange(self.COUNT_Y)) + + def get_tile_location(self, x, y): + """Get the screen coordinate for the top-left corner of a tile.""" + x1, y1 = self.origin + x1 += self.BORDER + (self.BORDER + self.cell_width) * x + y1 += self.BORDER + (self.BORDER + self.cell_height) * y + return x1, y1 + + def draw_grid(self): + """Draws the grid and tiles.""" + self.screen.fill((0xbb, 0xad, 0xa0), self.origin + (self.game_width, self.game_height)) + for y, row in enumerate(self.grid): + for x, cell in enumerate(row): + self.screen.blit(self.tiles[cell], self.get_tile_location(x, y)) + + def _draw_score_box(self, label, score, (x1, y1), (width, height)): + """Draw a score box, whether current or best.""" + pygame.draw.rect(self.screen, (187, 173, 160), (x1, y1, width, height)) + w, h = label.get_size() + self.screen.blit(label, (x1 + (width - w) / 2, y1 + 8)) + score = self.score_font.render(str(score), True, (255, 255, 255)) + w, h = score.get_size() + self.screen.blit(score, (x1 + (width - w) / 2, y1 + (height + label.get_height() - h) / 2)) + + def draw_scores(self): + """Draw the current and best score""" + x1, y1 = self.WIDTH - self.BORDER - 200 - 2 * self.BORDER, self.BORDER + width, height = 100, 60 + self.screen.fill((255, 255, 255), (x1, 0, self.WIDTH - x1, height + y1)) + self._draw_score_box(self.score_label, self.score, (x1, y1), (width, height)) + x2 = x1 + width + self.BORDER + self._draw_score_box(self.best_label, self.manager.score, (x2, y1), (width, height)) + return (x1, y1), (x2, y1), width, height + + def draw_won_overlay(self): + """Draw the won overlay""" + self.screen.blit(self.won_overlay, self.origin) + + def draw_lost_overlay(self): + """Draw the lost overlay""" + self.screen.blit(self.losing_overlay, self.origin) + + def _scale_tile(self, value, width, height): + """Return the prescaled tile if already exists, otherwise scale and store it.""" + try: + return self._scale_cache[value, width, height] + except KeyError: + tile = pygame.transform.smoothscale(self.tiles[value], (width, height)) + self._scale_cache[value, width, height] = tile + return tile + + def _center_tile(self, (x, y), (w, h)): + """Calculate the centre of a tile given the top-left corner and the size of the image.""" + return x + (self.cell_width - w) / 2, y + (self.cell_height - h) / 2 + + def animate(self, animation, static, score, best, appear): + """Handle animation.""" + + # Create a surface of static parts in the animation. + surface = pygame.Surface((self.game_width, self.game_height), 0) + surface.fill(self.BACKGROUND) + + # Draw all static tiles. + for y in xrange(self.COUNT_Y): + for x in xrange(self.COUNT_X): + x1, y1 = self.get_tile_location(x, y) + x1 -= self.origin[0] + y1 -= self.origin[1] + surface.blit(self.tiles[static.get((x, y), 0)], (x1, y1)) + + # Pygame clock for FPS control. + clock = pygame.time.Clock() + + if score: + score_label = self.label_font.render('+%d' % score, True, (119, 110, 101)) + w1, h1 = score_label.get_size() + + if best: + best_label = self.label_font.render('+%d' % best, True, (119, 110, 101)) + w2, h2 = best_label.get_size() + + # Loop through every frame. + for frame in xrange(self.ANIMATION_FRAMES): + # Limit at 60 fps. + clock.tick(60) + + # Pump events. + pygame.event.pump() + + self.screen.blit(surface, self.origin) + + # Calculate animation progress. + dt = (frame + 0.) / self.ANIMATION_FRAMES + + for tile in animation: + self.screen.blit(self.tiles[tile.value], tile.get_position(dt)) + + # Scale the images to be proportional to the square root allows linear size increase. + scale = dt ** 0.5 + + w, h = int(self.cell_width * scale) & ~1, int(self.cell_height * scale) & ~1 + + for x, y, value in appear: + self.screen.blit(self._scale_tile(value, w, h), + self._center_tile(self.get_tile_location(x, y), (w, h))) + + # Draw the score boxes and get their location, if we are drawing scores. + if best or score: + (x1, y1), (x2, y2), w, h = self.draw_scores() + if score: + self.screen.blit(score_label, (x1 + (w - w1) / 2, y1 + (h - h1) / 2 - dt * h)) + if best: + self.screen.blit(best_label, (x2 + (w - w2) / 2, y2 + (h - h2) / 2 - dt * h)) + + pygame.display.flip() + + def _spawn_new(self, count=1): + """Spawn some new tiles.""" + free = self.free_cells() + for x, y in random.sample(free, min(count, len(free))): + self.grid[y][x] = random.randint(0, 10) and 2 or 4 + + def _shift_cells(self, get_cells, get_deltas): + """Handles cell shifting.""" + # Don't do anything when there is an overlay. + if self.lost or self.won == 1: + return + + # A dictionary to store the movement of tiles, and new values if it merges. + tile_moved = {} + for y, row in enumerate(self.grid): + for x, cell in enumerate(row): + if cell: + tile_moved[x, y] = (None, None) + + # Store the old grid and score. + old_grid = [row[:] for row in self.grid] + old_score = self.score + self.old.append((old_grid, self.score)) + if len(self.old) > 10: + self.old.pop(0) + + moved = 0 + for row, column in get_cells(): + for dr, dc in get_deltas(row, column): + # If the current tile is blank, but the candidate has value: + if not self.grid[row][column] and self.grid[dr][dc]: + # Move the candidate to the current tile. + self.grid[row][column], self.grid[dr][dc] = self.grid[dr][dc], 0 + moved += 1 + tile_moved[dc, dr] = (column, row), None + if self.grid[dr][dc]: + # If the candidate can merge with the current tile: + if self.grid[row][column] == self.grid[dr][dc]: + self.grid[row][column] *= 2 + self.grid[dr][dc] = 0 + self.score += self.grid[row][column] + self.won += self.grid[row][column] == self.WIN_TILE + tile_moved[dc, dr] = (column, row), self.grid[row][column] + moved += 1 + # When hitting a tile we stop trying. + break + + # Submit the high score and get the change. + delta = self.manager.got_score(self.score) + free = self.free_cells() + new_tiles = set() + + if moved: + # Spawn new tiles if there are holes. + if free: + x, y = random.choice(free) + value = self.grid[y][x] = random.randint(0, 10) and 2 or 4 + new_tiles.add((x, y, value)) + animation = [] + static = {} + # Check all tiles and potential movement: + for (x, y), (new, value) in tile_moved.iteritems(): + # If not moved, store as static. + if new is None: + static[x, y] = old_grid[y][x] + else: + # Store the moving tile. + animation.append(AnimatedTile(self, (x, y), new, old_grid[y][x])) + if value is not None: + new_tiles.add(new + (value,)) + self.animate(animation, static, self.score - old_score, delta, new_tiles) + else: + self.old.pop() + + if not self.has_free_cells() and not self.has_free_moves(): + self.lost = True + + def on_event(self, event): + self.handlers.get(event.type, lambda e: None)(event) + + def on_key_down(self, event): + self.key_handlers.get(event.key, lambda e: None)(event) + + def on_mouse_up(self, event): + if self._is_in_restart(*event.pos) or self._is_in_try_again(*event.pos): + self.manager.new_game() + elif self._is_in_keep_going(*event.pos): + self.won += 1 + + def on_draw(self): + self.screen.fill((255, 255, 255)) + self.screen.blit(self.title, (0, 0)) + self.draw_scores() + self.draw_grid() + if self.won == 1: + self.draw_won_overlay() + elif self.lost: + self.draw_lost_overlay() + pygame.display.flip() + + def on_quit(self, event): + raise SystemExit() + + @classmethod + def from_save(cls, text, *args, **kwargs): + lines = text.strip().split('\n') + kwargs['score'] = int(lines[0]) + kwargs['grid'] = [map(int, row.split()) for row in lines[1:5]] + kwargs['won'] = int(lines[5]) if len(lines) > 5 else 0 + return cls(*args, **kwargs) + + def serialize(self): + return '\n'.join([str(self.score)] + + [' '.join(map(str, row)) for row in self.grid] + + [str(self.won)]) diff --git a/_2048/lock.py b/_2048/lock.py new file mode 100644 index 0000000..b51e64f --- /dev/null +++ b/_2048/lock.py @@ -0,0 +1,28 @@ +"""An implementation of a file locker.""" + +# Import msvcrt if possible. +try: + import msvcrt +except ImportError: + # Currently no linux solution with fcntl. + raise RuntimeError('Linux locker not written yet.') +else: + class FileLock(object): + def __init__(self, fd, size=65536): + if hasattr(fd, 'fileno') and callable(fd.fileno): + self.fd = fd.fileno() + else: + self.fd = fd + self.size = size + + def acquire(self, blocking=True): + msvcrt.locking(self.fd, (msvcrt.LK_NBLCK, msvcrt.LK_LOCK)[blocking], self.size) + + def release(self): + msvcrt.locking(self.fd, msvcrt.LK_UNLCK, self.size) + + def __enter__(self): + self.acquire() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.release() diff --git a/_2048/main.py b/_2048/main.py new file mode 100644 index 0000000..963e9dd --- /dev/null +++ b/_2048/main.py @@ -0,0 +1,42 @@ +import os + +import pygame + +from .game import Game2048 +from .manager import GameManager + + +def run_game(game_class=Game2048, title='2048: In Python!', data_dir=None): + pygame.init() + pygame.display.set_caption(title) + + # Try to set the game icon. + try: + pygame.display.set_icon(game_class.icon(32)) + except pygame.error: + # On windows, this can fail, so use GDI to draw then. + print 'Consider getting a newer card or drivers.' + os.environ['SDL_VIDEODRIVER'] = 'windib' + + if data_dir is None: + # Use current directory for now. + data_dir = os.getcwd() + + screen = pygame.display.set_mode((game_class.WIDTH, game_class.HEIGHT)) + manager = GameManager(Game2048, screen, + os.path.join(data_dir, '2048.score'), + os.path.join(data_dir, '2048.%d.state')) + try: + while True: + event = pygame.event.wait() + manager.dispatch(event) + for event in pygame.event.get(): + manager.dispatch(event) + manager.draw() + finally: + pygame.quit() + manager.close() + + +def main(): + run_game() diff --git a/_2048/manager.py b/_2048/manager.py new file mode 100644 index 0000000..4462297 --- /dev/null +++ b/_2048/manager.py @@ -0,0 +1,161 @@ +"""Defines the Game manager.""" + +import os +import errno +import itertools +from threading import Event, Thread + +from .lock import FileLock +from .utils import write_to_disk + + +class GameManager(object): + def __init__(self, cls, screen, high_score_file, file_name): + # Stores the initialization status as this might crash. + self.created = False + + self.score_name = high_score_file + self.screen = screen + self.save_name = file_name + self.game_class = cls + + self._score_changed = False + self._running = True + + self._change_event = Event() + self._saved_event = Event() + + try: + self.score_fd = self.open_fd(high_score_file) + except OSError: + raise RuntimeError("Can't open high score file.") + self.score_file = os.fdopen(self.score_fd, 'r+') + self.score_lock = FileLock(self.score_fd) + + with self.score_lock: + try: + self._score = self._load_score() + except ValueError: + self._score = 0 + self._score_changed = True + self.save() + + # Try opening save files from zero and counting up. + for i in itertools.count(0): + name = file_name % (i,) + try: + save = self.open_fd(name) + except IOError: + continue + else: + self.save_lock = FileLock(save) + try: + self.save_lock.acquire(False) + except IOError: + del self.save_lock + os.close(save) + continue + + self.save_fd = save + self.save_file = os.fdopen(save, 'r+') + + read = self.save_file.read() + if read: + self.game = self.game_class.from_save(read, self, screen) + else: + self.new_game() + self.save_file.seek(0, os.SEEK_SET) + + print 'Running as instance #%d.' % i + break + + self._worker = Thread(target=self._save_daemon) + self._worker.start() + + self._saved_event.set() + + self.created = True + + @classmethod + def open_fd(cls, name): + """Open a file or create it.""" + # Try to create it, if can't, try to open. + try: + return os.open(name, os.O_CREAT | os.O_RDWR | os.O_EXCL) + except OSError, e: + if e.errno != errno.EEXIST: + raise + return os.open(name, os.O_RDWR | os.O_EXCL) + + def new_game(self): + """Creates a new game of 2048.""" + self.game = self.game_class(self, self.screen) + self.save() + + def _load_score(self): + """Load the best score from file.""" + score = int(self.score_file.read()) + self.score_file.seek(0, os.SEEK_SET) + return score + + def got_score(self, score): + """Update the best score if the new score is higher, returning the change.""" + if score > self._score: + delta = score - self._score + self._score = score + self._score_changed = True + self.save() + return delta + return 0 + + @property + def score(self): + return self._score + + def save(self): + self._saved_event.clear() + self._change_event.set() + + def _save_daemon(self): + while self._running: + self._change_event.wait() + if self._score_changed: + with self.score_lock: + try: + score = self._load_score() + self._score = max(score, self._score) + except ValueError: + pass + self.score_file.write(str(self._score)) + self.score_file.truncate() + self.score_file.seek(0, os.SEEK_SET) + write_to_disk(self.score_file) + self._score_changed = False + if self.game.lost: + self.save_file.truncate() + else: + self.save_file.write(self.game.serialize()) + self.save_file.truncate() + self.save_file.seek(0, os.SEEK_SET) + + write_to_disk(self.save_file) + self._change_event.clear() + self._saved_event.set() + + def close(self): + if self.created: + self._running = False + self._saved_event.wait() + self.save() + self._worker.join() + self.save_lock.release() + self.score_file.close() + self.save_file.close() + + __del__ = close + + def dispatch(self, event): + self.game.on_event(event) + + def draw(self): + self.game.on_draw() diff --git a/_2048/utils.py b/_2048/utils.py new file mode 100644 index 0000000..78c61ba --- /dev/null +++ b/_2048/utils.py @@ -0,0 +1,41 @@ +import os +import tempfile +import time + +import pygame + +# Accurate timer for platform. +timer = [time.time, time.clock][os.name == 'nt'] + +# Get the temp file dir. +tempdir = tempfile.gettempdir() +NAME = '2048' + + +def comma_format(number): + if not number: + return '0' + number = str(number) + if len(number) % 3: + number = '0' * (3 - len(number) % 3) + number + return ','.join(number[i * 3:i * 3 + 3] for i in xrange(len(number) / 3)).lstrip('0') + + +def center(total, size): + return (total - size) / 2 + + +def load_font(name, size, cache={}): + if (name, size) in cache: + return cache[name, size] + if name.startswith('SYS:'): + font = pygame.font.SysFont(name[4:], size) + else: + font = pygame.font.Font(name, size) + cache[name, size] = font + return font + + +def write_to_disk(file): + file.flush() + os.fsync(file.fileno())