Finish python 2.4 conversion.
[roguelike.git] / level.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 curses
21 import random
22 import math
23 import re
24 from cStringIO import StringIO
25
26 import appui
27 import loc
28 import thing
29 import cacher
30 import __main__
31
32 class Loc (object):
33     def __init__(self, lid, y, x):
34
35         self.level = lid
36         self.y = y
37         self.x = x
38
39     def __str__(self):
40         return "%s %d %d" % (self.level, self.y, self.x)
41
42
43 class LocationProxy (object):
44     def __init__(self, level=None, y=None, x=None, locstr=None):
45         if locstr:
46             (lstr, ystr, xstr) = locstr.split(None, 2)
47             level = LevelProxy(lstr)
48             y = int(ystr)
49             x = int(xstr)
50         assert (level and y is not None and x is not None)
51         self.lvl = level
52         self.y = y
53         self.x = x
54
55     def as_loc(self):
56         return Loc(self.lvl.lid, self.y, self.x)
57
58     def level(self):
59         if isinstance(self.lvl, LevelProxy):
60             self.lvl = self.lvl.level()
61         return self.lvl
62
63     def location(self):
64         return self.level().loc(self.y, self.x)
65     
66 class Room (object):
67     """A room, with a union-find structure for connected rooms."""
68     def __init__(self, y, x, h, w):
69         (self.y, self.x, self.h, self.w) = (y, x, h, w)
70         self.ymax = y + h - 1
71         self.xmax = x + w - 1
72         self.doors = { 't': False, 'b': False, 'l': False, 'r': False }
73
74     def facing(self, room):
75         possible = []
76         assert not (self & room), "Can't face intersecting room"
77
78         if self.y > room.ymax:
79             dy = room.ymax - self.y
80         elif room.y > self.ymax:
81             dy = room.y - self.ymax
82         else:
83             dy = 0
84         
85         if self.x > room.xmax:
86             dx = room.xmax - self.x
87         elif room.x > self.xmax:
88             dx = room.x - self.xmax
89         else:
90             dx = 0
91
92         if abs(dx) > abs(dy):
93             if dx > 0:
94                 return 'r'
95             else:
96                 return 'l'
97         else:
98             if dx > 0:
99                 return 'b'
100             else:
101                 return 't'
102
103     def add_door(self, face):
104         if face == 't':
105             dx = random.randint(self.x+1, self.xmax-1)
106             dy = self.y
107         elif face == 'b':
108             dx = random.randint(self.x+1, self.xmax-1)
109             dy = self.ymax
110         elif face == 'l':
111             dx = self.x
112             dy = random.randint(self.y+1, self.ymax-1)
113         elif face == 'r':
114             dx = self.xmax
115             dy = random.randint(self.y+1, self.ymax-1)
116         else:
117             raise Exception("Unknown face %s" % (face,))
118
119         return (dy, dx)
120
121     def __and__(self, room):
122         yoverlap = (self.y <= room.ymax+1 and self.ymax >= room.y-1)
123         xoverlap = (self.x <= room.xmax+1 and self.xmax >= room.x-1)
124         return yoverlap and xoverlap
125
126
127 class Level (object):
128     """Represents an area of the map comprising a contiguous rectangular
129     region of Tiles surrounded by an impassable boundary.  Levels may
130     be travelled between via transport points.
131     """
132     __metaclass__ = cacher.FirstArg
133
134     gen_count = 0
135
136     @classmethod
137     def generate_id(cls):
138         cls.gen_count = cls.gen_count + 1
139         return 'gen%d' % (cls.gen_count,)
140
141     def __init__(self, lid=None, height=None, width=None, name=None):
142         self.active_tiles = set()
143         self.invalid_tiles = set()
144         self.important_repaints = set()
145         self.unconnected_stairs = set()
146         self.unconnected_drops = []
147         self.needs_full_repaint = True
148
149         if lid:
150             self.lid = lid
151             self.filename = "levels/%s.sav" % (lid,)
152             assert height == width == name == None, \
153                     "Level constructor specified more than just the filename"
154             self.load_file(open(self.filename, "r"))
155         else:
156             self.lid = self.generate_id()
157             self.filename = None
158             assert height and width
159             self.h = height
160             self.w = width
161             self.name = name or ""
162
163             if random.random() < 0.1:
164                 self.pick_tile = random.choice(
165                         (self.pick_tile_1, self.pick_tile))
166
167                 self.grid = [ [ self.activate(self.pick_tile(i,j))
168                     for j in xrange(self.w) ]
169                     for i in xrange(self.h) ]
170             else:
171                 self.make_room_map()
172
173         random.shuffle(self.unconnected_drops)
174
175         self.boardwin = curses.newwin(self.h + 1, self.w + 1)
176
177     def stair_unavailable(self, stair):
178         if stair in self.unconnected_stairs:
179             self.unconnected_stairs.remove(stair)
180     def stair_available(self, stair):
181         self.unconnected_stairs.add(stair)
182
183
184     def connect(self, rm1, rm2):
185         # pick nearer sides of each room
186         faces = (rm1.facing(rm2), rm2.facing(rm1))
187         (y1,x1) = rm1.add_door(faces[0])
188         (y2,x2) = rm2.add_door(faces[1])
189         
190         self.grid[y1][x1] = loc.Door(self, y1, x1)
191         self.grid[y2][x2] = loc.Door(self, y2, x2)
192
193         dy = y2 - y1
194         dx = x2 - x1
195
196         if faces[0] == 'r' and faces[1] == 'l':
197             # >V> or >^>
198             if dy:
199                 ydir = dy/abs(dy)
200             else:
201                 ydir = 0
202             ty = y1
203             for tx in xrange(x1+1, x2):
204                 self.grid[ty][tx] = loc.Floor(self, ty, tx)
205                 if tx == (x1 + x2) // 2 and dy != 0:
206                     while ty != y2:
207                         self.grid[ty][tx] = loc.Floor(self, ty, tx)
208                         ty = ty + ydir
209                     self.grid[ty][tx] = loc.Floor(self, ty,tx)
210         elif faces[0] == 'l' and faces[1] == 'r':
211             # >V> or >^>
212             if dy:
213                 ydir = -dy/abs(dy)
214             else:
215                 ydir = 0
216             ty = y2
217             for tx in xrange(x2+1, x1):
218                 self.grid[ty][tx] = loc.Floor(self, ty, tx)
219                 if tx == (x1 + x2) // 2 and dy != 0:
220                     while ty != y1:
221                         self.grid[ty][tx] = loc.Floor(self, ty, tx)
222                         ty = ty + ydir
223                     self.grid[ty][tx] = loc.Floor(self, ty,tx)
224         elif faces[0] == 'b' and faces[1] == 't':
225             # V>V or V<V
226             if dx:
227                 xdir = dx/abs(dx)
228             else:
229                 xdir = 0
230             tx = x1
231             for ty in xrange(y1+1, y2):
232                 self.grid[ty][tx] = loc.Floor(self, ty, tx)
233                 if ty == (y1 + y2) // 2 and dx != 0:
234                     while tx != x2:
235                         self.grid[ty][tx] = loc.Floor(self, ty, tx)
236                         tx = tx + xdir
237                     self.grid[ty][tx] = loc.Floor(self, ty,tx)
238         elif faces[0] == 't' and faces[1] == 'b':
239             # ^>^ or ^<^
240             if dx:
241                 xdir = -dx/abs(dx)
242             else:
243                 xdir = 0
244             tx = x2
245             for ty in xrange(y2+1, y1):
246                 self.grid[ty][tx] = loc.Floor(self, ty, tx)
247                 if ty == (y1 + y2) // 2 and dx != 0:
248                     while tx != x1:
249                         self.grid[ty][tx] = loc.Floor(self, ty, tx)
250                         tx = tx + xdir
251                     self.grid[ty][tx] = loc.Floor(self, ty,tx)
252
253     def make_room_map(self, nrooms = 10, nstairs = 2):
254         self.grid = [ [ loc.Wall(self,i,j)
255             for j in xrange(self.w) ]
256             for i in xrange(self.h) ]
257         rooms = []
258
259         st_rooms = set(random.sample(xrange(nrooms), nstairs))
260
261         for rno in xrange(nrooms):
262             rm = None
263
264             while not rm or any(orm & rm for orm in rooms):
265                 rh = random.randint(4, self.h // (math.sqrt(nrooms)/1.5))
266                 rw = random.randint(4, self.w // (math.sqrt(nrooms)/1.5))
267                 ry = random.randint(0, self.h - rh)
268                 rx = random.randint(0, self.w - rw)
269                 rm = Room(ry, rx, rh, rw)
270
271             wet = random.random() < 0.1
272
273             rooms.append(rm)
274             for i in xrange(rm.y+1, rm.ymax):
275                 for j in xrange(rm.x+1, rm.xmax):
276                     if rno in st_rooms and i == ry + rh//2 and j == rx + rw//2:
277                         self.grid[i][j] = loc.Stair(self, i, j)
278                     elif wet:
279                         self.grid[i][j] = loc.Water(self, i, j)
280                     else:
281                         self.grid[i][j] = loc.Floor(self, i, j)
282
283         for rno in xrange(1,nrooms):
284             if rno == 1 or random.random() < 0.8 :
285                 nct = 1
286             else:
287                 nct = 2
288             for tgt in random.sample(range(0, rno), nct):
289                 self.connect(rooms[rno], rooms[tgt])
290
291         for row in self.grid:
292             for tile in row:
293                 self.activate(tile)
294     
295     def activate(self, tile):
296         (y, x) = (tile.y, tile.x)
297         if tile.is_active():
298             self.active_tiles.add((y, x))
299         if isinstance(tile, loc.Stair) and not tile.target() and not tile.oneway:
300             self.stair_available(tile)
301         return tile
302
303     def load_file(self, f):
304         attrs = {}
305         def parse_line(line, lno):
306             linere = r'^(\d+)\s+(\d+)\s+(\w+)\s+(\w+)\s+(.*?)\s*\n$'
307             m = re.match(linere, line)
308             (y, x, attr, typ, valstr) = m.groups()
309             y = int(y) ; x = int(x)
310             if not 0 <= y < self.h:
311                 raise Exception("extra out of bounds: y=%d" % (y))
312             if not 0 <= x < self.w:
313                 raise Exception("extra out of bounds: x=%d" % (x))
314
315             val = None
316             if typ == "float":
317                 val = float(valstr)
318             elif typ == "int":
319                 val = int(valstr)
320             elif typ == "bool":
321                 val = valstr != "False"
322             elif typ == "str":
323                 val = valstr.strip()
324             elif typ == "level":
325                 val = LevelProxy(valstr.strip())
326             elif typ == "Loc":
327                 val = LocationProxy(locstr = valstr.strip())
328             else:
329                 raise Exception("unknown type %s (val '%s')" % (typ, valstr))
330             if (y,x) not in attrs:
331                 attrs[(y,x)] = {}
332             attrs[(y,x)][attr] = val
333
334         line = f.readline() ; lno = 1
335         if line.rstrip('\n') != "#!RgLkLvL":
336             raise Exception("Bad first line: '%s'" % (line))
337
338         line = f.readline() ; lno = 2
339
340         (hs, ws, name) = re.split("\\s+", line, 2)
341         self.h = int(hs)
342         self.w = int(ws)
343         self.name = name.rstrip('\n')
344
345         self.grid = [[None] * self.w for x in xrange(self.h)]
346
347         lines = []
348
349         first_lno = lno + 1
350         for i in range(self.h):
351             lines.append(f.readline())
352             lno = lno + 1
353
354         for l in f.readlines():
355             lno = lno + 1
356             parse_line(l, lno)
357
358         for i in range(self.h):
359             line = lines[i]
360             if len(line) != self.w + 1:
361                 raise Exception("incorrect line length: line %d = %d"
362                         % (first_lno + i, len(line)))
363             for j in range(self.w):
364                 if (i,j) in attrs:
365                     attr = attrs[(i,j)]
366                 else:
367                     attr = {}
368                 t = self.decode_tile(i, j, line[j], attr)
369                 self.grid[i][j] = self.activate(t)
370             if line[self.w] != '\n':
371                 raise Exception("line %d has no newline" % (i))
372
373
374         f.close()
375
376     def pick_tile_1(self, i, j):
377         if i == 15 and j == 60:
378             return loc.Stair(self, i, j)
379         elif (i-15)**2 + ((j-60)**2)/2 <= 36:
380             flowy = (j-60) / (math.sqrt(2) * 6) - (i-15)/(math.sqrt(2)*18)
381             flowx = (15-i) * math.sqrt(2) / 6 - (j-60)*math.sqrt(2)/18
382             return loc.Water(self, i, j, flowy, flowx)
383         elif 580 < (i-5)**2 + ((j-45)**2)/2 < 900:
384             flowy = (45-j) / (math.sqrt(2) * 27)
385             flowx = (i-5) * math.sqrt(2) / 27
386             return loc.Water(self, i, j, flowy, flowx)
387         elif random.random() < .1:
388             return loc.Wall(self, i, j)
389         else:
390             return loc.Floor(self, i, j, self.randitems())
391     
392     def pick_tile(self, i, j):
393         if i==0 or j==0 or i == self.h - 1 or j == self.w - 1:
394             return loc.Wall(self, i, j)
395         if (i == 5 and j == 9) or (i == self.h-5 and j == self.w - 10):
396             return loc.Stair(self, i, j)
397         if i % 9 == 0:
398             if (j == 9 and i % 18 == 0) or (j == self.w-10 and i % 18 == 9):
399                 return loc.Door(self, i, j)
400             else:
401                 return loc.Wall(self, i, j)
402         if j % 18 == 0:
403             if i % 9 == 4:
404                 return loc.Door(self, i, j)
405             else:
406                 return loc.Wall(self, i, j)
407         else:
408             return loc.Floor(self, i, j, self.randitems())
409
410     def get_target(self, oneway):
411         if oneway:
412             return self.unconnected_drops.pop()
413         else:
414             return self.unconnected_stairs.pop()
415
416     def invalidate(self, tile=None, important=False):
417         if tile == None:
418             self.needs_full_repaint = True
419         else:
420             assert tile.level() == self
421             self.invalid_tiles.add((tile.y, tile.x))
422             if important:
423                 self.important_repaints.add((tile.y, tile.x))
424
425     def decode_tile(self, i, j, char, attrs):
426         if char == "#":
427             return loc.Wall(self, i, j, **attrs)
428         elif char == " ":
429             return loc.Floor(self, i, j, self.randitems(), **attrs)
430         elif char == "~":
431             return loc.Water(self, i, j, **attrs)
432         elif char == "+":
433             return loc.Door(self, i, j, opened = False, **attrs)
434         elif char == "-":
435             return loc.Door(self, i, j, opened = True, **attrs)
436         elif char == ">":
437             return loc.Stair(self, i, j, **attrs)
438         elif char == "<":
439             # Target for one-way stairs
440             tile = loc.Floor(self, i, j, **attrs)
441             self.unconnected_drops.append(tile)
442             return tile
443         else:
444             raise Exception
445
446     def randitems(self):
447         r = 10000*random.random()
448         if r >= 100:
449             return []
450         elif r >= 50:
451             return [ thing.ItemClass("gold piece")() ]
452         elif r >= 20:
453             return [ thing.ItemClass("gem")() ]
454         elif r >= 10:
455             return [ thing.ItemClass("sword")() ]
456         elif r >= 2:
457             return [ thing.ItemClass("shield")() ]
458         else:
459             return [ thing.ItemClass("mask of water breathing")() ]
460
461     def loc(self, i, j):
462         return self.grid[i][j]
463
464     def clip(self, y, x):
465         if y<0:
466             y = 0
467         elif y >= self.h:
468             y = self.h-1
469         if x<0:
470             x = 0
471         elif x >= self.w:
472             x = self.w-1
473         return (y, x)
474
475     def players(self):
476         return (pl for pl in __main__.AppUI.instance.players if pl.level() == self)
477
478     def visible_path(self, py, px, oy, ox):
479         """Returns a list of coordinates that can be seen by (py,px) when
480         looking at (oy,ox)."""
481         (dy, dx) = (oy-py, ox-px)
482         if abs(dy) < 1 and abs(dx) < 1:
483             return (py, px)
484
485         path = []
486
487         # Go in the direction of the longer axis
488         flipped = abs(dy) > abs(dx)
489         if flipped:
490             (dy, dx) = (dx, dy)
491             (py, px) = (px, py)
492             (oy, ox) = (ox, oy)
493
494         if dx >= 0:
495             xinc = 1
496         else:
497             xinc = -1
498
499         # For each long-axis coordinate between p and o (inclusive):
500         for x in xrange(px, ox + xinc, xinc):
501             # Find the corresponding short-axis coordinate.
502             y = int(round(py + (x - px)*(float(dy)/dx)))
503
504             # Convert to real coordinates
505             if flipped:
506                 (ly, lx) = (x, y)
507             else:
508                 (ly, lx) = (y, x)
509
510             # This cell can be seen.
511             path.append((ly, lx))
512
513             # If it is opaque, no more cells can be seen.
514             if not self.loc(ly, lx).transparent():
515                 break
516         return path
517
518     # TODO: use dynamic programming: if we can't see a near tile, there is
519     # no reason to test the tiles behind it.
520     def draw(self):
521         if self.needs_full_repaint:
522             self.invalid_tiles = set((y,x) for y in range(self.h) for x in range(self.w))
523             self.needs_full_repaint = False
524         vis = set()
525         
526         for pl in self.players():
527             vis |= pl.visibles()
528
529         vis &= (self.invalid_tiles | self.active_tiles)
530         vis |= self.important_repaints
531         for (y,x) in vis:
532             (char, unic, color) = self.grid[y][x].render()
533             if appui.hasuni:
534                 self.boardwin.addwstr(y, x, unic, color)
535             else:
536                 self.boardwin.addstr(y, x, char, color)
537         self.invalid_tiles -= vis
538         self.important_repaints.clear()
539
540     def export(self):
541         extras = {}
542         f = StringIO()
543         f.write("#!RgLkLvL\n")
544         f.write("%d %d %s\n" % (self.h, self.w, self.name))
545         for i in range(self.h):
546             for j in range(self.w):
547                 tile = self.grid[i][j]
548                 (s, extra) = tile.export()
549                 f.write(s)
550                 if len(extra) > 0:
551                     extras[(i,j)] = extra
552             f.write("\n")
553         for ((y, x), ex) in extras.iteritems():
554             for (k,v) in ex.iteritems():
555                 f.write("%d %d %s %s %s\n" % (y, x, k, type(v).__name__, v))
556         val = f.getvalue()
557         f.close()
558         return val
559
560     def level(self):
561         return self
562
563     def creatures(self):
564         return [ creat 
565                 for y in range(self.h)
566                 for x in range(self.w)
567                 for creat in self.loc(y,x)._creatures ]
568
569     def affect_all(self):
570         for (y,x) in self.active_tiles:
571             self.grid[y][x].affect_all()
572
573
574 class LevelProxy (object):
575     def __init__(self, lid):
576         self.lid = lid
577         self.lvl = None
578
579     def level(self):
580         if not self.lvl:
581             self.lvl = Level(self.lid)
582         return self.lvl
583