@@ +tally system @@ Revision: 1999-03-11 @@ Copyright (c) 1999 Alan Schwartz @@ Released under the same terms as PennMUSH @@ @@ This is code that can keep track of win-lose-draw records of players @@ for an arbitrary set of 2-player games and provide standings and ratings. @@ It requires no permissions. It uses @mail. @@ @@ To install it, upload this file piped through 'mpp', which is available @@ from ftp.pennmush.org @@ @@ Each game that you want the system to track needs its own database @@ object, which should live inside the command object. In addition, the @@ @VD attribute on the command object should be a list of alternating @@ game names and their database dbrefs. @@ The system comes with a scrabble database already installed as an example. @@ @@ Once installed, use +tally/help for a command list. @fo me=@va me=[create(Tally Commands,10)] @fo me=@vf [v(va)]=[create(Tally Functions,10)] @fo me=@vc [v(va)]=[create(Confirmations,10)] @fo me=@vo [xget(v(va),VF)]=[v(va)] @cpattr [v(va)]/VF=me/VF @dol vf vc={ @lock [xget(v(va),##)]=me; @link [xget(v(va),##)]=[v(va)]; @tel [xget(v(va),##)]=[v(va)] } @fo me=@lock [v(va)]=me @fo me=@link [v(va)]=#2 @set [v(va)]=!no_command @set [v(va)]=safe @set [v(vf)]=safe @desc [v(va)]=Commands for the tally/rating system. @desc [v(vf)]=Functions for the tally/rating system. @fo me=@vd me=[create(Scrabble Database,10)] @fo me=@vd [v(va)]=scrabble [v(vd)] @lock [v(vd)]=me @link [v(vd)]=[v(va)] @tel [v(vd)]=[v(va)] @@ Commands: @@ +tally beat|tied at @@ +tally/confirm @@ +tally/deny @@ +tally/short at @@ +tally/long at @@ +tally/rating at @@ +tally/help @@ can be a player name or *. can be any registered game. @@ @startup [v(va)]=@drain me; @notify me @tr [v(va)]/startup &do_tally_beat [v(va)] = $+tally * beat * at *: @wait me = {@select 0 = [u(%VF/isgame,%2)], { @pemit %#=I don't know a game called %2.; @notify me }, [comp(setr(1,pmatch(%0)),#-1)], { @pemit %#=I don't know a player named %0.; @notify me }, [comp(setr(2,pmatch(%1)),#-1)], { @pemit %#=I don't know a player named %1.; @notify me }, [member(%q1 %q2,%#)], { @pemit %#=You can only tally games you played in.; @notify me }, { &C_[setr(3,secs())] %VC=%# %q1 %q2 W %2; @pemit %#=You register the game. Waiting for confirmation.; @mail [setdiff(%q1 %q2,%#)]=Confirmation request/ { %N has reported that you played a game of %2 with %o on or around [time()]. %S reports that [name(%q1)] won the game. If you agree with this report, type:%r %t+tally/confirm %q3%rIf you don't agree, type:%r %t+tally/deny %q3 }; @notify me }} &do_tally_tied [v(va)] = $+tally * tied * at *: @wait me = {@select 0 = [u(%VF/isgame,%2)], { @pemit %#=I don't know a game called %2.; @notify me }, [comp(setr(1,pmatch(%0)),#-1)], { @pemit %#=I don't know a player named %0.; @notify me }, [comp(setr(2,pmatch(%1)),#-1)], { @pemit %#=I don't know a player named %1.; @notify me }, [member(%q1 %q2,%#)], { @pemit %#=You can only tally games you played in.; @notify me }, { &C_[setr(3,secs())] %VC=%# %q1 %q2 D %2; @pemit %#=You register the game. Waiting for confirmation.; @mail [setdiff(%q1 %q2,%#)]=Confirmation request/ { %N has reported that you played a game of %2 with %o on or around [time()]. %S reports that the game was a tie. If you agree with this report, type:%r %t+tally/confirm %q3%rIf you don't agree, type:%r %t+tally/deny %q3 }; @notify me }} &do_tally_confirm [v(va)] = $+tally/confirm *: @wait me = { @select 0=[hasattr(%VC,C_%0)], { @pemit %#=I don't know that confirmation code.; @notify me }, [setq(0,get(%VC/C_%0))][setq(1,first(%q0))][setq(2,extract(%q0,2,2))] [not(comp(%#,setdiff(%q2,%q1)))], { @pemit %#=You're not allowed to confirm that game.; @notify me }, [setq(3,last(%q0))][setq(4,extract(%VD,add(1,match(%VD,%q3)),1))] [setq(5,first(%q2))][setq(6,last(%q2))] [match(lcon(me),%q4)], { @pemit %#=Oops. Something's bad. Contact [name(owner(me))] with your confirmation number.; @notify me }, { &R_%q5 %q4 = [ulocal(%VF/rating[extract(%q0,4,1)],%q3,%q5,%q6)] [rest(xget(%q4,R_%q5))] [extract(%q0,4,1)]%q6; &R_%q6 %q4 = [ulocal(%VF/rating[edit(extract(%q0,4,1),W,L)],%q3,%q6,%q5)] [rest(xget(%q4,R_%q6))] [edit(extract(%q0,4,1),W,L)]%q5; @mail %q5 %q6=Confirmation notice/ { %N has confirmed the results of the game of %q3 played on or around [convsecs(%0)] between [name(%q5)] and [name(%q6)] and standings have been updated. Thanks! }; @wipe %VC/C_%0; @notify me }} &do_tally_deny [v(va)] = $+tally/deny *: @wait me = {@select 0=[hasattr(%VC,C_%0)], { @pemit %#=I don't know that confirmation code.; @notify me }, [setq(0,get(%VC/C_%0))][setq(1,first(%q0))][setq(2,extract(%q0,2,2))] [setq(3,last(%q0))][setq(5,first(%q2))][setq(6,last(%q2))] [not(comp(%#,setdiff(%q2,%q1)))], { @pemit %#=You're not allowed to deny that game.; @notify me }, { @mail %q5 %q6=Denial notice/ { %N has denied the results of the game of %q3 played on or around [convsecs(%0)] between [name(%q5)] and [name(%q6)]. Standings have not been updated. }; @wipe %VC/C_%0; @notify me }} &do_tally_help [v(va)] = $+tally/help: @pemit %#={ The +tally commands maintain standings and ratings for MUSH games. Currently, +tally knows the following games: [ulocal(%vf/games)]%r Commands to register a game result:%r %t+tally beat at %r %t+tally tied at %r These commands send @mail to the other player asking them to confirm or deny the game result. If the result is confirmed, it is recorded.%r%r Commands to see results:%r %t+tally/short at %r %t+tally/long at %r %t+tally/rating at %r These commands show the win-lose-draw standings, list of games played, and game rating of a player. Rating uses a simplified version of the Elo system used for chess ratings. A new player has rating 1000. You get more points for beating better players. } @@ +tally/short at @@ +tally/long at @@ +tally/rating at @@ can be a player name or *. can be any registered game. &do_tally_rating [v(va)] = $+tally/rating * at *: @select 0 = [u(%VF/isgame,%1)], { @pemit %#=I don't know a game called %1. }, [not(strmatch(%0,-))], { @pemit %#=Ratings at %1: [iter(ulocal(%VF/munge_rating,%1), %r[ljust(#@.,3)] [ljust(name(##),15)] [ljust(ulocal(%VF/rating,%1,##),5)] [ulocal(%VF/veryshort,%1,##)] )] }, [comp(setr(1,pmatch(%0)),#-1)], { @pemit %#=I don't know a player named %0. }, { @pemit %#=[name(%q1)]'s rating at %1 ([ulocal(%VF/num_games,%1,%q1)] games played) is [ulocal(%VF/rating,%1,%q1)] } &do_tally_short [v(va)] = $+tally/short * at *: @select 0 = [u(%VF/isgame,%1)], { @pemit %#=I don't know a game called %1. }, [not(strmatch(%0,-))], { @pemit %#=Standings at %1: [iter(ulocal(%VF/munge_rating,%1), %r[ljust(name(##),15)] [ulocal(%VF/short,%1,##)])] }, [comp(setr(1,pmatch(%0)),#-1)], { @pemit %#=I don't know a player named %0. }, { @pemit %#=[name(%q1)]'s record at %1 is [ulocal(%VF/short,%1,%q1)] } &do_tally_long [v(va)] = $+tally/long * at *: @select 0 = [u(%VF/isgame,%1)], { @pemit %#=I don't know a game called %1. }, [comp(setr(1,pmatch(%0)),#-1)], { @pemit %#=I don't know a player named %0. }, { @pemit %#=[name(%q1)]'s detailed record at %1 ([ulocal(%VF/veryshort,%1,%q1)]):%r [ulocal(%VF/long,%1,%q1)] } @@ Functions: @@ short - output short standings by counting w/l/d for a player %1 @@ at game %0 @@ long - output long standings by sorting by w/l/d for player %1 @@ at game %0 and reporting who they w/l/d'd with. @@ rating - extract rating for player %1 at game %0 @@ competitors - list everyone who's played %0 @@ isgame - is %0 a valid game we know? @@ games - list all games we know @@ num_games - how many games of %0 has %1 played? @@ rating_diff - Abs diff in ratings between player %1 and %2 at game %0 @@ rating_prob - Convert an abs diff to a prob between .5 and 1 @@ rating_mult - Multiplier for player %1 at game %0 @@ rating_ge - Is %1's rating at game %0 >= %2's rating? @@ ratingW - Given players %1 and %2 at game %0, where %1 beat %2, compute @@ %1's new rating. @@ ratingL - Given players %1 and %2 at game %0, where %1 lost to %2, compute @@ %1's new rating. @@ ratingD - Given players %1 and %2 at game %0, where %1 tied %2, compute @@ %1's new rating. &isgame [v(vf)]=[mod(match(get(%VO/VD),%0),2)] &games [v(vf)]=[filter(isgame,get(%VO/VD))] &competitors [v(vf)]= [setq(9,get(%VO/VD))] [setq(0,extract(%q9,add(1,match(%q9,%0)),1))] [iter(lattr(%q0/R_*),after(##,R_))] &rating_diff [v(vf)]= [setq(9,get(%VO/VD))] [setq(0,extract(%q9,add(1,match(%q9,%0)),1))] [setq(1,first(default(%q0/R_%1,1000)))] [setq(2,first(default(%q0/R_%2,1000)))] [abs(sub(%q1,%q2))] &rating_prob [v(vf)]= [switch(%0,<50,.50,<100,.57,<150,.63,<200,.70,<250,.76,<300,.81, <400,.85,<500,.92,.96)] &num_games [v(vf)]= [setq(9,get(%VO/VD))] [setq(0,extract(%q9,add(1,match(%q9,%0)),1))] [dec(words(default(%q0/R_%1,1)))] &veryshort [v(vf)]= [setq(9,get(%VO/VD))] [setq(0,extract(%q9,add(1,match(%q9,%0)),1))] [setq(1,rest(get(%q0/R_%1)))] [words(matchall(%q1,W*))]-[words(matchall(%q1,L*))]- [words(matchall(%q1,D*))] &short [v(vf)]= [setq(9,get(%VO/VD))] [setq(0,extract(%q9,add(1,match(%q9,%0)),1))] [setq(1,rest(get(%q0/R_%1)))] [setr(3,words(matchall(%q1,W*)))] wins - [setr(4,words(matchall(%q1,L*)))] losses - [setr(5,words(matchall(%q1,D*)))] ties (won [setq(6,add(%q3,%q4,%q5))][round(mul(100,fdiv(add(%q3,fdiv(%q5,2)),%q6)),1)]\% of %q6 games) &LONG [v(vf)]=[setq(9,get(%VO/VD))] [setq(0,extract(%q9,add(1,match(%q9,%0)),1))] [setq(1,rest(get(%q0/R_%1)))] [setq(2,)] [trim(iter(%q1,if(strmatch(##,W*),switch(1,strmatch(%q2,*[after(##,W)]*),,setq(2,%q2 [after(##,W)])))))] [switch(%q2,,,%r Won against: [iter(%q2,[name(##)] [switch(setr(3,words(matchall(%q1,W##))),>1,%b(x%q3))],%b,\,%b)]%r )] [setq(2,)] [trim(iter(%q1,if(strmatch(##,L*),if(strmatch(%q2,*[after(##,L)]*),,setq(2,%q2 [after(##,L)])))))] [switch(%q2,,,%r Lost against: [iter(%q2,[name(##)] [switch(setr(3,words(matchall(%q1,L##))),>1,%b(x%q3))],%b,\,%b)]%r )] [setq(2,)] [trim(iter(%q1,if(strmatch(##,D*),if(strmatch(%q2,*[after(##,D)]*),,setq(2,%q2 [after(##,D)])))))] [switch(%q2,,,%r Tied against: [iter(%q2,[name(##)] [switch(setr(3,words(matchall(%q1,D##))),>1,%b(x%q3))],%b,\,%b)])] &rating [v(vf)]= [setq(9,get(%VO/VD))] [setq(0,extract(%q9,add(1,match(%q9,%0)),1))] [first(default(%q0/R_%1,unrated))] &rating_mult [v(vf)]= [setq(9,get(%VO/VD))] [setq(0,extract(%q9,add(1,match(%q9,%0)),1))] [setq(1,dec(words(default(%q0/R_%1,1))))] [setq(2,first(default(%q0/R_%1,1000)))] [switch(%q1,<11,sub(70,%q1), <50,switch(%q2,<1800,30,<2000,24,20), ,switch(%q2,<1800,20,<2000,16,10))] &rating_ge [v(vf)]= [setq(9,get(%VO/VD))] [setq(0,extract(%q9,add(1,match(%q9,%0)),1))] [setq(1,first(default(%q0/R_%1,1000)))] [setq(2,first(default(%q0/R_%2,1000)))] [gte(%q1,%q2)] @@ %1 beat %2. If %1 is better than %2, we do 1-p * mult. If worse, @@ we do p * mult @@ &ratingW [v(vf)]= [setq(9,get(%VO/VD))] [setq(0,extract(%q9,add(1,match(%q9,%0)),1))] [setq(1,first(default(%q0/R_%1,1000)))] [setq(2,u(rating_prob,ulocal(rating_diff,%0,%1,%2)))] [setq(3,ulocal(rating_mult,%0,%1))] [add(%q1,if(ulocal(rating_ge,%0,%1,%2), round(mul(sub(1,%q2),%q3),0), round(mul(%q2,%q3),0)))] @@ %1 lost to %2. If %1 is better than %2, we do p * mult. If worse, @@ we do 1-p * mult @@ &ratingL [v(vf)]= [setq(9,get(%VO/VD))] [setq(0,extract(%q9,add(1,match(%q9,%0)),1))] [setq(1,first(default(%q0/R_%1,1000)))] [setq(2,u(rating_prob,ulocal(rating_diff,%0,%1,%2)))] [setq(3,ulocal(rating_mult,%0,%1))] [sub(%q1,if(ulocal(rating_ge,%0,%1,%2), round(mul(%q2,%q3),0), round(mul(sub(1,%q2),%q3),0)))] @@ %1 tied with %2. If %1 is better than %2, we add (.5 - p) * mult, which will @@ be negative. If %1 is worse, we do (p - .5) * mult, which is positive @@ &ratingD [v(vf)]= [setq(9,get(%VO/VD))] [setq(0,extract(%q9,add(1,match(%q9,%0)),1))] [setq(1,first(default(%q0/R_%1,1000)))] [setq(2,u(rating_prob,ulocal(rating_diff,%0,%1,%2)))] [setq(3,ulocal(rating_mult,%0,%1))] [add(%q1,if(ulocal(rating_ge,%0,%1,%2), round(mul(sub(.5,%q2),%q3),0), round(mul(sub(%q2,.5),%q3),0)))] &sort_num [v(vf)] = [revwords(sort(%0))] &munge_rating [v(vf) = [setq(0,ulocal(competitors,%0))] [munge(sort_num,iter(%q0,ulocal(rating,%0,##)),%q0)] @@ @@ Technical notes: @@ @@ The rating system, btw, works like this: @@ - Each player has a rating (400-2200), higher is better @@ - After a game, we compute the probability of a player winning, based @@ on difference in ratings: @@ 0 .50 @@ 50 .57 @@ 100 .63 @@ 150 .70 @@ 200 .76 @@ 250 .81 @@ 300 .85 @@ 400 .92 @@ 500 .96 @@ (We use the lowest rating diff, so a diff of 350 = 300) @@ If the result is in the expected direction (better beats worse), @@ we use 1-p(abs(rating diff)). In the opposite direction, (worse @@ beats better), we use p(abs(rating diff)). In a tie, we use @@ abs(rating diff) - .50. @@ - We multiply that probability by a multiplier based on how stable @@ a player's current rating is: @@ # games played 2000+ 1800-1999 0-1799 @@ 0-10 70-# played 70-# played 70-# played @@ 11-49 20 24 30 @@ 50- 10 16 20 @@ - That many points are given to the winner, and taken from the loser @@ - We start everyone at 1000 @@ @@ @@ Each player is represented by an attribute containing: @@ .... @@ A is represented by @@ Ex: &R_#7 Database = 1100 W#79 W#23 T#46 L#400 @@ @@ The confirmation database keeps track of unconfirmed results: @@ &C_ Confirm = @@