From: Daniel Karbach Date: Sun, 4 Jan 2026 09:00:36 +0000 (+0100) Subject: time based scoring X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=c4c4296609c2a419b4f2968a2b9d3f31938132ee;p=alttp.git time based scoring --- diff --git a/app/Models/Round.php b/app/Models/Round.php index b88aac9..12d8510 100644 --- a/app/Models/Round.php +++ b/app/Models/Round.php @@ -36,7 +36,44 @@ class Round extends Model } public function updatePlacement(): void { - if (!$this->tournament->hasFixedRunners()) { + if ($this->tournament->hasTimeBasedScoring()) { + $results = $this->results->sort([Result::class, 'compareResult']); + $reversed = $results->reverse(); + + $reference_times = []; + foreach ($reversed as $result) { + $time = $result->getEffectiveTime(); + if ($time > 0) { + $reference_times[] = $time; + } + if (count($reference_times) >= 5) { + break; + } + } + $reference_time = empty($reference_times) ? 0 : array_sum($reference_times) / count($reference_times); + + $running = 0; + $bonus = 1; + $lastResult = null; + foreach ($reversed as $result) { + $betterThanLast = is_null($lastResult) || $result->getEffectiveTime() < $lastResult; + if (!$result->disqualified && !$result->forfeit && $betterThanLast) { + $running += $bonus; + $lastResult = $result->getEffectiveTime(); + $bonus = 1; + } else { + ++$bonus; + } + if ($result->disqualified) { + $result->updatePlacement(0, count($results) + 1); + } elseif ($result->forfeit) { + $result->updatePlacement(0, count($results)); + } else { + $score = $reference_time > 0 ? ($reference_time / $result->getEffectiveTime() * 100) : 0; + $result->updatePlacement($score, count($results) - $running + 1); + } + } + } elseif (!$this->tournament->hasFixedRunners()) { $results = $this->results->sort([Result::class, 'compareResult']); $reversed = $results->reverse(); diff --git a/app/Models/Tournament.php b/app/Models/Tournament.php index 71ac44b..ce12ace 100644 --- a/app/Models/Tournament.php +++ b/app/Models/Tournament.php @@ -17,10 +17,18 @@ class Tournament extends Model { } + public function hasAssignedGroups(): bool { + return in_array($this->type, ['open-grouped-async']); + } + public function hasFixedRunners(): bool { return in_array($this->type, ['signup-async']); } + public function hasTimeBasedScoring(): bool { + return in_array($this->type, ['open-grouped-async']); + } + public function getRunners() { $runners = []; foreach ($this->participants as $participant) { diff --git a/database/migrations/2026_01_04_081256_fractional_scores.php b/database/migrations/2026_01_04_081256_fractional_scores.php new file mode 100644 index 0000000..501cfbc --- /dev/null +++ b/database/migrations/2026_01_04_081256_fractional_scores.php @@ -0,0 +1,34 @@ +double('score')->nullable()->default(null)->change(); + }); + Schema::table('results', function (Blueprint $table) { + $table->double('score')->nullable()->default(null)->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('participants', function (Blueprint $table) { + $table->integer('score')->nullable()->default(null)->change(); + }); + Schema::table('results', function (Blueprint $table) { + $table->integer('score')->nullable()->default(null)->change(); + }); + } +}; diff --git a/resources/js/components/results/DetailDialog.jsx b/resources/js/components/results/DetailDialog.jsx index 91f74ea..2ebd593 100644 --- a/resources/js/components/results/DetailDialog.jsx +++ b/resources/js/components/results/DetailDialog.jsx @@ -10,11 +10,16 @@ import Box from '../users/Box'; import { formatTime, getTime } from '../../helpers/Result'; import { formatNumberAlways } from '../../helpers/Round'; import { mayModifyResult, maySeeResult, mayVerifyResult } from '../../helpers/permissions'; +import { getScoreFormatOptions } from '../../helpers/Tournament'; import { findResult } from '../../helpers/User'; import { useUser } from '../../hooks/user'; +import i18n from '../../i18n'; -const getPlacement = (result, t) => - `${result.placement}. (${t('results.points', { count: result.score })})`; +const getPlacement = (tournament, result, t) => + `${result.placement}. (${t('results.points', { + count: result.score, + score: i18n.number(result.score, getScoreFormatOptions(tournament)), + })})`; const DetailDialog = ({ actions, @@ -79,7 +84,7 @@ const DetailDialog = ({ {t('results.placement')}
{(maySee || mayVerify) && result && result.placement - ? getPlacement(result, t) + ? getPlacement(tournament, result, t) : t('results.pending')}
diff --git a/resources/js/components/results/TableRow.jsx b/resources/js/components/results/TableRow.jsx index 4b697f2..5815527 100644 --- a/resources/js/components/results/TableRow.jsx +++ b/resources/js/components/results/TableRow.jsx @@ -7,8 +7,10 @@ import VodLink from './VodLink'; import ResultProtocol from '../protocol/ResultProtocol'; import Box from '../users/Box'; import { maySeeResult, mayVerifyResult } from '../../helpers/permissions'; +import { getScoreFormatOptions } from '../../helpers/Tournament'; import { findResult } from '../../helpers/User'; import { useUser } from '../../hooks/user'; +import i18n from '../../i18n'; const TableRow = ({ actions, @@ -57,7 +59,10 @@ const TableRow = ({ {showPlacement ? {maySee && result && result.placement - ? `${result.placement}. (${t('results.points', { count: result.score })})` + ? `${result.placement}. (${t('results.points', { + count: result.score, + score: i18n.number(result.score, getScoreFormatOptions(tournament)), + })})` : t('results.pending')} : null} diff --git a/resources/js/helpers/Tournament.js b/resources/js/helpers/Tournament.js index a65246f..68ca767 100644 --- a/resources/js/helpers/Tournament.js +++ b/resources/js/helpers/Tournament.js @@ -205,6 +205,13 @@ export const hasScoreboard = tournament => !!(tournament && tournament.type === export const hasSignup = tournament => !!(tournament && tournament.type === 'signup-async'); +export const hasTimeBasedScoring = tournament => (tournament?.type === 'open-grouped-async'); + +export const getScoreFormatOptions = tournament => (hasTimeBasedScoring(tournament) + ? { decimals: 2 } + : { decimals: 0 } +); + export const getScoreTable = tournament => { if (!tournament || !tournament.rounds || !tournament.rounds.length) return []; const runners = getRunners(tournament); diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index db4c576..ba00b05 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -646,8 +646,8 @@ export default { modifySuccess: 'Ergebnis angepasst', pending: 'Ausstehend', placement: 'Platzierung', - points_one: '{{ count }} Punkt', - points_other: '{{ count }} Punkte', + points_one: '{{ score }} Punkt', + points_other: '{{ score }} Punkte', report: 'Ergebnis eintragen', reportError: 'Fehler beim Eintragen :(', reportPreview: 'Wird als {{ time }} festgehalten', diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index ca922e4..5adc102 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -647,8 +647,8 @@ export default { modifySuccess: 'Result modified', pending: 'Pending', placement: 'Placement', - points_one: '{{ count }} point', - points_other: '{{ count }} points', + points_one: '{{ score }} point', + points_other: '{{ score }} points', report: 'Report result', reportError: 'Error saving :(', reportPreview: 'Will be recorded as {{ time }}',