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