From 89e77ffa7dc2f2485049763b780fd5519541daa7 Mon Sep 17 00:00:00 2001 From: Jeremy Thurgood Date: Fri, 16 May 2014 19:00:46 +0200 Subject: [PATCH] Save game. --- naja/actions.py | 3 +- naja/gameboard.py | 19 +++++---- naja/gamestate.py | 10 ++--- naja/options.py | 17 ++++++++ naja/scenes/load_save.py | 79 ++++++++++++++++++++++++++++++++++++ naja/scenes/menu.py | 26 ++++++++---- naja/tests/test_gameboard.py | 51 +++++++++++++++-------- naja/widgets/save_slot.py | 72 ++++++++++++++++++++++++++++++++ naja/widgets/text.py | 3 ++ 9 files changed, 239 insertions(+), 41 deletions(-) create mode 100644 naja/scenes/load_save.py create mode 100644 naja/widgets/save_slot.py diff --git a/naja/actions.py b/naja/actions.py index ca472d3..596a505 100644 --- a/naja/actions.py +++ b/naja/actions.py @@ -32,7 +32,8 @@ class LocationAction(object): self.data['chesspiece']) if 'rot_direction' in self.data: - substitutions['rot_direction_name'] = '{%s}' % (substitutions['rot_direction'],) + substitutions['rot_direction_name'] = '{%s}' % ( + substitutions['rot_direction'],) if location is None: substitutions['location_bits'] = 'bits specified by this location' diff --git a/naja/gameboard.py b/naja/gameboard.py index 2cce169..bd4eb82 100644 --- a/naja/gameboard.py +++ b/naja/gameboard.py @@ -66,24 +66,24 @@ class GameBoard(object): for definition in locations_definition] def export_board_locations(self): - return dict( + return sorted( (position, location.export()) for position, location in self.board_locations.iteritems()) @classmethod def import_board_locations(cls, board_locations_definition): return dict( - (position, LocationCard.import_location(definition)) - for position, definition in board_locations_definition.iteritems()) + (tuple(position), LocationCard.import_location(definition)) + for position, definition in board_locations_definition) @classmethod def generate_board(cls, locations_definition): - board_locations = {} + board_locations = [] for x in range(5): for y in range(5): board_location = LocationCard.new_location( choice(locations_definition).copy()) - board_locations[(x, y)] = board_location.export() + board_locations.append([(x, y), board_location.export()]) return board_locations def lose_health(self): @@ -135,14 +135,14 @@ class GameBoard(object): rotated_locations = {} if py > 0: - for i in range(max(0, px -1), min(5, px + 2)): + for i in range(max(0, px - 1), min(5, px + 2)): locations_to_rotate.append((i, py - 1)) if px < 4: locations_to_rotate.append((px + 1, py)) if py < 4: - for i in reversed(range(max(0, px -1), min(5, px + 2))): + for i in reversed(range(max(0, px - 1), min(5, px + 2))): locations_to_rotate.append((i, py + 1)) if px > 0: @@ -151,7 +151,8 @@ class GameBoard(object): if ROTATION[direction] == ROTATION.CLOCKWISE: new_positions = locations_to_rotate[1:] + [locations_to_rotate[0]] elif ROTATION[direction] == ROTATION.ANTICLOCKWISE: - new_positions = [locations_to_rotate[-1]] + locations_to_rotate[:-1] + new_positions = ( + [locations_to_rotate[-1]] + locations_to_rotate[:-1]) for old, new in zip(locations_to_rotate, new_positions): rotated_locations[old] = self.board_locations[new] @@ -223,7 +224,7 @@ class LocationCard(object): def export(self): return { - 'bitwise_operand': self.bitwise_operand, + 'bitwise_operand': sorted(self.bitwise_operand), 'actions': [action.export() for action in self.actions], } diff --git a/naja/gamestate.py b/naja/gamestate.py index a2ed6e0..d5c624c 100644 --- a/naja/gamestate.py +++ b/naja/gamestate.py @@ -18,12 +18,12 @@ class GameState(object): Naja game state. """ - def __init__(self): - # This is a very simple deck to allow testing more drawing logic - # on tiles. These will need to be replaced with better stuff. + def __init__(self, data=None): locations_deck = load_location_deck('standard') - # locations_deck = load_location_deck('test') - self.gameboard = GameBoard.new_game(locations_deck['cards']) + if data is None: + self.gameboard = GameBoard.new_game(locations_deck['cards']) + else: + self.gameboard = GameBoard.import_game(data) @property def player(self): diff --git a/naja/options.py b/naja/options.py index 816d91c..6e5f00d 100644 --- a/naja/options.py +++ b/naja/options.py @@ -1,5 +1,6 @@ import optparse import os +import sys from naja.attrdict import AttrDict from naja.constants import DEFAULTS @@ -28,8 +29,24 @@ def parse_args(args): dest='music', action='store_false', default=True, help='Disable music (but not sound)') + parser.add_option("--save-location", default=_get_default_save_location(), + dest="save_location", help="Saved game location") + opts, _ = parser.parse_args(args) for k in DEFAULTS: if getattr(opts, k, None) is not None: options[k] = getattr(opts, k) + options['save_location'] = opts.save_location + + +def _get_default_save_location(): + """Return a default save game location.""" + app = "naja" + if sys.platform.startswith("win"): + if "APPDATA" in os.environ: + return os.path.join(os.environ["APPDATA"], app) + return os.path.join(os.path.expanduser("~"), "." + app) + elif 'XDG_DATA_HOME' in os.environ: + return os.path.join(os.environ["XDG_DATA_HOME"], app) + return os.path.join(os.path.expanduser("~"), ".local", "share", app) diff --git a/naja/scenes/load_save.py b/naja/scenes/load_save.py new file mode 100644 index 0000000..485b032 --- /dev/null +++ b/naja/scenes/load_save.py @@ -0,0 +1,79 @@ +""" +Load and save scenes. +""" + +import json +import os +from datetime import datetime + +import pygame.locals as pgl + +from naja.constants import KEYS +from naja.events import SceneChangeEvent, InvalidateTheWorld +from naja.options import options +from naja.scenes.scene import Scene +from naja.widgets.save_slot import SaveSlotWidget +from naja.widgets.selector import SelectorWidget + + +def save_path(path): + return os.path.join(options.save_location, *path.split('/')) + + +def ensure_save_path_exists(): + location = save_path('') + if not os.path.isdir(location): + os.makedirs(location) + + +class SaveGameScene(Scene): + def __init__(self, state): + super(SaveGameScene, self).__init__(state) + selector = SelectorWidget() + self.add(selector) + self.slots = {} + + for slot_num in range(8): + slot = self.make_slot_widget(slot_num) + self.slots[slot_num] = slot + selector.add(slot) + + def save_path(self, slot_num): + return save_path('slot_%s.json' % (slot_num,)) + + def get_game_data(self, slot_num): + try: + with open(self.save_path(slot_num), 'r') as save_file: + return json.load(save_file) + except IOError: + return None + except Exception as e: + print "Error reading savegame in slot %s: %s" % (slot_num, e) + return None + + def make_slot_widget(self, slot_num): + game_data = self.get_game_data(slot_num) + y_offset = 74 * slot_num + slot = SaveSlotWidget((100, y_offset), slot_num, game_data) + slot.add_callback('click', lambda event: self.save_game(slot_num)) + return slot + + def save_game(self, slot_num): + save_data = { + 'timestamp': datetime.now().ctime(), + 'data': self.state.gameboard.export(), + } + try: + ensure_save_path_exists() + with open(self.save_path(slot_num), 'w') as save_file: + json.dump(save_data, save_file) + except Exception as e: + print "Error saving game in slot %s: %s" % (slot_num, e) + self.slots[slot_num].game_data = self.get_game_data(slot_num) + InvalidateTheWorld.post() + + def handle_scene_event(self, ev): + if ev.type == pgl.KEYDOWN and ev.key in KEYS.QUIT: + from naja.scenes.menu import MenuScene + SceneChangeEvent.post(MenuScene) + return diff --git a/naja/scenes/menu.py b/naja/scenes/menu.py index 3a78969..e0383b5 100644 --- a/naja/scenes/menu.py +++ b/naja/scenes/menu.py @@ -5,13 +5,13 @@ Main menu scene. import pygame.locals as pgl from naja.constants import KEYS +from naja.events import SceneChangeEvent, QuitGameEvent from naja.scenes.scene import Scene -from naja.widgets.text import TextWidget -from naja.widgets.selector import SelectorWidget -from naja.events import QuitGameEvent from naja.scenes.credits import CreditsScene from naja.scenes.game import GameScene -from naja.events import SceneChangeEvent +from naja.scenes.load_save import SaveGameScene +from naja.widgets.selector import SelectorWidget +from naja.widgets.text import TextWidget class MenuScene(Scene): @@ -19,15 +19,23 @@ class MenuScene(Scene): super(MenuScene, self).__init__(state) selector = SelectorWidget() self.add(selector) + run_game = TextWidget((100, 100), 'Game', fontsize=32, colour='white') - run_game.add_callback('click', - lambda event: SceneChangeEvent.post(GameScene)) + run_game.add_callback( + 'click', lambda event: SceneChangeEvent.post(GameScene)) selector.add(run_game) + credits = TextWidget( - (100, 200), 'Credits', fontsize=32, colour='white') - credits.add_callback('click', - lambda event: SceneChangeEvent.post(CreditsScene)) + (100, 150), 'Credits', fontsize=32, colour='white') + credits.add_callback( + 'click', lambda event: SceneChangeEvent.post(CreditsScene)) selector.add(credits) + + save = TextWidget((100, 250), 'Save', fontsize=32, colour='white') + save.add_callback( + 'click', lambda event: SceneChangeEvent.post(SaveGameScene)) + selector.add(save) + quit = TextWidget((100, 300), 'Quit', fontsize=32, colour='white') quit.add_callback('click', lambda event: QuitGameEvent.post()) selector.add(quit) diff --git a/naja/tests/test_gameboard.py b/naja/tests/test_gameboard.py index d843c7e..e3aa32e 100644 --- a/naja/tests/test_gameboard.py +++ b/naja/tests/test_gameboard.py @@ -21,10 +21,15 @@ class TestGameBoard(TestCase): self.assertEqual(state1, state2) def test_export_new_board(self): - board = GameBoard.new_game([{'actions': [{ - 'action_class': 'LoseHealthOrMSB', - 'required_bits': [], - }]}]) + board = GameBoard.new_game([{'actions': [ + { + 'action_class': 'LoseHealthOrMSB', + 'required_bits': [], + }, { + 'action_class': 'GainHealth', + 'required_bits': [BITS.RED], + }, + ]}]) exported_state = board.export() board_locations = exported_state.pop('board_locations') self.assertEqual(exported_state, { @@ -32,24 +37,36 @@ class TestGameBoard(TestCase): 'health': 4, 'wins_required': 4, 'wins': 0, - 'locations': [{'actions': [{ - 'action_class': 'LoseHealthOrMSB', - 'required_bits': [], - }]}], + 'locations': [{'actions': [ + { + 'action_class': 'LoseHealthOrMSB', + 'required_bits': [], + }, { + 'action_class': 'GainHealth', + 'required_bits': [BITS.RED], + }, + ]}], 'player': board.player.export(), }) - self.assertEqual( - set(board_locations.keys()), - set((x, y) for x in range(5) for y in range(5))) - for location_state in board_locations.values(): + positions = [] + for position, location_state in board_locations: + positions.append(position) self.assertEqual( sorted(location_state.keys()), ['actions', 'bitwise_operand']) - self.assertEqual(location_state['actions'], [{ - 'action_class': 'LoseHealthOrMSB', - 'required_bits': [], - 'data': {}, - }]) + self.assertEqual(location_state['actions'], [ + { + 'action_class': 'LoseHealthOrMSB', + 'required_bits': [], + 'data': {}, + }, { + 'action_class': 'GainHealth', + 'required_bits': [BITS.RED], + 'data': {}, + }, + ]) self.assertTrue(2 <= len(location_state['bitwise_operand']) <= 3) + self.assertEqual( + positions, sorted((x, y) for x in range(5) for y in range(5))) def test_lose_health(self): board = GameBoard.new_game([{'actions': []}]) diff --git a/naja/widgets/save_slot.py b/naja/widgets/save_slot.py new file mode 100644 index 0000000..97be9ee --- /dev/null +++ b/naja/widgets/save_slot.py @@ -0,0 +1,72 @@ +import pygame + +from naja.constants import BITS, PALETTE +from naja.widgets.base import Widget +from naja.widgets.text import TextWidget, TextBoxWidget + + +class SaveSlotWidget(Widget): + """ + Widget for displaying a save slot. + """ + def __init__(self, pos, slot, game_data): + super(SaveSlotWidget, self).__init__(pos, (600, 64)) + self.slot = slot + self.game_data = game_data + + def prepare(self): + self.surface = pygame.surface.Surface(self.size) + header = self.get_slot_header() + name_text = "Slot %s: %s" % (self.slot, header) + name = TextWidget((0, 0), name_text, colour=PALETTE.WHITE) + name.render(self.surface) + self.prepare_game_state() + + def get_slot_header(self): + if self.game_data is None: + return "--" + else: + return self.game_data['timestamp'] + + def render_state_glyph(self, x_offset, text, colour): + widget = TextBoxWidget( + (x_offset, 32), text, box_width=32, colour=colour, + bg_colour=PALETTE.BLACK) + widget.render(self.surface) + + def prepare_game_state(self): + if self.game_data is None: + state_text = TextWidget((32, 32), '-EMPTY-', colour=PALETTE.WHITE) + state_text.render(self.surface) + else: + state_data = self.game_data['data'] + x_offset = 0 + + for i in range(state_data['max_health']): + x_offset += 32 + if i < state_data['health']: + colour = PALETTE.PINK + else: + colour = PALETTE.DARK_RED + self.render_state_glyph(x_offset, '{HEALTH_NOCOLOUR}', colour) + + for i in range(state_data['wins_required']): + x_offset += 32 + if i < state_data['wins']: + colour = PALETTE.LIGHT_TURQUOISE + else: + colour = PALETTE.DARK_OLIVE + self.render_state_glyph( + x_offset, '{WINTOKEN_NOCOLOUR}', colour) + + x_offset += 64 + bit_names = dict((v, k) for k, v in BITS.items()) + player_bits = state_data['player']['bits'] + for bit in reversed(range(8)): + x_offset += 32 + if (1 << bit) & player_bits: + text = '{%s}' % (bit_names[bit],) + self.render_state_glyph(x_offset, text, PALETTE.GREY) + + def draw(self, surface): + surface.blit(self.surface, self.pos) diff --git a/naja/widgets/text.py b/naja/widgets/text.py index e4eecaa..4343d01 100644 --- a/naja/widgets/text.py +++ b/naja/widgets/text.py @@ -21,6 +21,9 @@ MARKUP_MAP = { 'BLUE': ('glyphs/key.png', PALETTE.BLUE), 'CLOCKWISE': ('glyphs/clockwise.png', None), 'ANTICLOCKWISE': ('glyphs/anticlockwise.png', None), + + 'HEALTH_NOCOLOUR': ('glyphs/health.png', None), + 'WINTOKEN_NOCOLOUR': ('glyphs/win.png', None), } -- 2.34.1