Put this project under version control, finally.
Scripts for rlgallery.org, using a PostgreSQL backend. The recorder system is in py/, CGI scripts are in web/.
This commit is contained in:
commit
ddf0ec25b0
6 changed files with 937 additions and 0 deletions
20
py/cleandb.py
Normal file
20
py/cleandb.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# cleandb.py: empty the database in an orderly fashion
|
||||||
|
|
||||||
|
import rlgalldb as rlgall
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
dbconn = psycopg2.connect("dbname=rlg")
|
||||||
|
dbcur = dbconn.cursor()
|
||||||
|
|
||||||
|
dbcur.execute("UPDATE games SET offbytes = %s", [0])
|
||||||
|
|
||||||
|
for game in rlgall.gamelist:
|
||||||
|
dbcur.execute("DELETE FROM " + game.uname + ";")
|
||||||
|
|
||||||
|
dbcur.execute("DELETE FROM players;")
|
||||||
|
dbconn.commit()
|
||||||
|
|
||||||
|
dbcur.close()
|
||||||
|
dbconn.close()
|
||||||
|
exit()
|
||||||
46
py/recorder.py
Executable file
46
py/recorder.py
Executable file
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import calendar
|
||||||
|
import psycopg2
|
||||||
|
import rlgalldb as rlgall
|
||||||
|
|
||||||
|
# Contains a dir for everyone who registered
|
||||||
|
everydir = "/var/dgl/dgldir/ttyrec/"
|
||||||
|
# Contains a page for everyone we know about
|
||||||
|
#knowndir = rlgall.dbdir + "players/"
|
||||||
|
|
||||||
|
# Contact the database
|
||||||
|
conn = psycopg2.connect("dbname=rlg")
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# newnames is the list of newly registered players who are not yet in the
|
||||||
|
# database. updatenames is the set of players whose pages need updating.
|
||||||
|
cur.execute("SELECT pname FROM players;")
|
||||||
|
playersInDB = [ row[0] for row in cur.fetchall() ]
|
||||||
|
playersAll = os.listdir(everydir)
|
||||||
|
newnames = [ name for name in playersAll if name not in playersInDB ]
|
||||||
|
updatenames = set(newnames)
|
||||||
|
|
||||||
|
# Add the new names to the database
|
||||||
|
for newplayer in newnames:
|
||||||
|
cur.execute("INSERT INTO players VALUES (%s);", [newplayer])
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Update the database for each game.
|
||||||
|
for game in rlgall.gamelist:
|
||||||
|
updatenames.update(game.loadnew())
|
||||||
|
|
||||||
|
# All the databases have been updated. Now make the pages.
|
||||||
|
|
||||||
|
# Currently the high scores for all the games are on the same page. If
|
||||||
|
# they split up, this will have to change to a Game method.
|
||||||
|
rlgall.highpage()
|
||||||
|
|
||||||
|
for name in updatenames:
|
||||||
|
rlgall.playerpage(name)
|
||||||
|
|
||||||
|
exit()
|
||||||
425
py/rlgalldb.py
Normal file
425
py/rlgalldb.py
Normal file
|
|
@ -0,0 +1,425 @@
|
||||||
|
# rlgalldb.py
|
||||||
|
# Module for the Roguelike Gallery, using a postgres database
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import calendar
|
||||||
|
import re
|
||||||
|
import psycopg2
|
||||||
|
from datetime import datetime, tzinfo, timedelta
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
logdir = "/var/dgl/var/games/roguelike/"
|
||||||
|
webdir = "/var/www/lighttpd/scoring/"
|
||||||
|
ppagename = webdir + "players/{0}.html"
|
||||||
|
hpagename = webdir + "highscores.html"
|
||||||
|
|
||||||
|
# HTML fragments for templating
|
||||||
|
phead = """<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||||
|
<html><head>
|
||||||
|
<title>{0}</title>
|
||||||
|
<link rel="stylesheet" href="/scoring/scores.css" type="text/css">
|
||||||
|
</head>
|
||||||
|
"""
|
||||||
|
|
||||||
|
ptop = """<body>
|
||||||
|
<h1>Yendor Guild</h1>
|
||||||
|
"""
|
||||||
|
|
||||||
|
navtop = '<div class="nav"><a href="/">rlgallery.org</a> -> {0}</div>\n'
|
||||||
|
navscore = '<div class="nav"><a href="/">rlgallery.org</a> -> \
|
||||||
|
<a href="/scoring/">Scores</a> -> {0}</div>\n'
|
||||||
|
navplayer = '<div class="nav"><a href="/">rlgallery.org</a> -> \
|
||||||
|
<a href="/scoring/">Scores</a> -> <a href="/scoring/players/">Players</a> \
|
||||||
|
-> {0}</div>'
|
||||||
|
|
||||||
|
pti = '<h2>{0}</h2>\n'
|
||||||
|
|
||||||
|
secthead = '<h3>{0}</h3>\n'
|
||||||
|
tblhead = '<div class="stable">\n'
|
||||||
|
rowstart = '<div class="sentry">\n'
|
||||||
|
rowend = '</div>\n'
|
||||||
|
cell = ' <span class="sdata">{0}</span>\n'
|
||||||
|
hcell = ' <span class="shdata">{0}</span>\n'
|
||||||
|
tblend = '</div>\n'
|
||||||
|
pend = "</body></html>\n"
|
||||||
|
|
||||||
|
# This would be more useful if we had to do translation
|
||||||
|
headerbook = {"etime":"End time", "score":"Score", "name":"Name", "xl":"XL",
|
||||||
|
"fate":"Fate", "rank":"Rank", "game":"Game"}
|
||||||
|
# Queries for the games table
|
||||||
|
offselstr = "SELECT offbytes FROM games WHERE gname = %s;"
|
||||||
|
newoffstr = "UPDATE games SET offbytes = %s WHERE gname = %s;"
|
||||||
|
|
||||||
|
# A representation of the UTC time zone. They say Py3k can better handle
|
||||||
|
# this madness.
|
||||||
|
class UTC(tzinfo):
|
||||||
|
def utcoffset(self, dt):
|
||||||
|
return timedelta(0)
|
||||||
|
def dst(self, dt):
|
||||||
|
return timedelta(0)
|
||||||
|
def tzname(self, dt):
|
||||||
|
return "UTC"
|
||||||
|
utc = UTC()
|
||||||
|
|
||||||
|
def getconn():
|
||||||
|
"Returns a database connection, or None if the connection fails."
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect("dbname=rlg")
|
||||||
|
except psycopg2.OperationalError:
|
||||||
|
return None
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def recnameToInt(filename):
|
||||||
|
recre = r"(\d{4})-(\d{2})-(\d{2})\.(\d{2}):(\d{2}):(\d{2})\.ttyrec$"
|
||||||
|
match = re.match(recre, filename)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
return calendar.timegm([int(val) for val in match.groups()])
|
||||||
|
|
||||||
|
def recnameToTS(filename):
|
||||||
|
pattern = "%Y-%m-%d.%H:%M:%S.ttyrec"
|
||||||
|
try:
|
||||||
|
dt = datetime.strptime(filename, pattern)
|
||||||
|
dt.replace(tzinfo=utc)
|
||||||
|
return dt
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def ttyreclink(text, name, game, gtime):
|
||||||
|
"Returns a link to the ttyrec archivist"
|
||||||
|
lstr = '<a href="/archive.cgi?name={0};game={1};time={2}">{3}</a>'
|
||||||
|
return lstr.format(name, game, gtime, text)
|
||||||
|
|
||||||
|
def playerlink(name):
|
||||||
|
"Returns a link to a player's page"
|
||||||
|
lstr = '<a href="/scoring/players/' + name + '.html">' + name + '</a>'
|
||||||
|
return lstr
|
||||||
|
|
||||||
|
def linktoArchive(entry):
|
||||||
|
"Takes an entry dict and returns a link to the ttyrec archivist."
|
||||||
|
lstr = '<a href="/archive.cgi?name={0};game={1};time={2}">{3}</a>'
|
||||||
|
#linktext = time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(entry["etime"]))
|
||||||
|
linktext = entry["etstamp"].strftime("%Y/%m/%d %H:%M:%S")
|
||||||
|
return lstr.format(entry["name"], entry["game"].uname, entry["etime"],
|
||||||
|
linktext)
|
||||||
|
|
||||||
|
def maketablerow(cells, isheader=None):
|
||||||
|
"Takes a list of strings and returns a HTML table row with each string \
|
||||||
|
in its own cell. isheader will make them header cells, obviously."
|
||||||
|
if isheader:
|
||||||
|
celler = hcell
|
||||||
|
else:
|
||||||
|
celler = cell
|
||||||
|
rowstr = rowstart
|
||||||
|
for entry in cells:
|
||||||
|
rowstr = rowstr + celler.format(entry)
|
||||||
|
rowstr = rowstr + rowend
|
||||||
|
return rowstr
|
||||||
|
|
||||||
|
def printTable(entries, fields, of):
|
||||||
|
"Takes a list of entry dicts and a list of field strings and writes a \
|
||||||
|
HTML table to of."
|
||||||
|
of.write(tblhead)
|
||||||
|
clist = []
|
||||||
|
for field in fields:
|
||||||
|
if field in headerbook:
|
||||||
|
clist.append(headerbook[field])
|
||||||
|
else:
|
||||||
|
clist.append("{0}".format(field))
|
||||||
|
of.write(maketablerow(clist, True))
|
||||||
|
rnum = 0
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
clist = []
|
||||||
|
for field in fields:
|
||||||
|
if field == "rank":
|
||||||
|
clist.append("{0}".format(i + 1))
|
||||||
|
elif field in entry:
|
||||||
|
thing = entry[field]
|
||||||
|
if field == "game":
|
||||||
|
clist.append(thing.name)
|
||||||
|
elif field == "xl" or field == "score": # Numerics
|
||||||
|
clist.append(str(thing))
|
||||||
|
elif field == "name":
|
||||||
|
clist.append(playerlink(thing))
|
||||||
|
elif field == "fate":
|
||||||
|
clist.append(thing)
|
||||||
|
elif field == "etime":
|
||||||
|
clist.append(linktoArchive(entry))
|
||||||
|
else:
|
||||||
|
clist.append("{0}".format(thing))
|
||||||
|
else:
|
||||||
|
clist.append("N/A")
|
||||||
|
of.write(maketablerow(clist))
|
||||||
|
of.write(tblend)
|
||||||
|
return
|
||||||
|
|
||||||
|
def readentries(entfile, entlist):
|
||||||
|
"Reads a list of entries from a file object"
|
||||||
|
while True:
|
||||||
|
nextentry = entfile.readline()
|
||||||
|
if not nextentry:
|
||||||
|
break
|
||||||
|
if nextentry[-1] != '\n': # The line is incomplete
|
||||||
|
break
|
||||||
|
entlist.append(nextentry.split(None, 4))
|
||||||
|
return
|
||||||
|
|
||||||
|
class Game:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class RogueGame(Game):
|
||||||
|
def __init__(self, name, uname, suffix):
|
||||||
|
self.name = name
|
||||||
|
self.uname = uname
|
||||||
|
self.scores = logdir + uname + ".log"
|
||||||
|
self.logspec = ["etime", "score", "name", "xl", "fate"]
|
||||||
|
self.sqltypes = {"etime": "int", "score": "int", "name": "varchar(20)",
|
||||||
|
"xl": "int", "fate": "text", "stime": "int",
|
||||||
|
"ttyrecs": "text ARRAY", "ststamp": "timestamptz",
|
||||||
|
"etstamp": "timestamptz"}
|
||||||
|
self.logdelim = " "
|
||||||
|
# Class variables, used by some methods
|
||||||
|
fields = ["name", "score", "xl", "fate", "etime"]
|
||||||
|
rankfields = ["rank", "score", "name", "xl", "fate", "etime"]
|
||||||
|
pfields = ["score", "xl", "fate", "etime"]
|
||||||
|
def logtoDict(self, entry):
|
||||||
|
"Processes a log entry string, returning a dict."
|
||||||
|
ndict = {"game": self}
|
||||||
|
entrylist = entry.strip().split(self.logdelim, len(self.logspec) - 1)
|
||||||
|
for item, value in zip(self.logspec, entrylist):
|
||||||
|
if self.sqltypes[item] == "int":
|
||||||
|
ndict[item] = int(value)
|
||||||
|
else:
|
||||||
|
ndict[item] = value
|
||||||
|
return ndict
|
||||||
|
def getEntryDicts(self, entfile, entlist):
|
||||||
|
"Reads logfile entries from entfile, interprets them according to the \
|
||||||
|
instructions in self.logspec/logdelim, and adds them as dicts to entlist."
|
||||||
|
while True:
|
||||||
|
nextentry = entfile.readline()
|
||||||
|
if not nextentry:
|
||||||
|
break
|
||||||
|
if nextentry[-1] != '\n':
|
||||||
|
break
|
||||||
|
entlist.append(self.logtoDict(nextentry))
|
||||||
|
return
|
||||||
|
def getRecent(self, n=20):
|
||||||
|
"Gets the n most recent games out of the database, returning a list \
|
||||||
|
of dicts."
|
||||||
|
try:
|
||||||
|
n = int(n)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return []
|
||||||
|
if n <= 0:
|
||||||
|
return []
|
||||||
|
dictlist = []
|
||||||
|
conn = psycopg2.connect("dbname=rlg")
|
||||||
|
cur = conn.cursor()
|
||||||
|
qstr = "SELECT etime, score, name, xl, fate, ststamp, etstamp FROM {0} ORDER BY etstamp DESC \
|
||||||
|
LIMIT %s;".format(self.uname)
|
||||||
|
cur.execute(qstr, [n])
|
||||||
|
for record in cur:
|
||||||
|
# This should be less hardcodish
|
||||||
|
ndict = {"game": self}
|
||||||
|
ndict["etime"] = record[0]
|
||||||
|
ndict["score"] = record[1]
|
||||||
|
ndict["name"] = record[2]
|
||||||
|
ndict["xl"] = record[3]
|
||||||
|
ndict["fate"] = record[4]
|
||||||
|
ndict["ststamp"] = record[5]
|
||||||
|
ndict["etstamp"] = record[6]
|
||||||
|
dictlist.append(ndict)
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return dictlist
|
||||||
|
def getHigh(self, n=10, offset=0):
|
||||||
|
"Gets the n highest scores (starting at offset) from the database, \
|
||||||
|
returning a list of dicts."
|
||||||
|
qstr = "SELECT etime, score, name, xl, fate, ststamp, etstamp FROM {0} ORDER BY score DESC\
|
||||||
|
".format(self.uname)
|
||||||
|
qvals = []
|
||||||
|
try:
|
||||||
|
n = int(n)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return []
|
||||||
|
if n <= 0:
|
||||||
|
return []
|
||||||
|
qstr += " LIMIT %s"
|
||||||
|
qvals.append(n)
|
||||||
|
try:
|
||||||
|
offset = int(offset)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return []
|
||||||
|
if n > 0:
|
||||||
|
qstr += " OFFSET %s"
|
||||||
|
qvals.append(offset)
|
||||||
|
qstr += ";"
|
||||||
|
conn = psycopg2.connect("dbname=rlg")
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(qstr, qvals)
|
||||||
|
dictlist = []
|
||||||
|
for record in cur:
|
||||||
|
ndict = {"game": self}
|
||||||
|
ndict["etime"] = record[0]
|
||||||
|
ndict["score"] = record[1]
|
||||||
|
ndict["name"] = record[2]
|
||||||
|
ndict["xl"] = record[3]
|
||||||
|
ndict["fate"] = record[4]
|
||||||
|
ndict["ststamp"] = record[5]
|
||||||
|
ndict["etstamp"] = record[6]
|
||||||
|
dictlist.append(ndict)
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return dictlist
|
||||||
|
def getPlayer(self, player):
|
||||||
|
"Gets all player's games from the database."
|
||||||
|
qstr = "SELECT etime, score, name, xl, fate, ststamp, etstamp FROM " + self.uname + " WHERE \
|
||||||
|
name = %s;"
|
||||||
|
conn = getconn()
|
||||||
|
if conn == None:
|
||||||
|
return []
|
||||||
|
cur = conn.cursor()
|
||||||
|
entries = []
|
||||||
|
cur.execute(qstr, [player])
|
||||||
|
for record in cur:
|
||||||
|
ndict = {"game": self}
|
||||||
|
ndict["etime"] = record[0]
|
||||||
|
ndict["score"] = record[1]
|
||||||
|
ndict["name"] = record[2]
|
||||||
|
ndict["xl"] = record[3]
|
||||||
|
ndict["fate"] = record[4]
|
||||||
|
ndict["ststamp"] = record[5]
|
||||||
|
ndict["etstamp"] = record[6]
|
||||||
|
entries.append(ndict)
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return entries
|
||||||
|
def putIntoDB(self, dictlist, conn):
|
||||||
|
"Add the entries in dictlist to the database through connection conn, \
|
||||||
|
which needs the INSERT privilege."
|
||||||
|
# FIXME this monster needs to be procedurally generated
|
||||||
|
qstr = "INSERT INTO " + self.uname + " (etime, score, name, xl, fate, stime, ttyrecs, ststamp, etstamp) \
|
||||||
|
VALUES (%(etime)s, %(score)s, %(name)s, %(xl)s, %(fate)s, %(stime)s, %(ttyrecs)s, %(ststamp)s, %(etstamp)s);"
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.executemany(qstr, [ d for d in dictlist if d["game"] == self ])
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
return
|
||||||
|
def postprocess(self, gamelist):
|
||||||
|
names = set([ e["name"] for e in gamelist ])
|
||||||
|
conn = getconn()
|
||||||
|
if conn == None:
|
||||||
|
return []
|
||||||
|
cur = conn.cursor()
|
||||||
|
for nameF in names:
|
||||||
|
# Get all this player's games ordered by time
|
||||||
|
itsEntries = [ entry for entry in gamelist if entry["name"] == nameF ]
|
||||||
|
itsEntries.sort(key=lambda e: e["etime"])
|
||||||
|
# Find the end time of the latest game already in the db
|
||||||
|
tquery = "SELECT etstamp FROM {0} WHERE name = %s ORDER BY etstamp DESC LIMIT 1;".format(self.uname)
|
||||||
|
cur.execute(tquery, [nameF])
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result:
|
||||||
|
prev = calendar.timegm(result[0].utctimetuple())
|
||||||
|
else:
|
||||||
|
prev = 0
|
||||||
|
ttyrecdir = "/var/dgl/dgldir/ttyrec/{0}/{1}/".format(nameF, self.uname)
|
||||||
|
allfilekeys = [ (recnameToInt(f), f) for f in os.listdir(ttyrecdir) ]
|
||||||
|
vfilekeys = [ e for e in allfilekeys if e[0] > prev ]
|
||||||
|
vfilekeys.sort(key=lambda e: e[0])
|
||||||
|
# Now determine stime and ttyrecs for each game
|
||||||
|
for i in range(0, len(itsEntries)):
|
||||||
|
if i == 0:
|
||||||
|
lowlim = prev
|
||||||
|
else:
|
||||||
|
lowlim = itsEntries[i-1]["etime"]
|
||||||
|
hilim = itsEntries[i]["etime"]
|
||||||
|
recs = [ k[1] for k in vfilekeys if lowlim <= k[0] < hilim ]
|
||||||
|
itsEntries[i]["stime"] = recnameToInt(recs[0])
|
||||||
|
itsEntries[i]["ttyrecs"] = recs
|
||||||
|
# While we're at it, replace numeric timestamps with SQL timestamps.
|
||||||
|
itsEntries[i]["ststamp"] = datetime.fromtimestamp(itsEntries[i]["stime"], utc)
|
||||||
|
itsEntries[i]["etstamp"] = datetime.fromtimestamp(itsEntries[i]["etime"], utc)
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
def loadnew(self):
|
||||||
|
conn = getconn()
|
||||||
|
if conn == None:
|
||||||
|
return []
|
||||||
|
cur = conn.cursor()
|
||||||
|
# Get the previous offset
|
||||||
|
cur.execute(offselstr, [self.uname])
|
||||||
|
offset = cur.fetchone()[0]
|
||||||
|
newlist = []
|
||||||
|
try:
|
||||||
|
scr = open(self.scores)
|
||||||
|
scr.seek(offset)
|
||||||
|
self.getEntryDicts(scr, newlist)
|
||||||
|
except IOError:
|
||||||
|
noffset = offset # Can't read anything, assume no new games
|
||||||
|
else:
|
||||||
|
noffset = scr.tell()
|
||||||
|
scr.close()
|
||||||
|
cur.execute(newoffstr, [noffset, self.uname])
|
||||||
|
# The new players must already be added to the players table.
|
||||||
|
updatenames = set([ e["name"] for e in newlist ])
|
||||||
|
self.postprocess(newlist)
|
||||||
|
self.putIntoDB(newlist, conn)
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return updatenames
|
||||||
|
# End RogueGame class definition
|
||||||
|
|
||||||
|
rogue3 = RogueGame("Rogue V3", "rogue3", "r3")
|
||||||
|
rogue4 = RogueGame("Rogue V4", "rogue4", "r4")
|
||||||
|
rogue5 = RogueGame("Rogue V5", "rogue5", "r5")
|
||||||
|
srogue = RogueGame("Super-Rogue", "srogue", "sr")
|
||||||
|
|
||||||
|
gamelist = [rogue3, rogue4, rogue5, srogue]
|
||||||
|
|
||||||
|
def playerpage(pname):
|
||||||
|
"Generate a player's HTML page"
|
||||||
|
# Write the beginning of the page
|
||||||
|
ppagefi = open(ppagename.format(pname), "w")
|
||||||
|
ppagefi.write(phead.format(pname))
|
||||||
|
ppagefi.write(ptop)
|
||||||
|
ppagefi.write(navplayer.format(pname))
|
||||||
|
ppagefi.write(pti.format("Results for " + pname))
|
||||||
|
for game in gamelist:
|
||||||
|
ppagefi.write(secthead.format(game.name))
|
||||||
|
entries = game.getPlayer(pname)
|
||||||
|
if not entries:
|
||||||
|
ppagefi.write("<div>" + pname + " has not yet completed an expedition\
|
||||||
|
in this dungeon.</div>\n")
|
||||||
|
else:
|
||||||
|
printTable(entries, RogueGame.pfields, ppagefi)
|
||||||
|
scoresum = 0
|
||||||
|
for entry in entries:
|
||||||
|
scoresum += int(entry["score"])
|
||||||
|
avgscr = scoresum / len(entries)
|
||||||
|
ppagefi.write("<p>Average score: {0}</p>\n".format(avgscr))
|
||||||
|
# That should settle it. Print the end; then stop.
|
||||||
|
ppagefi.write(pend)
|
||||||
|
ppagefi.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
def highpage():
|
||||||
|
# open the page and print the beginning
|
||||||
|
highpfi = open(hpagename, "w")
|
||||||
|
highpfi.write(phead.format("High Scores"))
|
||||||
|
highpfi.write(ptop)
|
||||||
|
highpfi.write(navscore.format("High Scores"))
|
||||||
|
highpfi.write(pti.format("List of High Scores"))
|
||||||
|
for game in gamelist:
|
||||||
|
highpfi.write(secthead.format(game.name))
|
||||||
|
entries = game.getHigh(40)
|
||||||
|
if not entries:
|
||||||
|
highpfi.write("<div>No one has braved this dungeon yet.</div>\n")
|
||||||
|
else:
|
||||||
|
printTable(entries, game.rankfields, highpfi)
|
||||||
|
# That should finish it.
|
||||||
|
highpfi.write(pend)
|
||||||
|
highpfi.close()
|
||||||
|
return
|
||||||
37
py/setupdb.py
Normal file
37
py/setupdb.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# setupdb.py: initializes the database tables used by the rlg system.
|
||||||
|
|
||||||
|
import rlgalldb as rlgall
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
webuser = "webserver"
|
||||||
|
allowquery = "GRANT SELECT ON {0} TO " + webuser + ";"
|
||||||
|
|
||||||
|
dbconn = psycopg2.connect("dbname=rlg")
|
||||||
|
dbcur = dbconn.cursor()
|
||||||
|
|
||||||
|
dbcur.execute("CREATE TABLE games ( gname varchar(20), offbytes int );")
|
||||||
|
dbcur.execute("CREATE TABLE players (pname varchar(20) PRIMARY KEY);")
|
||||||
|
dbconn.commit()
|
||||||
|
|
||||||
|
for game in rlgall.gamelist:
|
||||||
|
dbcur.execute("INSERT INTO games VALUES (%s, %s);", (game.uname, 0))
|
||||||
|
|
||||||
|
createquery = "CREATE TABLE " + game.uname + " ( "
|
||||||
|
for i, field in enumerate(game.sqltypes.keys()):
|
||||||
|
createquery += "{0} {1}".format(field, game.sqltypes[field])
|
||||||
|
if field == "name":
|
||||||
|
createquery += " REFERENCES players(pname)"
|
||||||
|
if i == len(game.sqltypes) - 1:
|
||||||
|
createquery += " )"
|
||||||
|
else:
|
||||||
|
createquery += ", "
|
||||||
|
createquery += ";"
|
||||||
|
#print createquery
|
||||||
|
dbcur.execute(createquery)
|
||||||
|
dbcur.execute(allowquery.format(game.uname))
|
||||||
|
dbconn.commit()
|
||||||
|
|
||||||
|
dbcur.close()
|
||||||
|
dbconn.close()
|
||||||
|
exit()
|
||||||
35
py/stats.py
Normal file
35
py/stats.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import math
|
||||||
|
import psycopg2
|
||||||
|
import rlgalldb as rlgall
|
||||||
|
|
||||||
|
def makeExpPDF(lbd):
|
||||||
|
def lpdf(x):
|
||||||
|
return lbd * math.exp(-lbd * x)
|
||||||
|
return lpdf
|
||||||
|
|
||||||
|
def makeExpCDF(lbd):
|
||||||
|
def lcdf(x):
|
||||||
|
return 1 - math.exp(-lbd * x)
|
||||||
|
return lcdf
|
||||||
|
|
||||||
|
conn = psycopg2.connect("dbname=rlg")
|
||||||
|
cur = conn.cursor()
|
||||||
|
for game in rlgall.gamelist:
|
||||||
|
query = "SELECT score FROM {0};".format(game.uname)
|
||||||
|
cur.execute(query)
|
||||||
|
scores = [ r[0] for r in cur.fetchall() ]
|
||||||
|
count = len(scores)
|
||||||
|
total = sum(scores)
|
||||||
|
lbd = float(count) / total
|
||||||
|
cdf = makeExpCDF(lbd)
|
||||||
|
print "{0}: {1} games, average {2}".format(game.name, count, int(1/lbd))
|
||||||
|
for i in range(0, 10000, 1000):
|
||||||
|
actual = len([ s for s in scores if i <= s < i + 1000 ])
|
||||||
|
predicted = (cdf(i + 1000) - cdf(i)) * count
|
||||||
|
print "{0:5}: {1:4} {2}".format(i, actual, predicted)
|
||||||
|
high = max(scores)
|
||||||
|
print "Max: {0} {1}\n".format(high, 1 - cdf(high))
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
exit()
|
||||||
374
web/archive.cgi
Executable file
374
web/archive.cgi
Executable file
|
|
@ -0,0 +1,374 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import cgi
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import calendar
|
||||||
|
import rlgalldb as rlgall
|
||||||
|
import cgitb
|
||||||
|
|
||||||
|
cgitb.enable()
|
||||||
|
|
||||||
|
infop = """<p>Looking for ttyrec files? Don't like digging through Web directory listings, converting time zones in your head, or guessing how many files
|
||||||
|
the game was spread across? The Archivist can find them all for you.</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
ttyrecbase = "/var/dgl/dgldir/ttyrec/"
|
||||||
|
recre = r"(\d{4,4})-(\d{2,2})-(\d{2,2})\.(\d{2,2}):(\d{2,2}):(\d{2,2})\.ttyrec"
|
||||||
|
recrec = re.compile(recre)
|
||||||
|
|
||||||
|
def name_in_time(filename, timerange):
|
||||||
|
"Checks whether filename is a ttyrec with time between Unix times \
|
||||||
|
timerange[0] and timerange[1]."
|
||||||
|
nmatch = recrec.match(filename)
|
||||||
|
if not nmatch:
|
||||||
|
return False
|
||||||
|
ntime = calendar.timegm([int(val) for val in nmatch.groups()])
|
||||||
|
if ntime > timerange[0] and ntime <= timerange[1]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def input_game(outf, selected=None):
|
||||||
|
"Prints the form components for selecting a game."
|
||||||
|
selstr = '<option label="{0}" value="{1}" selected="selected">{0}</option>\n'
|
||||||
|
unselstr = '<option label="{0}" value="{1}">{0}</option>\n'
|
||||||
|
outf.write('<div>Dungeon name: <select name="game">\n')
|
||||||
|
for game in rlgall.gamelist:
|
||||||
|
# This will cause trouble if someone puts games with identical names
|
||||||
|
# into rlgall.
|
||||||
|
if selected and selected == game.uname:
|
||||||
|
outf.write(selstr.format(game.name, game.uname))
|
||||||
|
else:
|
||||||
|
outf.write(unselstr.format(game.name, game.uname))
|
||||||
|
outf.write('</select></div>\n')
|
||||||
|
return
|
||||||
|
|
||||||
|
def input_name(outf, defaultval=None):
|
||||||
|
defstr = '<div>Adventurer\'s name: <input type="text" name="name" value="{0}"></div>\n'
|
||||||
|
if defaultval:
|
||||||
|
outf.write(defstr.format(defaultval))
|
||||||
|
else:
|
||||||
|
outf.write('<div>Adventurer\'s Name: <input type="text" name="name"></div>\n')
|
||||||
|
return
|
||||||
|
|
||||||
|
def input_time(outf, defaultval=None):
|
||||||
|
defstr = '<div>Time: <input type="text" name="time" value="{0}"></div>\n'
|
||||||
|
if defaultval:
|
||||||
|
outf.write(defstr.format(defaultval))
|
||||||
|
else:
|
||||||
|
outf.write('<div>Time: <input type="text" name="time"></div>\n')
|
||||||
|
return
|
||||||
|
|
||||||
|
def input_datetime(outf, dvals=[None, None, None, None, None, None]):
|
||||||
|
optstr = '<option value="{0}" label="{1}">{1}</option>\n'
|
||||||
|
soptstr = '<option value="{0}" label="{1}" selected="selected">{1}</option>\n'
|
||||||
|
tf = '<input type="text" size="2" maxlength="2" name="{0}" value="{1:02}">'
|
||||||
|
emptf = '<input type="text" size="2" maxlength="2" name="{0}">'
|
||||||
|
sstr = '<div>Date: <select name="year">\n'
|
||||||
|
# Default to today
|
||||||
|
now = time.gmtime()
|
||||||
|
for dindex in range(3):
|
||||||
|
if not dvals[dindex]:
|
||||||
|
dvals[dindex] = now[dindex]
|
||||||
|
for year in range(2010, now[0] + 1):
|
||||||
|
if year == dvals[0]:
|
||||||
|
sstr += soptstr.format(year, year)
|
||||||
|
else:
|
||||||
|
sstr += optstr.format(year, year)
|
||||||
|
sstr += '</select>\n'
|
||||||
|
outf.write(sstr)
|
||||||
|
sstr = '<select name="month">\n'
|
||||||
|
for month in range(1, 13):
|
||||||
|
if month == dvals[1]:
|
||||||
|
sstr += soptstr.format(month, calendar.month_name[month])
|
||||||
|
else:
|
||||||
|
sstr += optstr.format(month, calendar.month_name[month])
|
||||||
|
sstr += '</select>\n'
|
||||||
|
outf.write(sstr)
|
||||||
|
sstr = '<select name="day">\n'
|
||||||
|
for day in range(1, 32):
|
||||||
|
if day == dvals[2]:
|
||||||
|
sstr += soptstr.format(day, day)
|
||||||
|
else:
|
||||||
|
sstr += optstr.format(day, day)
|
||||||
|
sstr += '</select></div>\n'
|
||||||
|
outf.write(sstr)
|
||||||
|
sstr = '<div>Approximate time: '
|
||||||
|
if dvals[3] != None:
|
||||||
|
sstr += tf.format("hour", dvals[3])
|
||||||
|
else:
|
||||||
|
sstr += emptf.format("hour")
|
||||||
|
sstr += " : "
|
||||||
|
if dvals[4] != None:
|
||||||
|
sstr += tf.format("minute", dvals[4])
|
||||||
|
else:
|
||||||
|
sstr += emptf.format("minute")
|
||||||
|
sstr += " : "
|
||||||
|
if dvals[5] != None:
|
||||||
|
sstr += tf.format("second", dvals[5])
|
||||||
|
else:
|
||||||
|
sstr += emptf.format("second")
|
||||||
|
sstr += ' (optional)</div>\n'
|
||||||
|
outf.write(sstr)
|
||||||
|
return
|
||||||
|
|
||||||
|
def checkempty(cgidata):
|
||||||
|
exkeys = set(cgidata.keys())
|
||||||
|
wanted = set(["name", "game", "time", "year", "month", "day", "hour",
|
||||||
|
"minute", "second"])
|
||||||
|
return wanted.isdisjoint(exkeys)
|
||||||
|
|
||||||
|
def processname(fdata, errlist):
|
||||||
|
"Takes a CGI data object, extracts the player name, checks its validity, \
|
||||||
|
and returns it. If errors are encountered, they are appended to errlist."
|
||||||
|
cantfind = "I have no record of an adventurer called {0}."
|
||||||
|
if "name" not in fdata:
|
||||||
|
errlist.append("You didn\'t tell me the adventurer\'s name.")
|
||||||
|
return None
|
||||||
|
# Just in case someone supplies something nasty, make sure they can't go
|
||||||
|
# digging all over the filesystem.
|
||||||
|
formname = fdata.getfirst("name").rpartition("/")[2]
|
||||||
|
try:
|
||||||
|
os.stat(ttyrecbase + formname)
|
||||||
|
except OSError:
|
||||||
|
errlist.append(cantfind.format(cgi.escape(formname)))
|
||||||
|
return None
|
||||||
|
return formname
|
||||||
|
|
||||||
|
def processgame(fdata, errlist):
|
||||||
|
"Takes a CGI data object and returns the game from rlgall.gamelist that \
|
||||||
|
it asks for. Errors appended to errlist."
|
||||||
|
cantfind = "I've never heard of a dungeon called {0}."
|
||||||
|
if "game" not in fdata:
|
||||||
|
errlist.append('You didn\'t choose a dungeon.')
|
||||||
|
return None
|
||||||
|
formgame = fdata.getfirst("game")
|
||||||
|
for agame in rlgall.gamelist:
|
||||||
|
if agame.uname == formgame:
|
||||||
|
return agame
|
||||||
|
errlist.append(cantfind.format(cgi.escape(formgame)))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def processtime(fdata, errlist, hlist):
|
||||||
|
"Takes a CGI data object and converts to a Unix timestamp by finding fields \
|
||||||
|
called year, month, etc. Any errors get appended to errlist. hlist \
|
||||||
|
should contain 6 components, for ymd-hms fields."
|
||||||
|
|
||||||
|
# Timestamp overrides human-readable, even if it's invalid.
|
||||||
|
badtime = 'The time field is for Unix clocks, which never say anything \
|
||||||
|
like {0}. Try the form instead.'
|
||||||
|
utime = None
|
||||||
|
formtime = fdata.getfirst("time")
|
||||||
|
if formtime:
|
||||||
|
try:
|
||||||
|
utime = int(formtime)
|
||||||
|
except ValueError:
|
||||||
|
errlist.append(badtime.format(cgi.escape(formtime)))
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
if utime < 0:
|
||||||
|
utime = 0
|
||||||
|
if utime != None:
|
||||||
|
chtime = time.gmtime(utime)
|
||||||
|
for i in range(6):
|
||||||
|
hlist[i] = chtime[i]
|
||||||
|
return utime
|
||||||
|
|
||||||
|
# Now try to get a human-readable specification.
|
||||||
|
lerrors = []
|
||||||
|
year = month = day = hour = minute = second = None
|
||||||
|
fyear = fdata.getfirst("year")
|
||||||
|
if not fyear:
|
||||||
|
lerrors.append("No year was provided.")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
year = int(fyear)
|
||||||
|
except ValueError:
|
||||||
|
lerrors.append("Invalid year.")
|
||||||
|
else:
|
||||||
|
if year < 2010:
|
||||||
|
lerrors.append("The Gallery has only existed since 2010.")
|
||||||
|
elif year > time.gmtime()[0]:
|
||||||
|
lerrors.append("I can't see into the future.")
|
||||||
|
else:
|
||||||
|
hlist[0] = year # It is valid
|
||||||
|
fmonth = fdata.getfirst("month")
|
||||||
|
if not fmonth:
|
||||||
|
lerrors.append("No month was provided.")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
month = int(fmonth)
|
||||||
|
except ValueError:
|
||||||
|
lerrors.append("Invalid month.")
|
||||||
|
else:
|
||||||
|
if month < 1 or month > 12:
|
||||||
|
lerrors.append("Invalid month.")
|
||||||
|
else:
|
||||||
|
hlist[1] = month
|
||||||
|
fday = fdata.getfirst("day")
|
||||||
|
if not fday:
|
||||||
|
lerrors.append("No day was provided.")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
day = int(fday)
|
||||||
|
except ValueError:
|
||||||
|
lerrors.append("Invalid day.")
|
||||||
|
else:
|
||||||
|
if day < 1 or day > 31:
|
||||||
|
lerrors.append("Invalid day.")
|
||||||
|
elif not lerrors:
|
||||||
|
if day > calendar.monthrange(year, month)[1]:
|
||||||
|
lerrors.append("Invalid day.")
|
||||||
|
else:
|
||||||
|
hlist[2] = day
|
||||||
|
fhour = fdata.getfirst("hour")
|
||||||
|
if not fhour:
|
||||||
|
hour = 0 # Assume things.
|
||||||
|
hlist[3] = 0
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
hour = int(fhour)
|
||||||
|
except ValueError:
|
||||||
|
lerrors.append("Invalid hour.")
|
||||||
|
else:
|
||||||
|
if hour < 0 or hour > 23:
|
||||||
|
lerrors.append("Invalid hour.")
|
||||||
|
else:
|
||||||
|
hlist[3] = hour
|
||||||
|
fminute = fdata.getfirst("minute")
|
||||||
|
if not fminute:
|
||||||
|
minute = 0
|
||||||
|
hlist[4] = 0
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
minute = int(fminute)
|
||||||
|
except ValueError:
|
||||||
|
lerrors.append("Invalid minute.")
|
||||||
|
else:
|
||||||
|
if minute < 0 or minute > 59:
|
||||||
|
lerrors.append("Invalid minute.")
|
||||||
|
else:
|
||||||
|
hlist[4] = minute
|
||||||
|
fsecond = fdata.getfirst("second")
|
||||||
|
if not fsecond:
|
||||||
|
second = 0
|
||||||
|
hlist[5] = 0
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
second = int(fsecond)
|
||||||
|
except ValueError:
|
||||||
|
lerrors.append("Invalid second.")
|
||||||
|
else:
|
||||||
|
if second < 0 or second > 59:
|
||||||
|
lerrors.append("Invalid second.")
|
||||||
|
if second == 60 or second == 61:
|
||||||
|
lerrors.append("Leap seconds are worse than purple worms.")
|
||||||
|
else:
|
||||||
|
hlist[5] = second
|
||||||
|
if lerrors:
|
||||||
|
errlist.extend(lerrors)
|
||||||
|
return None
|
||||||
|
return calendar.timegm([year, month, day, hour, minute, second, 0, 0, 0])
|
||||||
|
|
||||||
|
# Begin processing
|
||||||
|
fdata = cgi.FieldStorage()
|
||||||
|
|
||||||
|
# If this is true, the user didn't supply any search criteria, so assume it
|
||||||
|
# doesn't want a search. Otherwise, every error or omission must be printed.
|
||||||
|
isnotsearch = checkempty(fdata)
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
formname = dungeon = utime = None
|
||||||
|
timepieces = [None, None, None, None, None, None]
|
||||||
|
|
||||||
|
if not isnotsearch:
|
||||||
|
formname = processname(fdata, errors)
|
||||||
|
dungeon = processgame(fdata, errors)
|
||||||
|
utime = processtime(fdata, errors, timepieces)
|
||||||
|
|
||||||
|
dosearch = formname != None and dungeon != None and utime != None
|
||||||
|
|
||||||
|
# Find the actual files, and put them in a list called gamefiles.
|
||||||
|
gtimes = [0, int(time.time())]
|
||||||
|
relgame = None
|
||||||
|
gamefiles = []
|
||||||
|
if dosearch:
|
||||||
|
ttyrecdir = "{0}/{1}/{2}/".format(ttyrecbase, formname, dungeon.uname)
|
||||||
|
query1 = "SELECT ttyrecs FROM {0} WHERE name = %s AND stime <= %s AND etime >= %s;".format(dungeon.uname)
|
||||||
|
query2 = "SELECT ttyrecs FROM {0} WHERE name = %s AND etime >= %s ORDER BY etime LIMIT 1;".format(dungeon.uname)
|
||||||
|
query3 = "SELECT ttyrecs FROM {0} WHERE name = %s AND stime <= %s ORDER BY stime DESC LIMIT 1;".format(dungeon.uname)
|
||||||
|
conn = rlgall.getconn()
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(query1, [formname, utime, utime])
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result:
|
||||||
|
gamefiles = result[0]
|
||||||
|
else:
|
||||||
|
cur.execute(query2, [formname, utime])
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result:
|
||||||
|
gamefiles = result[0]
|
||||||
|
else:
|
||||||
|
cur.execute(query3, [formname, utime])
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result:
|
||||||
|
gamefiles = result[0]
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Now we are ready to print the page.
|
||||||
|
sys.stdout.write("Content-type: text/html\r\n\r\n")
|
||||||
|
|
||||||
|
sys.stdout.write(rlgall.phead.format("Archive"))
|
||||||
|
sys.stdout.write(rlgall.ptop)
|
||||||
|
sys.stdout.write(rlgall.navtop.format("Archive"))
|
||||||
|
sys.stdout.write('<div class="content">\n')
|
||||||
|
sys.stdout.write(rlgall.pti.format("Guild Archives"))
|
||||||
|
|
||||||
|
if dosearch:
|
||||||
|
sys.stdout.write("<p>Expedition by {0} to {1} about {2}:</p>\n".format(
|
||||||
|
rlgall.playerlink(formname), dungeon.name,
|
||||||
|
time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(utime))))
|
||||||
|
if not gamefiles:
|
||||||
|
sys.stdout.write("<p>No record found.</p>\n")
|
||||||
|
elif len(gamefiles) == 1:
|
||||||
|
sys.stdout.write('<p><a href="/ttyrecs/{0}/{1}/{2}">1 ttyrec found.</a>\
|
||||||
|
</p>\n'.format(formname, dungeon.uname, gamefiles[0]))
|
||||||
|
else:
|
||||||
|
sys.stdout.write('<p>{0}-part ttyrec found.</p>\n'.format(len(gamefiles)))
|
||||||
|
sys.stdout.write('<ul>\n')
|
||||||
|
for i, afile in enumerate(gamefiles):
|
||||||
|
sys.stdout.write('<li><a href="/ttyrecs/{0}/{1}/{2}">Section {3}</a>\
|
||||||
|
</li>\n'.format(formname, dungeon.uname, afile, i + 1))
|
||||||
|
sys.stdout.write('</ul>\n')
|
||||||
|
if isnotsearch:
|
||||||
|
sys.stdout.write(infop)
|
||||||
|
else:
|
||||||
|
# There was information, but not good enough, i.e. errors.
|
||||||
|
sys.stdout.write('<p>')
|
||||||
|
for errmsg in errors:
|
||||||
|
sys.stdout.write(errmsg + " ")
|
||||||
|
sys.stdout.write('</p>\n')
|
||||||
|
# Print out a search form, whether a search was done or not.
|
||||||
|
sys.stdout.write('<form action="/archivedb.cgi" method="get">\n')
|
||||||
|
if dungeon != None:
|
||||||
|
input_game(sys.stdout, dungeon.uname)
|
||||||
|
else:
|
||||||
|
input_game(sys.stdout)
|
||||||
|
if formname != None:
|
||||||
|
input_name(sys.stdout, formname)
|
||||||
|
else:
|
||||||
|
input_name(sys.stdout)
|
||||||
|
input_datetime(sys.stdout, timepieces)
|
||||||
|
if dosearch:
|
||||||
|
sys.stdout.write('<div><input type="submit" value="Search Again"></div></form>')
|
||||||
|
else:
|
||||||
|
sys.stdout.write('<div><input type="submit" value="Search the Archive"></div></form>')
|
||||||
|
|
||||||
|
sys.stdout.write('<p>Or <a href="/ttyrecs/">browse all ttyrecs</a>.</p>')
|
||||||
|
sys.stdout.write('<p><a href="/about/ttyrec.html">What are ttyrecs?</a></p>')
|
||||||
|
sys.stdout.write("</div>\n</body>\n</html>\n")
|
||||||
|
exit()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue