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