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