Hook up some basic sound support
[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         self.keyboard.unbind(on_key_down=self._on_key_down)
166
167     def _on_key_down(self, keyboard, keycode, text, modifiers):
168         direction = None
169         letter = keycode[1].lower()
170         if letter in UP:
171             direction = (0, 1)
172         elif letter in DOWN:
173             direction = (0, -1)
174         elif letter in LEFT:
175             direction = (-1, 0)
176         elif letter in RIGHT:
177             direction = (1, 0)
178         if direction:
179             self.do_move(direction)
180
181     def do_move(self, direction):
182         if not self.level_obj:
183             return
184         # Do nothing on null moves
185         if not self.player.move(direction, self.level_obj):
186             return
187         self.check_state()
188         self.do_nemesis_move()
189
190     def do_nemesis_move(self):
191         self.nemesis.move(self.level_obj, self.player.pos)
192         self.check_state()
193         if self.move_counter > 4:
194             self.move_counter = 0
195             self.draw_nemesis()
196             self.nemesis.move(self.level_obj, self.player.pos)
197             self.check_state()
198         else:
199             self.move_counter += 1
200         self.draw_nemesis()
201         self.draw_player()
202         self.reset_timer()
203
204     def timed_move(self, event):
205         if not self.level_obj:
206             return
207         self.do_nemesis_move()
208
209     def reset_timer(self):
210         self.timer_set = True
211         Clock.unschedule(self.timed_move)
212         Clock.schedule_once(self.timed_move, 3)
213
214     def check_caught(self):
215         return self.nemesis.pos == self.player.pos
216
217     def reset_level(self):
218         Clock.unschedule(self.timed_move)
219         self.timer_set = False
220         self.move_counter = 0
221         if self.nemesis_tile:
222             self.remove_widget(self.nemesis_tile)
223         self.nemesis.reset_pos()
224
225     def load_level(self):
226         if self.level_obj:
227             self.level_obj.load_tiles()
228             self.player.pos = self.level_obj.enter_pos
229             self.remove_widget(self.player_tile)
230             self.view.scroll_x = 0
231             self.view.scroll_y = 0
232             self.build()
233             self.draw_nemesis()
234             self.draw_player()
235             return True
236         return False
237
238     def check_state(self):
239         if not self.level_obj:
240             return
241         if self.level_obj.at_exit(self.player.pos):
242             self.reset_level()
243             # Jump to next level
244             self.level_obj = self.level_list.advance_to_next_level()
245             if not self.load_level():
246                 app = self.app
247                 self.app = None
248                 self._closed()
249                 app.game_over(True)
250             return
251         elif self.check_caught():
252             # Caught
253             if self.app.config.getdefault('bane', 'sound', '0') != '0':
254                 self.caught.play()
255             self.reset_level()
256             app = self.app
257             self.app = None
258             self._closed()
259             app.game_over(False)
260             return
261         elif self.level_obj.is_button(self.player.pos):
262             self.level_obj.trigger_button(self.player.pos)
263         elif self.level_obj.is_button(self.nemesis.pos):
264             self.level_obj.trigger_button(self.nemesis.pos)
265         for map_pos, new_tile in self.level_obj.get_changed_tiles():
266             pos = (map_pos[0] * TILE_SIZE, map_pos[1] * TILE_SIZE)
267             self.draw_tile(pos, new_tile)
268
269     def _calc_mouse_pos(self, pos):
270         pos = self.to_local(*pos)
271         return (int(pos[0] / TILE_SIZE), int(pos[1] / TILE_SIZE))
272
273     def on_touch_down(self, touch):
274         pos = self._calc_mouse_pos(touch.pos)
275         if pos == self.player.pos:
276             self.mouse_move = True
277             self.mouse_start = pos
278
279     def on_touch_up(self, touch):
280         self.mouse_move = False
281
282     def on_touch_move(self, touch):
283         if self.mouse_move:
284             pos = self._calc_mouse_pos(touch.pos)
285             if (pos[0] - self.mouse_start[0] != 0) or (
286                     pos[1] - self.mouse_start[1] != 0):
287                 direction = (pos[0] - self.mouse_start[0],
288                         pos[1] - self.mouse_start[1])
289                 self.do_move(direction)
290                 self.mouse_start = pos
291
292
293 class Screen(Widget):
294
295     BACKGROUND = None
296     START = 'Start'
297
298     def __init__(self, app):
299         super(Screen, self).__init__()
300         self.image = load_image(self.BACKGROUND)
301         self.app = app
302         with self.canvas:
303             Rectangle(pos=(0, 0), size=(1026, 760),
304                     texture=self.image.texture)
305
306         self.stop_button = Label(
307                 text='[ref=quit][color=ff0066]Quit[/color][/ref]',
308                 font_size=30,
309                 markup=True,
310                 size=(200, 40),
311                 pos=((1026 - 200) / 2 - 100, 100))
312         self.stop_button.bind(on_ref_press=self.app.stop_app)
313         self.start_button = Label(
314                 text="[ref=start][color=00ff66]%s[/color][/ref]" % self.START,
315                 font_size=30,
316                 markup=True, size=(200, 40),
317                 pos=((1026 - 200) / 2 + 100, 100))
318         self.start_button.bind(on_ref_press=self.app.start_game)
319         self.add_widget(self.stop_button)
320         self.add_widget(self.start_button)
321
322
323 class IntroScreen(Screen):
324
325     BACKGROUND = 'screens/intro_screen.png'
326     START = 'Start the Game'
327
328
329 class WonScreen(Screen):
330
331     BACKGROUND = 'screens/won.png'
332     START = 'Play again?'
333
334
335 class LostScreen(Screen):
336
337     BACKGROUND = 'screens/lost.png'
338     START = 'Retry?'
339
340
341 class GameApp(App):
342
343     title = "Bane's Befuddlement"
344
345     def __init__(self):
346         super(GameApp, self).__init__()
347         self.levels = LevelList()
348         self.game = None
349
350     def build_config(self, config):
351         config.setdefaults('bane', {
352             'start_level': 'levels/level1.txt',
353             'sound': 'True'
354             })
355
356     def build_settings(self, settings):
357         config_json = """[
358             { "type": "title",
359               "title": "Bane's Befuddlement"
360             },
361
362             { "type": "options",
363               "title": "Start Level",
364               "desc": "Level to start at",
365               "section": "bane",
366               "key": "start_level",
367               "options": ["%s"] },
368
369             { "type": "bool",
370               "title": "Sound",
371               "desc": "Enable sound",
372               "section": "bane",
373               "key": "sound"
374              }
375              ]""" % '", "'.join(self.levels.get_level_names())
376         settings.add_json_panel("Bane's Befuddlement",
377                 self.config, data=config_json)
378
379     def build(self):
380         root = ScrollView(size_hint=(None, None))
381         level_name = self.config.getdefault('bane', 'start_level', None)
382         if level_name:
383             self.levels.set_level_to(level_name)
384         return root
385
386     def on_start(self):
387         from kivy.base import EventLoop
388         window = EventLoop.window
389         if platform() == 'android':
390             window.fullscreen = True
391         self.root.size = window.size
392         errors = self.levels.get_errors()
393         if errors:
394             popup = Popup(title='Levels excluded',
395                     content=Label(text='\n'.join(errors)),
396                     size_hint=(.5, .5))
397             popup.open()
398         self.make_intro()
399
400     def make_intro(self):
401         self.root.clear_widgets()
402         screen = IntroScreen(self)
403         self.root.add_widget(screen)
404
405     def stop_app(self, label, ref):
406         self.stop()
407
408     def start_game(self, label, ref):
409         """Start the game"""
410         self.game = GameWindow(self.levels, self)
411         self.game.build()
412         self.root.clear_widgets()
413         self.root.add_widget(self.game)
414         # Ensure the player is visible
415         self.root.scroll_x = 0
416         self.root.scroll_y = 0
417         self.game.draw_player()
418         self.game.draw_nemesis()
419
420     def game_over(self, won):
421         if won:
422             screen = WonScreen(self)
423             self.levels.reset()
424         else:
425             screen = LostScreen(self)
426         del self.game
427         self.game = None
428         self.root.clear_widgets()
429         self.root.add_widget(screen)
430
431
432 def main():
433     """ Erdslangetjie, a maze game of eluding nemesis
434     """
435     GameApp().run()