1 """
2 This modules implements the springbot, a creature builded with
3 spring and nodes.
4 """
5
6 import sys
7 from warnings import warn
8
9
10 import xml.dom.minidom
11 from xml.parsers.expat import ExpatError
12 import exceptions
13
14
15 from gear import *
16
17 from math import sqrt, pi, fabs
18
19 try:
20 import pygame
21 HAS_PYGAME = True
22 except ImportError:
23 HAS_PYGAME = False
24
25
26
27
29 """
30 Springbot: Creature builded with nodes, which are masses, connected by
31 springs which act like links between nodes pulling them together.
32 Springs may also behave in a cicle muscle pulling and pushing the nodes it
33 connects giving the creature some movement.
34 """
35
36 - def __init__(self, parent=None, name="Unnamed", startangle=0):
37 """
38 Creates a brand new creature or a descendent from a parent
39 """
40
41 self.nodes = []
42 self.springs = []
43 self._node_count_id = 0
44
45 if not parent:
46 self._info = {"name": name, "angle": startangle}
47 else:
48
49 for node in parent.nodes:
50 newnode = Node((node.pos.x, node.pos.y))
51 newnode.id = node.id
52 self.add(newnode)
53
54
55 for spring in parent.springs:
56
57 for node in self.nodes:
58 if node.id == spring.a.id:
59 A = node
60 for node in self.nodes:
61 if node.id == spring.b.id:
62 B = node
63 self.add(Spring(A, B, spring.amplitude, spring.offset, spring.normal))
64
65
66 self._node_count_id = parent._node_count_id
67 self._info = dict(parent._info)
68
70 """
71 Gets an info inten like a dictionary
72 """
73 return self._info[key] if key in self._info else ''
74
76 """
77 Gets an info inten like a dictionary
78 """
79 self._info[key] = value
80 return value
81
83 """
84 Iterate between _info values
85 """
86 return self._info.__iter__()
87
89 """
90 Refreshes creature's state
91 """
92 for node in self.nodes:
93 node.refresh(atr, grav, elast)
94
95 for spring in self.springs:
96 spring.refresh(elast, self['angle'] if moving else None, visc)
97
98 if moving:
99 self['angle'] += ANGLE_STEP
100
102 """
103 Get node from id, None if id does not exists
104 """
105 for node in self.nodes:
106 if node.id == id:
107 return node
108 else:
109 return None
110
111 - def loadXML(self, file=sys.stdin):
112 """
113 Reads a xml into this springbot
114 """
115 self.__init__(load_xml(file, limit=1)[0])
116 return self
117
119 """
120 Store this springbot into a xml file
121 """
122 store_xml([self], file)
123
125 """
126 Colides this springbot to another
127 """
128 for node in self.nodes:
129 for node2 in other.nodes:
130 node.colide(node2, radius)
131
132 - def colideWall(self, limit, side, atr_normal=0.6, atr_surface=0.5, min_vel=0.99, radius=RADIUS):
133 """
134 Colides this springbot with a straigt wall
135 """
136 colision_strenght = 0
137 for node in self.nodes:
138 colision_strenght += node.colideWall(limit, side, atr_normal, atr_surface, min_vel, radius)
139
140 return colision_strenght/len(self.nodes) if len(self.nodes) > 0 else 0
141
143 """
144 Lenght(number of nodes)
145 """
146 return len(self.nodes)
147
148 - def add(self, objeto):
149 """
150 Adds an object: node or spring
151 """
152 if objeto.__class__ == Node:
153 objeto.parent = self
154
155 if not hasattr(objeto, "id"):
156 self._node_count_id += 1
157 objeto.id = self._node_count_id
158
159 self.nodes.append(objeto)
160
161 elif objeto.__class__ == Spring:
162
163
164 for spring in self.springs:
165 if (spring.a is objeto.a or spring.b is objeto.a) and (spring.a is objeto.b or spring.b is objeto.b):
166 break
167 else:
168 objeto.parent = self
169 self.springs.append(objeto)
170
172 """
173 Remove an object: node or spring
174 """
175 if objeto.__class__ == Node:
176
177 removelist = set()
178 for spring in self.springs:
179 if spring.a is objeto or spring.b is objeto:
180 removelist.add(spring)
181 for spring in removelist:
182 self.springs.remove(spring)
183 self.nodes.remove(objeto)
184
185 elif objeto.__class__ == Spring:
186 self.springs.remove(objeto)
187
189 """
190 Calculates the center of mass
191 """
192 cx, cy = 0, 0
193 total = 0
194 for node in self.nodes:
195 total += 1
196 cx += node.pos.x
197 cy += node.pos.y
198
199 return cx / total, cy / total
200
202 """
203 Calculates its bounding box limits
204 """
205 if len(self.nodes) == 0:
206 return 0, 0, 0, 0
207
208 min_x, max_x = self.nodes[0].pos.x, self.nodes[0].pos.x
209 min_y, max_y = self.nodes[0].pos.y, self.nodes[0].pos.y
210
211 for node in self.nodes:
212 min_x = min(min_x, node.pos.x)
213 min_y = min(min_y, node.pos.y)
214 max_x = max(max_x, node.pos.x)
215 max_y = max(max_y, node.pos.y)
216
217 return min_x-radius, min_y-radius, max_x+radius, max_y+radius
218
220 """
221 Gets all unconnected nodes
222 """
223
224 if len(self.nodes) == 0:
225 return []
226
227 allsprings = self.springs[:]
228 atualsprings = []
229
230 allnodes = self.nodes[1:]
231 atualnodes = [self.nodes[0]]
232
233 while len(atualnodes) > 0:
234 atual = atualnodes.pop()
235
236
237 for spring in allsprings:
238 if spring.a is atual or spring.b is atual:
239 atualsprings.append(spring)
240
241
242
243 for spring in atualsprings:
244 allsprings.remove(spring)
245 if spring.a in allnodes:
246 allnodes.remove(spring.a)
247 atualnodes.append(spring.a)
248 elif spring.b in allnodes:
249 allnodes.remove(spring.b)
250 atualnodes.append(spring.b)
251
252
253 atualsprings = []
254
255
256 return allnodes
257
259 """
260 Remove all nodes and springs unconnected
261 """
262 uncon_nodes = self.unconnected()
263
264
265 for node in uncon_nodes:
266 self.nodes.remove(node)
267
268
269 uncon_springs = []
270 for spring in self.springs:
271 if spring.a in uncon_nodes or spring.b in uncon_nodes:
272 uncon_springs.append(spring)
273
274 for spring in uncon_springs:
275 self.springs.remove(spring)
276
278 return "<Springbot %s: %d nodes, %d springs>" % (self['name'], len(self.nodes), len(self.springs))
279
281 """
282 Center springbot width and touch ground
283 """
284
285 x1, y1, x2, y2 = self.boundingBox()
286
287
288 cx = - ((x2+x1)/2)
289
290
291
292 for node in self.nodes:
293 node.pos.y -= (y2 - height) + RADIUS
294 node.pos.x += cx
295
296 return self
297
298
299 - def draw(self, screen, ticks=None, track_x=False, track_y=False,
300 backgroundcolor=(20,10,0), showText=True, extrainfo=None):
301 """
302 Draws Springbot using pygame engine.
303 Use this function to show the springbot
304 """
305 if not HAS_PYGAME:
306 raise exceptions.NotImplementedError("No pygame module found")
307
308 width, height = screen.get_size()
309
310
311
312 x1, y1, x2, y2 = self.boundingBox()
313
314 zm = min(min(width/((x2-x1)+50.0), height/((y2-y1)+50.0)), 1.0)
315
316 if track_x:
317 cxp = (x2+x1)/2
318 cx = cxp*zm - (width/2)
319 else:
320 cxp = cx = -width/2
321
322 if track_y:
323 cyp = (y2+y1)/2
324 cy = cyp*zm - (height/2)
325 else:
326 cyp = cy = 0
327
328
329 if showText:
330
331 font = pygame.font.Font(None, 20)
332
333
334 screen.fill((0,0,0))
335
336 siz_x, siz_y = width/10, height/10
337 for x in xrange(0,width+100,siz_x):
338 for y in xrange(0,height+100,siz_y):
339 pygame.draw.rect(screen, backgroundcolor,
340 (((x - cxp) % (width+siz_x) - siz_x, (y - cyp) % (height+siz_y) - siz_y),
341 (siz_x-10,siz_y-10)))
342
343
344 for spring in self.springs:
345 length = (spring.a.pos - spring.b.pos).length()
346 fator = (length - spring.normal)/length if length > 0 else 0
347
348 if fator <= 0:
349 color = (255, 255-min(-fator * 255, 255), 255-min(-fator * 255, 255))
350 elif fator > 0:
351 color = (255-min(fator * 255, 255), 255, 255-min(fator * 255, 255))
352
353 pygame.draw.line(screen, color, (spring.a.pos.x*zm - cx, spring.a.pos.y*zm - cy),
354 (spring.b.pos.x*zm - cx, spring.b.pos.y*zm - cy), int(3*zm))
355
356
357 velx, vely = 0, 0
358 for node in self.nodes:
359 velx += node.vel.x
360 vely += node.vel.y
361
362 pygame.draw.circle(screen, (0,0,0), (int(node.pos.x*zm - cx), int(node.pos.y*zm - cy)), int(RADIUS*zm), 0)
363 pygame.draw.circle(screen, (10,255,255), (int(node.pos.x*zm - cx), int(node.pos.y*zm - cy)),
364 int(RADIUS*zm), int(2*zm))
365
366 velx /= len(self.nodes)
367 vely /= len(self.nodes)
368
369 if showText:
370
371 nametext = font.render("%s%s" % (self['name'], ": " + extrainfo if extrainfo else "."), False, (255,255,255))
372 speedtext = font.render("speed: %.2f m/c" % (sqrt(velx**2 + vely**2)), False, (255,255,255))
373
374 screen.blit(nametext, (5,5))
375 screen.blit(speedtext, (5,20))
376
377 if ticks:
378 tickstext = font.render("clock: %d c" % (ticks), False, (255,255,255))
379 screen.blit(tickstext, (5,35))
380
381
382
383
384
385
386 -def store_xml(springbots, outfile=sys.stdout):
387 """
388 Writes a set os springbots into a xml
389 """
390 closeoutfile = (type(outfile) is str)
391 if closeoutfile:
392 outfile = open(outfile, 'w')
393
394 outfile.write('<?xml version="1.0" encoding="UTF-8"?>\n')
395 outfile.write('<!DOCTYPE springbots SYSTEM "http://springbots.sourceforge.net/springbots.dtd">\n')
396 outfile.write('<springbots>\n')
397
398 for n, springbot in enumerate(springbots):
399 x1, y1, x2, y2 = springbot.boundingBox()
400 cx, cy = (x1+x2)/2, (y1+y1)/2
401
402 outfile.write('\t<springbot ')
403
404 for key, value in springbot._info.iteritems():
405 outfile.write('%s="%s" ' % (key, str(value)))
406
407 outfile.write('>\n')
408
409 for node in springbot.nodes:
410 outfile.write('\t\t<node pos="%d,%d" id="S%dN%d" />\n' % (node.pos.x-cx, node.pos.y-cy, n, node.id))
411
412 for spring in springbot.springs:
413 outfile.write('\t\t<spring from="S%dN%d" to="S%dN%d" ' % (n, spring.a.id, n, spring.b.id))
414 if round(spring.amplitude, 2) != 0:
415 outfile.write('offset="%.3f" amplitude="%.3f" ' % (spring.offset, spring.amplitude))
416 if fabs(spring.normal - sqrt(sum((spring.a.pos - spring.b.pos)**2))) > 1:
417 outfile.write('normal="%.3f" ' % (spring.normal))
418 outfile.write('/>\n')
419
420 outfile.write("\t</springbot>\n")
421
422 outfile.write("</springbots>\n")
423
424 if closeoutfile:
425 outfile.close()
426 else:
427 outfile.flush()
428
429
430
432 """
433 Tests if a string is representing a float
434 """
435 if s.isdigit() or s == '.': return False
436 spl = s.split('.')
437 if len(spl) > 2: return False
438 for p in spl:
439 if p and not p.isdigit(): return False
440 return True
441
442
443
444 -def load_xml(arq=sys.stdin, limit=None):
445 """
446 Reads an xml into a set of springbots
447 """
448
449 try:
450 doc = xml.dom.minidom.parse(arq)
451 except ExpatError:
452 warn("springbot.loadXML: XML parse error at file %s." %
453 (arq.name if type(arq) == file else str(arq)))
454 return []
455
456
457 springbots = []
458
459 for cnode in doc.childNodes:
460 if cnode.nodeType is xml.dom.minidom.Node.ELEMENT_NODE:
461 springbotsNode = cnode
462 break
463 else:
464 warn("springbot.load_xml: There is no <springbots> XML element in file %s." %
465 (arq.name if type(arq) == file else str(arq)))
466 return []
467
468 for springbotNode in springbotsNode.childNodes:
469
470 if str(springbotNode.nodeName) == "springbot":
471
472 newspringbot = Springbot()
473
474 for key, value in springbotNode.attributes.items():
475 newspringbot[key] = float(value) if isfloat(value) else \
476 (int(value) if value.isdigit() else value)
477
478
479 for cnode in springbotNode.childNodes:
480 if str(cnode.nodeName) == "node":
481 newnode = Node(pos=tuple((int(x) for x in str(cnode.attributes["pos"].value).split(","))))
482 newnode.id = int(str(cnode.attributes["id"].value).split('N')[-1])
483 newspringbot._node_count_id = max(newspringbot._node_count_id, newnode.id)
484 newspringbot.add(newnode)
485
486 elif str(cnode.nodeName) == "spring":
487 id_a = int(str(cnode.attributes["from"].value).split('N')[-1])
488 id_b = int(str(cnode.attributes["to"].value).split('N')[-1])
489
490
491 for node in newspringbot.nodes:
492 if node.id == id_a:
493 a = node
494 break
495 else:
496 raise exceptions.IndexError("node id %d(from) not found\n" % (id_a))
497
498
499 for node in newspringbot.nodes:
500 if node.id == id_b:
501 b = node
502 break
503 else:
504 raise exceptions.IndexError("node id %d(to) not found\n" % (id_b))
505
506
507
508 offs = float(str(cnode.attributes["offset"].value)) if cnode.hasAttribute("offset") else pi
509 ampl = float(str(cnode.attributes["amplitude"].value)) if cnode.hasAttribute("amplitude") else 0.0
510 norm = float(str(cnode.attributes["normal"].value)) if cnode.hasAttribute("normal") else None
511 newspringbot.add(Spring(a, b, offset=offs, amplitude=ampl, normal=norm))
512
513
514 springbots.append(newspringbot)
515
516
517 if limit is not None and len(springbots) >= limit:
518 break
519
520 return springbots
521