Browse code
Merge branch 'release/0.1'
edwlan authored on 31/07/2013 01:40:01
Showing 34 changed files
Showing 34 changed files
- .gitignore
- blobs/.gitignore
- blobs/darwin/libtcod.dylib
- blobs/darwin/libtcodgui.dylib
- blobs/darwin/libtcodxx.dylib
- libtcod.so
- libtcod_debug.so
- libtcodgui.so
- libtcodgui_debug.so
- libtcodxx.so
- libtcodxx_debug.so
- blobs/terminal.png
- data/terrain.yml
- libs/__init__.py
- libs/bresenham.py
- libs/cache.py
- libs/combat.py
- libs/combat_parts/__init__.py
- libs/combat_parts/constants.py
- libs/combat_parts/equipment.py
- libs/combat_parts/races.py
- libs/combat_parts/skills.py
- libs/dice.py
- libs/overlay.py
- libs/patch_random.py
- libs/type_dict.py
- main.py
- makelinks.sh
- src/__init__.py
- src/console.py
- src/events.py
- src/map.py
- src/player.py
- terminal.png
26 | 30 |
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 |
1 | 38 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,36 @@ |
1 |
+from __future__ import print_function, division |
|
2 |
+ |
|
3 |
+# from: http://roguebasin.roguelikedevelopment.org/index.php?title=Bresenham's_Line_Algorithm#Python |
|
4 |
+ |
|
5 |
+def line(x0, y0, x1, y1, wd): |
|
6 |
+ dx = abs(x1-x0); sx = 1 if x0 < x1 else -1 |
|
7 |
+ dy = abs(y1-y0); sy = 1 if y0 < y1 else -1 |
|
8 |
+ err = dx-dy |
|
9 |
+ ed = 1 if dx + dy == 0 else (dx*dx + dy*dy)**0.5 |
|
10 |
+ |
|
11 |
+ e2 = x2 = y2 = 0 |
|
12 |
+ |
|
13 |
+ wd = (wd + 1) / 2.0 |
|
14 |
+ while True: |
|
15 |
+ yield (x0,y0) |
|
16 |
+ e2 = err; x2 = x0 |
|
17 |
+ if 2*e2 >= -dx: |
|
18 |
+ e2 += dx |
|
19 |
+ y2 = y0 |
|
20 |
+ while e2 < ed*wd and (y1 != y2 or dx > dy): |
|
21 |
+ yield (x0, y2) |
|
22 |
+ y2 += sy |
|
23 |
+ e2 += dx |
|
24 |
+ if x0 == x1: break |
|
25 |
+ e2 = err |
|
26 |
+ err -= dy |
|
27 |
+ x0 += sx |
|
28 |
+ if 2*e2 <= dy: |
|
29 |
+ e2 = dx-e2 |
|
30 |
+ while e2 < ed*wd and (x1 != x2 or dx < dy): |
|
31 |
+ yield (x2, y0) |
|
32 |
+ x2 += sx |
|
33 |
+ e2 += dy |
|
34 |
+ if y0 == y1: break |
|
35 |
+ err += dx |
|
36 |
+ y0 += sy |
0 | 10 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,122 @@ |
1 |
+from . import dice |
|
2 |
+ |
|
3 |
+from .combat_parts import races |
|
4 |
+from .combat_parts import equipment |
|
5 |
+from .combat_parts import skills |
|
6 |
+from .combat_parts import constants as const |
|
7 |
+ |
|
8 |
+import collections |
|
9 |
+import random |
|
10 |
+ |
|
11 |
+def get_stats(sum_): |
|
12 |
+ stats = [sum_/4]*4 |
|
13 |
+ if sum(stats) != sum_: |
|
14 |
+ stats[0] += sum_ - sum(stats) |
|
15 |
+ |
|
16 |
+ mod = +1 |
|
17 |
+ for x in range(100): |
|
18 |
+ idx = random.choice([x for x in range(len(stats)) if stats[x] > 3]) |
|
19 |
+ stats[idx] += mod |
|
20 |
+ mod *= -1 |
|
21 |
+ return stats |
|
22 |
+ |
|
23 |
+class Attributes(object): |
|
24 |
+ @classmethod |
|
25 |
+ def randomize(cls, sum_): |
|
26 |
+ chosen_stats = get_stats(sum_) |
|
27 |
+ return cls(*chosen_stats) |
|
28 |
+ |
|
29 |
+ def __init__(self, strength, intellect, dexterity, spirit): |
|
30 |
+ self.str = strength |
|
31 |
+ self.int = intellect |
|
32 |
+ self.dex = dexterity |
|
33 |
+ self.spt = spirit |
|
34 |
+ self.state = const.HEALTHY |
|
35 |
+ def __str__(self): |
|
36 |
+ return '%s %s %s %s %s' % (self.str, self.int, self.dex, self.spt, self.state) |
|
37 |
+ |
|
38 |
+ |
|
39 |
+@equipment.Slot.add_slot('weapon', equipment.Weapon) |
|
40 |
+@equipment.Slot.add_slot('armor', equipment.Armor) |
|
41 |
+class Adventurer(object): |
|
42 |
+ @property |
|
43 |
+ def state(self): |
|
44 |
+ return self.attributes.state |
|
45 |
+ @state.setter |
|
46 |
+ def state(self, val): |
|
47 |
+ if val < 0: val == 0 |
|
48 |
+ if val > const.KNOCKOUT: val = const.KNOCKOUT |
|
49 |
+ self.attributes.state = val |
|
50 |
+ |
|
51 |
+ @classmethod |
|
52 |
+ def randomize(cls, stat_sum=28): |
|
53 |
+ name = '%s %s%s' % ( |
|
54 |
+ random.choice(['Bill', 'Bob', 'Jerry']), |
|
55 |
+ random.choice(['Longnose', 'Short-Tail', 'Squint']), |
|
56 |
+ random.choice(['', ' the Great', ' The Cross-eyed', ' The Magnificent']) |
|
57 |
+ ) |
|
58 |
+ race = races.Race.random_race() |
|
59 |
+ attr = Attributes.randomize(stat_sum) |
|
60 |
+ print attr |
|
61 |
+ return cls(name, race, attr) |
|
62 |
+ |
|
63 |
+ @classmethod |
|
64 |
+ def with_stats(cls, name, race, str, int, dex, spt): |
|
65 |
+ attr = Attributes(str, int, dex, spt) |
|
66 |
+ return cls(name, race, attr) |
|
67 |
+ |
|
68 |
+ def __init__(self, name, race, attr): |
|
69 |
+ self.name = name |
|
70 |
+ self.attributes = attr |
|
71 |
+ self.skills = skills.Skills(self.attributes) |
|
72 |
+ self.race = races.Race.registry[race](self.attributes) |
|
73 |
+ |
|
74 |
+ def wield(self, slot, equipment): |
|
75 |
+ turns = 1 |
|
76 |
+ if self.equipment_slots[slot].filled(self): |
|
77 |
+ turns += 1 |
|
78 |
+ self.equipment_slots[slot].setitem(self, equipment) |
|
79 |
+ return turns |
|
80 |
+ |
|
81 |
+ def attack(self, opponent, type='fighting', num_allies=0): |
|
82 |
+ dieroll = dice.DieRoll() |
|
83 |
+ |
|
84 |
+ ally_bonus = -1 if num_allies > 0 else 0 |
|
85 |
+ # TODO: implement professions, figure out how to mod ally_bonus for thieves cleanly |
|
86 |
+ dieroll.add_adjustment(add=ally_bonus) |
|
87 |
+ |
|
88 |
+ pass_, damage = self.skills.check(type, dieroll) |
|
89 |
+ if pass_: |
|
90 |
+ if self.weapon is not None: |
|
91 |
+ damage += self.weapon.damage_mod |
|
92 |
+ print '%s inflicts %d damage upon %s' % (self.name, damage, opponent.name) |
|
93 |
+ opponent.take_damage(damage) |
|
94 |
+ |
|
95 |
+ def take_damage(self, damage): |
|
96 |
+ if self.armor is not None: |
|
97 |
+ damage += self.armor.damage_mod |
|
98 |
+ |
|
99 |
+ dieroll = dice.DieRoll() |
|
100 |
+ dieroll.add_adjustment(-damage) |
|
101 |
+ |
|
102 |
+ result, __ = self.skills.check('toughness', dieroll) |
|
103 |
+ |
|
104 |
+ if not result: |
|
105 |
+ if damage > 0: |
|
106 |
+ self.state += 1 |
|
107 |
+ print '%s takes %d damage and is now %s' % (self.name, damage, ['healthy', 'wounded', 'ko'][self.state if self.state < 3 else 2]) |
|
108 |
+ if self.state >= const.KNOCKOUT: |
|
109 |
+ self.die() |
|
110 |
+ |
|
111 |
+ def die(self): pass |
|
112 |
+ |
|
113 |
+if __name__ == '__main__': |
|
114 |
+ a = Adventurer.randomize(28) |
|
115 |
+ b = Adventurer.randomize(28) |
|
116 |
+ |
|
117 |
+ while const.KNOCKOUT not in {a.state, b.state}: |
|
118 |
+ a.attack(b) |
|
119 |
+ b.attack(a) |
|
120 |
+ |
|
121 |
+ print a.name, 'is', ['healthy', 'wounded', 'ko'][a.state if a.state < 3 else 2] |
|
122 |
+ print b.name, 'is', ['healthy', 'wounded', 'ko'][b.state if b.state < 3 else 2] |
0 | 6 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,77 @@ |
1 |
+from .. import type_dict |
|
2 |
+ |
|
3 |
+import collections |
|
4 |
+ |
|
5 |
+class Equipment(object): pass |
|
6 |
+class Weapon(Equipment): pass |
|
7 |
+class Armor(Equipment): pass |
|
8 |
+ |
|
9 |
+ |
|
10 |
+class Slot(object): |
|
11 |
+ def __init__(self, name, type): |
|
12 |
+ self.name = name |
|
13 |
+ self.attr = 'slotted_%s' % name |
|
14 |
+ self.type = type |
|
15 |
+ |
|
16 |
+ def __get__(self, instance, owner): |
|
17 |
+ return self.getitem(instance) |
|
18 |
+ def __set__(self, instance, value): |
|
19 |
+ self.setitem(instance, value) |
|
20 |
+ |
|
21 |
+ def setitem(self, instance, value): |
|
22 |
+ if isinstance(value, self.type): |
|
23 |
+ setattr(instance, 'slotted_%s' % self.name, value) |
|
24 |
+ else: |
|
25 |
+ raise ValueError( |
|
26 |
+ 'Can\'t use an object of type %s in a slot of type %s' % (type(value), self.type) |
|
27 |
+ ) |
|
28 |
+ |
|
29 |
+ |
|
30 |
+ def getitem(self, instance): |
|
31 |
+ return getattr(instance, self.attr, None) |
|
32 |
+ |
|
33 |
+ def filled(self, instance): |
|
34 |
+ return self.getitem(instance) is not None |
|
35 |
+ |
|
36 |
+ @classmethod |
|
37 |
+ def add_slot(cls, name, type): |
|
38 |
+ def _inner(to_cls): |
|
39 |
+ inst = cls(name, type) |
|
40 |
+ setattr(to_cls, name, inst) |
|
41 |
+ if not hasattr(to_cls, 'equipment_slots'): |
|
42 |
+ to_cls.equipment_slots = Slots((name,inst)) |
|
43 |
+ else: |
|
44 |
+ to_cls.equipment_slots[name] = inst |
|
45 |
+ return to_cls |
|
46 |
+ return _inner |
|
47 |
+ |
|
48 |
+ |
|
49 |
+class Slots(collections.MutableMapping): |
|
50 |
+ def __init__(self, *args, **kw): |
|
51 |
+ self.slots = dict(args) |
|
52 |
+ self.slots.update(kw) |
|
53 |
+ self.slots_by_type = type_dict.TypeDict(__default=[]) |
|
54 |
+ for k in self.slots: |
|
55 |
+ slot = self.slots[k] |
|
56 |
+ self.slots_by_type[slot.type].append(slot) |
|
57 |
+ |
|
58 |
+ def __getitem__(self, key): |
|
59 |
+ return self.slots[key] |
|
60 |
+ def __setitem__(self, k, v): |
|
61 |
+ self.slots[k] = v |
|
62 |
+ def __delitem__(self, key): |
|
63 |
+ del self.slots[key] |
|
64 |
+ |
|
65 |
+ def __iter__(self): |
|
66 |
+ return iter(self.slots) |
|
67 |
+ def __len__(self): |
|
68 |
+ return len(self.slots) |
|
69 |
+ |
|
70 |
+ def slots_of_type(self, slot_type): |
|
71 |
+ return self.slots_by_type[slot_type] |
|
72 |
+ |
|
73 |
+class Sword(Weapon): |
|
74 |
+ attack = 1 |
|
75 |
+ damage_mod = 1 |
|
76 |
+ |
|
77 |
+ |
0 | 78 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,48 @@ |
1 |
+import random |
|
2 |
+ |
|
3 |
+class Race(object): |
|
4 |
+ registry = {} |
|
5 |
+ @classmethod |
|
6 |
+ def register(cls, new): |
|
7 |
+ cls.registry[new.__name__.lower()] = new |
|
8 |
+ |
|
9 |
+ @classmethod |
|
10 |
+ def random_race(cls): |
|
11 |
+ return random.choice(cls.registry.keys()) |
|
12 |
+ |
|
13 |
+ @property |
|
14 |
+ def name(self): |
|
15 |
+ return self.__class__.__name__.lower() |
|
16 |
+ |
|
17 |
+ allowed_professions = set() |
|
18 |
+ def __init__(self, attr): |
|
19 |
+ self.mod(attr) |
|
20 |
+ |
|
21 |
+ def allows_profession(self, prof): |
|
22 |
+ return prof in self.allowed_professions |
|
23 |
+ |
|
24 |
+@Race.register |
|
25 |
+class Human(Race): |
|
26 |
+ def mod(self, attr): |
|
27 |
+ attr.spt += 1 |
|
28 |
+ |
|
29 |
+@Race.register |
|
30 |
+class Elf(Race): |
|
31 |
+ allowed_professions = {'fighter', 'wizard', 'thief'} |
|
32 |
+ def mod(self, attr): |
|
33 |
+ attr.int += 1 |
|
34 |
+ |
|
35 |
+@Race.register |
|
36 |
+class Dwarf(Race): |
|
37 |
+ allowed_professions = {'fighter', 'Priest', 'thief'} |
|
38 |
+ def mod(self, attr): |
|
39 |
+ attr.str += 1 |
|
40 |
+ |
|
41 |
+@Race.register |
|
42 |
+class Hobbit(Race): |
|
43 |
+ allowed_professions = {'thief', 'barbarian'} |
|
44 |
+ def mod(self, attr): |
|
45 |
+ attr.dex += 1 |
|
46 |
+ |
|
47 |
+ |
|
48 |
+ |
0 | 49 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,124 @@ |
1 |
+from ..dice import MULT, ADD |
|
2 |
+from .. import dice |
|
3 |
+from .constants import * |
|
4 |
+ |
|
5 |
+import collections |
|
6 |
+ |
|
7 |
+def health_mod(func): |
|
8 |
+ def _inner(self): |
|
9 |
+ mod = 0 |
|
10 |
+ if self.attr.state >= WOUNDED: |
|
11 |
+ mod = -3 |
|
12 |
+ result = func(self) |
|
13 |
+ result += mod |
|
14 |
+ return result |
|
15 |
+ return _inner |
|
16 |
+ |
|
17 |
+class Skills(object): |
|
18 |
+ def __init__(self, attr): |
|
19 |
+ self.skills = { |
|
20 |
+ 'agility','craft','fighting','knowledge', |
|
21 |
+ 'perception','persuasion','shooting','speed', |
|
22 |
+ 'stealth','toughness' |
|
23 |
+ } |
|
24 |
+ self.attr = attr |
|
25 |
+ self.training = Trainer(self) |
|
26 |
+ self.train = self.training.select |
|
27 |
+ |
|
28 |
+ def check(self, skill, dieroll=None): |
|
29 |
+ die = dice.DieJar().d20 |
|
30 |
+ if dieroll is None: |
|
31 |
+ dieroll = die |
|
32 |
+ else: |
|
33 |
+ dieroll.dice = dice.DieJar().d20 |
|
34 |
+ |
|
35 |
+ roll = dieroll.roll() |
|
36 |
+ if roll == 1: |
|
37 |
+ result = False |
|
38 |
+ if roll == 20: |
|
39 |
+ result = True |
|
40 |
+ else: |
|
41 |
+ result = roll < getattr(self, skill) |
|
42 |
+ return result, roll-result # (passes, difference) |
|
43 |
+ |
|
44 |
+ @property |
|
45 |
+ @health_mod |
|
46 |
+ def agility(self): |
|
47 |
+ return (self.attr.dex * 2) + self.training.agility |
|
48 |
+ @property |
|
49 |
+ @health_mod |
|
50 |
+ def craft(self): |
|
51 |
+ return self.attr.dex + self.attr.int + self.training.craft |
|
52 |
+ @property |
|
53 |
+ @health_mod |
|
54 |
+ def fighting(self): |
|
55 |
+ return self.attr.str + self.attr.int + self.training.fighting |
|
56 |
+ @property |
|
57 |
+ @health_mod |
|
58 |
+ def knowledge(self): |
|
59 |
+ return self.attr.int * 2 + self.training.knowledge |
|
60 |
+ @property |
|
61 |
+ @health_mod |
|
62 |
+ def perception(self): |
|
63 |
+ return self.attr.int + attr.spt + self.training.perception |
|
64 |
+ @property |
|
65 |
+ @health_mod |
|
66 |
+ def persuasion(self): |
|
67 |
+ return self.attr.spt * 2 + self.training.persuasion |
|
68 |
+ @property |
|
69 |
+ @health_mod |
|
70 |
+ def shooting(self): |
|
71 |
+ return self.attr.dex + self.attr.int + self.training.shooting |
|
72 |
+ @property |
|
73 |
+ @health_mod |
|
74 |
+ def speed(self): |
|
75 |
+ return self.attr.str + self.attr.dex + self.training.speed |
|
76 |
+ @property |
|
77 |
+ @health_mod |
|
78 |
+ def stealth(self): |
|
79 |
+ return self.attr.dex + self.attr.spt + self.training.stealth |
|
80 |
+ @property |
|
81 |
+ @health_mod |
|
82 |
+ def toughness(self): |
|
83 |
+ return self.attr.str + self.attr.spt + self.training.toughness |
|
84 |
+ |
|
85 |
+class Trainer(object): |
|
86 |
+ mods = dict( |
|
87 |
+ untrained=-1, |
|
88 |
+ familiar=0, |
|
89 |
+ trained=1, |
|
90 |
+ experienced=2, |
|
91 |
+ mastered=3 |
|
92 |
+ ) |
|
93 |
+ |
|
94 |
+ def __init__(self, skills): |
|
95 |
+ self.skills = skills |
|
96 |
+ self.training = collections.defaultdict(lambda: -1) |
|
97 |
+ self.points = 500 |
|
98 |
+ self.training_cost = 100 |
|
99 |
+ |
|
100 |
+ def select(self, skill, degree): |
|
101 |
+ if hasattr(degree, 'upper'): |
|
102 |
+ degree = self.mods.get(degree, 0) |
|
103 |
+ if self.points >= self.training_cost and degree in self.mods.values(): |
|
104 |
+ self.training[skill] = degree |
|
105 |
+ self.points -= self.training_cost |
|
106 |
+ |
|
107 |
+ def __getattr__(self, key): |
|
108 |
+ skills = object.__getattribute__(self, 'skills') |
|
109 |
+ if key in self.skills.skills: |
|
110 |
+ return self.training[key] |
|
111 |
+ else: |
|
112 |
+ raise |
|
113 |
+ def __setattr__(self, key, value): |
|
114 |
+ if hasattr(self, 'skills') and key in self.skills.skills: |
|
115 |
+ if -1 <= value <= 3: |
|
116 |
+ self.training[key] = int(round(value)) |
|
117 |
+ else: |
|
118 |
+ raise AttributeError( |
|
119 |
+ 'cannot set training of %s to %d, (out of range [-1,3])' % (key, value) |
|
120 |
+ ) |
|
121 |
+ else: |
|
122 |
+ object.__setattr__(self, key, value) |
|
123 |
+ |
|
124 |
+ |
0 | 125 |
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,119 @@ |
1 |
+import abc |
|
2 |
+import collections |
|
3 |
+from . import combat |
|
4 |
+ |
|
5 |
+class Overlay(object): |
|
6 |
+ __metaclass__ = abc.ABCMeta |
|
7 |
+ |
|
8 |
+ char = abc.abstractproperty() |
|
9 |
+ color = (0,0,0) |
|
10 |
+ blocks = False |
|
11 |
+ |
|
12 |
+ @property |
|
13 |
+ def pos(self): |
|
14 |
+ return self.x, self.y |
|
15 |
+ def __init__(self, x,y, map): |
|
16 |
+ self.events = collections.OrderedDict() |
|
17 |
+ self.x = x |
|
18 |
+ self.y = y |
|
19 |
+ self.map = map |
|
20 |
+ for key, value in self.handled_events.viewitems(): |
|
21 |
+ self.events.setdefault(key, []).extend(value) |
|
22 |
+ |
|
23 |
+ def draw(self): |
|
24 |
+ self.map.add(self) |
|
25 |
+ |
|
26 |
+ def register_event(self, event, cb): |
|
27 |
+ '''register a callback for an event |
|
28 |
+ |
|
29 |
+ if the callback returns a false value besides None, execution will end after that event''' |
|
30 |
+ self.events.setdefault(event, []).append(cb) |
|
31 |
+ |
|
32 |
+ def trigger_event(self, event, *args, **kw): |
|
33 |
+ if event not in self.events: |
|
34 |
+ raise ValueError('%r has no event %r' % (self, event)) |
|
35 |
+ |
|
36 |
+ for cb in self.events[event]: |
|
37 |
+ result = cb(self, *args, **kw) |
|
38 |
+ result = result is None or result # if the event returns a false value besides None, break |
|
39 |
+ if result == False: break |
|
40 |
+ |
|
41 |
+ handled_events = collections.OrderedDict() |
|
42 |
+ @classmethod |
|
43 |
+ def handle_event(cls, event): |
|
44 |
+ def _inner(cb): |
|
45 |
+ cls.handled_events.setdefault(event, []).append(cb) |
|
46 |
+ return cb |
|
47 |
+ return _inner |
|
48 |
+ |
|
49 |
+ |
|
50 |
+ |
|
51 |
+class Actor(Overlay): |
|
52 |
+ char = ord('@') |
|
53 |
+ color = (255,0,0) |
|
54 |
+ blocks = True |
|
55 |
+ |
|
56 |
+ @property |
|
57 |
+ def pos(self): |
|
58 |
+ return self.x, self.y |
|
59 |
+ |
|
60 |
+ def __init__(self, x,y, map, adventurer=None): |
|
61 |
+ super(Actor, self).__init__(x,y, map) |
|
62 |
+ self.inventory = [] |
|
63 |
+ if adventurer is None: |
|
64 |
+ adventurer = combat.Adventurer.randomize() |
|
65 |
+ self.adventurer = adventurer |
|
66 |
+ |
|
67 |
+ def move(self, dx, dy): |
|
68 |
+ dx, dy = self.map.move(self, dx,dy) |
|
69 |
+ self.x += dx |
|
70 |
+ self.y += dy |
|
71 |
+ |
|
72 |
+ def tick(self): |
|
73 |
+ result = True |
|
74 |
+ if self.adventurer.state >= 2: |
|
75 |
+ result = False |
|
76 |
+ self.char = ord('%') |
|
77 |
+ self.blocks = False |
|
78 |
+ return result |
|
79 |
+ |
|
80 |
+ def ishostile(self, other): |
|
81 |
+ return self.adventurer.state < 2 #TODO: implement factions |
|
82 |
+ |
|
83 |
+ def bump(self, other): |
|
84 |
+ print '%s bumped %s' % (type(self).__name__, type(other).__name__) |
|
85 |
+ if isinstance(other, Actor) and other.ishostile(self): |
|
86 |
+ self.adventurer.attack(other.adventurer) |
|
87 |
+ other.trigger_event('attacked', self) |
|
88 |
+ elif isinstance(other, Object): |
|
89 |
+ self.pickup(other) |
|
90 |
+ other.trigger_event('picked_up', self) |
|
91 |
+ |
|
92 |
+ other.trigger_event('bumped', self) |
|
93 |
+ |
|
94 |
+ @Overlay.handle_event('attacked') |
|
95 |
+ def attacked_by(self, other): |
|
96 |
+ if self.adventurer.skills.check('agility'): |
|
97 |
+ self.adventurer.attack(other.adventurer) |
|
98 |
+ |
|
99 |
+ @Overlay.handle_event('bumped') |
|
100 |
+ def bumped_by(self, other): |
|
101 |
+ print '%s was bumped by %s' % (type(self).__name__, type(other).__name__) |
|
102 |
+ |
|
103 |
+class Object(Overlay): |
|
104 |
+ item = None |
|
105 |
+ @Overlay.handle_event('picked_up') |
|
106 |
+ def picked_up_by(self, other): |
|
107 |
+ self.map.remove(self) |
|
108 |
+ |
|
109 |
+class Weapon(Object): |
|
110 |
+ item = None |
|
111 |
+ |
|
112 |
+class Potion(Object): |
|
113 |
+ char = ord('!') |
|
114 |
+ |
|
115 |
+class Scroll(Object): |
|
116 |
+ char = ord('!') |
|
117 |
+ |
|
118 |
+class Equipment(Object): |
|
119 |
+ pass |
0 | 120 |
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,106 +1,64 @@ |
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 |
|
4 |
-class Player(object): |
|
5 |
- @property |
|
6 |
- def pos(self): |
|
7 |
- return self.x, self.y |
|
8 |
- |
|
9 |
- def __init__(self, x,y, map): |
|
10 |
- self.x = x |
|
11 |
- self.y = y |
|
12 |
- self.char = ord('@') |
|
13 |
- self.color = (255,255,255) |
|
14 |
- self.map = map |
|
15 |
- def move(self, dx, dy): |
|
16 |
- self.map.move(self, dx,dy) |
|
17 |
- self.x += dx |
|
18 |
- self.y += dy |
|
19 |
- |
|
20 |
-class Console(object): |
|
21 |
- # TBI: must have a self.con member with the console to be drawn on |
|
22 |
- pass |
|
23 |
- |
|
24 |
-class Screen(object): |
|
25 |
- def __init__(self, w, h): |
|
26 |
- self.width = w |
|
27 |
- self.height = h |
|
28 |
- self.con = 0 |
|
29 |
- def init(self, title, fullscreen=False): |
|
30 |
- tc.console_init_root(self.width, self.height, title, fullscreen, 2) |
|
31 |
- return self |
|
32 |
- |
|
33 |
-class Map(object): |
|
34 |
- def __init__(self, w, h): |
|
35 |
- self.width = w |
|
36 |
- self.height = h |
|
37 |
- self.map = np.random.random_integers(ord('a'), ord('z'), (w,h) ) |
|
38 |
- self.fgcolors = np.random.random_integers(100,255, (w,h,3) ) |
|
39 |
- self.bgcolors = np.random.random_integers(0,100, (w,h,3) ) |
|
40 |
- overlays = {} # (x,y): { set( (char,fg), ... ) } |
|
41 |
- |
|
42 |
- def move(self, object, dx,dy): |
|
43 |
- overlays[object.pos].remove(object) |
|
44 |
- ox,oy = object.pos |
|
45 |
- x = ox+dx |
|
46 |
- y = oy+dy |
|
47 |
- if 0 < x < self.width and 0 < y < self.height: |
|
48 |
- self.overlays[x,y] = object |
|
49 |
- return x,y |
|
50 |
- #elif |
|
51 |
- |
|
52 |
- def get_rgb(self, fg=True,slices=(slice(0),slice(0))): |
|
53 |
- if fg: |
|
54 |
- return np.rollaxis(self.fgcolors[slices], 2) |
|
55 |
- else: |
|
56 |
- return np.rollaxis(self.bgcolors[slices], 2) |
|
57 |
- |
|
58 |
- def draw(self, con, tl=(0,0)): |
|
59 |
- br = tl[0]+con.width, tl[1]+con.height |
|
60 |
- slices = tuple(map(slice, tl,br)) |
|
61 |
- tc.console_fill_foreground(con.con, *self.get_rgb(slices=slices)) |
|
62 |
- tc.console_fill_background(con.con, *self.get_rgb(False,slices=slices)) |
|
63 |
- tc.console_fill_char(con.con, self.map[slices]) |
|
64 |
- |
|
65 |
-class EventHandler(object): |
|
66 |
- def __init__(self): |
|
67 |
- self.key = tc.Key() |
|
68 |
- self.mouse = tc.Mouse() |
|
69 |
- self.cbs = {} |
|
70 |
- |
|
71 |
- def tick(self): |
|
72 |
- tc.sys_check_for_event(tc.EVENT_KEY_PRESS|tc.EVENT_MOUSE|tc.KEY_PRESSED, |
|
73 |
- self.key, self.mouse |
|
74 |
- ) |
|
75 |
- |
|
76 |
- char = chr(self.key.c) |
|
77 |
- if char != '\x00' and char in self.cbs: |
|
78 |
- for cb in self.cbs[char]: |
|
79 |
- cb() |
|
80 |
- |
|
81 |
- elif self.key.vk in self.cbs: |
|
82 |
- for cb in self.cbs[self.key.vk]: |
|
83 |
- cb() |
|
8 |
+from libs import overlay |
|
9 |
+from libs import combat |
|
10 |
+from src import events |
|
11 |
+from src import player |
|
12 |
+from src import console |
|
13 |
+from src import map |
|
84 | 14 |
|
15 |
+import random |
|
16 |
+import bisect |
|
85 | 17 |
|
86 | 18 |
|
19 |
+WIDTH = 79 |
|
20 |
+HEIGHT = 46 |
|
87 | 21 |
class Application(object): |
88 | 22 |
def __init__(self): |
89 |
- self.screen = Screen(120,75) |
|
90 |
- self.map = Map(120,78) |
|
91 |
- self.player = Player(40,25, self.map) |
|
92 |
- self.events = EventHandler() |
|
23 |
+ self.screen = console.Screen(WIDTH, HEIGHT) |
|
24 |
+ self.terrain_registry = map.TerrainRegistry() |
|
25 |
+ self.terrain_registry.load_from_file('data/terrain.yml') |
|
26 |
+ self.map = map.Map(WIDTH,HEIGHT, self.terrain_registry) |
|
27 |
+ self.player = player.Player(4,5, self.map, combat.Adventurer.randomize()) |
|
28 |
+ self.events = events.EventHandler() |
|
29 |
+ player.ArrowHandler(self.player, self.events) |
|
30 |
+ |
|
31 |
+ self.actors = [] |
|
32 |
+ for x in range(40): |
|
33 |
+ self.actors.append(overlay.Actor(random.randrange(WIDTH), random.randrange(HEIGHT), self.map)) |
|
34 |
+ |
|
35 |
+ self.objects = [] |
|
36 |
+ for x in range(50): |
|
37 |
+ self.objects.append(overlay.Potion(random.randrange(WIDTH), random.randrange(HEIGHT), self.map)) |
|
93 | 38 |
|
94 | 39 |
tc.sys_set_fps(60) |
95 | 40 |
|
41 |
+ def update_actors(self): |
|
42 |
+ to_pop = [] |
|
43 |
+ for idx,actor in enumerate(self.actors): |
|
44 |
+ if not actor.tick(): |
|
45 |
+ bisect.insort(to_pop, idx) |
|
46 |
+ for pop in reversed(to_pop): |
|
47 |
+ self.actors.pop(pop) |
|
48 |
+ |
|
96 | 49 |
def init(self): |
97 | 50 |
self.screen.init("test") |
51 |
+ self.player.draw() |
|
52 |
+ self.update_actors() |
|
53 |
+ for overlay in self.actors + self.objects: |
|
54 |
+ overlay.draw() |
|
98 | 55 |
|
99 | 56 |
def run(self): |
100 | 57 |
while not tc.console_is_window_closed(): |
101 | 58 |
self.events.tick() |
59 |
+ self.update_actors() |
|
102 | 60 |
self.map.draw(self.screen) |
103 |
- tc.console_print(0, 0,0, '%d' % tc.sys_get_fps()) |
|
61 |
+ tc.console_print(0, 0,1, '%d' % tc.sys_get_fps()) |
|
104 | 62 |
tc.console_flush() |
105 | 63 |
|
106 | 64 |
|
1 | 14 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,14 @@ |
1 |
+import libtcodpy as tc |
|
2 |
+ |
|
3 |
+class Console(object): |
|
4 |
+ # TBI: must have a self.con member with the console to be drawn on |
|
5 |
+ pass |
|
6 |
+ |
|
7 |
+class Screen(object): |
|
8 |
+ def __init__(self, w, h): |
|
9 |
+ self.width = w |
|
10 |
+ self.height = h |
|
11 |
+ self.con = 0 |
|
12 |
+ def init(self, title, fullscreen=False): |
|
13 |
+ tc.console_init_root(self.width, self.height, title, fullscreen, 2) |
|
14 |
+ return self |
0 | 15 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,28 @@ |
1 |
+import libtcodpy as tc |
|
2 |
+ |
|
3 |
+class EventHandler(object): |
|
4 |
+ def __init__(self): |
|
5 |
+ self.key = tc.Key() |
|
6 |
+ self.mouse = tc.Mouse() |
|
7 |
+ self.cbs = {} |
|
8 |
+ |
|
9 |
+ def register(self, event, cb): |
|
10 |
+ self.cbs.setdefault(event,[]).append(cb) |
|
11 |
+ |
|
12 |
+ def tick(self): |
|
13 |
+ tc.sys_check_for_event(tc.EVENT_KEY_PRESS|tc.EVENT_MOUSE|tc.KEY_PRESSED, |
|
14 |
+ self.key, self.mouse |
|
15 |
+ ) |
|
16 |
+ |
|
17 |
+ alt, shift, ctrl = self.key.lalt|self.key.ralt, self.key.shift, self.key.lctrl|self.key.rctrl |
|
18 |
+ char = chr(self.key.c) |
|
19 |
+ if char != '\x00' and char in self.cbs: |
|
20 |
+ for cb in self.cbs[char]: |
|
21 |
+ cb(alt,shift,ctrl) |
|
22 |
+ |
|
23 |
+ elif self.key.vk in self.cbs: |
|
24 |
+ for cb in self.cbs[self.key.vk]: |
|
25 |
+ cb(alt,shift,ctrl) |
|
26 |
+ |
|
27 |
+ |
|
28 |
+ |
0 | 29 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,257 @@ |
1 |
+import numpy as np |
|
2 |
+import libtcodpy as tc |
|
3 |
+import yaml |
|
4 |
+import libs.bresenham |
|
5 |
+import libs.cache |
|
6 |
+ |
|
7 |
+def squeeze(val, low, high): |
|
8 |
+ return min(max(val, low), high) |
|
9 |
+ |
|
10 |
+class TerrainGen(object): |
|
11 |
+ def __init__(self, map): |
|
12 |
+ self.width, self.height = map.dim |
|
13 |
+ self.terrain_registry = map.terrain_registry |
|
14 |
+ |
|
15 |
+ def generate(self): |
|
16 |
+ w,h = self.width, self.height |
|
17 |
+ terrains, probs = self.terrain_registry.get_terrain_types() |
|
18 |
+ map = np.random.choice(terrains, (w,h), p=probs) |
|
19 |
+ chars = np.zeros((w,h)).astype('int') |
|
20 |
+ fgcolors = np.zeros((w,h,3)) |
|
21 |
+ bgcolors = np.zeros((w,h,3)) |
|
22 |
+ |
|
23 |
+ for x, row in enumerate(map): |
|
24 |
+ for y, cell in enumerate(row): |
|
25 |
+ char, fgcolor, bgcolor = self.terrain_registry.get_display(cell) |
|
26 |
+ chars[x,y] = char |
|
27 |
+ fgcolors[x,y] = fgcolor |
|
28 |
+ bgcolors[x,y] = bgcolor |
|
29 |
+ return map, chars, fgcolors, bgcolors |
|
30 |
+ |
|
31 |
+ |
|
32 |
+class Map(object): |
|
33 |
+ def __init__(self, w, h, terrain_registry): |
|
34 |
+ self.pov = None |
|
35 |
+ self.povcache = libs.cache.Cache() |
|
36 |
+ |
|
37 |
+ self.width = w |
|
38 |
+ self.height = h |
|
39 |
+ self.terrain_registry = terrain_registry |
|
40 |
+ self.overlays = {} # (x,y): { set( object, ... ) } |
|
41 |
+ |
|
42 |
+ self.ids, self.map, self.fgcolors, self.bgcolors = TerrainGen(self).generate() |
|
43 |
+ |
|
44 |
+ self.fov = FovCache(self, self.terrain_registry) |
|
45 |
+ |
|
46 |
+ def add(self, object): |
|
47 |
+ self.overlays.setdefault(object.pos,[]).append(object) |
|
48 |
+ |
|
49 |
+ def check_and_execute_bump(self, object, x,y): |
|
50 |
+ if (x,y) in self.overlays: |
|
51 |
+ for other in self.overlays[x,y]: |
|
52 |
+ object.bump(other) |
|
53 |
+ |
|
54 |
+ def move(self, object, dx,dy): |
|
55 |
+ self.overlays[object.pos].remove(object) |
|
56 |
+ |
|
57 |
+ collide_x, collide_y = None, None |
|
58 |
+ |
|
59 |
+ if abs(dx) < 2 and abs(dy) < 2: |
|
60 |
+ ox,oy = object.pos |
|
61 |
+ x = squeeze(ox+dx, 0, self.width-1) |
|
62 |
+ y = squeeze(oy+dy, 0, self.height-1) |
|
63 |
+ if not self.is_passable((x,y)): |
|
64 |
+ collide_x, collide_y = x,y |
|
65 |
+ x,y = ox,oy |
|
66 |
+ |
|
67 |
+ else: |
|
68 |
+ ox,oy = object.pos |
|
69 |
+ tx,ty = ox+dx, oy+dy |
|
70 |
+ gx,gy = ox,oy |
|
71 |
+ for x,y in libs.bresenham.line(ox,oy, tx,ty, 1): |
|
72 |
+ if not self.is_passable((x,y)): |
|
73 |
+ collide_x, collide_y = x,y |
|
74 |
+ break |
|
75 |
+ else: gx,gy = x,y |
|
76 |
+ x,y = gx,gy |
|
77 |
+ |
|
78 |
+ if collide_x is not None: |
|
79 |
+ self.check_and_execute_bump(object, collide_x, collide_y) |
|
80 |
+ |
|
81 |
+ self.overlays.setdefault((x,y), []).append(object) |
|
82 |
+ self.update_overlay(ox,oy) |
|
83 |
+ return x-ox, y-oy |
|
84 |
+ |
|
85 |
+ def update_overlay(self, x=None, y=None): |
|
86 |
+ if x is None or y is None: pass |
|
87 |
+ else: |
|
88 |
+ if (x,y) in self.overlays and self.overlays[x,y] == []: |
|
89 |
+ self.overlays.pop((x,y)) |
|
90 |
+ |
|
91 |
+ def set_pov(self, pov): |
|
92 |
+ self.pov = pov |
|
93 |
+ |
|
94 |
+ def get_visible_objects(self): |
|
95 |
+ o,r = self.pov |
|
96 |
+ results = set() |
|
97 |
+ for x,y in self.overlays: |
|
98 |
+ if self.fov.is_visible(o,r, (x,y)): |
|
99 |
+ results.update(self.overlays[x,y]) |
|
100 |
+ return results |
|
101 |
+ |
|
102 |
+ |
|
103 |
+ |
|
104 |
+ def get_rgb(self, colors, fg=True,slices=(slice(0),slice(0))): |
|
105 |
+ result = np.rollaxis(colors[slices], 2) |
|
106 |
+ return [x.transpose() for x in result] |
|
107 |
+ |
|
108 |
+ def draw(self, con, tl=(0,0)): |
|
109 |
+ br = tl[0]+con.width, tl[1]+con.height |
|
110 |
+ |
|
111 |
+ fgcolors = self.fgcolors.astype('int') |
|
112 |
+ bgcolors = self.bgcolors.astype('int') |
|
113 |
+ color_mask = np.ones( (con.width, con.height, 3) ) |
|
114 |
+ char_mask = np.ones( (con.width, con.height) ).astype('bool') |
|
115 |
+ if self.pov is not None: |
|
116 |
+ origin, radius = self.pov |
|
117 |
+ def calc_mask(): |
|
118 |
+ for x in range(tl[0], br[0]): |
|
119 |
+ for y in range(tl[1], br[1]): |
|
120 |
+ if not self.fov.is_visible(origin, radius, (x,y)): |
|
121 |
+ color_mask[x,y] = (0.25, 0.5, 0.5) |
|
122 |
+ char_mask[x,y] = False |
|
123 |
+ return color_mask, char_mask |
|
124 |
+ color_mask, char_mask = self.povcache.get( (origin,radius), calc_mask ) |
|
125 |
+ fgcolors = (fgcolors * color_mask).astype('int') |
|
126 |
+ bgcolors = (bgcolors * color_mask).astype('int') |
|
127 |
+ |
|
128 |
+ slices = slice(tl[0], tl[0]+con.width), slice(tl[1], tl[1]+con.height) |
|
129 |
+ tc.console_fill_foreground(con.con, *self.get_rgb(fgcolors, slices=slices)) |
|
130 |
+ tc.console_fill_background(con.con, *self.get_rgb(bgcolors, False,slices=slices)) |
|
131 |
+ |
|
132 |
+ chars = np.copy(self.map[slices]) |
|
133 |
+ for x,y in self.overlays: |
|
134 |
+ screen_x = x-tl[0] |
|
135 |
+ screen_y = y-tl[1] |
|
136 |
+ if not char_mask[screen_x,screen_y]: continue |
|
137 |
+ if 0 <= screen_x < con.width and 0 <= screen_y < con.height: |
|
138 |
+ obj = self.overlays[x,y][-1] |
|
139 |
+ chars[screen_x,screen_y] = obj.char |
|
140 |
+ tc.console_set_char_background(con.con, screen_x,screen_y, tc.Color(*bgcolors[screen_x,screen_y])) |
|
141 |
+ tc.console_set_char_foreground(con.con, screen_x,screen_y, tc.Color(*obj.color)) |
|
142 |
+ chars[np.logical_not(char_mask)] = ord(' ') |
|
143 |
+ tc.console_fill_char(con.con, chars.transpose()) |
|
144 |
+ |
|
145 |
+ def coord_iter(self): |
|
146 |
+ return ( (x,y) for x in range(self.width) for y in range(self.height) ) |
|
147 |
+ |
|
148 |
+ @property |
|
149 |
+ def dim(self): |
|
150 |
+ return self.width, self.height |
|
151 |
+ |
|
152 |
+ def is_passable(self, coord): |
|
153 |
+ if coord in self.overlays and any(x.blocks for x in self.overlays[coord]): |
|
154 |
+ return False |
|
155 |
+ else: |
|
156 |
+ return self.fov.is_passable(coord) |
|
157 |
+ |
|
158 |
+ |
|
159 |
+class FovCache(object): |
|
160 |
+ # TODO: get allow updates to base_map |
|
161 |
+ def __init__(self, map, terrain_registry): |
|
162 |
+ self.width, self.height = map.dim |
|
163 |
+ |
|
164 |
+ self.base_map = tc.map_new(*map.dim) |
|
165 |
+ |
|
166 |
+ for x,y in map.coord_iter(): |
|
167 |
+ if (x,y) in map.overlays: |
|
168 |
+ object = map.overlays[x,y] |
|
169 |
+ pssble,trnsprnt = object.passable, object.transparent |
|
170 |
+ else: |
|
171 |
+ pssble,trnsprnt = terrain_registry.get_props(map.ids[x,y]) |
|
172 |
+ tc.map_set_properties(self.base_map, x,y, trnsprnt,pssble) |
|
173 |
+ |
|
174 |
+ self.fovmaps = {} |
|
175 |
+ |
|
176 |
+ def get_fovmap(self, origin, radius): |
|
177 |
+ key = origin,radius |
|
178 |
+ |
|
179 |
+ if key in self.fovmaps: fovmap = self.fovmaps[key] |
|
180 |
+ else: |
|
181 |
+ fovmap = tc.map_new(self.width, self.height) |
|
182 |
+ tc.map_copy(self.base_map, fovmap) |
|
183 |
+ self.fovmaps[key] = fovmap |
|
184 |
+ |
|
185 |
+ x,y = origin |
|
186 |
+ tc.map_compute_fov(fovmap, x,y, radius) |
|
187 |
+ |
|
188 |
+ return fovmap |
|
189 |
+ |
|
190 |
+ def is_visible(self, origin, radius, coord): |
|
191 |
+ fovmap = self.get_fovmap(origin, radius) |
|
192 |
+ return tc.map_is_in_fov(fovmap, *coord) |
|
193 |
+ |
|
194 |
+ def is_transparent(self, coord): |
|
195 |
+ return tc.map_is_transparent(self.base_map, *coord) |
|
196 |
+ def is_passable(self, coord): |
|
197 |
+ return tc.map_is_walkable(self.base_map, *coord) |
|
198 |
+ |
|
199 |
+ |
|
200 |
+class TerrainInfo(object): |
|
201 |
+ passable = False |
|
202 |
+ transparent = False |
|
203 |
+ char = ord(' ') |
|
204 |
+ fg = (255,255,255) |
|
205 |
+ bg = (0,0,0) |
|
206 |
+ prob = 1 |
|
207 |
+ |
|
208 |
+ @classmethod |
|
209 |
+ def make_terrain(cls, name, char, passable, transparent,fg,bg, prob=1): |
|
210 |
+ if hasattr(char, 'upper'): char = ord(char) |
|
211 |
+ passable = bool(passable) |
|
212 |
+ transparent = bool(transparent) |
|
213 |
+ return type(name, (cls,), dict(char=char, passable=passable, transparent=transparent,fg=fg,bg=bg,prob=prob)) |
|
214 |
+ |
|
215 |
+class TerrainRegistry(object): |
|
216 |
+ def __init__(self): |
|
217 |
+ self.id = 0 |
|
218 |
+ self.registry = {} |
|
219 |
+ self.names = {} |
|
220 |
+ |
|
221 |
+ def get_terrain(self, id): |
|
222 |
+ ter = self.registry[id] |
|
223 |
+ return ter |
|
224 |
+ |
|
225 |
+ def get_props(self, id): |
|
226 |
+ ter = self.get_terrain(id) |
|
227 |
+ return ter.passable, ter.transparent |
|
228 |
+ |
|
229 |
+ def get_display(self, char): |
|
230 |
+ ter = self.get_terrain(char) |
|
231 |
+ return ter.char, ter.fg, ter.bg |
|
232 |
+ |
|
233 |
+ def get_terrain_types(self): |
|
234 |
+ types, probabilities = zip(*[(x, self.registry[x].prob) for x in self.registry]) |
|
235 |
+ probs = [float(x) / sum(probabilities) for x in probabilities] |
|
236 |
+ return types, probs |
|
237 |
+ |
|
238 |
+ |
|
239 |
+ def register(self, ter): |
|
240 |
+ self.id += 1 |
|
241 |
+ self.registry[self.id] = ter |
|
242 |
+ self.names[ter.__name__] = ter |
|
243 |
+ return ter |
|
244 |
+ |
|
245 |
+ def new_terrain(self, name, char, |
|
246 |
+ passable=False, transparent=False, fg=(255,255,255), bg=(0,0,0), prob=1 |
|
247 |
+ ): |
|
248 |
+ ter = TerrainInfo.make_terrain(name, char, passable, transparent, fg,bg, prob=prob) |
|
249 |
+ return self.register(ter) |
|
250 |
+ |
|
251 |
+ def load_from_file(self, fn, loader=yaml.safe_load): |
|
252 |
+ with open(fn) as f: |
|
253 |
+ values = loader(f) |
|
254 |
+ for name, terrain in values.viewitems(): |
|
255 |
+ self.new_terrain(name, **terrain) |
|
256 |
+ |
|
257 |
+ |
0 | 258 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,48 @@ |
1 |
+import libtcodpy as tc |
|
2 |
+import libs.combat |
|
3 |
+import libs.overlay |
|
4 |
+ |
|
5 |
+class Player(libs.overlay.Actor): |
|
6 |
+ char = ord('@') |
|
7 |
+ color = (255,255,255) |
|
8 |
+ light_radius = 10 |
|
9 |
+ def __init__(self, x,y, map, adventurer): |
|
10 |
+ libs.overlay.Actor.__init__(self, x,y, map, adventurer) |
|
11 |
+ print 'Player\'s name is %s' % self.adventurer.name |
|
12 |
+ self.map.set_pov((self.pos, self.light_radius)) |
|
13 |
+ def move(self, dx, dy): |
|
14 |
+ libs.overlay.Actor.move(self, dx,dy) |
|
15 |
+ self.map.set_pov((self.pos, self.light_radius)) |
|
16 |
+ |
|
17 |
+class ArrowHandler(object): |
|
18 |
+ def __init__(self, player, eh): |
|
19 |
+ self.player = player |
|
20 |
+ eh.register(tc.KEY_LEFT,self.left) |
|
21 |
+ eh.register(tc.KEY_RIGHT,self.right) |
|
22 |
+ eh.register(tc.KEY_UP,self.up) |
|
23 |
+ eh.register(tc.KEY_DOWN,self.down) |
|
24 |
+ def left(self, alt, shift, ctrl): |
|
25 |
+ val = 10 if shift else 1 |
|
26 |
+ if alt: |
|
27 |
+ self.player.move(-val, -val) |
|
28 |
+ else: |
|
29 |
+ self.player.move(-val, 0) |
|
30 |
+ def right(self, alt, shift, ctrl): |
|
31 |
+ val = 10 if shift else 1 |
|
32 |
+ if alt: |
|
33 |
+ self.player.move(val, val) |
|
34 |
+ else: |
|
35 |
+ self.player.move(val, 0) |
|
36 |
+ def up(self, alt, shift, ctrl): |
|
37 |
+ val = 10 if shift else 1 |
|
38 |
+ if alt: |
|
39 |
+ self.player.move(val, -val) |
|
40 |
+ else: |
|
41 |
+ self.player.move(0, -val) |
|
42 |
+ def down(self, alt, shift, ctrl): |
|
43 |
+ val = 10 if shift else 1 |
|
44 |
+ if alt: |
|
45 |
+ self.player.move(-val, val) |
|
46 |
+ else: |
|
47 |
+ self.player.move(0, val) |
|
48 |
+ |