changeset 33:25843238434a

Change the Python module's name back to rlgall. It is no longer an experimental variant. Using a database as a backend is a settled feature.
author John "Elwin" Edwards
date Thu, 02 Jan 2014 13:09:48 -0500
parents 05a4afbe6299
children 86b616d88020
files README.txt py/cleandb.py py/recorder.py py/rlgall.py py/rlgalldb.py py/setupdb.py py/stats.py web/archive.cgi web/recent.cgi web/scoring/players/index.cgi
diffstat 10 files changed, 529 insertions(+), 527 deletions(-) [+]
line wrap: on
line diff
--- a/README.txt	Thu Jan 02 11:48:15 2014 -0500
+++ b/README.txt	Thu Jan 02 13:09:48 2014 -0500
@@ -12,7 +12,7 @@
 py/recorder.py processes the log files and stores the data in a PostgreSQL
 database.  It should be run periodically by cron.
 
-py/rlgalldb.py is a module which recorder.py requires.  It should be installed
+py/rlgall.py is a module which recorder.py requires.  It should be installed
 in /lib/python<x.y>/site-packages or the equivalent location.
 
 web/ contains the static parts of the rlgallery.org website.  Note that when 
--- a/py/cleandb.py	Thu Jan 02 11:48:15 2014 -0500
+++ b/py/cleandb.py	Thu Jan 02 13:09:48 2014 -0500
@@ -1,7 +1,7 @@
 #!/usr/bin/python3
 # cleandb.py: empty the database in an orderly fashion
 
-import rlgalldb as rlgall
+import rlgall
 import psycopg2
 
 dbconn = psycopg2.connect("dbname=rlg")
--- a/py/recorder.py	Thu Jan 02 11:48:15 2014 -0500
+++ b/py/recorder.py	Thu Jan 02 13:09:48 2014 -0500
@@ -2,7 +2,7 @@
 
 import os
 import psycopg2
-import rlgalldb as rlgall
+import rlgall
 
 # Contains a dir for everyone who registered
 everydir = "/var/dgl/dgldir/ttyrec/"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/py/rlgall.py	Thu Jan 02 13:09:48 2014 -0500
@@ -0,0 +1,516 @@
+# rlgall.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
--- a/py/rlgalldb.py	Thu Jan 02 11:48:15 2014 -0500
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,516 +0,0 @@
-# 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
--- a/py/setupdb.py	Thu Jan 02 11:48:15 2014 -0500
+++ b/py/setupdb.py	Thu Jan 02 13:09:48 2014 -0500
@@ -1,7 +1,7 @@
 #!/usr/bin/python3
 # setupdb.py: initializes the database tables used by the rlg system.
 
-import rlgalldb as rlgall
+import rlgall
 import psycopg2
 
 webuser = "webserver"
--- a/py/stats.py	Thu Jan 02 11:48:15 2014 -0500
+++ b/py/stats.py	Thu Jan 02 13:09:48 2014 -0500
@@ -1,6 +1,8 @@
+#!/usr/bin/python3
+
 import math
 import psycopg2
-import rlgalldb as rlgall
+import rlgall
 
 def makeExpPDF(lbd):
   def lpdf(x):
@@ -22,13 +24,13 @@
   total = sum(scores)
   lbd = float(count) / total
   cdf = makeExpCDF(lbd)
-  print "{0}: {1} games, average {2}".format(game.name, count, int(1/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)
+    print("{0:5}: {1:4} {2}".format(i, actual, predicted))
   high = max(scores)
-  print "Max: {0} {1}\n".format(high, 1 - cdf(high))
+  print("Max: {0} {1}\n".format(high, 1 - cdf(high)))
 
 cur.close()
 conn.close()
--- a/web/archive.cgi	Thu Jan 02 11:48:15 2014 -0500
+++ b/web/archive.cgi	Thu Jan 02 13:09:48 2014 -0500
@@ -7,7 +7,7 @@
 import calendar
 from datetime import datetime
 import pytz
-import rlgalldb as rlgall
+import rlgall
 #import cgitb
 
 #cgitb.enable()
--- a/web/recent.cgi	Thu Jan 02 11:48:15 2014 -0500
+++ b/web/recent.cgi	Thu Jan 02 13:09:48 2014 -0500
@@ -3,7 +3,7 @@
 
 import sys
 import time
-import rlgalldb as rlgall
+import rlgall
 
 # It is assumed that sys.stdout uses UTF-8 encoding.  If this is not the case,
 # configure the Web server to set the LC_CTYPE environment variable to a UTF-8
--- a/web/scoring/players/index.cgi	Thu Jan 02 11:48:15 2014 -0500
+++ b/web/scoring/players/index.cgi	Thu Jan 02 13:09:48 2014 -0500
@@ -2,7 +2,7 @@
 
 import os
 import sys
-import rlgalldb as rlgall
+import rlgall
 
 playerdir = "/var/www/lighttpd/scoring/players/"
 linkstr = '<li><a href="./{0}.html">{0}</a></li>\n'