diff --git a/ctf/teams.py b/ctf/teams.py index 4f73799..41da559 100755 --- a/ctf/teams.py +++ b/ctf/teams.py @@ -3,7 +3,11 @@ import fcntl import time import os -from urllib.parse import quote, unquote +# python 2 compatibility +try: + from urllib.parse import quote, unquote +except: + from urllib import quote, unquote from . import config house = config.get('global', 'house_team') diff --git a/tanks/AI/easy/berzerker b/tanks/AI/easy/berzerker new file mode 100644 index 0000000..e0351d0 --- /dev/null +++ b/tanks/AI/easy/berzerker @@ -0,0 +1,5 @@ +random(1,10): move(50, 100); +random(1,10): move(100, 50); +random(1,10): turretcw(); +random(1,10): turretccw(); +random(1,30): fire(); \ No newline at end of file diff --git a/tanks/AI/easy/rabbitwithgun b/tanks/AI/easy/rabbitwithgun new file mode 100644 index 0000000..b820488 --- /dev/null +++ b/tanks/AI/easy/rabbitwithgun @@ -0,0 +1,8 @@ +>addsensor(50, 0, 0, 1); +>addsensor(70, 0, 50); # 1-Anti-collision sensor + + : move(100, 100) . turretset(180); +random(1, 8): move(70, 100); +random(1, 8): move(100, 70); +sense(0) : fire(); +sense(1) : move(-0, 100); \ No newline at end of file diff --git a/tanks/AI/hard/crashmaster b/tanks/AI/hard/crashmaster new file mode 100644 index 0000000..ca94b98 --- /dev/null +++ b/tanks/AI/hard/crashmaster @@ -0,0 +1,57 @@ +# 3 +# 000 33 +# 2 +# 2 +# 2 +# 11111 4 +# 4 +# 4 +# @@/ +# @@@ +# @@@ +# +# +# +# + + +>addsensor(50, 0, 05, 1); # 0 Fire Sensor +>addsensor(30, 0, 50); # 1 Anti-collision sensor +>addsensor(50, 0, 10); # 2 Anti-collision sensor +>addsensor(100, 315, 100, 1); # 3 Turret ccw +>addsensor(100, 45, 100, 1); # 4 Turret cw +>addsensor(60, 180, 180, 0); # 5 Ass + +## +## Add "ears" so the tank is easy to pick out. +## +>addsensor(20, 90, 30, 0); +>addsensor(20, 270, 30, 0); + +# Can't fire + : led(0) . move(80, 80) . turretset(0); +random(1, 3): led(0) . move(60, 80) . turretset(0); +random(2, 3): led(0) . move(80, 60) . turretset(0); + +sense(0) : led(0) . move(10, 20) . turretset(0); +sense(1) : led(0) . move(10, 10) . turretset(0); +sense(2) : led(0) . move(10, 20) . turretset(0); +sense(3) : led(0) . move(70, 50) . turretset(0); +sense(4) : led(0) . move(50, 70) . turretset(0); +sense(3) & sense(4): led(0) . move(-100, 20) . turretset(0); +sense(5) : led(0) . move(100, 50) . turretset(0); + + +# Can fire +fireready() : led(1) . move(70, 70) . turretset(0); +fireready() & random(2, 40): led(1) . move(40, 70) . turretset(0); +fireready() & random(1, 40): led(1) . move(70, 40) . turretset(0); + +fireready() & sense(3) : led(1) . move(0, 60) . turretccw(50); +fireready() & sense(4) : led(1) . move(60, 0) . turretcw(50); +fireready() & sense(3) & sense(4): led(1) . move(100, 100) . turretset(); +fireready() & sense(1) : led(1) . turretset(0) . move(10, 10); +fireready() & sense(2) : led(1) . turretset(0) . move(10, 10); +fireready() & sense(0) : led(1) . turretset() . fire(); + +fireready() & sense(5) : led(1) . move(100, 40); \ No newline at end of file diff --git a/tanks/AI/hard/foobar b/tanks/AI/hard/foobar new file mode 100644 index 0000000..bcdf807 --- /dev/null +++ b/tanks/AI/hard/foobar @@ -0,0 +1,22 @@ +>addsensor(55, 0, 5, 1); +>addsensor(40, 0, 30); +>addsensor(80, 30, 59, 0); +>addsensor(80, 330, 59, 0); +>addsensor(70, 180, 180); +>addsensor(80, 90, 59, 0); +>addsensor(80, 270, 59, 0); + +# : move(70,80); +# random(3,6) : move(80,70); + : move(65,85); +random(2,6) : move(90,65); +sense(2) : move(80,10).turretcw(100); +sense(3) : move(10,80).turretccw(100); +sense(4) : move(90, 90); +sense(5) : move(90,10).turretcw(100); +sense(6) : move(10,90).turretccw(100); +sense(0) & fireready() : turretset().move(90,90).fire(); +sense(1) : move(-100, -100); + : turretset(0); +fireready() : led(); + diff --git a/tanks/AI/hard/pflarr b/tanks/AI/hard/pflarr new file mode 100644 index 0000000..b4df468 --- /dev/null +++ b/tanks/AI/hard/pflarr @@ -0,0 +1,31 @@ +>addsensor(50, 0, 45, 1); # 0-Fire Sensor +>addsensor(30, 0, 180); # 1-Anti-collision sensor +>addsensor(100, 40, 60, 1); # 2 turret clockwise +>addsensor(100, 320, 60, 1); # 3 turret ccw +>addsensor(80, 180, 160); # 4 Coward +>addsensor(100, 0, 0, 1); # 5-Fire Sensor2 +>addsensor(100, 0, 0); # 6-Chase Sensor +>addsensor(75, 75, 30); # 7-quick turn right +>addsensor(75, 285, 30); # 8-quick turn left + +# Commands + : move(70, 75). + turretset(0); +random(1, 10): move(75, 75). + turretset(0); +sense(2) : turretcw(50). + move(85, 70); +sense(2) & sense(0): turretcw(25). + move(85, 70); +sense(3) : turretccw(50). + move(70, 85); +sense(3) & sense(0) : turretccw(25). + move(70, 85); +sense(5) & sense(7) : move(70, 30); +sense(5) & sense(8) : move(30, 70); +#sense(5) : turretset(); +sense(0) & sense(5) : fire(); +sense(6) & sense(5) & fireready(): move(100,100); +sense(4) : move(100,100); +sense(1) : move(-50, 25); +fireready() : led(); diff --git a/tanks/AI/medium/simpleton b/tanks/AI/medium/simpleton new file mode 100644 index 0000000..64fc607 --- /dev/null +++ b/tanks/AI/medium/simpleton @@ -0,0 +1,8 @@ +>addsensor(50, 0, 5, 1); # 0-Fire Sensor +>addsensor(30, 0, 50); # 1-Anti-collision sensor + +# Commands + : move(90, 100). + turretset(0); +sense(0) : fire(); +sense(1) : move(-100, 100) diff --git a/tanks/AI/medium/sittingduckwithteeth b/tanks/AI/medium/sittingduckwithteeth new file mode 100644 index 0000000..4ea551f --- /dev/null +++ b/tanks/AI/medium/sittingduckwithteeth @@ -0,0 +1,9 @@ +>addsensor(50, 0, 10, 1); # 0-Fire Sensor +>addsensor(100, 90, 150, 1); +>addsensor(100, 270, 150, 1); + +: turretcw(75); +sense(0): fire(); +sense(1): turretcw(); +sense(2): turretccw(); + diff --git a/tanks/AI/medium/sweeper b/tanks/AI/medium/sweeper new file mode 100644 index 0000000..e02e950 --- /dev/null +++ b/tanks/AI/medium/sweeper @@ -0,0 +1,18 @@ +# Just sit there and sweep the field until it finds something to shoot. +# Uses a long-range sensor on the left and right to hone in. + +>addsensor(50, 0, 5, 1); # 0 +>addsensor(100, 90, 150, 1); # 1 +>addsensor(100, 270, 150, 1); # 2 +>addsensor(100, 0, 359, 0); # 3 + +# Default movement if nothing is detected + : move(70, 70) . turretccw(); +random(2, 3): move(40, 70) . turretccw(); +random(1, 3): move(70, 40) . turretccw(); + +# We found something!! +sense(3): move(0, 0); +sense(1): turretcw(); +sense(2): turretccw(); +sense(0): fire(); diff --git a/tanks/Makefile b/tanks/Makefile new file mode 100644 index 0000000..23fdfc3 --- /dev/null +++ b/tanks/Makefile @@ -0,0 +1,34 @@ +FAKE = fakeroot -s fake -i fake +INSTALL = $(fake) install + +all: tanks.tce + +tanks.tce: target + $(FAKE) sh -c 'cd target && tar -czf - .' > $@ + +target: + $(INSTALL) -d target/var/lib/tanks/ + $(INSTALL) -d target/var/lib/tanks/results/ + $(INSTALL) -d target/var/lib/tanks/ai/easy + $(INSTALL) -d target/var/lib/tanks/ai/medium + $(INSTALL) -d target/var/lib/tanks/ai/hard + $(INSTALL) -d target/var/lib/tanks/ai/players + $(INSTALL) AI/easy/* target/var/lib/tanks/ai/easy/ + $(INSTALL) AI/medium/* target/var/lib/tanks/ai/medium/ + $(INSTALL) AI/hard/* target/var/lib/tanks/ai/hard/ + + $(INSTALL) -d target/var/lib/www/tanks/ + $(INSTALL) www/* target/var/lib/www/tanks/ +# $(FAKE) ln -s target/var/lib/tanks/ target/var/lib/www/data + + $(INSTALL) -d target/usr/lib/python2.6/site-packages/tanks/ + $(INSTALL) lib/* target/usr/lib/python2.6/site-packages/tanks/ + + $(INSTALL) -d target/var/service/tanks + $(INSTALL) run target/var/service/tanks/run + + $(INSTALL) -d target/var/service/tanks/log/ + $(INSTALL) log.run target/var/service/tanks/log/run + +clean: + rm -rf target tanks.tce fake diff --git a/tanks/lib/.actions.py.swp b/tanks/lib/.actions.py.swp new file mode 100644 index 0000000..420b228 Binary files /dev/null and b/tanks/lib/.actions.py.swp differ diff --git a/tanks/lib/Function.py b/tanks/lib/Function.py new file mode 100644 index 0000000..7017f55 --- /dev/null +++ b/tanks/lib/Function.py @@ -0,0 +1,47 @@ +import math + +class Function(object): + """Represents a single condition or action. This doc string is printed + as user documentation. You should override it to say something useful.""" + + def __call__(self, tank): + """The __call__ method should be of this basic form. Actions + should return None, conditions should return True or False. Actions + should utilize the set* methods of tanks. Conditions can utilize the + tanks get* methods.""" + pass + + def _limitArgs(self, args, max): + """Raises a ValueError if there are more than max args.""" + if len(args) > max: + raise ValueError("Too many arguments: %s" % ','.join(args)) + + def _checkRange(self, value, name, min=0, max=100): + """Check that the value is in the given range. + Raises an exception with useful info for invalid values. Name is used to + let the user know which value is wrong.""" + try: + value = int(value) + except: + raise ValueError("Invalid %s value: %s" % (name, value)) + assert value >= min and value <= max, "Invalid %s. %ss must be in"\ + " the %s %d-%d" % \ + (name, name.capitalize(), value, min, max) + + return value + + @staticmethod + def _convertAngle(value, name): + """Parse the given value as an angle in degrees, and return its value + in radians. Raise useful errors. + Name is used in the errors to describe the field.""" + try: + angle = math.radians(value) + except: + raise ValueError("Invalid %s value: %s" % (name, value)) + + assert angle >= 0 and angle < 2*math.pi, "Invalid %s; "\ + "It be in the range 0 and 359." % name + + return angle + diff --git a/tanks/lib/GameMath.py b/tanks/lib/GameMath.py new file mode 100644 index 0000000..ff47880 --- /dev/null +++ b/tanks/lib/GameMath.py @@ -0,0 +1,206 @@ +import math + +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 = point[0] + disp[0], 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 = 2*math.pi - theta + + return r, theta + +def reduceAngle(angle): + """Reduce the angle such that it is in 0 <= angle < 2pi""" + + while angle >= math.pi*2: + angle = angle - math.pi*2 + while angle < 0: + angle = angle + math.pi*2 + + return angle + +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).. + """ + + for angle in angle1, angle2: + assert angle < 2*math.pi and angle >= 0, \ + 'angleDiff: bad angle %s' % angle + + diff = angle2 - angle1 + if diff > math.pi: + diff = diff - 2*math.pi + elif diff < -math.pi: + diff = diff + 2*math.pi + + return diff + +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/lib/Pflanzarr.py b/tanks/lib/Pflanzarr.py new file mode 100644 index 0000000..8f05ed3 --- /dev/null +++ b/tanks/lib/Pflanzarr.py @@ -0,0 +1,400 @@ +import fcntl +import math +import os +import random +import subprocess +import xml.sax.saxutils + +from PIL import Image, ImageColor, ImageDraw + +try: + from ctf import teams +except: + import sys + path = '/home/pflarr/repos/gctf/' + sys.path.append(path) + from ctf import teams + +import Tank + +class Pflanzarr: + + FRAME_DELAY = 15 + + SPACING = 150 + backgroundColor = '#ffffff' + + def __init__(self, dir, difficulty='easy'): + """Initialize a new game of Pflanzarr. +@param dir: The data directory.""" + + assert difficulty in ('easy', 'medium', 'hard') + + # Setup the game environment + self._setupDirectories(dir) + + # Figure out what game number this is. + self._gameNum = self._getGameNum() + self._gameDir = os.path.join(self._resultsDir, str(self._gameNum)) + if not os.path.exists(self._gameDir): + os.mkdir(self._gameDir) + + tmpPlayers = os.listdir(self._playerDir) + players = [] + for p in tmpPlayers: + p = unquote(p) + if not (p.startswith('.') or p.endswith('#') or p.endswith('~')) + and p in teams.teams: + players.append(p) + + AIs = {} + for player in players: + AIs[player] = open(os.path.join(self._playerDir, player)).read() + defaultAIs = self._getDefaultAIs(dir, difficulty) + + assert len(players) >= 1, "There must be at least one player." + + # The one is added to ensure that there is at least one defaultAI bot. + size = math.sqrt(len(players) + 1) + if int(size) != size: + size = size + 1 + + size = int(size) + if size < 2: + size = 2 + + self._board = (size*self.SPACING, size*self.SPACING) + + while len(players) < size**2: + players.append('#default') + + self._tanks = [] + for i in range(size): + for j in range(size): + startX = i*self.SPACING + self.SPACING/2 + startY = j*self.SPACING + self.SPACING/2 + player = random.choice(players) + players.remove(player) + if player == '#default': + color = '#a0a0a0' + else: + color = team.teams[player][1] + tank = Tank.Tank( player, (startX, startY), color, + self._board, testMode=True) + if player == '#default': + tank.program(random.choice(defaultAIs)) + else: + tank.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): + + print "Starting new game with %d players." % len(self._tanks) + + kills = {} + for tank in self._tanks: + kills[tank] = set() + + turn = 0 + lastTurns = 3 + while ((maxTurns is None) or turn < maxTurns) and lastTurns > 0: + if len(self._tanks) - len(self._deadTanks) < 2: + lastTurns = lastTurns - 1 + + image = Image.new('RGB', self._board) + draw = ImageDraw.Draw(image) + draw.rectangle(((0,0), self._board), fill=self.backgroundColor) + near = self._getNear() + deadThisTurn = set() + + liveTanks = set(self._tanks).difference(self._deadTanks) + + for tank in liveTanks: + # Shoot now, if we said to shoot last turn + dead = tank.fire( near[tank] ) + kills[tank] = kills[tank].union(dead) + self._killTanks(dead, 'Shot by %s' % tank.name) + + for tank in liveTanks: + # We also check for collisions here, while we're at it. + dead = tank.sense( near[tank] ) + kills[tank] = kills[tank].union(dead) + self._killTanks(dead, 'Collision') + + # Draw the explosions + for tank in self._deadTanks: + tank.draw(image) + + # Draw the live tanks. + for tank in self._tanksByX: + # Have the tank run its program, move, etc. + tank.draw(image) + + # Have the live tanks do their turns + for tank in self._tanksByX: + tank.execute() + + fileName = os.path.join(self._imageDir, '%05d.ppm' % turn) + image.save(fileName, 'PPM') + turn = turn + 1 + + # Removes tanks from their own kill lists. + for tank in kills: + if tank in kills[tank]: + kills[tank].remove(tank) + + self._saveResults(kills) + for tank in self._tanks: + self._outputErrors(tank) + self._makeMovie() + + 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 _saveResults(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.""" + resultsFile = open(os.path.join(self._gameDir, 'results.html'), 'w') + winnerFile = open(os.path.join(self._dir, 'winner'),'w') + + 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, reverse=1) + + # 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) + + html = ['', + '
TeamKillsCause of Death'] + for tank in tanks: + if tank is winner: + rowStyle = 'style="color:red;"' + else: + rowStyle = '' + html.append('
%s%d%s' % + (rowStyle, + xml.sax.saxutils.escape(tank.name), + len(kills[tank]), + xml.sax.saxutils.escape(tank.deathReason))) + + html.append('
') + + if winner.name != '#default': + winnerFile.write(tanks[0].name) + winnerFile.close() + + resultsFile.write('\n'.join(html)) + resultsFile.close() + + def _makeMovie(self): + """Make the game movie.""" + + movieCmd = ['ffmpeg', + '-r', '10', # Set the framerate to 10/second + '-b', '8k', # Set the bitrate + '-i', '%s/%%05d.ppm' % self._imageDir, # The input files. +# '-vcodec', 'msmpeg4v2', + '%s/game.avi' % self._gameDir] + +# movieCmd = ['mencoder', 'mf://%s/*.png' % self._imageDir, +# '-mf', 'fps=10', '-o', '%s/game.avi' % self._gameDir, +# '-ovc', 'lavc', '-lavcopts', +# 'vcodec=msmpeg4v2:vbitrate=800'] + clearFrames = ['rm', '-rf', '%s' % self._imageDir] + + print 'Making Movie' + subprocess.call(movieCmd) +# subprocess.call(movieCmd, stderr=open('/dev/null', 'w'), +# stdout=open('/dev/null', 'w')) + subprocess.call(clearFrames) + + def _outputErrors(self, tank): + """Output errors for each team.""" + if tank.name == '#default': + return + + if tank._program.errors: + print tank.name, 'has errors' + + + fileName = os.path.join(self._errorDir, tank.name) + file = open(fileName, 'w') + for error in tank._program.errors: + file.write(error) + file.write('\n') + file.close() + + 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._imageDir = os.path.join(dir, 'frames') + os.mkdir( self._imageDir ) + self._playerDir = os.path.join(dir, 'ai', 'players') + + def _getDefaultAIs(self, dir, difficulty): + """Load all the 'computer' controlled bot AIs for the given + difficulty.""" + defaultAIs = [] + + path = os.path.join(dir, 'ai', difficulty) + files = os.listdir( path ) + for file in files: + if file.startswith('.'): + continue + + path = os.path.join(dir, 'ai', difficulty, file) + file = open( path ) + defaultAIs.append( file.read() ) + + return defaultAIs + + def _getGameNum(self): + """Figure out what game number this is from the past games played.""" + + oldGames = os.listdir(self._resultsDir) + games = [] + for dir in oldGames: + try: + games.append( int(dir) ) + except: + continue + + games.sort(reverse=True) + if games: + return games[0] + 1 + else: + return 0 + +if __name__ == '__main__': + import sys, traceback + try: + p = Pflanzarr(sys.argv[1], sys.argv[2]) + p.run( int(sys.argv[3]) ) + except: + traceback.print_exc() + print "Usage: python2.6 Pflanzarr.py dataDirectory easy|medium|hard #turns" + + diff --git a/tanks/lib/Program.py b/tanks/lib/Program.py new file mode 100644 index 0000000..ad30b1d --- /dev/null +++ b/tanks/lib/Program.py @@ -0,0 +1,232 @@ +"""

Introduction

+You are the proud new operator of a M-375 Pflanzarr Tank. Your tank is +equipped with a powerful laser cannon, independently rotating turret +section, up to 10 enemy detection sensors, and a standard issue NATO hull. +Unfortunately, it lacks seats, and thus must rely own its own wits and your +skills at designing those wits to survive. + +

Programming Your Tank

+Your tanks are programmed using the Super Useful Command and Kontrol language, +the very best in laser tank AI languages. It includes amazing feature such +as comments (Started by a #, ended at EOL), logic, versatility, and +semi-colons (all lines must end in one). As with all new military systems +it utilizes only integers; we must never rest in our +diligence against the communist floating point conspiracy. Whitespace is +provided by trusted contractors, and should never interfere with operations. +

+Your program should be separated into Setup and AI commands. The definitions +section lets you designated the behaviors of its sensors and memory. +Each setup command must begin with a '>'. Placing setup commands after +the first AI command is a violation of protocol. +Here are some examples of correct setup commands: +

>addsensor(80, 90, 33);
+>addsensor(50, 0, 10, 1);
+>addtimer(3);
+ +The AI section will act as the brain of your tank. Each AI line is +separated into a group of conditions functions and a group of action +functions. If all the conditions are satisfactory (true), all of the actions +are given as orders. Conditions are separated by ampersands, actions separated +by periods. Here are some examples of AI commands: +
+sensor(1) & sensor(2) & fireready() : fire();
+sensor(0,0)&sin(5): move(40, 30) . turretcw(50);
+sensor(4) & random(4,5) : led(1).settoggle(0,1);
+ +Your tank will check its program each turn, and attempt to the best of its +abilities to carry out its orders (or die trying). Like any military mind, +your tank may receive a plethora of often conflicting orders and information. +This a SMART TANK, however. It knows that the proper thing to do with each +subsystem is to have that subsystem follow only the last order given each turn. +""" + +import conditions +import actions +import setup + +class Statement(object): + """Represents a single program statement. If all the condition Functions + evaluate to True, the actions are all executed in order.""" + + def __init__(self, lineNum, line, conditions, actions): + self.lineNum = lineNum + self.line = line + self._conditions = conditions + self._actions = actions + + def __call__(self, tank): + success = True + for condition in self._conditions: + if not condition(tank): + success = False + break + + if success: + for action in self._actions: + action(tank) + +class Program(object): + """This parses and represents a Tank program.""" + CONDITION_SEP = '&' + ACTION_SEP = '.' + + def __init__(self, text): + """Initialize this program, parsing the given text.""" + self.errors = [] + + self._program, self._setup = self._parse(text) + + def setup(self, tank): + """Execute all the setup actions.""" + for action in self._setup: + try: + action(tank) + except Exception, msg: + self.errors.append("Bad setup action, line %d, msg: %s" % \ + (action.lineNum, msg)) + + def __call__(self, tank): + """Execute this program on the given tank.""" + for statement in self._program: + try: + statement(tank) + except Exception, msg: + self.errors.append('Error executing program. \n' + '(%d) - %s\n' + 'msg: %s\n' % + (statement.lineNum, statement.line, msg) ) + + def _parse(self, text): + """Parse the text of the given program.""" + program = [] + setup = [] + inSetup = True + lines = text.split(';') + lineNum = 0 + for line in lines: + lineNum = lineNum + 1 + + originalLine = line + + # Remove Comments + parts = line.split('\n') + for i in range(len(parts)): + comment = parts[i].find('#') + if comment != -1: + parts[i] = parts[i][:comment] + # Remove all whitespace + line = ''.join(parts) + line = line.replace('\r', '') + line = line.replace('\t', '') + line = line.replace(' ', '') + + if line == '': + continue + + if line.startswith('>'): + if inSetup: + if '>' in line[1:] or ':' in line: + self.errors.append('(%d) Missing semicolon: %s' % + (lineNum, line)) + continue + + try: + setupAction = self._parseSection(line[1:], 'setup')[0] + setupAction.lineNum = lineNum + setup.append(setupAction) + except Exception, msg: + self.errors.append('(%d) Error parsing setup line: %s' + '\nThe error was: %s' % + (lineNum, originalLine, msg)) + + continue + else: + self.errors.append('(%d) Setup lines aren\'t allowed ' + 'after the first command: %s' % + (lineNum, originalLine)) + else: + # We've hit the first non-blank, non-comment, non-setup + # line + inSetup = False + + semicolons = line.count(':') + if semicolons > 1: + self.errors.append('(%d) Missing semicolon: %s' % + (lineNum, line)) + continue + elif semicolons == 1: + conditions, actions = line.split(':') + else: + self.errors.append('(%d) Invalid Line, no ":" seperator: %s'% + (lineNum, line) ) + + try: + conditions = self._parseSection(conditions, 'condition') + except Exception, msg: + self.errors.append('(%d) %s - "%s"' % + (lineNum, msg, line) ) + continue + + try: + actions = self._parseSection(actions, 'action') + except Exception, msg: + self.errors.append('(%d) %s - "%s"' % + (lineNum, msg, originalLine) ) + continue + program.append(Statement(lineNum, line, conditions, actions)) + + return program, setup + + def _parseSection(self, section, sectionType): + """Parses either the action or condition section of each command. +@param section: The text of the section of the command to be parsed. +@param sectionType: The type of section to be parsed. Should be: + 'condition', 'action', or 'setup'. +@raises ValueError: Raises ValueErrors when parsing errors occur. +@returns: Returns a list of parsed section components (Function objects). + """ + + if sectionType == 'condition': + parts = section.split(self.CONDITION_SEP) + functions = conditions.conditions + if section == '': + return [] + elif sectionType == 'action': + parts = section.split(self.ACTION_SEP) + functions = actions.actions + if section == '': + raise ValueError("The action section cannot be empty.") + elif sectionType == 'setup': + parts = [section] + functions = setup.setup + else: + raise ValueError('Invalid section Type - Contact Contest Admin') + + parsed = [] + for part in parts: + + pos = part.find('(') + if pos == -1: + raise ValueError("Missing open paren in %s: %s" % + (sectionType, part) ) + funcName = part[:pos] + + if funcName not in functions: + raise ValueError("%s function %s is not accepted." % + (sectionType.capitalize(), funcName) ) + + if part[-1] != ')': + raise ValueError("Missing closing paren in %s: %s" % + (condition, sectionType) ) + + args = part[pos+1:-1] + if args != '': + args = args.split(',') + for i in range(len(args)): + args[i] = int(args[i]) + else: + args = [] + + parsed.append(functions[funcName](*args)) + + return parsed diff --git a/tanks/lib/Tank.py b/tanks/lib/Tank.py new file mode 100644 index 0000000..dffa85e --- /dev/null +++ b/tanks/lib/Tank.py @@ -0,0 +1,537 @@ +import math +import random +from PIL import Image +from PIL import ImageDraw + +import GameMath as gm +import Program + +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 + SPEED = 7.0 + # Max acceleration, as a fraction of speed. + ACCEL = 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, + testMode=True): + """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. +@param testMode: When True, extra debugging information is displayed. Namely, + arcs for each sensor are drawn, which turn white when + activated. + """ + + # Keep track of what turn number it is for this tank. + self._turn = 0 + + self.name = name + self._testMode = testMode + + 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 + # The frame of the death animation. + self._deadFrame = 10 + # Death reason + self.deathReason = 'survived' + + 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) + + 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 (percent). +@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] + + @property + def turn(self): + return self._turn + + def setMove(self, left, right): + """Parse the speed values given, and set them for the next move.""" + + self._nextMove = left, right + + 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 addTimer(self, period): + """Add a timer with timeout period 'period'.""" + + if len(self._timers) >= self.SENSOR_LIMIT: + raise ValueError('You can only have 10 timers') + + self._timers.append(None) + self._timerPeriods.append(period) + + def resetTimer(self, key): + """Reset, and start the given timer, but only if it is inactive. + If it is active, raise a ValueError.""" + if self._timer[key] is None: + self._timer[key] = self._timerPeriods[key] + else: + raise ValueError("You can't reset an active timer (#%d)" % key) + + def clearTimer(self, key): + """Clear the timer.""" + self._timer[key] = None + + def checkTimer(self, key): + """Returns True if the timer has expired.""" + return self._timer[key] == 0 + + def _manageTimers(self): + """Decrement each active timer.""" + for i in range(len(self._timers)): + if self._timers[i] is not None and \ + self._timers[i] > 0: + self._timers[i] = self._timers[i] - 1 + + def program(self, text): + """Set the program for this tank.""" + + self._program = Program.Program(text) + self._program.setup(self) + + def execute(self): + """Execute this tanks program.""" + + # Decrement the active timers + self._manageTimers() + self.led = False + + self._program(self) + + self._move(self._nextMove[0], self._nextMove[1]) + self._moveTurret() + if self._fireReady > 0: + self._fireReady = self._fireReady - 1 + + self._turn = self._turn + 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.ACCEL < lSpeed: + lSpeed = self._lastSpeed[0] + self.ACCEL + elif self._lastSpeed[0] > lSpeed and \ + self._lastSpeed[0] - self.ACCEL > lSpeed: + lSpeed = self._lastSpeed[0] - self.ACCEL + + if self._lastSpeed[1] < rSpeed and \ + self._lastSpeed[1] + self.ACCEL < rSpeed: + rSpeed = self._lastSpeed[1] + self.ACCEL + elif self._lastSpeed[1] > rSpeed and \ + self._lastSpeed[1] - self.ACCEL > rSpeed: + rSpeed = self._lastSpeed[1] - self.ACCEL + + self._lastSpeed = lSpeed, rSpeed + + # The simple case + if lSpeed == rSpeed: + fSpeed = self.SPEED*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.SPEED * lSpeed/100.0 + R = self.SPEED * rSpeed/100.0 + friction = .75 * abs(L-R)/(2.0*self.SPEED) + 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 + + # A rectangle starting 2 pixels past the tank front, and two pixels to the + # side. width = 4, height = 18. + tread1 = [(9,-9), (9, -5), (-8, -5), (-8, -9)] + # Same as tread one, except to the right. + tread2 = [(9, 9), (9,5), (-8,5), (-8,9)] + # A circle of radius 7.5 centered on center + body = [(-6,-6), (6,6)] + gun = [(0, -2), (0,2), (12,1), (12,-1)] + ledPoly = [(0, -2), (0,2), (-2,2), (-2,-2)] + hood = [(8, -6), (8, 6), (-7, 6), (-7, -6)] + laser = [(12,2), (FIRE_RANGE, 2), (FIRE_RANGE,-2), (12, -2)] + treadColor = '#777777' + def draw(self, image): + + d = ImageDraw.Draw(image) + + if self.isDead: + if self._deadFrame > 0: + # Draw the explosion instead of the normal art + self._drawExplosion(d) + self._drawLaser(d) + return image + + if self._testMode: + self._drawSensors(d) + + hood = gm.rotatePoly(self.hood, self._angle) + tread1 = gm.rotatePoly(self.tread1, self._angle) + tread2 = gm.rotatePoly(self.tread2, self._angle) + gun = gm.rotatePoly( self.gun, self._angle + self._tAngle ) + led = gm.rotatePoly( self.ledPoly, self._angle + self._tAngle ) + + # The base body rectangle. + for poly in gm.displacePoly(hood, self.pos, self._limits): + d.polygon( poly, fill=self._color ) + + # The treads + for poly in gm.displacePoly(tread1, self.pos, self._limits) + \ + gm.displacePoly(tread2, self.pos, self._limits): + d.polygon( poly, fill=self.treadColor ) + + # The turret circle + for poly in gm.displacePoly(self.body, self.pos, self._limits): + d.ellipse( poly, fill=self._color, outline='black') + + self._drawLaser(d) + + for poly in gm.displacePoly(gun, self.pos, self._limits): + d.polygon( poly, fill='#000000') + + if self.led: + for poly in gm.displacePoly(led, self.pos, self._limits): + d.polygon( poly, fill='#ffffff') + + def _drawLaser(self, drawing): + """Draw the laser line if we shot this turn.""" + if self._fired: + laser = gm.rotatePoly( self.laser, self._angle + self._tAngle ) + for poly in gm.displacePoly(laser, self.pos, self._limits): + drawing.polygon(poly, fill=self._color) + + self._fired = False + + def _drawExplosion(self, drawing): + """Draw the tank exploding.""" + + self._deadFrame = self._deadFrame - 1 + innerBoom = [] + outerBoom = [] + points = 20 + for i in range(points): + if i%2 == 1: + radius = 1.5 * self.RADIUS / 2 + else: + radius = 1.5 * self.RADIUS + innerBoom.append(gm.polar2cart(radius/2, i*2*math.pi/points)) + outerBoom.append(gm.polar2cart(radius, i*2*math.pi/points)) + + for poly in gm.displacePoly(outerBoom, self.pos, self._limits): + drawing.polygon(poly, fill='red') + for poly in gm.displacePoly(innerBoom, self.pos, self._limits): + drawing.polygon(poly, fill='orange') + + def _drawSensors(self, drawing): + """Draw sensor arcs for all of the defined sensors.""" + + for i in range( len( self._sensors) ): + if self._sensorState[i]: + color = '#000000' + else: + color = self._color + + r, angle, width, tAttached = self._sensors[i] + r = int(r) + sensorCircle = [(-r, -r), (r, r)] + angle = angle + self._angle + if tAttached: + angle = angle + self._tAngle + min = int( math.degrees( angle - width/2 ) ) + max = int( math.degrees( angle + width/2 ) ) + for poly in gm.displacePoly(sensorCircle, self.pos, self._limits, + coordSequence=True): + drawing.chord(poly, min, max, outline=color) diff --git a/tanks/lib/__init__.py b/tanks/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tanks/lib/actions.py b/tanks/lib/actions.py new file mode 100644 index 0000000..790afab --- /dev/null +++ b/tanks/lib/actions.py @@ -0,0 +1,123 @@ +"""Define new action Functions here. They should inherit from the +Function.Function class. To make an action usable, add it to the +actions dictionary at the end of this file.""" + +import Function + +class Move(Function.Function): + """move(left tread speed, right tread speed) + Set the speeds for the tanks right and left treads. The speeds should + be a number (percent power) between -100 and 100.""" + + def __init__(self, left, right): + self._checkRange(left, 'left tread speed', min=-100) + self._checkRange(right, 'right tread speed', min=-100) + + self._left = left + self._right = right + + def __call__(self, tank): + tank.setMove(self._left, self._right) + +class TurretCounterClockwise(Function.Function): + """turretccw([percent speed]) + Rotate the turret counter clockwise as a percentage of the max speed.""" + def __init__(self, speed=100): + self._checkRange(speed, 'turret percent speed') + self._speed = speed/100.0 + def __call__(self, tank): + tank.setTurretAngle(tank._tAngle - tank.TURRET_TURN_RATE*self._speed) + +class TurretClockwise(Function.Function): + """turretcw([percent speed]) + Rotate the turret clockwise at a rate preportional to speed.""" + + def __init__(self, speed=100): + self._checkRange(speed, 'turret percent speed') + self._speed = speed/100.0 + def __call__(self, tank): + tank.setTurretAngle(tank._tAngle + tank.TURRET_TURN_RATE*self._speed) + +class TurretSet(Function.Function): + """turretset([angle]) + Set the turret to the given angle, in degrees, relative to the front of + the tank. Angles increase counterclockwise. + The angle can be left out; in that case, this locks the turret + to it's current position.""" + + def __init__(self, angle=None): + # Convert the angle to radians + if angle is not None: + angle = self._convertAngle(angle, 'turret angle') + + self._angle = angle + + def __call__(self, tank): + tank.setTurretAngle(self._angle) + +class Fire(Function.Function): + """fire() + Attempt to fire the tanks laser cannon.""" + + def __call__(self, tank): + tank.setFire() + +class SetToggle(Function.Function): + """settoggle(key, state) +Set toggle 'key' to 'state'. +""" + def __init__(self, key, state): + self._key = key + self._state = state + def __call__(self, tank): + tank.toggles[self._key] = self._state + +class Toggle(Function.Function): + """toggle(key) +Toggle the value of toggle 'key'. +""" + def __init__(self, key): + self._key = key + def __call__(self, tank): + tank.toggles[key] = not tank.toggles[key] + +class LED(Function.Function): + """led(state) +Set the tanks LED to state (true is on, false is off). +The led is a light that appears behind the tanks turret. +It remains on for a single turn.""" + def __init__(self, state=1): + self._state = state + def __call__(self, tank): + tank.led = self._state + +class StartTimer(Function.Function): + """starttimer(#) +Start (and reset) the given timer, but only if it is inactive. +""" + def __init__(self, key): + self._key = key + def __call__(self, tank): + tank.resetTimer(key) + +class ClearTimer(Function.Function): + """cleartimer(#) +Clear the given timer such that it is no longer active (inactive timers +are always False).""" + def __init__(self, key): + self._key = key + def __call__(self, tank): + tank.clearTimer(self._key) + +### When adding names to this dict, make sure they are lower case and alpha +### numeric. +actions = {'move': Move, + 'turretccw': TurretCounterClockwise, + 'turretcw': TurretClockwise, + 'turretset': TurretSet, + 'fire': Fire, + 'settoggle': SetToggle, + 'toggle': Toggle, + 'led': LED, + 'starttimer': StartTimer, + 'cleartimer': ClearTimer} diff --git a/tanks/lib/conditions.py b/tanks/lib/conditions.py new file mode 100644 index 0000000..3a6c0ac --- /dev/null +++ b/tanks/lib/conditions.py @@ -0,0 +1,125 @@ +"""Define new condition functions here. Add it to the conditions dictionary +at the end to make it usable by Program.Program. These should inherit from +Function.Function.""" + +import Function +import random + +class Sense(Function.Function): + """sense(#, [invert]) + Takes a Sensor number as an argument. + Returns True if the given sensor is currently activated, False otherwise. + If the option argument invert is set to true then logic is inverted, + and then sensor returns True when it is NOT activated, and False when + it is. Invert is false by default. + """ + + def __init__(self, sensor, invert=0): + self._sensor = sensor + self._invert = invert + + def __call__(self, tank): + state = tank.getSensorState(self._sensor) + if self._invert: + return not state + else: + return state + +class Toggle(Function.Function): + """toggle(#) +Returns True if the given toggle is set, False otherwise. """ + def __init__(self, toggle): + self._toggle = toggle + def __call__(self, tank): + return tank.toggles[toggle] + +class TimerCheck(Function.Function): + """timer(#, [invert]) +Checks the state of timer # 'key'. Returns True if time has run out. +If invert is given (and true), then True is returned if the timer has +yet to expire. +""" + def __init__(self, key, invert=0): + self._key = key + self._invert = invert + def __call__(self, tank): + state = tank.checkTimer(self._key) + if invert: + return not state + else: + return state + +class Random(Function.Function): + """random(n,m) + Takes two arguments, n and m. Generates a random number between 1 + and m (inclusive) each time it's checked. If the random number is less + than or equal + to n, then the condition returns True. Returns False otherwise.""" + + def __init__(self, n, m): + self._n = n + self._m = m + + def __call__(self, tank): + if random.randint(1,self._m) <= self._n: + return True + else: + return False + +class Sin(Function.Function): + """sin(T) + A sin wave of period T (in turns). Returns True when the wave is positive. + A wave with period 1 or 2 is always False (it's 0 each turn), only + at periods of 3 or more does this become useful.""" + + def __init__(self, T): + self._T = T + + def __call__(self, tank): + turn = tank.turn + factor = math.pi/self._T + if math.sin(turn * factor) > 0: + return True + else: + return False + +class Cos(Function.Function): + """cos(T) + A cos wave with period T (in turns). Returns True when the wave is + positive. A wave of period 1 is always True. Period 2 is True every + other turn, etc.""" + + def __init__(self, T): + self._T = T + + def __call__(self, tank): + + turn = tank.turn + factor = math.pi/self._T + if math.cos(turn * factor) > 0: + return True + else: + return False + +class FireReady(Function.Function): + """fireready() + True when the tank can fire.""" + def __call__(self, tank): + return tank.fireReady() + +class FireNotReady(Function.Function): + """firenotready() + True when the tank can not fire.""" + def __call__(self, tank): + return not tank.fireReady() + +### When adding names to this dict, make sure they are lower case and alpha +### numeric. +conditions = {'sense': Sense, + 'random': Random, + 'toggle': Toggle, + 'sin': Sin, + 'cos': Cos, + 'fireready': FireReady, + 'firenotready': FireNotReady, + 'timer': TimerCheck } diff --git a/tanks/lib/docs.py b/tanks/lib/docs.py new file mode 100644 index 0000000..1724d52 --- /dev/null +++ b/tanks/lib/docs.py @@ -0,0 +1,27 @@ +import xml.sax.saxutils + +def mkDocTable(objects): + objects.sort(lambda o1, o2: cmp(o1.__doc__, o2.__doc__)) + + for object in objects: + print '' + 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) + print '
' + diff --git a/tanks/lib/setup.py b/tanks/lib/setup.py new file mode 100644 index 0000000..29bc861 --- /dev/null +++ b/tanks/lib/setup.py @@ -0,0 +1,72 @@ +"""Each of these classes provides a function for configuring a tank. +They should inherit from Function.Function. +To make one available to the tank programmer, add it to the dictionary at +the end of this file.""" + +import Function + +class AddSensor(Function.Function): + """addsensor(range, angle, width, [turretAttached]) +Add a new sensor to the tank. Sensors are an arc (pie slice) centered on +the tank that detect other tanks within their sweep. +A sensor is 'on' if another tank is within this arc. +Sensors are numbered, starting at 0, in the order they are added. +

+range - The range of the sensor, as a percent of the tanks max range. +angle - The angle of the center of the sensor, in degrees. +width - The width of the sensor, in percent (100 is a full circle). +turretAttached - Normally, the angle is relative to the front of the +tank. When this is set, the angle is relative to the current turret +direction. +

+Sensors are drawn for each tank, but not in the way you might expect. +Instead of drawing a pie slice (the actual shap of the sensor), an arc with +the end points connected by a line is drawn. Sensors with 0 width don't show +up, but still work. +""" + + def __init__(self, range, angle, width, turretAttached=False): + + self._checkRange(range, 'sensor range') + + self._range = range / 100.0 + self._width = self._convertAngle(width, 'sensor width') + self._angle = self._convertAngle(angle, 'sensor angle') + self._turretAttached = turretAttached + + def __call__(self, tank): + tank.addSensor(self._range, self._angle, self._width, + self._turretAttached) + +class AddToggle(Function.Function): + """addtoggle([state]) +Add a toggle to the tank. The state of the toggle defaults to 0 (False). +These essentially act as a single bit of memory. +Use the toggle() condition to check its state and the settoggle, cleartoggle, +and toggle actions to change the state. Toggles are named numerically, +starting at 0. +""" + def __init__(self, state=0): + self._state = state + + def __call__(self, tank): + if len(tank.toggles) >= tank.SENSOR_LIMIT: + raise ValueError('You can not have more than 10 toggles.') + + tank.toggles.append[self._state] + +class AddTimer(Function.Function): + """addtimer(timeout) +Add a new timer (they're numbered in the order added, starting from 0), +with the given timeout. The timeout is in number of turns. The timer +is created in inactive mode. You'll need to do a starttimer() action +to reset and start the timer. When the timer expires, the timer() +condition will begin to return True.""" + def __init__(self, timeout): + self._timeout = timeout + def __call__(self, tank): + tank.addTimer(timeout) + +setup = {'addsensor': AddSensor, + 'addtoggle': AddToggle, + 'addtimer': AddTimer} diff --git a/tanks/log.run b/tanks/log.run new file mode 100755 index 0000000..e69de29 diff --git a/tanks/run b/tanks/run new file mode 100755 index 0000000..e69de29 diff --git a/tanks/www/ctf.css b/tanks/www/ctf.css new file mode 100644 index 0000000..ca7c9f6 --- /dev/null +++ b/tanks/www/ctf.css @@ -0,0 +1,99 @@ +/**** document ****/ + +html { + background: #222 url(grunge.png) repeat-x; +} + +body { + font-family: sans-serif; + color: #eee; + margin: 50px 0 0 100px; + padding: 10px; + max-width: 700px; +} + +/**** heading ****/ + +h1:first-child { + text-transform: lowercase; + font-size: 1.6em; +/* background-color: #222; */ +/* opacity: 0.9; */ + padding: 3px; + color: #2a2; + margin: 0 0 1em 70px; +} + +h1:first-child:before { + color: #fff; + letter-spacing: -0.1em; + content: "Capture The Flag: "; +} + +/**** body ****/ + +a img { + border: 0px; +} + +a { + text-decoration: none; + color: #2a2; + font-weight: bold; +} + +a:hover { + color: #fff; + background: #2a2; + font-weight: bold; +} + + +h1, h2, h3 { + color: #999; + letter-spacing: -0.05em; +} + +code, pre, .readme { + color: #fff; + background-color: #555; + margin: 1em; +} + +th, td { + vertical-align: top; +} + +.scoreboard td { + height: 400px; +} + +p { + line-height: 1.4em; + margin-bottom: 20px; + color: #f4f4f4; +} + +dt { + white-space: pre; +} + +dt div.tab { + background-color: #333; + display: inline-block; + padding: 5px; + border: 3px solid green; + border-bottom: none; + font-weight: bold; +} + +dd { + border: 3px solid green; + margin: 0px; + padding: 5px; + background-color: #282828; +} + +fieldset * { + margin: 3px; +} diff --git a/tanks/www/docs.cgi b/tanks/www/docs.cgi new file mode 100755 index 0000000..26eb752 --- /dev/null +++ b/tanks/www/docs.cgi @@ -0,0 +1,37 @@ +#!/usr/bin/python + +print """Content-Type: text/html\n\n""" +print """\n\n""" +import cgitb; cgitb.enable() +import os +import sys + +try: + from Tanks import Program, setup, conditions, actions, docs +except: + path = os.getcwd().split('/') + path.pop() + path.append('lib') + sys.path.append(os.path.join('/', *path)) + import Program, setup, conditions, actions, docs + +print open('head.html').read() % "Documentation" +print '' +print '

Pflanzarr Documentation

' +print 'Submit | Results | Documentation' +print Program.__doc__ + +print '

Setup Actions:

' +print 'These functions can be used to setup your tank. Abuse of these functions has, in the past, resulted in mine sweeping duty. With a broom.' +print "

" +docs.mkDocTable(setup.setup.values()) + +print '

Conditions:

' +print 'These functions are used to check the state of reality. If reality stops being real, refer to chapter 5 in your girl scout handbook.

' +docs.mkDocTable(conditions.conditions.values()) + +print '

Actions:

' +print 'These actions are not for cowards. Remember, if actions contradict, your tank will simply do the last thing it was told in a turn. If ordered to hop on a plane to hell it will gladly do so. If order to make tea shortly afterwards, it will serve it politely and with cookies instead.

' +docs.mkDocTable(actions.actions.values()) + +print '' diff --git a/tanks/www/grunge.png b/tanks/www/grunge.png new file mode 100644 index 0000000..2b98730 Binary files /dev/null and b/tanks/www/grunge.png differ diff --git a/tanks/www/head.html b/tanks/www/head.html new file mode 100644 index 0000000..b98713e --- /dev/null +++ b/tanks/www/head.html @@ -0,0 +1,5 @@ + + + ' + %s" + diff --git a/tanks/www/results.cgi b/tanks/www/results.cgi new file mode 100755 index 0000000..4901ac4 --- /dev/null +++ b/tanks/www/results.cgi @@ -0,0 +1,47 @@ +#!/usr/bin/python + +import cgitb; cgitb.enable() +import os + +print """Content-Type: text/html\n\n""" +print """\n\n""" +head = open('head.html').read() % "Pflanzarr Results" +print head +print "

Results

" +print 'Submit | Results | Documentation' + +try: + winner = open(os.path.join('data', 'winner')).read() +except: + winner = "No winner yet." + +print "

Last Winner: ", winner, '

' +print "

Results so far:

" + +try: + games = os.listdir(os.path.join('data', 'results')) +except: + print '

The data directory does not exist.' + games = [] + +if not games: + print "

No games have occurred yet." +gameNums = [] +for game in games: + try: + num = int(game) + path = os.path.join( 'data', "results", game, 'results.html') + if os.path.exists( path ): + gameNums.append( int(num) ) + else: + continue + + except: + continue + +gameNums.sort(reverse=True) + +for num in gameNums: + print '

%d - ' % num, + print 'v' % num, + print 'r' % num diff --git a/tanks/www/style.css b/tanks/www/style.css new file mode 100644 index 0000000..ac787f0 --- /dev/null +++ b/tanks/www/style.css @@ -0,0 +1,16 @@ +body { background-color : #000000; + color : #E0E0E0; + } + +table { + border : 2px solid #00EE00; + border-collapse : collapse; + margin : 3px; + } + +table td { border : 1px solid #00BB00; + padding-left: 3px; + padding-right: 3px; + text-align: left; + vertical-align: top; + } diff --git a/tanks/www/submit.cgi b/tanks/www/submit.cgi new file mode 100755 index 0000000..01ad8c8 --- /dev/null +++ b/tanks/www/submit.cgi @@ -0,0 +1,53 @@ +#!/usr/bin/python + +import cgi +import cgitb; cgitb.enable() +import os + +try: + from urllib.parse import quote +except: + from urllib import quote + +try: + from ctf import teams +except: + import sys + path = '/home/pflarr/repos/gctf/' + sys.path.append(path) + from ctf import teams + +print """Content-Type: text/html\n\n""" +print """\n\n""" +head = open('head.html').read() % "Submission Results" +print head +print "

Results

" +print 'Submit | Results | Documentation' + +def done(): + print '' + sys.exit(0) + +fields = cgi.FieldStorage() +team = fields.getfirst('team', '').strip() +passwd = fields.getfirst('passwd', '').strip() +code = fields.getfirst('code', '') +if not team: + print '

No team specified'; done() +elif not passwd: + print '

No password given'; done() +elif not code: + print '

No program given.'; done() + +if team not in teams.teams: + print '

Team is not registered.'; done() + +if passwd != teams.teams[team][0]: + print '

Invalid password.'; done() + +path = os.path.join('data/ai/players', encode(team) ) +file = open(path, 'w') +file.write(code) +file.close() + +done() diff --git a/tanks/www/submit.html b/tanks/www/submit.html new file mode 100644 index 0000000..86cc13c --- /dev/null +++ b/tanks/www/submit.html @@ -0,0 +1,21 @@ + + + ' + Program Submission" + + + +

Program Submission

+

Submit | Results | Documentation + +

+
+ Your program: + Team:
+ Password:
+
+ +
+
+ +