Make buttons trigger
[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         self.player.move(direction, self.level_obj)
187         self.check_state()
188         self.do_nemesis_move()
189
190     def do_nemesis_move(self):
191         self.nemesis.move(self.level_obj)
192         self.check_state()
193         self.reset_timer()
194         self.draw_nemesis()
195         self.draw_player()
196
197     def timed_move(self, event):
198         if not self.level_obj:
199             return
200         self.do_nemesis_move()
201
202     def reset_timer(self):
203         self.timer_set = True
204         Clock.unschedule(self.timed_move)
205         Clock.schedule_once(self.timed_move, 0.5)
206
207     def check_caught(self):
208         return self.nemesis.pos == self.player.pos
209
210     def reset_level(self):
211         Clock.unschedule(self.timed_move)
212         self.timer_set = False
213         if self.nemesis_tile:
214             self.remove_widget(self.nemesis_tile)
215         self.nemesis.reset_pos()
216         if self.level_obj:
217             self.level_obj.load_tiles()
218             self.player.pos = self.level_obj.enter_pos
219             self.remove_widget(self.player_tile)
220             self.view.scroll_x = 0
221             self.view.scroll_y = 0
222             self.build()
223             self.draw_nemesis()
224             self.draw_player()
225             return True
226         return False
227
228     def check_state(self):
229         if not self.level_obj:
230             return
231         if self.level_obj.at_exit(self.player.pos):
232             # Jump to next level
233             self.level_obj = self.level_list.advance_to_next_level()
234             if not self.reset_level():
235                 self.app.game_over(True)
236             return
237         elif self.check_caught():
238             # Caught
239             self.app.game_over(False)
240             return
241         elif self.level_obj.is_button(self.player.pos):
242             self.level_obj.trigger_button(self.player.pos)
243         elif self.level_obj.is_button(self.nemesis.pos):
244             self.level_obj.trigger_button(self.nemesis.pos)
245         for map_pos, new_tile in self.level_obj.get_changed_tiles():
246             pos = (map_pos[0] * TILE_SIZE, map_pos[1] * TILE_SIZE)
247             self.draw_tile(pos, new_tile)
248
249     def _calc_mouse_pos(self, pos):
250         pos = self.to_local(*pos)
251         return (int(pos[0] / TILE_SIZE), int(pos[1] / TILE_SIZE))
252
253     def on_touch_down(self, touch):
254         pos = self._calc_mouse_pos(touch.pos)
255         if pos == self.player.pos:
256             self.mouse_move = True
257             self.mouse_start = pos
258
259     def on_touch_up(self, touch):
260         self.mouse_move = False
261
262     def on_touch_move(self, touch):
263         if self.mouse_move:
264             pos = self._calc_mouse_pos(touch.pos)
265             if (pos[0] - self.mouse_start[0] != 0) or (
266                     pos[1] - self.mouse_start[1] != 0):
267                 direction = (pos[0] - self.mouse_start[0],
268                         pos[1] - self.mouse_start[1])
269                 self.do_move(direction)
270                 self.mouse_start = pos
271
272
273 class Screen(Widget):
274
275     BACKGROUND = None
276     START = 'Start'
277
278     def __init__(self, app):
279         super(Screen, self).__init__()
280         self.image = load_image(self.BACKGROUND)
281         self.app = app
282         with self.canvas:
283             Rectangle(pos=(0, 0), size=(1026, 760),
284                     texture=self.image.texture)
285
286         self.stop_button = Label(
287                 text='[ref=quit][color=ff0066]Quit[/color][/ref]',
288                 font_size=30,
289                 markup=True,
290                 size=(200, 40),
291                 pos=((1026 - 200) / 2 - 100, 100))
292         self.stop_button.bind(on_ref_press=self.app.stop_app)
293         self.start_button = Label(
294                 text="[ref=start][color=00ff66]%s[/color][/ref]" % self.START,
295                 font_size=30,
296                 markup=True, size=(200, 40),
297                 pos=((1026 - 200) / 2 + 100, 100))
298         self.start_button.bind(on_ref_press=self.app.start_game)
299         self.add_widget(self.stop_button)
300         self.add_widget(self.start_button)
301
302
303 class IntroScreen(Screen):
304
305     BACKGROUND = 'screens/intro_screen.png'
306     START = 'Start the Game'
307
308
309 class WonScreen(Screen):
310
311     BACKGROUND = 'screens/won.png'
312     START = 'Play again?'
313
314
315 class LostScreen(Screen):
316
317     BACKGROUND = 'screens/lost.png'
318     START = 'Retry?'
319
320
321 class GameApp(App):
322
323     title = "Peter's thread snake"
324
325     def __init__(self):
326         super(GameApp, self).__init__()
327         self.levels = LevelList()
328
329     def build(self):
330         root = ScrollView(size_hint=(None, None))
331         return root
332
333     def on_start(self):
334         from kivy.base import EventLoop
335         window = EventLoop.window
336         if platform() == 'android':
337             window.fullscreen = True
338         self.root.size = window.size
339         errors = self.levels.get_errors()
340         if errors:
341             popup = Popup(title='Levels excluded',
342                     content=Label(text='\n'.join(errors)),
343                     size_hint=(.5, .5))
344             popup.open()
345         self.make_intro()
346
347     def make_intro(self):
348         self.root.clear_widgets()
349         screen = IntroScreen(self)
350         self.root.add_widget(screen)
351
352     def stop_app(self, label, ref):
353         self.stop()
354
355     def start_game(self, label, ref):
356         """Start the game"""
357         game = GameWindow(self.levels, self)
358         game.build()
359         self.root.clear_widgets()
360         self.root.add_widget(game)
361         # Ensure the player is visible
362         self.root.scroll_x = 0
363         self.root.scroll_y = 0
364         game.draw_player()
365         game.draw_nemesis()
366
367     def game_over(self, won):
368         if won:
369             screen = WonScreen(self)
370             self.levels.reset()
371         else:
372             screen = LostScreen(self)
373         self.root.clear_widgets()
374         self.root.add_widget(screen)
375
376
377 def main():
378     """ Erdslangetjie, a maze game of eluding nemesis
379     """
380     GameApp().run()