74a0c33a2509ac939988175ea4de3624d6a033f2
[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                 node = Widget(size=(TILE_SIZE, TILE_SIZE),
71                         pos=(bx, by),
72                         size_hint=(None, None))
73                 self.add_widget(node)
74                 with node.canvas:
75                     Color(1, 1, 1)
76                     Rectangle(pos=node.pos, size=node.size,
77                             texture=tile.texture)
78                 self.tiles[(bx, by)] = node
79                 bx += TILE_SIZE
80             by += TILE_SIZE
81
82     def draw_player(self):
83         if self.player_tile:
84             self.remove_widget(self.player_tile)
85         sprite_pos = (self.player.pos[0] * TILE_SIZE,
86                 self.player.pos[1] * TILE_SIZE)
87         self.player_tile = Widget(size=(TILE_SIZE, TILE_SIZE),
88                 pos=sprite_pos)
89         with self.player_tile.canvas:
90             Color(1, 1, 1)
91             Rectangle(pos=sprite_pos, size=self.player_tile.size,
92                     texture=self.player.get_texture())
93         self.add_widget(self.player_tile)
94         for offset in [(TILE_SIZE - 1, TILE_SIZE - 1),
95                 (-TILE_SIZE + 1, TILE_SIZE - 1),
96                 (TILE_SIZE - 1, -TILE_SIZE + 1),
97                 (-TILE_SIZE + 1, -TILE_SIZE + 1),
98                 (0, 2 * TILE_SIZE - 2),
99                 (-2 * TILE_SIZE + 2, 0),
100                 (2 * TILE_SIZE - 2, 0),
101                 (0, -2 * TILE_SIZE + 2),
102                 (0, 0)]:
103             # Aim is to ensure a 'neighbourhood' around the player
104             # is visible if possible
105             check_point = (sprite_pos[0] + offset[0] + TILE_SIZE / 2,
106                     sprite_pos[1] + offset[1] + TILE_SIZE / 2)
107             true_point = self.to_parent(*check_point)
108             if check_point[0] < 0:
109                 continue
110             if check_point[1] < 0:
111                 continue
112             if check_point[0] >= self.size[0]:
113                 continue
114             if check_point[1] >= self.size[1]:
115                 continue
116             while not self.included(true_point, 0):
117                 # Scroll ourselves
118                 if true_point[0] >= self.view.size[0]:
119                     self.view.scroll_x += self.x_scroll_margin
120                     true_point = self.to_parent(*check_point)
121                     #print '-x', self.view.scroll_x, self.view.scroll_y
122                 elif true_point[0] < 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[1] >= self.view.size[1]:
127                     self.view.scroll_y += self.y_scroll_margin
128                     true_point = self.to_parent(*check_point)
129                     #print '+y', self.view.scroll_x, self.view.scroll_y
130                 elif true_point[1] < 0:
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                 #print true_point, self.view.size
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         self.player.move(direction, self.level_obj)
183         self.draw_player()
184         self.check_state()
185         if not self.timer_set:
186             self.reset_timer()
187
188     def timed_move(self, event):
189         if not self.level_obj:
190             return
191         self.nemesis.move(self.level_obj, self.check_caught)
192         self.draw_nemesis()
193         self.check_state()
194         self.reset_timer()
195
196     def reset_timer(self):
197         self.timer_set = True
198         Clock.unschedule(self.timed_move)
199         Clock.schedule_once(self.timed_move, 0.5)
200
201     def check_caught(self):
202         return self.nemesis.pos == self.player.pos
203
204     def reset_level(self):
205         Clock.unschedule(self.timed_move)
206         self.timer_set = False
207         if self.nemesis_tile:
208             self.remove_widget(self.nemesis_tile)
209         self.nemesis.reset_pos()
210         if self.level_obj:
211             self.level_obj.load_tiles()
212             self.player.pos = self.level_obj.enter_pos
213             self.remove_widget(self.player_tile)
214             self.view.scroll_x = 0
215             self.view.scroll_y = 0
216             self.build()
217             self.draw_nemesis()
218             self.draw_player()
219             return True
220         return False
221
222     def check_state(self):
223         if self.level_obj.at_exit(self.player.pos):
224             # Jump to next level
225             self.level_obj = self.level_list.advance_to_next_level()
226             if not self.reset_level():
227                 self.app.game_over(True)
228         elif self.check_caught():
229             # Caught
230             self.app.game_over(False)
231
232     def _calc_mouse_pos(self, pos):
233         pos = self.to_local(*pos)
234         return (int(pos[0] / TILE_SIZE), int(pos[1] / TILE_SIZE))
235
236     def on_touch_down(self, touch):
237         pos = self._calc_mouse_pos(touch.pos)
238         if pos == self.player.pos:
239             self.mouse_move = True
240             self.mouse_start = pos
241
242     def on_touch_up(self, touch):
243         self.mouse_move = False
244
245     def on_touch_move(self, touch):
246         if self.mouse_move:
247             pos = self._calc_mouse_pos(touch.pos)
248             if (pos[0] - self.mouse_start[0] != 0) or (
249                     pos[1] - self.mouse_start[1] != 0):
250                 direction = (pos[0] - self.mouse_start[0],
251                         pos[1] - self.mouse_start[1])
252                 self.do_move(direction)
253                 self.mouse_start = pos
254
255
256 class Screen(Widget):
257
258     BACKGROUND = None
259     START = 'Start'
260
261     def __init__(self, app):
262         super(Screen, self).__init__()
263         self.image = load_image(self.BACKGROUND)
264         self.app = app
265         with self.canvas:
266             Rectangle(pos=(0, 0), size=(1026, 760),
267                     texture=self.image.texture)
268
269         self.stop_button = Label(
270                 text='[ref=quit][color=ff0066]Quit[/color][/ref]',
271                 font_size=30,
272                 markup=True,
273                 size=(200, 40),
274                 pos=((1026 - 200) / 2 - 100, 100))
275         self.stop_button.bind(on_ref_press=self.app.stop_app)
276         self.start_button = Label(
277                 text="[ref=start][color=00ff66]%s[/color][/ref]" % self.START,
278                 font_size=30,
279                 markup=True, size=(200, 40),
280                 pos=((1026 - 200) / 2 + 100, 100))
281         self.start_button.bind(on_ref_press=self.app.start_game)
282         self.add_widget(self.stop_button)
283         self.add_widget(self.start_button)
284
285
286 class IntroScreen(Screen):
287
288     BACKGROUND = 'screens/intro_screen.png'
289     START = 'Start the Game'
290
291
292 class WonScreen(Screen):
293
294     BACKGROUND = 'screens/won.png'
295     START = 'Play again?'
296
297
298 class LostScreen(Screen):
299
300     BACKGROUND = 'screens/lost.png'
301     START = 'Retry?'
302
303
304 class GameApp(App):
305
306     title = "Peter's thread snake"
307
308     def __init__(self):
309         super(GameApp, self).__init__()
310         self.levels = LevelList()
311
312     def build(self):
313         root = ScrollView(size_hint=(None, None))
314         return root
315
316     def on_start(self):
317         from kivy.base import EventLoop
318         window = EventLoop.window
319         if platform() == 'android':
320             window.fullscreen = True
321         self.root.size = window.size
322         errors = self.levels.get_errors()
323         if errors:
324             popup = Popup(title='Levels excluded',
325                     content=Label(text='\n'.join(errors)),
326                     size_hint=(.5, .5))
327             popup.open()
328         self.make_intro()
329
330     def make_intro(self):
331         self.root.clear_widgets()
332         screen = IntroScreen(self)
333         self.root.add_widget(screen)
334
335     def stop_app(self, label, ref):
336         self.stop()
337
338     def start_game(self, label, ref):
339         """Start the game"""
340         game = GameWindow(self.levels, self)
341         game.build()
342         self.root.clear_widgets()
343         self.root.add_widget(game)
344         # Ensure the player is visible
345         self.root.scroll_x = 0
346         self.root.scroll_y = 0
347         game.draw_player()
348         game.draw_nemesis()
349
350     def game_over(self, won):
351         if won:
352             screen = WonScreen(self)
353             self.levels.reset()
354         else:
355             screen = LostScreen(self)
356         self.root.clear_widgets()
357         self.root.add_widget(screen)
358
359
360 def main():
361     """ Erdslangetjie, a maze game of eluding nemesis
362     """
363     GameApp().run()