mirror of https://github.com/dirtbags/moth.git
398 lines
13 KiB
Python
398 lines
13 KiB
Python
import fcntl
|
|
import math
|
|
import os
|
|
import random
|
|
import cgi
|
|
from sets import Set as set
|
|
from ctf import teams, html, paths
|
|
from cStringIO import StringIO
|
|
|
|
from urllib import unquote, quote
|
|
|
|
import Tank
|
|
|
|
class NotEnoughPlayers(Exception):
|
|
pass
|
|
|
|
class Pflanzarr:
|
|
SPACING = 150
|
|
|
|
def __init__(self, dir):
|
|
"""Initialize a new game of Pflanzarr.
|
|
@param dir: The data directory."""
|
|
|
|
# Setup the game environment
|
|
self._setupDirectories(dir)
|
|
|
|
# Figure out what game number this is.
|
|
self.gameNum = self._getGameNum()
|
|
self.gameFilename = os.path.join(self._resultsDir, '%04d.html' % self.gameNum)
|
|
|
|
tmpPlayers = os.listdir(self._playerDir)
|
|
players = []
|
|
AIs = {}
|
|
for fn in tmpPlayers:
|
|
p = unquote(fn)
|
|
print (p, fn)
|
|
if (not (p.startswith('.')
|
|
or p.endswith('#')
|
|
or p.endswith('~'))
|
|
and teams.exists(p)):
|
|
players.append(p)
|
|
AIs[p] = open(os.path.join(self._playerDir, fn)).read()
|
|
defaultAIs = self._getDefaultAIs(dir)
|
|
|
|
if len(players) < 1:
|
|
raise NotEnoughPlayers()
|
|
|
|
# The one is added to ensure that there is at least one house
|
|
# bot.
|
|
cols = int(math.ceil(math.sqrt(len(players) + 1)))
|
|
cols = max(cols, 2)
|
|
|
|
rows = int(math.ceil(len(players)/float(cols)))
|
|
rows = max(rows, 2)
|
|
|
|
self._board = (cols*self.SPACING, rows*self.SPACING)
|
|
|
|
while len(players) < cols*rows:
|
|
players.append(None)
|
|
|
|
self._tanks = []
|
|
for i in range(cols):
|
|
for j in range(rows):
|
|
startX = i*self.SPACING + self.SPACING/2
|
|
startY = j*self.SPACING + self.SPACING/2
|
|
player = random.choice(players)
|
|
players.remove(player)
|
|
color = '#' + teams.color(player)
|
|
tank = Tank.Tank( player, (startX, startY), color,
|
|
self._board, testMode=True)
|
|
if player == None:
|
|
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):
|
|
kills = {}
|
|
for tank in self._tanks:
|
|
kills[tank] = set()
|
|
|
|
# Open HTML output
|
|
hdr = StringIO()
|
|
hdr.write('<script type="application/javascript" src="../tanks.js"></script>\n'
|
|
'<script type="application/javascript">\n')
|
|
hdr.write('turns = [%d, %d,[\n' % self._board)
|
|
|
|
# Describe tanks
|
|
for tank in self._tanks:
|
|
tank.describe(hdr)
|
|
hdr.write('],\n')
|
|
hdr.write('[\n')
|
|
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
|
|
|
|
near = self._getNear()
|
|
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' % cgi.escape(tank.name or teams.house))
|
|
|
|
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')
|
|
|
|
hdr.write(' [\n')
|
|
for tank in self._tanks:
|
|
tank.draw(hdr)
|
|
hdr.write(' ],\n')
|
|
|
|
# Have the live tanks do their turns
|
|
for tank in self._tanksByX:
|
|
tank.execute()
|
|
|
|
turn = turn + 1
|
|
|
|
# Removes tanks from their own kill lists.
|
|
for tank in kills:
|
|
if tank in kills[tank]:
|
|
kills[tank].remove(tank)
|
|
|
|
for tank in self._tanks:
|
|
self._outputErrors(tank)
|
|
|
|
hdr.write(']];\n')
|
|
hdr.write('</script>\n')
|
|
|
|
# Decide on the winner
|
|
winner = self._chooseWinner(kills)
|
|
self.winner = winner.name
|
|
|
|
# Now generate HTML body
|
|
body = StringIO()
|
|
body.write(' <canvas id="battlefield" width="%d" height="%d">\n' % self._board)
|
|
body.write(' Sorry, you need an HTML5-capable browser to see this.\n'
|
|
' </canvas>\n'
|
|
' <p>\n')
|
|
if self.gameNum > 0:
|
|
body.write(' <a href="%04d.html">← Prev</a> |' %
|
|
(self.gameNum - 1))
|
|
body.write(' <a href="%04d.html">Next →</a> |' %
|
|
(self.gameNum + 1))
|
|
body.write(' <span id="fps">0</span> fps\n'
|
|
' </p>\n'
|
|
' <table class="results">\n'
|
|
' <tr>\n'
|
|
' <th>Team</th>\n'
|
|
' <th>Kills</th>\n'
|
|
' <th>Cause of Death</th>\n'
|
|
' </tr>\n')
|
|
|
|
tanks = self._tanks[:]
|
|
tanks.remove(winner)
|
|
tanks[0:0] = [winner]
|
|
for tank in tanks:
|
|
if tank is winner:
|
|
rowStyle = ('style="font-weight: bold; '
|
|
'color: #000; '
|
|
'background-color: %s;"' % tank.color)
|
|
else:
|
|
rowStyle = 'style="background-color:%s; color: #000;"' % tank.color
|
|
if tank.name:
|
|
name = cgi.escape(tank.name)
|
|
else:
|
|
name = teams.house
|
|
body.write('<tr %s><td>%s</td><td>%d</td><td>%s</td></tr>' %
|
|
(rowStyle,
|
|
name,
|
|
len(kills[tank]),
|
|
cgi.escape(tank.deathReason)))
|
|
body.write(' </table>\n')
|
|
|
|
links='''<h3>Tanks</h3>
|
|
<ul>
|
|
<li><a href="docs.html">Docs</a></li>
|
|
<li><a href="results.cgi">Results</a></li>
|
|
<li><a href="submit.html">Submit</a></li>
|
|
<li><a href="errors.cgi">My Errors</a></li>
|
|
</ul>
|
|
'''
|
|
|
|
# Write everything out
|
|
html.write(self.gameFilename,
|
|
'Tanks round %d' % self.gameNum,
|
|
body.getvalue(),
|
|
hdr=hdr.getvalue(),
|
|
links=links,
|
|
onload='start(turns);')
|
|
|
|
|
|
|
|
def _killTanks(self, tanks, reason):
|
|
for tank in tanks:
|
|
if tank in self._tanksByX:
|
|
self._tanksByX.remove(tank)
|
|
if tank in self._tanksByY:
|
|
self._tanksByY.remove(tank)
|
|
|
|
tank.die(reason)
|
|
|
|
self._deadTanks = self._deadTanks.union(tanks)
|
|
|
|
def _chooseWinner(self, kills):
|
|
"""Choose a winner. In case of a tie, live tanks prevail, in case
|
|
of further ties, a winner is chosen at random. This outputs the winner
|
|
to the winners file and outputs a results table html file."""
|
|
tanks = list(self._tanks)
|
|
def winSort(t1, t2):
|
|
"""Sort by # of kill first, then by life status."""
|
|
result = cmp(len(kills[t1]), len(kills[t2]))
|
|
if result != 0:
|
|
return result
|
|
|
|
if t1.isDead and not t2.isDead:
|
|
return -1
|
|
elif not t1.isDead and t2.isDead:
|
|
return 1
|
|
else:
|
|
return 0
|
|
tanks.sort(winSort)
|
|
tanks.reverse()
|
|
|
|
# Get the list of potential winners
|
|
winners = []
|
|
for i in range(len(tanks)):
|
|
if len( kills[tanks[0]] ) == len( kills[tanks[i]] ) and \
|
|
tanks[0].isDead == tanks[i].isDead:
|
|
winners.append(tanks[i])
|
|
else:
|
|
break
|
|
winner = random.choice(winners)
|
|
return winner
|
|
|
|
|
|
def _outputErrors(self, tank):
|
|
"""Output errors for each team."""
|
|
if tank.name == None:
|
|
return
|
|
|
|
if tank._program.errors:
|
|
print tank.name, 'has errors'
|
|
|
|
|
|
fileName = os.path.join(self._errorDir, quote(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._playerDir = os.path.join(dir, 'ai', 'players')
|
|
|
|
def _getDefaultAIs(self, basedir):
|
|
"""Load all the house bot AIs."""
|
|
defaultAIs = []
|
|
|
|
path = os.path.join(basedir, 'ai', 'house')
|
|
files = os.listdir(path)
|
|
for fn in files:
|
|
if fn.startswith('.'):
|
|
continue
|
|
|
|
fn = os.path.join(path, fn)
|
|
file = open(fn)
|
|
defaultAIs.append(file.read())
|
|
|
|
return defaultAIs
|
|
|
|
def _getGameNum(self):
|
|
"""Figure out what game number this is from the past games played."""
|
|
|
|
games = os.listdir(self._resultsDir)
|
|
games.sort()
|
|
if games:
|
|
fn = games[-1]
|
|
s, _ = os.path.splitext(fn)
|
|
return int(s) + 1
|
|
else:
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
import sys, traceback
|
|
try:
|
|
p = Pflanzarr(sys.argv[1])
|
|
p.run(int(sys.argv[3]))
|
|
except:
|
|
traceback.print_exc()
|
|
print "Usage: Pflanzarr.py dataDirectory #turns"
|
|
|
|
|