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> -&gt; {0}</div>\n'
29 navscore = '<div class="nav"><a href="/">rlgallery.org</a> -&gt; \
30 <a href="/scoring/">Scores</a> -&gt; {0}</div>\n'
31 navplayer = '<div class="nav"><a href="/">rlgallery.org</a> -&gt; \
32 <a href="/scoring/">Scores</a> -&gt; <a href="/scoring/players/">Players</a> \
33 -&gt; {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()