Speed up arrival of the nemesis
[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                     #print '-x', self.view.scroll_x, self.view.scroll_y
127                 elif true_point[0] < 0:
128                     self.view.scroll_x -= self.x_scroll_margin
129                     true_point = self.to_parent(*check_point)
130                     #print '+x', self.view.scroll_x, self.view.scroll_y
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                     #print '+y', self.view.scroll_x, self.view.scroll_y
135                 elif true_point[1] < 0:
136                     self.view.scroll_y -= self.y_scroll_margin
137                     true_point = self.to_parent(*check_point)
138                     #print '-y', self.view.scroll_x, self.view.scroll_y
139                 #print true_point, self.view.size
140
141     def included(self, point, margin):
142         if point[0] < margin:
143             return False
144         if point[0] >= self.view.size[0] - margin:
145             return False
146         if point[1] < margin:
147             return False
148         if point[1] >= self.view.size[1] - margin:
149             return False
150         return True
151
152     def draw_nemesis(self):
153         if not self.nemesis.on_board():
154             return
155         if self.nemesis_tile:
156             self.remove_widget(self.nemesis_tile)
157         sprite_pos = (self.nemesis.pos[0] * TILE_SIZE,
158                 self.nemesis.pos[1] * TILE_SIZE)
159         self.nemesis_tile = Widget(size=(TILE_SIZE, TILE_SIZE),
160                 pos=sprite_pos)
161         with self.nemesis_tile.canvas:
162             Color(1, 1, 1)
163             Rectangle(pos=sprite_pos, size=self.nemesis_tile.size,
164                     texture=self.nemesis.get_texture())
165         self.add_widget(self.nemesis_tile)
166
167     def _closed(self):
168         self.keyboard.unbind(on_key_down=self._on_key_down)
169
170     def _on_key_down(self, keyboard, keycode, text, modifiers):
171         direction = None
172         letter = keycode[1].lower()
173         if letter in UP:
174             direction = (0, 1)
175         elif letter in DOWN:
176             direction = (0, -1)
177         elif letter in LEFT:
178             direction = (-1, 0)
179         elif letter in RIGHT:
180             direction = (1, 0)
181         if direction:
182             self.do_move(direction)
183
184     def do_move(self, direction):
185         if not self.level_obj:
186             return
187         # Do nothing on null moves
188         if not self.player.move(direction, self.level_obj):
189             return
190         self.check_state()
191         self.do_nemesis_move()
192
193     def do_nemesis_move(self):
194         self.nemesis.move(self.level_obj, self.player.pos)
195         self.move_counter += 1
196         self.check_state()
197         self.reset_timer()
198         self.draw_nemesis()
199         self.draw_player()
200
201     def timed_move(self, event):
202         if not self.level_obj:
203             return
204         self.do_nemesis_move()
205
206     def reset_timer(self):
207         self.timer_set = True
208         Clock.unschedule(self.timed_move)
209         if self.move_counter > 4:
210             self.move_counter = 0
211             # The superhero is faster than the player
212             Clock.schedule_once(self.timed_move, 0.02)
213         else:
214             Clock.schedule_once(self.timed_move, 1)
215
216     def check_caught(self):
217         return self.nemesis.pos == self.player.pos
218
219     def reset_level(self):
220         Clock.unschedule(self.timed_move)
221         self.timer_set = False
222         self.move_counter = 0
223         if self.nemesis_tile:
224             self.remove_widget(self.nemesis_tile)
225         self.nemesis.reset_pos()
226
227     def load_level(self):
228         if self.level_obj:
229             self.level_obj.load_tiles()
230             self.player.pos = self.level_obj.enter_pos
231             self.remove_widget(self.player_tile)
232             self.view.scroll_x = 0
233             self.view.scroll_y = 0
234             self.build()
235             self.draw_nemesis()
236             self.draw_player()
237             return True
238         return False
239
240     def check_state(self):
241         if not self.level_obj:
242             return
243         if self.level_obj.at_exit(self.player.pos):
244             self.reset_level()
245             # Jump to next level
246             self.level_obj = self.level_list.advance_to_next_level()
247             if not self.load_level():
248                 self.app.game_over(True)
249             return
250         elif self.check_caught():
251             # Caught
252             self.reset_level()
253             self.load_level()
254             self.app.game_over(False)
255             return
256         elif self.level_obj.is_button(self.player.pos):
257             self.level_obj.trigger_button(self.player.pos)
258         elif self.level_obj.is_button(self.nemesis.pos):
259             self.level_obj.trigger_button(self.nemesis.pos)
260         for map_pos, new_tile in self.level_obj.get_changed_tiles():
261             pos = (map_pos[0] * TILE_SIZE, map_pos[1] * TILE_SIZE)
262             self.draw_tile(pos, new_tile)
263
264     def _calc_mouse_pos(self, pos):
265         pos = self.to_local(*pos)
266         return (int(pos[0] / TILE_SIZE), int(pos[1] / TILE_SIZE))
267
268     def on_touch_down(self, touch):
269         pos = self._calc_mouse_pos(touch.pos)
270         if pos == self.player.pos:
271             self.mouse_move = True
272             self.mouse_start = pos
273
274     def on_touch_up(self, touch):
275         self.mouse_move = False
276
277     def on_touch_move(self, touch):
278         if self.mouse_move:
279             pos = self._calc_mouse_pos(touch.pos)
280             if (pos[0] - self.mouse_start[0] != 0) or (
281                     pos[1] - self.mouse_start[1] != 0):
282                 direction = (pos[0] - self.mouse_start[0],
283                         pos[1] - self.mouse_start[1])
284                 self.do_move(direction)
285                 self.mouse_start = pos
286
287
288 class Screen(Widget):
289
290     BACKGROUND = None
291     START = 'Start'
292
293     def __init__(self, app):
294         super(Screen, self).__init__()
295         self.image = load_image(self.BACKGROUND)
296         self.app = app
297         with self.canvas:
298             Rectangle(pos=(0, 0), size=(1026, 760),
299                     texture=self.image.texture)
300
301         self.stop_button = Label(
302                 text='[ref=quit][color=ff0066]Quit[/color][/ref]',
303                 font_size=30,
304                 markup=True,
305                 size=(200, 40),
306                 pos=((1026 - 200) / 2 - 100, 100))
307         self.stop_button.bind(on_ref_press=self.app.stop_app)
308         self.start_button = Label(
309                 text="[ref=start][color=00ff66]%s[/color][/ref]" % self.START,
310                 font_size=30,
311                 markup=True, size=(200, 40),
312                 pos=((1026 - 200) / 2 + 100, 100))
313         self.start_button.bind(on_ref_press=self.app.start_game)
314         self.add_widget(self.stop_button)
315         self.add_widget(self.start_button)
316
317
318 class IntroScreen(Screen):
319
320     BACKGROUND = 'screens/intro_screen.png'
321     START = 'Start the Game'
322
323
324 class WonScreen(Screen):
325
326     BACKGROUND = 'screens/won.png'
327     START = 'Play again?'
328
329
330 class LostScreen(Screen):
331
332     BACKGROUND = 'screens/lost.png'
333     START = 'Retry?'
334
335
336 class GameApp(App):
337
338     title = "Peter's thread snake"
339
340     def __init__(self):
341         super(GameApp, self).__init__()
342         self.levels = LevelList()
343
344     def build(self):
345         root = ScrollView(size_hint=(None, None))
346         return root
347
348     def on_start(self):
349         from kivy.base import EventLoop
350         window = EventLoop.window
351         if platform() == 'android':
352             window.fullscreen = True
353         self.root.size = window.size
354         errors = self.levels.get_errors()
355         if errors:
356             popup = Popup(title='Levels excluded',
357                     content=Label(text='\n'.join(errors)),
358                     size_hint=(.5, .5))
359             popup.open()
360         self.make_intro()
361
362     def make_intro(self):
363         self.root.clear_widgets()
364         screen = IntroScreen(self)
365         self.root.add_widget(screen)
366
367     def stop_app(self, label, ref):
368         self.stop()
369
370     def start_game(self, label, ref):
371         """Start the game"""
372         game = GameWindow(self.levels, self)
373         game.build()
374         self.root.clear_widgets()
375         self.root.add_widget(game)
376         # Ensure the player is visible
377         self.root.scroll_x = 0
378         self.root.scroll_y = 0
379         game.draw_player()
380         game.draw_nemesis()
381
382     def game_over(self, won):
383         if won:
384             screen = WonScreen(self)
385             self.levels.reset()
386         else:
387             screen = LostScreen(self)
388         self.root.clear_widgets()
389         self.root.add_widget(screen)
390
391
392 def main():
393     """ Erdslangetjie, a maze game of eluding nemesis
394     """
395     GameApp().run()