]> git.localhorst.tv Git - alttp.git/commitdiff
time based scoring
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 4 Jan 2026 09:00:36 +0000 (10:00 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 4 Jan 2026 09:00:36 +0000 (10:00 +0100)
app/Models/Round.php
app/Models/Tournament.php
database/migrations/2026_01_04_081256_fractional_scores.php [new file with mode: 0644]
resources/js/components/results/DetailDialog.jsx
resources/js/components/results/TableRow.jsx
resources/js/helpers/Tournament.js
resources/js/i18n/de.js
resources/js/i18n/en.js

index b88aac9ca792e0d8ab234fa385e832570d3819f1..12d8510921385fda185115dccc8a284843c7e34c 100644 (file)
@@ -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();
 
index 71ac44baa2b2b977e83fc29846a37dfdf1fd761a..ce12ace8f8d6aeaf1f001215b14bf0185307f19e 100644 (file)
@@ -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 (file)
index 0000000..501cfbc
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        */
+       public function up(): void
+       {
+               Schema::table('participants', function (Blueprint $table) {
+                       $table->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();
+               });
+       }
+};
index 91f74eab60440ae72ec090001f107be4e84bbb37..2ebd59349ce2cef13677ea91c916107f648faec1 100644 (file)
@@ -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 = ({
                                        <Form.Label>{t('results.placement')}</Form.Label>
                                        <div>
                                                {(maySee || mayVerify) && result && result.placement
-                                                       ? getPlacement(result, t)
+                                                       ? getPlacement(tournament, result, t)
                                                        : t('results.pending')}
                                        </div>
                                </Form.Group>
index 4b697f2e7fde625b4868e3615161e82cdc8bec92..5815527e733aec40c9918830c1ff9505f8ef61e2 100644 (file)
@@ -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 ?
                        <td className="result-placement">
                                {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')}
                        </td>
                : null}
index a65246ffa9257fc0668778e8fb37c30120d35d29..68ca767019ab9f89bae81c09e44634e9f04990cd 100644 (file)
@@ -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);
index db4c5763bf85364f945fd18321a2a08603077330..ba00b051a43f2ca69ccd57dad00c42b28455a128 100644 (file)
@@ -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',
index ca922e4ccc57a4575833939530b08c5db5127ee1..5adc10209cc84424a23b2f5290a85adc4332a8eb 100644 (file)
@@ -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 }}',