simu

Unnamed repository; edit this file 'description' to name the repository.
git clone https://git.inz.fi/simu
Log | Files | Refs

commit 8b28e56a62290385cd5605112d6d5bc9c6336167
Author: Santtu Lakkala <inz@inz.fi>
Date:   Thu,  7 Mar 2024 17:52:39 +0200

Initial import

Diffstat:
AMakefile | 8++++++++
Aparsedata | 31+++++++++++++++++++++++++++++++
Asimu.c | 621+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asimuformat | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 752 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,8 @@ +LDFLAGS = -lm +CFLAGS = -W -Wall -std=c99 -pthread -DUSE_LEHMER +TARGET = simu + +all: $(TARGET) + +$(TARGET): simu.c + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) diff --git a/parsedata b/parsedata @@ -0,0 +1,31 @@ +#!/usr/bin/env perl + +use JSON; + +my $data = JSON->new->utf8->decode(join '', <<>>) || die "$!"; +my %teams; +my @played; +my @upcoming; + +for (@{$data}) { + my $ht = $_->{'homeTeam'}{'teamName'}; + my $at = $_->{'awayTeam'}{'teamName'}; + my $hid = $teams{$ht} // ($teams{$ht} = scalar %teams); + my $aid = $teams{$at} // ($teams{$at} = scalar %teams); + + if ($_->{ended}) { + push @played, [$hid, $aid, $_->{'homeTeam'}{'goals'}, $_->{'awayTeam'}{'goals'}, $_->{'gameTime'}]; + } else { + push @upcoming, [$hid, $aid]; + } +} + +printf("%d\n", scalar %teams); +print join("\n", sort { $teams{$a} <=> $teams{$b} } keys %teams), "\n\n"; + +printf("%d\n", scalar @played); +print map { sprintf("%d %d %d %d %s\n", @$_[0..3], $_->[4] > 3600 ? $_->[4] == 3900 ? "SO" : "OT" : "") } @played; +print "\n"; + +printf("%d\n", scalar @upcoming); +print map { (join ' ', @$_) . "\n" } @upcoming; diff --git a/simu.c b/simu.c @@ -0,0 +1,621 @@ +/* + * Input: + * <number of teams> + * Team1 + * Team2 + * + * <number of played games> + * <index of home team> <index of away team> <home score> <away score> <OT/SO> + * + * <number of remaining games> + * <index of home team> <index of away team> + * + * Output: + * Team1 <current games> <current points> <elo rating> <total games> <expected points> <probab finish 1st> <probab finish 2nd> ... + * Team2 <current games> <current points> <elo rating> <total games> <expected points> <probab finish 1st> <probab finish 2nd> ... + */ +#define _POSIX_C_SOURCE 200811L +#include <limits.h> +#include <math.h> +#include <pthread.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#if defined(USE_LEHMER) +#define R_MAX UINT32_MAX +struct rand_t { + __uint128_t state; +}; + +static void getseed(struct rand_t *r) +{ + struct timespec s; + clock_gettime(CLOCK_MONOTONIC, &s); + r->state = s.tv_nsec; + r->state <<= 64; + r->state |= s.tv_sec | 1; +} + +static inline uint32_t getrand(struct rand_t *r) +{ + r->state *= 0xda942042e4dd58b5; + return r->state >> 64; +} + +#elif defined(USE_URANDOM) +#define R_MAX UINT32_MAX +struct rand_t { + int fd; + uint32_t rbuf[4096]; + uint32_t *r; +}; + +static void getseed(struct rand_t *r) +{ + r->fd = open("/dev/urandom", O_RDONLY); + r->r = 1[&r->rbuf]; +} + +static inline uint32_t getrand(struct rand_t *r) +{ + if (r->r == 1[&r->rbuf]) { + read(r->fd, r->rbuf, sizeof(r->rbuf)); + r->r = r->rbuf; + } + return *r->r++; +} +#else +struct rand_t { + unsigned int seed; +}; +#define R_MAX RAND_MAX +static void getseed(struct rand_t *r) +{ + struct timespec s; + clock_gettime(CLOCK_MONOTONIC, &s); + r->seed = s.tv_sec ^ s.tv_nsec; +} + +static inline uint32_t getrand(struct rand_t *r) +{ + return rand_r(&r->seed); +} +#endif + +static inline bool urand(uint32_t limit, bool *anti, struct rand_t *r) +{ + uint32_t v = getrand(r); + if (anti) + *anti = (uint32_t)R_MAX - v < limit; + return (uint32_t)v < limit; +} + +struct team { + size_t id; + char name[128]; + unsigned points; + unsigned games; + unsigned wins; + double elo; + long long unsigned pointsum; + long long unsigned gamessum; + size_t *poscounts; +}; + +struct game { + size_t t[2]; + uint32_t expected; +}; + +struct standing { + size_t ti; + unsigned points; + unsigned wins; + unsigned games; + unsigned random; +}; + +int pointscmp(const void *a, const void *b) +{ + int r; + const struct standing *at = a; + const struct standing *bt = b; + + if ((r = bt->points * at->games - at->points * bt->games)) + return r; + if ((r = bt->wins - at->wins)) + return r; + return bt->random - at->random; +} + +int teampointscmp(const void *a, const void *b) +{ + int r; + const struct team *at = a; + const struct team *bt = b; + + if ((r = bt->points * at->games - at->points * bt->games)) + return r; + if ((r = bt->wins - at->wins)) + return r; + return 0; +} + +int teamcmp(const void *a, const void *b) +{ + const struct team *at = a; + const struct team *bt = b; + + if (at->pointsum * bt->gamessum > bt->pointsum * at->gamessum) + return -1; + if (bt->pointsum * at->gamessum > at->pointsum * bt->gamessum) + return 1; + return 0; +} + +void isort(void *data, size_t n, size_t sz, int (*cmp)(const void *a, const void *b)) +{ + char (*d)[sz][1] = data; + char tmp[sz]; + size_t i, j; + + for (i = 1; i < n; i++) { + if (cmp(d[i], d[i - 1]) >= 0) + continue; + + memcpy(tmp, d[i], sizeof(tmp)); + + for (j = i - 1; j > 0 && cmp(tmp, d[j - 1]) < 0; j--); + + memmove(d[j + 1], d[j], *d[i] - *d[j]); + memcpy(d[j], tmp, sizeof(tmp)); + } +} + +void simulate(struct team *teams, + size_t n_teams, + const struct game *games, + size_t n_games, + uint32_t otprob, + size_t iterations, + struct rand_t *seedp) +{ + struct standing s0[n_teams]; + struct standing standings[n_teams]; + struct standing antistandings[n_teams]; + size_t i; + + for (i = 0; i < n_teams; i++) { + s0[i].ti = i; + s0[i].points = teams[i].points; + s0[i].wins = teams[i].wins; + s0[i].games = teams[i].games; + } + + for (i = 0; i < iterations / 2; i++) { + size_t j; + + memcpy(standings, s0, sizeof(standings)); + memcpy(antistandings, s0, sizeof(antistandings)); + + for (j = 0; j < n_games; j++) { + size_t wi; + size_t li; + size_t awi; + size_t ali; + + bool antihw; + bool antiot; + bool hw = urand(games[j].expected, &antihw, seedp); + bool ot = urand(otprob, &antiot, seedp); + + wi = games[j].t[!hw]; + li = games[j].t[hw]; + + awi = games[j].t[!antihw]; + ali = games[j].t[antihw]; + + standings[wi].points += 3 - ot; + standings[li].points += ot; + standings[wi].wins += !ot; + standings[wi].games++; + standings[li].games++; + + antistandings[awi].points += 3 - ot; + antistandings[ali].points += ot; + antistandings[awi].wins += !ot; + antistandings[awi].games++; + antistandings[ali].games++; + } + + for (j = 0; j < n_teams; j++) { + standings[j].random = getrand(seedp) % (INT_MAX / 2); + antistandings[j].random = getrand(seedp) % (INT_MAX / 2); + } + isort(standings, n_teams, sizeof(*standings), pointscmp); + + for (j = 0; j < n_teams; j++) { + teams[standings[j].ti].pointsum += standings[j].points; + teams[standings[j].ti].gamessum += standings[j].games; + teams[standings[j].ti].poscounts[j]++; + } + + isort(antistandings, n_teams, sizeof(*antistandings), pointscmp); + + for (j = 0; j < n_teams; j++) { + teams[antistandings[j].ti].pointsum += antistandings[j].points; + teams[antistandings[j].ti].gamessum += antistandings[j].games; + teams[antistandings[j].ti].poscounts[j]++; + } + } + +} + +void simulate_winall(struct team *teams, + size_t n_teams, + const struct game *games, + size_t n_games, + uint32_t otprob, + size_t iterations, + size_t team_id, + bool win, + struct rand_t *seedp) +{ + struct team teams_cpy[n_teams]; + struct game games_cpy[n_games]; + + size_t r; + size_t w; + + memcpy(teams_cpy, teams, sizeof(teams_cpy)); + + for (w = r = 0; r < n_games; r++) { + if (games[r].t[0] == team_id || games[r].t[1] == team_id) { + size_t opp = games[r].t[0] + games[r].t[1] - team_id; + teams_cpy[team_id].points += win * 3; + teams_cpy[team_id].games++; + teams_cpy[team_id].wins += win; + + teams_cpy[opp].points += !win * 3; + teams_cpy[opp].games++; + teams_cpy[opp].wins += !win; + } else { + games_cpy[w++] = games[r]; + } + } + + simulate(teams_cpy, n_teams, games_cpy, w, otprob, iterations, seedp); +} + +void simulate_winloseall(struct team *teams, + size_t n_teams, + size_t team, + const struct game *games, + size_t n_games, + uint32_t otprob, + size_t iterations, + struct rand_t *seedp) +{ + struct team teams_cpy[n_teams]; + size_t poscounts[n_teams][n_teams]; + size_t e; + size_t i; + + memcpy(teams_cpy, teams, sizeof(teams_cpy)); + memset(poscounts, 0, sizeof(poscounts)); + + for (i = 0; i < n_teams; i++) + teams_cpy[i].poscounts = poscounts[i]; + + simulate_winall(teams_cpy, n_teams, + games, n_games, + otprob, + iterations, + team, true, + seedp); + simulate_winall(teams_cpy, n_teams, + games, n_games, + otprob, + iterations, + team, false, + seedp); + + for (i = 0; i < n_teams; i++) + if (teams_cpy[team].poscounts[i]) + break; + for (e = n_teams; e > 0; e--) + if (teams_cpy[team].poscounts[e - 1]) + break; + for (; i < e; i++) + teams[team].poscounts[i] = 1; +} + +struct thread_data { + struct team *teams; + bool *winloseall; + size_t n_teams; + + struct game *games; + size_t n_games; + + uint32_t otprob; + + size_t iterations; + size_t iterated; + pthread_mutex_t mutex; +}; + +void *simulate_thread(void *data) +{ + struct thread_data *td = data; + struct team teams_cpy[td->n_teams]; + size_t i; + size_t iters; + size_t poscounts[td->n_teams][td->n_teams]; + size_t n = 0; + struct rand_t seedp; + + getseed(&seedp); + + pthread_mutex_lock(&td->mutex); + memcpy(teams_cpy, td->teams, sizeof(teams_cpy)); + pthread_mutex_unlock(&td->mutex); + + memset(poscounts, 0, sizeof(poscounts)); + + for (i = 0; i < td->n_teams; i++) { + teams_cpy[i].poscounts = poscounts[i]; + pthread_mutex_lock(&td->mutex); + if (!td->winloseall[i]) { + td->winloseall[i] = true; + pthread_mutex_unlock(&td->mutex); + + simulate_winloseall(teams_cpy, td->n_teams, i, + td->games, td->n_games, + td->otprob, + td->iterations / td->n_teams / 2, + &seedp); + } else { + pthread_mutex_unlock(&td->mutex); + } + } + + for (;;) { + pthread_mutex_lock(&td->mutex); + + if (td->iterations - td->iterated > 100000) + iters = 100000; + else + iters = td->iterations - td->iterated; + + td->iterated += iters; + pthread_mutex_unlock(&td->mutex); + + if (!iters) + break; + n += iters; + + simulate(teams_cpy, td->n_teams, + td->games, td->n_games, + td->otprob, + iters, &seedp); + } + + pthread_mutex_lock(&td->mutex); + for (i = 0; i < td->n_teams; i++) { + size_t j; + td->teams[i].pointsum += teams_cpy[i].pointsum; + td->teams[i].gamessum += teams_cpy[i].gamessum; + + for (j = 0; j < td->n_teams; j++) + td->teams[i].poscounts[j] += teams_cpy[i].poscounts[j]; + } + pthread_mutex_unlock(&td->mutex); + + return (void *)n; +} + +const char *ltrim(const char *s) +{ + while (*s++ == ' '); + return s - 1; +} + +int main(int argc, char **argv) +{ + int opt; + double homeadv = 58; + double elok = 32; + uint32_t otprob = 0.23 * R_MAX; + size_t n; + size_t m; + size_t i; + size_t j; + size_t iterations = 1000000; + size_t threads = 1; + + while ((opt = getopt(argc, argv, "i:t:a:o:")) != -1) { + switch (opt) { + case 'i': + iterations = strtoull(optarg, NULL, 10); + break; + + case 't': + threads = strtoull(optarg, NULL, 10); + break; + + case 'a': + homeadv = strtod(optarg, NULL); + break; + + case 'o': + otprob = strtod(optarg, NULL) * R_MAX; + break; + + case 'k': + elok = strtod(optarg, NULL); + break; + + default: + exit(1); + break; + } + } + + iterations += iterations & 1; + + if (scanf(" %zu", &n) < 1) + return 1; + + struct team teams[n]; + size_t teammap[n]; + size_t poscounts[n][n]; + memset(poscounts, 0, sizeof(poscounts)); + + for (i = 0; i < n; i++) { + if (scanf(" %127s", teams[i].name) < 1) + return 1; + teams[i].id = i; + teams[i].points = 0; + teams[i].games = 0; + teams[i].wins = 0; + teams[i].pointsum = 0; + teams[i].gamessum = 0; + teams[i].poscounts = poscounts[i]; + teams[i].elo = 2000; + } + + if (scanf(" %zu", &m) < 1) + return 1; + + for (i = 0; i < m; i++) { + size_t hi, ai; + unsigned hs, as; + bool ot = false; + char det[16] = ""; + + switch (scanf(" %zu %zu %u %u%15[^\n]", &hi, &ai, &hs, &as, det)) { + case 5: + ot = !strcmp(ltrim(det), "OT") || + !strcmp(ltrim(det), "SO"); + break; + + case 4: + break; + + default: + exit(1); + } + + double expected = 1. / (1 + pow(10, (teams[ai].elo - teams[hi].elo - homeadv) / 400.0)); + double actual; + + teams[hi].games++; + teams[ai].games++; + + if (hs > as) { + if (!ot) { + actual = 1; + teams[hi].wins++; + teams[hi].points += 3; + } else { + actual = 2./3; + teams[hi].points += 2; + teams[ai].points += 1; + } + } else { + if (ot) { + actual = 1./3; + teams[hi].points += 1; + teams[ai].points += 2; + } else { + teams[ai].points += 3; + actual = 0; + } + } + + teams[hi].elo += elok * (actual - expected); + teams[ai].elo -= elok * (actual - expected); + } + + isort(teams, 1[&teams] - teams, sizeof(*teams), teampointscmp); + + for (i = 0; i < n; i++) + teammap[teams[i].id] = i; + + if (scanf(" %zu", &m) < 1) + return 1; + + struct game games[m]; + + for (i = 0; i < m; i++) { + size_t h, a; + if (scanf(" %zu %zu", &h, &a) < 2) + return 1; + games[i].t[0] = teammap[h]; + games[i].t[1] = teammap[a]; + games[i].expected = R_MAX / (1 + pow(10.0, (teams[games[i].t[1]].elo - (teams[games[i].t[0]].elo + homeadv)) / 400.0)); + } + + if (threads > 1) { + pthread_t pthrds[threads]; + bool winloseall[n]; + memset(winloseall, 0, sizeof(winloseall)); + struct thread_data td = { + teams, winloseall, n, + games, m, + otprob, + iterations, 0, + PTHREAD_MUTEX_INITIALIZER + }; + for (i = 0; i < threads; i++) + pthread_create(&pthrds[i], NULL, simulate_thread, &td); + iterations = 0; + for (i = 0; i < threads; i++) { + void *a; + pthread_join(pthrds[i], &a); + iterations += (size_t)a; + } + } else { + struct rand_t seedp; + getseed(&seedp); + + for (i = 0; i < n; i++) { + simulate_winloseall(teams, n, i, + games, m, + otprob, + iterations / n / 2, + &seedp); + } + + simulate(teams, n, + games, m, + otprob, + iterations, + &seedp); + } + + isort(teams, n, sizeof(*teams), teamcmp); + for (i = 0; i < n; i++) { + printf("%s %u %u %llu %.1f %d", teams[i].name, + teams[i].games, teams[i].points, + teams[i].gamessum / iterations, + (double)teams[i].pointsum / iterations, + (int)round(teams[i].elo)); + for (j = 0; j < n; j++) { + if (teams[i].poscounts[j] == iterations + 1) + printf(" +"); + else if (teams[i].poscounts[j]) + printf(" %e", (teams[i].poscounts[j] - 1) * 100.0 / iterations); + else + printf(" -"); + } + printf("\n"); + } + + return 1; +} diff --git a/simuformat b/simuformat @@ -0,0 +1,92 @@ +#!/usr/bin/perl -w -CSAio + +use utf8; + +my @seps = @ARGV; +@ARGV = (); + +print <<FOO; +<html> +<head> +<style type="text/css"> +table { + border-spacing: 0; +} +small { + font-size: 50%; +} +thead td, thead th { + border-bottom: 4px solid black; +} +tbody tr > td:nth-child(3) { + border-right: 4px solid black; +} +td { + text-align: center; +} +FOO +if (@seps) { + print join(", ", map { "tr:nth-child($_) > *" } @seps); + print <<FOO; +{ + border-bottom: 1px solid black; +} +FOO + print join(", ", map { "tr:nth-child(" . ($_ + 1) . ") > *" } @seps); + print <<FOO; +{ + border-top: 1px solid black; +} +FOO + print join(", ", map { "tr > *:nth-child(" . ($_ + 6) . ")" } @seps); + print <<FOO; +{ + border-right: 1px solid black; +} +FOO + print join(", ", map { "tr > *:nth-child(" . ($_ + 7) . ")" } @seps); + print <<FOO; +{ + border-left: 1px solid black; +} +FOO +} +print <<FOO; +td.no { + background: red; +} +td.yes { + background: cyan; + border: 1px solid darkcyan; + font-weight: bold; +} +td.points { + background: inherit; +} +td.sep { + background: black; + width: 3px; + height: 3px; +} +</style> +</head> +<body> +<table> +FOO +my $headed = 0; +my $j = 0; +while (<<>>) { + chomp; + my ($team, $games, $actualpoints, $fingames, $points, $elo, @probs) = split / /; + print "<thead><tr><th>Joukkue</th><th>O</th><th>P</th><th>Elo</th><th>O</th><th>Ennuste</th>" . join('', map { '<th>' . $_ . '.</th>' } 1..($#probs + 1)) . "</tr></thead>\n<tbody>\n" unless $headed++; + print "<tr><th>$team</th><td class=\"games\">$games</td><td class=\"actualpoints\">$actualpoints</td><td class=\"elo\">$elo</td><td class=\"fingames\">$fingames</td><td class=\"points\">$points</td>" . join("", map { $_ eq '+' ? '<td class="yes">' . $_ . '</td>' : $_ eq '-' ? '<td class="no">' . $_ . '</td>' : sprintf('<td style="border: 1px solid rgb(%.1f%%, 80%%, %.1f%%); padding: -1px; background-color: rgb(%.1f%%, 100%%, %.1f%%)">%s</td>', 80 - 80 * sqrt($_ / 100), 80 - 80 * sqrt($_ / 100), 100 - 100 * sqrt($_ / 100), 100 - 100 * sqrt($_ / 100), ($_ < 0.005 ? $_ > 0 ? sprintf("<small>10<sup>%.0f</sup></small>", log($_) / log(10)) : '<small>ε</small>' : $_ < 9.995 ? sprintf("%.2f", $_) : sprintf("%.1f", $_)) ) } @probs) . "</tr>\n"; +} + +my $now = `LC_CTIME=fi_FI.UTF-8 date`; +print <<FOO; +</tbody> +</table> +<small>Viimeksi päivitetty $now</small> +</body> +</html> +FOO