comparison pyntnclick/state.py @ 854:79b5c1be9a5e default tip

Remove pyntnclick, it's its own library, now
author Stefano Rivera <stefano@rivera.za.net>
date Sat, 21 Jun 2014 22:06:09 +0200
parents f95830b58336
children
comparison
equal deleted inserted replaced
852:f95830b58336 854:79b5c1be9a5e
1 """Utilities and base classes for dealing with scenes."""
2
3 import os
4 import json
5 import copy
6
7 from widgets.text import LabelWidget
8 from pygame.color import Color
9
10 from pyntnclick.engine import ScreenEvent
11 from pyntnclick.utils import draw_rect_image
12
13
14 class Result(object):
15 """Result of interacting with a thing"""
16
17 def __init__(self, message=None, soundfile=None, detail_view=None,
18 widget=None, end_game=False):
19 self.message = message
20 self.soundfile = soundfile
21 self.detail_view = detail_view
22 self.widget = widget
23 self.end_game = end_game
24
25 def play_sound(self, screen):
26 if self.soundfile:
27 sound = screen.gd.sound.get_sound(self.soundfile)
28 sound.play()
29
30 def process(self, screen):
31 """Helper function to do the right thing with a result object"""
32 self.play_sound(screen)
33 if self.widget:
34 screen.queue_widget(self.widget)
35 if self.message:
36 screen.show_message(self.message)
37 if self.detail_view:
38 screen.game.show_detail(self.detail_view)
39 if self.end_game:
40 screen.end_game()
41
42
43 class GameState(object):
44 """This holds the serializable game state.
45
46 Games wanting to do fancier stuff with the state should
47 sub-class this and feed the subclass into
48 GameDescription via the custom_data parameter."""
49
50 def __init__(self, state_dict=None):
51 if state_dict is None:
52 state_dict = {
53 'inventories': {'main': []},
54 'item_factories': {},
55 'current_scene': None,
56 }
57 self._game_state = copy.deepcopy(state_dict)
58
59 def __getitem__(self, key):
60 return self._game_state[key]
61
62 def __contains__(self, key):
63 return key in self._game_state
64
65 def export_data(self):
66 return copy.deepcopy(self._game_state)
67
68 def get_data(self, state_key, data_key):
69 """Get a single entry"""
70 return self[state_key].get(data_key, None)
71
72 def set_data(self, state_key, data_key, value):
73 """Set a single value"""
74 self[state_key][data_key] = value
75
76 def _initialize_state(self, state_dict, state_key, initial_data):
77 if state_key not in self._game_state:
78 state_dict[state_key] = copy.deepcopy(initial_data)
79
80 def initialize_state(self, state_key, initial_data):
81 """Initialize a gizmo entry"""
82 self._initialize_state(self._game_state, state_key, initial_data)
83
84 def initialize_item_factory_state(self, state_key, initial_data):
85 """Initialize an item factory entry"""
86 self._initialize_state(
87 self._game_state['item_factories'], state_key, initial_data)
88
89 def inventory(self, name='main'):
90 return self['inventories'][name]
91
92 def set_current_scene(self, scene_name):
93 self._game_state['current_scene'] = scene_name
94
95 @classmethod
96 def get_save_fn(cls, save_dir, save_name):
97 return os.path.join(save_dir, '%s.json' % (save_name,))
98
99 @classmethod
100 def load_game(cls, save_dir, save_name):
101 fn = cls.get_save_fn(save_dir, save_name)
102 if os.access(fn, os.R_OK):
103 f = open(fn, 'r')
104 state = json.load(f)
105 f.close()
106 return state
107
108 def save_game(self, save_dir, save_name):
109 fn = self.get_save_fn(save_dir, save_name)
110 if not os.path.isdir(save_dir):
111 os.makedirs(save_dir)
112 f = open(fn, 'w')
113 json.dump(self.export_data(), f)
114 f.close()
115
116
117 class Game(object):
118 """Complete game state.
119
120 Game state consists of:
121
122 * items
123 * scenes
124 """
125 def __init__(self, gd, game_state):
126 # game description
127 self.gd = gd
128 # map of scene name -> Scene object
129 self.scenes = {}
130 # map of detail view name -> DetailView object
131 self.detail_views = {}
132 # map of item prefix -> ItemFactory object
133 self.item_factories = {}
134 # list of item objects in inventory
135 self.current_inventory = 'main'
136 # currently selected tool (item)
137 self.tool = None
138 # Global game data
139 self.data = game_state
140 # debug rects
141 self.debug_rects = False
142
143 def get_current_scene(self):
144 scene_name = self.data['current_scene']
145 if scene_name is not None:
146 return self.scenes[scene_name]
147 return None
148
149 def get_item(self, item_name):
150 base_name, _, _suffix = item_name.partition(':')
151 factory = self.item_factories[base_name]
152 return factory.get_item(item_name)
153
154 def create_item(self, base_name):
155 assert ":" not in base_name
156 factory = self.item_factories[base_name]
157 return factory.create_item()
158
159 def inventory(self, name=None):
160 if name is None:
161 name = self.current_inventory
162 return self.data.inventory(name)
163
164 def set_custom_data(self, data_object):
165 self.data = data_object
166
167 def set_debug_rects(self, value=True):
168 self.debug_rects = value
169
170 def add_scene(self, scene):
171 scene.set_game(self)
172 self.scenes[scene.name] = scene
173
174 def add_detail_view(self, detail_view):
175 detail_view.set_game(self)
176 self.detail_views[detail_view.name] = detail_view
177
178 def add_item_factory(self, item_class):
179 name = item_class.NAME
180 assert name not in self.item_factories, (
181 "Factory for %s already added." % (name,))
182 factory = item_class.ITEM_FACTORY(item_class)
183 factory.set_game(self)
184 self.item_factories[name] = factory
185
186 def load_scenes(self, modname):
187 mod = __import__('%s.%s' % (self.gd.SCENE_MODULE, modname),
188 fromlist=[modname])
189 for scene_cls in mod.SCENES:
190 scene = scene_cls(self)
191 self.add_scene(scene)
192 if hasattr(mod, 'DETAIL_VIEWS'):
193 for scene_cls in mod.DETAIL_VIEWS:
194 scene = scene_cls(self)
195 self.add_detail_view(scene)
196
197 def change_scene(self, name):
198 ScreenEvent.post('game', 'change_scene',
199 {'name': name, 'detail': False})
200
201 def show_detail(self, name):
202 ScreenEvent.post('game', 'change_scene',
203 {'name': name, 'detail': True})
204
205 def _update_inventory(self):
206 ScreenEvent.post('game', 'inventory', None)
207
208 def add_inventory_item(self, item_name):
209 item = self.create_item(item_name)
210 self.inventory().append(item.name)
211 self._update_inventory()
212
213 def is_in_inventory(self, name):
214 return name in self.inventory()
215
216 def remove_inventory_item(self, name):
217 self.inventory().remove(name)
218 # Unselect tool if it's removed
219 if self.tool == self.get_item(name):
220 self.set_tool(None)
221 self._update_inventory()
222
223 def replace_inventory_item(self, old_item_name, new_item_name):
224 """Try to replace an item in the inventory with a new one"""
225 try:
226 index = self.inventory().index(old_item_name)
227 new_item = self.create_item(new_item_name)
228 self.inventory()[index] = new_item.name
229 if self.tool == self.get_item(old_item_name):
230 self.set_tool(new_item)
231 except ValueError:
232 return False
233 self._update_inventory()
234 return True
235
236 def set_tool(self, item):
237 self.tool = item
238
239
240 class GameDeveloperGizmo(object):
241 """Base class for objects game developers see."""
242
243 def __init__(self):
244 """Set """
245 self.game = None
246 self.gd = None
247 self.resource = None
248 self.sound = None
249
250 def set_game(self, game):
251 self.game = game
252 self.gd = game.gd
253 self.resource = self.gd.resource
254 self.sound = self.gd.sound
255 self.set_state(self.game.data)
256 self.setup()
257
258 def set_state(self, state):
259 """Hack to allow set_state() to be called before setup()."""
260 pass
261
262 def setup(self):
263 """Game developers should override this to do their setup.
264
265 It will be called after all the useful state functions have been
266 set.
267 """
268 pass
269
270
271 class StatefulGizmo(GameDeveloperGizmo):
272
273 # initial data (optional, defaults to none)
274 INITIAL_DATA = None
275 STATE_KEY = None
276
277 def __init__(self):
278 GameDeveloperGizmo.__init__(self)
279 self.state_key = self.STATE_KEY
280 self.state = None # set this with set_state if required
281
282 def set_state(self, state):
283 """Set the state object and initialize if needed"""
284 self.state = state
285 if self.state_key is None:
286 assert self.INITIAL_DATA is None, (
287 "Can't provide self.INITIAL_DATA without self.state_key.")
288 if self.INITIAL_DATA is not None:
289 self.state.initialize_state(self.state_key, self.INITIAL_DATA)
290
291 def set_data(self, key, value):
292 if self.state:
293 self.state.set_data(self.state_key, key, value)
294
295 def get_data(self, key):
296 if self.state:
297 return self.state.get_data(self.state_key, key)
298
299
300 class Scene(StatefulGizmo):
301 """Base class for scenes."""
302
303 # sub-folder to look for resources in
304 FOLDER = None
305
306 # name of background image resource
307 BACKGROUND = None
308
309 # name of scene (optional, defaults to folder)
310 NAME = None
311
312 # Offset of the background image
313 OFFSET = (0, 0)
314
315 def __init__(self, state):
316 StatefulGizmo.__init__(self)
317 # scene name
318 self.name = self.NAME if self.NAME is not None else self.FOLDER
319 self.state_key = self.name
320 # map of thing names -> Thing objects
321 self.things = {}
322 self.current_thing = None
323 self._background = None
324
325 def add_item_factory(self, item_factory):
326 self.game.add_item_factory(item_factory)
327
328 def add_thing(self, thing):
329 thing.set_game(self.game)
330 if not thing.should_add():
331 return
332 self.things[thing.name] = thing
333 thing.set_scene(self)
334
335 def remove_thing(self, thing):
336 del self.things[thing.name]
337 if thing is self.current_thing:
338 self.current_thing.leave()
339 self.current_thing = None
340
341 def _get_description(self, dest_rect):
342 text = (self.current_thing and
343 self.current_thing.get_description())
344 if text is None:
345 return None
346 label = LabelWidget((0, 10), self.gd, text)
347 label.do_prepare()
348 # TODO: Centre more cleanly
349 label.rect.left += (dest_rect.width - label.rect.width) / 2
350 return label
351
352 def draw_description(self, surface):
353 description = self._get_description(surface.get_rect())
354 if description is not None:
355 description.draw(surface)
356
357 def _cache_background(self):
358 if self.BACKGROUND and not self._background:
359 self._background = self.resource.get_image(
360 self.FOLDER, self.BACKGROUND)
361
362 def draw_background(self, surface):
363 self._cache_background()
364 if self._background is not None:
365 surface.blit(self._background, self.OFFSET, None)
366 else:
367 surface.fill((200, 200, 200))
368
369 def draw_things(self, surface):
370 for thing in self.things.itervalues():
371 thing.draw(surface)
372
373 def draw(self, surface):
374 self.draw_background(surface)
375 self.draw_things(surface)
376
377 def interact(self, item, pos):
378 """Interact with a particular position.
379
380 Item may be an item in the list of items or None for the hand.
381
382 Returns a Result object to provide feedback to the player.
383 """
384 if self.current_thing is not None:
385 return self.current_thing.interact(item)
386
387 def animate(self):
388 """Animate all the things in the scene.
389
390 Return true if any of them need to queue a redraw"""
391 result = False
392 for thing in self.things.itervalues():
393 if thing.animate():
394 result = True
395 return result
396
397 def enter(self):
398 return None
399
400 def leave(self):
401 return None
402
403 def update_current_thing(self, pos):
404 if self.current_thing is not None:
405 if not self.current_thing.contains(pos):
406 self.current_thing.leave()
407 self.current_thing = None
408 for thing in self.things.itervalues():
409 if thing.contains(pos):
410 thing.enter(self.game.tool)
411 self.current_thing = thing
412 break
413
414 def mouse_move(self, pos):
415 """Call to check whether the cursor has entered / exited a thing.
416
417 Item may be an item in the list of items or None for the hand.
418 """
419 self.update_current_thing(pos)
420
421 def get_detail_size(self):
422 self._cache_background()
423 return self._background.get_size()
424
425 def get_image(self, *image_name_fragments, **kw):
426 return self.resource.get_image(*image_name_fragments, **kw)
427
428 def set_state(self, state):
429 return super(Scene, self).set_state(state)
430
431
432 class InteractiveMixin(object):
433 def is_interactive(self, tool=None):
434 return True
435
436 def interact(self, tool):
437 if not self.is_interactive(tool):
438 return None
439 if tool is None:
440 return self.interact_without()
441 handler = getattr(self, 'interact_with_' + tool.tool_name, None)
442 inverse_handler = self.get_inverse_interact(tool)
443 if handler is not None:
444 return handler(tool)
445 elif inverse_handler is not None:
446 return inverse_handler(self)
447 else:
448 return self.interact_default(tool)
449
450 def get_inverse_interact(self, tool):
451 return None
452
453 def interact_without(self):
454 return self.interact_default(None)
455
456 def interact_default(self, item=None):
457 return None
458
459
460 class Thing(StatefulGizmo, InteractiveMixin):
461 """Base class for things in a scene that you can interact with."""
462
463 # name of thing
464 NAME = None
465
466 # sub-folder to look for resources in (defaults to scenes folder)
467 FOLDER = None
468
469 # list of Interact objects
470 INTERACTS = {}
471
472 # name first interact
473 INITIAL = None
474
475 # Interact rectangle hi-light color (for debugging)
476 # (set to None to turn off)
477 _interact_hilight_color = Color('red')
478
479 def __init__(self):
480 StatefulGizmo.__init__(self)
481 # name of the thing
482 self.name = self.NAME
483 # folder for resource (None is overridden by scene folder)
484 self.folder = self.FOLDER
485 self.state_key = self.NAME
486 # interacts
487 self.interacts = self.INTERACTS
488 # these are set by set_scene
489 self.scene = None
490 self.current_interact = None
491 self.rect = None
492 self.orig_rect = None
493
494 def _fix_rect(self):
495 """Fix rects to compensate for scene offset"""
496 # Offset logic is to always work with copies, to avoid
497 # flying effects from multiple calls to _fix_rect
498 # See footwork in draw
499 if hasattr(self.rect, 'collidepoint'):
500 self.rect = self.rect.move(self.scene.OFFSET)
501 else:
502 self.rect = [x.move(self.scene.OFFSET) for x in self.rect]
503
504 def should_add(self):
505 return True
506
507 def set_scene(self, scene):
508 assert self.scene is None
509 self.scene = scene
510 if self.folder is None:
511 self.folder = scene.FOLDER
512 self.game = scene.game
513 for interact in self.interacts.itervalues():
514 interact.set_thing(self)
515 self.set_interact()
516
517 def set_interact(self):
518 return self._set_interact(self.select_interact())
519
520 def _set_interact(self, name):
521 self.current_interact = self.interacts[name]
522 self.rect = self.current_interact.interact_rect
523 if self.scene:
524 self._fix_rect()
525 assert self.rect is not None, name
526
527 def select_interact(self):
528 return self.INITIAL
529
530 def contains(self, pos):
531 if hasattr(self.rect, 'collidepoint'):
532 return self.rect.collidepoint(pos)
533 else:
534 for rect in list(self.rect):
535 if rect.collidepoint(pos):
536 return True
537 return False
538
539 def get_description(self):
540 return None
541
542 def enter(self, item):
543 """Called when the cursor enters the Thing."""
544 pass
545
546 def leave(self):
547 """Called when the cursor leaves the Thing."""
548 pass
549
550 def animate(self):
551 return self.current_interact.animate()
552
553 def draw(self, surface):
554 old_rect = self.current_interact.rect
555 if old_rect:
556 self.current_interact.rect = old_rect.move(self.scene.OFFSET)
557 self.current_interact.draw(surface)
558 self.current_interact.rect = old_rect
559 if self.game.debug_rects and self._interact_hilight_color:
560 if hasattr(self.rect, 'collidepoint'):
561 draw_rect_image(surface, self._interact_hilight_color,
562 self.rect.inflate(1, 1), 1)
563 else:
564 for rect in self.rect:
565 draw_rect_image(surface, self._interact_hilight_color,
566 rect.inflate(1, 1), 1)
567
568
569 class ItemFactory(StatefulGizmo):
570 INITIAL_DATA = {
571 'created': [],
572 }
573
574 def __init__(self, item_class):
575 super(ItemFactory, self).__init__()
576 self.item_class = item_class
577 assert self.item_class.NAME is not None, (
578 "%s has no NAME set" % (self.item_class,))
579 self.state_key = self.item_class.NAME + '_factory'
580 self.items = {}
581
582 def get_item(self, item_name):
583 assert item_name in self.get_data('created'), (
584 "Object %s has not been created" % (item_name,))
585 if item_name not in self.items:
586 item = self.item_class(item_name)
587 item.set_game(self.game)
588 self.items[item_name] = item
589 return self.items[item_name]
590
591 def get_item_suffix(self):
592 return ''
593
594 def create_item(self):
595 item_name = '%s:%s' % (self.item_class.NAME, self.get_item_suffix())
596 created_list = self.get_data('created')
597 assert item_name not in created_list, (
598 "Already created object %s" % (item_name,))
599 created_list.append(item_name)
600 self.set_data('created', created_list)
601 return self.get_item(item_name)
602
603
604 class Item(GameDeveloperGizmo, InteractiveMixin):
605 """Base class for inventory items."""
606
607 # image for inventory
608 INVENTORY_IMAGE = None
609
610 # Base name of item
611 NAME = None
612
613 # name for interactions (i.e. def interact_with_<TOOL_NAME>)
614 TOOL_NAME = None
615
616 # set to instance of CursorSprite
617 CURSOR = None
618
619 ITEM_FACTORY = ItemFactory
620
621 def __init__(self, name=None):
622 GameDeveloperGizmo.__init__(self)
623 self.name = self.NAME
624 if name is not None:
625 self.name = name
626 self.tool_name = self.NAME
627 if self.TOOL_NAME is not None:
628 self.tool_name = self.TOOL_NAME
629 self.inventory_image = None
630
631 def _cache_inventory_image(self):
632 if not self.inventory_image:
633 self.inventory_image = self.resource.get_image(
634 'items', self.INVENTORY_IMAGE)
635
636 def get_inventory_image(self):
637 self._cache_inventory_image()
638 return self.inventory_image
639
640 def get_inverse_interact(self, tool):
641 return getattr(tool, 'interact_with_' + self.tool_name, None)
642
643 def is_interactive(self, tool=None):
644 if tool:
645 return True
646 return False
647
648
649 class ClonableItemFactory(ItemFactory):
650 def get_item_suffix(self):
651 # Works as long as we never remove anything from our 'created' list.
652 count = len(self.get_data('created'))
653 assert self.item_class.MAX_COUNT is not None
654 assert count <= self.item_class.MAX_COUNT
655 return str(count)
656
657
658 class CloneableItem(Item):
659 ITEM_FACTORY = ClonableItemFactory
660 MAX_COUNT = None