'Fix' crash bug on android
[erdslangetjie.git] / erdslangetjie / __main__.py
1 from erdslangetjie.constants import TILE_SIZE, LEFT, RIGHT, UP, DOWN
2
3 from kivy.app import App
4 from kivy.uix.widget import Widget
5 from kivy.uix.relativelayout import RelativeLayout
6 from kivy.uix.scrollview import ScrollView
7 from kivy.uix.label import Label
8 from kivy.uix.popup import Popup
9 from kivy.graphics import Color, Rectangle
10 from kivy.utils import platform
11 from kivy.clock import Clock
12 from kivy.config import Config
13
14 from erdslangetjie.level import LevelList
15 from erdslangetjie.data import load_image, load_sound
16 from erdslangetjie.player import ThePlayer, Nemesis
17
18
19 if platform() != 'android':
20     Config.set('graphics', 'width', '1026')
21     Config.set('graphics', 'height', '760')
22
23
24 class GameWindow(RelativeLayout):
25
26     def __init__(self, level_list, app):
27         self.level_list = level_list
28         self.level_obj = self.level_list.get_current_level()
29         self.level_obj.load_tiles()
30         self.tiles = {}
31         self.view = app.root
32         self.app = app
33
34         cols, rows = self.level_obj.get_size()
35
36         super(GameWindow, self).__init__(
37                 size=(cols * TILE_SIZE, rows * TILE_SIZE),
38                 size_hint=(None, None))
39
40         self.x_scroll_margin = float(TILE_SIZE) / self.view.size[0]
41         self.y_scroll_margin = float(TILE_SIZE) / self.view.size[1]
42
43         self.mouse_move = False
44
45         self.caught = load_sound('sounds/caught.ogg')
46
47         self.player = ThePlayer()
48         self.nemesis = Nemesis(self.app.config)
49         if not self.level_obj.enter_pos:
50             raise RuntimeError('No entry point')
51         self.player_tile = None
52         self.nemesis_tile = None
53         self.timer_set = False
54         self.move_counter = 0
55
56         self.player.pos = self.level_obj.enter_pos
57         if platform() != 'android':
58             # Very hack'ish
59             # We need to delay this import until after the window creation by
60             # the app, else our size config doesn't work
61             from kivy.core.window import Window
62             self.keyboard = Window.request_keyboard(self._closed, self)
63             self.keyboard.bind(on_key_down=self._on_key_down)
64
65     def build(self):
66         self.clear_widgets()
67         self.tiles = {}
68         tiles = self.level_obj.get_tiles()
69         bx, by = 0, 0
70         for tile_line in tiles:
71             bx = 0
72             for tile in tile_line:
73                 self.draw_tile((bx, by), tile)
74                 bx += TILE_SIZE
75             by += TILE_SIZE
76
77     def draw_tile(self, pos, tile):
78         if pos in self.tiles:
79             self.remove_widget(self.tiles[pos])
80         node = Widget(size=(TILE_SIZE, TILE_SIZE),
81                 pos=pos, size_hint=(None, None))
82         self.add_widget(node)
83         with node.canvas:
84             Color(1, 1, 1)
85             Rectangle(pos=node.pos, size=node.size,
86                     texture=tile.texture)
87         self.tiles[pos] = node
88
89     def draw_player(self):
90         if self.player_tile:
91             self.remove_widget(self.player_tile)
92         sprite_pos = (self.player.pos[0] * TILE_SIZE,
93                 self.player.pos[1] * TILE_SIZE)
94         self.player_tile = Widget(size=(TILE_SIZE, TILE_SIZE),
95                 pos=sprite_pos)
96         with self.player_tile.canvas:
97             Color(1, 1, 1)
98             Rectangle(pos=sprite_pos, size=self.player_tile.size,
99                     texture=self.player.get_texture())
100         self.add_widget(self.player_tile)
101         for offset in [(TILE_SIZE - 1, TILE_SIZE - 1),
102                 (-TILE_SIZE + 1, TILE_SIZE - 1),
103                 (TILE_SIZE - 1, -TILE_SIZE + 1),
104                 (-TILE_SIZE + 1, -TILE_SIZE + 1),
105                 (0, 2 * TILE_SIZE - 2),
106                 (-2 * TILE_SIZE + 2, 0),
107                 (2 * TILE_SIZE - 2, 0),
108                 (0, -2 * TILE_SIZE + 2),
109                 (0, 0)]:
110             # Aim is to ensure a 'neighbourhood' around the player
111             # is visible if possible
112             check_point = (sprite_pos[0] + offset[0] + TILE_SIZE / 2,
113                     sprite_pos[1] + offset[1] + TILE_SIZE / 2)
114             true_point = self.to_parent(*check_point)
115             if check_point[0] < 0:
116                 continue
117             if check_point[1] < 0:
118                 continue
119             if check_point[0] >= self.size[0]:
120                 continue
121             if check_point[1] >= self.size[1]:
122                 continue
123             while not self.included(true_point, 0):
124                 # Scroll ourselves
125                 if true_point[0] >= self.view.size[0]:
126                     self.view.scroll_x += self.x_scroll_margin
127                     true_point = self.to_parent(*check_point)
128                 elif true_point[0] < 0:
129                     self.view.scroll_x -= self.x_scroll_margin
130                     true_point = self.to_parent(*check_point)
131                 elif true_point[1] >= self.view.size[1]:
132                     self.view.scroll_y += self.y_scroll_margin
133                     true_point = self.to_parent(*check_point)
134                 elif true_point[1] < 0:
135                     self.view.scroll_y -= self.y_scroll_margin
136                     true_point = self.to_parent(*check_point)
137
138     def included(self, point, margin):
139         if point[0] < margin:
140             return False
141         if point[0] >= self.view.size[0] - margin:
142             return False
143         if point[1] < margin:
144             return False
145         if point[1] >= self.view.size[1] - margin:
146             return False
147         return True
148
149     def draw_nemesis(self):
150         if not self.nemesis.on_board():
151             return
152         if self.nemesis_tile:
153             self.remove_widget(self.nemesis_tile)
154         sprite_pos = (self.nemesis.pos[0] * TILE_SIZE,
155                 self.nemesis.pos[1] * TILE_SIZE)
156         self.nemesis_tile = Widget(size=(TILE_SIZE, TILE_SIZE),
157                 pos=sprite_pos)
158         with self.nemesis_tile.canvas:
159             Color(1, 1, 1)
160             Rectangle(pos=sprite_pos, size=self.nemesis_tile.size,
161                     texture=self.nemesis.get_texture())
162         self.add_widget(self.nemesis_tile)
163
164     def _closed(self):
165         if platform() != 'android':
166             self.keyboard.unbind(on_key_down=self._on_key_down)
167
168     def _on_key_down(self, keyboard, keycode, text, modifiers):
169         direction = None
170         letter = keycode[1].lower()
171         if letter in UP:
172             direction = (0, 1)
173         elif letter in DOWN:
174             direction = (0, -1)
175         elif letter in LEFT:
176             direction = (-1, 0)
177         elif letter in RIGHT:
178             direction = (1, 0)
179         if direction:
180             self.do_move(direction)
181
182     def do_move(self, direction):
183         if not self.level_obj:
184             return
185         # Do nothing on null moves
186         if not self.player.move(direction, self.level_obj):
187             return
188         self.check_state()
189         self.do_nemesis_move()
190
191     def do_nemesis_move(self):
192         self.nemesis.move(self.level_obj, self.player.pos)
193         self.check_state()
194         if self.move_counter > 4:
195             self.move_counter = 0
196             self.draw_nemesis()
197             self.nemesis.move(self.level_obj, self.player.pos)
198             self.check_state()
199         else:
200             self.move_counter += 1
201         self.draw_nemesis()
202         self.draw_player()
203         self.reset_timer()
204
205     def timed_move(self, event):
206         if not self.level_obj:
207             return
208         self.do_nemesis_move()
209
210     def reset_timer(self):
211         self.timer_set = True
212         Clock.unschedule(self.timed_move)
213         Clock.schedule_once(self.timed_move, 3)
214
215     def check_caught(self):
216         return self.nemesis.pos == self.player.pos
217
218     def reset_level(self):
219         Clock.unschedule(self.timed_move)
220         self.timer_set = False
221         self.move_counter = 0
222         if self.nemesis_tile:
223             self.remove_widget(self.nemesis_tile)
224         self.nemesis.reset_pos()
225
226     def load_level(self):
227         if self.level_obj:
228             self.level_obj.load_tiles()
229             self.player.pos = self.level_obj.enter_pos
230             self.remove_widget(self.player_tile)
231             self.view.scroll_x = 0
232             self.view.scroll_y = 0
233             self.build()
234             self.draw_nemesis()
235             self.draw_player()
236             return True
237         return False
238
239     def check_state(self):
240         if not self.level_obj:
241             return
242         if self.level_obj.at_exit(self.player.pos):
243             self.reset_level()
244             # Jump to next level
245             self.level_obj = self.level_list.advance_to_next_level()
246             if not self.load_level():
247                 app = self.app
248                 self.app = None
249                 self._closed()
250                 app.game_over(True)
251             return
252         elif self.check_caught():
253             # Caught
254             if self.app.config.getdefault('bane', 'sound', '0') != '0':
255                 self.caught.play()
256             self.reset_level()
257             app = self.app
258             self.app = None
259             self._closed()
260             app.game_over(False)
261             return
262         elif self.level_obj.is_button(self.player.pos):
263             self.level_obj.trigger_button(self.player.pos)
264         elif self.level_obj.is_button(self.nemesis.pos):
265             self.level_obj.trigger_button(self.nemesis.pos)
266         for map_pos, new_tile in self.level_obj.get_changed_tiles():
267             pos = (map_pos[0] * TILE_SIZE, map_pos[1] * TILE_SIZE)
268             self.draw_tile(pos, new_tile)
269
270     def _calc_mouse_pos(self, pos):
271         pos = self.to_local(*pos)
272         return (int(pos[0] / TILE_SIZE), int(pos[1] / TILE_SIZE))
273
274     def on_touch_down(self, touch):
275         pos = self._calc_mouse_pos(touch.pos)
276         if pos == self.player.pos:
277             self.mouse_move = True
278             self.mouse_start = pos
279
280     def on_touch_up(self, touch):
281         self.mouse_move = False
282
283     def on_touch_move(self, touch):
284         if self.mouse_move:
285             pos = self._calc_mouse_pos(touch.pos)
286             if (pos[0] - self.mouse_start[0] != 0) or (
287                     pos[1] - self.mouse_start[1] != 0):
288                 direction = (pos[0] - self.mouse_start[0],
289                         pos[1] - self.mouse_start[1])
290                 self.do_move(direction)
291                 self.mouse_start = pos
292
293
294 class Screen(Widget):
295
296     BACKGROUND = None
297     START = 'Start'
298
299     def __init__(self, app):
300         super(Screen, self).__init__()
301         self.image = load_image(self.BACKGROUND)
302         self.app = app
303         with self.canvas:
304             Rectangle(pos=(0, 0), size=(1026, 760),
305                     texture=self.image.texture)
306
307         self.stop_button = Label(
308                 text='[ref=quit][color=ff0066]Quit[/color][/ref]',
309                 font_size=30,
310                 markup=True,
311                 size=(200, 40),
312                 pos=((1026 - 200) / 2 - 100, 100))
313         self.stop_button.bind(on_ref_press=self.app.stop_app)
314         self.start_button = Label(
315                 text="[ref=start][color=00ff66]%s[/color][/ref]" % self.START,
316                 font_size=30,
317                 markup=True, size=(200, 40),
318                 pos=((1026 - 200) / 2 + 100, 100))
319         self.start_button.bind(on_ref_press=self.app.start_game)
320         self.add_widget(self.stop_button)
321         self.add_widget(self.start_button)
322
323
324 class IntroScreen(Screen):
325
326     BACKGROUND = 'screens/intro_screen.png'
327     START = 'Start the Game'
328
329
330 class WonScreen(Screen):
331
332     BACKGROUND = 'screens/won.png'
333     START = 'Play again?'
334
335
336 class LostScreen(Screen):
337
338     BACKGROUND = 'screens/lost.png'
339     START = 'Retry?'
340
341
342 class GameApp(App):
343
344     title = "Bane's Befuddlement"
345
346     def __init__(self):
347         super(GameApp, self).__init__()
348         self.levels = LevelList()
349         self.game = None
350
351     def build_config(self, config):
352         config.setdefaults('bane', {
353             'start_level': 'levels/level1.txt',
354             'sound': 'True'
355             })
356
357     def build_settings(self, settings):
358         config_json = """[
359             { "type": "title",
360               "title": "Bane's Befuddlement"
361             },
362
363             { "type": "options",
364               "title": "Start Level",
365               "desc": "Level to start at",
366               "section": "bane",
367               "key": "start_level",
368               "options": ["%s"] },
369
370             { "type": "bool",
371               "title": "Sound",
372               "desc": "Enable sound",
373               "section": "bane",
374               "key": "sound"
375              }
376              ]""" % '", "'.join(self.levels.get_level_names())
377         settings.add_json_panel("Bane's Befuddlement",
378                 self.config, data=config_json)
379
380     def build(self):
381         root = ScrollView(size_hint=(None, None))
382         level_name = self.config.getdefault('bane', 'start_level', None)
383         if level_name:
384             self.levels.set_level_to(level_name)
385         return root
386
387     def on_start(self):
388         from kivy.base import EventLoop
389         window = EventLoop.window
390         if platform() == 'android':
391             window.fullscreen = True
392         self.root.size = window.size
393         errors = self.levels.get_errors()
394         if errors:
395             popup = Popup(title='Levels excluded',
396                     content=Label(text='\n'.join(errors)),
397                     size_hint=(.5, .5))
398             popup.open()
399         self.make_intro()
400
401     def make_intro(self):
402         self.root.clear_widgets()
403         screen = IntroScreen(self)
404         self.root.add_widget(screen)
405
406     def stop_app(self, label, ref):
407         self.stop()
408
409     def start_game(self, label, ref):
410         """Start the game"""
411         self.game = GameWindow(self.levels, self)
412         self.game.build()
413         self.root.clear_widgets()
414         self.root.add_widget(self.game)
415         # Ensure the player is visible
416         self.root.scroll_x = 0
417         self.root.scroll_y = 0
418         self.game.draw_player()
419         self.game.draw_nemesis()
420
421     def game_over(self, won):
422         if won:
423             screen = WonScreen(self)
424             self.levels.reset()
425         else:
426             screen = LostScreen(self)
427         del self.game
428         self.game = None
429         self.root.clear_widgets()
430         self.root.add_widget(screen)
431
432
433 def main():
434     """ Erdslangetjie, a maze game of eluding nemesis
435     """
436     GameApp().run()