Avoid a infinite loop when scrolling is involved
[erdslangetjie.git] / erdslangetjie / __main__.py
index 74a0c33a2509ac939988175ea4de3624d6a033f2..e408ac8204851dcb0ee40209e4a7c5adea5ab8be 100644 (file)
@@ -6,13 +6,13 @@ from kivy.uix.relativelayout import RelativeLayout
 from kivy.uix.scrollview import ScrollView
 from kivy.uix.label import Label
 from kivy.uix.popup import Popup
 from kivy.uix.scrollview import ScrollView
 from kivy.uix.label import Label
 from kivy.uix.popup import Popup
-from kivy.graphics import Color, Rectangle
+from kivy.graphics import Rectangle
 from kivy.utils import platform
 from kivy.clock import Clock
 from kivy.config import Config
 
 from erdslangetjie.level import LevelList
 from kivy.utils import platform
 from kivy.clock import Clock
 from kivy.config import Config
 
 from erdslangetjie.level import LevelList
-from erdslangetjie.data import load_image
+from erdslangetjie.data import load_image, load_sound
 from erdslangetjie.player import ThePlayer, Nemesis
 
 
 from erdslangetjie.player import ThePlayer, Nemesis
 
 
@@ -27,8 +27,6 @@ class GameWindow(RelativeLayout):
         self.level_list = level_list
         self.level_obj = self.level_list.get_current_level()
         self.level_obj.load_tiles()
         self.level_list = level_list
         self.level_obj = self.level_list.get_current_level()
         self.level_obj.load_tiles()
-        self.tiles = {}
-        self.view = app.root
         self.app = app
 
         cols, rows = self.level_obj.get_size()
         self.app = app
 
         cols, rows = self.level_obj.get_size()
@@ -37,47 +35,58 @@ class GameWindow(RelativeLayout):
                 size=(cols * TILE_SIZE, rows * TILE_SIZE),
                 size_hint=(None, None))
 
                 size=(cols * TILE_SIZE, rows * TILE_SIZE),
                 size_hint=(None, None))
 
-        self.x_scroll_margin = float(TILE_SIZE) / self.view.size[0]
-        self.y_scroll_margin = float(TILE_SIZE) / self.view.size[1]
-
         self.mouse_move = False
 
         self.mouse_move = False
 
+        self.caught = load_sound('sounds/caught.ogg')
+
         self.player = ThePlayer()
         self.player = ThePlayer()
-        self.nemesis = Nemesis()
+        self.nemesis = Nemesis(self.app.config)
         if not self.level_obj.enter_pos:
             raise RuntimeError('No entry point')
         self.player_tile = None
         self.nemesis_tile = None
         self.timer_set = False
         if not self.level_obj.enter_pos:
             raise RuntimeError('No entry point')
         self.player_tile = None
         self.nemesis_tile = None
         self.timer_set = False
+        self.move_counter = 0
 
         self.player.pos = self.level_obj.enter_pos
 
         self.player.pos = self.level_obj.enter_pos
-        if platform() != 'android':
+        self.keyboard = None
+        self._key_bound = False
+        self._background = None
+
+    def build(self):
+        if platform() != 'android' and not self.keyboard:
             # Very hack'ish
             # We need to delay this import until after the window creation by
             # the app, else our size config doesn't work
             from kivy.core.window import Window
             self.keyboard = Window.request_keyboard(self._closed, self)
             # Very hack'ish
             # We need to delay this import until after the window creation by
             # the app, else our size config doesn't work
             from kivy.core.window import Window
             self.keyboard = Window.request_keyboard(self._closed, self)
+        if self.keyboard and not self._key_bound:
+            # We remove this binding when we're the not top level widget,
+            # so re-add it here
+            self._key_bound = True
             self.keyboard.bind(on_key_down=self._on_key_down)
             self.keyboard.bind(on_key_down=self._on_key_down)
-
-    def build(self):
         self.clear_widgets()
         self.clear_widgets()
-        self.tiles = {}
+        self._background = Widget(size=self.size, pos=(0, 0))
         tiles = self.level_obj.get_tiles()
         bx, by = 0, 0
         for tile_line in tiles:
             bx = 0
             for tile in tile_line:
         tiles = self.level_obj.get_tiles()
         bx, by = 0, 0
         for tile_line in tiles:
             bx = 0
             for tile in tile_line:
-                node = Widget(size=(TILE_SIZE, TILE_SIZE),
-                        pos=(bx, by),
-                        size_hint=(None, None))
-                self.add_widget(node)
-                with node.canvas:
-                    Color(1, 1, 1)
-                    Rectangle(pos=node.pos, size=node.size,
-                            texture=tile.texture)
-                self.tiles[(bx, by)] = node
+                self.draw_tile((bx, by), tile)
                 bx += TILE_SIZE
             by += TILE_SIZE
                 bx += TILE_SIZE
             by += TILE_SIZE
+        self.add_widget(self._background)
+
+    def draw_tile(self, pos, tile):
+        with self._background.canvas:
+            Rectangle(pos=pos, size=(TILE_SIZE, TILE_SIZE),
+                    texture=tile.texture)
+
+    def fix_scroll_margins(self):
+        # We need to call this after app.root is set
+        self.view = self.app.root
+        self.x_scroll_margin = float(TILE_SIZE) / self.view.size[0]
+        self.y_scroll_margin = float(TILE_SIZE) / self.view.size[1]
 
     def draw_player(self):
         if self.player_tile:
 
     def draw_player(self):
         if self.player_tile:
@@ -87,7 +96,6 @@ class GameWindow(RelativeLayout):
         self.player_tile = Widget(size=(TILE_SIZE, TILE_SIZE),
                 pos=sprite_pos)
         with self.player_tile.canvas:
         self.player_tile = Widget(size=(TILE_SIZE, TILE_SIZE),
                 pos=sprite_pos)
         with self.player_tile.canvas:
-            Color(1, 1, 1)
             Rectangle(pos=sprite_pos, size=self.player_tile.size,
                     texture=self.player.get_texture())
         self.add_widget(self.player_tile)
             Rectangle(pos=sprite_pos, size=self.player_tile.size,
                     texture=self.player.get_texture())
         self.add_widget(self.player_tile)
@@ -118,20 +126,28 @@ class GameWindow(RelativeLayout):
                 if true_point[0] >= self.view.size[0]:
                     self.view.scroll_x += self.x_scroll_margin
                     true_point = self.to_parent(*check_point)
                 if true_point[0] >= self.view.size[0]:
                     self.view.scroll_x += self.x_scroll_margin
                     true_point = self.to_parent(*check_point)
-                    #print '-x', self.view.scroll_x, self.view.scroll_y
+                    # Avoid an infinite loop that can happen we
+                    # changing screens
+                    if self.view.scroll_x > 0.99:
+                        return
                 elif true_point[0] < 0:
                     self.view.scroll_x -= self.x_scroll_margin
                     true_point = self.to_parent(*check_point)
                 elif true_point[0] < 0:
                     self.view.scroll_x -= self.x_scroll_margin
                     true_point = self.to_parent(*check_point)
-                    #print '+x', self.view.scroll_x, self.view.scroll_y
+                    # See above
+                    if self.view.scroll_x < 0.01:
+                        return
                 elif true_point[1] >= self.view.size[1]:
                     self.view.scroll_y += self.y_scroll_margin
                     true_point = self.to_parent(*check_point)
                 elif true_point[1] >= self.view.size[1]:
                     self.view.scroll_y += self.y_scroll_margin
                     true_point = self.to_parent(*check_point)
-                    #print '+y', self.view.scroll_x, self.view.scroll_y
+                    # See above
+                    if self.view.scroll_y > 0.99:
+                        return
                 elif true_point[1] < 0:
                     self.view.scroll_y -= self.y_scroll_margin
                     true_point = self.to_parent(*check_point)
                 elif true_point[1] < 0:
                     self.view.scroll_y -= self.y_scroll_margin
                     true_point = self.to_parent(*check_point)
-                    #print '-y', self.view.scroll_x, self.view.scroll_y
-                #print true_point, self.view.size
+                    # See above
+                    if self.view.scroll_y < 0.01:
+                        return
 
     def included(self, point, margin):
         if point[0] < margin:
 
     def included(self, point, margin):
         if point[0] < margin:
@@ -154,13 +170,14 @@ class GameWindow(RelativeLayout):
         self.nemesis_tile = Widget(size=(TILE_SIZE, TILE_SIZE),
                 pos=sprite_pos)
         with self.nemesis_tile.canvas:
         self.nemesis_tile = Widget(size=(TILE_SIZE, TILE_SIZE),
                 pos=sprite_pos)
         with self.nemesis_tile.canvas:
-            Color(1, 1, 1)
             Rectangle(pos=sprite_pos, size=self.nemesis_tile.size,
                     texture=self.nemesis.get_texture())
         self.add_widget(self.nemesis_tile)
 
     def _closed(self):
             Rectangle(pos=sprite_pos, size=self.nemesis_tile.size,
                     texture=self.nemesis.get_texture())
         self.add_widget(self.nemesis_tile)
 
     def _closed(self):
-        self.keyboard.unbind(on_key_down=self._on_key_down)
+        if self.keyboard:
+            self._key_bound = False
+            self.keyboard.unbind(on_key_down=self._on_key_down)
 
     def _on_key_down(self, keyboard, keycode, text, modifiers):
         direction = None
 
     def _on_key_down(self, keyboard, keycode, text, modifiers):
         direction = None
@@ -179,38 +196,62 @@ class GameWindow(RelativeLayout):
     def do_move(self, direction):
         if not self.level_obj:
             return
     def do_move(self, direction):
         if not self.level_obj:
             return
-        self.player.move(direction, self.level_obj)
+        # Do nothing on null moves
+        if not self.player.move(direction, self.level_obj):
+            return
+        if self.check_state():
+            return
+        self.do_nemesis_move()
+
+    def do_nemesis_move(self):
+        self.nemesis.move(self.level_obj, self.player.pos)
+        if self.check_state():
+            return
+        if self.move_counter > 4:
+            self.move_counter = 0
+            self.draw_nemesis()
+            self.nemesis.move(self.level_obj, self.player.pos)
+            if self.check_state():
+                return
+        else:
+            self.move_counter += 1
+        self.draw_nemesis()
         self.draw_player()
         self.draw_player()
-        self.check_state()
-        if not self.timer_set:
-            self.reset_timer()
+        self.reset_timer()
 
     def timed_move(self, event):
         if not self.level_obj:
             return
 
     def timed_move(self, event):
         if not self.level_obj:
             return
-        self.nemesis.move(self.level_obj, self.check_caught)
-        self.draw_nemesis()
-        self.check_state()
-        self.reset_timer()
+        self.do_nemesis_move()
 
     def reset_timer(self):
         self.timer_set = True
         Clock.unschedule(self.timed_move)
 
     def reset_timer(self):
         self.timer_set = True
         Clock.unschedule(self.timed_move)
-        Clock.schedule_once(self.timed_move, 0.5)
+        Clock.schedule_once(self.timed_move, 3)
 
     def check_caught(self):
         return self.nemesis.pos == self.player.pos
 
 
     def check_caught(self):
         return self.nemesis.pos == self.player.pos
 
+    def stop_game(self):
+        Clock.unschedule(self.timed_move)
+        if self.nemesis_tile:
+            self.remove_widget(self.nemesis_tile)
+        self.nemesis.reset_pos()
+
     def reset_level(self):
         Clock.unschedule(self.timed_move)
         self.timer_set = False
     def reset_level(self):
         Clock.unschedule(self.timed_move)
         self.timer_set = False
+        self.move_counter = 0
         if self.nemesis_tile:
             self.remove_widget(self.nemesis_tile)
         self.nemesis.reset_pos()
         if self.nemesis_tile:
             self.remove_widget(self.nemesis_tile)
         self.nemesis.reset_pos()
+
+    def load_level(self):
         if self.level_obj:
             self.level_obj.load_tiles()
             self.player.pos = self.level_obj.enter_pos
         if self.level_obj:
             self.level_obj.load_tiles()
             self.player.pos = self.level_obj.enter_pos
-            self.remove_widget(self.player_tile)
+            if self.player_tile:
+                self.remove_widget(self.player_tile)
             self.view.scroll_x = 0
             self.view.scroll_y = 0
             self.build()
             self.view.scroll_x = 0
             self.view.scroll_y = 0
             self.build()
@@ -219,23 +260,48 @@ class GameWindow(RelativeLayout):
             return True
         return False
 
             return True
         return False
 
+    def do_reload(self):
+        self.level_obj = self.level_list.get_current_level()
+
     def check_state(self):
     def check_state(self):
+        if not self.level_obj:
+            return True
         if self.level_obj.at_exit(self.player.pos):
         if self.level_obj.at_exit(self.player.pos):
+            self.reset_level()
             # Jump to next level
             self.level_obj = self.level_list.advance_to_next_level()
             # Jump to next level
             self.level_obj = self.level_list.advance_to_next_level()
-            if not self.reset_level():
+            if not self.load_level():
+                self._closed()
                 self.app.game_over(True)
                 self.app.game_over(True)
+            return True
         elif self.check_caught():
             # Caught
         elif self.check_caught():
             # Caught
+            if self.app.config.getdefault('bane', 'sound', '0') != '0':
+                self.caught.play()
+            self.reset_level()
+            self._closed()
             self.app.game_over(False)
             self.app.game_over(False)
+            return True
+        elif self.level_obj.is_button(self.player.pos):
+            self.level_obj.trigger_button(self.player.pos)
+        elif self.level_obj.is_button(self.nemesis.pos):
+            self.level_obj.trigger_button(self.nemesis.pos)
+        for map_pos, new_tile in self.level_obj.get_changed_tiles():
+            pos = (map_pos[0] * TILE_SIZE, map_pos[1] * TILE_SIZE)
+            self.draw_tile(pos, new_tile)
+        return False
 
     def _calc_mouse_pos(self, pos):
         pos = self.to_local(*pos)
         return (int(pos[0] / TILE_SIZE), int(pos[1] / TILE_SIZE))
 
 
     def _calc_mouse_pos(self, pos):
         pos = self.to_local(*pos)
         return (int(pos[0] / TILE_SIZE), int(pos[1] / TILE_SIZE))
 
+    def _near_player(self, pos):
+        return (abs(pos[0] - self.player.pos[0]) < 2 and
+                abs(pos[1] - self.player.pos[1]) < 2)
+
     def on_touch_down(self, touch):
         pos = self._calc_mouse_pos(touch.pos)
     def on_touch_down(self, touch):
         pos = self._calc_mouse_pos(touch.pos)
-        if pos == self.player.pos:
+        if self._near_player(pos):
             self.mouse_move = True
             self.mouse_start = pos
 
             self.mouse_move = True
             self.mouse_start = pos
 
@@ -303,14 +369,48 @@ class LostScreen(Screen):
 
 class GameApp(App):
 
 
 class GameApp(App):
 
-    title = "Peter's thread snake"
+    title = "Bane's Befuddlement"
 
     def __init__(self):
         super(GameApp, self).__init__()
         self.levels = LevelList()
 
     def __init__(self):
         super(GameApp, self).__init__()
         self.levels = LevelList()
+        self.game = None
+
+    def build_config(self, config):
+        config.setdefaults('bane', {
+            'start_level': 'levels/level1.txt',
+            'sound': 'True'
+            })
+
+    def build_settings(self, settings):
+        config_json = """[
+            { "type": "title",
+              "title": "Bane's Befuddlement"
+            },
+
+            { "type": "options",
+              "title": "Start Level",
+              "desc": "Level to start at",
+              "section": "bane",
+              "key": "start_level",
+              "options": ["%s"] },
+
+            { "type": "bool",
+              "title": "Sound",
+              "desc": "Enable sound",
+              "section": "bane",
+              "key": "sound"
+             }
+             ]""" % '", "'.join(self.levels.get_level_names())
+        settings.add_json_panel("Bane's Befuddlement",
+                self.config, data=config_json)
 
     def build(self):
         root = ScrollView(size_hint=(None, None))
 
     def build(self):
         root = ScrollView(size_hint=(None, None))
+        level_name = self.config.getdefault('bane', 'start_level', None)
+        if level_name:
+            self.levels.set_level_to(level_name)
+        self.game = GameWindow(self.levels, self)
         return root
 
     def on_start(self):
         return root
 
     def on_start(self):
@@ -337,22 +437,25 @@ class GameApp(App):
 
     def start_game(self, label, ref):
         """Start the game"""
 
     def start_game(self, label, ref):
         """Start the game"""
-        game = GameWindow(self.levels, self)
-        game.build()
         self.root.clear_widgets()
         self.root.clear_widgets()
-        self.root.add_widget(game)
+        self.root.add_widget(self.game)
+        self.game.fix_scroll_margins()
+        self.game.reset_level()
+        self.game.load_level()
         # Ensure the player is visible
         self.root.scroll_x = 0
         self.root.scroll_y = 0
         # Ensure the player is visible
         self.root.scroll_x = 0
         self.root.scroll_y = 0
-        game.draw_player()
-        game.draw_nemesis()
+        self.game.draw_player()
+        self.game.draw_nemesis()
 
     def game_over(self, won):
         if won:
             screen = WonScreen(self)
             self.levels.reset()
 
     def game_over(self, won):
         if won:
             screen = WonScreen(self)
             self.levels.reset()
+            self.game.do_reload()
         else:
             screen = LostScreen(self)
         else:
             screen = LostScreen(self)
+        self.game.stop_game()
         self.root.clear_widgets()
         self.root.add_widget(screen)
 
         self.root.clear_widgets()
         self.root.add_widget(screen)