Browse code
beginning implementation of combat
edwlan authored on 29/07/2013 03:45:45
Showing 11 changed files
Showing 11 changed files
- .gitignore
- data/terrain.yml
- libs/cache.py
- libs/combat.py
- libs/dice.py
- libs/patch_random.py
- libs/type_dict.py
- main.py
- src/map.py
- src/player.py
- terminal.png
4 | 6 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,36 @@ |
1 |
+grass: |
|
2 |
+ char: '.' |
|
3 |
+ fg: [0,255,0] |
|
4 |
+ bg: [0,100,0] |
|
5 |
+ passable: true |
|
6 |
+ transparent: true |
|
7 |
+ prob: 10 |
|
8 |
+sand: |
|
9 |
+ char: ',' |
|
10 |
+ fg: [255,255,0] |
|
11 |
+ bg: [100,100,0] |
|
12 |
+ passable: true |
|
13 |
+ transparent: true |
|
14 |
+scrub: |
|
15 |
+ char: ',' |
|
16 |
+ fg: [0,127,0] |
|
17 |
+ bg: [0,64,0] |
|
18 |
+ passable: true |
|
19 |
+ transparent: true |
|
20 |
+water: |
|
21 |
+ char: '~' |
|
22 |
+ fg: [128,128,255] |
|
23 |
+ bg: [64,64,128] |
|
24 |
+ transparent: true |
|
25 |
+ prob: 5 |
|
26 |
+brush: |
|
27 |
+ char: '#' |
|
28 |
+ fg: [0,122,0] |
|
29 |
+ bg: [0,64,0] |
|
30 |
+ passable: true |
|
31 |
+ prob: 0.5 |
|
32 |
+tree: |
|
33 |
+ char: '#' |
|
34 |
+ fg: [0,244,0] |
|
35 |
+ bg: [0,122,0] |
|
36 |
+ prob: 0.1 |
0 | 10 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,328 @@ |
1 |
+from . import dice |
|
2 |
+from .dice import MULT,ADD |
|
3 |
+from . import type_dict |
|
4 |
+ |
|
5 |
+import collections |
|
6 |
+ |
|
7 |
+class Attributes(object): |
|
8 |
+ def __init__(self, strength, intellect, dexterity, spirit): |
|
9 |
+ self.str = strength |
|
10 |
+ self.int = intellect |
|
11 |
+ self.dex = dexterity |
|
12 |
+ self.spt = spirit |
|
13 |
+ self.state = HEALTHY |
|
14 |
+ |
|
15 |
+def health_mod(func): |
|
16 |
+ def _inner(self): |
|
17 |
+ mod = 0 |
|
18 |
+ if self.attr.state >= WOUNDED: |
|
19 |
+ mod = -3 |
|
20 |
+ result = func(self) |
|
21 |
+ result += mod |
|
22 |
+ return result |
|
23 |
+ return _inner |
|
24 |
+ |
|
25 |
+ |
|
26 |
+ |
|
27 |
+class Skills(object): |
|
28 |
+ def __init__(self, attr): |
|
29 |
+ self.skills = { |
|
30 |
+ 'agility','craft','fighting','knowledge', |
|
31 |
+ 'perception','persuasion','shooting','speed', |
|
32 |
+ 'stealth','toughness' |
|
33 |
+ } |
|
34 |
+ self.attr = attr |
|
35 |
+ self.training = Trainer(self) |
|
36 |
+ self.train = self.training.select |
|
37 |
+ |
|
38 |
+ def check(self, skill, dieroll): |
|
39 |
+ dieroll.dice = dice.DieJar().d20 |
|
40 |
+ roll = dieroll.roll() |
|
41 |
+ if roll == 1: |
|
42 |
+ result = False |
|
43 |
+ if roll == 20: |
|
44 |
+ result = True |
|
45 |
+ else: |
|
46 |
+ result = roll < getattr(self, skill) |
|
47 |
+ return result, roll-result # (passes, difference) |
|
48 |
+ |
|
49 |
+ @property |
|
50 |
+ @health_mod |
|
51 |
+ def agility(self): |
|
52 |
+ return (self.attr.dex * 2) + self.training.agility |
|
53 |
+ @property |
|
54 |
+ @health_mod |
|
55 |
+ def craft(self): |
|
56 |
+ return self.attr.dex + self.attr.int + self.training.craft |
|
57 |
+ @property |
|
58 |
+ @health_mod |
|
59 |
+ def fighting(self): |
|
60 |
+ return self.attr.str + self.attr.int + self.training.fighting |
|
61 |
+ @property |
|
62 |
+ @health_mod |
|
63 |
+ def knowledge(self): |
|
64 |
+ return self.attr.int * 2 + self.training.knowledge |
|
65 |
+ @property |
|
66 |
+ @health_mod |
|
67 |
+ def perception(self): |
|
68 |
+ return self.attr.int + attr.spt + self.training.perception |
|
69 |
+ @property |
|
70 |
+ @health_mod |
|
71 |
+ def persuasion(self): |
|
72 |
+ return self.attr.spt * 2 + self.training.persuasion |
|
73 |
+ @property |
|
74 |
+ @health_mod |
|
75 |
+ def shooting(self): |
|
76 |
+ return self.attr.dex + self.attr.int + self.training.shooting |
|
77 |
+ @property |
|
78 |
+ @health_mod |
|
79 |
+ def speed(self): |
|
80 |
+ return self.attr.str + self.attr.dex + self.training.speed |
|
81 |
+ @property |
|
82 |
+ @health_mod |
|
83 |
+ def stealth(self): |
|
84 |
+ return self.attr.dex + self.attr.spt + self.training.stealth |
|
85 |
+ @property |
|
86 |
+ @health_mod |
|
87 |
+ def toughness(self): |
|
88 |
+ return self.attr.str + self.attr.spt + self.training.toughness |
|
89 |
+ |
|
90 |
+class Trainer(object): |
|
91 |
+ mods = dict( |
|
92 |
+ untrained=-1, |
|
93 |
+ familiar=0, |
|
94 |
+ trained=1, |
|
95 |
+ experienced=2, |
|
96 |
+ mastered=3 |
|
97 |
+ ) |
|
98 |
+ |
|
99 |
+ def __init__(self, skills): |
|
100 |
+ self.skills = skills |
|
101 |
+ self.training = collections.defaultdict(lambda: -1) |
|
102 |
+ self.points = 500 |
|
103 |
+ self.training_cost = 100 |
|
104 |
+ |
|
105 |
+ def select(self, skill, degree): |
|
106 |
+ if hasattr(degree, 'upper'): |
|
107 |
+ degree = self.mods.get(degree, 0) |
|
108 |
+ if self.points >= self.training_cost and degree in self.mods.values(): |
|
109 |
+ self.training[skill] = degree |
|
110 |
+ self.points -= self.training_cost |
|
111 |
+ |
|
112 |
+ def __getattr__(self, key): |
|
113 |
+ skills = object.__getattribute__(self, 'skills') |
|
114 |
+ if key in self.skills.skills: |
|
115 |
+ return self.training[key] |
|
116 |
+ else: |
|
117 |
+ raise |
|
118 |
+ def __setattr__(self, key, value): |
|
119 |
+ if hasattr(self, 'skills') and key in self.skills.skills: |
|
120 |
+ if -1 <= value <= 3: |
|
121 |
+ self.training[key] = int(round(value)) |
|
122 |
+ else: |
|
123 |
+ raise AttributeError( |
|
124 |
+ 'cannot set training of %s to %d, (out of range [-1,3])' % (key, value) |
|
125 |
+ ) |
|
126 |
+ else: |
|
127 |
+ object.__setattr__(self, key, value) |
|
128 |
+ |
|
129 |
+ |
|
130 |
+class Equipment(object): pass |
|
131 |
+class Weapon(Equipment): pass |
|
132 |
+class Armor(Equipment): pass |
|
133 |
+ |
|
134 |
+ |
|
135 |
+class Slot(object): |
|
136 |
+ def __init__(self, name, type): |
|
137 |
+ self.name = name |
|
138 |
+ self.attr = 'slotted_%s' % name |
|
139 |
+ self.type = type |
|
140 |
+ |
|
141 |
+ def __get__(self, instance, owner): |
|
142 |
+ return self.getitem(instance) |
|
143 |
+ def __set__(self, instance, value): |
|
144 |
+ self.setitem(instance, value) |
|
145 |
+ |
|
146 |
+ def setitem(self, instance, value): |
|
147 |
+ if isinstance(value, self.type): |
|
148 |
+ setattr(instance, 'slotted_%s' % self.name, value) |
|
149 |
+ else: |
|
150 |
+ raise ValueError( |
|
151 |
+ 'Can\'t use an object of type %s in a slot of type %s' % (type(value), self.type) |
|
152 |
+ ) |
|
153 |
+ |
|
154 |
+ |
|
155 |
+ def getitem(self, instance): |
|
156 |
+ return getattr(instance, self.attr, None) |
|
157 |
+ |
|
158 |
+ def filled(self, instance): |
|
159 |
+ return self.getitem(instance) is not None |
|
160 |
+ |
|
161 |
+ @classmethod |
|
162 |
+ def add_slot(cls, name, type): |
|
163 |
+ def _inner(to_cls): |
|
164 |
+ inst = cls(name, type) |
|
165 |
+ setattr(to_cls, name, inst) |
|
166 |
+ if not hasattr(to_cls, 'equipment_slots'): |
|
167 |
+ to_cls.equipment_slots = Slots((name,inst)) |
|
168 |
+ else: |
|
169 |
+ to_cls.equipment_slots[name] = inst |
|
170 |
+ return to_cls |
|
171 |
+ return _inner |
|
172 |
+ |
|
173 |
+ |
|
174 |
+class Slots(collections.MutableMapping): |
|
175 |
+ def __init__(self, *args, **kw): |
|
176 |
+ self.slots = dict(args) |
|
177 |
+ self.slots.update(kw) |
|
178 |
+ self.slots_by_type = type_dict.TypeDict(__default=[]) |
|
179 |
+ for k in self.slots: |
|
180 |
+ slot = self.slots[k] |
|
181 |
+ self.slots_by_type[slot.type].append(slot) |
|
182 |
+ |
|
183 |
+ def __getitem__(self, key): |
|
184 |
+ return self.slots[key] |
|
185 |
+ def __setitem__(self, k, v): |
|
186 |
+ self.slots[k] = v |
|
187 |
+ def __delitem__(self, key): |
|
188 |
+ del self.slots[key] |
|
189 |
+ |
|
190 |
+ def __iter__(self): |
|
191 |
+ return iter(self.slots) |
|
192 |
+ def __len__(self): |
|
193 |
+ return len(self.slots) |
|
194 |
+ |
|
195 |
+ def slots_of_type(self, slot_type): |
|
196 |
+ return self.slots_by_type[slot_type] |
|
197 |
+ |
|
198 |
+class Race(object): |
|
199 |
+ registry = {} |
|
200 |
+ @classmethod |
|
201 |
+ def register(cls, new): |
|
202 |
+ cls.registry[new.__name__.lower()] = new |
|
203 |
+ |
|
204 |
+ @classmethod |
|
205 |
+ def random_race(cls): |
|
206 |
+ return random.choice(self.registry.values()) |
|
207 |
+ @property |
|
208 |
+ def name(self): |
|
209 |
+ return self.__class__.__name__.lower() |
|
210 |
+ |
|
211 |
+ allowed_professions = set() |
|
212 |
+ def __init__(self, attr): |
|
213 |
+ self.mod(attr) |
|
214 |
+ |
|
215 |
+ def allows_profession(self, prof): |
|
216 |
+ return prof in self.allowed_professions |
|
217 |
+ |
|
218 |
+@Race.register |
|
219 |
+class Human(Race): |
|
220 |
+ def mod(self, attr): |
|
221 |
+ attr.spt += 1 |
|
222 |
+ |
|
223 |
+@Race.register |
|
224 |
+class Elf(Race): |
|
225 |
+ allowed_professions = {'fighter', 'wizard', 'thief'} |
|
226 |
+ def mod(self, attr): |
|
227 |
+ attr.int += 1 |
|
228 |
+ |
|
229 |
+@Race.register |
|
230 |
+class Dwarf(Race): |
|
231 |
+ allowed_professions = {'fighter', 'Priest', 'thief'} |
|
232 |
+ def mod(self, atr): |
|
233 |
+ attr.str += 1 |
|
234 |
+ |
|
235 |
+@Race.register |
|
236 |
+class Hobbit(Race): |
|
237 |
+ allowed_professions = {'thief', 'barbarian'} |
|
238 |
+ def mod(self, atr): |
|
239 |
+ attr.dex += 1 |
|
240 |
+ |
|
241 |
+ |
|
242 |
+class Sword(Weapon): |
|
243 |
+ attack = 1 |
|
244 |
+ damage_mod = 1 |
|
245 |
+ |
|
246 |
+ |
|
247 |
+HEALTHY = 0 |
|
248 |
+WOUNDED = 1 |
|
249 |
+KNOCKOUT = 2 |
|
250 |
+ |
|
251 |
+@Slot.add_slot('weapon', Weapon) |
|
252 |
+@Slot.add_slot('armor', Armor) |
|
253 |
+class Adventurer(object): |
|
254 |
+ @property |
|
255 |
+ def state(self): |
|
256 |
+ return self.attributes.state |
|
257 |
+ @state.setter |
|
258 |
+ def state(self, val): |
|
259 |
+ if val not in {HEALTHY, WOUNDED, KNOCKOUT}: |
|
260 |
+ raise ValueError('Value for state invalid') |
|
261 |
+ self.attributes.state = val |
|
262 |
+ |
|
263 |
+ def __init__(self, name, race, str, int, dex, spt): |
|
264 |
+ self.name = name |
|
265 |
+ self.attributes = Attributes(str, int, dex, spt) |
|
266 |
+ self.skills = Skills(self.attributes) |
|
267 |
+ self.race = Race.registry[race](self.attributes) |
|
268 |
+ |
|
269 |
+ def wield(self, slot, equipment): |
|
270 |
+ turns = 1 |
|
271 |
+ if self.equipment_slots[slot].filled(self): |
|
272 |
+ turns += 1 |
|
273 |
+ self.equipment_slots[slot].setitem(self, equipment) |
|
274 |
+ return turns |
|
275 |
+ |
|
276 |
+ def attack(self, opponent, type='fighting', num_allies=0): |
|
277 |
+ dieroll = dice.DieRoll() |
|
278 |
+ |
|
279 |
+ ally_bonus = -1 if num_allies > 0 else 0 |
|
280 |
+ # TODO: implement professions, figure out how to mod ally_bonus for thieves cleanly |
|
281 |
+ dieroll.add_adjustment(add=ally_bonus) |
|
282 |
+ |
|
283 |
+ pass_, damage = self.skills.check(type, dieroll) |
|
284 |
+ if pass_: |
|
285 |
+ if self.weapon is not None: |
|
286 |
+ damage += self.weapon.damage_mod |
|
287 |
+ print '%s inflicts %d damage upon %s' % (self.name, damage, opponent.name) |
|
288 |
+ opponent.take_damage(damage) |
|
289 |
+ |
|
290 |
+ def take_damage(self, damage): |
|
291 |
+ if self.armor is not None: |
|
292 |
+ damage += self.armor.damage_mod |
|
293 |
+ |
|
294 |
+ dieroll = dice.DieRoll() |
|
295 |
+ dieroll.add_adjustment(-damage) |
|
296 |
+ |
|
297 |
+ result, damage = self.skills.check('toughness', dieroll) |
|
298 |
+ |
|
299 |
+ if not result: |
|
300 |
+ if damage > 0: |
|
301 |
+ self.state += 1 |
|
302 |
+ print '%s takes %d damage and is now %s' % (self.name, damage, ['healthy', 'wounded', 'ko'][self.state if self.state < 3 else 2]) |
|
303 |
+ if self.state >= KNOCKOUT: |
|
304 |
+ self.die() |
|
305 |
+ |
|
306 |
+ def die(self): pass |
|
307 |
+ |
|
308 |
+def get_stats(): |
|
309 |
+ stats = [7,7,7,7] |
|
310 |
+ import random |
|
311 |
+ mod = +1 |
|
312 |
+ for x in range(100): |
|
313 |
+ idx = random.choice([x for x in range(len(stats)) if stats[x] > 5]) |
|
314 |
+ stats[idx] += mod |
|
315 |
+ mod *= -1 |
|
316 |
+ print sum(stats), stats |
|
317 |
+ return stats |
|
318 |
+ |
|
319 |
+ |
|
320 |
+a = Adventurer('bob', 'elf', *get_stats()) |
|
321 |
+b = Adventurer('bill', 'elf', *get_stats()) |
|
322 |
+ |
|
323 |
+while KNOCKOUT not in {a.state, b.state}: |
|
324 |
+ a.attack(b) |
|
325 |
+ b.attack(a) |
|
326 |
+ |
|
327 |
+print a.name, 'is', ['healthy', 'wounded', 'ko'][a.state if a.state < 3 else 2] |
|
328 |
+print b.name, 'is', ['healthy', 'wounded', 'ko'][b.state if b.state < 3 else 2] |
0 | 329 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,173 @@ |
1 |
+import random |
|
2 |
+import collections |
|
3 |
+import itertools |
|
4 |
+import re |
|
5 |
+ |
|
6 |
+basestr = (str,unicode) |
|
7 |
+ |
|
8 |
+class DieJar(object): |
|
9 |
+ parser = re.compile(r'^(?:_?(?P<num>[1-9][0-9]*))?d(?P<sides>[1-9][0-9]*)$') |
|
10 |
+ def __getattr__(self, key): |
|
11 |
+ out = [] |
|
12 |
+ for sect in key.split('__'): |
|
13 |
+ out.extend(self.parse_section(sect)) |
|
14 |
+ return Dice(out) |
|
15 |
+ |
|
16 |
+ def parse_section(self, key): |
|
17 |
+ out = self.parser.match(key) |
|
18 |
+ if out is None: |
|
19 |
+ raise AttributeError('invalid die specification') |
|
20 |
+ else: |
|
21 |
+ dct = out.groupdict() |
|
22 |
+ dct['sides'] = int(dct['sides']) |
|
23 |
+ dct['num'] = int(dct['num'] or 1) |
|
24 |
+ dice = [Die(dct['sides']) for __ in range(dct['num'])] |
|
25 |
+ return dice |
|
26 |
+ |
|
27 |
+ |
|
28 |
+ |
|
29 |
+ |
|
30 |
+ |
|
31 |
+def iterable(obj): |
|
32 |
+ return (not isinstance(obj, basestr)) and isinstance(obj, collections.Iterable) |
|
33 |
+ |
|
34 |
+def flatten(lis): |
|
35 |
+ return itertools.chain(*[(x if iterable(x) else [x]) for x in lis]) |
|
36 |
+ |
|
37 |
+class Die(object): |
|
38 |
+ 'Note: hashed by sides, min and step. Consequently not necessarily preserved when used as a dictionary key' |
|
39 |
+ def __init__(self, sides, min=1, step=1): |
|
40 |
+ self.sides = sides |
|
41 |
+ self.min = min |
|
42 |
+ self.step = 1 |
|
43 |
+ self.sides = sides |
|
44 |
+ self.choices = range(min, (min+sides)*step, step) |
|
45 |
+ self._value = None |
|
46 |
+ self.combine_func = lambda a,b: a+b |
|
47 |
+ |
|
48 |
+ def roll(self): |
|
49 |
+ self._value = random.choice(self.choices) |
|
50 |
+ return self._value |
|
51 |
+ |
|
52 |
+ @property |
|
53 |
+ def value(self): |
|
54 |
+ if self._value is None: self.roll() |
|
55 |
+ return self._value |
|
56 |
+ |
|
57 |
+ def combine(self, other): |
|
58 |
+ if hasattr(other, 'value'): other = other.value |
|
59 |
+ return self.combine_func(self.value, other.value) |
|
60 |
+ |
|
61 |
+ def __str__(self): |
|
62 |
+ base = 'd%d' % self.sides |
|
63 |
+ if self.min != 1: |
|
64 |
+ base = '%s+%d' % (base,self.min) |
|
65 |
+ if self.step != 1: |
|
66 |
+ base = '(%s)*%d' % (base, self.step) |
|
67 |
+ return base |
|
68 |
+ |
|
69 |
+ def __eq__(self, other): |
|
70 |
+ return (self.sides == other.sides) and (self.min == other.min) and (self.step == other.step) |
|
71 |
+ |
|
72 |
+ def __hash__(self): |
|
73 |
+ return hash((self.sides,self.min,self.step)) |
|
74 |
+ |
|
75 |
+class Dice(collections.Sequence): |
|
76 |
+ 'A collection of dice, can be initialized either with Die instances or lists of Die instances' |
|
77 |
+ |
|
78 |
+ def __init__(self, *dice, **kw): |
|
79 |
+ self.dice = list(flatten(dice)) |
|
80 |
+ self.combiner = kw.get('combiner', lambda a,b:a+b) |
|
81 |
+ |
|
82 |
+ def __getitem__(self, k): return self.dice[k] |
|
83 |
+ def __len__(self): return len(self.dice) |
|
84 |
+ |
|
85 |
+ def roll(self): |
|
86 |
+ return reduce(self.combiner, (die.roll() for die in self.dice)) |
|
87 |
+ def __str__(self): |
|
88 |
+ groups = collections.Counter(self.dice) |
|
89 |
+ out = [] |
|
90 |
+ dice = sorted(groups, key=lambda k:-groups[k]) |
|
91 |
+ if len(dice) > 1: |
|
92 |
+ for die in dice[:-1]: |
|
93 |
+ count = groups[die] |
|
94 |
+ out.append('%d%s' % (count,die)) |
|
95 |
+ out = ','.join(out) |
|
96 |
+ out = ' and '.join([out, str(dice[-1])]) |
|
97 |
+ else: |
|
98 |
+ out = '%d%s' % (groups[dice[0]],dice[0]) |
|
99 |
+ |
|
100 |
+ |
|
101 |
+ return out |
|
102 |
+ |
|
103 |
+ |
|
104 |
+ |
|
105 |
+MULT=1 |
|
106 |
+ADD=2 |
|
107 |
+class DieRoll(object): |
|
108 |
+ def __init__(self, dice=None, adjustments=None): |
|
109 |
+ self.dice = dice |
|
110 |
+ self.adjustments = [] if adjustments is None else adjustments |
|
111 |
+ |
|
112 |
+ def add_adjustment(self, add=None, mult=None): |
|
113 |
+ if None not in {add,mult}: raise ValueError('can\'t add two adjustments at once') |
|
114 |
+ elif {add,mult} - {None} == set(): raise ValueError('No adjustment given') |
|
115 |
+ |
|
116 |
+ if add is not None: |
|
117 |
+ adjustment = (ADD,add) |
|
118 |
+ elif mult is not None: |
|
119 |
+ adjustment = (MULT,mult) |
|
120 |
+ self.adjustments.append(adjustment) |
|
121 |
+ |
|
122 |
+ def roll(self): |
|
123 |
+ result = self.dice.roll() |
|
124 |
+ for type,bonus in self.adjustments: |
|
125 |
+ if type == MULT: |
|
126 |
+ result *= type |
|
127 |
+ elif type == ADD: |
|
128 |
+ result += bonus |
|
129 |
+ return result |
|
130 |
+ |
|
131 |
+if __name__ == '__main__': |
|
132 |
+ import unittest |
|
133 |
+ tests = unittest.TestSuite() |
|
134 |
+ inst = lambda a:a() |
|
135 |
+ |
|
136 |
+ class TestDie(unittest.TestCase): |
|
137 |
+ def test_roll_plain(self): |
|
138 |
+ a = Die(6) |
|
139 |
+ for __ in range(40): |
|
140 |
+ self.assertLess(a.roll(), 7) |
|
141 |
+ self.assertGreater(a.roll(), 0) |
|
142 |
+ def test_roll_min(self): |
|
143 |
+ a = Die(6,min=4) |
|
144 |
+ for __ in range(40): |
|
145 |
+ self.assertLess(a.roll(), 6+4+1) |
|
146 |
+ self.assertGreater(a.roll(), 3) |
|
147 |
+ def test_roll_step(self): |
|
148 |
+ a = Die(6,step=2) |
|
149 |
+ for __ in range(40): |
|
150 |
+ self.assertLess(a.roll(), 6+(6*2)+1) |
|
151 |
+ self.assertGreater(a.roll(), 0) |
|
152 |
+ self.assertTrue((a.roll()-1) % 2 == 0) |
|
153 |
+ def test_str(self): |
|
154 |
+ self.assertEqual(str(Die(6)), 'd6') |
|
155 |
+ self.assertEqual(str(Die(6,min=2)), 'd6+2') |
|
156 |
+ |
|
157 |
+ class TestDice(unittest.TestCase): |
|
158 |
+ def test_init(self): |
|
159 |
+ dice = [Die(6) for __ in range(20)] |
|
160 |
+ a = Dice(dice) |
|
161 |
+ self.assertEqual(dice,a.dice) |
|
162 |
+ a = Dice(*dice) |
|
163 |
+ self.assertEqual(dice,a.dice) |
|
164 |
+ def test_roll(self): |
|
165 |
+ dice = [Die(6) for __ in range(2)] |
|
166 |
+ dice = Dice(dice) |
|
167 |
+ self.assertGreater(dice.roll(), 1) |
|
168 |
+ self.assertLess(dice.roll(), 13) |
|
169 |
+ |
|
170 |
+ class TestDieRoll(unittest.TestCase): |
|
171 |
+ pass |
|
172 |
+ |
|
173 |
+ unittest.main() |
0 | 174 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,74 @@ |
1 |
+# Copyright (c) 2011 Edward Langley |
|
2 |
+# All rights reserved. |
|
3 |
+# |
|
4 |
+# Redistribution and use in source and binary forms, with or without |
|
5 |
+# modification, are permitted provided that the following conditions |
|
6 |
+# are met: |
|
7 |
+# |
|
8 |
+# Redistributions of source code must retain the above copyright notice, |
|
9 |
+# this list of conditions and the following disclaimer. |
|
10 |
+# |
|
11 |
+# Redistributions in binary form must reproduce the above copyright |
|
12 |
+# notice, this list of conditions and the following disclaimer in the |
|
13 |
+# documentation and/or other materials provided with the distribution. |
|
14 |
+# |
|
15 |
+# Neither the name of the project's author nor the names of its |
|
16 |
+# contributors may be used to endorse or promote products derived from |
|
17 |
+# this software without specific prior written permission. |
|
18 |
+# |
|
19 |
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|
20 |
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|
21 |
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
|
22 |
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|
23 |
+# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
24 |
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED |
|
25 |
+# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
|
26 |
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
|
27 |
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
|
28 |
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
|
29 |
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|
30 |
+ |
|
31 |
+import random |
|
32 |
+import numpy.random |
|
33 |
+ |
|
34 |
+class MyRand(random.Random): |
|
35 |
+ def random(self): |
|
36 |
+ return numpy.random.random() |
|
37 |
+ def seed(self, a=None): |
|
38 |
+ if a is None: |
|
39 |
+ import time |
|
40 |
+ a = time.time() * 10000 |
|
41 |
+ print a |
|
42 |
+ numpy.random.seed(int(a)) |
|
43 |
+ self.gauss_next = None |
|
44 |
+ def setstate(self, state): |
|
45 |
+ numpy.random.set_state(state) |
|
46 |
+ def getstate(self): |
|
47 |
+ return numpy.random.get_state() |
|
48 |
+ def jumpahead(self, n): |
|
49 |
+ self.seed() |
|
50 |
+ |
|
51 |
+_inst = random._inst = MyRand() |
|
52 |
+random.seed = _inst.seed |
|
53 |
+random.random = _inst.random |
|
54 |
+random.uniform = _inst.uniform |
|
55 |
+random.triangular = _inst.triangular |
|
56 |
+random.randint = _inst.randint |
|
57 |
+random.choice = _inst.choice |
|
58 |
+random.randrange = _inst.randrange |
|
59 |
+random.sample = _inst.sample |
|
60 |
+random.shuffle = _inst.shuffle |
|
61 |
+random.normalvariate = _inst.normalvariate |
|
62 |
+random.lognormvariate = _inst.lognormvariate |
|
63 |
+random.expovariate = _inst.expovariate |
|
64 |
+random.vonmisesvariate = _inst.vonmisesvariate |
|
65 |
+random.gammavariate = _inst.gammavariate |
|
66 |
+random.gauss = _inst.gauss |
|
67 |
+random.betavariate = _inst.betavariate |
|
68 |
+random.paretovariate = _inst.paretovariate |
|
69 |
+random.weibullvariate = _inst.weibullvariate |
|
70 |
+random.getstate = _inst.getstate |
|
71 |
+random.setstate = _inst.setstate |
|
72 |
+random.jumpahead = _inst.jumpahead |
|
73 |
+random.getrandbits = _inst.getrandbits |
|
74 |
+ |
0 | 75 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,72 @@ |
1 |
+import collections |
|
2 |
+import copy |
|
3 |
+ |
|
4 |
+class TypeDict(collections.MutableMapping): |
|
5 |
+ def __init__(self, *args, **kw): |
|
6 |
+ if '__default' in kw: |
|
7 |
+ self.default = kw.pop('__default') |
|
8 |
+ |
|
9 |
+ self.store = dict(args) |
|
10 |
+ self.store.update(kw) |
|
11 |
+ |
|
12 |
+ def get_store(self, cls): |
|
13 |
+ cur = self.store |
|
14 |
+ bases = cls.__bases__ |
|
15 |
+ if bases: |
|
16 |
+ head, tail = bases[0], bases[1:] |
|
17 |
+ while tail: |
|
18 |
+ cur = self.store.setdefault(head, {}) |
|
19 |
+ head, tail = tail[0], tail[1:] |
|
20 |
+ return cur |
|
21 |
+ |
|
22 |
+ def __getitem__(self, cls): |
|
23 |
+ store = self.get_store(cls) |
|
24 |
+ try: |
|
25 |
+ print store |
|
26 |
+ return store[cls] |
|
27 |
+ except KeyError: |
|
28 |
+ if hasattr(self, 'default'): |
|
29 |
+ return copy.copy(self.default) |
|
30 |
+ else: |
|
31 |
+ raise |
|
32 |
+ |
|
33 |
+ def __setitem__(self, cls, value): |
|
34 |
+ store = self.get_store(cls) |
|
35 |
+ store[cls] = value |
|
36 |
+ def __delitem__(self, cls): |
|
37 |
+ store = self.get_store(cls) |
|
38 |
+ del store[cls] |
|
39 |
+ |
|
40 |
+ def __iter__(self): |
|
41 |
+ return TypeDictIterator(self) |
|
42 |
+ def __len__(self): |
|
43 |
+ return len(self.slots) |
|
44 |
+ |
|
45 |
+class _Null: pass |
|
46 |
+ |
|
47 |
+class TypeDictIterator(collections.Iterator): |
|
48 |
+ def __init__(self, dct): |
|
49 |
+ self.dct = dct |
|
50 |
+ self.dcts = [dct] |
|
51 |
+ self.iters = [iter(dct)] |
|
52 |
+ self.cur = self.iters[-1] |
|
53 |
+ |
|
54 |
+ def next(self): |
|
55 |
+ result = _Null |
|
56 |
+ try: result = self.cur.next() |
|
57 |
+ except StopIteration: |
|
58 |
+ self.iters.pop() |
|
59 |
+ self.dcts.pop() |
|
60 |
+ if self.iters: |
|
61 |
+ self.cur = self.iters[-1] |
|
62 |
+ result = self.cur.next() |
|
63 |
+ else: |
|
64 |
+ raise StopIteration |
|
65 |
+ |
|
66 |
+ if result in self.dcts[-1] and self.dcts[-1][result]: |
|
67 |
+ self.iters.append(iter(self.dcts[-1][result])) |
|
68 |
+ self.cur = self.iters[-1] |
|
69 |
+ |
|
70 |
+ return result |
|
71 |
+ |
|
72 |
+ |
... | ... |
@@ -1,3 +1,7 @@ |
1 |
+import libs.patch_random |
|
2 |
+import random |
|
3 |
+random.seed(2) |
|
4 |
+ |
|
1 | 5 |
import libtcodpy as tc |
2 | 6 |
import numpy as np |
3 | 7 |
|
... | ... |
@@ -19,14 +23,17 @@ class Application(object): |
19 | 23 |
self.events = events.EventHandler() |
20 | 24 |
player.ArrowHandler(self.player, self.events) |
21 | 25 |
|
22 |
- |
|
23 |
- |
|
26 |
+ self.actors = [] |
|
27 |
+ for x in range(40): |
|
28 |
+ self.actors.append(player.Actor(random.randrange(100), random.randrange(63), self.map)) |
|
24 | 29 |
|
25 | 30 |
tc.sys_set_fps(60) |
26 | 31 |
|
27 | 32 |
def init(self): |
28 | 33 |
self.screen.init("test") |
29 | 34 |
self.player.draw() |
35 |
+ for actor in self.actors: |
|
36 |
+ actor.draw() |
|
30 | 37 |
|
31 | 38 |
def run(self): |
32 | 39 |
while not tc.console_is_window_closed(): |
... | ... |
@@ -53,7 +53,7 @@ class Map(object): |
53 | 53 |
ox,oy = object.pos |
54 | 54 |
x = squeeze(ox+dx, 0, self.width-1) |
55 | 55 |
y = squeeze(oy+dy, 0, self.height-1) |
56 |
- if not self.fov.is_passable((x,y)): |
|
56 |
+ if not self.is_passable((x,y)): |
|
57 | 57 |
x,y = ox,oy |
58 | 58 |
|
59 | 59 |
else: |
... | ... |
@@ -61,7 +61,7 @@ class Map(object): |
61 | 61 |
tx,ty = ox+dx, oy+dy |
62 | 62 |
gx,gy = ox,oy |
63 | 63 |
for x,y in libs.bresenham.line(ox,oy, tx,ty, 1): |
64 |
- if not self.fov.is_passable((x,y)): break |
|
64 |
+ if not self.is_passable((x,y)): break |
|
65 | 65 |
else: gx,gy = x,y |
66 | 66 |
x,y = gx,gy |
67 | 67 |
|
... | ... |
@@ -75,6 +75,19 @@ class Map(object): |
75 | 75 |
if (x,y) in self.overlays and self.overlays[x,y] == []: |
76 | 76 |
self.overlays.pop((x,y)) |
77 | 77 |
|
78 |
+ def set_pov(self, pov): |
|
79 |
+ self.pov = pov |
|
80 |
+ |
|
81 |
+ def get_visible_objects(self): |
|
82 |
+ o,r = self.pov |
|
83 |
+ results = set() |
|
84 |
+ for x,y in self.overlays: |
|
85 |
+ if self.fov.is_visible(o,r, (x,y)): |
|
86 |
+ results.update(self.overlays[x,y]) |
|
87 |
+ return results |
|
88 |
+ |
|
89 |
+ |
|
90 |
+ |
|
78 | 91 |
def get_rgb(self, colors, fg=True,slices=(slice(0),slice(0))): |
79 | 92 |
result = np.rollaxis(colors[slices], 2) |
80 | 93 |
return [x.transpose() for x in result] |
... | ... |
@@ -123,6 +136,12 @@ class Map(object): |
123 | 136 |
def dim(self): |
124 | 137 |
return self.width, self.height |
125 | 138 |
|
139 |
+ def is_passable(self, coord): |
|
140 |
+ if coord in self.overlays and any(x.blocks for x in self.overlays[coord]): |
|
141 |
+ return False |
|
142 |
+ else: |
|
143 |
+ return self.fov.is_passable(coord) |
|
144 |
+ |
|
126 | 145 |
|
127 | 146 |
class FovCache(object): |
128 | 147 |
# TODO: get allow updates to base_map |
... | ... |
@@ -1,8 +1,10 @@ |
1 | 1 |
import libtcodpy as tc |
2 |
+import libs.combat |
|
2 | 3 |
|
3 | 4 |
class Actor(object): |
4 | 5 |
char = ord('@') |
5 | 6 |
color = (255,0,0) |
7 |
+ blocks = True |
|
6 | 8 |
|
7 | 9 |
@property |
8 | 10 |
def pos(self): |
... | ... |
@@ -20,11 +22,20 @@ class Actor(object): |
20 | 22 |
dx, dy = self.map.move(self, dx,dy) |
21 | 23 |
self.x += dx |
22 | 24 |
self.y += dy |
23 |
- self.map.pov = (self.pos, 10) |
|
25 |
+ |
|
26 |
+ def tick(self): |
|
27 |
+ pass |
|
24 | 28 |
|
25 | 29 |
class Player(Actor): |
26 | 30 |
char = ord('@') |
27 | 31 |
color = (255,255,255) |
32 |
+ light_radius = 10 |
|
33 |
+ def __init__(self, x,y, map): |
|
34 |
+ Actor.__init__(self, x,y, map) |
|
35 |
+ self.map.set_pov((self.pos, self.light_radius)) |
|
36 |
+ def move(self, dx, dy): |
|
37 |
+ Actor.move(self, dx,dy) |
|
38 |
+ self.map.set_pov((self.pos, self.light_radius)) |
|
28 | 39 |
|
29 | 40 |
class ArrowHandler(object): |
30 | 41 |
def __init__(self, player, eh): |