Various work on Tanks.

This commit is contained in:
Paul S. Ferrell 2009-10-06 11:50:21 -06:00
parent e95fabccb7
commit 41f077192d
31 changed files with 2244 additions and 1 deletions

View File

@ -3,7 +3,11 @@
import fcntl
import time
import os
# 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')

5
tanks/AI/easy/berzerker Normal file
View File

@ -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();

View File

@ -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);

57
tanks/AI/hard/crashmaster Normal file
View File

@ -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);

22
tanks/AI/hard/foobar Normal file
View File

@ -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();

31
tanks/AI/hard/pflarr Normal file
View File

@ -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();

View File

@ -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)

View File

@ -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();

18
tanks/AI/medium/sweeper Normal file
View File

@ -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();

34
tanks/Makefile Normal file
View File

@ -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

BIN
tanks/lib/.actions.py.swp Normal file

Binary file not shown.

47
tanks/lib/Function.py Normal file
View File

@ -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

206
tanks/lib/GameMath.py Normal file
View File

@ -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

400
tanks/lib/Pflanzarr.py Normal file
View File

@ -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 = ['<html><body>',
'<table><tr><th>Team<th>Kills<th>Cause of Death']
for tank in tanks:
if tank is winner:
rowStyle = 'style="color:red;"'
else:
rowStyle = ''
html.append('<tr %s><td>%s<td>%d<td>%s' %
(rowStyle,
xml.sax.saxutils.escape(tank.name),
len(kills[tank]),
xml.sax.saxutils.escape(tank.deathReason)))
html.append('</table><body></html>')
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"

232
tanks/lib/Program.py Normal file
View File

@ -0,0 +1,232 @@
"""<H2>Introduction</H2>
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.
<H2>Programming Your Tank</H2>
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.
<P>
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:
<pre class="docs">>addsensor(80, 90, 33);
>addsensor(50, 0, 10, 1);
>addtimer(3);</pre>
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:
<pre class="docs">
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);</pre>
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

537
tanks/lib/Tank.py Normal file
View File

@ -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 '<tank: %s, (%d, %d)>' % (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)

0
tanks/lib/__init__.py Normal file
View File

123
tanks/lib/actions.py Normal file
View File

@ -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}

125
tanks/lib/conditions.py Normal file
View File

@ -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 }

27
tanks/lib/docs.py Normal file
View File

@ -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 '<table class="docs">'
if object.__doc__ is None:
print '<tr><th>%s<tr><td colspan=2>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('.', '.<BR>')
body.append(line)
body = '\n'.join(body)
print '<DL><DT><DIV class="tab">%s</DIV></DT><DD>%s</DD></DL>' % (head, body)
#print '<tr><th>%s<th>Intentionally blank<th><tr><td colspan=3>%s' % (head, body)
print '</table>'

72
tanks/lib/setup.py Normal file
View File

@ -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.
<p>
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.
<p>
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}

0
tanks/log.run Executable file
View File

0
tanks/run Executable file
View File

99
tanks/www/ctf.css Normal file
View File

@ -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;
}

37
tanks/www/docs.cgi Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/python
print """Content-Type: text/html\n\n"""
print """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">\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 '<BODY>'
print '<H1>Pflanzarr Documentation</H1>'
print '<a href="submit.html">Submit</a> | <a href="results.cgi">Results</a> | <a href="docs.cgi">Documentation</a>'
print Program.__doc__
print '<H3>Setup Actions:</H3>'
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 "<P>"
docs.mkDocTable(setup.setup.values())
print '<H3>Conditions:</H3>'
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.<P>'
docs.mkDocTable(conditions.conditions.values())
print '<H3>Actions:</H3>'
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.<P>'
docs.mkDocTable(actions.actions.values())
print '</body></html>'

BIN
tanks/www/grunge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

5
tanks/www/head.html Normal file
View File

@ -0,0 +1,5 @@
<html>
<head>
<link href="ctf.css" rel="stylesheet" type="text/css">'
<title>%s</title>"
</head>

47
tanks/www/results.cgi Executable file
View File

@ -0,0 +1,47 @@
#!/usr/bin/python
import cgitb; cgitb.enable()
import os
print """Content-Type: text/html\n\n"""
print """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">\n\n"""
head = open('head.html').read() % "Pflanzarr Results"
print head
print "<H1>Results</H1>"
print '<a href="submit.html">Submit</a> | <a href="results.cgi">Results</a> | <a href="docs.cgi">Documentation</a>'
try:
winner = open(os.path.join('data', 'winner')).read()
except:
winner = "No winner yet."
print "<H3>Last Winner: ", winner, '<H3>'
print "<H2>Results so far:</H2>"
try:
games = os.listdir(os.path.join('data', 'results'))
except:
print '<p>The data directory does not exist.'
games = []
if not games:
print "<p>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 '<p>%d - ' % num,
print '<a href="data/results/%d/game.avi">v</a>' % num,
print '<a href="data/results/%d/results.html">r</a>' % num

16
tanks/www/style.css Normal file
View File

@ -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;
}

53
tanks/www/submit.cgi Executable file
View File

@ -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 """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN">\n\n"""
head = open('head.html').read() % "Submission Results"
print head
print "<H1>Results</H1>"
print '<a href="submit.html">Submit</a> | <a href="results.cgi">Results</a> | <a href="docs.cgi">Documentation</a>'
def done():
print '</body></html>'
sys.exit(0)
fields = cgi.FieldStorage()
team = fields.getfirst('team', '').strip()
passwd = fields.getfirst('passwd', '').strip()
code = fields.getfirst('code', '')
if not team:
print '<p>No team specified'; done()
elif not passwd:
print '<p>No password given'; done()
elif not code:
print '<p>No program given.'; done()
if team not in teams.teams:
print '<p>Team is not registered.'; done()
if passwd != teams.teams[team][0]:
print '<p>Invalid password.'; done()
path = os.path.join('data/ai/players', encode(team) )
file = open(path, 'w')
file.write(code)
file.close()
done()

21
tanks/www/submit.html Normal file
View File

@ -0,0 +1,21 @@
<html>
<head>
<link href="ctf.css" rel="stylesheet" type="text/css">'
<title>Program Submission</title>"
</head>
<body>
<H1>Program Submission</H1>
<p><a href="submit.html">Submit</a> | <a href="results.cgi">Results</a> | <a href="docs.cgi">Documentation</a>
<form action="submit.cgi" method="post">
<fieldset>
<legend>Your program:</legend>
Team: <input type="text" name="team"><BR>
Password: <input type="text" name="passwd"><BR>
<textarea cols="80" rows="30" name="code"></textarea><BR>
<button type="submit">Submit</button>
</fieldset>
</form>
</body>
</html>