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 = ['
', + 'Team | Kills | Cause 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(' |
+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 '
%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 | Intentionally blank | |
%s' % (head, body) + print ' |
+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 '" +docs.mkDocTable(setup.setup.values()) + +print '
' +docs.mkDocTable(conditions.conditions.values()) + +print '
' +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 @@ + +
+ ' +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 "
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 @@ + +
+ ' +Submit | Results | Documentation + +
+ +