ryp преди 1 месец
родител
ревизия
640ed2ef42

+ 6 - 3
app/controllers/contest_inside.php

@@ -117,13 +117,16 @@
 				global $contest;
 				global $contest;
 				$contest_data = queryContestData($contest);
 				$contest_data = queryContestData($contest);
 				calcStandings($contest, $contest_data, $score, $standings, true);
 				calcStandings($contest, $contest_data, $score, $standings, true);
+				$ratings = array();
+				$rating_rds = array();
+				$rating_vols = array();
 				if (!isset($contest['extra_config']['unrated'])) {
 				if (!isset($contest['extra_config']['unrated'])) {
-					$rating_k = isset($contest['extra_config']['rating_k']) ? $contest['extra_config']['rating_k'] : 400;
-					$ratings = calcRating($standings, $rating_k);
+					calcRatingGlicko2($standings, $ratings, $rating_rds, $rating_vols);
 				} else {
 				} else {
-					$ratings = array();
 					for ($i = 0; $i < count($standings); $i++) {
 					for ($i = 0; $i < count($standings); $i++) {
 						$ratings[$i] = $standings[$i][2][1];
 						$ratings[$i] = $standings[$i][2][1];
+						$rating_rds[$i] = $standings[$i][2][2];
+						$rating_vols[$i] = $standings[$i][2][3];
 					}
 					}
 				}
 				}
 
 

+ 5 - 9
app/controllers/contest_manage.php

@@ -116,7 +116,7 @@
 	);
 	);
 	
 	
 	if (isSuperUser($myUser)) {
 	if (isSuperUser($myUser)) {
-		$rating_k_form = new UOJForm('rating_k');
+		/*$rating_k_form = new UOJForm('rating_k');
 		$rating_k_form->addInput('rating_k', 'text', 'rating 变化上限', isset($contest['extra_config']['rating_k']) ? $contest['extra_config']['rating_k'] : 400,
 		$rating_k_form->addInput('rating_k', 'text', 'rating 变化上限', isset($contest['extra_config']['rating_k']) ? $contest['extra_config']['rating_k'] : 400,
 			function ($x) {
 			function ($x) {
 				if (!validateUInt($x) || $x < 1 || $x > 1000) {
 				if (!validateUInt($x) || $x < 1 || $x > 1000) {
@@ -133,7 +133,7 @@
 			$esc_extra_config = DB::escape($esc_extra_config);
 			$esc_extra_config = DB::escape($esc_extra_config);
 			DB::update("update contests set extra_config = '$esc_extra_config' where id = {$contest['id']}");
 			DB::update("update contests set extra_config = '$esc_extra_config' where id = {$contest['id']}");
 		};
 		};
-		$rating_k_form->runAtServer();
+		$rating_k_form->runAtServer();*/
 		
 		
 		$rated_form = new UOJForm('rated');
 		$rated_form = new UOJForm('rated');
 		$rated_form->handle = function() {
 		$rated_form->handle = function() {
@@ -152,7 +152,8 @@
 		$rated_form->submit_button_config['smart_confirm'] = '';
 		$rated_form->submit_button_config['smart_confirm'] = '';
 	
 	
 		$rated_form->runAtServer();
 		$rated_form->runAtServer();
-		
+
+		/*
 		$version_form = new UOJForm('version');
 		$version_form = new UOJForm('version');
 		$version_form->addInput('standings_version', 'text', '排名版本', $contest['extra_config']['standings_version'],
 		$version_form->addInput('standings_version', 'text', '排名版本', $contest['extra_config']['standings_version'],
 			function ($x) {
 			function ($x) {
@@ -170,7 +171,7 @@
 			$esc_extra_config = DB::escape($esc_extra_config);
 			$esc_extra_config = DB::escape($esc_extra_config);
 			DB::update("update contests set extra_config = '$esc_extra_config' where id = {$contest['id']}");
 			DB::update("update contests set extra_config = '$esc_extra_config' where id = {$contest['id']}");
 		};
 		};
-		$version_form->runAtServer();
+		$version_form->runAtServer();*/
 
 
 		$contest_type_form = new UOJForm('contest_type');
 		$contest_type_form = new UOJForm('contest_type');
 		$contest_type_form->addInput('contest_type', 'text', '赛制', $contest['extra_config']['contest_type'],
 		$contest_type_form->addInput('contest_type', 'text', '赛制', $contest['extra_config']['contest_type'],
@@ -268,11 +269,6 @@
 					</div>
 					</div>
 				</div>
 				</div>
 				<div class="top-buffer-sm"></div>
 				<div class="top-buffer-sm"></div>
-				<?php $rating_k_form->printHTML(); ?>
-			</div>
-			<div class="col-sm-12 top-buffer-sm">
-				<h3>版本控制</h3>
-				<?php $version_form->printHTML(); ?>
 			</div>
 			</div>
 			<div class="col-sm-12 top-buffer-sm">
 			<div class="col-sm-12 top-buffer-sm">
 				<h3>赛制</h3>
 				<h3>赛制</h3>

+ 6 - 0
app/controllers/contests.php

@@ -37,6 +37,12 @@ EOD;
 		} elseif ($contest['cur_progress'] == CONTEST_FINISHED) {
 		} elseif ($contest['cur_progress'] == CONTEST_FINISHED) {
 			$contest_name_link .= '<sup><a style="color:grey" href="/contest/'.$contest['id'].'/standings">'.UOJLocale::get('contests::ended').'</a></sup>';
 			$contest_name_link .= '<sup><a style="color:grey" href="/contest/'.$contest['id'].'/standings">'.UOJLocale::get('contests::ended').'</a></sup>';
 		}
 		}
+
+		if (isset($contest['extra_config']['unrated']))
+			$contest_name_link .= '<sup><span style="color:green"> Unrated</span>';
+		else
+			$contest_name_link .= '<sup><span style="color:red"> Rated</span>';
+
 		
 		
 		$last_hour = round($contest['last_min'] / 60, 2);
 		$last_hour = round($contest['last_min'] / 60, 2);
 		
 		

+ 18 - 2
app/controllers/index.php

@@ -1,7 +1,8 @@
 <?php
 <?php
 $blogs = DB::selectAll("select blogs.id, title, poster, post_time from important_blogs, blogs where is_hidden = 0 and important_blogs.blog_id = blogs.id order by level desc, important_blogs.blog_id desc limit 8");
 $blogs = DB::selectAll("select blogs.id, title, poster, post_time from important_blogs, blogs where is_hidden = 0 and important_blogs.blog_id = blogs.id order by level desc, important_blogs.blog_id desc limit 8");
 $activateusers = DB::selectAll("select submitter, count(*) as cnt from submissions where datediff(now(), submit_time) <= 30 group by submitter order by cnt desc limit 12;");
 $activateusers = DB::selectAll("select submitter, count(*) as cnt from submissions where datediff(now(), submit_time) <= 30 group by submitter order by cnt desc limit 12;");
-$topusers = DB::selectAll("select username, rating, qq, motto from user_info order by rating desc, username asc limit 12;");
+$topusers = DB::selectAll("select username, rating, rating_rd, qq, motto from user_info order by rating desc, username asc limit 12;");
+$hotusers = DB::selectAll("select username, rating, rating_rd, rating_vol, qq, motto from user_info order by rating_vol desc, username asc limit 12;");
 $topblogs = DB::selectAll("select id from blogs order by zan desc limit 10;");
 $topblogs = DB::selectAll("select id from blogs order by zan desc limit 10;");
 $songs_data = htmlspecialchars(json_encode(DB::selectAll("select * from songs;")), ENT_QUOTES, 'UTF-8');
 $songs_data = htmlspecialchars(json_encode(DB::selectAll("select * from songs;")), ENT_QUOTES, 'UTF-8');
 
 
@@ -336,12 +337,27 @@ $('#recent-contest-countdown-<?= $contest['id'] ?>').countdown(<?= $contest['sta
 			<div class="card h-100 p-3 text-center">
 			<div class="card h-100 p-3 text-center">
 				<img class="uoj-avatar mx-auto mb-2" src=<?= "https://q.qlogo.cn/g?b=qq&nk=" . $user['qq'] . "&s=100" ?> alt="...">
 				<img class="uoj-avatar mx-auto mb-2" src=<?= "https://q.qlogo.cn/g?b=qq&nk=" . $user['qq'] . "&s=100" ?> alt="...">
 				<span class="mb-2"><?= getUserLink($user['username']) ?></span>
 				<span class="mb-2"><?= getUserLink($user['username']) ?></span>
+				<strong style="color: red" class="mb-2"><?= $user['rating'] ?> (±<?= round($user['rating_rd']) * 2 ?>)</strong>
+				<span class="text-center text-bottom"><?= $user['motto'] ?></span>
+			</div>
+		</div>
+	<?php endforeach ?>
+</div>
+<div class="row shadow my-4 wyoj-topcard">
+	<div class="col-12 p-4">
+		<h3>黑马 / 爆冷榜</h3>
+	</div>
+	<?php foreach($hotusers as $user): ?>
+		<div class="col-md-3 mb-4">
+			<div class="card h-100 p-3 text-center">
+				<img class="uoj-avatar mx-auto mb-2" src=<?= "https://q.qlogo.cn/g?b=qq&nk=" . $user['qq'] . "&s=100" ?> alt="...">
+				<span class="mb-2"><?= getUserLink($user['username']) ?></span>
+				<strong style="color: red" class="mb-2">Vol <?= round(1000 * $user['rating_vol']) / 1000 ?>, <?= $user['rating'] ?> (±<?= round($user['rating_rd']) * 2 ?>)</strong>
 				<span class="text-center text-bottom"><?= $user['motto'] ?></span>
 				<span class="text-center text-bottom"><?= $user['motto'] ?></span>
 			</div>
 			</div>
 		</div>
 		</div>
 	<?php endforeach ?>
 	<?php endforeach ?>
 </div>
 </div>
-
 <div class="row shadow my-4 wyoj-topcard">
 <div class="row shadow my-4 wyoj-topcard">
 	<div class="col-12 p-4">
 	<div class="col-12 p-4">
 		<h3>博客广场</h3>
 		<h3>博客广场</h3>

+ 7 - 6
app/controllers/problem_set.php

@@ -213,12 +213,13 @@ EOD;
 		)
 		)
 	);
 	);
 
 
-	foreach (queryUserExtgroups($myUser) as $i => $grp)
-		if ($grp != '')
-			$tabs_info[$grp] = array(
-				'name' => '团队 ' . $grp .' 题目',
-				'url' => "/problems/group/" . $grp
-			);
+	if ($myUser != null)
+		foreach (queryUserExtgroups($myUser) as $i => $grp)
+			if ($grp != '')
+				$tabs_info[$grp] = array(
+					'name' => '团队 ' . $grp .' 题目',
+					'url' => "/problems/group/" . $grp
+				);
 	
 	
 
 
 	$pag_config = array('page_len' => 100);
 	$pag_config = array('page_len' => 100);

+ 51 - 0
app/controllers/super_manage.php

@@ -5,6 +5,47 @@
 	if ($myUser == null || !isSuperUser($myUser)) {
 	if ($myUser == null || !isSuperUser($myUser)) {
 		become403Page();
 		become403Page();
 	}
 	}
+
+	$purge_rating_form = new UOJForm('purgerating');
+	$purge_rating_form->addInput('confirm', 'text', '确认删除所有 Rating(重复)', '',
+		function($confirm) {
+			if ($confirm != '确认删除所有 Rating')
+				return '已取消';
+			return '';
+		}, null);
+	$purge_rating_form->handle = function() {
+		set_time_limit(0);
+		ignore_user_abort(true);
+
+		DB::update("update user_info set rating = 1500, rating_rd = 350, rating_vol = 0.06;");
+		$contests = DB::selectAll("select id from contests where status = 'finished' order by id;");
+		foreach ($contests as $id) {
+			$id = $id['id'];
+			DB::update("update contests_registrants c join user_info u on c.username = u.username "
+				. "set c.user_rating = u.rating, c.user_rd = u.rating_rd, c.user_vol = u.rating_vol where contest_id = $id");
+			$contest = queryContest($id);
+			genMoreContestInfo($contest);
+			if (isset($contest['extra_config']['unrated'])) continue;
+			error_log("Recalculating contest rating: $id {$contest['name']}");
+			error_log(print_r($contest, true));
+
+			$contest_data = queryContestData($contest);
+			error_log(print_r($contest_data, true));
+			$score = array();
+			$standings = array();
+			$ratings = array();
+			$rating_rds = array();
+			$rating_vols = array();
+			calcStandings($contest, $contest_data, $score, $standings, true);
+			calcRatingGlicko2($standings, $ratings, $rating_rds, $rating_vols);
+
+			for ($i = 0; $i < count($standings); ++$i) {
+				error_log("New rating for $i {$ratings[$i]}, {$rating_rds[$i]}");
+				DB::update("update user_info set rating = {$ratings[$i]}, rating_rd = {$rating_rds[$i]}, rating_vol = {$rating_vols[$i]} where username = '{$standings[$i][2][0]}';");
+			}
+		}
+	};
+	$purge_rating_form->runAtServer();
 	
 	
 	$user_form = new UOJForm('user');
 	$user_form = new UOJForm('user');
 	$user_form->addInput('username', 'text', '用户名', '',
 	$user_form->addInput('username', 'text', '用户名', '',
@@ -462,6 +503,10 @@ EOD;
 		'certs' => array(
 		'certs' => array(
 			'name' => '勋章管理',
 			'name' => '勋章管理',
 			'url' => '/super-manage/certs'
 			'url' => '/super-manage/certs'
+		),
+		'rating' => array(
+			'name' => 'Rating 管理',
+			'url' => '/super-manage/rating'
 		)
 		)
 	);
 	);
 	
 	
@@ -615,6 +660,12 @@ EOD;
 				<h4>将勋章授予某人</h4>
 				<h4>将勋章授予某人</h4>
 				<?php $assign_certs_form->printHTML(); ?>
 				<?php $assign_certs_form->printHTML(); ?>
 			</div>
 			</div>
+		<?php elseif ($cur_tab == 'rating'): ?>
+			<div>
+				<h4>Rating 管理</h4>
+				<h4 style="color: red">重置 Rating</h4>
+				<?php $purge_rating_form->printHTML(); ?>
+			</div>
 		<?php endif ?>
 		<?php endif ?>
 	</div>
 	</div>
 </div>
 </div>

+ 57 - 1
app/controllers/user_info.php

@@ -38,7 +38,7 @@
 					<div class="list-group">
 					<div class="list-group">
 						<div class="list-group-item">
 						<div class="list-group-item">
 							<h4 class="list-group-item-heading"><?= UOJLocale::get('rating') ?></h4>
 							<h4 class="list-group-item-heading"><?= UOJLocale::get('rating') ?></h4>
-							<p class="list-group-item-text"><strong style="color:red"><?= $user['rating'] ?></strong></p>
+							<p class="list-group-item-text"><strong style="color:red"><?= $user['rating'] ?> (±<?= 2 * round($user['rating_rd']) ?>), vol <?= round($user['rating_vol'] * 100) / 100.0 ?></strong></p>
 						</div>
 						</div>
 						<div class="list-group-item">
 						<div class="list-group-item">
 							<h4 class="list-group-item-heading"><?= UOJLocale::get('email') ?></h4>
 							<h4 class="list-group-item-heading"><?= UOJLocale::get('email') ?></h4>
@@ -98,6 +98,11 @@
 					<h4 class="list-group-item-heading"><?= UOJLocale::get('rating changes') ?></h4>
 					<h4 class="list-group-item-heading"><?= UOJLocale::get('rating changes') ?></h4>
 					<div class="list-group-item-text" id="rating-plot" style="height:500px;"></div>
 					<div class="list-group-item-text" id="rating-plot" style="height:500px;"></div>
 				</div>
 				</div>
+				<div class="list-group-item">
+					<h4 class="list-group-item-heading">Rating 分布</h4>
+					<div class="list-group-item-text" id="rating-dist-plot" style="height:500px;"></div>
+				</div>
+
 				<div class="list-group-item">
 				<div class="list-group-item">
 					<?php
 					<?php
 						$ac_problems = DB::selectAll("select problem_id from best_ac_submissions where submitter = '{$user['username']}'");
 						$ac_problems = DB::selectAll("select problem_id from best_ac_submissions where submitter = '{$user['username']}'");
@@ -257,6 +262,57 @@ $("#rating-plot").bind("plotclick", function (event, pos, item) {
 		prev = -1;
 		prev = -1;
 	}
 	}
 });
 });
+
+(() => {
+	var E = <?php echo $user['rating']; ?>, sigma = <?php echo $user['rating_rd']; ?>;
+	var data = [], step = 0.1;
+	for (var x = E - 6 * sigma; x <= E + 6 * sigma; x += step) {
+		var e = -Math.pow(x - E, 2) / (2 * Math.pow(sigma, 2));
+		var pdf = (1 / (sigma * Math.sqrt(2 * Math.PI))) * Math.exp(e);
+		data.push([x, pdf]);
+	}
+
+	var realratings = [
+		<?php 	
+		$result = DB::query("select contest_id, user_rating from contests_registrants where username = '{$user['username']}' and has_participated = 1");
+		foreach ($result as $r) echo "{$r['user_rating']}, ";
+		echo '0';
+		?>
+	];
+	
+		console.log(realratings);
+
+	var realdata = [];
+	realratings.forEach((x) => {
+		if (x >= E - 6 * sigma && x <= E + 6 * sigma) {
+			var e = -Math.pow(x - E, 2) / (2 * Math.pow(sigma, 2));
+			var pdf = (1 / sigma / Math.sqrt(2 * Math.PI)) * Math.exp(e);
+			realdata.push([x, pdf]);
+		}
+	});
+
+	console.log(realdata);
+
+	$.plot('#rating-dist-plot', [{
+		data: data,
+		color: '#FF0000',
+		lines: { show: true, lineWidth: 2 },
+		points: { show: false }
+	}, {
+		data: realdata,
+		color: '#0000FF',
+		lines: { show: false },
+		points: {
+			show: true,
+			radius: 3,
+			fillColor: '#FFFFFF',
+			symbol: 'circle'
+		}
+	}], {
+		grid: { hoverable: true, borderWidth: 1 },
+		legend: { show: true, position: 'ne' }
+	});
+}) ();
 </script>
 </script>
 <?php else: ?>
 <?php else: ?>
 	<?php echoUOJPageHeader('不存在该用户' . ' - 用户信息') ?>
 	<?php echoUOJPageHeader('不存在该用户' . ' - 用户信息') ?>

BIN
app/libs/.uoj-contest-lib.php.swp


+ 170 - 44
app/libs/uoj-contest-lib.php

@@ -3,9 +3,167 @@ define("CONTEST_NOT_STARTED", 0);
 define("CONTEST_IN_PROGRESS", 1);
 define("CONTEST_IN_PROGRESS", 1);
 define("CONTEST_PENDING_FINAL_TEST", 2);
 define("CONTEST_PENDING_FINAL_TEST", 2);
 define("CONTEST_TESTING", 10);
 define("CONTEST_TESTING", 10);
-define("CONTEST_FINISHED", 20);	
+define("CONTEST_FINISHED", 20);
 
 
-function calcRating($standings, $K = 400) {
+function g2_g($x) {
+	return 1 / sqrt(1 + 3 * $x * $x / M_PI / M_PI);
+}
+
+function g2_E($u, $uj, $pj) {
+	return 1 / (1 + exp(-g2_g($pj) * ($u - $uj)));
+}
+
+function g2_f($x, $delta, $phi, $v, $a, $tau) {
+	return (exp($x) * ($delta * $delta - $phi * $phi - $v - exp($x)) 
+		/ (2 * pow($phi * $phi + $v + exp($x), 2)))
+		- ($x - $a) / $tau / $tau;
+}
+
+function calcRatingGlicko2($standings, &$new_rating, &$new_rd, &$new_vol) {
+	$TAU = 0.6;
+
+	$n = count($standings);
+	$rating = array();
+	$rd = array();
+	$vol = array();
+	for ($i = 0; $i < $n; ++$i) {
+		$rating[$i] = ($standings[$i][2][1] - 1500) / 173.7178;
+		$rd[$i] = $standings[$i][2][2] / 173.7178;
+		$vol[$i] = ($standings[$i][2][3] ?? 0.06);
+		if ($vol[$i] <= 0) $vol[$i] = 0.06;
+	}
+
+	if ($n == 1) {
+		$new_rating[0] = $rating[0];
+		$new_rd[0] = $rd[0];
+		$new_vol[0] = $vol[0];
+	}
+	
+	for ($i = 0; $i < $n; $i++) {
+		$delta = 0;
+		$v = 0;
+
+		for ($j = 0; $j < $n; $j++) if ($i != $j) {
+			$s = 0;
+			if ($standings[$i][3] > $standings[$j][3])
+				$s = 0;
+			else if ($standings[$i][3] == $standings[$j][3])
+				$s = 0.5;
+			else
+				$s = 1;
+			$e = g2_E($rating[$i], $rating[$j], $rd[$j]);
+			$v += pow(g2_g($rd[$j]), 2) * $e * (1 - $e);
+			$delta += g2_g($rd[$j]) * ($s - $e);
+		}
+
+		if ($v < 1e-20) $v = 1e-20;
+		$v = 1 / $v;
+		$delta *= $v;
+
+		if ($delta > 10) $delta = 10;
+		if ($delta < -10) $delta = -10;
+
+		$a = 2 * log($vol[$i]);
+
+		$A = $a;
+		if ($delta * $delta > $rd[$i] * $rd[$i] + $v)
+			$B = log($delta * $delta - $rd[$i] * $rd[$i] - $v);
+		else {
+			for ($k = 1; $k < 100 && g2_f($a - $k * $TAU, $delta, $rd[$i], $v, $a, $TAU) < 0; ++$k)
+				;
+			$B = $a - $k * $TAU;
+		}
+
+		$fa = g2_f($A, $delta, $rd[$i], $v, $a, $TAU);
+		$fb = g2_f($B, $delta, $rd[$i], $v, $a, $TAU);
+		while ($fa * $fb > 0) {
+			$B -= $TAU;
+			$fb = g2_f($B, $delta, $rd[$i], $v, $a, $TAU);
+		}
+
+		$k = 0;
+		while (abs($B - $A) > 1e-6 && $k < 100) {
+			if (!is_finite($fa) || !is_finite($fb)) break;
+			if (abs($fa - $fb) < 1e-15) break;
+			$C = $A + ($A - $B) * $fa / ($fb - $fa);
+			$fc = g2_f($C, $delta, $rd[$i], $v, $a, $TAU);
+			if ($fc * $fb <= 0) {
+				$A = $B;
+				$fa = $fb;
+			}
+			else
+				$fa = $fa / 2;
+			$B = $C;
+			$fb = $fc;
+			++$k;
+		}
+
+		$new_vol[$i] = exp(($A + $B) / 4);
+		$new_rd[$i] = sqrt($rd[$i] * $rd[$i] + $new_vol[$i] * $new_vol[$i]);
+		$new_rd[$i] = 1 / sqrt(1 / $v + 1 / $new_rd[$i] / $new_rd[$i]);
+		$new_rating[$i] = $rating[$i] + $delta / $v * $new_rd[$i] * $new_rd[$i];
+		$new_rating[$i] = $new_rating[$i] * 173.7178 + 1500;
+		$new_rd[$i] *= 173.7178;
+	}
+}
+
+/*
+ * 新的 Rating 计算函数,采用 Codeforces 算法。
+ * 具体算法是:
+ * 设参赛前每人的 Rating 是 r_i, 比赛的最终排名是 R_i,那么
+ * 首先承认:在先前 Rating 的基础上,i 战胜 j 的概率是 P_ij = \dfrac{1}{1 + 10^{\dfrac{r_j - r_i}{400}}}
+ * 立即得到 i 的期望排名 E_i = \sum_{j \ne i} P_ji + 1
+ * 显然,真正的排名和期望排名之间的差距,就是 rating delta 的来源。
+ * 我们取 S_i = \sqrt{E_i R_i},二分出一个 u 使得计算出的 S_i = E_i,rating 应当向 u 的方向变化。
+ * 取 \delta = \dfrac{u + r_i}{2},随后我们对 \delta 进行微调。
+ * 第一次微调:取 d = \dfrac{-1 - \sum_{i} \delta_i}{n},将所有人的 \delta 加上 d。
+ * 第二次微调:取排名前 s = \min{n, 4\sqrt n} 高的人,让这些人的 rating 变化量之和为零,得到 d。
+ * 随后再将所有人的 \delta 加上 d,得到最终的 rating。
+ * */
+function calcRatingX($standings, &$new_ratings, &$new_rating_rds, &$new_rating_vols) {
+	$n = count($standings);
+	$rating = array();
+	$delta = array();
+	$ds = 0;
+	for ($i = 0; $i < $n; ++$i) {
+		$rating[$i] = $standings[$i][2][1];
+		$new_rating_rds[$i] = 0;
+		$new_rating_vols[$i] = 0;
+	}
+	for ($i = 0; $i < $n; ++$i) {
+		$l = 0;
+		$r = 4000;
+		while ($l <= $r) {
+			$mid = ($l + $r) / 2;
+			$e = 1;
+			for ($j = 0; $j < $n; ++$j)
+				if ($i != $j)
+					$e += 1 / (1 + pow(10, ($mid - $rating[$j]) / 400));
+			$s = sqrt($e * ($i + 1));
+			if ($e <= $s)
+				$r = $mid - 1;
+			else
+				$l = $mid + 1;
+		}
+		$delta[$i] = (($l + $r) / 2 - $rating[$i]) / 2;
+		$ds += $delta[$i];
+	}
+	$ds = (-1 - $ds) / $n;
+	for ($i = 0; $i < $n; ++$i)
+		$delta[$i] += $ds;
+	$s = min($n, 4 * sqrt($n));
+	$ds = 0;
+	for ($i = 0; $i < $s; ++$i)
+		$ds += $delta[$i];
+	$ds = -$ds / $s;
+
+	for ($i = 0; $i < $s; ++$i)
+		$delta[$i] += $ds;
+	for ($i = 0; $i < $n; ++$i)
+		$new_ratings[$i] = ceil($rating[$i] + $delta[$i]);
+}
+
+function calcRating($standings, &$new_ratings, &$new_rating_rds, &$new_rating_vols, $K = 400) {
 	$DELTA = 500;
 	$DELTA = 500;
 
 
 	$n = count($standings);
 	$n = count($standings);
@@ -13,6 +171,8 @@ function calcRating($standings, $K = 400) {
 	$rating = array();
 	$rating = array();
 	for ($i = 0; $i < $n; ++$i) {
 	for ($i = 0; $i < $n; ++$i) {
 		$rating[$i] = $standings[$i][2][1];
 		$rating[$i] = $standings[$i][2][1];
+		$new_rating_rds[$i] = 0;
+		$new_rating_vols[$i] = 0;
 	}
 	}
 	
 	
 	$rank = array();
 	$rank = array();
@@ -43,59 +203,25 @@ function calcRating($standings, $K = 400) {
 		}
 		}
 	}
 	}
 	
 	
-	$new_rating = array();
 	for ($i = 0; $i < $n; $i++) {
 	for ($i = 0; $i < $n; $i++) {
-		$new_rating[$i] = $rating[$i];
-		$new_rating[$i] += ceil($K * ($foot[$i] - $exp[$i]) / ($n - 1));
+		$new_ratings[$i] = $rating[$i];
+		$new_ratings[$i] += ceil($K * ($foot[$i] - $exp[$i]) / ($n - 1));
 	}
 	}
 	
 	
 	for ($i = $n - 1; $i >= 0; $i--) {
 	for ($i = $n - 1; $i >= 0; $i--) {
 		if ($i + 1 < $n && $standings[$i][3] != $standings[$i + 1][3]) {
 		if ($i + 1 < $n && $standings[$i][3] != $standings[$i + 1][3]) {
 			break;
 			break;
 		}
 		}
-		if ($new_rating[$i] > $rating[$i]) {
-			$new_rating[$i] = $rating[$i];
+		if ($new_ratings[$i] > $rating[$i]) {
+			$new_ratings[$i] = $rating[$i];
 		}
 		}
 	}
 	}
 	
 	
 	for ($i = 0; $i < $n; $i++) {
 	for ($i = 0; $i < $n; $i++) {
-		if ($new_rating[$i] < 0) {
-			$new_rating[$i] = 0;
+		if ($new_ratings[$i] < 0) {
+			$new_ratings[$i] = 0;
 		}
 		}
 	}
 	}
-	
-	return $new_rating;
-}
-
-function calcRatingSelfTest() {
-	$tests = [
-		[[1500, 1], [1500, 1]],
-		[[1500, 1], [1600, 1]],
-		[[1500, 1], [1600, 2], [1600, 2]],
-		[[1500, 1], [200, 2], [100, 2]],
-		[[1500, 1], [100, 2], [200, 2]],
-		[[1500, 1], [100, 2], [200, 3]],
-		[[1500, 1], [200, 2], [100, 3]],
-		[[1500, 1], [3000, 2], [1500, 3]],
-		[[1500, 1], [3000, 2], [1500, 3], [1500, 3]],
-		[[1500, 1], [1500, 2], [1500, 3], [3000, 4]],
-		[[1500, 1], [1500, 2], [10, 3], [1, 4]]
-	];
-	foreach ($tests as $test_num => $test) {
-		print "test #{$test_num}\n";
-		
-		$standings = array();
-		$n = count($test);
-		for ($i = 0; $i < $n; $i++) {
-			$standings[] = [0, 0, [(string)$i, $test[$i][0]], $test[$i][1]];
-		}
-		$new_rating = calcRating($standings);
-		
-		for ($i = 0; $i < $n; $i++) {
-			printf("%3d: %4d -> %4d delta: %+4d\n", $test[$i][1], $test[$i][0], $new_rating[$i], $new_rating[$i] - $test[$i][0]);
-		}
-		print "\n";
-	}
 }
 }
 
 
 function genMoreContestInfo(&$contest) {
 function genMoreContestInfo(&$contest) {
@@ -175,7 +301,7 @@ function queryContestData($contest, $config = array()) {
 	}
 	}
 
 
 	$people = [];
 	$people = [];
-	$result = DB::query("select username, user_rating from contests_registrants where contest_id = {$contest['id']} and has_participated = 1");
+	$result = DB::query("select username, user_rating, user_rd, user_vol from contests_registrants where contest_id = {$contest['id']} and has_participated = 1");
 	while ($row = DB::fetch($result, MYSQLI_NUM)) {
 	while ($row = DB::fetch($result, MYSQLI_NUM)) {
 		$row[1] = (int)$row[1];
 		$row[1] = (int)$row[1];
 		$people[] = $row;
 		$people[] = $row;
@@ -202,7 +328,7 @@ function calcStandings($contest, $contest_data, &$score, &$standings, $update_co
 		$score[$submission[2]][$submission[3]] = array($submission[4], $penalty, $submission[0]);
 		$score[$submission[2]][$submission[3]] = array($submission[4], $penalty, $submission[0]);
 	}
 	}
 
 
-	// standings: rank => score, penalty, [username, user_rating], virtual_rank
+	// standings: rank => score, penalty, [username, user_rating, user_rd, user_vol], virtual_rank
 	$standings = array();
 	$standings = array();
 	foreach ($contest_data['people'] as $person) {
 	foreach ($contest_data['people'] as $person) {
 		$cur = array(0, 0, $person);
 		$cur = array(0, 0, $person);

+ 1 - 1
app/libs/uoj-html-lib.php

@@ -1322,7 +1322,7 @@ function echoRanklist($config = array()) {
 		
 		
 		$users[] = $user;
 		$users[] = $user;
 	};
 	};
-	$col_names = array('username', 'email', 'qq', 'rating', 'motto');
+	$col_names = array('username', 'email', 'qq', 'rating', 'motto', 'rating_rd');
 	$tail = 'order by rating desc, username asc';
 	$tail = 'order by rating desc, username asc';
 	
 	
 	if (isset($config['top10'])) {
 	if (isset($config['top10'])) {

+ 2 - 2
app/views/page-header.php

@@ -47,7 +47,7 @@
 		<?= HTML::css_link('/css/bootstrap-glyphicons.min.css?v=2019.5.31') ?>
 		<?= HTML::css_link('/css/bootstrap-glyphicons.min.css?v=2019.5.31') ?>
 
 
 		<!-- Custom styles for this template -->
 		<!-- Custom styles for this template -->
-		<?= HTML::css_link('/css/uoj-theme.css?v=2.3333') ?>
+		<?= HTML::css_link('/css/uoj-theme.css?v=3.2223') ?>
 		
 		
 		<!-- jQuery (necessary for Bootstrap\'s JavaScript plugins) -->
 		<!-- jQuery (necessary for Bootstrap\'s JavaScript plugins) -->
 		<?= HTML::js_src('/js/jquery.min.js') ?>
 		<?= HTML::js_src('/js/jquery.min.js') ?>
@@ -79,7 +79,7 @@
 		<?= HTML::js_src('/js/color-converter.min.js') ?>
 		<?= HTML::js_src('/js/color-converter.min.js') ?>
 		
 		
 		<!-- uoj -->
 		<!-- uoj -->
-		<?= HTML::js_src('/js/uoj.js?v=2017.01.01') ?>
+		<?= HTML::js_src('/js/uoj.js?v=2026.09.06') ?>
 		
 		
 		<!-- readmore -->
 		<!-- readmore -->
 		<?= HTML::js_src('/js/readmore/readmore.min.js') ?>
 		<?= HTML::js_src('/js/readmore/readmore.min.js') ?>

BIN
js/.uoj.js.swp


+ 8 - 3
js/uoj.js

@@ -156,7 +156,10 @@ function getUserLink(username, rating, addSymbol, certs) {
 		var certhonor = certs.split ('#')[1];
 		var certhonor = certs.split ('#')[1];
 	}
 	}
 
 
-	var html = '<a class="uoj-username" href="' + uojHome + '/user/profile/' + username + '" style="color:' + getColOfRating(rating) + '">' + text + '</a>';
+	if (username != 'ryp')
+		var html = '<a class="uoj-username" href="' + uojHome + '/user/profile/' + username + '" style="color:' + getColOfRating(rating) + '">' + text + '</a>';
+	else
+		var html = '<a class="uoj-username" href="' + uojHome + '/user/profile/' + username + '" style="color: black">' + text + '</a>';
 	if (certhonor != undefined)
 	if (certhonor != undefined)
 		html += '<span class="uoj-certs uoj-certs-' + certcolor + '">' + certhonor + '</span>';
 		html += '<span class="uoj-certs uoj-certs-' + certcolor + '">' + certhonor + '</span>';
 	return html;
 	return html;
@@ -186,8 +189,10 @@ function getUserSpan(username, rating, addSymbol, certs) {
 		var certcolor = certs.split ('#')[0];
 		var certcolor = certs.split ('#')[0];
 		var certhonor = certs.split ('#')[1];
 		var certhonor = certs.split ('#')[1];
 	}
 	}
-
-	var html = '<span class="uoj-username" style="color:' + getColOfRating(rating) + '">' + text + '</span>';
+	if (username != 'ryp')
+		var html = '<span class="uoj-username" style="color:' + getColOfRating(rating) + '">' + text + '</span>';
+	else
+		var html = '<span class="uoj-username" style="color: black">' + text + '</span>';
 	if (certhonor != undefined)
 	if (certhonor != undefined)
 		html += '<span class="uoj-certs uoj-certs-' + certcolor + '">' + certhonor +  '</span>';
 		html += '<span class="uoj-certs uoj-certs-' + certcolor + '">' + certhonor +  '</span>';
 	return html;
 	return html;