rlgallery-misc/py/rlgalldb.py

517 lines
17 KiB
Python
Raw Normal View History

# rlgalldb.py
# Module for the Roguelike Gallery, using a postgres database
# Requires Python 3.3
import os
import psycopg2
from datetime import datetime
import pytz
# 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> -&gt; {0}</div>\n'
navscore = '<div class="nav"><a href="/">rlgallery.org</a> -&gt; \
<a href="/scoring/">Scores</a> -&gt; {0}</div>\n'
navplayer = '<div class="nav"><a href="/">rlgallery.org</a> -&gt; \
<a href="/scoring/">Scores</a> -&gt; <a href="/scoring/players/">Players</a> \
-&gt; {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'
rcell = ' <span class="sdatar">{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 = {"endt":"End time", "score":"Score", "name":"Name", "xl":"XL",
"fate":"Fate", "rank":"Rank", "game":"Game", "class": "Class"}
# Queries for the games table
offselstr = "SELECT offbytes FROM games WHERE gname = %s;"
newoffstr = "UPDATE games SET offbytes = %s WHERE gname = %s;"
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 recnameToTS(filename):
pattern = "%Y-%m-%d.%H:%M:%S.ttyrec"
try:
dt = datetime.strptime(filename, pattern).replace(tzinfo=pytz.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 = entry["endt"].strftime("%Y/%m/%d %H:%M:%S")
stamp = int(entry["endt"].timestamp())
return lstr.format(entry["name"], entry["game"].uname, stamp, 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), rcell))
elif field in entry:
thing = entry[field]
if field == "game":
clist.append((thing.name, cell))
elif field == "xl" or field == "score": # Numerics
clist.append((str(thing), rcell))
elif field == "name":
clist.append((playerlink(thing), cell))
elif field == "fate":
clist.append((thing, cell))
elif field == "endt":
clist.append((linktoArchive(entry), cell))
else:
clist.append(("{0}".format(thing), cell))
else:
clist.append(("N/A", cell))
rowstr = rowstart + "".join([ t.format(s) for (s, t) in clist ]) + rowend
of.write(rowstr)
of.write(tblend)
return
class Game:
def __init__(self, name, uname):
pass
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)
if self.sqltypes[item] == "bool":
ndict[item] = bool(int(value))
elif self.sqltypes[item] == "timestamptz":
ndict[item] = datetime.fromtimestamp(int(value), pytz.utc)
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 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, encoding="utf-8")
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
def postprocess(self, gamelist):
"Default postprocessing function: adds start time and ttyrec list"
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["endt"])
# Find the end time of the latest game already in the db
tquery = "SELECT endt FROM {0} WHERE name = %s ORDER BY endt DESC LIMIT 1;".format(self.uname)
cur.execute(tquery, [nameF])
result = cur.fetchone()
if result:
prev = result[0]
else:
prev = datetime.fromtimestamp(0, pytz.utc);
ttyrecdir = "/var/dgl/dgldir/ttyrec/{0}/{1}/".format(nameF, self.uname)
allfilekeys = [ (recnameToTS(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 startt and ttyrecs for each game
for i in range(0, len(itsEntries)):
if i == 0:
lowlim = prev
else:
lowlim = itsEntries[i-1]["endt"]
hilim = itsEntries[i]["endt"]
recs = [ k[1] for k in vfilekeys if lowlim <= k[0] < hilim ]
itsEntries[i]["startt"] = recnameToTS(recs[0])
itsEntries[i]["ttyrecs"] = recs
cur.close()
conn.close()
def putIntoDB(self, dictlist, conn):
cur = conn.cursor()
cur.executemany(self.insertq, [ d for d in dictlist if d["game"] == self ])
conn.commit()
cur.close()
return
def tablerecent(self, of):
"Prints the most recent games from the logfile, NOT the database."
newest = []
try:
scr = open(self.scores, encoding="utf-8")
except FileNotFoundError:
pass
else:
# Text streams don't support random seeking.
try:
scr.buffer.seek(self.lookback, 2)
except OSError:
scr.buffer.seek(0) # The file wasn't that long, start at the beginning
if scr.buffer.tell() != 0:
scr.buffer.readline() # Throw away the incomplete line
self.getEntryDicts(scr, newest)
newest.reverse()
scr.close()
of.write(secthead.format(self.name))
if not newest:
of.write("<div>No one has braved this dungeon yet.</div>\n")
else:
printTable(newest, self.fields, of)
return
# End Game class definition
class RogueGame(Game):
def __init__(self, name, uname, suffix):
self.name = name
self.uname = uname
self.scores = logdir + uname + ".log"
self.logspec = ["endt", "score", "name", "xl", "fate"]
self.sqltypes = {"endt": "timestamptz", "score": "int",
"name": "varchar(20)", "xl": "int", "fate": "text",
"ttyrecs": "text ARRAY", "startt": "timestamptz"}
self.logdelim = " "
self.lookback = -1500
# Construct the insert query
fields = self.sqltypes.keys()
colspec = ", ".join(fields)
valspec = ", ".join([ "%({})s".format(c) for c in fields ])
self.insertq = "INSERT INTO {0} ({1}) VALUES ({2});".format(self.uname,
colspec, valspec)
# Class variables, used by some methods
fields = ["name", "score", "xl", "fate", "endt"]
rankfields = ["rank", "score", "name", "xl", "fate", "endt"]
pfields = ["score", "xl", "fate", "endt"]
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 endt, score, name, xl, fate, startt FROM {0} ORDER BY endt DESC \
LIMIT %s;".format(self.uname)
cur.execute(qstr, [n])
for record in cur:
# This should be less hardcodish
ndict = {"game": self}
ndict["endt"] = record[0]
ndict["score"] = record[1]
ndict["name"] = record[2]
ndict["xl"] = record[3]
ndict["fate"] = record[4]
ndict["startt"] = record[5]
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 endt, score, name, xl, fate, startt 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["endt"] = record[0]
ndict["score"] = record[1]
ndict["name"] = record[2]
ndict["xl"] = record[3]
ndict["fate"] = record[4]
ndict["startt"] = record[5]
dictlist.append(ndict)
cur.close()
conn.close()
return dictlist
def getPlayer(self, player):
"Gets all player's games from the database."
qstr = "SELECT endt, score, name, xl, fate, startt 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["endt"] = record[0]
ndict["score"] = record[1]
ndict["name"] = record[2]
ndict["xl"] = record[3]
ndict["fate"] = record[4]
ndict["startt"] = record[5]
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."
fields = self.sqltypes.keys()
fstr = ", ".join(fields)
vstr = ", ".join([ "%({})s".format(c) for c in fields ])
qstr = "INSERT INTO {0} ({1}) VALUES ({2});".format(self.uname, fstr, vstr);
cur = conn.cursor()
cur.executemany(self.insertq, [ d for d in dictlist if d["game"] == self ])
conn.commit()
cur.close()
return
# End RogueGame class definition
class ARogueGame(Game):
def __init__(self, name, uname, suffix):
self.name = name
self.uname = uname
self.scores = logdir + uname + ".log"
self.logspec = ["endt", "score", "name", "xl", "class", "depth",
"maxdepth", "quest", "hadquest", "fate"]
self.sqltypes = {"endt": "timestamptz", "score": "int",
"name": "varchar(20)", "xl": "int", "class": "text", "depth": "int",
"maxdepth": "int", "quest": "int", "hadquest": "bool",
"fate": "text", "ttyrecs": "text ARRAY", "startt": "timestamptz"}
self.logdelim = " "
self.lookback = -1800
# Construct the insert query
fields = self.sqltypes.keys()
colspec = ", ".join(fields)
valspec = ", ".join([ "%({})s".format(c) for c in fields ])
self.insertq = "INSERT INTO {0} ({1}) VALUES ({2});".format(self.uname,
colspec, valspec)
# Class variables
fields = ["name", "score", "class", "xl", "fate", "endt"]
rankfields = ["rank", "score", "name", "class", "xl", "fate", "endt"]
pfields = ["score", "class", "xl", "fate", "endt"]
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 endt, score, name, xl, class, fate 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["endt"] = record[0]
ndict["score"] = record[1]
ndict["name"] = record[2]
ndict["xl"] = record[3]
ndict["class"] = record[4]
ndict["fate"] = record[5]
dictlist.append(ndict)
cur.close()
conn.close()
return dictlist
def getRecent(self, n=20):
return []
def getPlayer(self, player):
"Gets all player's games from the database."
qstr = "SELECT endt, score, name, xl, class, fate 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["endt"] = record[0]
ndict["score"] = record[1]
ndict["name"] = record[2]
ndict["xl"] = record[3]
ndict["class"] = record[4]
ndict["fate"] = record[5]
entries.append(ndict)
cur.close()
conn.close()
return entries
rogue3 = RogueGame("Rogue V3", "rogue3", "r3")
rogue4 = RogueGame("Rogue V4", "rogue4", "r4")
rogue5 = RogueGame("Rogue V5", "rogue5", "r5")
srogue = RogueGame("Super-Rogue", "srogue", "sr")
arogue5 = ARogueGame("Advanced Rogue 5", "arogue5", "ar5")
gamelist = [rogue3, rogue4, rogue5, srogue, arogue5]
def playerpage(pname):
"Generate a player's HTML page"
# Write the beginning of the page
ppagefi = open(ppagename.format(pname), "w", encoding="utf-8")
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:
entries.sort(key=lambda e: e["endt"])
printTable(entries, game.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", encoding="utf-8")
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