Package: netris
Version: 0.52-3netsoc0.1
Severity: wishlist

The attached patch adds (local) scoring support for netris. These
patches have existed in various forms locally on Trinity Netsoc's
systems for at least 5 years. The attached patch should bring them into
Debian. This particular version has been in use in production for about
5 months, although I only packaged the auxilary files (crontab and
netris-scores) just now.

Notes:
 - We're on Sarge/AMD64 
 - Netris now installed SGID games
 - Additional netris-scores executable (Python)
 - Uses a weekly cronjob to keep a cache of score state.
   This makes a difference when your score file is 4MB
 - 4 different scoring algorithms to choose from
 - Also fixes #345305
 - Also makes the "Press the key for a new game" clearer
 - This patch is against the 0.52-3 orig + debian diffs

You'll probably have to poke debian/changelog to merge this, and add a
dependancy on python.

Yours,
Brian


-- System Information:
Debian Release: 3.1
Architecture: amd64 (x86_64)
Kernel: Linux 2.6.14.3
Locale: [EMAIL PROTECTED], [EMAIL PROTECTED] (charmap=ISO-8859-15)

Versions of packages netris depends on:
ii  libc6                       2.3.2.ds1-22 GNU C Library: Shared libraries an
ii  libncurses5                 5.4-4        Shared libraries for terminal hand

-- no debconf information
diff -urN netris-0.52/curses.c netris-0.52-backup/curses.c
--- netris-0.52/curses.c	2006-06-13 15:12:56.000000000 +0100
+++ netris-0.52-backup/curses.c	2006-06-13 15:14:52.000000000 +0100
@@ -20,6 +20,7 @@
  */
 
 #include "netris.h"
+#include <time.h>
 #include <sys/types.h>
 #include <unistd.h>
 #include <curses.h>
@@ -274,7 +275,7 @@
 		addstr("Waiting for opponent...      ");
 		break;
 	case STATE_WAIT_KEYPRESS:
-		addstr("Press the key for a new game.");
+		addstr("Press the 'n' key for a new game.");
 		break;
 	default:
 		addstr("                             ");
diff -urN netris-0.52/debian/changelog netris-0.52-backup/debian/changelog
--- netris-0.52/debian/changelog	2006-06-13 15:12:56.000000000 +0100
+++ netris-0.52-backup/debian/changelog	2006-06-13 15:12:09.000000000 +0100
@@ -1,3 +1,10 @@
+netris (0.52-3netsoc0.1) unstable; urgency=low
+
+  * Now saves results
+  * Clarified text for 'Press the new key'
+
+ -- Brian Brazil <[EMAIL PROTECTED]>  Sun, 15 Jan 2006 14:29:11 +0000
+
 netris (0.52-3) unstable; urgency=low
 
   * Quote all entries in the menu file.
diff -urN netris-0.52/debian/netris-crontab netris-0.52-backup/debian/netris-crontab
--- netris-0.52/debian/netris-crontab	1970-01-01 01:00:00.000000000 +0100
+++ netris-0.52-backup/debian/netris-crontab	2006-06-13 15:12:09.000000000 +0100
@@ -0,0 +1,2 @@
+#Netris scores cache - rebuild weekly
+6       6       1       *       *       games   /usr/games/netris-scores --buildcaches
diff -urN netris-0.52/debian/postinst netris-0.52-backup/debian/postinst
--- netris-0.52/debian/postinst	2006-06-13 15:12:56.000000000 +0100
+++ netris-0.52-backup/debian/postinst	2006-06-13 15:12:09.000000000 +0100
@@ -5,6 +5,12 @@
 	exit 0
 fi
 
+if [ ! -f /var/games/netris/results ]; then
+	touch /var/games/netris/results
+	chmod 664 /var/games/netris/results
+	chown root.games /var/games/netris/results
+fi
+
 if [ -x /usr/bin/update-menus ]; then
 	update-menus
 fi
diff -urN netris-0.52/debian/rules netris-0.52-backup/debian/rules
--- netris-0.52/debian/rules	2006-06-13 15:12:56.000000000 +0100
+++ netris-0.52-backup/debian/rules	2006-06-13 15:12:09.000000000 +0100
@@ -41,13 +41,17 @@
 	$(checkroot)
 	-rm -rf debian/netris
 	$(INSTALL_DIR) debian/netris
+	$(INSTALL_DIR) -o root -g games -m 2775 debian/netris/var/games/netris
 	cd debian/netris && $(INSTALL_DIR) usr/games usr/share/man/man6 \
 		usr/share/doc/netris/examples
-	$(INSTALL_PROGRAM) netris debian/netris/usr/games
+	$(INSTALL_PROGRAM) -o root -g games -m 2755 netris debian/netris/usr/games
 	$(INSTALL_PROGRAM) sr     debian/netris/usr/games/netris-sample-robot
+	$(INSTALL_FILE) -m755 netris-scores debian/netris/usr/games
 	$(INSTALL_FILE) debian/netris*.6 debian/netris/usr/share/man/man6
 	$(INSTALL_FILE) FAQ robot_desc   debian/netris/usr/share/doc/netris
 	$(INSTALL_FILE) sr.c debian/netris/usr/share/doc/netris/examples
+	$(INSTALL_DIR) debian/netris/etc/cron.d/
+	$(INSTALL_FILE) debian/netris-crontab debian/netris/etc/cron.d/netris
 	gzip -9 debian/netris/usr/share/man/man6/netris*.6 \
 		debian/netris/usr/share/doc/netris/FAQ \
 		debian/netris/usr/share/doc/netris/robot_desc \
diff -urN netris-0.52/game.c netris-0.52-backup/game.c
--- netris-0.52/game.c	2006-06-13 15:12:56.000000000 +0100
+++ netris-0.52-backup/game.c	2006-06-13 15:12:09.000000000 +0100
@@ -21,10 +21,12 @@
 
 #define NOEXT
 #include "netris.h"
+#include <time.h>
 #include <stdlib.h>
 #include <ctype.h>
 #include <string.h>
 #include <sys/types.h>
+#include <sys/file.h>
 #include <netinet/in.h>
 
 enum { KT_left, KT_rotate, KT_right, KT_drop, KT_down,
@@ -365,6 +367,8 @@
 {
 	int initConn = 0, waitConn = 0, ch, done = 0;
 	char *hostStr = NULL, *portStr = NULL;
+	time_t startTime=0, endTime;
+    char *userName;
 	MyEvent event;
 
 	standoutEnable = colorEnable = 1;
@@ -514,7 +518,6 @@
 				}
 			}
 			{
-				char *userName;
 				int len, i;
 
 				userName = getenv("LOGNAME");
@@ -539,6 +542,7 @@
 					if (!isprint(opponentHost[i]))
 						opponentHost[i] = '?';
 			}
+			startTime = time(0);
 			OneGame(0, 1);
 		}
 		else {
@@ -547,8 +551,16 @@
 		}
 		if (wonLast) {
 			won++;
+			if(startTime){
+					endTime = time(0);
+					saveGame(userName,opponentName,opponentHost,startTime,endTime,1);
+			}
 		} else {
 			lost++;
+			if(startTime){
+					endTime = time(0);
+					saveGame(opponentName,userName,opponentHost,startTime,endTime,2);
+			}
 			WaitMyEvent(&event, EM_net);
 		}
 		CloseNet();
@@ -566,6 +578,24 @@
 	return 0;
 }
 
+ExtFunc void saveGame(char* winner, char* loser, char* hostname, time_t start, time_t end, int flag)
+{
+		FILE *f;
+		f = fopen("/var/games/netris/results","a");
+		if(!f){
+				printf("Error opening scoreboard file\n");
+				return;
+		}
+		if(flock(fileno(f),LOCK_EX)){
+				printf("Error locking scoreboard file\n");
+				return;
+		}
+		fseek(f,0,SEEK_END);
+		fprintf(f,"%s %s %s %lu %lu %u\n",winner,loser,hostname,start,end,flag);
+		flock(fileno(f),LOCK_UN);
+		fclose(f);
+}
+
 /*
  * vi: ts=4 ai
  * vim: noai si
diff -urN netris-0.52/netris.h netris-0.52-backup/netris.h
--- netris-0.52/netris.h	2006-06-13 15:12:56.000000000 +0100
+++ netris-0.52-backup/netris.h	2006-06-13 15:12:09.000000000 +0100
@@ -179,6 +179,7 @@
 
 EXT char scratch[1024];
 
+
 extern ShapeOption stdOptions[];
 extern char *version_string;
 
diff -urN netris-0.52/netris-scores netris-0.52-backup/netris-scores
--- netris-0.52/netris-scores	1970-01-01 01:00:00.000000000 +0100
+++ netris-0.52-backup/netris-scores	2006-06-13 15:12:09.000000000 +0100
@@ -0,0 +1,444 @@
+#!/usr/bin/env python
+# Netris-scores -- A scoreboard for netris
+# Copyright Brian Brazil <[EMAIL PROTECTED]> 2006
+# 
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+#
+
+resultsfile = '/var/games/netris/results'
+cachefile = "/var/games/netris/score.cache"
+
+import fcntl
+import os
+import sys
+import pickle
+
+import time
+
+
+class simpleScore:
+	"""
+* Transfer 5% of score from loser to winner
+	"""
+	def __init__(self):
+		self.users = {}
+
+	def getState(self):
+		"""For saving state to cache"""
+		return self.users
+	
+	def setState(self,state):
+		"""For retreiving state from cache"""
+		self.users=state
+
+		
+	def addGame(self,data):
+		(winner,loser,start,end)=data
+		self.createUser(winner)
+		self.createUser(loser)
+		
+		#Other stats
+		w = self.users[winner]
+		l = self.users[loser]
+
+		w['wins']+=1
+		l['loses']+=1
+		
+		#Transfer 5% of loser's score to winner
+		score = l['score']*0.05
+		w['score']+= score
+		l['score']-= score
+
+		#Streak handling
+		l['onstreak']=False;
+		l['curstreak']=0;
+
+		if w['onstreak']:
+			w['curstreak']+=1
+		else:
+			w['onstreak']=True
+			w['curstreak']=1
+
+		if(w['curstreak']>w['maxstreak']):
+			w['maxstreak'] = w['curstreak']
+
+	def createUser(self,user):
+		"""Utility function"""
+		if self.users.has_key(user):
+			return
+		self.users[user]={'wins':0,'loses':0,'score':1000.0,'maxstreak':0,'onstreak':False,'curstreak':0}
+
+	def printScores(self,number):
+		if -1 == number or number > len(self.users):
+			number = len(self.users)
+		data = [(k,self.users[k]) for k in self.users.keys()]
+		data.sort(lambda x,y: cmp(y[1]['score'],x[1]['score']))
+		print
+		print "        Name     Games  Wins   Loses  %Wins  Streak  Score "
+		print "------------------------------------------------------------"
+		for i in range(number):
+			username = data[i][0]
+			u = data[i][1]
+			if u['maxstreak'] == u['curstreak'] and u['curstreak']>=3:
+				streakchar='*'
+			else:
+				streakchar=' '
+
+			print "%-3u | %8s%s|%5u |%5u |%5u |%5.1f%% |%4u |%9.2f"%(i+1,username,streakchar,
+				u['wins']+u['loses'],
+				u['wins'],u['loses'],
+				(u['wins']*100.0)/(u['loses']+u['wins']),
+				u['maxstreak'],
+				u['score'])
+		print "------------------------------------------------------------"
+		print
+
+class scoreDecay(simpleScore):
+	"""
+* For every 2 weeks you don't play you drop a place and 
+	lose 5% score
+* Transfer 5% of score from loser to winner
+	"""
+	def __init__(self):
+		simpleScore.__init__(self)
+		self.numDays = 14
+	
+	def addGame(self,data):
+		(winner,loser,start,end)=data
+		simpleScore.createUser(self,winner)
+		simpleScore.createUser(self,loser)
+
+		#For every day each hasn't played, drop them by one rank
+		self.decayUser(winner,start)
+		self.decayUser(loser,start)
+
+		self.users[winner]['last_played'] = end
+		self.users[loser]['last_played'] = end
+		
+		simpleScore.addGame(self,data)
+		
+	def decayUser(self,user,time):
+		#User and what the time is "now"
+		u = self.users[user]
+		if u.has_key('last_played'):
+			pos = (time - u['last_played']) / (86400 * self.numDays)
+			if 0 == pos or time < u['last_played']: 
+				return
+			#Take 5% of score for each pos
+			#and redistribute
+			#return
+			diff = u['score'] * ((1.05 ** pos) - 1)
+			if (u['score'] - diff) < 1:
+				diff = u['score'] - 1
+			if diff < 0: 
+				return
+			u['score'] -= diff
+			diff /= len(self.users)
+			for i in self.users.values():
+				i['score'] += diff
+				
+	
+	def printScores(self,number):
+		t = time.time()
+		
+		#Allow for users who need to be dacyed, but haven't played recently
+		#Do users with the biggest scores first
+		data = [(k,self.users[k]) for k in self.users.keys()]
+		data.sort(lambda x,y: cmp(y[1]['score'],x[1]['score']))
+		for i in [i[0] for i in data]:
+			self.decayUser(i,t)
+			
+		simpleScore.printScores(self,number)
+
+class simpleLadder(simpleScore):
+	"""
+* If you beat the person directly above you in the ladder, 
+	you swap places
+* If you beat a person further above you, you move half way
+	up to them
+* If you beat a person below you there is no change
+* Transfer 5% of score from loser to winner
+* Socre has no effect on ranking
+	"""
+	def __init__(self):
+		simpleScore.__init__(self)
+		self.ladder = []
+
+	def getState(self):
+		"""For saving state to cache"""
+		return (self.users,self.ladder)
+	
+	def setState(self,state):
+		"""For retreiving state from cache"""
+		(self.users,self.ladder)=state
+	
+	def createUser(self,user):
+		"""Utility function"""
+		if self.users.has_key(user):
+			return
+		simpleScore.createUser(self,user)
+		self.ladder.append(user)
+
+		
+	def addGame(self,data):
+		(winner,loser,start,end)=data
+		self.createUser(winner)
+		self.createUser(loser)
+		simpleScore.addGame(self,data)
+
+		#Ladder handling
+		wpos = self.ladder.index(winner)
+		lpos = self.ladder.index(loser)
+		if wpos-1 == lpos:
+			#One above, switch places
+			del self.ladder[wpos]
+			self.ladder.insert(lpos,winner)
+		elif wpos > lpos:
+			#Many above, move half towards
+			del self.ladder[wpos]
+			self.ladder.insert((wpos+lpos)/2,winner)
+		#Otherwise leave as is
+
+	def printScores(self,number):
+		if -1 == number or number > len(self.ladder):
+			number = len(self.ladder)
+		print
+		print "        Name     Games  Wins   Loses  %Wins  Streak  Score "
+		print "------------------------------------------------------------"
+		for i in range(number):
+			username = self.ladder[i]
+			u = self.users[username]
+			if u['maxstreak'] == u['curstreak'] and u['curstreak']>=3:
+				streakchar='*'
+			else:
+				streakchar=' '
+
+			print "%-3u | %8s%s|%5u |%5u |%5u |%5.1f%% |%4u |%9.2f"%(i+1,username,streakchar,
+				u['wins']+u['loses'],
+				u['wins'],u['loses'],
+				(u['wins']*100.0)/(u['loses']+u['wins']),
+				u['maxstreak'],
+				u['score'])
+		print "------------------------------------------------------------"
+		print
+
+class ladderDecay(simpleLadder):
+	"""
+* If you beat the person directly above you in the ladder, 
+	you swap places
+* If you beat a person further above you, you move half way
+	up to them
+* If you beat a person below you there is no change
+* For every 2 weeks you don't play you drop a place and 
+	lose 5% score
+* Transfer 5% of score from loser to winner
+* Socre has no effect on ranking
+	"""
+	def __init__(self):
+		simpleLadder.__init__(self)
+		self.numDays = 14
+
+	def addGame(self,data):
+		(winner,loser,start,end)=data
+		simpleLadder.createUser(self,winner)
+		simpleLadder.createUser(self,loser)
+
+		#For every day each hasn't played, drop them by one rank
+		self.decayUser(winner,start)
+		self.decayUser(loser,start)
+
+		self.users[winner]['last_played'] = end
+		self.users[loser]['last_played'] = end
+		
+		simpleLadder.addGame(self,data)
+		
+	def decayUser(self,user,time):
+		#User and what the time is "now"
+		u = self.users[user]
+		upos = self.ladder.index(user)
+		if u.has_key('last_played'):
+			pos = (time - u['last_played']) / (86400 * self.numDays)
+			if 0 == pos or time < u['last_played']: 
+				return
+			lpos = int(min(pos+upos,len(self.ladder)))
+			del self.ladder[upos]
+			self.ladder.insert(lpos,user)
+
+			#Take 5% of score for each pos
+			#and redistribute
+			diff = u['score'] * ((1.05 ** pos) - 1)
+			if (u['score'] - diff) < 1:
+				diff = u['score'] - 1
+			u['score'] -= diff
+			diff /= len(self.users)
+			for i in self.users.values():
+				i['score'] += diff
+				
+	
+	def printScores(self,number):
+		saveLadder = self.ladder[:]
+
+		t = time.time()
+
+		#Allow for users who need to be dacyed, but haven't played recently
+		for i in saveLadder:
+			self.decayUser(i,t)
+			
+		simpleLadder.printScores(self,number)
+
+		#Don't accidentally alter state
+		self.ladder = saveLadder
+		
+
+###Core code is below this point
+
+def processResults(callback,unmatched,pos):
+
+	f = open(resultsfile, 'r')
+	f.seek(pos);
+	fcntl.flock(f,fcntl.LOCK_EX)
+	while True:
+		line = f.readline()
+		if '' == line: break
+		#Winner, loser, starttime, endtime
+		parts = line.split(' ');
+		for i in (3,4,5):
+			parts[i] = int(parts[i])
+
+		#Playing with yourself
+		if parts[0] == parts[1]:
+			continue
+		
+		for i in range(len(unmatched)):
+			if matches(unmatched[i],parts):
+				del unmatched[i]
+				callback(parts[0:2] + parts[3:5])
+				break
+		else:
+			unmatched.append(parts)
+
+		#Keep the list small
+		if len(unmatched) > 5:
+			del unmatched[0]
+
+	pos = f.tell();
+	f.close()
+	return (unmatched,pos)
+
+def matches(a,b):
+	return (a[0] == b[0] and 
+		a[1] == b[1] and 
+		abs(a[3]-b[3]) <=5 and 
+		abs(a[4]-b[4]) <=5 and
+		((1 == a[5] and 2 == b[5]) 
+		or
+		(2 == a[5] and 1 == b[5]))
+		)
+
+def loadCache(scores,algorithm):
+	try: 
+		state = pickle.load(open(cachefile + "." + algorithm))
+		scores.setState(state['data'])
+		unmatched = state['unmatched']
+		loc = state['pos']
+	except:
+		print "Error opening cache - calculating from scratch"
+		unmatched=[]
+		loc = 0
+	return (unmatched,loc)
+	
+def updateState(scores,state):
+	(unmatched,loc) = state
+	return processResults(scores.addGame,unmatched,loc)
+
+def saveCache(scores,algorithm,state):
+	(unmatched,loc) = state
+	newstate = {'pos':loc,'unmatched':unmatched,'data':scores.getState()}
+	pickle.dump(newstate,open(cachefile + "." + algorithm,'w'))
+
+
+if '__main__' == __name__:
+	import getopt
+	usage = """
+netris-scores {-h|--help}
+netris-scores --buildcaches [--fresh]
+netris-scores [-l|--long] [-a algorithm|--algo=algorithm] [-f|--fresh]
+
+-l --long Display whole scoreboard
+-a --algo Specify scoring algorithm to use, --algo=list will list
+          all known algorithms
+-f --fresh Calculate from scratch - don't use the caches
+
+Specifying 'list' as the algorighm will list all known algorithms
+"""
+
+	algorithms=['ladderDecay','simpleScore','simpleLadder','scoreDecay']
+
+	try:
+		opts, args = getopt.getopt(sys.argv[1:], "hla:f", ["help", "long", "algo=", "buildcaches", "fresh"])
+	except getopt.GetoptError:
+		print usage
+		sys.exit(2)
+	algorithm = algorithms[0];
+	number = 15
+	fresh = False
+	rebuild = False
+	for o, a in opts:
+		if o in ("-l","--long"):
+			number = -1
+		if o in ("-a","--algo"):
+			if 'list' == a:
+				print "Known algorthms:"
+				algorithms.sort()
+				for i in algorithms:
+					print
+					print "--"+i+"--"
+					print locals()[i].__doc__
+				sys.exit()
+			if a not in algorithms:
+				print "Unknown algorithm"
+				sys.exit()
+			else:
+				algorithm = a
+		if o in ("-h", "--help"):
+			print usage
+			sys.exit()
+		if o in ("-f", "--fresh"):
+			fresh = True
+		if o == "--buildcaches":
+			rebuild = True
+
+	if rebuild:
+		for i in algorithms:
+			scores = locals()[i]();
+			if fresh:
+				state=([],0)
+			else:
+				state=loadCache(scores,i)
+			state = updateState(scores,state)
+			saveCache(scores,i,state)
+		sys.exit()
+
+
+	#Update cache
+	scores = locals()[algorithm]()
+	
+	if fresh:
+		state=([],0)
+	else:
+		state=loadCache(scores,algorithm)
+	updateState(scores,state)
+
+	scores.printScores(number)

Reply via email to