changeset 551:9d3ed4d05f55

Hack in test level mode to editor
author Neil Muller <drnlmuller@gmail.com>
date Wed, 21 Sep 2011 14:16:58 +0200
parents 91e1a95343b2
children b2e0f1267282
files mamba/habitats/editor.py mamba/widgets/level.py
diffstat 2 files changed, 125 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/mamba/habitats/editor.py	Tue Sep 20 17:29:38 2011 +0200
+++ b/mamba/habitats/editor.py	Wed Sep 21 14:16:58 2011 +0200
@@ -1,14 +1,16 @@
 """Habitat for editing levels."""
 
 import pygame.display
-from pygame.locals import SWSURFACE, KEYDOWN, K_1, K_2, MOUSEBUTTONDOWN
+from pygame.locals import (SWSURFACE, KEYDOWN, K_1, K_2, MOUSEBUTTONDOWN,
+        K_LEFT, K_RIGHT, K_DOWN, K_UP, K_SPACE)
 import sys
 import traceback
 from StringIO import StringIO
 import urllib
 import urllib2
 
-from mamba.engine import Habitat, NewHabitatEvent
+from mamba.engine import (Habitat, NewHabitatEvent, SnakeDiedEvent,
+        LevelCompletedEvent)
 from mamba.sprites import find_special_sprites
 from mamba.widgets.level import EditLevelWidget
 from mamba.widgets.text import TextWidget, TextButton
@@ -23,7 +25,7 @@
 from mamba.data import (check_level_exists, get_level_list, load_file,
                         load_image, load_tile_image)
 from mamba.constants import (SCREEN, EDIT_SCREEN, NAME, ESCAPE_KEYS,
-        RESERVED_NAMES, WINDOW_ICON, LEVEL_SERVER)
+        RESERVED_NAMES, WINDOW_ICON, LEVEL_SERVER, UP, DOWN, LEFT, RIGHT)
 
 MAX_TOOLS = 6
 MODE_HEIGHT = 370
@@ -37,19 +39,20 @@
 A new level can be created from one of the existing templates using "New". Level properties, such as the
 name, filename and background music track can be changed using the "Edit Level Metadata" button.
 
-The level editor has 3 modes: placing tiles, placing things and editing sprites.
+The level editor has 4 modes: placing tiles, placing things, editing sprites and testing the level.
 
 Tiles are level geography, such as floors, walls and so forth.  Things are items that the snake can eat, such as rats.
 Sprites are special elements with more complex items which require extra configuration information, such as tunnels.
 
-To place tiles and things, select the desired element and click on the map to place the tile or thing.
-Deleting anything replaces the space with a floor tile (Right click also deletes). The selection of
-tiles can be scrolled using the arrow buttons.
+To place tiles and things, select the desired element and click on the map to place the tile or thing.  Deleting anything
+replaces the space with a floor tile (Right click also deletes). The selection of tiles can be scrolled using the arrow buttons.
 
-Sprites can be placed as well. Sprites require parameters. Each sprite needs a unique id tag, which can
-be any string without spaces, and then additional parameters that depend on the type of sprite, usually
-the id of another sprite. Sprites can be added, edited and deleted.  Look at existing maps to see how
-the parameters can be used.
+Sprites can be placed as well. Sprites require parameters. Each sprite needs a unique id tag, which can be any string
+without spaces, and then additional parameters that depend on the type of sprite, usually the id of another sprite.
+Sprites can be added, edited and deleted.  Look at existing maps to see how the parameters can be used.
+
+Test mode allows you to test the level without any clock running. The snake is moved purely using the cursor keys, but
+interacts with the level as in the actual game. Space will also advance the snake one step, useful for testing arrows.
 
 Levels can be saved, and uploaded to be shared with other people. Shared levels
 only become available after the levels have been curated by the Mamba team.""")
@@ -65,9 +68,19 @@
         self.container.add(self.edit_widget)
         self.container.add_callback(KEYDOWN, self.keydown_event)
         self.container.add_callback(MOUSEBUTTONDOWN, self.mouse_event)
+        self.edit_widget.add_callback(SnakeDiedEvent, self.halt_test)
+        self.edit_widget.add_callback(LevelCompletedEvent, self.halt_test)
         self.mode = 'Tiles'
         self.sprite_mode = 'Add'
         self.sprite_cls = None
+        # Map for test mode lookups
+        self.action_map = {
+                K_UP: UP,
+                K_DOWN: DOWN,
+                K_LEFT: LEFT,
+                K_RIGHT: RIGHT,
+                K_SPACE: None,
+                }
 
     def on_enter(self):
         # We need to juggle the display to the correct size
@@ -99,6 +112,26 @@
             # Activate floor button
             self.floor_button.forced_click()
             return True
+        elif self.mode == 'Test' and ev.key in self.action_map:
+            self.edit_widget.apply_action(self.action_map[ev.key])
+            return True
+
+    def halt_test(self, ev, widget):
+        if SnakeDiedEvent.matches(ev):
+            text = 'Snake died: %s' % ev.reason
+        else:
+            text = 'Level completed'
+        if self.edit_widget.snake_alive:
+            self.edit_widget.kill_snake()
+            message = MessageBox((300, 200), text,
+                    post_callback=self.test_restart)
+            self.display_dialog(message)
+        return True
+
+    def test_restart(self):
+        self.unpause()
+        self.edit_widget.restart()
+        return True
 
     def setup_toolbar(self):
         """Draw the editor toolbar"""
@@ -199,8 +232,12 @@
                                    'Sprites')
         sprite_button.add_callback('clicked', self.change_toolbar,
                                    'Sprites')
-        help_button = TextButton((sprite_button.rect.right + button_padding,
+        test_button = TextButton((sprite_button.rect.right + button_padding,
                                   thing_button.rect.bottom + button_padding),
+                                  'Test')
+        test_button.add_callback('clicked', self.change_toolbar, 'Test')
+        help_button = TextButton((button_left,
+                                  test_button.rect.bottom + button_padding),
                                  'Help')
         help_button.add_callback('clicked', self.show_help)
 
@@ -210,9 +247,12 @@
             thing_button.disable()
         elif self.mode == "Sprites":
             sprite_button.disable()
+        elif self.mode == 'Test':
+            test_button.disable()
         self.container.add(tile_button)
         self.container.add(thing_button)
         self.container.add(sprite_button)
+        self.container.add(test_button)
         self.container.add(help_button)
 
         button_height = LOAD_SAVE_HEIGHT
@@ -242,6 +282,11 @@
         save.add_callback('clicked', self.save)
         self.container.add(save)
 
+        if self.mode == 'Test':
+            save.disable()
+            load.disable()
+            new.disable()
+
     def change_tool(self, ev, widget, new_tool, text):
         self.edit_widget.set_tool(new_tool)
         self.current_tool.text = 'Tool: %s' % text
@@ -249,7 +294,7 @@
         return True
 
     def show_help(self, ev, widget):
-        message = MessageBox((50, 50), HELP_MSG, color="black",
+        message = MessageBox((20, 20), HELP_MSG, color="black",
                              fontsize=12)
         self.display_dialog(message)
         return True
@@ -349,16 +394,30 @@
             self.level.level_name = ''  # Special case for new level
         self.container.paused = False
         self.edit_widget = EditLevelWidget(self.level)
+        self.edit_widget.add_callback(SnakeDiedEvent, self.halt_test)
+        self.edit_widget.add_callback(LevelCompletedEvent, self.halt_test)
         self.container.add(self.edit_widget)
         self.clear_toolbar()
         self.setup_toolbar()
         return True
 
     def change_toolbar(self, ev, widget, new_mode):
+        if new_mode == 'Test':
+            try:
+                self.level.validate_level()
+            except InvalidMapError, error:
+                # Fail to change mode on invalid maps
+                message = MessageBox((300, 300), "Map isn't valid\n%s" % error)
+                self.display_dialog(message)
+                return True
         self.mode = new_mode
-        self.edit_widget.tile_mode = (self.mode != 'Sprites')
+        self.edit_widget.tile_mode = (self.mode in ('Tiles', 'Sprites'))
         self.clear_toolbar()
         self.setup_toolbar()
+        if self.mode == 'Test':
+            self.edit_widget.start_test()
+        else:
+            self.edit_widget.stop_test()
         return True
 
     def clear_toolbar(self):
--- a/mamba/widgets/level.py	Tue Sep 20 17:29:38 2011 +0200
+++ b/mamba/widgets/level.py	Wed Sep 21 14:16:58 2011 +0200
@@ -1,8 +1,11 @@
 from pygame.rect import Rect
 from pygame.locals import MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION
+from pygame.key import set_repeat
 
 from mamba.widgets.base import Widget
 from mamba.constants import TILE_SIZE
+from mamba.snake import Snake
+from mamba.engine import FlipArrowsEvent
 
 
 class EditLevelWidget(Widget):
@@ -18,9 +21,55 @@
         self.add_callback(MOUSEBUTTONDOWN, self.place_tile)
         self.add_callback(MOUSEBUTTONUP, self.end_draw)
         self.add_callback(MOUSEMOTION, self.draw_tiles)
+        self.add_callback(FlipArrowsEvent, self.flip_arrows)
+        self.snake = None
+        self.snake_alive = False
+
+    def start_test(self):
+        self.level.restart()
+        tile_pos, orientation = self.level.get_entry()
+        self.snake = Snake(tile_pos, orientation)
+        set_repeat(40, 100)
+        self.snake_alive = True
+
+    def stop_test(self):
+        self.snake = None
+        self.snake_alive = False
+        self.level.restart()
+        set_repeat(0, 0)
 
     def draw(self, surface):
         self.level.draw(surface)
+        if self.snake:
+            self.snake.draw(surface)
+
+    def kill_snake(self):
+        """Prevent user interaction while snake is dead"""
+        self.snake_alive = False
+
+    def restart(self):
+        self.start_test()
+
+    def interact(self, segment):
+        if not self.snake or not self.snake_alive:
+            return
+        tiles = segment.filter_collisions(self.level.sprites)
+        for tile in tiles:
+            tile.interact(self, segment)
+
+    def get_sprite(self, sprite_id):
+        return self.level.extra_sprites[sprite_id]
+
+    def apply_action(self, orientation):
+        if not self.snake or not self.snake_alive:
+            return
+        # We choose numbers that are close to, but not exactly, move 0.5 tiles
+        # to avoid a couple of rounding corner cases in the snake code
+        if orientation is None or orientation == self.snake.orientation:
+            self.snake.update(9.99 / self.snake.speed, self)
+        else:
+            self.snake.send_new_direction(orientation)
+            self.snake.update(9.99 / self.snake.speed, self)
 
     def set_tool(self, new_tool):
         self.main_tool = new_tool
@@ -34,6 +83,9 @@
             # FIXME: Need to consider leaving and re-entering the widget
             self.update_tile(self.convert_pos(event.pos))
 
+    def flip_arrows(self, ev, widget):
+        self.level.flip_arrows()
+
     def place_tile(self, event, widget):
         if self.tile_mode:
             if event.button == 1:  # Left button