Mercurial > boomslang
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 |