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