Editor screenshot
[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 from kivy.config import Config
14
15 from erdslangetjie.level import LevelList
16 from erdslangetjie.data import load_image
17 from erdslangetjie.player import ThePlayer, Nemesis
18
19
20 if platform() != 'android':
21     Config.set('graphics', 'width', '1026')
22     Config.set('graphics', 'height', '760')
23
24
25 class GameWindow(RelativeLayout):
26
27     def __init__(self, level_list, app):
28         self.level_list = level_list
29         self.level_obj = self.level_list.get_current_level()
30         self.level_obj.load_tiles()
31         self.tiles = {}
32         self.view = app.root
33         self.app = app
34
35         cols, rows = self.level_obj.get_size()
36
37         super(GameWindow, self).__init__(
38                 size=(cols * TILE_SIZE, rows * TILE_SIZE),
39                 size_hint=(None, None))
40
41         self.x_scroll_margin = float(TILE_SIZE) / self.view.size[0]
42         self.y_scroll_margin = float(TILE_SIZE) / self.view.size[1]
43
44         self.mouse_move = False
45
46         self.player = ThePlayer()
47         self.nemesis = Nemesis()
48         if not self.level_obj.enter_pos:
49             raise RuntimeError('No entry point')
50         self.player_tile = None
51         self.nemesis_tile = None
52         self.timer_set = False
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                 node = Widget(size=(TILE_SIZE, TILE_SIZE),
72                         pos=(bx, by),
73                         size_hint=(None, None))
74                 self.add_widget(node)
75                 with node.canvas:
76                     Color(1, 1, 1)
77                     Rectangle(pos=node.pos, size=node.size,
78                             texture=tile.texture)
79                 self.tiles[(bx, by)] = node
80                 bx += TILE_SIZE
81             by += TILE_SIZE
82
83     def draw_player(self):
84         if self.player_tile:
85             self.remove_widget(self.player_tile)
86         sprite_pos = (self.player.pos[0] * TILE_SIZE,
87                 self.player.pos[1] * TILE_SIZE)
88         self.player_tile = Widget(size=(TILE_SIZE, TILE_SIZE),
89                 pos=sprite_pos)
90         with self.player_tile.canvas:
91             Color(1, 1, 1)
92             Rectangle(pos=sprite_pos, size=self.player_tile.size,
93                     texture=self.player.get_texture())
94         self.add_widget(self.player_tile)
95         for offset in [(TILE_SIZE - 1, TILE_SIZE - 1),
96                 (-TILE_SIZE + 1, TILE_SIZE - 1),
97                 (TILE_SIZE - 1, -TILE_SIZE + 1),
98                 (-TILE_SIZE + 1, -TILE_SIZE + 1),
99                 (0, 2 * TILE_SIZE - 2),
100                 (-2 * TILE_SIZE + 2, 0),
101                 (2 * TILE_SIZE - 2, 0),
102                 (0, -2 * TILE_SIZE + 2),
103                 (0, 0)]:
104             # Aim is to ensure a 'neighbourhood' around the player
105             # is visible if possible
106             check_point = (sprite_pos[0] + offset[0] + TILE_SIZE / 2,
107                     sprite_pos[1] + offset[1] + TILE_SIZE / 2)
108             true_point = self.to_parent(*check_point)
109             if check_point[0] < 0:
110                 continue
111             if check_point[1] < 0:
112                 continue
113             if check_point[0] >= self.size[0]:
114                 continue
115             if check_point[1] >= self.size[1]:
116                 continue
117             while not self.included(true_point, 0):
118                 # Scroll ourselves
119                 if true_point[0] >= self.view.size[0]:
120                     self.view.scroll_x += self.x_scroll_margin
121                     true_point = self.to_parent(*check_point)
122                     #print '-x', self.view.scroll_x, self.view.scroll_y
123                 elif true_point[0] < 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[1] >= self.view.size[1]:
128                     self.view.scroll_y += self.y_scroll_margin
129                     true_point = self.to_parent(*check_point)
130                     #print '+y', self.view.scroll_x, self.view.scroll_y
131                 elif true_point[1] < 0:
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                 #print true_point, self.view.size
136
137     def included(self, point, margin):
138         if point[0] < margin:
139             return False
140         if point[0] >= self.view.size[0] - margin:
141             return False
142         if point[1] < margin:
143             return False
144         if point[1] >= self.view.size[1] - margin:
145             return False
146         return True
147
148     def draw_nemesis(self):
149         if not self.nemesis.on_board():
150             return
151         if self.nemesis_tile:
152             self.remove_widget(self.nemesis_tile)
153         sprite_pos = (self.nemesis.pos[0] * TILE_SIZE,
154                 self.nemesis.pos[1] * TILE_SIZE)
155         self.nemesis_tile = Widget(size=(TILE_SIZE, TILE_SIZE),
156                 pos=sprite_pos)
157         with self.nemesis_tile.canvas:
158             Color(1, 1, 1)
159             Rectangle(pos=sprite_pos, size=self.nemesis_tile.size,
160                     texture=self.nemesis.get_texture())
161         self.add_widget(self.nemesis_tile)
162
163     def _closed(self):
164         self.keyboard.unbind(on_key_down=self._on_key_down)
165
166     def _on_key_down(self, keyboard, keycode, text, modifiers):
167         # FIXME - likely portablity issues
168         direction = None
169         if keycode[0] == pygame.K_UP:
170             direction = (0, 1)
171         elif keycode[0] == pygame.K_DOWN:
172             direction = (0, -1)
173         elif keycode[0] == pygame.K_LEFT:
174             direction = (-1, 0)
175         elif keycode[0] == pygame.K_RIGHT:
176             direction = (1, 0)
177         if direction:
178             self.do_move(direction)
179
180     def do_move(self, direction):
181         if not self.level_obj:
182             return
183         self.player.move(direction, self.level_obj)
184         self.draw_player()
185         self.check_state()
186         if not self.timer_set:
187             self.reset_timer()
188
189     def timed_move(self, event):
190         if not self.level_obj:
191             return
192         self.nemesis.move(self.level_obj, self.check_caught)
193         self.draw_nemesis()
194         self.check_state()
195         self.reset_timer()
196
197     def reset_timer(self):
198         self.timer_set = True
199         Clock.unschedule(self.timed_move)
200         Clock.schedule_once(self.timed_move, 0.5)
201
202     def check_caught(self):
203         return self.nemesis.pos == self.player.pos
204
205     def reset_level(self):
206         Clock.unschedule(self.timed_move)
207         self.timer_set = False
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         self.levels = LevelList()
310         super(GameApp, self).__init__()
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         self.make_intro()
323
324     def make_intro(self):
325         self.root.clear_widgets()
326         screen = IntroScreen(self)
327         self.root.add_widget(screen)
328
329     def stop_app(self, label, ref):
330         self.stop()
331
332     def start_game(self, label, ref):
333         """Start the game"""
334         game = GameWindow(self.levels, self)
335         game.build()
336         self.root.clear_widgets()
337         self.root.add_widget(game)
338         # Ensure the player is visible
339         self.root.scroll_x = 0
340         self.root.scroll_y = 0
341         game.draw_player()
342         game.draw_nemesis()
343
344     def game_over(self, won):
345         if won:
346             screen = WonScreen(self)
347             self.levels.reset()
348         else:
349             screen = LostScreen(self)
350         self.root.clear_widgets()
351         self.root.add_widget(screen)
352
353
354 def main():
355     """ Erdslangetjie, a maze game of eluding nemesis
356     """
357     GameApp().run()