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)