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