Mercurial > hg > rlgallery-misc
comparison py/rlgall.py @ 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 | py/rlgalldb.py@7303535b5a5d |
| children | 86b616d88020 |
comparison
equal
deleted
inserted
replaced
| 32:05a4afbe6299 | 33:25843238434a |
|---|---|
| 1 # rlgall.py | |
| 2 # Module for the Roguelike Gallery, using a postgres database | |
| 3 # Requires Python 3.3 | |
| 4 | |
| 5 import os | |
| 6 import psycopg2 | |
| 7 from datetime import datetime | |
| 8 import pytz | |
| 9 | |
| 10 # Configuration | |
| 11 logdir = "/var/dgl/var/games/roguelike/" | |
| 12 webdir = "/var/www/lighttpd/scoring/" | |
| 13 ppagename = webdir + "players/{0}.html" | |
| 14 hpagename = webdir + "highscores.html" | |
| 15 | |
| 16 # HTML fragments for templating | |
| 17 phead = """<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> | |
| 18 <html><head> | |
| 19 <title>{0}</title> | |
| 20 <link rel="stylesheet" href="/scoring/scores.css" type="text/css"> | |
| 21 </head> | |
| 22 """ | |
| 23 | |
| 24 ptop = """<body> | |
| 25 <h1>Yendor Guild</h1> | |
| 26 """ | |
| 27 | |
| 28 navtop = '<div class="nav"><a href="/">rlgallery.org</a> -> {0}</div>\n' | |
| 29 navscore = '<div class="nav"><a href="/">rlgallery.org</a> -> \ | |
| 30 <a href="/scoring/">Scores</a> -> {0}</div>\n' | |
| 31 navplayer = '<div class="nav"><a href="/">rlgallery.org</a> -> \ | |
| 32 <a href="/scoring/">Scores</a> -> <a href="/scoring/players/">Players</a> \ | |
| 33 -> {0}</div>' | |
| 34 | |
| 35 pti = '<h2>{0}</h2>\n' | |
| 36 | |
| 37 secthead = '<h3>{0}</h3>\n' | |
| 38 tblhead = '<div class="stable">\n' | |
| 39 rowstart = '<div class="sentry">\n' | |
| 40 rowend = '</div>\n' | |
| 41 cell = ' <span class="sdata">{0}</span>\n' | |
| 42 rcell = ' <span class="sdatar">{0}</span>\n' | |
| 43 hcell = ' <span class="shdata">{0}</span>\n' | |
| 44 tblend = '</div>\n' | |
| 45 pend = "</body></html>\n" | |
| 46 | |
| 47 # This would be more useful if we had to do translation | |
| 48 headerbook = {"endt":"End time", "score":"Score", "name":"Name", "xl":"XL", | |
| 49 "fate":"Fate", "rank":"Rank", "game":"Game", "class": "Class"} | |
| 50 # Queries for the games table | |
| 51 offselstr = "SELECT offbytes FROM games WHERE gname = %s;" | |
| 52 newoffstr = "UPDATE games SET offbytes = %s WHERE gname = %s;" | |
| 53 | |
| 54 def getconn(): | |
| 55 "Returns a database connection, or None if the connection fails." | |
| 56 try: | |
| 57 conn = psycopg2.connect("dbname=rlg") | |
| 58 except psycopg2.OperationalError: | |
| 59 return None | |
| 60 return conn | |
| 61 | |
| 62 def recnameToTS(filename): | |
| 63 pattern = "%Y-%m-%d.%H:%M:%S.ttyrec" | |
| 64 try: | |
| 65 dt = datetime.strptime(filename, pattern).replace(tzinfo=pytz.utc) | |
| 66 return dt | |
| 67 except ValueError: | |
| 68 return None | |
| 69 | |
| 70 def ttyreclink(text, name, game, gtime): | |
| 71 "Returns a link to the ttyrec archivist" | |
| 72 lstr = '<a href="/archive.cgi?name={0};game={1};time={2}">{3}</a>' | |
| 73 return lstr.format(name, game, gtime, text) | |
| 74 | |
| 75 def playerlink(name): | |
| 76 "Returns a link to a player's page" | |
| 77 lstr = '<a href="/scoring/players/' + name + '.html">' + name + '</a>' | |
| 78 return lstr | |
| 79 | |
| 80 def linktoArchive(entry): | |
| 81 "Takes an entry dict and returns a link to the ttyrec archivist." | |
| 82 lstr = '<a href="/archive.cgi?name={0};game={1};time={2}">{3}</a>' | |
| 83 linktext = entry["endt"].strftime("%Y/%m/%d %H:%M:%S") | |
| 84 stamp = int(entry["endt"].timestamp()) | |
| 85 return lstr.format(entry["name"], entry["game"].uname, stamp, linktext) | |
| 86 | |
| 87 def maketablerow(cells, isheader=None): | |
| 88 "Takes a list of strings and returns a HTML table row with each string \ | |
| 89 in its own cell. isheader will make them header cells, obviously." | |
| 90 if isheader: | |
| 91 celler = hcell | |
| 92 else: | |
| 93 celler = cell | |
| 94 rowstr = rowstart | |
| 95 for entry in cells: | |
| 96 rowstr = rowstr + celler.format(entry) | |
| 97 rowstr = rowstr + rowend | |
| 98 return rowstr | |
| 99 | |
| 100 def printTable(entries, fields, of): | |
| 101 "Takes a list of entry dicts and a list of field strings and writes a \ | |
| 102 HTML table to of." | |
| 103 of.write(tblhead) | |
| 104 clist = [] | |
| 105 for field in fields: | |
| 106 if field in headerbook: | |
| 107 clist.append(headerbook[field]) | |
| 108 else: | |
| 109 clist.append("{0}".format(field)) | |
| 110 of.write(maketablerow(clist, True)) | |
| 111 rnum = 0 | |
| 112 for i, entry in enumerate(entries): | |
| 113 clist = [] | |
| 114 for field in fields: | |
| 115 if field == "rank": | |
| 116 clist.append(("{0}".format(i + 1), rcell)) | |
| 117 elif field in entry: | |
| 118 thing = entry[field] | |
| 119 if field == "game": | |
| 120 clist.append((thing.name, cell)) | |
| 121 elif field == "xl" or field == "score": # Numerics | |
| 122 clist.append((str(thing), rcell)) | |
| 123 elif field == "name": | |
| 124 clist.append((playerlink(thing), cell)) | |
| 125 elif field == "fate": | |
| 126 clist.append((thing, cell)) | |
| 127 elif field == "endt": | |
| 128 clist.append((linktoArchive(entry), cell)) | |
| 129 else: | |
| 130 clist.append(("{0}".format(thing), cell)) | |
| 131 else: | |
| 132 clist.append(("N/A", cell)) | |
| 133 rowstr = rowstart + "".join([ t.format(s) for (s, t) in clist ]) + rowend | |
| 134 of.write(rowstr) | |
| 135 of.write(tblend) | |
| 136 return | |
| 137 | |
| 138 class Game: | |
| 139 def __init__(self, name, uname): | |
| 140 pass | |
| 141 def logtoDict(self, entry): | |
| 142 "Processes a log entry string, returning a dict." | |
| 143 ndict = {"game": self} | |
| 144 entrylist = entry.strip().split(self.logdelim, len(self.logspec) - 1) | |
| 145 for item, value in zip(self.logspec, entrylist): | |
| 146 if self.sqltypes[item] == "int": | |
| 147 ndict[item] = int(value) | |
| 148 if self.sqltypes[item] == "bool": | |
| 149 ndict[item] = bool(int(value)) | |
| 150 elif self.sqltypes[item] == "timestamptz": | |
| 151 ndict[item] = datetime.fromtimestamp(int(value), pytz.utc) | |
| 152 else: | |
| 153 ndict[item] = value | |
| 154 return ndict | |
| 155 def getEntryDicts(self, entfile, entlist): | |
| 156 "Reads logfile entries from entfile, interprets them according to the \ | |
| 157 instructions in self.logspec/logdelim, and adds them as dicts to entlist." | |
| 158 while True: | |
| 159 nextentry = entfile.readline() | |
| 160 if not nextentry: | |
| 161 break | |
| 162 if nextentry[-1] != '\n': | |
| 163 break | |
| 164 entlist.append(self.logtoDict(nextentry)) | |
| 165 return | |
| 166 def loadnew(self): | |
| 167 conn = getconn() | |
| 168 if conn == None: | |
| 169 return [] | |
| 170 cur = conn.cursor() | |
| 171 # Get the previous offset | |
| 172 cur.execute(offselstr, [self.uname]) | |
| 173 offset = cur.fetchone()[0] | |
| 174 newlist = [] | |
| 175 try: | |
| 176 scr = open(self.scores, encoding="utf-8") | |
| 177 scr.seek(offset) | |
| 178 self.getEntryDicts(scr, newlist) | |
| 179 except IOError: | |
| 180 noffset = offset # Can't read anything, assume no new games | |
| 181 else: | |
| 182 noffset = scr.tell() | |
| 183 scr.close() | |
| 184 cur.execute(newoffstr, [noffset, self.uname]) | |
| 185 # The new players must already be added to the players table. | |
| 186 updatenames = set([ e["name"] for e in newlist ]) | |
| 187 self.postprocess(newlist) | |
| 188 self.putIntoDB(newlist, conn) | |
| 189 cur.close() | |
| 190 conn.close() | |
| 191 return updatenames | |
| 192 def postprocess(self, gamelist): | |
| 193 "Default postprocessing function: adds start time and ttyrec list" | |
| 194 names = set([ e["name"] for e in gamelist ]) | |
| 195 conn = getconn() | |
| 196 if conn == None: | |
| 197 return [] | |
| 198 cur = conn.cursor() | |
| 199 for nameF in names: | |
| 200 # Get all this player's games ordered by time | |
| 201 itsEntries = [ entry for entry in gamelist if entry["name"] == nameF ] | |
| 202 itsEntries.sort(key=lambda e: e["endt"]) | |
| 203 # Find the end time of the latest game already in the db | |
| 204 tquery = "SELECT endt FROM {0} WHERE name = %s ORDER BY endt DESC LIMIT 1;".format(self.uname) | |
| 205 cur.execute(tquery, [nameF]) | |
| 206 result = cur.fetchone() | |
| 207 if result: | |
| 208 prev = result[0] | |
| 209 else: | |
| 210 prev = datetime.fromtimestamp(0, pytz.utc); | |
| 211 ttyrecdir = "/var/dgl/dgldir/ttyrec/{0}/{1}/".format(nameF, self.uname) | |
| 212 allfilekeys = [ (recnameToTS(f), f) for f in os.listdir(ttyrecdir) ] | |
| 213 vfilekeys = [ e for e in allfilekeys if e[0] > prev ] | |
| 214 vfilekeys.sort(key=lambda e: e[0]) | |
| 215 # Now determine startt and ttyrecs for each game | |
| 216 for i in range(0, len(itsEntries)): | |
| 217 if i == 0: | |
| 218 lowlim = prev | |
| 219 else: | |
| 220 lowlim = itsEntries[i-1]["endt"] | |
| 221 hilim = itsEntries[i]["endt"] | |
| 222 recs = [ k[1] for k in vfilekeys if lowlim <= k[0] < hilim ] | |
| 223 itsEntries[i]["startt"] = recnameToTS(recs[0]) | |
| 224 itsEntries[i]["ttyrecs"] = recs | |
| 225 cur.close() | |
| 226 conn.close() | |
| 227 def putIntoDB(self, dictlist, conn): | |
| 228 cur = conn.cursor() | |
| 229 cur.executemany(self.insertq, [ d for d in dictlist if d["game"] == self ]) | |
| 230 conn.commit() | |
| 231 cur.close() | |
| 232 return | |
| 233 def tablerecent(self, of): | |
| 234 "Prints the most recent games from the logfile, NOT the database." | |
| 235 newest = [] | |
| 236 try: | |
| 237 scr = open(self.scores, encoding="utf-8") | |
| 238 except FileNotFoundError: | |
| 239 pass | |
| 240 else: | |
| 241 # Text streams don't support random seeking. | |
| 242 try: | |
| 243 scr.buffer.seek(self.lookback, 2) | |
| 244 except OSError: | |
| 245 scr.buffer.seek(0) # The file wasn't that long, start at the beginning | |
| 246 if scr.buffer.tell() != 0: | |
| 247 scr.buffer.readline() # Throw away the incomplete line | |
| 248 self.getEntryDicts(scr, newest) | |
| 249 newest.reverse() | |
| 250 scr.close() | |
| 251 of.write(secthead.format(self.name)) | |
| 252 if not newest: | |
| 253 of.write("<div>No one has braved this dungeon yet.</div>\n") | |
| 254 else: | |
| 255 printTable(newest, self.fields, of) | |
| 256 return | |
| 257 # End Game class definition | |
| 258 | |
| 259 class RogueGame(Game): | |
| 260 def __init__(self, name, uname, suffix): | |
| 261 self.name = name | |
| 262 self.uname = uname | |
| 263 self.scores = logdir + uname + ".log" | |
| 264 self.logspec = ["endt", "score", "name", "xl", "fate"] | |
| 265 self.sqltypes = {"endt": "timestamptz", "score": "int", | |
| 266 "name": "varchar(20)", "xl": "int", "fate": "text", | |
| 267 "ttyrecs": "text ARRAY", "startt": "timestamptz"} | |
| 268 self.logdelim = " " | |
| 269 self.lookback = -1500 | |
| 270 # Construct the insert query | |
| 271 fields = self.sqltypes.keys() | |
| 272 colspec = ", ".join(fields) | |
| 273 valspec = ", ".join([ "%({})s".format(c) for c in fields ]) | |
| 274 self.insertq = "INSERT INTO {0} ({1}) VALUES ({2});".format(self.uname, | |
| 275 colspec, valspec) | |
| 276 # Class variables, used by some methods | |
| 277 fields = ["name", "score", "xl", "fate", "endt"] | |
| 278 rankfields = ["rank", "score", "name", "xl", "fate", "endt"] | |
| 279 pfields = ["score", "xl", "fate", "endt"] | |
| 280 def getRecent(self, n=20): | |
| 281 "Gets the n most recent games out of the database, returning a list \ | |
| 282 of dicts." | |
| 283 try: | |
| 284 n = int(n) | |
| 285 except (ValueError, TypeError): | |
| 286 return [] | |
| 287 if n <= 0: | |
| 288 return [] | |
| 289 dictlist = [] | |
| 290 conn = psycopg2.connect("dbname=rlg") | |
| 291 cur = conn.cursor() | |
| 292 qstr = "SELECT endt, score, name, xl, fate, startt FROM {0} ORDER BY endt DESC \ | |
| 293 LIMIT %s;".format(self.uname) | |
| 294 cur.execute(qstr, [n]) | |
| 295 for record in cur: | |
| 296 # This should be less hardcodish | |
| 297 ndict = {"game": self} | |
| 298 ndict["endt"] = record[0] | |
| 299 ndict["score"] = record[1] | |
| 300 ndict["name"] = record[2] | |
| 301 ndict["xl"] = record[3] | |
| 302 ndict["fate"] = record[4] | |
| 303 ndict["startt"] = record[5] | |
| 304 dictlist.append(ndict) | |
| 305 cur.close() | |
| 306 conn.close() | |
| 307 return dictlist | |
| 308 def getHigh(self, n=10, offset=0): | |
| 309 "Gets the n highest scores (starting at offset) from the database, \ | |
| 310 returning a list of dicts." | |
| 311 qstr = "SELECT endt, score, name, xl, fate, startt FROM {0} ORDER BY score DESC ".format(self.uname) | |
| 312 qvals = [] | |
| 313 try: | |
| 314 n = int(n) | |
| 315 except (ValueError, TypeError): | |
| 316 return [] | |
| 317 if n <= 0: | |
| 318 return [] | |
| 319 qstr += " LIMIT %s" | |
| 320 qvals.append(n) | |
| 321 try: | |
| 322 offset = int(offset) | |
| 323 except (ValueError, TypeError): | |
| 324 return [] | |
| 325 if n > 0: | |
| 326 qstr += " OFFSET %s" | |
| 327 qvals.append(offset) | |
| 328 qstr += ";" | |
| 329 conn = psycopg2.connect("dbname=rlg") | |
| 330 cur = conn.cursor() | |
| 331 cur.execute(qstr, qvals) | |
| 332 dictlist = [] | |
| 333 for record in cur: | |
| 334 ndict = {"game": self} | |
| 335 ndict["endt"] = record[0] | |
| 336 ndict["score"] = record[1] | |
| 337 ndict["name"] = record[2] | |
| 338 ndict["xl"] = record[3] | |
| 339 ndict["fate"] = record[4] | |
| 340 ndict["startt"] = record[5] | |
| 341 dictlist.append(ndict) | |
| 342 cur.close() | |
| 343 conn.close() | |
| 344 return dictlist | |
| 345 def getPlayer(self, player): | |
| 346 "Gets all player's games from the database." | |
| 347 qstr = "SELECT endt, score, name, xl, fate, startt FROM " + self.uname + " WHERE name = %s;" | |
| 348 conn = getconn() | |
