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