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