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