diff --git a/Makefile b/Makefile index 3bf5baf..8618ce3 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ MDWNTOHTML = $(CURDIR)/mdwntohtml.py --template=$(TEMPLATE) --base=$(BASE_URL) default: install -TARGETS = tanks puzzles +TARGETS = puzzles include $(wildcard */*.mk) CLEAN_TARGETS = $(addsuffix -clean, $(TARGETS)) INSTALL_TARGETS = $(addsuffix -install, $(TARGETS)) @@ -36,22 +36,6 @@ puzzles-install: puzzles-build puzzles-clean: rm -rf $(BUILD_DIR)/puzzles $(DESTDIR)$(WWW)/puzzler $(DESTDIR)$(LIB)/puzzler.keys -tanks-install: - install --directory $(DESTDIR)$(VAR)/tanks - install --directory $(DESTDIR)$(VAR)/tanks/results - install --directory $(DESTDIR)$(VAR)/tanks/errors - install --directory $(DESTDIR)$(VAR)/tanks/ai - install --directory $(DESTDIR)$(VAR)/tanks/ai/players - install --directory $(DESTDIR)$(VAR)/tanks/ai/house - - ln -sf $(VAR)/tanks/results $(DESTDIR)$(WWW)/tanks/results - - install bin/run-tanks $(DESTDIR)$(SBIN) - -tanks-clean: - rm -rf $(DESTDIR)$(VAR)/tanks - rm -rf $(DESTDIR)$(WWW)/tanks - install: $(INSTALL_TARGETS) install bin/pointscli $(DESTDIR)$(BIN) install bin/in.pointsd bin/in.flagd \ diff --git a/setup.py b/setup.py index 87813d8..45ab699 100755 --- a/setup.py +++ b/setup.py @@ -8,4 +8,4 @@ setup(name='ctf', author='Neale Pickett', author_email='neale@lanl.gov', url='http://dirtbags.net/ctf/', - packages=['ctf', 'tanks']) + packages=['ctf']) diff --git a/tanks/GameMath.py b/tanks/GameMath.py deleted file mode 100644 index 4af140c..0000000 --- a/tanks/GameMath.py +++ /dev/null @@ -1,196 +0,0 @@ -import math - -pi2 = math.pi * 2 - -def rotatePoint(point, angle): - """Assuming 0,0 is the center, rotate the given point around it.""" - - x,y = point - r = math.sqrt(x**2 + y**2) - if r == 0: - return 0, 0 - - theta = math.acos(x/r) - if y < 0: - theta = -theta - theta = theta + angle - return int(round(r*math.cos(theta))), int(round(r*math.sin(theta))) - -def rotatePoly(points, angle): - """Rotate the given list of points around 0,0 by angle.""" - return [ rotatePoint(point, angle) for point in points ] - -def displace(point, disp, limits): - """Displace point by disp, wrapping around limits.""" - x = (point[0] + disp[0]) - while x >= limits[0]: - x = x - limits[0] - while x < 0: - x = x + limits[0] - - y = (point[1] + disp[1]) - while y >= limits[1]: - y = y - limits[1] - while y < 0: - y = y + limits[1] - - return x,y - -def displacePoly(points, disp, limits, coordSequence=False): - """Displace each point (x,y) in 'points' by 'disp' (x,y). The limits of - the drawing space are assumed to be at x=0, y=0 and x=limits[0], - y=limits[1]. If the poly overlaps the edge of the drawing space, the - poly is duplicated on each side. -@param coordSequence: If true, the coordinates are returned as a sequence - - x1, y1, x2, y2, ... This is need by some PIL drawing - commands. -@returns: A list of polys, displaced by disp - """ - xDup = 0; yDup = 0 - maxX, maxY = limits - basePoints = [] - for point in points: - x,y = int(point[0] + disp[0]), int(point[1] + disp[1]) - - # Check if duplication is needed on each axis - if x > maxX: - # If this is negative, then we need to duplicate in the negative - # direction. - xDup = -1 - elif x < 0: - xDup = 1 - - if y > maxY: - yDup = -1 - elif y < 0: - yDup = 1 - - basePoints.append( (x,y) ) - - polys = [basePoints] - if xDup: - polys.append([(x + maxX*xDup, y) for x,y in basePoints] ) - if yDup: - polys.append([(x, maxY*yDup + y) for x,y in basePoints] ) - if xDup and yDup: - polys.append([(x+maxX*xDup, maxY*yDup+y) for x,y in basePoints]) - - # Switch coordinates to sequence mode. - # (x1, y1, x2, y2) instead of ((x1, y1), (x2, y2)) - if coordSequence: - seqPolys = [] - for poly in polys: - points = [] - for point in poly: - points.extend(point) - seqPolys.append(points) - polys = seqPolys - - return polys - -def polar2cart(r, theta): - """Return the cartesian coordinates for r, theta.""" - x = r*math.cos(theta) - y = r*math.sin(theta) - return x,y - -def minShift(center, point, limits): - """Get the minimum distances between the two points, given that the board - wraps at the givin limits.""" - dx = point[0] - center[0] - if dx < -limits[0]/2.0: - dx = point[0] + limits[0] - center[0] - elif dx > limits[0]/2.0: - dx = point[0] - (center[0] + limits[0]) - - dy = point[1] - center[1] - if dy < - limits[1]/2.0: - dy = point[1] + limits[1] - center[1] - elif dy > limits[1]/2.0: - dy = point[1] - (limits[1] + center[1]) - - return dx, dy - -def relativePolar(center, point, limits): - """Returns the angle, from zero, to the given point assuming this -center is the origin. Take into account wrapping round the limits of the board. -@returns: r, theta - """ - - dx, dy = minShift(center, point, limits) - - r = math.sqrt(dx**2 + dy**2) - theta = math.acos(dx/r) - if dy < 0: - theta = pi2 - theta - - return r, theta - -def reduceAngle(angle): - """Reduce the angle such that it is in 0 <= angle < 2pi""" - - return angle % pi2 - -def angleDiff(angle1, angle2): - """Returns the difference between the two angles. They are assumed -to be in radians, and must be in the range 0 <= angle < 2*pi. -@raises AssertionError: The angles given must be in the range 0 <= angle < 2pi -@returns: The minimum distance between the two angles; The distance - is negative if angle2 leads angle1 (clockwise).. - """ - - ret = (angle2 - angle1) % pi2 - if ret > math.pi: - ret -= pi2 - return ret - -def getDist(point1, point2): - """Returns the distance between point1 and point2.""" - dx = point2[0] - point1[0] - dy = point2[1] - point1[1] - - return math.sqrt(dx**2 + dy**2) - -def segmentCircleCollision(segment, center, radius): - """Return True if the given circle touches the given line segment. -@param segment: A list of two points [(x1,y1), (x2, y2)] that define - the line segment. -@param center: The center point of the circle. -@param radius: The radius of the circle. -@returns: True if the the circle touches the line segment, False otherwise. - """ - - a = getDist(segment[0], center) - c = getDist(segment[1], center) - base = getDist(segment[0], segment[1]) - - # If we're close enough to the end points, then we're close - # enough to the segment. - if a < radius or c < radius: - return True - - # First we find the are of the triangle formed by the line segment - # and point. I use Heron's formula for the area. Using this, we'll - # find the distance d from the point to the line. We'll later make - # sure that the collision is with the line segment, and not just the - # line. - s = (a + c + base)/2 - A = math.sqrt(s*(s - a)*(s - c)*(s - base)) - d = 2*A/base - -# print s, a, c, A, d, radius - - # If the distance from the point to the line is more than the - # target radius, this isn't a hit. - if d > radius: - return False - - # If the distance from an endpoint to the intersection between - # our line segment and the line perpendicular to it that passes through - # the point is longer than the line segment, then this isn't a hit. - elif math.sqrt(a**2 - d**2) > base or \ - math.sqrt(c**2 - d**2) > base: - return False - else: - # The triangle is acute, that means we're close enough. - return True diff --git a/tanks/Pflanzarr.py b/tanks/Pflanzarr.py deleted file mode 100644 index 58fdf4b..0000000 --- a/tanks/Pflanzarr.py +++ /dev/null @@ -1,392 +0,0 @@ -import fcntl -import math -import os -import random -import cgi -from sets import Set as set -from ctf import teams, html, paths -from cStringIO import StringIO - -from urllib import unquote, quote - -import Tank - -class NotEnoughPlayers(Exception): - pass - -class Pflanzarr: - SPACING = 150 - - def __init__(self, dir): - """Initialize a new game of Pflanzarr. -@param dir: The data directory.""" - - # Setup the game environment - self._setupDirectories(dir) - - # Figure out what game number this is. - self.gameNum = self._getGameNum() - self.gameFilename = os.path.join(self._resultsDir, '%04d.html' % self.gameNum) - - tmpPlayers = os.listdir(self._playerDir) - players = [] - AIs = {} - for fn in tmpPlayers: - p = unquote(fn) - if (not (p.startswith('.') - or p.endswith('#') - or p.endswith('~')) - and teams.exists(p)): - players.append(p) - AIs[p] = open(os.path.join(self._playerDir, fn)).read() - defaultAIs = self._getDefaultAIs(dir) - - if len(players) < 1: - raise NotEnoughPlayers() - - # The one is added to ensure that there is at least one house - # bot. - cols = int(math.ceil(math.sqrt(len(players) + 1))) - cols = max(cols, 2) - - rows = int(math.ceil(len(players)/float(cols))) - rows = max(rows, 2) - - self._board = (cols*self.SPACING, rows*self.SPACING) - - while len(players) < cols*rows: - players.append(None) - - self._tanks = [] - for i in range(cols): - for j in range(rows): - startX = i*self.SPACING + self.SPACING/2 - startY = j*self.SPACING + self.SPACING/2 - player = random.choice(players) - players.remove(player) - color = '#' + teams.color(player) - tank = Tank.Tank( player, (startX, startY), color, - self._board) - if player == None: - tank.set_program(random.choice(defaultAIs)) - else: - tank.set_program(AIs[player]) - self._tanks.append(tank) - - # We only want to make these once, so we do it here. - self._tanksByX = list(self._tanks) - self._tanksByY = list(self._tanks) - - self._deadTanks = set() - - def run(self, maxTurns=None): - kills = {} - for tank in self._tanks: - kills[tank] = set() - - # Open HTML output - hdr = StringIO() - hdr.write('\n' - '\n') - - # Decide on the winner - winner = self._chooseWinner(kills) - self.winner = winner.name - - # Now generate HTML body - body = StringIO() - body.write(' \n' % self._board) - body.write(' Sorry, you need an HTML5-capable browser to see this.\n' - ' \n' - '

\n') - if self.gameNum > 0: - body.write(' ← Prev |' % - (self.gameNum - 1)) - body.write(' Next → |' % - (self.gameNum + 1)) - body.write(' 0 fps\n' - '

\n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n' - ' \n') - - tanks = self._tanks[:] - tanks.remove(winner) - tanks[0:0] = [winner] - for tank in tanks: - if tank is winner: - rowStyle = ('style="font-weight: bold; ' - 'color: #000; ' - 'background-color: %s;"' % tank.color) - else: - rowStyle = 'style="background-color:%s; color: #000;"' % tank.color - if tank.name: - name = cgi.escape(tank.name) - else: - name = teams.house - body.write('' % - (rowStyle, - name, - len(kills[tank]), - cgi.escape(tank.deathReason))) - body.write('
TeamKillsCause of Death
%s%d%s
\n') - - links='''

Tanks

- -''' - - # Write everything out - html.write(self.gameFilename, - 'Tanks round %d' % self.gameNum, - body.getvalue(), - hdr=hdr.getvalue(), - links=links, - onload='start(turns);') - - - - def _killTanks(self, tanks, reason): - for tank in tanks: - if tank in self._tanksByX: - self._tanksByX.remove(tank) - if tank in self._tanksByY: - self._tanksByY.remove(tank) - - tank.die(reason) - - self._deadTanks = self._deadTanks.union(tanks) - - def _chooseWinner(self, kills): - """Choose a winner. In case of a tie, live tanks prevail, in case - of further ties, a winner is chosen at random. This outputs the winner - to the winners file and outputs a results table html file.""" - tanks = list(self._tanks) - def winSort(t1, t2): - """Sort by # of kill first, then by life status.""" - result = cmp(len(kills[t1]), len(kills[t2])) - if result != 0: - return result - - if t1.isDead and not t2.isDead: - return -1 - elif not t1.isDead and t2.isDead: - return 1 - else: - return 0 - tanks.sort(winSort) - tanks.reverse() - - # Get the list of potential winners - winners = [] - for i in range(len(tanks)): - if len( kills[tanks[0]] ) == len( kills[tanks[i]] ) and \ - tanks[0].isDead == tanks[i].isDead: - winners.append(tanks[i]) - else: - break - winner = random.choice(winners) - return winner - - - def _outputErrors(self, tank): - """Output errors for each team.""" - - out = tank.program.get_output() - print 'Errors %r: %r' % (tank, out) - - if tank.name == None: - return - - fileName = os.path.join(self._errorDir, quote(tank.name, '')) - open(fileName, 'w').write(tank.program.get_output()) - - def _getNear(self): - """A dictionary of the set of tanks nearby each tank. Nearby is - defined as within the square centered the tank with side length equal - twice the sensor range. Only a few tanks within the set (those in the - corners of the square) should be outside the sensor range.""" - - self._tanksByX.sort(lambda t1, t2: cmp(t1.pos[0], t2.pos[0])) - self._tanksByY.sort(lambda t1, t2: cmp(t1.pos[1], t2.pos[1])) - - nearX = {} - nearY = {} - for tank in self._tanksByX: - nearX[tank] = set() - nearY[tank] = set() - - numTanks = len(self._tanksByX) - offset = 1 - for index in range(numTanks): - cTank = self._tanksByX[index] - maxRange = cTank.SENSOR_RANGE + cTank.RADIUS + 1 - near = set([cTank]) - for i in [(j + index) % numTanks for j in range(1, offset)]: - near.add(self._tanksByX[i]) - while offset < numTanks: - nTank = self._tanksByX[(index + offset) % numTanks] - if (index + offset >= numTanks and - self._board[0] + nTank.pos[0] - cTank.pos[0] < maxRange): - near.add(nTank) - offset = offset + 1 - elif (index + offset < numTanks and - nTank.pos[0] - cTank.pos[0] < maxRange ): - near.add(nTank) - offset = offset + 1 - else: - break - - if offset > 1: - offset = offset - 1 - - for tank in near: - nearX[tank] = nearX[tank].union(near) - - offset = 1 - for index in range(numTanks): - cTank = self._tanksByY[index] - maxRange = cTank.SENSOR_RANGE + cTank.RADIUS + 1 - near = set([cTank]) - for i in [(j + index) % numTanks for j in range(1, offset)]: - near.add(self._tanksByY[i]) - while offset < numTanks: - nTank = self._tanksByY[(index + offset) % numTanks] - if (index + offset < numTanks and - nTank.pos[1] - cTank.pos[1] < maxRange): - near.add(nTank) - offset = offset + 1 - elif (index + offset >= numTanks and - self._board[1] + nTank.pos[1] - cTank.pos[1] < maxRange): - near.add(nTank) - offset = offset + 1 - else: - break - - if offset > 1: - offset = offset - 1 - - for tank in near: - nearY[tank] = nearY[tank].union(near) - - near = {} - for tank in self._tanksByX: - near[tank] = nearX[tank].intersection(nearY[tank]) - near[tank].remove(tank) - - return near - - def _setupDirectories(self, dir): - """Setup all the directories needed by the game.""" - - if not os.path.exists(dir): - os.mkdir(dir) - - self._dir = dir - - # Don't run more than one game at the same time. - self._lockFile = open(os.path.join(dir, '.lock'), 'a') - try: - fcntl.flock(self._lockFile, fcntl.LOCK_EX|fcntl.LOCK_NB) - except: - sys.exit(1) - - # Setup all the directories we'll need. - self._resultsDir = os.path.join(dir, 'results') - self._errorDir = os.path.join(dir, 'errors') - self._playerDir = os.path.join(dir, 'players') - - def _getDefaultAIs(self, basedir): - """Load all the house bot AIs.""" - defaultAIs = [] - - path = os.path.join(basedir, 'house') - files = os.listdir(path) - for fn in files: - if fn.startswith('.'): - continue - - fn = os.path.join(path, fn) - f = open(fn) - defaultAIs.append(f.read()) - - return defaultAIs - - def _getGameNum(self): - """Figure out what game number this is from the past games played.""" - - games = os.listdir(self._resultsDir) - games.sort() - if games: - fn = games[-1] - s, _ = os.path.splitext(fn) - return int(s) + 1 - else: - return 0 - -if __name__ == '__main__': - import sys, traceback - try: - p = Pflanzarr(sys.argv[1]) - p.run(int(sys.argv[3])) - except: - traceback.print_exc() - print "Usage: Pflanzarr.py dataDirectory #turns" - - diff --git a/tanks/Program.py b/tanks/Program.py deleted file mode 100755 index cb2a824..0000000 --- a/tanks/Program.py +++ /dev/null @@ -1,100 +0,0 @@ -#! /usr/bin/python - -import forf -import random -import rfc822 -from cStringIO import StringIO -from math import pi - -def deg2rad(deg): - return float(deg) * pi / 180 - -def rad2deg(rad): - return int(rad * 180 / pi) - -class Environment(forf.Environment): - def __init__(self, tank, stdout): - forf.Environment.__init__(self) - self.tank = tank - self.stdout = stdout - - def err(self, msg): - self.stdout.write('Error: %s\n' % msg) - - def msg(self, msg): - self.stdout.write('%s\n' % msg) - - def cmd_random(self, data): - high = data.pop() - ret = random.randrange(high) - data.push(ret) - - def cmd_fireready(self, data): - ret = self.tank.fireReady() - data.push(ret) - - def cmd_sensoractive(self, data): - sensor = data.pop() - try: - ret = int(self.tank.getSensorState(sensor)) - except KeyError: - ret = 0 - data.push(ret) - - def cmd_getturret(self, data): - rad = self.tank.getTurretAngle() - deg = rad2deg(rad) - data.push(deg) - - def cmd_setled(self, data): - self.tank.setLED() - - def cmd_fire(self, data): - self.tank.setFire() - - def cmd_move(self, data): - right = data.pop() - left = data.pop() - self.tank.setMove(left, right) - - def cmd_setturret(self, data): - deg = data.pop() - rad = deg2rad(deg) - self.tank.setTurretAngle(rad) - - -class Program: - def __init__(self, tank, source): - self.tank = tank - self.stdout = StringIO() - self.env = Environment(self.tank, self.stdout) - - code_str = self.read_source(StringIO(source)) - self.env.parse_str(code_str) - - def get_output(self): - return self.stdout.getvalue() - - def read_source(self, f): - """Read in a tank program, establish sensors, and return code. - - Tank programs are stored as rfc822 messages. The header - block includes fields for sensors (Sensor:) - and other crap which may be used later. - """ - - message = rfc822.Message(f) - print 'reading tank %s' % message['Name'] - sensors = message.getallmatchingheaders('Sensor') - for s in sensors: - k, v = s.strip().split(':') - r, angle, width, turret = [int(p) for p in v.split()] - r = float(r) / 100 - angle = deg2rad(angle) - width = deg2rad(width) - self.tank.addSensor(r, angle, width, turret) - return message.fp.read() - - def run(self): - self.env.eval() - diff --git a/tanks/Tank.py b/tanks/Tank.py deleted file mode 100644 index 906f183..0000000 --- a/tanks/Tank.py +++ /dev/null @@ -1,426 +0,0 @@ -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)) diff --git a/tanks/__init__.py b/tanks/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tanks/docs.py b/tanks/docs.py deleted file mode 100644 index 0318250..0000000 --- a/tanks/docs.py +++ /dev/null @@ -1,26 +0,0 @@ -import xml.sax.saxutils - -def mkDocTable(objects): - objects.sort(lambda o1, o2: cmp(o1.__doc__, o2.__doc__)) - - for object in objects: - if object.__doc__ is None: - print '
%s
Bad object
' % \ - xml.sax.saxutils.escape(str(object)) - continue - text = object.__doc__ - lines = text.split('\n') - head = lines[0].strip() - head = xml.sax.saxutils.escape(head) - - body = [] - for line in lines[1:]: - line = line.strip() #xml.sax.saxutils.escape( line.strip() ) - line = line.replace('.', '.
') - body.append(line) - - body = '\n'.join(body) - print '
%s
%s
' % (head, body) - #print '%sIntentionally blank%s' % (head, body) - - diff --git a/tanks/forf.py b/tanks/forf.py deleted file mode 100755 index e2c8b29..0000000 --- a/tanks/forf.py +++ /dev/null @@ -1,290 +0,0 @@ -#! /usr/bin/python - -"""A shitty FORTH interpreter - -15:58 WELCOME TO FORF! -15:58 *PUNCH* -""" - -import operator - -class ParseError(Exception): - pass - -class Overflow(Exception): - pass - -class Underflow(Exception): - pass - -class Stack: - def __init__(self, init=None, size=50): - self.size = size - self.stack = init or [] - - def __str__(self): - if not self.stack: - return '{}' - guts = ' '.join(repr(i) for i in self.stack) - return '{ %s }' % guts - __repr__ = __str__ - - def push(self, *values): - for val in values: - if len(self.stack) == self.size: - raise Overflow() - self.stack.append(val) - - def extend(self, other): - self.stack.extend(other.stack) - - def dup(self): - return Stack(init=self.stack[:], size=self.size) - - def pop(self): - if not self.stack: - raise Underflow() - return self.stack.pop() - - def mpop(self, n): - return [self.pop() for i in range(n)] - - def __nonzero__(self): - return bool(self.stack) - - -class Environment: - def __init__(self, ticks=2000, codelen=500): - self.ticks = ticks - self.codelen = codelen - self.registers = [0] * 10 - self.unfuncs = {'~' : operator.inv, - '!' : operator.not_, - 'abs': operator.abs, - } - self.binfuncs = {'+' : operator.add, - '-' : operator.sub, - '*' : operator.mul, - '/' : operator.div, - '%' : operator.mod, - '**': operator.pow, - '&' : operator.and_, - '|' : operator.or_, - '^' : operator.xor, - '<<': operator.lshift, - '>>': operator.rshift, - '>' : operator.gt, - '>=': operator.ge, - '<' : operator.lt, - '<=': operator.le, - '=' : operator.eq, - '<>': operator.ne, - '!=': operator.ne, - } - self.data = Stack() - - def get(self, s): - unfunc = self.unfuncs.get(s) - if unfunc: - return self.apply_unfunc(unfunc) - - binfunc = self.binfuncs.get(s) - if binfunc: - return self.apply_binfunc(binfunc) - - try: - return getattr(self, 'cmd_' + s) - except AttributeError: - return None - - def apply_unfunc(self, func): - """Apply a unary function""" - - def f(data): - a = data.pop() - data.push(int(func(a))) - return f - - def apply_binfunc(self, func): - """Apply a binary function""" - - def f(data): - a = data.pop() - b = data.pop() - data.push(int(func(b, a))) - return f - - def run(self, s): - self.parse_str(s) - self.eval() - - def parse_str(self, s): - tokens = s.strip().split() - tokens.reverse() # so .parse can tokens.pop() - self.code = self.parse(tokens) - - def parse(self, tokens, token=0, depth=0): - if depth > 4: - raise ParseError('Maximum recursion depth exceeded at token %d' % token) - code = [] - while tokens: - val = tokens.pop() - token += 1 - f = self.get(val) - if f: - code.append(f) - elif val == '(': - # Comment - while val != ')': - val = tokens.pop() - token += 1 - elif val == '{}': - # Empty block - code.append(Stack()) - elif val == '{': - block = self.parse(tokens, token, depth+1) - code.append(block) - elif val == '}': - break - else: - # The only literals we support are ints - try: - code.append(int(val)) - except ValueError: - raise ParseError('Invalid literal at token %d (%s)' % (token, val)) - if len(code) > self.codelen: - raise ParseError('Code stack overflow') - # Reverse so we can .pop() - code.reverse() - return Stack(code, size=self.codelen) - - def eval(self): - ticks = self.ticks - code_orig = self.code.dup() - while self.code and ticks: - ticks -= 1 - val = self.code.pop() - try: - if callable(val): - val(self.data) - else: - self.data.push(val) - except Underflow: - self.err('Stack underflow at proc %r' % (val)) - except Overflow: - self.err('Stack overflow at proc %r' % (val)) - if self.code: - self.err('Ran out of ticks!') - self.code = code_orig - - def err(self, msg): - print 'Error: %s' % msg - - def msg(self, msg): - print msg - - ## - ## Commands - ## - def cmd_print(self, data): - a = data.pop() - self.msg(a) - - def cmd_dumpstack(self, data): - a = data.pop() - self.msg('(dumpstack %d) %r' % (a, data.stack)) - - def cmd_dumpmem(self, data): - a = data.pop() - self.msg('(dumpmem %d) %r' % (a, self.registers)) - - def cmd_exch(self, data): - a, b = data.mpop(2) - data.push(a, b) - - def cmd_dup(self, data): - a = data.pop() - data.push(a, a) - - def cmd_pop(self, data): - data.pop() - - def cmd_store(self, data): - a, b = data.mpop(2) - self.registers[a % 10] = b - - def cmd_fetch(self, data): - a = data.pop() - data.push(self.registers[a % 10]) - - ## - ## Evaluation commands - ## - def eval_block(self, block): - try: - self.code.extend(block) - except TypeError: - # If it's not a block, just append it - self.code.push(block) - - def cmd_if(self, data): - block = data.pop() - cond = data.pop() - if cond: - self.eval_block(block) - - def cmd_ifelse(self, data): - elseblock = data.pop() - ifblock = data.pop() - cond = data.pop() - if cond: - self.eval_block(ifblock) - else: - self.eval_block(elseblock) - - def cmd_eval(self, data): - # Interestingly, this is the same as "1 exch if" - block = data.pop() - self.eval_block(block) - - def cmd_call(self, data): - # Shortcut for "fetch eval" - self.cmd_fetch(data) - self.cmd_eval(data) - - -def repl(): - env = Environment() - while True: - try: - s = raw_input('>8[= =] ') - except (KeyboardInterrupt, EOFError): - print - break - try: - env.run(s) - print env.data - except ParseError, err: - print r' \ nom nom nom, %s!' % err - print r' \ bye bye!' - -if __name__ == '__main__': - import sys - import time - try: - import readline - except ImportError: - pass - - if len(sys.argv) > 1: - s = open(sys.argv[1]).read() - env = Environment() - begin = time.time() - env.run(s) - end = time.time() - elapsed = end - begin - print 'Evaluated in %.2f seconds' % elapsed - else: - print 'WELCOME TO FORF!' - print '*PUNCH*' - repl()