git.fiddlerwoaroof.com
Browse code

djikstra pathfinding ai... adds lags to the game

Ed L authored on 29/07/2012 04:10:45
Showing 10 changed files
2 2
new file mode 100644
... ...
@@ -0,0 +1,171 @@
1
+import random
2
+import copy
3
+
4
+class DjikstraMap(object):
5
+	def __init__(self, mp=None):
6
+		#print '__init__ djm'
7
+		self.map = None
8
+		if mp is not None:
9
+			self.load_map(mp)
10
+
11
+	def load_map(self, mp):
12
+		self.map = [
13
+			[ [255,None][cell] for cell in row ]
14
+				for row in mp
15
+		]
16
+		self.width = len(self.map)
17
+		self.height = len(self.map[0])
18
+	def set_goals(self, *args, **k):
19
+		for x,y in args:
20
+			self.map[x][y] = k.get('weight', 0)
21
+
22
+	def iter_map(self):
23
+		for x, row in enumerate(self.map):
24
+			for y, cell in enumerate(row):
25
+				if cell is not None:
26
+					yield (x,y), cell
27
+
28
+	def get_cross(self, pos, rad):
29
+		ox,oy = pos
30
+		# up, down, left, right
31
+		result = [
32
+			(ox, oy-rad),
33
+			(ox, oy+rad),
34
+			(ox-rad, oy),
35
+			(ox+rad, oy),
36
+		]
37
+		for idx, (x,y) in enumerate(result):
38
+			if x < 0 or x >= self.width:
39
+				result[idx] = None
40
+			elif y < 0 or y >= self.height:
41
+				result[idx] = None
42
+			else:
43
+				result[idx] = self.map[x][y]
44
+		return result
45
+
46
+	def get_rect(self, pos, rad):
47
+		x,y = pos
48
+		result = []
49
+		for cx in range(x-rad, x+rad+1):
50
+			result.append([])
51
+			for cy in range(y-rad, y+rad+1):
52
+				if cx < 0 or cx >= len(self.map):
53
+					result[-1].append(None)
54
+				elif cy < 0 or cy >= len(self.map[0]):
55
+					result[-1].append(None)
56
+				else:
57
+					result[-1].append(self.map[cx][cy])
58
+
59
+		return result
60
+
61
+	def get_line(self, pos1, pos2):
62
+		x1,y1 = pos1
63
+		x2,y2 = pos2
64
+		if y1 == y2:
65
+			return [ (x,y1) for x in range(x1,x2+1) ]
66
+		if x1 == x2:
67
+			return [ (x1,y) for y in range(y1,y2+1) ]
68
+
69
+	def get_borders(self, pos, rad):
70
+		x,y = pos
71
+
72
+		results = []
73
+		results.extend(
74
+			self.get_line(
75
+				(min(x-rad, 0), min(y-rad, 0)),
76
+				(min(x-rad, 0), min(y+rad, self.height))
77
+			)
78
+		)
79
+		results.extend(
80
+			self.get_line(
81
+				(min(x-rad, 0),          min(y-rad, 0)),
82
+				(min(x+rad, self.width), min(y-rad, 0))
83
+			)
84
+		)
85
+
86
+		results.extend(
87
+			self.get_line(
88
+				(min(x+rad, self.width), min(y+rad, self.height)),
89
+				(min(x-rad, 0),          min(y+rad, self.height))
90
+			)
91
+		)
92
+
93
+		results.extend(
94
+			self.get_line(
95
+				(min(x+rad, self.width), min(y+rad, self.height)),
96
+				(min(x+rad, self.width), min(y-rad, 0))
97
+			)
98
+		)
99
+
100
+	def iter(self, num):
101
+		result = True
102
+		for _ in range(num):
103
+			result = self.cycle()
104
+			if result == False:
105
+				break
106
+		return result
107
+
108
+	def cycle(self):
109
+		changed = False
110
+		out = self.map
111
+		for pos, cell in self.iter_map():
112
+			x,y = pos
113
+
114
+			#rect = self.get_rect(pos, 2)
115
+			#neighbors = [n for n in borders(rect)]
116
+			neighbors = (r for r in sum(self.get_rect(pos, 1), []) if r is not None)
117
+			#neighbors = (r for r in self.get_cross(pos, 1) if r is not None)
118
+
119
+			try:
120
+				min_neighbor = min(neighbors)
121
+			except ValueError: continue
122
+
123
+			if cell > min_neighbor + 1:
124
+				changed = True
125
+				out[x][y] = min_neighbor + 1
126
+		return changed
127
+
128
+	def visualize(self):
129
+		print
130
+		for row in zip(*self.map):
131
+			for cell in row:
132
+				if cell is None: print ' ',
133
+				elif cell > 9: print '*',
134
+				else: print cell,
135
+			print
136
+
137
+	def get_neighbor_values(self, x,y):
138
+		b = enumerate((enumerate(r,-1) for r in self.get_rect( (x,y), 1 )),-1)
139
+		result = [(i1,i2, v) for i1, r in b for i2,v in r if v is not None]
140
+		#print result
141
+		return result
142
+
143
+	def get_low_neighbors(self, x,y, num=2):
144
+		result = sorted(self.get_neighbor_values(x,y), key=lambda a: a[-1])
145
+		return result[:num]
146
+
147
+	def categorize(self, values):
148
+		results = {}
149
+		for i1,i2,v in values:
150
+			results.setdefault(v,[]).append( (i1,i2) )
151
+		return results
152
+	def nav(self, x,y):
153
+		results = self.get_neighbor_values(x,y)
154
+		results = self.categorize(results)
155
+		dx,dy = random.choice(results[min(results)])
156
+		#print dx,dy,min(results)
157
+
158
+		return dx,dy
159
+
160
+def borders(rect):
161
+	mx, my = len(rect)-1, len(rect[0])-1
162
+	for x, row in enumerate(rect):
163
+		for y, cell in enumerate(row):
164
+			if x in {0,mx} or y in {0,my}:
165
+				if cell is not None:
166
+					yield cell
167
+
168
+def dist( p1, p2 ):
169
+	x1,y1 = p1
170
+	x2,y2 = p2
171
+	return int( ( (x2-x1)**2+(y2-y1)**2 ) ** .5 )
... ...
@@ -4,7 +4,7 @@ namegen_class: "male"
4 4
 char: "h"
5 5
 color: amber
6 6
 
7
-spawn_chance: 4
7
+spawn_chance: 0
8 8
     
9 9
 hp: 10
10 10
 defense: 1
... ...
@@ -18,7 +18,7 @@ namegen_class: "male"
18 18
 char: "i"
19 19
 color: dark_red
20 20
 
21
-spawn_chance: 7
21
+spawn_chance: 3
22 22
     
23 23
 hp: 4
24 24
 defense: 0
... ...
@@ -30,7 +30,7 @@ namegen_class: "demon male"
30 30
 char: "%"
31 31
 color: darker_yellow
32 32
 
33
-spawn_chance: 1
33
+spawn_chance: 0
34 34
     
35 35
 hp: 17
36 36
 defense: 2
... ...
@@ -24,7 +24,7 @@ class GameBase:
24 24
 	def message(self, msg, color=None):
25 25
 		if color is None:
26 26
 			color = libtcod.white
27
-		utilities.message(self.game_msgs, self.MSG_HEIGHT, self.MSG_WIDTH, msg)
27
+		utilities.message(self.game_msgs, self.MSG_HEIGHT, self.MSG_WIDTH, msg, color=color)
28 28
 
29 29
 	def __init__(self, app_name='test app', screen_width=None, screen_height=None):
30 30
 		print '__init__'
... ...
@@ -1,3 +1,4 @@
1
+from algorithms import djikstra
1 2
 import libtcodpy as libtcod
2 3
 import maps
3 4
 import debug
... ...
@@ -16,7 +17,16 @@ class Level(object):
16 17
 		self.number = num
17 18
 		return self
18 19
 
20
+	def get_djikstra(self, x,y):
21
+		if (x,y) not in self.djikstra_cache:
22
+			dj = self.djikstra_cache[x,y] = djikstra.DjikstraMap(self.map.map.data)
23
+			dj.set_goals( (x,y), weight=0)
24
+		dj = self.djikstra_cache[x,y]
25
+		dj.cycle()
26
+		return dj
27
+
19 28
 	def __init__(self, width, height, con, item_types=None, monster_types=None):
29
+		self.djikstra_cache = {}
20 30
 		self.objects = []
21 31
 		self.map = maps.Map(width, height, con, self)
22 32
 		self.fov_map = libtcod.map_new(self.map.width, self.map.height)
... ...
@@ -119,7 +129,13 @@ class Level(object):
119 129
 		return libtcod.map_is_in_fov(self.fov_map, x,y)
120 130
 
121 131
 	def is_blocked(self, x,y):
122
-		return self.map.is_blocked(x,y)
132
+		if x < 0 or x > self.map.width:
133
+			result = True
134
+		elif y < 0 or y > self.map.height:
135
+			result = True
136
+		else:
137
+			result = self.map.is_blocked(x,y)
138
+		return result
123 139
 
124 140
 
125 141
 import game
... ...
@@ -366,7 +366,7 @@ if __name__ == 'main':
366 366
 				'',
367 367
 			)
368 368
 
369
-			options = ['Play', 'Exit']
369
+			options = ['Resume', 'Play', 'Exit']
370 370
 			result = self.menu('\n'.join(message), options, len(message[0]))
371 371
 			if result is not None:
372 372
 				return options[result]
... ...
@@ -396,9 +396,11 @@ if __name__ == '__main__':
396 396
 	game_instance.load_settings()
397 397
 	action = game_instance.main_menu()
398 398
 	while action is None or action.lower() != 'exit':
399
-		if action is not None and action.lower() == 'play':
400
-			game_instance.setup_map()
401
-			game_instance.main()
402
-			libtcod.console_clear(0)
399
+		if action is not None:
400
+			if action.lower() == 'play':
401
+				game_instance.setup_map()
402
+			if action.lower() in {'play', 'resume'}:
403
+				game_instance.main()
404
+				libtcod.console_clear(0)
403 405
 		action = game_instance.main_menu()
404 406
 
... ...
@@ -15,6 +15,7 @@ class Tile(object):
15 15
 		self.block_sight = block_sight
16 16
 
17 17
 
18
+
18 19
 class AutomataEngine(object):
19 20
 	def __init__(self, width=None, height=None, data=None, randomize=True):
20 21
 		if data:
... ...
@@ -187,6 +188,47 @@ class MazeGen(AutomataEngine):
187 188
 				tmp_data[x][y] = self.rule(x,y, cell)
188 189
 		return AutomataEngine(data=tmp_data)
189 190
 
191
+class AutomataLoader(AutomataEngine):
192
+	def load_rules(self):
193
+		self.rules = yaml.load(
194
+			file(
195
+				os.path.join('.', 'data', 'mapgenerator.yml')
196
+			)
197
+		)
198
+
199
+	def parse_rule(self, rule):
200
+		comp, val = rule.split('->')
201
+		val = val.strip()
202
+		if comp == 'is':
203
+			if val == 'odd':
204
+				return lambda a: (a%2)==1
205
+			elif val == 'even':
206
+				return lambda a: (a%2)==0
207
+			else:
208
+				return lambda a: a == int(val)
209
+		else:
210
+			val = int(val)
211
+			if comp.startswith('>'):
212
+				if comp.startswith('>='):
213
+					return lambda a: a >= val
214
+				return lambda a: a > val
215
+
216
+			elif comp.startswith('<'):
217
+				if comp.startswith('<='):
218
+					return lambda a: a <= val
219
+				return lambda a: a < val
220
+
221
+			elif comp.startswith('=='):
222
+				return lambda a: a == val
223
+
224
+	def rule(self, x,y, cell):
225
+		sum = self.sum_area((x,y), 1)
226
+		for rule in self.rules:
227
+			rule, _, result = rule.partition('::')
228
+			if parse_rule(rule)(cell):
229
+				if hasattr(result, 'upper') and result.lower()=='cell':
230
+					result = cell
231
+				return result
190 232
 
191 233
 class Automata1(AutomataEngine):
192 234
 	def rule(self, x,y, cell):
... ...
@@ -227,15 +269,15 @@ class NewSmoother(AutomataEngine):
227 269
 			return 1
228 270
 
229 271
 import collections
272
+from algorithms import djikstra
230 273
 class Map(collections.MutableSequence):
231 274
 	def __init__(self, width, height, con, level):
232 275
 		print 'hello again'
233
-		#self.data = Smoother(data=Automata1(width, height).iter(6).data).munge().to_map()
234 276
 		self.gen = MazeGen(width, height)
235
-		self.data = self.gen.munge()
277
+		self.map = self.data = self.gen.munge()
236 278
 		self.data = Automata1(data=self.data.data).iter(2)
237
-		self.data = Smoother(data=self.data.data).munge()
238
-		self.data = self.data.to_map()
279
+		self.map = Smoother(data=self.data.data).munge()
280
+		self.data = self.map.to_map()
239 281
 
240 282
 		self.width = width
241 283
 		self.height = height
... ...
@@ -378,6 +420,8 @@ class Map(collections.MutableSequence):
378 420
 	def choose_empty_point(self, room):
379 421
 		empty_points = [p for p in room.iter_cells() if not self.is_blocked(*p)]
380 422
 		if empty_points:
423
+			for x in empty_points:
424
+				self.level.get_djikstra(*x)
381 425
 			return random.choice(empty_points)
382 426
 		return None,None
383 427
 
... ...
@@ -6,8 +6,10 @@ import libtcodpy as libtcod
6 6
 import items
7 7
 
8 8
 from main import game_instance
9
+from algorithms import djikstra
9 10
 
10 11
 class Monster(object):
12
+	def init(self,*a): pass
11 13
 	def take_turn(self): pass
12 14
 
13 15
 class BasicMonster(Monster):
... ...
@@ -20,10 +22,62 @@ class BasicMonster(Monster):
20 22
 				while (dx,dy) == (0,0) and counter < 10: # wiggle around if stuck
21 23
 					counter += 1
22 24
 					dx,dy = monster.move(random.randrange(-1,2,2), random.randrange(-1,2,2))
23
-				print 'wiggled %s times' % counter
25
+				#print 'wiggled %s times' % counter
24 26
 			elif game_instance.player.fighter.hp > 0:
25 27
 				monster.fighter.attack(game_instance.player)
26 28
 
29
+class DjikstraMonster(Monster):
30
+	maps = {}
31
+
32
+	@property
33
+	def dj(self):
34
+		result = self.maps.get(id(self.level))
35
+		if result is None:
36
+			result = self.maps[id(self.level)] = djikstra.DjikstraMap()
37
+		return result
38
+
39
+	def init(self, level):
40
+		self.level = level
41
+		#print
42
+		#print 'now olog on level:', self.level, self.maps
43
+
44
+		self.opos = self.owner.x, self.owner.y
45
+		self.ppos = None
46
+
47
+		map = level.map
48
+		if self.dj.map is None:
49
+			self.dj.load_map(map.map.data)
50
+
51
+			self.dj.set_goals(*(room.center for room in map.gen.rooms), weight = 0)
52
+
53
+			while self.dj.cycle(): pass
54
+
55
+		self.dj.visualize()
56
+
57
+	def take_turn(self):
58
+		pos = self.owner.x, self.owner.y
59
+
60
+		dx,dy = 0,0
61
+		if self.level.is_visible(*pos):
62
+			if self.level.player.distance(*pos) < 2:
63
+				self.owner.fighter.attack(game_instance.player)
64
+			else:
65
+				dx, dy = self.owner.move_towards(*self.level.player.pos)
66
+
67
+		elif random.random() < .4:
68
+			dx,dy = self.dj.nav(*pos)
69
+
70
+		else:
71
+			dj = self.level.get_djikstra(*self.level.player.pos)
72
+			#print pos, '<---', self.level.player.distance(*pos)
73
+			x,y = pos
74
+			dx,dy = dj.nav(x,y)
75
+
76
+		self.owner.move(dx,dy)
77
+
78
+
79
+
80
+
27 81
 class AdvancedMonster(Monster):
28 82
 	def perimeter(self, rect):
29 83
 		for dx,row in enumerate(rect, -1):
... ...
@@ -160,25 +214,25 @@ class MonsterLoader(object):
160 214
 				)
161 215
 			)(doc), doc['spawn_chance'])
162 216
 
163
-Game.register_monster_type(
164
-	lambda map,level, con,x,y: objects.Object(map, con,
165
-		x,y, '\x02', '%s the Orc' % libtcod.namegen_generate('Fantasy male'),
166
-			libtcod.blue, True,
167
-
168
-		fighter=objects.Fighter(hp=10, defense=2, power=3, death_function=monster_death),
169
-		ai=AdvancedMonster(),
170
-		level=level
171
-), 8)
172
-
173
-Game.register_monster_type(
174
-	lambda map,level, con,x,y: objects.Object(map, con,
175
-		x,y, '\x01', '%s the Troll' % libtcod.namegen_generate('Norse male'),
176
-			libtcod.orange, True,
177
-
178
-		fighter=objects.Fighter(hp=16, defense=1, power=4, death_function=monster_death),
179
-		ai=AdvancedMonster(),
180
-		level=level
181
-), 2)
217
+#Game.register_monster_type(
218
+#	lambda map,level, con,x,y: objects.Object(map, con,
219
+#		x,y, '\x02', '%s the Orc' % libtcod.namegen_generate('Fantasy male'),
220
+#			libtcod.blue, True,
221
+#
222
+#		fighter=objects.Fighter(hp=10, defense=2, power=3, death_function=monster_death),
223
+#		ai=AdvancedMonster(),
224
+#		level=level
225
+#), 8)
226
+#
227
+#Game.register_monster_type(
228
+#	lambda map,level, con,x,y: objects.Object(map, con,
229
+#		x,y, '\x01', '%s the Troll' % libtcod.namegen_generate('Norse male'),
230
+#			libtcod.orange, True,
231
+#
232
+#		fighter=objects.Fighter(hp=16, defense=1, power=4, death_function=monster_death),
233
+#		ai=AdvancedMonster(),
234
+#		level=level
235
+#), 2)
182 236
 
183 237
 Game.register_monster_type(
184 238
 	lambda map,level, con,x,y: objects.Object(map, con,
... ...
@@ -186,7 +240,7 @@ Game.register_monster_type(
186 240
 			libtcod.amber, True,
187 241
 
188 242
 		fighter=objects.Fighter(hp=16, defense=1, power=7, death_function=monster_death),
189
-		ai=AdvancedMonster(),
243
+		ai=DjikstraMonster(),
190 244
 		level=level
191 245
 ), 1)
192 246
 Game.register_monster_type(None, 7)
... ...
@@ -14,23 +14,26 @@ class Object(object):
14 14
 		self.con = con
15 15
 #		self.map = map
16 16
 
17
+		if level is not None:
18
+			level.add_object(self)
19
+			level.get_djikstra(x,y)
20
+
21
+		self.level = level
22
+
23
+
17 24
 		if fighter is not None:
18 25
 			fighter.owner = self
19 26
 		self.fighter = fighter
20 27
 
21 28
 		if ai is not None:
22 29
 			ai.owner = self
30
+			ai.init(self.level)
23 31
 		self.ai = ai
24 32
 
25 33
 		if item is not None:
26 34
 			item.owner = self
27 35
 		self.item = item
28 36
 
29
-		if level is not None:
30
-			level.add_object(self)
31
-
32
-		self.level = level
33
-
34 37
 	def enter_level(self, level):
35 38
 		self.level = level
36 39
 		return self
... ...
@@ -39,6 +42,7 @@ class Object(object):
39 42
 		if not self.level.is_blocked(self.x+dx,self.y+dy):
40 43
 			self.x += dx
41 44
 			self.y += dy
45
+			self.level.get_djikstra(self.x, self.y)
42 46
 		else:
43 47
 			dx,dy = 0,0
44 48
 		return dx,dy
... ...
@@ -61,6 +65,7 @@ class Object(object):
61 65
 		dx = target_x - self.x
62 66
 		dy = target_y - self.y
63 67
 		distance = math.sqrt(dx**2+dy**2)
68
+		if distance == 0: distance = 1
64 69
 
65 70
 		dx = int(round(dx/distance))
66 71
 		dy = int(round(dy/distance))
... ...
@@ -204,7 +204,9 @@ class Player(Object):
204 204
 
205 205
 	@triggers_recompute
206 206
 	def move(self, dx, dy):
207
-		return Object.move(self, dx,dy)
207
+		result = Object.move(self, dx,dy)
208
+		self.level.get_djikstra(*self.pos)
209
+		return result
208 210
 
209 211
 	def move_or_attack(self, dx, dy):
210 212
 		x = self.x + dx