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