Hook up very, very stupid nemesis movement for testing logic
[erdslangetjie.git] / erdslangetjie / __main__.py
1 import pygame
2
3 from erdslangetjie.constants import TILE_SIZE
4
5 from kivy.app import App
6 from kivy.uix.widget import Widget
7 from kivy.uix.relativelayout import RelativeLayout
8 from kivy.uix.scrollview import ScrollView
9 from kivy.uix.label import Label
10 from kivy.graphics import Color, Rectangle
11 from kivy.utils import platform
12 from kivy.clock import Clock
13
14 from erdslangetjie.level import LevelList
15 from erdslangetjie.data import load_image
16 from erdslangetjie.player import ThePlayer, Nemesis
17
18
19 class GameWindow(RelativeLayout):
20
21     def __init__(self, level_list, app):
22         self.level_list = level_list
23         self.level_obj = self.level_list.get_current_level()
24         self.level_obj.load_tiles()
25         self.tiles = {}
26         self.view = app.root
27         self.app = app
28
29         cols, rows = self.level_obj.get_size()
30
31         super(GameWindow, self).__init__(
32                 size=(cols * TILE_SIZE, rows * TILE_SIZE),
33                 size_hint=(None, None))
34
35         self.x_scroll_margin = float(TILE_SIZE) / self.view.size[0]
36         self.y_scroll_margin = float(TILE_SIZE) / self.view.size[1]
37
38         self.mouse_move = False
39
40         self.player = ThePlayer()
41         self.nemesis = Nemesis()
42         if not self.level_obj.enter_pos:
43             raise RuntimeError('No entry point')
44         self.player_tile = None
45         self.nemesis_tile = None
46         self.timer_set = False
47
48         self.player.pos = self.level_obj.enter_pos
49         if platform() != 'android':
50             # Very hack'ish
51             # We need to delay this import until after the window creation by
52             # the app, else our size config doesn't work
53             from kivy.core.window import Window
54             self.keyboard = Window.request_keyboard(self._closed, self)
55             self.keyboard.bind(on_key_down=self._on_key_down)
56
57     def build(self):
58         self.clear_widgets()
59         self.tiles = {}
60         tiles = self.level_obj.get_tiles()
61         bx, by = 0, 0
62         for tile_line in tiles:
63             bx = 0
64             for tile in tile_line:
65                 node = Widget(size=(TILE_SIZE, TILE_SIZE),
66                         pos=(bx, by),
67                         size_hint=(None, None))
68                 self.add_widget(node)
69                 with node.canvas:
70                     Color(1, 1, 1)
71                     Rectangle(pos=node.pos, size=node.size,
72                             texture=tile.texture)
73                 self.tiles[(bx, by)] = node
74                 bx += TILE_SIZE
75             by += TILE_SIZE
76
77     def draw_player(self):
78         if self.player_tile:
79             self.remove_widget(self.player_tile)
80         sprite_pos = (self.player.pos[0] * TILE_SIZE,
81                 self.player.pos[1] * TILE_SIZE)
82         self.player_tile = Widget(size=(TILE_SIZE, TILE_SIZE),
83                 pos=sprite_pos)
84         with self.player_tile.canvas:
85             Color(1, 1, 1)
86             Rectangle(pos=sprite_pos, size=self.player_tile.size,
87                     texture=self.player.get_texture())
88         self.add_widget(self.player_tile)
89         for offset in [(TILE_SIZE - 1, TILE_SIZE - 1),
90                 (-TILE_SIZE + 1, TILE_SIZE - 1),
91                 (TILE_SIZE - 1, -TILE_SIZE + 1),
92                 (-TILE_SIZE + 1, -TILE_SIZE + 1),
93                 (0, 2 * TILE_SIZE - 2),
94                 (-2 * TILE_SIZE + 2, 0),
95                 (2 * TILE_SIZE - 2, 0),
96                 (0, -2 * TILE_SIZE + 2),
97                 (0, 0)]:
98             # Aim is to ensure a 'neighbourhood' around the player
99             # is visible if possible
100             check_point = (sprite_pos[0] + offset[0] + TILE_SIZE / 2,
101                     sprite_pos[1] + offset[1] + TILE_SIZE / 2)
102             true_point = self.to_parent(*check_point)
103             if check_point[0] < 0:
104                 continue
105             if check_point[1] < 0:
106                 continue
107             if check_point[0] >= self.size[0]:
108                 continue
109             if check_point[1] >= self.size[1]:
110                 continue
111             while not self.included(true_point, 0):
112                 # Scroll ourselves
113                 if true_point[0] >= self.view.size[0]:
114                     self.view.scroll_x += self.x_scroll_margin
115                     true_point = self.to_parent(*check_point)
116                     #print '-x', self.view.scroll_x, self.view.scroll_y
117                 elif true_point[0] < 0:
118                     self.view.scroll_x -= self.x_scroll_margin
119                     true_point = self.to_parent(*check_point)
120                     #print '+x', self.view.scroll_x, self.view.scroll_y
121                 elif true_point[1] >= self.view.size[1]:
122                     self.view.scroll_y += self.y_scroll_margin
123                     true_point = self.to_parent(*check_point)
124                     #print '+y', self.view.scroll_x, self.view.scroll_y
125                 elif true_point[1] < 0:
126                     self.view.scroll_y -= self.y_scroll_margin
127                     true_point = self.to_parent(*check_point)
128                     #print '-y', self.view.scroll_x, self.view.scroll_y
129                 #print true_point, self.view.size
130
131     def included(self, point, margin):
132         if point[0] < margin:
133             return False
134         if point[0] >= self.view.size[0] - margin:
135             return False
136         if point[1] < margin:
137             return False
138         if point[1] >= self.view.size[1] - margin:
139             return False
140         return True
141
142     def draw_nemesis(self):
143         if not self.nemesis.on_board():
144             return
145         if self.nemesis_tile:
146             self.remove_widget(self.nemesis_tile)
147         sprite_pos = (self.nemesis.pos[0] * TILE_SIZE,
148                 self.nemesis.pos[1] * TILE_SIZE)
149         self.nemesis_tile = Widget(size=(TILE_SIZE, TILE_SIZE),
150                 pos=sprite_pos)
151         with self.nemesis_tile.canvas:
152             Color(1, 1, 1)
153             Rectangle(pos=sprite_pos, size=self.nemesis_tile.size,
154                     texture=self.nemesis.get_texture())
155         self.add_widget(self.nemesis_tile)
156
157     def _closed(self):
158         self.keyboard.unbind(on_key_down=self._on_key_down)
159
160     def _on_key_down(self, keyboard, keycode, text, modifiers):
161         # FIXME - likely portablity issues
162         direction = None
163         if keycode[0] == pygame.K_UP:
164             direction = (0, 1)
165         elif keycode[0] == pygame.K_DOWN:
166             direction = (0, -1)
167         elif keycode[0] == pygame.K_LEFT:
168             direction = (-1, 0)
169         elif keycode[0] == pygame.K_RIGHT:
170             direction = (1, 0)
171         if direction:
172             self.do_move(direction)
173
174     def do_move(self, direction):
175         if not self.level_obj:
176             return
177         self.player.move(direction, self.level_obj)
178         self.draw_player()
179         self.check_state()
180         if not self.timer_set:
181             self.reset_timer()
182
183     def timed_move(self, event):
184         if not self.level_obj:
185             return
186         self.nemesis.move(self.level_obj, self.check_caught)
187         self.draw_nemesis()
188         self.check_state()
189         self.reset_timer()
190
191     def reset_timer(self):
192         self.timer_set = True
193         Clock.unschedule(self.timed_move)
194         Clock.schedule_once(self.timed_move, 0.5)
195
196     def check_caught(self):
197         return self.nemesis.pos == self.player.pos
198
199     def reset_level(self):
200         Clock.unschedule(self.timed_move)
201         self.timer_set = False
202         self.remove_widget(self.nemesis_tile)
203         self.nemesis.reset_pos()
204         if self.level_obj:
205             self.level_obj.load_tiles()
206             self.player.pos = self.level_obj.enter_pos
207             self.remove_widget(self.player_tile)
208             self.view.scroll_x = 0
209             self.view.scroll_y = 0
210             self.build()
211             self.draw_nemesis()
212             self.draw_player()
213             return True
214         return False
215
216     def check_state(self):
217         if self.level_obj.at_exit(self.player.pos):
218             # Jump to next level
219             self.level_obj = self.level_list.advance_to_next_level()
220             if not self.reset_level():
221                 self.app.game_over(True)
222         elif self.check_caught():
223             # Caught
224             self.app.game_over(False)
225
226     def _calc_mouse_pos(self, pos):
227         pos = self.to_local(*pos)
228         return (int(pos[0] / TILE_SIZE), int(pos[1] / TILE_SIZE))
229
230     def on_touch_down(self, touch):
231         pos = self._calc_mouse_pos(touch.pos)
232         if pos == self.player.pos:
233             self.mouse_move = True
234             self.mouse_start = pos
235
236     def on_touch_up(self, touch):
237         self.mouse_move = False
238
239     def on_touch_move(self, touch):
240         if self.mouse_move:
241             pos = self._calc_mouse_pos(touch.pos)
242             if (pos[0] - self.mouse_start[0] != 0) or (
243                     pos[1] - self.mouse_start[1] != 0):
244                 direction = (pos[0] - self.mouse_start[0],
245                         pos[1] - self.mouse_start[1])
246                 self.do_move(direction)
247                 self.mouse_start = pos
248
249
250 class Screen(Widget):
251
252     BACKGROUND = None
253     START = 'Start'
254
255     def __init__(self, app):
256         super(Screen, self).__init__()
257         self.image = load_image(self.BACKGROUND)
258         self.app = app
259         with self.canvas:
260             Rectangle(pos=(0, 0), size=(1026, 760),
261                     texture=self.image.texture)
262
263         self.stop_button = Label(
264                 text='[ref=quit][color=ff0066]Quit[/color][/ref]',
265                 font_size=30,
266                 markup=True,
267                 size=(200, 40),
268                 pos=((1026 - 200) / 2 - 100, 100))
269         self.stop_button.bind(on_ref_press=self.app.stop_app)
270         self.start_button = Label(
271                 text="[ref=start][color=00ff66]%s[/color][/ref]" % self.START,
272                 font_size=30,
273                 markup=True, size=(200, 40),
274                 pos=((1026 - 200) / 2 + 100, 100))
275         self.start_button.bind(on_ref_press=self.app.start_game)
276         self.add_widget(self.stop_button)
277         self.add_widget(self.start_button)
278
279
280 class IntroScreen(Screen):
281
282     BACKGROUND = 'screens/intro_screen.png'
283     START = 'Start the Game'
284
285
286 class WonScreen(Screen):
287
288     BACKGROUND = 'screens/won.png'
289     START = 'Play again?'
290
291
292 class LostScreen(Screen):
293
294     BACKGROUND = 'screens/lost.png'
295     START = 'Retry?'
296
297
298 class GameApp(App):
299
300     title = "Peter's thread snake"
301
302     def __init__(self):
303         self.levels = LevelList()
304         super(GameApp, self).__init__()
305
306     def build(self):
307         root = ScrollView(size_hint=(None, None))
308         return root
309
310     def on_start(self):
311         from kivy.base import EventLoop
312         window = EventLoop.window
313         if platform() == 'android':
314             window.fullscreen = True
315         self.root.size = window.size
316         self.make_intro()
317
318     def make_intro(self):
319         self.root.clear_widgets()
320         screen = IntroScreen(self)
321         self.root.add_widget(screen)
322
323     def stop_app(self, label, ref):
324         self.stop()
325
326     def start_game(self, label, ref):
327         """Start the game"""
328         game = GameWindow(self.levels, self)
329         game.build()
330         self.root.clear_widgets()
331         self.root.add_widget(game)
332         # Ensure the player is visible
333         self.root.scroll_x = 0
334         self.root.scroll_y = 0
335         game.draw_player()
336         game.draw_nemesis()
337
338     def game_over(self, won):
339         if won:
340             screen = WonScreen(self)
341             self.levels.reset()
342         else:
343             screen = LostScreen(self)
344         self.root.clear_widgets()
345         self.root.add_widget(screen)
346
347
348 def main():
349     """ Erdslangetjie, a maze game of eluding nemesis
350     """
351     GameApp().run()