8bd431ff1a18297635c188ff4cc7d23c16f7bec4
[roguelike.git] / things.py
1 import random
2 import re
3
4 import cacher
5 import loc
6 import ui
7
8 class Material(object):
9     __metaclass__ = cacher.FirstArg
10
11     def __init__(self, name, adj, attrs):
12         self.name = name
13         self.adj = adj or name
14         self.attrs = attrs
15     
16     @classmethod
17     def load(cls, fh):
18         for line in fh:
19             line = line.rstrip('\n')
20             name = None
21             adj = None
22             color = None
23             if re.match(r"#|\s*$", line):
24                 continue
25             mat = re.match(
26                     r'\s*(?:(\w+)|"([^"]*)")(?:\s+\(([^()]*)\))?\s*:\s+(.*?)\s*$',
27                     line)
28             assert mat, "Material line '%s' did not match" % (line,)
29
30             (name, qname, adj, val) = mat.groups()
31
32             name = name or qname
33             assert name, "Material '%s' does not have a name" % (line)
34
35             while val:
36                 vm = re.match(r'\s*(\w+)=(?:(\w+)|"([^"]*)")\s*', val)
37                 assert vm, "Material value '%s' is bad" % (val,)
38
39                 (vname, vval, qvval) = vm.groups()
40                 vval = vval or qvval
41
42                 if vname == 'color':
43                     color = vval
44                 else:
45                     raise Exception("Unknown attribute '%s'" % (vname,))
46
47                 val = val[vm.end():]
48
49             # Now we have the info we need
50             yield (Material(name, adj, color))
51
52     @classmethod
53     def loadall(cls, fh):
54         return list(cls.load(fh))
55
56     def attr(self):
57         if callable(self.attrs):
58             return self.attrs()
59         elif self.attrs is None:
60             self.attrs = ui.Color('Default').cp
61         elif isinstance(self.attrs, str):
62             self.attrs = ui.Color(self.attrs).cp
63
64         return self.attrs
65
66 class ItemClass (type):
67     __metaclass__ = cacher.FirstArg
68     def __new__(cls, name, properties):
69         base = Item
70         props = properties.copy()
71         props['name'] = name
72         if 'material' in props and isinstance(props['material'], str):
73             props['material'] = Material(props['material'])
74         if 'isa' in props:
75             base = ItemClass(props['isa'])
76             del props['isa']
77         return type.__new__(cls, name + " item", (base,), props)
78
79     @classmethod 
80     def load(cls, fh):
81         import pyparsing
82         from pyparsing import Word, Suppress, Optional, Dict, Group
83         ident = Word( pyparsing.alphas, pyparsing.alphanums )
84         string = ident | pyparsing.QuotedString(
85                 quoteChar = '"', escChar = '\\', unquoteResults = True)
86         value = string
87         attrib = Group(ident + Suppress(':') + string)
88         block = Suppress('{') + pyparsing.delimitedList(attrib, delim=',') + \
89                 Suppress(Optional(',') + '}')
90         item = Group(string.setResultsName('name') +
91                 Dict(block).setResultsName('props'))
92
93         itemlist = pyparsing.Dict(
94                 pyparsing.delimitedList(item, delim=',') +
95                 Suppress(Optional(',')))
96
97         for item in itemlist.parseFile(fh):
98             yield ( cls(item.name, item.props.asDict()) )
99     
100
101     @classmethod
102     def loadall(cls, fh):
103         return list(cls.load(fh))
104
105 class Thing (object):
106     "An item, creature, or piece of scenery."
107     def __init__(self, location=None):
108         self.location = None
109         self.place(location)
110     def invalidate(self):
111         self.location.invalidate(important = False)
112     def is_creature(self):
113         return False
114     def is_item(self):
115         return False
116     def is_scenery(self):
117         return False
118     def level(self):
119         return self.location.level()
120     def place(self, location):
121         if self.location is not None:
122             self.location.remove(self)
123         self.location = location
124         if location is not None:
125             self.location.add(self)
126     def try_move(self, newloc):
127         if newloc and newloc.can_hold(self):
128             self.place(newloc)
129             return True
130         else:
131             return False
132
133 class Item(Thing):
134     wielded = None
135     slot = None
136     def is_item(self):
137         return True
138     def render(self, pic=None):
139         return (self.ascii, self.material.attr())
140     def wieldable(self):
141         return self.slot is not None
142     def place(self, location):
143         if self.wielded:
144             self.unwield()
145         super(Item, self).place(location)
146     def unwield(self):
147         if self.wielded:
148             # Have the holder unwield it
149             self.wielded[0].unwield(self)
150
151 slotabbrevs = { 'hand': 'H', 'arm': 'A', 'body': 'B' }
152
153 class Creature(Thing):
154     def __init__(self, location=None):
155         Thing.__init__(self, location)
156         self.inv = loc.Inventory(self)
157         self.slots = { 'hand': None, 'arm': None, 'body': None }
158     def holding(self, name):
159         return self.slots[name]
160     def is_wielding(self, item):
161         return item in self.slots
162     def invalidate(self):
163         self.location.invalidate(important = True)
164     def vis_radius(self):
165         return 5
166     def can_see(self, y, x):
167         loc = self.location
168         if (y - loc.y)**2 + (x - loc.x)**2 <= self.vis_radius()**2:
169             return self.level().visible_path(loc.y, loc.x, y, x)
170         return False
171     def visibles(self):
172         vis = []
173         py = self.location.y
174         px = self.location.x
175         rad = self.vis_radius()
176         (ymin,xmin) = self.level().clip(py - rad, px - rad)
177         (ymax,xmax) = self.level().clip(py + rad, px + rad)
178         for y in range(ymin, ymax+1):
179             for x in range(xmin, xmax+1):
180                 if self.can_see(y,x):
181                     vis.append((y,x))
182         return vis
183
184     def is_creature(self):
185         return True
186     def can_swim(self):
187         return False
188     def render(self, pic=None):
189         return ( "?", ui.Color('BrightMagenta').cp )
190     def move_north(self):
191         return self.try_move(self.location.north())
192     def move_south(self):
193         return self.try_move(self.location.south())
194     def move_west(self):
195         return self.try_move(self.location.west())
196     def move_east(self):
197         return self.try_move(self.location.east())
198     def pick_up(self):
199         it = self.location.top_item()
200         if it:
201             it.try_move(self.inv)
202             return True
203         else:
204             return False
205     def wield(self, item):
206         assert item in self.inv, (
207                 "%s is wielding item %s not in inventory" % (self, item))
208         assert item.wieldable(), (
209                 "%s trying to wield the unwieldable %s" % (self, item))
210         assert not item.wielded, (
211                 "%s trying to wield %s, already wielded by %s"
212                 % (self, item, item.wielded))
213         slot = item.slot
214         if self.holding(slot):
215             self.unwield(self.holding(slot))
216         self.slots[slot] = item
217         item.wielded = (self, slot)
218         return True
219
220     def unwield(self, item):
221         assert item in self.inv, (
222                 "%s is unwielding item %s not in inventory" % (self, item))
223         assert item.wielded, (
224                 "%s trying to unwield already unwielded %s" % (self, item))
225
226         (creat, slot) = item.wielded
227         assert creat is self, "%s unwielding %s's %s" % (self, creat, item)
228         assert self.holding(slot) is item, (
229                 "%s unwielding %s from wrong hand %s?" % (self, item, slot))
230
231         item.wielded = None
232         self.slots[slot] = None
233         return True
234
235 class Player(Creature):
236     def __init__(self, location=None):
237         Creature.__init__(self, location)
238         self.active = False
239     def render(self, pic=None):
240         return ("@", ui.Color(('Bright' if self.active else '') + 'Magenta').cp)
241     def can_swim(self):
242         return random.random() < .5