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