Initial AI implementation.
[roguelike.git] / creature.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
22 import cacher
23 import loc
24 import thing
25 import ui
26
27 slotabbrevs = { 'hand': 'W', 'arm': 'A', 'body': 'B', 'head': 'H' }
28
29 class Creature(thing.Thing):
30     def __init__(self, location=None):
31         thing.Thing.__init__(self, location)
32
33         self.dead = False
34         self.hpmax = 10
35         self.hp = self.hpmax
36         self.oxygen = 10
37         self.speed = 1.0
38         
39         self.intrinsics = set()
40         self.extrinsics = set()
41
42         self.inv = loc.Inventory(self)
43         self.slots = { 'hand': None, 'arm': None, 'body': None, 'head': None }
44
45     def tick_turns(self):
46         speed = self.speed
47         turns = 0
48         if (speed >= 1.0):
49             turns = int(speed)
50             speed %= 1.0
51         if random.random() < self.speed:
52             turns += 1
53         return turns
54
55     def has(self, intrinsic):
56         if isinstance(intrinsic, basestring):
57             intrinsic = thing.Intrinsic(intrinsic)
58         return intrinsic in self.intrinsics or intrinsic in self.extrinsics
59
60     def holding(self, name):
61         return self.slots[name]
62     def is_wielding(self, item):
63         return item in self.slots
64     def vis_radius(self):
65         return 5
66     def can_see(self, y, x):
67         loc = self.location
68         if (y - loc.y)**2 + (x - loc.x)**2 <= self.vis_radius()**2:
69             return ((y, x) in self.level().visible_path(loc.y, loc.x, y, x))
70         return False
71
72     def visibles(self):
73         "Return the set of coordinates I can see."
74         vis = set()
75         py = self.location.y
76         px = self.location.x
77         rad = self.vis_radius()
78         
79         
80         # Use Bresenham's circle-drawing algorithm to find the perimeter
81         # of the visibility circle.
82         
83         perimeter = set()
84
85         # Position of the current point relative to (py, px)
86         (ry, rx) = (rad, 0)
87         d = 3 - (2*rad)
88
89         path = self.level().visible_path
90         while rx <= ry:
91             # Map into all 8 octants: SSE, SSW, NNE, NNW, ESE, WSW, ENE, ENW
92             perimeter.add((py + ry, px + rx))
93             perimeter.add((py + ry, px - rx))
94             perimeter.add((py - ry, px + rx))
95             perimeter.add((py - ry, px - rx))
96             perimeter.add((py + rx, px + ry))
97             perimeter.add((py + rx, px - ry))
98             perimeter.add((py - rx, px + ry))
99             perimeter.add((py - rx, px - ry))
100
101             if d < 0:
102                 d += (4*rx) + 6
103             else:
104                 d += 4 * (rx - ry) + 10
105                 ry -= 1
106             rx += 1
107
108         # Now raycast to each tile in the perimeter.  Each ray has max
109         # length ~(rad + 1), and there are ~(2 * PI * rad) coordinates in
110         # the perimeter.  Hence we will make a total of  ~(2 * PI* rad**2)
111         # visits (an average of two per in-radius tile).
112         clip = self.level().clip
113         for (y, x) in perimeter:
114             vis |= set(path(py, px, *clip(y, x)))
115
116         return vis
117
118     def is_creature(self):
119         return True
120
121     def can_breathe(self):
122         if isinstance(self.location, loc.Water):
123             return self.has("waterbreathing")
124         else:
125             return True
126
127     def breathe(self):
128         if self.can_breathe():
129             if self.oxygen < 10:
130                 self.oxygen += 1
131         else:
132             if self.oxygen > 0:
133                 self.oxygen -= 1
134             else:
135                 self.losehp(1, "asphyxia")
136
137     def tick(self):
138         self.breathe()
139
140     def losehp(self, n, cause):
141         if n >= self.hp:
142             self.die(cause)
143         else:
144             self.hp -= n
145
146     def die(self, cause):
147         self.dead = cause
148         loc = self.location
149         self.place(None)
150         for item in self.inv:
151             item.place(loc)
152         thing.ItemClass("corpse")().place(loc)
153
154     def can_swim(self):
155         return random.random() < .5
156     def render(self, pic=None):
157         return ( "?", u"⚗", ui.Color('BrightRed').cp )
158     def move_north(self):
159         return self.try_move(self.location.north())
160     def move_south(self):
161         return self.try_move(self.location.south())
162     def move_west(self):
163         return self.try_move(self.location.west())
164     def move_east(self):
165         return self.try_move(self.location.east())
166     def pick_up(self):
167         it = self.location.top_item()
168         if it:
169             it.try_move(self.inv)
170             return True
171         else:
172             return False
173     def wield(self, item):
174         assert item in self.inv, (
175                 "%s is wielding item %s not in inventory" % (self, item))
176         assert item.wieldable(), (
177                 "%s trying to wield the unwieldable %s" % (self, item))
178         assert not item.wielded, (
179                 "%s trying to wield %s, already wielded by %s"
180                 % (self, item, item.wielded))
181         slot = item.slot
182         if self.holding(slot):
183             self.unwield(self.holding(slot))
184         self.slots[slot] = item
185         item.wielded = (self, slot)
186
187         if item.intrinsics:
188             self.extrinsics |= item.intrinsics
189             
190         return True
191
192     def unwield(self, item):
193         assert item in self.inv, (
194                 "%s is unwielding item %s not in inventory" % (self, item))
195         assert item.wielded, (
196                 "%s trying to unwield already unwielded %s" % (self, item))
197
198         (creat, slot) = item.wielded
199         assert creat is self, "%s unwielding %s's %s" % (self, creat, item)
200         assert self.holding(slot) is item, (
201                 "%s unwielding %s from wrong hand %s?" % (self, item, slot))
202
203         item.wielded = None
204         self.slots[slot] = None
205         if item.intrinsics:
206             self.recompute_extrinsics()
207         return True
208
209     def recompute_extrinsics(self):
210         self.extrinsics.clear()
211         for equip in self.slots.values():
212             if equip and equip.intrinsics:
213                 self.extrinsics |= equip.intrinsics
214
215
216 class Player (Creature):
217     def __init__(self, location=None):
218         super(Player,self).__init__(location)
219         self.active = False
220     def render(self, pic=None):
221         if self.active:
222             col = 'BrightMagenta'
223         else:
224             col = 'Magenta'
225         return ("@", u"@", ui.Color(col).cp)
226     def can_swim(self):
227         return random.random() < .5
228     def format_intrinsics(self):
229         return " ".join([ intr.abbrev for intr in self.intrinsics ]
230                 + [ "/" ] + [ extr.abbrev for extr in self.extrinsics ])
231     def invalidate(self):
232         if self.location is not None:
233             self.location.invalidate(important = True)