changeset 816:eed75a1d50c4 pyntnclick

Better Item handling.
author Jeremy Thurgood <firxen@gmail.com>
date Sun, 27 Jan 2013 22:19:39 +0200
parents 8f94fbf05ab9
children beac13c4e982
files gamelib/scenes/bridge.py gamelib/scenes/crew_quarters.py gamelib/scenes/cryo.py gamelib/scenes/engine.py gamelib/scenes/machine.py gamelib/scenes/map.py gamelib/scenes/mess.py gamelib/tests/test_scene_interactions_cryo.py gamelib/tests/test_walkthrough.py pyntnclick/gamescreen.py pyntnclick/state.py pyntnclick/tests/game_logic_utils.py
diffstat 12 files changed, 243 insertions(+), 174 deletions(-) [+]
line wrap: on
line diff
--- a/gamelib/scenes/bridge.py	Sun Jan 27 22:09:34 2013 +0200
+++ b/gamelib/scenes/bridge.py	Sun Jan 27 22:19:39 2013 +0200
@@ -42,8 +42,9 @@
 
     def setup(self):
         self.background_playlist = None
-        self.add_item(Superconductor('superconductor'))
-        self.add_item(Stethoscope('stethoscope'))
+        self.add_item_factory(Superconductor)
+        self.add_item_factory(TapedSuperconductor)
+        self.add_item_factory(Stethoscope)
         self.add_thing(ToMap())
         self.add_thing(MonitorCamera())
         self.add_thing(MassageChair())
@@ -173,6 +174,7 @@
 class Stethoscope(Item):
     "Used for cracking safes. Found on the doctor on the chair"
 
+    NAME = 'stethoscope'
     INVENTORY_IMAGE = 'stethoscope.png'
     CURSOR = CursorSprite('stethoscope.png')
 
@@ -203,6 +205,7 @@
 class TapedSuperconductor(Item):
     "Used for connecting high-powered parts of the ship up"
 
+    NAME = 'taped_superconductor'
     INVENTORY_IMAGE = 'superconductor_taped.png'
     CURSOR = CursorSprite('superconductor_taped_cursor.png')
 
@@ -210,13 +213,12 @@
 class Superconductor(Item):
     "Used for connecting high-powered parts of the ship up"
 
+    NAME = 'superconductor'
     INVENTORY_IMAGE = 'superconductor_fixed.png'
     CURSOR = CursorSprite('superconductor_fixed.png')
 
     def interact_with_duct_tape(self, item):
-        taped_superconductor = TapedSuperconductor('taped_superconductor')
-        self.game.add_item(taped_superconductor)
-        self.game.replace_inventory_item(self.name, taped_superconductor.name)
+        self.game.replace_inventory_item(self.name, 'taped_superconductor')
         return Result(_("You rip off a piece of duct tape and stick it on the"
                         " superconductor. It almost sticks to itself, but you"
                         " successfully avoid disaster."))
--- a/gamelib/scenes/crew_quarters.py	Sun Jan 27 22:09:34 2013 +0200
+++ b/gamelib/scenes/crew_quarters.py	Sun Jan 27 22:19:39 2013 +0200
@@ -22,9 +22,10 @@
         self.add_thing(ToMap())
         self.add_thing(Safe())
         self.add_thing(FishbowlThing())
-        self.add_item(Fishbowl('fishbowl'))
-        self.add_item(DuctTape('duct_tape'))
-        self.add_item(EscherPoster('escher_poster'))
+        self.add_item_factory(Fishbowl)
+        self.add_item_factory(DuctTape)
+        self.add_item_factory(EscherPoster)
+        self.add_item_factory(FishbowlHelmet)
         self.add_thing(PosterThing())
         self.add_thing(MonitorCamera())
         self.add_thing(GenericDescThing('crew.plant', 1,
@@ -154,9 +155,7 @@
     NAME = "fishbowl"
 
     def interact_with_duct_tape(self, item):
-        helmet = FishbowlHelmet('helmet')
-        self.game.add_item(helmet)
-        self.game.replace_inventory_item(self.name, helmet.name)
+        self.game.replace_inventory_item(self.name, 'helmet')
         return Result(_("You duct tape the edges of the helmet. The seal is"
                         " crude, but it will serve as a workable helmet if"
                         " needed."))
@@ -173,6 +172,7 @@
 class DuctTape(Item):
     "A bowl. Sans fish."
 
+    NAME = 'duct_tape'
     INVENTORY_IMAGE = 'duct_tape.png'
     CURSOR = CursorSprite('duct_tape.png')
 
--- a/gamelib/scenes/cryo.py	Sun Jan 27 22:09:34 2013 +0200
+++ b/gamelib/scenes/cryo.py	Sun Jan 27 22:19:39 2013 +0200
@@ -34,7 +34,9 @@
             ]
 
     def setup(self):
-        self.add_item(TitaniumLeg("titanium_leg"))
+        self.add_item_factory(TitaniumLeg)
+        self.add_item_factory(TubeFragment)
+        self.add_item_factory(FullBottle)
         self.add_thing(CryoUnitAlpha())
         self.add_thing(CryoRoomDoor())
         self.add_thing(CryoComputer())
@@ -165,9 +167,7 @@
     def interact_with_machete(self, item):
         if self.get_data('fixed'):
             self.set_data('fixed', False)
-            pipe = TubeFragment('tube_fragment')
-            self.game.add_item(pipe)
-            self.game.add_inventory_item(pipe.name)
+            self.game.add_inventory_item('tube_fragment')
             self.set_interact()
             responses = [Result(_("It takes more effort than one would expect,"
                                   " but eventually the pipe is separated from"
@@ -226,9 +226,11 @@
 class TubeFragment(CloneableItem):
     "Obtained after cutting down a cryo room pipe."
 
+    NAME = "tube_fragment"
     INVENTORY_IMAGE = "tube_fragment.png"
     CURSOR = CursorSprite('tube_fragment_cursor.png')
     TOOL_NAME = "tube_fragment"
+    MAX_COUNT = 3
 
 
 class CryoPipeLeft(CryoPipeBase):
@@ -264,6 +266,7 @@
 class TitaniumLeg(Item):
     "Titanium leg, found on a piratical corpse."
 
+    NAME = 'titanium_leg'
     INVENTORY_IMAGE = "titanium_femur.png"
     CURSOR = CursorSprite('titanium_femur_cursor.png', 13, 5)
 
@@ -463,6 +466,7 @@
 
 
 class FullBottle(Item):
+    NAME = 'full_detergent_bottle'
     INVENTORY_IMAGE = 'bottle_full.png'
     CURSOR = CursorSprite('bottle_full_cursor.png', 27, 7)
 
@@ -489,9 +493,7 @@
         return Result(_("It's gooey"))
 
     def interact_with_detergent_bottle(self, item):
-        full = FullBottle('full_detergent_bottle')
-        self.game.add_item(full)
-        self.game.replace_inventory_item(item.name, full.name)
+        self.game.replace_inventory_item(item.name, 'full_detergent_bottle')
         return Result(_("You scoop up some coolant and fill the bottle."))
 
 
--- a/gamelib/scenes/engine.py	Sun Jan 27 22:09:34 2013 +0200
+++ b/gamelib/scenes/engine.py	Sun Jan 27 22:19:39 2013 +0200
@@ -22,7 +22,7 @@
         }
 
     def setup(self):
-        self.add_item(CanOpener('canopener'))
+        self.add_item_factory(CanOpener)
         self.add_thing(CanOpenerThing())
         self.add_thing(SuperconductorSocket())
         self.add_thing(PowerLines())
@@ -160,6 +160,7 @@
 
 
 class CanOpener(Item):
+    NAME = 'canopener'
     INVENTORY_IMAGE = 'can_opener.png'
     CURSOR = CursorSprite('can_opener_cursor.png')
 
--- a/gamelib/scenes/machine.py	Sun Jan 27 22:09:34 2013 +0200
+++ b/gamelib/scenes/machine.py	Sun Jan 27 22:19:39 2013 +0200
@@ -22,11 +22,11 @@
         self.add_thing(LaserWelderPowerLights())
         self.add_thing(Grinder())
         self.add_thing(ManualThing())
-        self.add_item(TitaniumMachete('machete'))
-        self.add_item(CryoPipesOne('cryo_pipes_one'))
-        self.add_item(CryoPipesTwo('cryo_pipes_two'))
-        self.add_item(CryoPipesThree('cryo_pipes_three'))
-        self.add_item(Manual('manual'))
+        self.add_item_factory(TitaniumMachete)
+        self.add_item_factory(CryoPipesOne)
+        self.add_item_factory(CryoPipesTwo)
+        self.add_item_factory(CryoPipesThree)
+        self.add_item_factory(Manual)
         self.add_thing(GenericDescThing('machine.wires', 2,
             _("Wires run to all the machines in the room"),
             (
@@ -181,19 +181,19 @@
         else:
             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",
+            if self.game.is_in_inventory("cryo_pipes_one:"):
+                self.game.replace_inventory_item("cryo_pipes_one:",
                                                   "cryo_pipes_two")
                 return Result(_("With high-precision spitzensparken, you weld"
                                 " together a second pipe. You bundle the two"
                                 " pipes together."), soundfile='laser.ogg')
-            elif self.game.is_in_inventory("cryo_pipes_two"):
-                self.game.replace_inventory_item("cryo_pipes_two",
+            elif self.game.is_in_inventory("cryo_pipes_two:"):
+                self.game.replace_inventory_item("cryo_pipes_two:",
                                                   "cryo_pipes_three")
                 return Result(_("With high-precision spitzensparken, you"
                                 " create yet another pipe. You store it with"
                                 " the other two."), soundfile='laser.ogg')
-            elif self.game.is_in_inventory("cryo_pipes_three"):
+            elif self.game.is_in_inventory("cryo_pipes_three:"):
                 # just for safety
                 return None
             else:
@@ -223,6 +223,7 @@
 class CryoPipesOne(Item):
     "A single cryo pipe (made from a tube fragment and can)."
 
+    NAME = 'cryo_pipes_one'
     INVENTORY_IMAGE = "cryo_pipes_one.png"
     CURSOR = CursorSprite('cryo_pipes_one_cursor.png')
     TOOL_NAME = "cryo_pipes_one"
@@ -231,6 +232,7 @@
 class CryoPipesTwo(Item):
     "Two cryo pipes (each made from a tube fragment and can)."
 
+    NAME = 'cryo_pipes_two'
     INVENTORY_IMAGE = "cryo_pipes_two.png"
     CURSOR = CursorSprite('cryo_pipes_two_cursor.png')
     TOOL_NAME = "cryo_pipes_two"
@@ -239,6 +241,7 @@
 class CryoPipesThree(Item):
     "Three cryo pipes (each made from a tube fragment and can)."
 
+    NAME = 'cryo_pipes_three'
     INVENTORY_IMAGE = "cryo_pipes_three.png"
     CURSOR = CursorSprite('cryo_pipes_three_cursor.png')
     TOOL_NAME = "cryo_pipes_three"
@@ -273,6 +276,7 @@
 class TitaniumMachete(Item):
     "Titanium machete, formerly a leg."
 
+    NAME = 'machete'
     INVENTORY_IMAGE = "machete.png"
     CURSOR = CursorSprite('machete_cursor.png', 23, 1)
 
@@ -298,6 +302,7 @@
 class Manual(Item):
     "A ship instruction manual."
 
+    NAME = 'manual'
     INVENTORY_IMAGE = "manual.png"
     CURSOR = None
 
--- a/gamelib/scenes/map.py	Sun Jan 27 22:09:34 2013 +0200
+++ b/gamelib/scenes/map.py	Sun Jan 27 22:19:39 2013 +0200
@@ -125,7 +125,7 @@
     INITIAL = 'door'
 
     def interact(self, item):
-        if not self.game.is_in_inventory('helmet'):
+        if not self.game.is_in_inventory('helmet:'):
             return Result(_('The airlock refuses to open. The automated'
                     ' voice says: "Hull breach beyond this door. Personnel'
                     ' must be equipped for vacuum before entry."'))
--- a/gamelib/scenes/mess.py	Sun Jan 27 22:09:34 2013 +0200
+++ b/gamelib/scenes/mess.py	Sun Jan 27 22:19:39 2013 +0200
@@ -28,7 +28,10 @@
         self.add_thing(ToMap())
         self.add_thing(DetergentThing())
         self.add_thing(Boomslang())
-        self.add_item(DetergentBottle('detergent_bottle'))
+        self.add_item_factory(DetergentBottle)
+        self.add_item_factory(EmptyCan)
+        self.add_item_factory(FullCan)
+        self.add_item_factory(DentedCan)
         # Flavour items
         # extra cans on shelf
         self.add_thing(GenericDescThing('mess.cans', 1,
@@ -48,6 +51,8 @@
 class BaseCan(CloneableItem):
     """Base class for the cans"""
 
+    MAX_COUNT = 3
+
     def interact_with_full_can(self, item):
         return Result(_("You bang the cans together. It sounds like two"
                         " cans being banged together."),
@@ -63,9 +68,7 @@
         return Result(_("You'd mangle it beyond usefulness."))
 
     def interact_with_canopener(self, item):
-        empty = EmptyCan('empty_can')
-        self.game.add_item(empty)
-        self.game.replace_inventory_item(self.name, empty.name)
+        self.game.replace_inventory_item(self.name, 'empty_can')
         return Result(_("You open both ends of the can, discarding the"
                         " hideous contents."))
 
@@ -73,6 +76,7 @@
 class EmptyCan(BaseCan):
     "After emptying the full can."
 
+    NAME = 'empty_can'
     INVENTORY_IMAGE = "empty_can.png"
     CURSOR = CursorSprite('empty_can_cursor.png')
 
@@ -87,13 +91,12 @@
 class FullCan(BaseCan):
     "Found on the shelf."
 
+    NAME = 'full_can'
     INVENTORY_IMAGE = "full_can.png"
     CURSOR = CursorSprite('full_can_cursor.png')
 
     def interact_with_titanium_leg(self, item):
-        dented = DentedCan("dented_can")
-        self.game.add_item(dented)
-        self.game.replace_inventory_item(self.name, dented.name)
+        self.game.replace_inventory_item(self.name, 'dented_can')
         return Result(_("You club the can with the femur. The can gets dented,"
                         " but doesn't open."), soundfile="can_hit.ogg")
 
@@ -101,6 +104,7 @@
 class DentedCan(BaseCan):
     "A can banged on with the femur"
 
+    NAME = 'dented_can'
     INVENTORY_IMAGE = "dented_can.png"
     CURSOR = CursorSprite('dented_can_cursor.png')
 
@@ -136,9 +140,7 @@
     def interact_without(self):
         starting_cans = self.get_data('cans_available')
         if starting_cans > 0:
-            can = FullCan("full_can")
-            self.game.add_item(can)
-            self.game.add_inventory_item(can.name)
+            self.game.add_inventory_item('full_can')
             self.set_data('cans_available', starting_cans - 1)
             self.set_interact()
             if starting_cans == 1:
@@ -324,6 +326,7 @@
 
 
 class DetergentBottle(Item):
+    NAME = 'detergent_bottle'
     INVENTORY_IMAGE = 'bottle_empty.png'
     CURSOR = CursorSprite('bottle_empty_cursor.png', 27, 7)
 
--- a/gamelib/tests/test_scene_interactions_cryo.py	Sun Jan 27 22:09:34 2013 +0200
+++ b/gamelib/tests/test_scene_interactions_cryo.py	Sun Jan 27 22:19:39 2013 +0200
@@ -23,10 +23,10 @@
         self.state.add_inventory_item('titanium_leg')
         self.assert_game_data('door', 'shut', 'cryo.door')
 
-        self.interact_thing('cryo.door', 'titanium_leg')
+        self.interact_thing('cryo.door', 'titanium_leg:')
 
         self.assert_game_data('door', 'shut', 'cryo.door')
-        self.assert_inventory_item('titanium_leg', True)
+        self.assert_inventory_item('titanium_leg:', True)
 
     def test_cryo_door_ajar_hand(self):
         "The door is ajar and we touch it with the hand. No change."
@@ -43,10 +43,10 @@
         self.state.add_inventory_item('titanium_leg')
         self.set_game_data('door', 'ajar', 'cryo.door')
 
-        self.interact_thing('cryo.door', 'titanium_leg')
+        self.interact_thing('cryo.door', 'titanium_leg:')
 
         self.assert_game_data('door', 'open', 'cryo.door')
-        self.assert_inventory_item('titanium_leg', True)
+        self.assert_inventory_item('titanium_leg:', True)
 
     def test_cryo_door_open_hand(self):
         "The door is open and we touch it with the hand. We go to the map."
@@ -64,7 +64,7 @@
         self.state.add_inventory_item('titanium_leg')
         self.set_game_data('door', 'open', 'cryo.door')
 
-        self.interact_thing('cryo.door', 'titanium_leg')
+        self.interact_thing('cryo.door', 'titanium_leg:')
 
         self.assert_game_data('door', 'open', 'cryo.door')
         self.assert_current_scene('cryo')
@@ -74,12 +74,12 @@
 
         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_inventory_item('titanium_leg:', False)
         self.assert_detail_thing('cryo.titanium_leg', True)
 
         self.interact_thing('cryo.titanium_leg', detail='cryo_detail')
 
-        self.assert_inventory_item('titanium_leg', True)
+        self.assert_inventory_item('titanium_leg:', True)
         self.assert_detail_thing('cryo.titanium_leg', False)
         self.assert_game_data('contains_titanium_leg', False, 'cryo.unit.1')
 
@@ -128,26 +128,26 @@
         self.assert_game_data('fixed', True, 'cryo.pipe.left')
         self.assert_game_data('fixed', True, 'cryo.pipe.right.top')
         self.assert_game_data('fixed', True, 'cryo.pipe.right.bottom')
-        self.assert_item_exists('cryo_pipe.0', False)
-        self.assert_item_exists('cryo_pipe.1', False)
-        self.assert_item_exists('cryo_pipe.2', False)
+        self.assert_item_exists('cryo_pipe:0', False)
+        self.assert_item_exists('cryo_pipe:1', False)
+        self.assert_item_exists('cryo_pipe:2', False)
 
         self.assertNotEquals(
-            None, self.interact_thing('cryo.pipe.left', 'machete'))
+            None, self.interact_thing('cryo.pipe.left', 'machete:'))
         self.assertNotEquals(
-            None, self.interact_thing('cryo.pipe.right.top', 'machete'))
+            None, self.interact_thing('cryo.pipe.right.top', 'machete:'))
         self.assertNotEquals(
-            None, self.interact_thing('cryo.pipe.right.bottom', 'machete'))
+            None, self.interact_thing('cryo.pipe.right.bottom', 'machete:'))
 
         self.assert_game_data('fixed', False, 'cryo.pipe.left')
         self.assert_game_data('fixed', False, 'cryo.pipe.right.top')
         self.assert_game_data('fixed', False, 'cryo.pipe.right.bottom')
-        self.assert_item_exists('tube_fragment.0')
-        self.assert_item_exists('tube_fragment.1')
-        self.assert_item_exists('tube_fragment.2')
-        self.assert_inventory_item('tube_fragment.0', True)
-        self.assert_inventory_item('tube_fragment.1', True)
-        self.assert_inventory_item('tube_fragment.2', True)
+        self.assert_item_exists('tube_fragment:0')
+        self.assert_item_exists('tube_fragment:1')
+        self.assert_item_exists('tube_fragment:2')
+        self.assert_inventory_item('tube_fragment:0', True)
+        self.assert_inventory_item('tube_fragment:1', True)
+        self.assert_inventory_item('tube_fragment:2', True)
 
     def test_pipes_chopped_machete(self):
         "Touch the chopped cryopipes with the machete. No change."
@@ -158,11 +158,11 @@
         self.set_game_data('fixed', False, 'cryo.pipe.right.bottom')
 
         self.assertEquals(
-            None, self.interact_thing('cryo.pipe.left', 'machete'))
+            None, self.interact_thing('cryo.pipe.left', 'machete:'))
         self.assertEquals(
-            None, self.interact_thing('cryo.pipe.right.top', 'machete'))
+            None, self.interact_thing('cryo.pipe.right.top', 'machete:'))
         self.assertEquals(
-            None, self.interact_thing('cryo.pipe.right.bottom', 'machete'))
+            None, self.interact_thing('cryo.pipe.right.bottom', 'machete:'))
 
         self.assert_game_data('fixed', False, 'cryo.pipe.left')
         self.assert_game_data('fixed', False, 'cryo.pipe.right.top')
--- a/gamelib/tests/test_walkthrough.py	Sun Jan 27 22:09:34 2013 +0200
+++ b/gamelib/tests/test_walkthrough.py	Sun Jan 27 22:19:39 2013 +0200
@@ -31,13 +31,13 @@
         self.assert_detail_thing('cryo.titanium_leg')
         self.interact_thing('cryo.titanium_leg', detail='cryo_detail')
         self.assert_detail_thing('cryo.titanium_leg', False)
-        self.assert_inventory_item('titanium_leg')
+        self.assert_inventory_item('titanium_leg:')
         self.close_detail()
 
         # Open the door the rest of the way.
-        self.interact_thing('cryo.door', 'titanium_leg')
+        self.interact_thing('cryo.door', 'titanium_leg:')
         self.assert_game_data('door', 'open', 'cryo.door')
-        self.assert_inventory_item('titanium_leg')
+        self.assert_inventory_item('titanium_leg:')
 
         # Go to the mess.
         self.move_to('mess')
@@ -48,54 +48,54 @@
         # Get the cans.
         self.assert_game_data('cans_available', 3, 'mess.cans')
         self.interact_thing('mess.cans')
-        self.assert_inventory_item('full_can.0')
+        self.assert_inventory_item('full_can:0')
         self.assert_game_data('cans_available', 2, 'mess.cans')
         self.interact_thing('mess.cans')
-        self.assert_inventory_item('full_can.1')
+        self.assert_inventory_item('full_can:1')
         self.assert_game_data('cans_available', 1, 'mess.cans')
         self.interact_thing('mess.cans')
-        self.assert_inventory_item('full_can.2')
+        self.assert_inventory_item('full_can:2')
         self.assert_scene_thing('mess.cans', False)
 
         # Bash one of the cans.
-        self.assert_item_exists('dented_can.0', False)
-        self.interact_item('full_can.1', 'titanium_leg')
-        self.assert_inventory_item('dented_can.0')
-        self.assert_inventory_item('full_can.1', False)
+        self.assert_item_exists('dented_can:0', False)
+        self.interact_item('full_can:1', 'titanium_leg:')
+        self.assert_inventory_item('dented_can:0')
+        self.assert_inventory_item('full_can:1', False)
 
         # Go to the machine room.
         self.move_to('machine')
 
         # Sharpen leg into machete.
-        self.interact_thing('machine.grinder', 'titanium_leg')
+        self.interact_thing('machine.grinder', 'titanium_leg:')
         self.assert_inventory_item('titanium_leg', False)
-        self.assert_inventory_item('machete')
+        self.assert_inventory_item('machete:')
 
         # Go to the cryo room.
         self.move_to('cryo')
 
         # Chop up some pipes.
         self.assert_game_data('fixed', True, 'cryo.pipe.left')
-        self.interact_thing('cryo.pipe.left', 'machete')
+        self.interact_thing('cryo.pipe.left', 'machete:')
         self.assert_game_data('fixed', False, 'cryo.pipe.left')
-        self.assert_inventory_item('tube_fragment.0')
+        self.assert_inventory_item('tube_fragment:0')
 
         self.assert_game_data('fixed', True, 'cryo.pipe.right.top')
-        self.interact_thing('cryo.pipe.right.top', 'machete')
+        self.interact_thing('cryo.pipe.right.top', 'machete:')
         self.assert_game_data('fixed', False, 'cryo.pipe.right.top')
-        self.assert_inventory_item('tube_fragment.1')
+        self.assert_inventory_item('tube_fragment:1')
 
         self.assert_game_data('fixed', True, 'cryo.pipe.right.bottom')
-        self.interact_thing('cryo.pipe.right.bottom', 'machete')
+        self.interact_thing('cryo.pipe.right.bottom', 'machete:')
         self.assert_game_data('fixed', False, 'cryo.pipe.right.bottom')
-        self.assert_inventory_item('tube_fragment.2')
+        self.assert_inventory_item('tube_fragment:2')
 
         # Go to the mess.
         self.move_to('mess')
 
         # Clear the broccoli.
         self.assert_game_data('status', 'blocked', 'mess.tubes')
-        self.interact_thing('mess.tubes', 'machete')
+        self.interact_thing('mess.tubes', 'machete:')
         self.assert_game_data('status', 'broken', 'mess.tubes')
 
         # Go to the bridge.
@@ -106,13 +106,13 @@
 
         # Get the stethoscope.
         self.interact_thing('bridge.stethoscope')
-        self.assert_inventory_item('stethoscope')
+        self.assert_inventory_item('stethoscope:')
         self.assert_scene_thing('bridge.stethoscope', False)
 
         # Get the superconductor.
         self.interact_thing('bridge.massagechair_base')
         self.interact_thing('bridge.superconductor', detail='chair_detail')
-        self.assert_inventory_item('superconductor')
+        self.assert_inventory_item('superconductor:')
         self.assert_detail_thing('bridge.superconductor', False)
         self.close_detail()
 
@@ -121,29 +121,29 @@
 
         # Get the poster.
         self.interact_thing('crew.poster')
-        self.assert_inventory_item('escher_poster')
+        self.assert_inventory_item('escher_poster:')
         self.assert_scene_thing('crew.poster', False)
 
         # Get the fishbowl.
         self.assert_game_data('has_bowl', True, 'crew.fishbowl')
         self.interact_thing('crew.fishbowl')
         self.assert_game_data('has_bowl', False, 'crew.fishbowl')
-        self.assert_inventory_item('fishbowl')
+        self.assert_inventory_item('fishbowl:')
 
         # Crack the safe.
         self.assert_game_data('is_cracked', False, 'crew.safe')
-        self.interact_thing('crew.safe', 'stethoscope')
+        self.interact_thing('crew.safe', 'stethoscope:')
         self.assert_game_data('is_cracked', True, 'crew.safe')
 
         # Get the duct tape.
         self.assert_game_data('has_tape', True, 'crew.safe')
         self.interact_thing('crew.safe')
         self.assert_game_data('has_tape', False, 'crew.safe')
-        self.assert_inventory_item('duct_tape')
+        self.assert_inventory_item('duct_tape:')
 
         # Make the helmet.
-        self.interact_item('fishbowl', 'duct_tape')
-        self.assert_inventory_item('helmet')
+        self.interact_item('fishbowl:', 'duct_tape:')
+        self.assert_inventory_item('helmet:')
         self.assert_inventory_item('fishbowl', False)
 
         # Go to the engine room.
@@ -154,69 +154,69 @@
 
         # Get the can opener.
         self.interact_thing('engine.canopener')
-        self.assert_inventory_item('canopener')
+        self.assert_inventory_item('canopener:')
         self.assert_scene_thing('engine.canopener', False)
 
         # Open the cans.
-        self.interact_item('full_can.2', 'canopener')
-        self.assert_inventory_item('full_can.2', False)
-        self.assert_inventory_item('empty_can.0')
+        self.interact_item('full_can:2', 'canopener:')
+        self.assert_inventory_item('full_can:2', False)
+        self.assert_inventory_item('empty_can:0')
 
-        self.interact_item('full_can.0', 'canopener')
-        self.assert_inventory_item('full_can.0', False)
-        self.assert_inventory_item('empty_can.1')
+        self.interact_item('full_can:0', 'canopener:')
+        self.assert_inventory_item('full_can:0', False)
+        self.assert_inventory_item('empty_can:1')
 
-        self.interact_item('dented_can.0', 'canopener')
-        self.assert_inventory_item('dented_can.0', False)
-        self.assert_inventory_item('empty_can.2')
+        self.interact_item('dented_can:0', 'canopener:')
+        self.assert_inventory_item('dented_can:0', False)
+        self.assert_inventory_item('empty_can:2')
 
         # Go to the machine room.
         self.move_to('machine')
 
         # Weld pipes and cans.
         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.interact_thing('machine.welder.slot', 'tube_fragment:0')
+        self.assert_inventory_item('tube_fragment:0', False)
         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.interact_thing('machine.welder.slot', 'empty_can:1')
+        self.assert_inventory_item('empty_can:1', False)
         self.assert_game_data(
             'contents', ['tube', 'can'], 'machine.welder.slot')
         self.interact_thing('machine.welder.button')
         self.assert_game_data('contents', [], 'machine.welder.slot')
-        self.assert_inventory_item('cryo_pipes_one')
+        self.assert_inventory_item('cryo_pipes_one:')
 
         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.interact_thing('machine.welder.slot', 'tube_fragment:2')
+        self.assert_inventory_item('tube_fragment:2', False)
         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.interact_thing('machine.welder.slot', 'empty_can:2')
+        self.assert_inventory_item('empty_can:2', False)
         self.assert_game_data(
             'contents', ['tube', 'can'], 'machine.welder.slot')
         self.interact_thing('machine.welder.button')
         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_inventory_item('cryo_pipes_two:')
 
         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.interact_thing('machine.welder.slot', 'tube_fragment:1')
+        self.assert_inventory_item('tube_fragment:1', False)
         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.interact_thing('machine.welder.slot', 'empty_can:0')
+        self.assert_inventory_item('empty_can:0', False)
         self.assert_game_data(
             'contents', ['tube', 'can'], 'machine.welder.slot')
         self.interact_thing('machine.welder.button')
         self.assert_game_data('contents', [], 'machine.welder.slot')
         self.assert_inventory_item('cryo_pipes_two', False)
-        self.assert_inventory_item('cryo_pipes_three')
+        self.assert_inventory_item('cryo_pipes_three:')
 
         # Go to the mess.
         self.move_to('mess')
 
         # Replace the tubes.
-        self.interact_thing('mess.tubes', 'cryo_pipes_three')
+        self.interact_thing('mess.tubes', 'cryo_pipes_three:')
         self.assert_inventory_item('cryo_pipes_three', False)
         self.assert_game_data('status', 'replaced', 'mess.tubes')
 
@@ -224,7 +224,7 @@
         self.assert_game_data('life support status', 'replaced')
 
         # Tape up the tubes.
-        self.interact_thing('mess.tubes', 'duct_tape')
+        self.interact_thing('mess.tubes', 'duct_tape:')
         self.assert_game_data('status', 'fixed', 'mess.tubes')
 
         # Check that life support is fixed
@@ -232,45 +232,45 @@
 
         # Get the detergent bottle.
         self.interact_thing('mess.detergent')
-        self.assert_inventory_item('detergent_bottle')
+        self.assert_inventory_item('detergent_bottle:')
 
         # Go to the cryo room.
         self.move_to('cryo')
 
         # Fill the detergent bottle.
-        self.interact_thing('cryo.pool', 'detergent_bottle')
+        self.interact_thing('cryo.pool', 'detergent_bottle:')
         self.assert_inventory_item('detergent_bottle', False)
-        self.assert_inventory_item('full_detergent_bottle')
+        self.assert_inventory_item('full_detergent_bottle:')
 
         # Go to the engine room.
         self.move_to('engine')
 
         # Patch the cracked pipe.
         self.assert_game_data('fixed', False, 'engine.cracked_pipe')
-        self.interact_thing('engine.cracked_pipe', 'duct_tape')
+        self.interact_thing('engine.cracked_pipe', 'duct_tape:')
         self.assert_game_data('fixed', True, 'engine.cracked_pipe')
 
         # Fill the cryofluid receptacles.
         self.assert_game_data('filled', False, 'engine.cryo_containers')
         self.interact_thing(
-            'engine.cryo_container_receptacle', 'full_detergent_bottle')
+            'engine.cryo_container_receptacle', 'full_detergent_bottle:')
         self.assert_game_data('filled', True, 'engine.cryo_containers')
         self.assert_inventory_item('full_detergent_bottle', False)
 
         # Remove the burned-out superconductor.
         self.assert_game_data('present', True, 'engine.superconductor')
         self.assert_game_data('working', False, 'engine.superconductor')
-        self.interact_thing('engine.superconductor', 'machete')
+        self.interact_thing('engine.superconductor', 'machete:')
         self.assert_game_data('present', False, 'engine.superconductor')
         self.assert_game_data('working', False, 'engine.superconductor')
 
         # Tape up new superconductor.
-        self.interact_item('superconductor', 'duct_tape')
+        self.interact_item('superconductor:', 'duct_tape:')
         self.assert_inventory_item('superconductor', False)
-        self.assert_inventory_item('taped_superconductor')
+        self.assert_inventory_item('taped_superconductor:')
 
         # Install superconductor.
-        self.interact_thing('engine.superconductor', 'taped_superconductor')
+        self.interact_thing('engine.superconductor', 'taped_superconductor:')
         self.assert_inventory_item('taped_superconductor', False)
         self.assert_game_data('present', True, 'engine.superconductor')
         self.assert_game_data('working', True, 'engine.superconductor')
@@ -282,16 +282,16 @@
         self.move_to('bridge')
 
         # Show JIM the poster.
-        self.interact_thing('bridge.camera', 'escher_poster')
+        self.interact_thing('bridge.camera', 'escher_poster:')
         self.assert_game_data('ai status', 'looping')
 
         # Get at JIM.
         self.assert_game_data('ai panel', 'closed')
-        self.interact_thing('jim_panel', 'machete')
+        self.interact_thing('jim_panel', 'machete:')
         self.assert_game_data('ai panel', 'open')
 
         # Break JIM.
-        self.interact_thing('jim_panel', 'machete')
+        self.interact_thing('jim_panel', 'machete:')
         self.assert_game_data('ai panel', 'broken')
 
         # Check that we've turned off JIM.
--- a/pyntnclick/gamescreen.py	Sun Jan 27 22:09:34 2013 +0200
+++ b/pyntnclick/gamescreen.py	Sun Jan 27 22:19:39 2013 +0200
@@ -124,7 +124,7 @@
     @property
     def slot_items(self):
         item_names = self.game.inventory()[self.inv_offset:][:len(self.slots)]
-        return [self.game.items[name] for name in item_names]
+        return [self.game.get_item(name) for name in item_names]
 
     def mouse_down(self, event, widget):
         if event.button != 1:
--- a/pyntnclick/state.py	Sun Jan 27 22:09:34 2013 +0200
+++ b/pyntnclick/state.py	Sun Jan 27 22:19:39 2013 +0200
@@ -49,7 +49,11 @@
 
     def __init__(self, state_dict=None):
         if state_dict is None:
-            state_dict = {'inventories': {'main': []}, 'current_scene': None}
+            state_dict = {
+                'inventories': {'main': []},
+                'item_factories': {},
+                'current_scene': None,
+                }
         self._game_state = copy.deepcopy(state_dict)
 
     def __getitem__(self, key):
@@ -61,10 +65,6 @@
     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]
-
     def get_data(self, state_key, data_key):
         """Get a single entry"""
         return self[state_key].get(data_key, None)
@@ -73,14 +73,18 @@
         """Set a single value"""
         self[state_key][data_key] = value
 
+    def _initialize_state(self, state_dict, state_key, initial_data):
+        if state_key not in self._game_state:
+            state_dict[state_key] = copy.deepcopy(initial_data)
+
     def initialize_state(self, state_key, initial_data):
         """Initialize a gizmo entry"""
-        if state_key not in self._game_state:
-            self._game_state[state_key] = {}
-            if 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))
+        self._initialize_state(self._game_state, state_key, initial_data)
+
+    def initialize_item_factory_state(self, state_key, initial_data):
+        """Initialize an item factory entry"""
+        self._initialize_state(
+            self._game_state['item_factories'], state_key, initial_data)
 
     def inventory(self, name='main'):
         return self['inventories'][name]
@@ -125,8 +129,8 @@
         self.scenes = {}
         # map of detail view name -> DetailView object
         self.detail_views = {}
-        # map of item name -> Item object
-        self.items = {}
+        # map of item prefix -> ItemFactory object
+        self.item_factories = {}
         # list of item objects in inventory
         self.current_inventory = 'main'
         # currently selected tool (item)
@@ -142,6 +146,16 @@
             return self.scenes[scene_name]
         return None
 
+    def get_item(self, item_name):
+        base_name, _, _suffix = item_name.partition(':')
+        factory = self.item_factories[base_name]
+        return factory.get_item(item_name)
+
+    def create_item(self, base_name):
+        assert ":" not in base_name
+        factory = self.item_factories[base_name]
+        return factory.create_item()
+
     def inventory(self, name=None):
         if name is None:
             name = self.current_inventory
@@ -161,9 +175,13 @@
         detail_view.set_game(self)
         self.detail_views[detail_view.name] = detail_view
 
-    def add_item(self, item):
-        item.set_game(self)
-        self.items[item.name] = item
+    def add_item_factory(self, item_class):
+        name = item_class.NAME
+        assert name not in self.item_factories, (
+           "Factory for %s already added." % (name,))
+        factory = item_class.ITEM_FACTORY(item_class)
+        factory.set_game(self)
+        self.item_factories[name] = factory
 
     def load_scenes(self, modname):
         mod = __import__('%s.%s' % (self.gd.SCENE_MODULE, modname),
@@ -187,8 +205,9 @@
     def _update_inventory(self):
         ScreenEvent.post('game', 'inventory', None)
 
-    def add_inventory_item(self, name):
-        self.inventory().append(name)
+    def add_inventory_item(self, item_name):
+        item = self.create_item(item_name)
+        self.inventory().append(item.name)
         self._update_inventory()
 
     def is_in_inventory(self, name):
@@ -197,7 +216,7 @@
     def remove_inventory_item(self, name):
         self.inventory().remove(name)
         # Unselect tool if it's removed
-        if self.tool == self.items[name]:
+        if self.tool == self.get_item(name):
             self.set_tool(None)
         self._update_inventory()
 
@@ -205,9 +224,10 @@
         """Try to replace an item in the inventory with a new one"""
         try:
             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])
+            new_item = self.create_item(new_item_name)
+            self.inventory()[index] = new_item.name
+            if self.tool == self.get_item(old_item_name):
+                self.set_tool(new_item)
         except ValueError:
             return False
         self._update_inventory()
@@ -302,8 +322,8 @@
         self.current_thing = None
         self._background = None
 
-    def add_item(self, item):
-        self.game.add_item(item)
+    def add_item_factory(self, item_factory):
+        self.game.add_item_factory(item_factory)
 
     def add_thing(self, thing):
         thing.set_game(self.game)
@@ -546,13 +566,48 @@
                             rect.inflate(1, 1), 1)
 
 
+class ItemFactory(StatefulGizmo):
+    INITIAL_DATA = {
+        'created': [],
+        }
+
+    def __init__(self, item_class):
+        super(ItemFactory, self).__init__()
+        self.item_class = item_class
+        assert self.item_class.NAME is not None, (
+            "%s has no NAME set" % (self.item_class,))
+        self.state_key = self.item_class.NAME + '_factory'
+        self.items = {}
+
+    def get_item(self, item_name):
+        assert item_name in self.get_data('created'), (
+            "Object %s has not been created" % (item_name,))
+        if item_name not in self.items:
+            item = self.item_class(item_name)
+            item.set_game(self.game)
+            self.items[item_name] = item
+        return self.items[item_name]
+
+    def get_item_suffix(self):
+        return ''
+
+    def create_item(self):
+        item_name = '%s:%s' % (self.item_class.NAME, self.get_item_suffix())
+        created_list = self.get_data('created')
+        assert item_name not in created_list, (
+            "Already created object %s" % (item_name,))
+        created_list.append(item_name)
+        self.set_data('created', created_list)
+        return self.get_item(item_name)
+
+
 class Item(GameDeveloperGizmo, InteractiveMixin):
     """Base class for inventory items."""
 
     # image for inventory
     INVENTORY_IMAGE = None
 
-    # name of item
+    # Base name of item
     NAME = None
 
     # name for interactions (i.e. def interact_with_<TOOL_NAME>)
@@ -561,12 +616,14 @@
     # set to instance of CursorSprite
     CURSOR = None
 
+    ITEM_FACTORY = ItemFactory
+
     def __init__(self, name=None):
         GameDeveloperGizmo.__init__(self)
         self.name = self.NAME
         if name is not None:
             self.name = name
-        self.tool_name = name
+        self.tool_name = self.NAME
         if self.TOOL_NAME is not None:
             self.tool_name = self.TOOL_NAME
         self.inventory_image = None
@@ -589,15 +646,15 @@
         return False
 
 
-class CloneableItem(Item):
-    _counter = 0
+class ClonableItemFactory(ItemFactory):
+    def get_item_suffix(self):
+        # Works as long as we never remove anything from our 'created' list.
+        count = len(self.get_data('created'))
+        assert self.item_class.MAX_COUNT is not None
+        assert count <= self.item_class.MAX_COUNT
+        return str(count)
 
-    @classmethod
-    def _get_new_id(cls):
-        cls._counter += 1
-        return cls._counter - 1
 
-    def __init__(self, name=None):
-        super(CloneableItem, self).__init__(name)
-        my_count = self._get_new_id()
-        self.name = "%s.%s" % (self.name, my_count)
+class CloneableItem(Item):
+    ITEM_FACTORY = ClonableItemFactory
+    MAX_COUNT = None
--- a/pyntnclick/tests/game_logic_utils.py	Sun Jan 27 22:09:34 2013 +0200
+++ b/pyntnclick/tests/game_logic_utils.py	Sun Jan 27 22:19:39 2013 +0200
@@ -37,11 +37,6 @@
         self.scene_stack.pop()
         self.assertTrue(len(self.scene_stack) > 0)
 
-    def tearDown(self):
-        for item in self.state.items.values():
-            if isinstance(item, pyntnclick.state.CloneableItem):
-                type(item)._counter = 0
-
     def clear_event_queue(self):
         # Since we aren't handling events, we may overflow the pygame
         # event buffer if we're generating a lot of events
@@ -80,7 +75,11 @@
         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)
+        try:
+            self.state.get_item(item)
+            self.assertTrue(exists)
+        except:
+            self.assertFalse(exists)
 
     def assert_current_scene(self, scene):
         self.assertEquals(scene, self.state.get_current_scene().name)
@@ -99,7 +98,7 @@
         item_obj = None
         if item is not None:
             self.assert_inventory_item(item)
-            item_obj = self.state.items[item]
+            item_obj = self.state.get_item(item)
         thing_container = self.scene_stack[-1]
         if detail is not None:
             self.assertEqual(detail, thing_container.name)
@@ -108,7 +107,7 @@
 
     def interact_item(self, target_item, item):
         self.assert_inventory_item(target_item)
-        item_obj = self.state.items[item]
-        target_obj = self.state.items[target_item]
+        item_obj = self.state.get_item(item)
+        target_obj = self.state.get_item(target_item)
         result = target_obj.interact(item_obj)
         return self.handle_result(result)