changeset 792:bdaffaa8b6bf pyntnclick

Loading and saving! (Plus a bunch of other stuff to make it possible.)
author Jeremy Thurgood <firxen@gmail.com>
date Sun, 27 Jan 2013 12:43:28 +0200
parents 56ec01e51f3d
children 0e5b80b3128c
files gamelib/constants.py gamelib/main.py gamelib/menu.py gamelib/scenes/bridge.py gamelib/scenes/cryo.py gamelib/scenes/machine.py gamelib/tests/test_scene_interactions_cryo.py gamelib/tests/test_walkthrough.py pyntnclick/constants.py pyntnclick/gamescreen.py pyntnclick/main.py pyntnclick/state.py pyntnclick/tests/game_logic_utils.py pyntnclick/tests/mad_clicker.py pyntnclick/widgets/text.py
diffstat 15 files changed, 156 insertions(+), 108 deletions(-) [+]
line wrap: on
line diff
--- 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'
--- 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()
--- 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')
--- 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
--- 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."
--- 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",
--- 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)
--- 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')
 
--- 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
 
--- 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)
--- 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)
--- 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:
--- 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)
 
--- 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)
--- 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):