]> git.sesse.net Git - wloh/blob - www/index.pl
Factor player and rating fetching into its own function.
[wloh] / www / index.pl
1 #! /usr/bin/perl
2 use strict;
3 use warnings;
4 no warnings qw(once);
5 use CGI;
6 use CGI::Carp qw( fatalsToBrowser );
7 use DBI;
8 use POSIX;
9 use Devel::Peek;
10 use HTML::Entities;
11 use Encode;
12 use utf8;
13 use locale;
14 require '../config.pm';
15 require '../common.pm';
16
17 my $cgi = CGI->new;
18
19 my $dbh = DBI->connect($config::local_connstr, $config::local_username, $config::local_password)
20         or die "connect: " . $DBI::errstr;
21 $dbh->{AutoCommit} = 0;
22 $dbh->{RaiseError} = 1;
23
24 my $trials = 25_000;
25
26 binmode STDOUT, ':utf8';
27
28 my %players = ();
29 my %ratings = ();
30 my %ratings_stddev = ();
31 my @matches = ();
32 my %parms = ();
33 my $match_stddev;
34
35 sub sanitize {
36         return HTML::Entities::encode_entities(shift);
37 }
38
39 sub color {
40         my $x = shift;
41         return int(255.0 * ($x ** (1.80)));
42 }
43
44 sub get_max_season {
45         my $dbh = shift;
46         my $ref = $dbh->selectrow_hashref('SELECT MAX(sesong) AS max_sesong FROM fotballserier');
47         return $ref->{'max_sesong'};
48 }
49
50 sub get_divisions {
51         my ($dbh, $season) = @_;
52
53         my @divisions = ();
54
55         my $q = $dbh->prepare('SELECT DISTINCT(divisjon) FROM fotballserier WHERE sesong=? ORDER BY divisjon');
56         $q->execute($season);
57
58         while (my $ref = $q->fetchrow_hashref) {
59                 push @divisions, $ref->{'divisjon'};
60         }
61
62         return @divisions;
63 }
64
65 sub get_subdivisions {
66         my ($dbh, $season, $division) = @_;
67
68         my @subdivisions = ();
69
70         my $q = $dbh->prepare('SELECT DISTINCT(avdeling) FROM fotballserier WHERE sesong=? AND divisjon=? ORDER BY avdeling');
71         $q->execute($season, $division);
72
73         while (my $ref = $q->fetchrow_hashref) {
74                 push @subdivisions, $ref->{'avdeling'};
75         }
76
77         return @subdivisions;
78 }
79
80 sub print_division_selector {
81         my ($dbh, $divisions, $subdivisions, $division, $subdivision) = @_;
82
83         print <<"EOF";
84     <form method="get" action="/">
85 EOF
86
87         my $max_division = $divisions->[(scalar @$divisions) - 1];
88
89         print <<"EOF";
90      <p>Divisjon:
91         <select name="divisjon" onchange="form.submit();">
92 EOF
93
94         for my $d (@$divisions) {
95                 if ($d == $division) {
96                         print "        <option value=\"$d\" selected=\"selected\">$d</option>\n";
97                 } else {
98                         print "        <option value=\"$d\">$d</option>\n";
99                 }
100         }
101
102         print <<"EOF";
103         </select>
104         Avdeling:
105         <select name="avdeling" onchange="form.submit();">
106 EOF
107
108         for my $sd (@$subdivisions) {
109                 if ($sd == $subdivision) {
110                         print "        <option value=\"$sd\" selected=\"selected\">$sd</option>\n";
111                 } else {
112                         print "        <option value=\"$sd\">$sd</option>\n";
113                 }
114         }
115
116         print <<"EOF";
117         </select>
118         <input type="submit" value="Vis" />
119       </p>
120     </form>
121 EOF
122 }
123
124 sub get_players_and_ratings {
125         my ($dbh, $season, $division, $subdivision) = @_;
126
127         my $q = $dbh->prepare('SELECT fotballdeltagere.id,fotballdeltagere.navn,rating,rating_stddev FROM fotballdeltagere JOIN fotballserier ON fotballdeltagere.serie=fotballserier.nr LEFT JOIN ratings ON fotballdeltagere.id=ratings.id WHERE sesong=? AND divisjon=? AND avdeling=?');
128         $q->execute($season, $division, $subdivision);
129
130         while (my $ref = $q->fetchrow_hashref) {
131                 my $id = $ref->{'id'};
132                 $players{$id} = sanitize(Encode::decode_utf8($ref->{'navn'}));
133                 $ratings{$id} = $ref->{'rating'};
134                 $ratings_stddev{$id} = $ref->{'rating_stddev'};
135         }
136         $q->finish;
137 }
138
139 sub get_matches {
140         my ($dbh, $season, $division, $subdivision) = @_;
141
142         my @matches = ();
143         my $q = $dbh->prepare('
144         SELECT
145           d1.id AS p1, d2.id AS p2, maalfor AS score1, maalmot AS score2
146         FROM fotballresultater r
147           JOIN fotballserier s ON r.serie=s.nr
148           JOIN fotballdeltagere d1 ON r.lagrecno=d1.nr AND r.serie=d1.serie
149           JOIN fotballdeltagere d2 ON r.motstander=d2.nr AND r.serie=d2.serie
150         WHERE
151           sesong=? AND divisjon=? AND avdeling=?
152           AND lagrecno > motstander
153         ');
154         $q->execute($season, $division, $subdivision);
155
156         while (my $ref = $q->fetchrow_hashref) {
157                 push @matches, [ $ref->{'p1'}, $ref->{'p2'}, $ref->{'score1'}, $ref->{'score2'} ];
158         }
159         $q->finish;
160
161         return @matches;
162 }
163
164 sub get_covariance_matrix {
165         my ($dbh, @players) = @_;
166
167         my $player_sql = '{' . join(',', @players ) . '}';
168         my $q = $dbh->prepare('SELECT * FROM covariance WHERE player1=ANY(?::smallint[]) AND player2=ANY(?::smallint[])', { pg_prepare_now => 0 });
169         $q->execute($player_sql, $player_sql);
170
171         my $cov = {};
172         while (my $ref = $q->fetchrow_hashref) {
173                 $cov->{$ref->{'player1'}}{$ref->{'player2'}} = $ref->{'cov'};
174         }
175
176         return $cov;
177 }
178
179 sub write_parms_to_file {
180         my ($used_ratings, $used_cov) = @_;
181
182         POSIX::setlocale(&POSIX::LC_ALL, 'C');
183
184         my $tmpnam = POSIX::tmpnam();
185         open MCCALC, ">", $tmpnam
186                 or die "$tmpnam: $!";
187
188         printf MCCALC "%f\n", $match_stddev;
189         printf MCCALC "%d\n", scalar keys %players;
190
191         for my $id (keys %players) {
192                 my $rating = $used_ratings->{$id} // 500.0;
193                 printf MCCALC "%s %f\n", $id, $rating;
194         }
195
196         # covariance matrix
197         for my $id1 (keys %players) {
198                 for my $id2 (keys %players) {
199                         if ($id1 == $id2) {
200                                 printf MCCALC "%f ", ($used_cov->{$id1}{$id2} // $parms{-3});
201                         } else {
202                                 printf MCCALC "%f ", ($used_cov->{$id1}{$id2} // 0.0);
203                         }
204                 }
205                 printf MCCALC "\n";
206         }
207
208         for my $match (@matches) {
209                 printf MCCALC "%s %s %d %d\n", $match->[0], $match->[1], $match->[2], $match->[3];
210         }
211         close MCCALC;
212
213         POSIX::setlocale(&POSIX::LC_ALL, 'nb_NO.UTF-8');
214
215         return $tmpnam;
216 }
217
218 sub make_table {
219         my ($lowest_division, $used_ratings, $used_cov) = @_;
220
221         print <<"EOF";
222
223     <table>
224       <tr>
225         <th></th>
226 EOF
227
228         my $tmpnam = write_parms_to_file($used_ratings, $used_cov);
229         my %prob = ();
230
231         open MCCALC, "$config::base_dir/mcwordfeud $trials < $tmpnam |"
232                 or die "mccalc: $!";
233         while (<MCCALC>) {
234                 chomp;
235                 my @x = split /\s+/;
236                 my $id = $x[0];
237                 my $player = sprintf "%s (%.0f ± %.0f)", $players{$id}, ($ratings{$id} // 500.0), ($ratings_stddev{$id} // $parms{-3});
238                 $prob{$player} = [ @x[1..$#x] ];
239         }
240         close MCCALC;
241         #unlink $tmpnam;
242
243         my $num_games = scalar keys %prob;
244         for my $i (1..$num_games) {
245                 print "        <th>$i.</th>\n";
246         }
247         print "        <th>NEDRYKK</th>\n" unless ($lowest_division);
248         print "      </tr>\n";
249
250         for my $player (sort { $a cmp $b } keys %prob) {
251                 print "      <tr>\n";
252                 print "        <th>$player</th>\n";
253
254                 for my $i (1..$num_games) {
255                         my $pn = $prob{$player}->[$i - 1] / $trials;
256
257                         my $r = color(1.0 - $pn / 3);
258                         my $g = color(1.0 - $pn / 3);
259                         my $b = color(1.0);
260
261                         if ($i == 1) {
262                                 ($g, $b) = ($b, $g);
263                         } elsif ($i >= $num_games - 1 && !$lowest_division) {
264                                 ($r, $b) = ($b, $r);
265                         }
266
267                         printf "        <td style=\"background-color: rgb($r, $g, $b)\" class=\"num\">%.1f%%</td>\n", $pn * 100.0;
268                 }
269
270                 unless ($lowest_division) {
271                         my $pn = ($prob{$player}->[$num_games - 1] + $prob{$player}->[$num_games - 2]) / $trials;
272
273                         my $r = color(1.0);
274                         my $g = color(1.0 - $pn / 3);
275                         my $b = color(1.0 - $pn / 3);
276                         printf "        <td style=\"background-color: rgb($r, $g, $b)\" class=\"num\">%.1f%%</td>\n", $pn * 100.0;
277                 }
278                 print "      </tr>\n";
279         }
280
281         print << "EOF";
282     </table>
283 EOF
284 }
285
286 sub find_avg_rating {
287         my ($ratings) = shift;
288
289         my $sum_rating = 0.0;
290         for my $r (values %$ratings) {
291                 $sum_rating += $r;
292         }
293         return $sum_rating / scalar keys %ratings;
294 }
295
296 # Get auxillary parameters
297 my $q = $dbh->prepare('SELECT * FROM ratings WHERE id < 0');
298 $q->execute;
299
300 while (my $ref = $q->fetchrow_hashref) {
301         $parms{$ref->{'id'}} = $ref->{'rating'};
302 }
303 $match_stddev = $parms{-2} * sqrt(2.0);
304
305 my $season;
306 my $division = $cgi->param('divisjon') // -1;
307 my $subdivision = $cgi->param('avdeling') // -1;
308 my $last_division = 0;
309
310 POSIX::setlocale(&POSIX::LC_ALL, 'nb_NO.UTF-8');
311
312 print $cgi->header(-type=>'text/html; charset=utf-8', -expires=>'now');
313 printf <<"EOF", $match_stddev;
314 <?xml version="1.0" encoding="UTF-8" ?>
315 <!DOCTYPE
316   html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
317   "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
318 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="no">
319   <head>
320     <title>WLoH-plasseringsannsynlighetsberegning</title>
321     <link rel="stylesheet" href="/style" type="text/css" />
322   </head>
323   <body>
324     <h1>WLoH-plasseringsannsynlighetsberegning</h1>
325
326     <p><em>Dette er et hobbyprosjekt fra tredjepart, og ikke en offisiell del av
327       <a href="http://wordfeud.aasmul.net/">Wordfeud Leage of Honour</a>.</em></p>
328
329     <p>Beregningen tar ikke hensyn til ujevn spillestyrke, ting som er sagt i forumet e.l.;
330       den antar at samtlige uspilte kamper trekkes fra en normalfordeling med standardavvik
331       %.1f poeng. Sannsynlighetene kan summere til andre tall enn 100%% pga. avrunding.
332       Tallene vil variere litt fra gang til gang fordi utregningen skjer ved randomisering.</p>
333
334     <p>Spillerne er sortert etter nick.</p>
335 EOF
336
337 my $season = get_max_season($dbh);
338 my @divisions = get_divisions($dbh, $season);
339 $division = 1 if (!grep { $_ == $division } @divisions);
340 my @subdivisions = get_subdivisions($dbh, $season, $division);
341 $subdivision = 1 if (!grep { $_ == $subdivision } @subdivisions);
342
343 print_division_selector($dbh, \@divisions, \@subdivisions, $division, $subdivision);
344
345 get_players_and_ratings($dbh, $season, $division, $subdivision);
346
347 my @matches = get_matches($dbh, $season, $division, $subdivision);
348
349 my $cov = get_covariance_matrix($dbh, keys %players);
350
351 my $max_division = $divisions[$#divisions];
352 my $lowest_division = ($division == $max_division);
353 make_table($lowest_division, {}, {});
354
355 print <<"EOF";
356     <p>Under er en variant som tar relativ spillestyrke med i beregningen;
357       se <a href="/rating">ratingsiden</a>.</p>
358 EOF
359
360 make_table($lowest_division, \%ratings, $cov);
361
362 my $avg_rating = find_avg_rating(\%ratings);
363 printf "    <p>Gjennomsnittlig rating i denne avdelingen er <strong>%.1f</strong>.</p>\n", $avg_rating;
364
365 wloh_common::output_last_sync($dbh);
366
367 print <<"EOF";
368   </body>
369 </html>
370 EOF