a6bd54b775457c503f295e61f1628e6c19396c7a
[roguelike.git] / roguelike.py
1 #! /usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 # Copyright © 2008 Neil Moore <neil@s-z.org>.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #  
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 import random
21 import curses
22 import curses.ascii
23 import curses.panel
24 import cProfile
25
26 import thing
27 import creature
28 import ai
29 import level
30 import ui
31
32 do_profile = False
33
34 class PlayerQuit (Exception):
35     pass
36
37 class AppUI:
38     instance = None
39
40     def is_arrow(self, ch):
41         return (ch == curses.KEY_RIGHT or ch == curses.KEY_LEFT
42                 or ch == curses.KEY_UP or ch == curses.KEY_DOWN)
43
44     def set_level(self, level):
45         self.mainpanel.replace(level.boardwin.derwin(
46             min(self.scrh-2, level.h), min(self.scrw, level.w), 0, 0))
47         self.mainpanel.move(1,0)
48         self.mainpanel.window().keypad(1)
49         self.recenter()
50         level.draw()
51         self.mainpanel.window().refresh()
52         curses.panel.update_panels()
53     
54     def set_player(self, player):
55         self.player = player
56         self.invpanel.set_userptr(player.inv)
57         self.set_level(player.level())
58     
59     def draw_stats(self):
60         pl = self.player
61         win = self.statpanel.window()
62         win.erase()
63         win.addstr(0, 1, "HP: %4d / %-4d   O2: %2d / 10   I/E: %s"
64                 % (pl.hp, pl.hpmax, pl.oxygen, pl.format_intrinsics()))
65         win.refresh()
66
67
68     def __init__(self, stdscr, players):
69         if AppUI.instance:
70             raise Exception("AppUI already exists")
71         else:
72             AppUI.instance = self
73
74         assert players, "No player for AppUI"
75         self.players = players
76         self.player = players[0]
77         self.player.active = True
78
79         self.stdscr = stdscr
80         (self.scrh, self.scrw) = stdscr.getmaxyx()
81
82         self.mainpanel = curses.panel.new_panel(
83                 curses.newwin(self.scrh-2, self.scrw))
84         self.mainpanel.move(1,0)
85         self.mainpanel.window().keypad(1)
86
87
88         self.msgpanel = curses.panel.new_panel(curses.newwin(1, self.scrw))
89         self.msgpanel.move(0,0)
90         self.statpanel = curses.panel.new_panel(curses.newwin(1, self.scrw))
91         self.statpanel.move(self.scrh-1, 0)
92         self.invpanel = curses.panel.new_panel(curses.newwin(16, 20))
93         self.invpanel.window().keypad(1)
94         self.invpanel.hide()
95         self.invpanel.move(3, 50)
96         self.invpanel.set_userptr(self.player.inv)
97         self.mainpanel.set_userptr({
98             'msg': self.msgpanel, 'stat': self.statpanel, 'inv': self.invpanel
99             })
100         self.mainpanel.bottom()
101         curses.panel.update_panels()
102         stdscr.refresh()
103
104         self.commands = {
105                 'north':        lambda: self.player.move_north() or True,
106                 'south':        lambda: self.player.move_south() or True,
107                 'east':         lambda: self.player.move_east()  or True,
108                 'west':         lambda: self.player.move_west()  or True,
109                 'wait':         lambda: True,
110                 'pickup':       lambda: self.player.pick_up(),  # returns True or False
111                 'inventory':    self.draw_inventory,
112                 'drop':         self.try_drop,
113                 'throw':        self.try_throw,
114                 'open':         self.try_open,
115                 'close':        self.try_close,
116                 'traverse':     self.try_traverse,
117                 'save':         self.save,
118                 'switch':       lambda: self.switch() and False,
119                 'wield':        self.try_wield,
120                 'unwield':      self.try_unwield,
121                 'quit':         self.quit,
122                 }
123         self.keymap = {
124                 curses.KEY_UP:    'north',
125                 curses.KEY_DOWN:  'south',
126                 curses.KEY_LEFT:  'west',
127                 curses.KEY_RIGHT: 'east',
128                 curses.ascii.SP:  'wait',
129                 ord('.'):         'wait',
130                 ord(','):         'pickup',
131                 ord('t'):         'throw',
132                 ord('i'):         'inventory',
133                 ord('d'):         'drop',
134                 ord('s'):         'save',
135                 ord('o'):         'open',
136                 ord('c'):         'close',
137                 ord('>'):         'traverse',
138                 ord('@'):         'switch',
139                 ord('w'):         'wield',
140                 ord('W'):         'unwield',
141                 ord('Q'):         'quit',
142                 }
143
144     def message(self, msg=None):
145         msgwin = self.msgpanel.window()
146         msgwin.erase()
147         if msg is not None:
148             msgwin.addnstr(0, 0, msg, self.scrw - 1, ui.Color('White').cp)
149         msgwin.refresh()
150
151     def get_direction(self):
152         self.message("Enter a direction")
153         try:
154             while 1:
155                 ch = self.mainpanel.window().getch()
156                 if self.is_arrow(ch):
157                     return ch
158                 elif ch == curses.ascii.SP:
159                     return None
160         finally:
161             self.message()
162     
163     def more(self):
164         try:
165             while self.mainpanel.window().getch() != curses.ascii.SP:
166                 pass
167         finally:
168             self.message()
169
170     def mapped(self, c):
171         return self.keymap.has_key(c) and self.commands.has_key(self.keymap[c])
172
173     def perform(self, c):
174         assert self.mapped(c)
175         return self.commands[self.keymap[c]]()
176
177     def draw_inventory(self):
178         win = self.invpanel.window()
179         inv = self.invpanel.userptr()
180         (h,w) = win.getmaxyx()
181         win.attrset(ui.Color('Red').cp)
182         win.erase()
183         win.border()
184         win.addstr(0,2, " Inventory ")
185         win.attrset(ui.Color('White').cp)
186         if inv != None:
187             for i in range(min(h-2, inv.num_items())):
188                 item = inv[i]
189                 win.addnstr(i+1, 2, "%s. %s" % (chr(i+ord('A')), item.name),
190                         w-6)
191                 (ascii, attr) = item.render()
192                 win.addstr(i+1, w-3, ascii, attr)
193                 if item.wielded:
194                     slotabbr = creature.slotabbrevs[item.wielded[1]]
195                     win.addstr(i+1, w-2, slotabbr, ui.Color('Cyan').cp)
196
197         self.invpanel.show()
198         win.refresh()
199         while 1:
200             c = win.getch()
201             if 0 <= c-ord('a') < min(26, inv.num_items()):
202                 index = c - ord('a')
203                 rv = inv[index]
204                 break
205             elif c == curses.ascii.ESC or c == curses.ascii.SP:
206                 rv = False
207                 break
208
209         self.invpanel.hide()
210         curses.panel.update_panels()
211         return rv
212     def quit(self):
213         raise PlayerQuit()
214     def try_drop(self):
215         it = self.draw_inventory()
216         if not it:
217             return False
218         else:
219             it.place(self.location())
220             return True
221     
222     def try_wield(self):
223         it = self.draw_inventory()
224         if not it:
225             return False
226         if not it.wieldable():
227             self.message("%s is not wieldable" % (it,))
228             return False
229         if it.wielded:
230             self.message("%s already wielded" % (it,))
231             return False
232         if self.player.wield(it):
233             self.message("Wielded %s in %s" % (it,it.wielded[1]))
234             return True
235         else:
236             self.message("Could not wield %s" % (it,))
237             return False
238     
239     def try_unwield(self):
240         it = self.draw_inventory()
241         if not it:
242             return False
243         if not it.wielded:
244             self.message("Not wielding %s" % (it,))
245             return False
246         if self.player.unwield(it):
247             self.message("Unwielded %s" % (it,))
248             return True
249         else:
250             self.message("Could not unwield %s" % (it,))
251             return False
252
253     def try_throw(self):
254         it = self.draw_inventory()
255         if not it:
256             return False
257         self.mainpanel.window().refresh()
258         dirn = self.get_direction()
259         if dirn is None:
260             return False
261         dest = self.location().throwdest(dirn, it)
262         if dest is None:
263             return False
264         it.place(dest)
265         return True
266
267     def try_traverse(self):
268         tgt = self.location().traverse()
269         if not tgt:
270             return False
271         # Force a repaint of the old location.
272         self.player.place(tgt)
273         self.set_level(tgt.level())
274         return True
275
276     def try_open(self):
277         dirn = self.get_direction()
278         if dirn is None:
279             return False
280         loc = self.location().dir(dirn, 1)
281         return loc.open()
282
283     def try_close(self):
284         dirn = self.get_direction()
285         if dirn is None:
286             return False
287         loc = self.location().dir(dirn, 1)
288         return loc.close()
289
290     def save(self):
291         filename = "lvl.sav"
292         f = open(filename, 'w+')
293         f.write(self.level().export())
294         f.close()
295         return False
296
297     def recenter(self):
298         me = self.player
299         if not me:
300             return False
301         (top, left) = self.mainpanel.window().getparyx()
302         (winh, winw) = self.mainpanel.window().getmaxyx()
303         bot = top + winh - 1
304         right = left + winw - 1
305         (newtop, newleft) = (top, left)
306
307         if me.location.x >= right:
308             newleft = min(me.location.x - winw/2, me.level().w - winw)
309         elif me.location.x <= left:
310             newleft = max(me.location.x - winw/2, 0)
311         if me.location.y >= bot:
312             newtop = min(me.location.y - winh/2, me.level().h - winh)
313         elif me.location.y <= top:
314             newtop = max(me.location.y - winh/2, 0)
315
316         if (newtop, newleft) != (top, left):
317             self.mainpanel.window().mvderwin(newtop, newleft)
318         return True
319
320     def switch(self):
321         self.player.active = False
322         self.player.invalidate()
323         self.players = self.players[1:] + self.players[0:1]
324         self.players[0].active = True
325         self.players[0].invalidate()
326         self.set_player(self.players[0])
327
328     def level(self):
329         return self.player.level()
330
331     def location(self):
332         return self.player.location
333
334     def event_loop(self):
335         while 1:
336             self.recenter()
337             self.draw_stats()
338             self.level().draw()
339             self.mainpanel.window().refresh()
340             c = self.stdscr.getch()
341             self.message()
342
343             if self.mapped(c):
344                 try:
345                     if not self.perform(c):
346                         continue
347                 except PlayerQuit:
348                     return
349             else:
350                 continue
351
352             self.level().affect_all()
353
354             for c in self.level().creatures():
355                 c.tick()
356             
357             for pl in [ p for p in self.players if p.dead ]:
358                 self.message("Player %s is dead: %s" % (pl, pl.dead))
359                 self.players.remove(pl)
360
361             if len(self.players) == 0:
362                 self.message("You have lost")
363                 self.more()
364                 return
365             elif self.player not in self.players:
366                 self.switch()
367                     
368
369 if __name__ == '__main__':
370     def main(stdscr):
371         me = creature.Player()
372         him = creature.Player()
373         it = ai.Monster()
374         ui = AppUI(stdscr, [me, him])
375
376         thing.Material.loadall(open("materials/materials.mtl"))
377         thing.ItemClass.loadall(open("items/items.itm"))
378
379         lvl = level.Level("level0")
380         me.place(lvl.loc(min(13, lvl.h - 2), min(2, lvl.w - 2)))
381         him.place(lvl.loc(max(2, lvl.h - 5), max(5, lvl.w - 10)))
382         it.place(lvl.loc(min(43, lvl.h - 2), min(5, lvl.w - 2)))
383         ui.set_level(lvl)
384
385         ui.event_loop()
386
387     if do_profile:
388         cProfile.run('curses.wrapper(main)', 'roguelike.prof')
389     else:
390         curses.wrapper(main)