changeset 0:5ba2123d2c20

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/.
author John "Elwin" Edwards <elwin@sdf.org>
date Wed, 25 Jul 2012 21:59:42 -0700
parents
children def7fecbd437
files py/cleandb.py py/recorder.py py/rlgalldb.py py/setupdb.py py/stats.py web/archive.cgi
diffstat 6 files changed, 937 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/py/cleandb.py	Wed Jul 25 21:59:42 2012 -0700
@@ -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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/py/recorder.py	Wed Jul 25 21:59:42 2012 -0700
@@ -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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/py/rlgalldb.py	Wed Jul 25 21:59:42 2012 -0700
@@ -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> -&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'
+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"]