# HG changeset patch # User Jeremy Thurgood # Date 1359283408 -7200 # Node ID bdaffaa8b6bfffbd25280b305cfd04a9f921253f # Parent 56ec01e51f3dbe682a1a5182bfa8423c500a99cc Loading and saving! (Plus a bunch of other stuff to make it possible.) diff -r 56ec01e51f3d -r bdaffaa8b6bf gamelib/constants.py --- a/gamelib/constants.py Sat Jan 26 20:29:58 2013 +0200 +++ b/gamelib/constants.py Sun Jan 27 12:43:28 2013 +0200 @@ -8,4 +8,4 @@ class SSConstants(GameConstants): title = _('Suspended Sentence') icon = 'suspended_sentence24x24.png' - i18n_name = 'suspended-sentence' + short_name = 'suspended-sentence' diff -r 56ec01e51f3d -r bdaffaa8b6bf gamelib/main.py --- a/gamelib/main.py Sat Jan 26 20:29:58 2013 +0200 +++ b/gamelib/main.py Sun Jan 27 12:43:28 2013 +0200 @@ -1,7 +1,7 @@ import scenes from constants import SSConstants -from menu import MenuScreen +from menu import SSMenuScreen from endscreen import EndScreen from ss_state import SSState @@ -13,13 +13,13 @@ INITIAL_SCENE = scenes.INITIAL_SCENE SCENE_LIST = scenes.SCENE_LIST SCREENS = { - 'menu': MenuScreen, + 'menu': SSMenuScreen, 'end': EndScreen, } START_SCREEN = 'menu' - def game_state(self): - return SSState() + def game_state_class(self): + return SSState def game_constants(self): return SSConstants() diff -r 56ec01e51f3d -r bdaffaa8b6bf gamelib/menu.py --- a/gamelib/menu.py Sat Jan 26 20:29:58 2013 +0200 +++ b/gamelib/menu.py Sun Jan 27 12:43:28 2013 +0200 @@ -2,44 +2,17 @@ # Copyright Boomslang team, 2010 (see COPYING File) # Main menu for the game -import pygame.event -from pygame.locals import QUIT -from pyntnclick.engine import Screen -from pyntnclick.widgets.imagebutton import ImageButtonWidget +from pyntnclick.menuscreen import MenuScreen -class MenuScreen(Screen): - def setup(self): - self._background = self.resource.get_image('splash/splash.png') +class SSMenuScreen(MenuScreen): + BACKGROUND_IMAGE = 'splash/splash.png' - self.add_image_button((16, 523), 'splash/play.png', self.start) - self._resume_button = self.add_image_button((256, 523), - 'splash/resume.png', self.resume) - self.add_image_button((580, 523), 'splash/quit.png', self.quit) - - def add_image_button(self, rect, image_name, callback): - image = self.resource.get_image(image_name) - widget = ImageButtonWidget(rect, self.gd, image) - widget.add_callback('clicked', callback) - self.container.add(widget) - return widget + def make_new_game_button(self): + return self.make_image_button((16, 523), 'splash/play.png') - def draw_background(self): - self.surface.blit(self._background, self.surface.get_rect()) - - def on_enter(self): - super(MenuScreen, self).on_enter() - self._resume_button.visible = self.check_running() + def make_resume_game_button(self): + return self.make_image_button((256, 523), 'splash/resume.png') - def start(self, ev, widget): - self.screen_event('game', 'restart') - self.change_screen('game') - - def check_running(self): - return self.gd.running - - def resume(self, ev, widget): - self.change_screen('game') - - def quit(self, ev, widget): - pygame.event.post(pygame.event.Event(QUIT)) + def make_quit_button(self): + return self.make_image_button((580, 523), 'splash/quit.png') diff -r 56ec01e51f3d -r bdaffaa8b6bf gamelib/scenes/bridge.py --- a/gamelib/scenes/bridge.py Sat Jan 26 20:29:58 2013 +0200 +++ b/gamelib/scenes/bridge.py Sun Jan 27 12:43:28 2013 +0200 @@ -163,8 +163,8 @@ INITIAL = 'chair' def get_description(self): - return self.game.current_scene.things['bridge.massagechair_base'] \ - .get_description() + base = self.game.get_current_scene().things['bridge.massagechair_base'] + return base.get_description() def is_interactive(self, tool=None): return False diff -r 56ec01e51f3d -r bdaffaa8b6bf gamelib/scenes/cryo.py --- a/gamelib/scenes/cryo.py Sat Jan 26 20:29:58 2013 +0200 +++ b/gamelib/scenes/cryo.py Sun Jan 27 12:43:28 2013 +0200 @@ -172,8 +172,8 @@ responses = [Result(_("It takes more effort than one would expect," " but eventually the pipe is separated from" " the wall."), soundfile="chop-chop.ogg")] - if self.game.current_scene.get_data('vandalism_warn'): - self.game.current_scene.set_data('vandalism_warn', False) + if self.game.get_current_scene().get_data('vandalism_warn'): + self.game.get_current_scene().set_data('vandalism_warn', False) responses.append(make_jim_dialog( _("Prisoner %s. Vandalism is an offence punishable by a " "minimum of an additional 6 months to your sentence." diff -r 56ec01e51f3d -r bdaffaa8b6bf gamelib/scenes/machine.py --- a/gamelib/scenes/machine.py Sat Jan 26 20:29:58 2013 +0200 +++ b/gamelib/scenes/machine.py Sun Jan 27 12:43:28 2013 +0200 @@ -99,7 +99,7 @@ INITIAL = "empty" INITIAL_DATA = { - 'contents': set(), + 'contents': [], } def select_interact(self): @@ -122,7 +122,7 @@ if "can" in contents: return Result(_("There is already a can in the welder.")) self.game.remove_inventory_item(item.name) - contents.add("can") + contents.append("can") self.set_interact() return Result(_("You carefully place the can in the laser welder.")) @@ -131,7 +131,7 @@ if "tube" in contents: return Result(_("There is already a tube fragment in the welder.")) self.game.remove_inventory_item(item.name) - contents.add("tube") + contents.append("tube") self.set_interact() return Result(_("You carefully place the tube fragments in the" " laser welder.")) @@ -179,7 +179,7 @@ return Result(_("The laser welder needs something to weld the" " tube fragments to.")) else: - welder_slot.set_data("contents", set()) + welder_slot.set_data("contents", []) welder_slot.set_interact() if self.game.is_in_inventory("cryo_pipes_one"): self.game.replace_inventory_item("cryo_pipes_one", diff -r 56ec01e51f3d -r bdaffaa8b6bf gamelib/tests/test_scene_interactions_cryo.py --- a/gamelib/tests/test_scene_interactions_cryo.py Sat Jan 26 20:29:58 2013 +0200 +++ b/gamelib/tests/test_scene_interactions_cryo.py Sun Jan 27 12:43:28 2013 +0200 @@ -72,6 +72,7 @@ def test_cryo_unit_alpha_full_hand(self): "The cryo unit has the leg in it and we touch it. We get the leg." + self.interact_thing('cryo.unit.1') self.assert_game_data('contains_titanium_leg', True, 'cryo.unit.1') self.assert_inventory_item('titanium_leg', False) self.assert_detail_thing('cryo.titanium_leg', True) diff -r 56ec01e51f3d -r bdaffaa8b6bf gamelib/tests/test_walkthrough.py --- a/gamelib/tests/test_walkthrough.py Sat Jan 26 20:29:58 2013 +0200 +++ b/gamelib/tests/test_walkthrough.py Sun Jan 27 12:43:28 2013 +0200 @@ -9,7 +9,7 @@ CURRENT_SCENE = 'cryo' def move_to(self, target): - self.interact_thing(self.state.current_scene.name + '.door') + self.interact_thing(self.state.get_current_scene().name + '.door') self.assert_current_scene('map') self.interact_thing('map.to' + target) self.assert_current_scene(target) @@ -32,6 +32,7 @@ self.interact_thing('cryo.titanium_leg', detail='cryo_detail') self.assert_detail_thing('cryo.titanium_leg', False) self.assert_inventory_item('titanium_leg') + self.close_detail() # Open the door the rest of the way. self.interact_thing('cryo.door', 'titanium_leg') @@ -113,6 +114,7 @@ self.interact_thing('bridge.superconductor', detail='chair_detail') self.assert_inventory_item('superconductor') self.assert_detail_thing('bridge.superconductor', False) + self.close_detail() # Go to the crew quarters. self.move_to('crew_quarters') @@ -172,41 +174,41 @@ self.move_to('machine') # Weld pipes and cans. - self.assert_game_data('contents', set(), 'machine.welder.slot') + self.assert_game_data('contents', [], 'machine.welder.slot') self.interact_thing('machine.welder.slot', 'tube_fragment.0') self.assert_inventory_item('tube_fragment.0', False) - self.assert_game_data('contents', set(['tube']), 'machine.welder.slot') + self.assert_game_data('contents', ['tube'], 'machine.welder.slot') self.interact_thing('machine.welder.slot', 'empty_can.1') self.assert_inventory_item('empty_can.1', False) self.assert_game_data( - 'contents', set(['tube', 'can']), 'machine.welder.slot') + 'contents', ['tube', 'can'], 'machine.welder.slot') self.interact_thing('machine.welder.button') - self.assert_game_data('contents', set(), 'machine.welder.slot') + self.assert_game_data('contents', [], 'machine.welder.slot') self.assert_inventory_item('cryo_pipes_one') - self.assert_game_data('contents', set(), 'machine.welder.slot') + self.assert_game_data('contents', [], 'machine.welder.slot') self.interact_thing('machine.welder.slot', 'tube_fragment.2') self.assert_inventory_item('tube_fragment.2', False) - self.assert_game_data('contents', set(['tube']), 'machine.welder.slot') + self.assert_game_data('contents', ['tube'], 'machine.welder.slot') self.interact_thing('machine.welder.slot', 'empty_can.2') self.assert_inventory_item('empty_can.2', False) self.assert_game_data( - 'contents', set(['tube', 'can']), 'machine.welder.slot') + 'contents', ['tube', 'can'], 'machine.welder.slot') self.interact_thing('machine.welder.button') - self.assert_game_data('contents', set(), 'machine.welder.slot') + self.assert_game_data('contents', [], 'machine.welder.slot') self.assert_inventory_item('cryo_pipes_one', False) self.assert_inventory_item('cryo_pipes_two') - self.assert_game_data('contents', set(), 'machine.welder.slot') + self.assert_game_data('contents', [], 'machine.welder.slot') self.interact_thing('machine.welder.slot', 'tube_fragment.1') self.assert_inventory_item('tube_fragment.1', False) - self.assert_game_data('contents', set(['tube']), 'machine.welder.slot') + self.assert_game_data('contents', ['tube'], 'machine.welder.slot') self.interact_thing('machine.welder.slot', 'empty_can.0') self.assert_inventory_item('empty_can.0', False) self.assert_game_data( - 'contents', set(['tube', 'can']), 'machine.welder.slot') + 'contents', ['tube', 'can'], 'machine.welder.slot') self.interact_thing('machine.welder.button') - self.assert_game_data('contents', set(), 'machine.welder.slot') + self.assert_game_data('contents', [], 'machine.welder.slot') self.assert_inventory_item('cryo_pipes_two', False) self.assert_inventory_item('cryo_pipes_three') diff -r 56ec01e51f3d -r bdaffaa8b6bf pyntnclick/constants.py --- a/pyntnclick/constants.py Sat Jan 26 20:29:58 2013 +0200 +++ b/pyntnclick/constants.py Sun Jan 27 12:43:28 2013 +0200 @@ -15,7 +15,7 @@ class GameConstants(object): title = None - i18n_name = 'pyntnclick' + short_name = 'pyntnclick' # Icon for the main window, in the icons basedir icon = None diff -r 56ec01e51f3d -r bdaffaa8b6bf pyntnclick/gamescreen.py --- a/pyntnclick/gamescreen.py Sat Jan 26 20:29:58 2013 +0200 +++ b/pyntnclick/gamescreen.py Sun Jan 27 12:43:28 2013 +0200 @@ -123,7 +123,8 @@ @property def slot_items(self): - return self.game.inventory()[self.inv_offset:][:len(self.slots)] + item_names = self.game.inventory()[self.inv_offset:][:len(self.slots)] + return [self.game.items[name] for name in item_names] def mouse_down(self, event, widget): if event.button != 1: @@ -257,11 +258,24 @@ getattr(self, 'game_event_%s' % event_name, lambda d: None)(data) def game_event_restart(self, data): - self.reset_game(self.create_initial_state()) + self.reset_game() + + def get_save_dir(self): + return self.gd.get_default_save_location() + + def game_event_load(self, data): + state = self.gd.game_state_class().load_game( + self.get_save_dir(), 'savegame') + # TODO: Handle this better. + if state is not None: + self.reset_game(state) + + def game_event_save(self, data): + self.game.data.save_game(self.get_save_dir(), 'savegame') def reset_game(self, game_state=None): self._clear_all() - self.game = self.create_initial_state() + self.game = self.create_initial_state(game_state) self.screen_modal = self.container.add( ModalStackContainer(self.container.rect.copy(), self.gd)) @@ -294,9 +308,8 @@ for scene_widget in reversed(self.scene_modal.children[:]): self.scene_modal.remove(scene_widget) scene_widget.scene.leave() - scene = self.game.scenes[scene_name] - self.game.current_scene = scene - self._add_scene(scene) + self.game.data.set_current_scene(scene_name) + self._add_scene(self.game.scenes[scene_name]) def show_detail(self, detail_name): self._add_scene(self.game.detail_views[detail_name], True) diff -r 56ec01e51f3d -r bdaffaa8b6bf pyntnclick/main.py --- a/pyntnclick/main.py Sat Jan 26 20:29:58 2013 +0200 +++ b/pyntnclick/main.py Sun Jan 27 12:43:28 2013 +0200 @@ -70,9 +70,9 @@ lang = locale.getdefaultlocale(['LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG'])[0] self.resource = Resources(self._resource_module, lang) - gettext.bindtextdomain(self.constants.i18n_name, + gettext.bindtextdomain(self.constants.short_name, self.resource.get_resource_path('locale')) - gettext.textdomain(self.constants.i18n_name) + gettext.textdomain(self.constants.short_name) self._check_translations() @@ -88,7 +88,7 @@ if candidate.endswith('.po'): polang = candidate.split('.', 1)[0] pofile = os.path.join(popath, candidate) - mofile = gettext.find(self.constants.i18n_name, mopath, + mofile = gettext.find(self.constants.short_name, mopath, (polang,)) if mofile is None: print 'Missing mo file for %s' % pofile @@ -97,17 +97,19 @@ print 'po file %s is newer than mo file %s' % (pofile, mofile) - def initial_state(self): + def initial_state(self, game_state=None): """Create a copy of the initial game state.""" - initial_state = state.Game(self) + initial_state = state.Game(self, self.game_state_class()(game_state)) initial_state.set_debug_rects(self._debug_rects) for scene in self._scene_list: initial_state.load_scenes(scene) - initial_state.change_scene(self._initial_scene) + if initial_state.data['current_scene'] is None: + initial_state.data.set_current_scene(self._initial_scene) + initial_state.change_scene(initial_state.data['current_scene']) return initial_state - def game_state(self): - return state.GameState() + def game_state_class(self): + return state.GameState def game_constants(self): return GameConstants() @@ -193,3 +195,14 @@ self.engine.run() except KeyboardInterrupt: pass + + def get_default_save_location(self): + """Return a default save game location.""" + app = self.constants.short_name + 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 -r 56ec01e51f3d -r bdaffaa8b6bf pyntnclick/state.py --- a/pyntnclick/state.py Sat Jan 26 20:29:58 2013 +0200 +++ b/pyntnclick/state.py Sun Jan 27 12:43:28 2013 +0200 @@ -1,5 +1,7 @@ """Utilities and base classes for dealing with scenes.""" +import os +import json import copy from widgets.text import LabelWidget @@ -45,8 +47,10 @@ sub-class this and feed the subclass into GameDescription via the custom_data parameter.""" - def __init__(self): - self._game_state = {'inventories': {'main': []}} + def __init__(self, state_dict=None): + if state_dict is None: + state_dict = {'inventories': {'main': []}, 'current_scene': None} + self._game_state = copy.deepcopy(state_dict) def __getitem__(self, key): return self._game_state[key] @@ -54,6 +58,9 @@ def __contains__(self, key): return key in self._game_state + def export_data(self): + return copy.deepcopy(self._game_state) + def get_all_gizmo_data(self, state_key): """Get all state for a gizmo - returns a dict""" return self[state_key] @@ -71,13 +78,37 @@ if state_key not in self._game_state: self._game_state[state_key] = {} if initial_data: - # deep copy of INITIAL_DATA allows lists, sets and - # other mutable types to safely be used in INITIAL_DATA + # deep copy of INITIAL_DATA allows lists, dicts and other + # mutable types to safely be used in INITIAL_DATA self._game_state[state_key].update(copy.deepcopy(initial_data)) def inventory(self, name='main'): return self['inventories'][name] + def set_current_scene(self, scene_name): + self._game_state['current_scene'] = scene_name + + @classmethod + def get_save_fn(cls, save_dir, save_name): + return os.path.join(save_dir, '%s.json' % (save_name,)) + + @classmethod + def load_game(cls, save_dir, save_name): + fn = cls.get_save_fn(save_dir, save_name) + if os.access(fn, os.R_OK): + f = open(fn, 'r') + state = json.load(f) + f.close() + return state + + def save_game(self, save_dir, save_name): + fn = self.get_save_fn(save_dir, save_name) + if not os.path.isdir(save_dir): + os.makedirs(save_dir) + f = open(fn, 'w') + json.dump(self.export_data(), f) + f.close() + class Game(object): """Complete game state. @@ -87,7 +118,7 @@ * items * scenes """ - def __init__(self, gd): + def __init__(self, gd, game_state): # game description self.gd = gd # map of scene name -> Scene object @@ -101,12 +132,16 @@ # currently selected tool (item) self.tool = None # Global game data - self.data = self.gd.game_state() - # current scene - self.current_scene = None + self.data = game_state # debug rects self.debug_rects = False + def get_current_scene(self): + scene_name = self.data['current_scene'] + if scene_name is not None: + return self.scenes[scene_name] + return None + def inventory(self, name=None): if name is None: name = self.current_inventory @@ -153,16 +188,14 @@ ScreenEvent.post('game', 'inventory', None) def add_inventory_item(self, name): - self.inventory().append(self.items[name]) + self.inventory().append(name) self._update_inventory() def is_in_inventory(self, name): - if name in self.items: - return self.items[name] in self.inventory() - return False + return name in self.inventory() def remove_inventory_item(self, name): - self.inventory().remove(self.items[name]) + self.inventory().remove(name) # Unselect tool if it's removed if self.tool == self.items[name]: self.set_tool(None) @@ -171,8 +204,8 @@ def replace_inventory_item(self, old_item_name, new_item_name): """Try to replace an item in the inventory with a new one""" try: - index = self.inventory().index(self.items[old_item_name]) - self.inventory()[index] = self.items[new_item_name] + index = self.inventory().index(old_item_name) + self.inventory()[index] = new_item_name if self.tool == self.items[old_item_name]: self.set_tool(self.items[new_item_name]) except ValueError: diff -r 56ec01e51f3d -r bdaffaa8b6bf pyntnclick/tests/game_logic_utils.py --- a/pyntnclick/tests/game_logic_utils.py Sat Jan 26 20:29:58 2013 +0200 +++ b/pyntnclick/tests/game_logic_utils.py Sun Jan 27 12:43:28 2013 +0200 @@ -19,12 +19,23 @@ self.game_description = self.GAME_DESCRIPTION_CLASS() self.state = self.game_description.initial_state() - self.state.current_scene = self.state.scenes[self.CURRENT_SCENE] + self.scene_stack = [] + + # We aren't handling events, monkey patch change_scene and show_detail + def change_scene(name): + self.state.data.set_current_scene(name) + self.scene_stack = [self.state.get_current_scene()] + self.state.change_scene = change_scene - # We aren't handling events, monkey patch change_scene - def change_scene(name): - self.state.current_scene = self.state.scenes[name] - self.state.change_scene = change_scene + def show_detail(name): + self.scene_stack.append(self.state.detail_views[name]) + self.state.show_detail = show_detail + + self.state.change_scene(self.CURRENT_SCENE) + + def close_detail(self): + self.scene_stack.pop() + self.assertTrue(len(self.scene_stack) > 0) def tearDown(self): for item in self.state.items.values(): @@ -42,14 +53,14 @@ self.state.inventory()[:] = [] def set_game_data(self, key, value, thing=None): - gizmo = self.state.current_scene + gizmo = self.state.get_current_scene() if thing is not None: gizmo = gizmo.things[thing] gizmo.set_data(key, value) def assert_game_data(self, key, value, thing=None, scene=None, detail=None): - gizmo = self.state.current_scene + gizmo = self.state.get_current_scene() if scene is not None: gizmo = self.state.scenes[scene] if detail is not None: @@ -62,17 +73,17 @@ self.assertEquals(in_inventory, self.state.is_in_inventory(item)) def assert_scene_thing(self, thing, in_scene=True): - self.assertEquals(in_scene, thing in self.state.current_scene.things) + self.assertEquals( + in_scene, thing in self.state.get_current_scene().things) def assert_detail_thing(self, thing, in_detail=True): - return - self.assertEquals(in_detail, thing in self.state.current_detail.things) + self.assertEquals(in_detail, thing in self.scene_stack[-1].things) def assert_item_exists(self, item, exists=True): self.assertEquals(exists, item in self.state.items) def assert_current_scene(self, scene): - self.assertEquals(scene, self.state.current_scene.name) + self.assertEquals(scene, self.state.get_current_scene().name) def handle_result(self, result): self.clear_event_queue() @@ -89,9 +100,9 @@ if item is not None: self.assert_inventory_item(item) item_obj = self.state.items[item] - thing_container = self.state.current_scene + thing_container = self.scene_stack[-1] if detail is not None: - thing_container = self.state.detail_views[detail] + self.assertEqual(detail, thing_container.name) result = thing_container.things[thing].interact(item_obj) return self.handle_result(result) diff -r 56ec01e51f3d -r bdaffaa8b6bf pyntnclick/tests/mad_clicker.py --- a/pyntnclick/tests/mad_clicker.py Sat Jan 26 20:29:58 2013 +0200 +++ b/pyntnclick/tests/mad_clicker.py Sun Jan 27 12:43:28 2013 +0200 @@ -86,7 +86,7 @@ def do_mad_clicker(self): """Implement frantic clicking behaviour""" for scene in self.state.scenes.values(): - self.state.current_scene = scene + self.state.data.set_current_scene(scene.name) for thing in scene.things.values(): for interact_name in thing.interacts: thing._set_interact(interact_name) diff -r 56ec01e51f3d -r bdaffaa8b6bf pyntnclick/widgets/text.py --- a/pyntnclick/widgets/text.py Sat Jan 26 20:29:58 2013 +0200 +++ b/pyntnclick/widgets/text.py Sun Jan 27 12:43:28 2013 +0200 @@ -15,6 +15,7 @@ self.fontname = fontname or constants.font self.fontsize = fontsize or constants.font_size self.color = color or constants.text_color + self.visible = True def prepare(self): self.font = self.resource.get_font(self.fontname, self.fontsize) @@ -26,8 +27,9 @@ self.rect.height = max(self.rect.height, height) def draw(self, surface): - self.do_prepare() - surface.blit(self.surface, self.rect) + if self.visible: + self.do_prepare() + surface.blit(self.surface, self.rect) class LabelWidget(TextWidget):