diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/ApplyScoresFromRaceLogTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/ApplyScoresFromRaceLogTest.java index 0632d72f20d..5868377e4ac 100644 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/ApplyScoresFromRaceLogTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/ApplyScoresFromRaceLogTest.java @@ -112,7 +112,9 @@ public class ApplyScoresFromRaceLogTest extends LeaderboardScoringAndRankingTest final List rankedCompetitorsBeforeApplying = leaderboard.getCompetitorsFromBestToWorst(later); assertEquals(competitors, rankedCompetitorsBeforeApplying); // no effects of preliminary results list yet f1RaceState.setFinishPositioningConfirmed(now); - final Function expectedPoints = (c)->scores.get(c)==null?(mprs.get(c) == null || mprs.get(c) == MaxPointsReason.NONE ? competitors.indexOf(c)+1 : competitors.size()+1):scores.get(c); + final Function expectedPoints = + (c)->scores.get(c)==null?(mprs.get(c) == null || mprs.get(c) == MaxPointsReason.NONE ? + competitors.indexOf(c)+1 : competitors.size()+1):scores.get(c); for (final Competitor c : competitors) { assertEquals(expectedPoints.apply(c), leaderboard.getTotalPoints(c, f1Column, now), 0.00000001); } @@ -169,16 +171,22 @@ public class ApplyScoresFromRaceLogTest extends LeaderboardScoringAndRankingTest } /** - * See bug 4025: When a scoring race log event is received it may contain a partial competitor set. It shall depend on - * the previous state resulting from the race log, without the last event, what needs to be done with the leaderboard - * score corrections. If a competitor which which an earlier message did have a correction now no longer is part of - * the latest scoring race log event, and if the score corrections in the leaderboard match up with what the previous - * event had caused, remove that score correction again. + * See bugs 4025 and 4167: When a scoring race log event is received it may contain a partial competitor set. Before bug4167 + * there was an implementation that reset score corrections if previous messages had a result for a competitor, and later + * messages did not. With bug 4167 we found that it should be possible to send in partial results, owed to the insight that + * multiple devices may be used for the same race, not all in sync at all times, therefore producing partial results that + * should not clear or overwrite valid results input by other devices.

+ * + * Therefore, now, with bug4167, this test has been modified such that first it ensures that a partial result + * does not clear a previous score correction. Furthermore it tests that when sending a result that + * has 0 as the rank and {@link MaxPointsReason#NONE} as the {@link MaxPointsReason} with no explicit score set, + * the score correction for the competitor will be reset in the leaderboard. */ @Test public void testApplyingOCSThenClearingOCS() { TimePoint now = MillisecondsTimePoint.now(); - TimePoint later = new MillisecondsTimePoint(now.asMillis()+1000); + TimePoint later = now.plus(1000); + TimePoint yetLater = later.plus(1000); final int numberOfCompetitors = 2; setUp(numberOfCompetitors, now, ScoringSchemeType.LOW_POINT); final CompetitorResults results = new CompetitorResultsImpl(); @@ -202,9 +210,15 @@ public class ApplyScoresFromRaceLogTest extends LeaderboardScoringAndRankingTest setResultForCompetitor(competitors.get(1), /* one-based rank */ 0, resultsWithOCSCleared, MaxPointsReason.OCS, /* explicit score */ null); f1RaceState.setFinishPositioningListChanged(later, resultsWithOCSCleared); f1RaceState.setFinishPositioningConfirmed(later); - // validate that it got cleared in leaderboard + // validate that it did not get cleared in leaderboard (changed by bug 4167): assertScoreCorrections(leaderboard, f1Column, competitors.get(1), MaxPointsReason.OCS, numberOfCompetitors+1, /* score is corrected */ false, later); - assertScoreCorrections(leaderboard, f1Column, competitors.get(0), MaxPointsReason.NONE, 1, /* score is corrected */ false, later); // ranking first, one point in low-point scheme + assertScoreCorrections(leaderboard, f1Column, competitors.get(0), MaxPointsReason.OCS, numberOfCompetitors+1, /* score is corrected */ false, later); // ranking first, one point in low-point scheme + // now explicitly overwrite with a 0/null/null result, validating that this will clear the corrections for competitor 0: + setResultForCompetitor(competitors.get(0), /* one-based rank */ 0, resultsWithOCSCleared, /* maxPointsReason */ null, /* explicit score */ null); + f1RaceState.setFinishPositioningListChanged(yetLater, resultsWithOCSCleared); + f1RaceState.setFinishPositioningConfirmed(yetLater); + assertScoreCorrections(leaderboard, f1Column, competitors.get(1), MaxPointsReason.OCS, numberOfCompetitors+1, /* score is corrected */ false, yetLater); + assertScoreCorrections(leaderboard, f1Column, competitors.get(0), MaxPointsReason.NONE, 1, /* score is corrected */ false, yetLater); // ranking first, one point in low-point scheme } private void assertScoreCorrections(Leaderboard leaderboard, RaceColumn raceColumn, Competitor competitor, @@ -219,7 +233,4 @@ public class ApplyScoresFromRaceLogTest extends LeaderboardScoringAndRankingTest final CompetitorResults results, MaxPointsReason maxPointsReason, Double explicitScore) { results.add(new CompetitorResultImpl(competitor.getId(), competitor.getName(), oneBasedRank, maxPointsReason, explicitScore, /* finishingTime */ null, /* comment */ null)); } - - - } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java index 5dc1b0c243e..fb0466c461d 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java @@ -71,15 +71,19 @@ public class RaceLogScoringReplicator implements RaceColumnListenerWithDefaultAc *

  • When no {@link CompetitorResult} object is received for a competitor, no change is applied to that * competitor's score corrections.
  • *
  • When a {@link CompetitorResult} object is received for a competitor, if it contains {@code null} for the - * score and 0 for the rank, any existing score correction (points) is removed for that competitor, otherwise, the + * score and 0 for the rank or {@code null} for the score and a valid non-{@link MaxPointsReason#NONE NONE} + * {@link MaxPointsReason}, any existing score correction (points) is removed for that competitor, otherwise, the * score correction (points) from the CompetitorResult object is applied if provided (in this case a rank provided - * in the {@link CompetitorResult} object is ignored); if only a rank but no score is provided in the - * {@link CompetitorResult}, the scoring scheme will compute the score from the rank, and that score will be applied - * as a score correction.
  • + * in the {@link CompetitorResult} object is ignored); if no {@link MaxPointsReason} is provided and only a rank but + * no score is provided in the {@link CompetitorResult}, the scoring scheme will compute the score from the rank, + * considering any {@link MaxPointsReason} provided in the result, and that score will be applied as a score + * correction. If a {@link MaxPointsReason} but no explicit score is provided, the rank is ignored because + * the scoring scheme is expected to compute the score from the {@link MaxPointsReason}; therefore, the score + * correction (points) is cleared in this case so that the scoring scheme will be used for score calculation. *
  • When a {@link CompetitorResult} object is received for a competitor, if it contains {@code null} for the * {@link MaxPointsReason}, any existing {@link MaxPointsReason} is removed for that competitor; otherwise, the - * {@link MaxPointsReason} from the {@link CompetitorResult} object is applied. This is handled entirely - * independently of the score (points).
  • + * {@link MaxPointsReason} from the {@link CompetitorResult} object is applied, and if no explicit score is provided + * in the result, the score is computed by the scoring scheme, ignoring any rank provided in the result. * *

    * @@ -116,7 +120,7 @@ public class RaceLogScoringReplicator implements RaceColumnListenerWithDefaultAc final Competitor competitor = service.getBaseDomainFactory().getExistingCompetitorById(positionedCompetitor.getCompetitorId()); int rankByRaceCommittee = getRankInPositioningListByRaceCommittee(positionedCompetitor); correctScoreInLeaderboardIfNecessary(leaderboard, raceColumn, timePoint, numberOfCompetitorsInRace, competitor, - rankByRaceCommittee, positionedCompetitor.getScore()); + rankByRaceCommittee, positionedCompetitor.getScore(), positionedCompetitor.getMaxPointsReason()); setMaxPointsReasonInLeaderboardIfNecessary(leaderboard, raceColumn, timePoint, positionedCompetitor.getMaxPointsReason(), competitor); } // Since the metadata update is used by the Sailing suite to determine the final state of a race, it has to @@ -137,11 +141,13 @@ public class RaceLogScoringReplicator implements RaceColumnListenerWithDefaultAc } } - private void correctScoreInLeaderboardIfNecessary(Leaderboard leaderboard, RaceColumn raceColumn, TimePoint timePoint, - final int numberOfCompetitorsInRace, - Competitor competitor, int rankByRaceCommittee, Double optionalExplicitScore) { - final Double scoreByRaceCommittee = getScoreFromRaceCommittee(leaderboard, raceColumn, timePoint, - numberOfCompetitorsInRace, competitor, rankByRaceCommittee, optionalExplicitScore); + private void correctScoreInLeaderboardIfNecessary(Leaderboard leaderboard, RaceColumn raceColumn, + TimePoint timePoint, final int numberOfCompetitorsInRace, Competitor competitor, int rankByRaceCommittee, + Double optionalExplicitScore, MaxPointsReason maxPointsReason) { + final Double scoreByRaceCommittee = optionalExplicitScore==null && maxPointsReason != null && + maxPointsReason != MaxPointsReason.NONE ? null : + getScoreFromRaceCommittee(leaderboard, raceColumn, timePoint, + numberOfCompetitorsInRace, competitor, rankByRaceCommittee, optionalExplicitScore, maxPointsReason); // Apply the score correction if it will cause a change in the explicit score set for the competitor if (!Util.equalsWithNull(leaderboard.getScoreCorrection().getExplicitScoreCorrection(competitor, raceColumn), scoreByRaceCommittee)) { applyScoreCorrectionOperation(leaderboard, raceColumn, competitor, scoreByRaceCommittee, timePoint); @@ -149,19 +155,28 @@ public class RaceLogScoringReplicator implements RaceColumnListenerWithDefaultAc } /** - * If a non-{@code null} {@code optionalExplicitScore} is provided, it it returned. Otherwise, a score is determined - * from the {@code oneBasedRankByRaceCommittee} using the leaderboard scoring scheme's + * If a non-{@code null} {@code optionalExplicitScore} is provided, it it returned. Otherwise, if a valid + * {@link MaxPointsReason} is provided, {@code null} is returned as the explicit score because the scoring scheme + * shall be used dynamically to determine the penalty score (see + * {@link ScoringScheme#getPenaltyScore(RaceColumn, Competitor, MaxPointsReason, Integer, com.sap.sailing.domain.leaderboard.NumberOfCompetitorsInLeaderboardFetcher, TimePoint)}). + * If no explicit score is provided and no {@link MaxPointsReason} is specified, a score is determined from the + * {@code oneBasedRankByRaceCommittee} using the leaderboard scoring scheme's * {@link ScoringScheme#getScoreForRank(Leaderboard, RaceColumn, Competitor, int, java.util.concurrent.Callable, com.sap.sailing.domain.leaderboard.NumberOfCompetitorsInLeaderboardFetcher, TimePoint) * getScoreForRank(...)} method. Usually, scoring schemes will return {@code null} as score for {@code 0} as * (one-based) rank. */ private Double getScoreFromRaceCommittee(Leaderboard leaderboard, RaceColumn raceColumn, TimePoint timePoint, final int numberOfCompetitorsInRace, Competitor competitor, int oneBasedRankByRaceCommittee, - Double optionalExplicitScore) { + Double optionalExplicitScore, MaxPointsReason maxPointsReason) { final Double scoreByRaceCommittee; if (optionalExplicitScore == null) { - scoreByRaceCommittee = leaderboard.getScoringScheme().getScoreForRank(leaderboard, raceColumn, competitor, - oneBasedRankByRaceCommittee, ()->numberOfCompetitorsInRace, leaderboard.getNumberOfCompetitorsInLeaderboardFetcher(), timePoint); + if (maxPointsReason != null && maxPointsReason != MaxPointsReason.NONE) { + // let scoring scheme calculate the penalty score dynamically + scoreByRaceCommittee = null; + } else { + scoreByRaceCommittee = leaderboard.getScoringScheme().getScoreForRank(leaderboard, raceColumn, competitor, + oneBasedRankByRaceCommittee, ()->numberOfCompetitorsInRace, leaderboard.getNumberOfCompetitorsInLeaderboardFetcher(), timePoint); + } } else { scoreByRaceCommittee = optionalExplicitScore; }