import math import random from sets import Set as set import GameMath as gm import Program from cStringIO import StringIO class Tank(object): # How often, in turns, that we can fire. FIRE_RATE = 20 # How far the laser shoots from the center of the tank FIRE_RANGE = 45.0 # The radius of the tank, from the center of the turret. # This is what is used for collision and hit detection. RADIUS = 7.5 # Max speed, in pixels MAXSPEED = 7.0 # Max acceleration, as a fraction of speed. MAXACCEL = 35 # Sensor range, in pixels SENSOR_RANGE = 90.0 # Max turret turn rate, in radians TURRET_TURN_RATE = math.pi/10 # The max number of sensors/timers/toggles SENSOR_LIMIT = 10 def __init__(self, name, pos, color, boardSize, angle=None, tAngle=None): """Create a new tank. @param name: The name name of the tank. Stored in self.name. @param pos: The starting position of the tank (x,y) @param color: The color of the tank. @param boardSize: The size of the board. (maxX, maxY) @param angle: The starting angle of the tank, defaults to random. @param tAngle: The starting turretAngle of the tank, defaults to random. """ self.name = name assert len(pos) == 2 and pos[0] > 0 and pos[1] > 0, \ 'Bad starting position: %s' % str(pos) self.pos = pos # The last speed of each tread (left, right) self._lastSpeed = 0.0, 0.0 # The next speed that the tank should try to attain. self._nextMove = 0,0 # When set, the led is drawn on the tank. self.led = False assert len(boardSize) == 2 and boardSize[0] > 0 and boardSize[1] > 0 # The limits of the playfield (maxX, maxY) self._limits = boardSize # The current angle of the tank. if angle is None: self._angle = random.random()*2*math.pi else: self._angle = angle # The current angle of the turret if tAngle is None: self._tAngle = random.random()*2*math.pi else: self._tAngle = tAngle self.color = color # You can't fire until fireReady is 0. self._fireReady = self.FIRE_RATE # Means the tank will fire at it's next opportunity. self._fireNow = False # True when the tank has fired this turn (for drawing purposes) self._fired = False # What the desired turret angle should be (from the front of the tank). # None means the turret should stay stationary. self._tGoal = None # Holds the properties of each sensor self._sensors = [] # Holds the state of each sensor self._sensorState = [] # The tanks toggle memory self.toggles = [] # The tanks timers self._timers = [] # Is this tank dead? self.isDead = False # Death reason self.deathReason = 'survived' # Something to log to self.stdout = StringIO() def __repr__(self): return '' % (self.name, self.pos[0], self.pos[1]) def fire(self, near): """Shoots, if ordered to and able. Returns the set of tanks destroyed.""" killed = set() if self._fireReady > 0: # Ignore the shoot order self._fireNow = False if self._fireNow: self._fireNow = False self._fireReady = self.FIRE_RATE self._fired = True firePoint = gm.polar2cart(self.FIRE_RANGE, self._angle + self._tAngle) for tank in near: enemyPos = gm.minShift(self.pos, tank.pos, self._limits) if gm.segmentCircleCollision(((0,0), firePoint), enemyPos, self.RADIUS): killed.add(tank) else: self._fired = False return killed def addSensor(self, range, angle, width, attachedTurret=False): """Add a sensor to this tank. @param angle: The angle, from the tanks front and going clockwise, of the center of the sensor. (radians) @param width: The width of the sensor (radians). @param range: The range of the sensor (percent) @param attachedTurret: If set, the sensor moves with the turret. """ assert range >=0 and range <= 1, "Invalid range value." if len(self._sensors) >= self.SENSOR_LIMIT: raise ValueError("You can only have 10 sensors.") range = range * self.SENSOR_RANGE if attachedTurret: attachedTurret = True else: attachedTurret = False self._sensors.append((range, angle, width, attachedTurret)) self._sensorState.append(False) def getSensorState(self, key): return self._sensorState[key] def setMove(self, left, right): """Parse the speed values given, and set them for the next move.""" self._nextMove = left, right def getTurretAngle(self): return self._tAngle def setTurretAngle(self, angle=None): """Set the desired angle of the turret. No angle means the turret should remain stationary.""" if angle is None: self._tGoal = None else: self._tGoal = gm.reduceAngle(angle) def setFire(self): """Set the tank to fire, next turn.""" self._fireNow = True def fireReady(self): """Returns True if the tank can fire now.""" return self._fireReady == 0 def setLED(self): self.led = True def set_program(self, text): """Set the program for this tank.""" self.program = Program.Program(self, text) def execute(self): """Execute this tanks program.""" self.led = False self.program.run() self._move(self._nextMove[0], self._nextMove[1]) self._moveTurret() if self._fireReady > 0: self._fireReady = self._fireReady - 1 def sense(self, near): """Detect collisions and trigger sensors. Returns the set of tanks collided with, plus this one. We do both these steps at once mainly because all the data is available.""" near = list(near) polar = [] for tank in near: polar.append(gm.relativePolar(self.pos, tank.pos, self._limits)) for sensorId in range(len(self._sensors)): # Reset the sensor self._sensorState[sensorId] = False dist, sensorAngle, width, tSens = self._sensors[sensorId] # Adjust the sensor angles according to the tanks angles. sensorAngle = sensorAngle + self._angle # If the angle is tied to the turret, add that too. if tSens: sensorAngle = sensorAngle + self._tAngle while sensorAngle >= 2*math.pi: sensorAngle = sensorAngle - 2*math.pi for i in range(len(near)): r, theta = polar[i] # Find the difference between the object angle and the sensor. # The max this can be is pi, so adjust for that. dAngle = gm.angleDiff(theta, sensorAngle) rCoord = gm.polar2cart(dist, sensorAngle - width/2) lCoord = gm.polar2cart(dist, sensorAngle + width/2) rightLine = ((0,0), rCoord) leftLine = ((0,0), lCoord) tankRelPos = gm.minShift(self.pos, near[i].pos, self._limits) if r < (dist + self.RADIUS): if abs(dAngle) <= (width/2) or \ gm.segmentCircleCollision(rightLine, tankRelPos, self.RADIUS) or \ gm.segmentCircleCollision(leftLine, tankRelPos, self.RADIUS): self._sensorState[sensorId] = True break # Check for collisions here, since we already have all the data. collided = set() for i in range(len(near)): r, theta = polar[i] if r < (self.RADIUS + near[i].RADIUS): collided.add(near[i]) # Add this tank (a collision kills both, after all if collided: collided.add(self) return collided def die(self, reason): self.isDead = True self.deathReason = reason def _moveTurret(self): if self._tGoal is None or self._tAngle == self._tGoal: return diff = gm.angleDiff(self._tGoal, self._tAngle) if abs(diff) < self.TURRET_TURN_RATE: self._tAngle = self._tGoal elif diff > 0: self._tAngle = gm.reduceAngle(self._tAngle - self.TURRET_TURN_RATE) else: self._tAngle = gm.reduceAngle(self._tAngle + self.TURRET_TURN_RATE) def _move(self, lSpeed, rSpeed): assert abs(lSpeed) <= 100, "Bad speed value: %s" % lSpeed assert abs(rSpeed) <= 100, "Bad speed value: %s" % rSpeed # Handle acceleration if self._lastSpeed[0] < lSpeed and \ self._lastSpeed[0] + self.MAXACCEL < lSpeed: lSpeed = self._lastSpeed[0] + self.MAXACCEL elif self._lastSpeed[0] > lSpeed and \ self._lastSpeed[0] - self.MAXACCEL > lSpeed: lSpeed = self._lastSpeed[0] - self.MAXACCEL if self._lastSpeed[1] < rSpeed and \ self._lastSpeed[1] + self.MAXACCEL < rSpeed: rSpeed = self._lastSpeed[1] + self.MAXACCEL elif self._lastSpeed[1] > rSpeed and \ self._lastSpeed[1] - self.MAXACCEL > rSpeed: rSpeed = self._lastSpeed[1] - self.MAXACCEL self._lastSpeed = lSpeed, rSpeed # The simple case if lSpeed == rSpeed: fSpeed = self.MAXSPEED*lSpeed/100 x = fSpeed*math.cos(self._angle) y = fSpeed*math.sin(self._angle) # Adjust our position self._reposition((x,y), 0) return # The works as follows: # The tank drives around in a circle of radius r, which is some # offset on a line perpendicular to the tank. The distance it travels # around the circle varies with the speed of each tread, and is # such that each side of the tank moves an equal angle around the # circle. L = self.MAXSPEED * lSpeed/100.0 R = self.MAXSPEED * rSpeed/100.0 friction = .75 * abs(L-R)/(2.0*self.MAXSPEED) L = L * (1 - friction) R = R * (1 - friction) # Si is the speed of the tread on the inside of the turn, # So is the speed on the outside of the turn. # dir is to note the direction of rotation. if abs(L) > abs(R): Si = R; So = L dir = 1 else: Si = L; So = R dir = -1 # The width of the tank... w = self.RADIUS * 2 # This is the angle that will determine the circle the tank travels # around. # theta = math.atan((So - Sl)/w) # This is the distance from the outer tread to the center of the # circle formed by it's movement. r = w*So/(So - Si) # The fraction of the circle traveled is equal to the speed of # the outer tread over the circumference of the circle. # Ft = So/(2*pi*r) # The angle traveled is equal to the Fraction traveled * 2 * pi # This reduces to a simple: So/r # We multiply it by dir to adjust for the direction of rotation theta = So/r * dir # These are the offsets from the center of the circle, given that # the tank is turned in some direction. The tank is facing # perpendicular to the circle # So far everything has been relative to the outer tread. At this # point, however, we need to move relative to the center of the # tank. Hence the adjustment in r. x = -math.cos( self._angle + math.pi/2*dir ) * (r - w/2.0) y = -math.sin( self._angle + math.pi/2*dir ) * (r - w/2.0) # Now we just rotate the tank's position around the center of the # circle to get the change in coordinates. mx, my = gm.rotatePoint((x,y), theta) mx = mx - x my = my - y # Finally, we shift the tank relative to the playing field, and # rotate it by theta. self._reposition((mx, my), theta) def _reposition(self, move, angleChange): """Move the tank by x,y = move, and change it's angle by angle. I assume the tanks move slower than the boardSize.""" x = self.pos[0] + move[0] y = self.pos[1] + move[1] self._angle = self._angle + angleChange if x < 0: x = self._limits[0] + x elif x > self._limits[0]: x = x - self._limits[0] if y < 0: y = self._limits[1] + y elif y > self._limits[1]: y = y - self._limits[1] self.pos = round(x), round(y) while self._angle < 0: self._angle = self._angle + math.pi * 2 while self._angle > math.pi * 2: self._angle = self._angle - math.pi * 2 def describe(self, f): """Output a description of this tank""" f.write(' ["%s",[' % self.color) for i in range(len(self._sensors)): dist, sensorAngle, width, tSens = self._sensors[i] f.write('[%d,%.2f,%.2f,%d],' % (dist, sensorAngle, width, tSens)) f.write(']],\n') def draw(self, f): """Output this tank's state as JSON. [color, x, y, angle, turret_angle, led, fired] """ if self.isDead: f.write(' 0,\n') else: flags = (self._fired << 0) | (self.led << 1) sensors = 0 for i in range(len(self._sensorState)): sensors |= self._sensorState[i] << i f.write(' [%d,%d,%.2f,%.2f,%d,%d],\n' % (self.pos[0], self.pos[1], self._angle, self._tAngle, flags, sensors))