diff --git a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/RaceResultOfCompetitorWithContext.java b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/RaceResultOfCompetitorWithContext.java index f0e7b4a0831..3648c6802b7 100644 --- a/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/RaceResultOfCompetitorWithContext.java +++ b/java/com.sap.sailing.datamining/src/com/sap/sailing/datamining/impl/data/RaceResultOfCompetitorWithContext.java @@ -209,15 +209,8 @@ public class RaceResultOfCompetitorWithContext implements HasRaceResultOfCompeti @Override public Boolean isWin() { - Leaderboard leaderboard = getLeaderboard(); final TimePoint now = MillisecondsTimePoint.now(); - double points = leaderboard.getTotalPoints(competitor, raceColumn, now); - if (leaderboard.getScoringScheme().isHigherBetter()) { - double competitorCount = Util.size(leaderboard.getCompetitors()); - return points >= (competitorCount - 0.05); - } else { - return points <= 1.05; - } + return getLeaderboard().isWin(competitor, raceColumn, now); } } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/ScoringSchemeType.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/ScoringSchemeType.java index c419b8d1bd1..c90803d5eda 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/ScoringSchemeType.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/ScoringSchemeType.java @@ -5,5 +5,5 @@ public enum ScoringSchemeType { LOW_POINT, LOW_POINT_WITH_AUTOMATIC_RDG, HIGH_POINT, HIGH_POINT_ESS_OVERALL,HIGH_POINT_ESS_OVERALL_12, HIGH_POINT_LAST_BREAKS_TIE, HIGH_POINT_FIRST_GETS_TEN, HIGH_POINT_FIRST_GETS_ONE, LOW_POINT_WINNER_GETS_ZERO, HIGH_POINT_WINNER_GETS_SIX, HIGH_POINT_WINNER_GETS_FIVE, HIGH_POINT_WINNER_GETS_EIGHT, HIGH_POINT_WINNER_GETS_EIGHT_AND_INTERPOLATION, HIGH_POINT_FIRST_GETS_TEN_OR_EIGHT, HIGH_POINT_FIRST_GETS_TWELVE_OR_EIGHT, HIGH_POINT_FIRST_GETS_TWELVE_OR_EIGHT_2017, LOW_POINT_WITH_ELIMINATIONS_AND_ROUNDS_WINNER_GETS_07, - LOW_POINT_LEAGUE_OVERALL, HIGH_POINT_MATCH_RACING, LOW_POINT_TIE_BREAK_BASED_ON_LAST_SERIES_ONLY + LOW_POINT_LEAGUE_OVERALL, HIGH_POINT_MATCH_RACING, LOW_POINT_TIE_BREAK_BASED_ON_LAST_SERIES_ONLY, LOW_POINT_FIRST_TO_WIN_TWO_RACES } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTestForLowPoints.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTestForLowPoints.java new file mode 100644 index 00000000000..8bbc958e8b1 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTestForLowPoints.java @@ -0,0 +1,900 @@ +package com.sap.sailing.domain.test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.junit.Test; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.SeriesImpl; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.impl.DelegatingRegattaLeaderboardWithCompetitorElimination; +import com.sap.sailing.domain.leaderboard.impl.LowPointFirstToWinTwoRaces; +import com.sap.sailing.domain.ranking.OneDesignRankingMetric; +import com.sap.sailing.domain.test.mock.MockedTrackedRaceWithStartTimeAndRanks; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +import junit.framework.Assert; + +/** + * This class contains several tests for the {@link LowPointFirstToWinTwoRaces} scoring rule defined by + * {@link ScoringSchemeType#LOW_POINT_FIRST_TO_WIN_TWO_RACES}. It tests that the carry is correctly applied and that the + * ordering is as expected. Furthermore it contains several negative tests, that validate, that the normal low point + * behavior is not changed and still works for those same cases in case the {@link ScoringSchemeType#LOW_POINT} scoring + * scheme is used. + */ +public class LeaderboardScoringAndRankingTestForLowPoints extends LeaderboardScoringAndRankingTestBase { + private static final double EPSILON = 0.000001; + + protected DelegatingRegattaLeaderboardWithCompetitorElimination createDelegatingRegattaLeaderboardWithCompetitorElimination( + Regatta regatta, String leaderboardName, int[] discardingThresholds) { + return new DelegatingRegattaLeaderboardWithCompetitorElimination( + () -> createLeaderboard(regatta, discardingThresholds), leaderboardName); + } + + private void executePreSeries(List yellow, List blue, TimePoint now) { + RaceColumn qColumn = series.get(0).getRaceColumnByName("Q"); + TrackedRace qYellow = new MockedTrackedRaceWithStartTimeAndRanks(now, yellow); + qColumn.setTrackedRace(qColumn.getFleetByName("Yellow"), qYellow); + TrackedRace qBlue = new MockedTrackedRaceWithStartTimeAndRanks(now, blue); + qColumn.setTrackedRace(qColumn.getFleetByName("Blue"), qBlue); + } + + private void manuallyTransferCarry(Leaderboard leaderboard, List medalCompetitors) { + int carryScore = 1; + for (Competitor medalCompetitor : medalCompetitors) { + leaderboard.getScoreCorrection().correctScore(medalCompetitor, leaderboard.getRaceColumnByName("Carry"), carryScore++); + } + } + + private Regatta setupRegatta(boolean useFirstTwoWins) { + final BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("470", /* typicallyStartsUpwind */ true); + Regatta regatta = new RegattaImpl(RegattaImpl.getDefaultName("Test Regatta", boatClass.getName()), boatClass, + false, /* startDate */ null, /* endDate */ null, series, /* persistent */false, + DomainFactory.INSTANCE + .createScoringScheme(useFirstTwoWins ? ScoringSchemeType.LOW_POINT_FIRST_TO_WIN_TWO_RACES + : ScoringSchemeType.LOW_POINT), + "123", /* course area */null, OneDesignRankingMetric::new); + return regatta; + } + + private void setupMedalSeriesWithCarryOverAndFourRaceColumns() { + Set medalFleets = Collections.singleton(new FleetImpl("Default")); + List medalRaceColumnNames = new ArrayList(); + medalRaceColumnNames.add("Carry"); + medalRaceColumnNames.add("M1"); + medalRaceColumnNames.add("M2"); + medalRaceColumnNames.add("M3"); + medalRaceColumnNames.add("M4"); + Series medalSeries = new SeriesImpl("Medal", /* isMedal */true, /* isFleetsCanRunInParallel */ true, + medalFleets, medalRaceColumnNames, /* trackedRegattaRegistry */ null); + medalSeries.setFirstColumnIsNonDiscardableCarryForward(true); + medalSeries.setStartsWithZeroScore(true); + series.add(medalSeries); + } + + private void setupQualificationSeriesWithOneRaceColumn() { + List qualificationFleets = new ArrayList(); + for (String qualificationFleetName : new String[] { "Yellow", "Blue" }) { + qualificationFleets.add(new FleetImpl(qualificationFleetName)); + } + List qualificationRaceColumnNames = new ArrayList(); + qualificationRaceColumnNames.add("Q"); + Series qualificationSeries = new SeriesImpl("Qualification", /* isMedal */false, + /* isFleetsCanRunInParallel */ true, qualificationFleets, qualificationRaceColumnNames, + /* trackedRegattaRegistry */ null); + series.add(qualificationSeries); + } + + private List> createCompetitorResultForTimestamp(TimePoint time, Leaderboard leaderboard) { + List> list = new ArrayList<>(); + for (Competitor competitor : leaderboard.getCompetitorsFromBestToWorst(time)) { + list.add(new Pair(competitor, leaderboard.getNetPoints(competitor, time))); + } + return list; + } + + private void assertNonFinalistsAreBehindFinalistsAndNotChanged( + List> preSeriesScoreRankResult, List> afterFinalResults) { + List> nonFinalistsPreScore = preSeriesScoreRankResult.subList(4, + preSeriesScoreRankResult.size()); + List> nonFinalistsAfterScore = afterFinalResults.subList(4, afterFinalResults.size()); + Assert.assertEquals(nonFinalistsPreScore, nonFinalistsAfterScore); + } + + /** + * In this test the preseries winner will win the first race, and should get a score of 2, all other finalists + * should be scored with Low_Points restarting at 0 for the medal series. The non finalists score should not change + * during the medalseries. + */ + @Test + public void testFirstPreseriesWinsAgain() throws NoWindException { + series = new ArrayList(); + setupQualificationSeriesWithOneRaceColumn(); + + setupMedalSeriesWithCarryOverAndFourRaceColumns(); + Regatta regatta = setupRegatta(true); + List competitors = createCompetitors(12); + List yellow = new ArrayList<>(competitors.subList(0, 6)); + List blue = new ArrayList<>(competitors); + blue.removeAll(yellow); + Collections.shuffle(yellow); + Collections.shuffle(blue); + + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + TimePoint now = MillisecondsTimePoint.now(); + TimePoint later = new MillisecondsTimePoint(now.asMillis() + 1000); + executePreSeries(yellow, blue, now); + + List preSeriesRankResult = leaderboard.getCompetitorsFromBestToWorst(later); + List medalCompetitorsBestToWorst = preSeriesRankResult.subList(0, 4); + List> preSeriesScoreRankResult = createCompetitorResultForTimestamp(later, + leaderboard); + + manuallyTransferCarry(leaderboard, medalCompetitorsBestToWorst); + + Competitor preFirst = medalCompetitorsBestToWorst.get(0); + Competitor preSecond = medalCompetitorsBestToWorst.get(1); + Competitor preThird = medalCompetitorsBestToWorst.get(2); + Competitor preFourth = medalCompetitorsBestToWorst.get(3); + + RaceColumn m1 = leaderboard.getRaceColumnByName("M1"); + + ArrayList m1Results = new ArrayList<>(); + m1Results.add(preFirst); + m1Results.add(preSecond); + m1Results.add(preThird); + m1Results.add(preFourth); + + TrackedRace mDefault = new MockedTrackedRaceWithStartTimeAndRanks(now, m1Results); + m1.setTrackedRace(m1.getFleetByName("Default"), mDefault); + + Assert.assertEquals(2.0, leaderboard.getNetPoints(preFirst, later), EPSILON); + Assert.assertEquals(4, leaderboard.getNetPoints(preSecond, later), EPSILON); + Assert.assertEquals(6, leaderboard.getNetPoints(preThird, later), EPSILON); + Assert.assertEquals(8, leaderboard.getNetPoints(preFourth, later), EPSILON); + + List> afterFinalResults = createCompetitorResultForTimestamp(later, leaderboard); + assertNonFinalistsAreBehindFinalistsAndNotChanged(preSeriesScoreRankResult, afterFinalResults); + } + + /** + * In this test the second best in the preseries wins the first two medal races, and should get a score of 2, all + * other finalists should be scored with Low_Points restarting at 0 for the medal series. The non finalists score + * should not change during the medalseries. + */ + @Test + public void testSecondPreSeriesWinsTwice() throws NoWindException { + series = new ArrayList(); + // -------- qualification series ------------ + setupQualificationSeriesWithOneRaceColumn(); + setupMedalSeriesWithCarryOverAndFourRaceColumns(); + Regatta regatta = setupRegatta(true); + List competitors = createCompetitors(12); + List yellow = new ArrayList<>(competitors.subList(0, 6)); + List blue = new ArrayList<>(competitors); + blue.removeAll(yellow); + Collections.shuffle(yellow); + Collections.shuffle(blue); + + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + TimePoint now = MillisecondsTimePoint.now(); + TimePoint later = new MillisecondsTimePoint(now.asMillis() + 1000); + executePreSeries(yellow, blue, now); + + List preSeriesRankResult = leaderboard.getCompetitorsFromBestToWorst(later); + List medalCompetitorsBestToWorst = preSeriesRankResult.subList(0, 4); + List> preSeriesScoreRankResult = createCompetitorResultForTimestamp(later, + leaderboard); + manuallyTransferCarry(leaderboard, medalCompetitorsBestToWorst); + + Competitor preFirst = medalCompetitorsBestToWorst.get(0); + Competitor preSecond = medalCompetitorsBestToWorst.get(1); + Competitor preThird = medalCompetitorsBestToWorst.get(2); + Competitor preFourth = medalCompetitorsBestToWorst.get(3); + + RaceColumn m1 = leaderboard.getRaceColumnByName("M1"); + + ArrayList m1Results = new ArrayList<>(); + m1Results.add(preSecond); + m1Results.add(preFirst); + m1Results.add(preThird); + m1Results.add(preFourth); + + TimePoint m1now = new MillisecondsTimePoint(later.asMillis() + 1000); + TimePoint m1later = new MillisecondsTimePoint(m1now.asMillis() + 1000); + + TrackedRace m1Default = new MockedTrackedRaceWithStartTimeAndRanks(now, m1Results); + m1.setTrackedRace(m1.getFleetByName("Default"), m1Default); + + Assert.assertEquals(3, leaderboard.getNetPoints(preSecond, later), EPSILON); + Assert.assertEquals(3, leaderboard.getNetPoints(preFirst, later), EPSILON); + Assert.assertEquals(6, leaderboard.getNetPoints(preThird, later), EPSILON); + Assert.assertEquals(8, leaderboard.getNetPoints(preFourth, later), EPSILON); + + List res = leaderboard.getCompetitorsFromBestToWorst(later); + Assert.assertEquals(res.get(0), preFirst); + Assert.assertEquals(res.get(1), preSecond); + + RaceColumn m2 = leaderboard.getRaceColumnByName("M2"); + + ArrayList m2Results = new ArrayList<>(); + m2Results.add(preSecond); + m2Results.add(preFirst); + m2Results.add(preThird); + m2Results.add(preFourth); + + TimePoint m2now = new MillisecondsTimePoint(m1later.asMillis() + 1000); + TimePoint m2later = new MillisecondsTimePoint(m2now.asMillis() + 1000); + TrackedRace m2Default = new MockedTrackedRaceWithStartTimeAndRanks(m2now, m2Results); + m2.setTrackedRace(m1.getFleetByName("Default"), m2Default); + + Assert.assertEquals(4.0, leaderboard.getNetPoints(preSecond, m2later), EPSILON); + Assert.assertEquals(5.0, leaderboard.getNetPoints(preFirst, m2later), EPSILON); + Assert.assertEquals(9, leaderboard.getNetPoints(preThird, m2later), EPSILON); + Assert.assertEquals(12, leaderboard.getNetPoints(preFourth, m2later), EPSILON); + + List res2 = leaderboard.getCompetitorsFromBestToWorst(m2later); + Assert.assertEquals(res2.get(0), preSecond); + Assert.assertEquals(res2.get(1), preFirst); + Assert.assertEquals(res2.get(2), preThird); + Assert.assertEquals(res2.get(3), preFourth); + + List> afterFinalResults = createCompetitorResultForTimestamp(m2later, leaderboard); + assertNonFinalistsAreBehindFinalistsAndNotChanged(preSeriesScoreRankResult, afterFinalResults); + } + + /** + * In this test the best in the preseries wins the second medal race, and should get a score of 2, all other + * finalists should be scored with Low_Points restarting at 0 for the medal series. The non finalists score should + * not change during the medalseries. Also asserts that during tie-breaking the pre-series carry score takes + * precedence. + */ + @Test + public void testFirstWinsSecondRace() throws NoWindException { + series = new ArrayList(); + // -------- qualification series ------------ + setupQualificationSeriesWithOneRaceColumn(); + setupMedalSeriesWithCarryOverAndFourRaceColumns(); + Regatta regatta = setupRegatta(true); + List competitors = createCompetitors(12); + List yellow = new ArrayList<>(competitors.subList(0, 6)); + List blue = new ArrayList<>(competitors); + blue.removeAll(yellow); + Collections.shuffle(yellow); + Collections.shuffle(blue); + + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + TimePoint now = MillisecondsTimePoint.now(); + TimePoint later = new MillisecondsTimePoint(now.asMillis() + 1000); + executePreSeries(yellow, blue, now); + + List preSeriesRankResult = leaderboard.getCompetitorsFromBestToWorst(later); + List medalCompetitorsBestToWorst = preSeriesRankResult.subList(0, 4); + List> preSeriesScoreRankResult = createCompetitorResultForTimestamp(later, + leaderboard); + manuallyTransferCarry(leaderboard, medalCompetitorsBestToWorst); + + Competitor preFirst = medalCompetitorsBestToWorst.get(0); + Competitor preSecond = medalCompetitorsBestToWorst.get(1); + Competitor preThird = medalCompetitorsBestToWorst.get(2); + Competitor preFourth = medalCompetitorsBestToWorst.get(3); + + RaceColumn m1 = leaderboard.getRaceColumnByName("M1"); + + ArrayList m1Results = new ArrayList<>(); + m1Results.add(preSecond); + m1Results.add(preFirst); + m1Results.add(preThird); + m1Results.add(preFourth); + + TimePoint m1now = new MillisecondsTimePoint(later.asMillis() + 1000); + TimePoint m1later = new MillisecondsTimePoint(m1now.asMillis() + 1000); + + TrackedRace m1Default = new MockedTrackedRaceWithStartTimeAndRanks(now, m1Results); + m1.setTrackedRace(m1.getFleetByName("Default"), m1Default); + + Assert.assertEquals(3, leaderboard.getNetPoints(preSecond, later), EPSILON); + Assert.assertEquals(3, leaderboard.getNetPoints(preFirst, later), EPSILON); + Assert.assertEquals(6, leaderboard.getNetPoints(preThird, later), EPSILON); + Assert.assertEquals(8, leaderboard.getNetPoints(preFourth, later), EPSILON); + // assert that during tie-breaking the pre-series carry score takes precedence + List res = leaderboard.getCompetitorsFromBestToWorst(later); + Assert.assertEquals(res.get(0), preFirst); + Assert.assertEquals(res.get(1), preSecond); + + RaceColumn m2 = leaderboard.getRaceColumnByName("M2"); + + ArrayList m2Results = new ArrayList<>(); + m2Results.add(preFirst); + m2Results.add(preSecond); + m2Results.add(preThird); + m2Results.add(preFourth); + + TimePoint m2now = new MillisecondsTimePoint(m1later.asMillis() + 1000); + TimePoint m2later = new MillisecondsTimePoint(m2now.asMillis() + 1000); + TrackedRace m2Default = new MockedTrackedRaceWithStartTimeAndRanks(m2now, m2Results); + m2.setTrackedRace(m1.getFleetByName("Default"), m2Default); + + Assert.assertEquals(4, leaderboard.getNetPoints(preFirst, m2later), EPSILON); + Assert.assertEquals(5, leaderboard.getNetPoints(preSecond, m2later), EPSILON); + Assert.assertEquals(9, leaderboard.getNetPoints(preThird, m2later), EPSILON); + Assert.assertEquals(12, leaderboard.getNetPoints(preFourth, m2later), EPSILON); + + List res2 = leaderboard.getCompetitorsFromBestToWorst(m2later); + Assert.assertEquals(res2.get(0), preFirst); + Assert.assertEquals(res2.get(1), preSecond); + Assert.assertEquals(res2.get(2), preThird); + Assert.assertEquals(res2.get(3), preFourth); + + List> afterFinalResults = createCompetitorResultForTimestamp(m2later, leaderboard); + assertNonFinalistsAreBehindFinalistsAndNotChanged(preSeriesScoreRankResult, afterFinalResults); + } + + /** + * In this test the worst finalist of the qualification wins the first and second medal races, and should get a + * score of 2, all other finalists should be scored with Low_Points restarting at 0 for the medal series. The non + * finalists score should not change during the medalseries + */ + @Test + public void testLastWinsTwoRaces() throws NoWindException { + series = new ArrayList(); + // -------- qualification series ------------ + setupQualificationSeriesWithOneRaceColumn(); + setupMedalSeriesWithCarryOverAndFourRaceColumns(); + Regatta regatta = setupRegatta(true); + List competitors = createCompetitors(12); + List yellow = new ArrayList<>(competitors.subList(0, 6)); + List blue = new ArrayList<>(competitors); + blue.removeAll(yellow); + Collections.shuffle(yellow); + Collections.shuffle(blue); + + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + TimePoint now = MillisecondsTimePoint.now(); + TimePoint later = new MillisecondsTimePoint(now.asMillis() + 1000); + executePreSeries(yellow, blue, now); + + List preSeriesRankResult = leaderboard.getCompetitorsFromBestToWorst(later); + List medalCompetitorsBestToWorst = preSeriesRankResult.subList(0, 4); + List> preSeriesScoreRankResult = createCompetitorResultForTimestamp(later, + leaderboard); + manuallyTransferCarry(leaderboard, medalCompetitorsBestToWorst); + + Competitor preFirst = medalCompetitorsBestToWorst.get(0); + Competitor preSecond = medalCompetitorsBestToWorst.get(1); + Competitor preThird = medalCompetitorsBestToWorst.get(2); + Competitor preFourth = medalCompetitorsBestToWorst.get(3); + + RaceColumn m1 = leaderboard.getRaceColumnByName("M1"); + + ArrayList m1Results = new ArrayList<>(); + m1Results.add(preFourth); + m1Results.add(preFirst); + m1Results.add(preSecond); + m1Results.add(preThird); + + TimePoint m1now = new MillisecondsTimePoint(later.asMillis() + 1000); + TimePoint m1later = new MillisecondsTimePoint(m1now.asMillis() + 1000); + + TrackedRace m1Default = new MockedTrackedRaceWithStartTimeAndRanks(now, m1Results); + m1.setTrackedRace(m1.getFleetByName("Default"), m1Default); + + Assert.assertEquals(5, leaderboard.getNetPoints(preFourth, later), EPSILON); + Assert.assertEquals(3.0, leaderboard.getNetPoints(preFirst, later), EPSILON); + Assert.assertEquals(5, leaderboard.getNetPoints(preSecond, later), EPSILON); + Assert.assertEquals(7, leaderboard.getNetPoints(preThird, later), EPSILON); + + List res = leaderboard.getCompetitorsFromBestToWorst(later); + Assert.assertEquals(res.get(0), preFirst); + Assert.assertEquals(res.get(1), preSecond); + Assert.assertEquals(res.get(2), preFourth); + + RaceColumn m2 = leaderboard.getRaceColumnByName("M2"); + + ArrayList m2Results = new ArrayList<>(); + m2Results.add(preFourth); + m2Results.add(preFirst); + m2Results.add(preSecond); + m2Results.add(preThird); + + TimePoint m2now = new MillisecondsTimePoint(m1later.asMillis() + 1000); + TimePoint m2later = new MillisecondsTimePoint(m2now.asMillis() + 1000); + TrackedRace m2Default = new MockedTrackedRaceWithStartTimeAndRanks(m2now, m2Results); + m2.setTrackedRace(m1.getFleetByName("Default"), m2Default); + + Assert.assertEquals(6.0, leaderboard.getNetPoints(preFourth, m2later), EPSILON); + Assert.assertEquals(5.0, leaderboard.getNetPoints(preFirst, m2later), EPSILON); + Assert.assertEquals(8, leaderboard.getNetPoints(preSecond, m2later), EPSILON); + Assert.assertEquals(11, leaderboard.getNetPoints(preThird, m2later), EPSILON); + + List res2 = leaderboard.getCompetitorsFromBestToWorst(m2later); + Assert.assertEquals(res2.get(0), preFourth); + Assert.assertEquals(res2.get(1), preFirst); + Assert.assertEquals(res2.get(2), preSecond); + Assert.assertEquals(res2.get(3), preThird); + + List> afterFinalResults = createCompetitorResultForTimestamp(m2later, leaderboard); + assertNonFinalistsAreBehindFinalistsAndNotChanged(preSeriesScoreRankResult, afterFinalResults); + } + + /** + * In this test no finalist reaches two wins before terminating (eg. due to bad weather) all finalists should be + * scored with Low_Points restarting at 0 for the medal series. The non finalists score should not change during the + * medalseries. + */ + @Test + public void noTwoWinsAbortAfterTwoRacesAndTieBreaker() throws NoWindException { + series = new ArrayList(); + // -------- qualification series ------------ + setupQualificationSeriesWithOneRaceColumn(); + setupMedalSeriesWithCarryOverAndFourRaceColumns(); + Regatta regatta = setupRegatta(true); + List competitors = createCompetitors(12); + List yellow = new ArrayList<>(competitors.subList(0, 6)); + List blue = new ArrayList<>(competitors); + blue.removeAll(yellow); + Collections.shuffle(yellow); + Collections.shuffle(blue); + + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + TimePoint now = MillisecondsTimePoint.now(); + TimePoint later = new MillisecondsTimePoint(now.asMillis() + 1000); + executePreSeries(yellow, blue, now); + + List preSeriesRankResult = leaderboard.getCompetitorsFromBestToWorst(later); + List medalCompetitorsBestToWorst = preSeriesRankResult.subList(0, 4); + List> preSeriesScoreRankResult = createCompetitorResultForTimestamp(later, + leaderboard); + manuallyTransferCarry(leaderboard, medalCompetitorsBestToWorst); + + Competitor preFirst = medalCompetitorsBestToWorst.get(0); + Competitor preSecond = medalCompetitorsBestToWorst.get(1); + Competitor preThird = medalCompetitorsBestToWorst.get(2); + Competitor preFourth = medalCompetitorsBestToWorst.get(3); + + RaceColumn m1 = leaderboard.getRaceColumnByName("M1"); + + ArrayList m1Results = new ArrayList<>(); + m1Results.add(preFourth); + m1Results.add(preFirst); + m1Results.add(preSecond); + m1Results.add(preThird); + + TimePoint m1now = new MillisecondsTimePoint(later.asMillis() + 1000); + TimePoint m1later = new MillisecondsTimePoint(m1now.asMillis() + 1000); + + TrackedRace m1Default = new MockedTrackedRaceWithStartTimeAndRanks(now, m1Results); + m1.setTrackedRace(m1.getFleetByName("Default"), m1Default); + + Assert.assertEquals(5, leaderboard.getNetPoints(preFourth, later), EPSILON); + Assert.assertEquals(3.0, leaderboard.getNetPoints(preFirst, later), EPSILON); + Assert.assertEquals(5, leaderboard.getNetPoints(preSecond, later), EPSILON); + Assert.assertEquals(7, leaderboard.getNetPoints(preThird, later), EPSILON); + + + List res = leaderboard.getCompetitorsFromBestToWorst(later); + Assert.assertEquals(res.get(0), preFirst); + Assert.assertEquals(res.get(1), preSecond); + Assert.assertEquals(res.get(2), preFourth); + + RaceColumn m2 = leaderboard.getRaceColumnByName("M2"); + + ArrayList m2Results = new ArrayList<>(); + m2Results.add(preThird); + m2Results.add(preFirst); + m2Results.add(preSecond); + m2Results.add(preFourth); + + TimePoint m2now = new MillisecondsTimePoint(m1later.asMillis() + 1000); + TimePoint m2later = new MillisecondsTimePoint(m2now.asMillis() + 1000); + TrackedRace m2Default = new MockedTrackedRaceWithStartTimeAndRanks(m2now, m2Results); + m2.setTrackedRace(m1.getFleetByName("Default"), m2Default); + + Assert.assertEquals(9, leaderboard.getNetPoints(preFourth, m2later), EPSILON); + Assert.assertEquals(5.0, leaderboard.getNetPoints(preFirst, m2later), EPSILON); + Assert.assertEquals(8, leaderboard.getNetPoints(preSecond, m2later), EPSILON); + Assert.assertEquals(8, leaderboard.getNetPoints(preThird, m2later), EPSILON); + + List> afterFinalResults = createCompetitorResultForTimestamp(m2later, leaderboard); + assertNonFinalistsAreBehindFinalistsAndNotChanged(preSeriesScoreRankResult, afterFinalResults); + } + + /** + * Using normal lowpoints, being the best in the preseries and having an additional win in the medals, should not + * make you the overall winner in case another finalist has a better score. + */ + @Test + public void negativeTestFirstPreseriesWinsAgain() throws NoWindException { + series = new ArrayList(); + setupQualificationSeriesWithOneRaceColumn(); + + setupMedalSeriesWithCarryOverAndFourRaceColumns(); + Regatta regatta = setupRegatta(false); + List competitors = createCompetitors(12); + List yellow = new ArrayList<>(competitors.subList(0, 6)); + List blue = new ArrayList<>(competitors); + blue.removeAll(yellow); + Collections.shuffle(yellow); + Collections.shuffle(blue); + + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + TimePoint now = MillisecondsTimePoint.now(); + TimePoint later = new MillisecondsTimePoint(now.asMillis() + 1000); + executePreSeries(yellow, blue, now); + + List preSeriesRankResult = leaderboard.getCompetitorsFromBestToWorst(later); + List> preSeriesScoreRankResult = createCompetitorResultForTimestamp(later, + leaderboard); + List medalCompetitors = preSeriesRankResult.subList(0, 4); + + manuallyTransferCarry(leaderboard, medalCompetitors); + + Competitor preFirst = medalCompetitors.get(0); + Competitor preSecond = medalCompetitors.get(1); + Competitor preThird = medalCompetitors.get(2); + Competitor preFourth = medalCompetitors.get(3); + + RaceColumn m1 = leaderboard.getRaceColumnByName("M1"); + + ArrayList m1Results = new ArrayList<>(); + m1Results.add(preFirst); + m1Results.add(preSecond); + m1Results.add(preThird); + m1Results.add(preFourth); + + TrackedRace mDefault = new MockedTrackedRaceWithStartTimeAndRanks(now, m1Results); + m1.setTrackedRace(m1.getFleetByName("Default"), mDefault); + + Assert.assertEquals(4, leaderboard.getNetPoints(preFirst, later), EPSILON); + Assert.assertEquals(8, leaderboard.getNetPoints(preSecond, later), EPSILON); + Assert.assertEquals(12, leaderboard.getNetPoints(preThird, later), EPSILON); + Assert.assertEquals(16, leaderboard.getNetPoints(preFourth, later), EPSILON); + Assert.assertEquals(1, leaderboard.getTotalRankOfCompetitor(preFirst, later)); + Assert.assertEquals(2, leaderboard.getTotalRankOfCompetitor(preSecond, later)); + Assert.assertEquals(3, leaderboard.getTotalRankOfCompetitor(preThird, later)); + Assert.assertEquals(4, leaderboard.getTotalRankOfCompetitor(preFourth, later)); + + List> afterFinalResults = createCompetitorResultForTimestamp(later, leaderboard); + assertNonFinalistsAreBehindFinalistsAndNotChanged(preSeriesScoreRankResult, afterFinalResults); + } + + /** + * Using normal lowpoints, having two wins should not matter, also the racecolum factor is expected to be 2 instead + * of 1. + */ + @Test + public void negativeTestSecondPreSeriesWinsTwice() throws NoWindException { + series = new ArrayList(); + // -------- qualification series ------------ + setupQualificationSeriesWithOneRaceColumn(); + setupMedalSeriesWithCarryOverAndFourRaceColumns(); + Regatta regatta = setupRegatta(false); + List competitors = createCompetitors(12); + List yellow = new ArrayList<>(competitors.subList(0, 6)); + List blue = new ArrayList<>(competitors); + blue.removeAll(yellow); + Collections.shuffle(yellow); + Collections.shuffle(blue); + + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + TimePoint now = MillisecondsTimePoint.now(); + TimePoint later = new MillisecondsTimePoint(now.asMillis() + 1000); + executePreSeries(yellow, blue, now); + + List preSeriesRankResult = leaderboard.getCompetitorsFromBestToWorst(later); + List medalCompetitorsBestToWorst = preSeriesRankResult.subList(0, 4); + List> preSeriesScoreRankResult = createCompetitorResultForTimestamp(later, + leaderboard); + manuallyTransferCarry(leaderboard, medalCompetitorsBestToWorst); + + Competitor preFirst = medalCompetitorsBestToWorst.get(0); + Competitor preSecond = medalCompetitorsBestToWorst.get(1); + Competitor preThird = medalCompetitorsBestToWorst.get(2); + Competitor preFourth = medalCompetitorsBestToWorst.get(3); + + RaceColumn m1 = leaderboard.getRaceColumnByName("M1"); + + ArrayList m1Results = new ArrayList<>(); + m1Results.add(preSecond); + m1Results.add(preFirst); + m1Results.add(preThird); + m1Results.add(preFourth); + + TimePoint m1now = new MillisecondsTimePoint(later.asMillis() + 1000); + TimePoint m1later = new MillisecondsTimePoint(m1now.asMillis() + 1000); + + TrackedRace m1Default = new MockedTrackedRaceWithStartTimeAndRanks(now, m1Results); + m1.setTrackedRace(m1.getFleetByName("Default"), m1Default); + + Assert.assertEquals(6, leaderboard.getNetPoints(preSecond, later), EPSILON); + Assert.assertEquals(6.0, leaderboard.getNetPoints(preFirst, later), EPSILON); + Assert.assertEquals(12, leaderboard.getNetPoints(preThird, later), EPSILON); + Assert.assertEquals(16, leaderboard.getNetPoints(preFourth, later), EPSILON); + + RaceColumn m2 = leaderboard.getRaceColumnByName("M2"); + + ArrayList m2Results = new ArrayList<>(); + m2Results.add(preSecond); + m2Results.add(preFirst); + m2Results.add(preThird); + m2Results.add(preFourth); + + TimePoint m2now = new MillisecondsTimePoint(m1later.asMillis() + 1000); + TimePoint m2later = new MillisecondsTimePoint(m2now.asMillis() + 1000); + TrackedRace m2Default = new MockedTrackedRaceWithStartTimeAndRanks(m2now, m2Results); + m2.setTrackedRace(m1.getFleetByName("Default"), m2Default); + + Assert.assertEquals(8, leaderboard.getNetPoints(preSecond, m2later), EPSILON); + Assert.assertEquals(10.0, leaderboard.getNetPoints(preFirst, m2later), EPSILON); + Assert.assertEquals(18, leaderboard.getNetPoints(preThird, m2later), EPSILON); + Assert.assertEquals(24, leaderboard.getNetPoints(preFourth, m2later), EPSILON); + Assert.assertEquals(1, leaderboard.getTotalRankOfCompetitor(preSecond, m2later)); + Assert.assertEquals(2, leaderboard.getTotalRankOfCompetitor(preFirst, m2later)); + Assert.assertEquals(3, leaderboard.getTotalRankOfCompetitor(preThird, m2later)); + Assert.assertEquals(4, leaderboard.getTotalRankOfCompetitor(preFourth, m2later)); + List> afterFinalResults = createCompetitorResultForTimestamp(m2later, leaderboard); + assertNonFinalistsAreBehindFinalistsAndNotChanged(preSeriesScoreRankResult, afterFinalResults); + } + + /** + * Using normal lowpoints, having two wins should not matter, also the racecolum factor is expected to be 2 instead + * of 1. + */ + @Test + public void negativeTestFirstWinsSecondRace() throws NoWindException { + series = new ArrayList(); + // -------- qualification series ------------ + setupQualificationSeriesWithOneRaceColumn(); + setupMedalSeriesWithCarryOverAndFourRaceColumns(); + Regatta regatta = setupRegatta(false); + List competitors = createCompetitors(12); + List yellow = new ArrayList<>(competitors.subList(0, 6)); + List blue = new ArrayList<>(competitors); + blue.removeAll(yellow); + Collections.shuffle(yellow); + Collections.shuffle(blue); + + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + TimePoint now = MillisecondsTimePoint.now(); + TimePoint later = new MillisecondsTimePoint(now.asMillis() + 1000); + executePreSeries(yellow, blue, now); + + List preSeriesRankResult = leaderboard.getCompetitorsFromBestToWorst(later); + List medalCompetitorsBestToWorst = preSeriesRankResult.subList(0, 4); + List> preSeriesScoreRankResult = createCompetitorResultForTimestamp(later, + leaderboard); + manuallyTransferCarry(leaderboard, medalCompetitorsBestToWorst); + + Competitor preFirst = medalCompetitorsBestToWorst.get(0); + Competitor preSecond = medalCompetitorsBestToWorst.get(1); + Competitor preThird = medalCompetitorsBestToWorst.get(2); + Competitor preFourth = medalCompetitorsBestToWorst.get(3); + + RaceColumn m1 = leaderboard.getRaceColumnByName("M1"); + + ArrayList m1Results = new ArrayList<>(); + m1Results.add(preSecond); + m1Results.add(preFirst); + m1Results.add(preThird); + m1Results.add(preFourth); + + TimePoint m1now = new MillisecondsTimePoint(later.asMillis() + 1000); + TimePoint m1later = new MillisecondsTimePoint(m1now.asMillis() + 1000); + + TrackedRace m1Default = new MockedTrackedRaceWithStartTimeAndRanks(now, m1Results); + m1.setTrackedRace(m1.getFleetByName("Default"), m1Default); + + Assert.assertEquals(6, leaderboard.getNetPoints(preSecond, later), EPSILON); + Assert.assertEquals(6, leaderboard.getNetPoints(preFirst, later), EPSILON); + Assert.assertEquals(12, leaderboard.getNetPoints(preThird, later), EPSILON); + Assert.assertEquals(16, leaderboard.getNetPoints(preFourth, later), EPSILON); + + + List res = leaderboard.getCompetitorsFromBestToWorst(later); + Assert.assertEquals(res.get(0), preSecond); + Assert.assertEquals(res.get(1), preFirst); + + RaceColumn m2 = leaderboard.getRaceColumnByName("M2"); + + ArrayList m2Results = new ArrayList<>(); + m2Results.add(preFirst); + m2Results.add(preSecond); + m2Results.add(preThird); + m2Results.add(preFourth); + + TimePoint m2now = new MillisecondsTimePoint(m1later.asMillis() + 1000); + TimePoint m2later = new MillisecondsTimePoint(m2now.asMillis() + 1000); + TrackedRace m2Default = new MockedTrackedRaceWithStartTimeAndRanks(m2now, m2Results); + m2.setTrackedRace(m1.getFleetByName("Default"), m2Default); + + Assert.assertEquals(8, leaderboard.getNetPoints(preFirst, m2later), EPSILON); + Assert.assertEquals(10, leaderboard.getNetPoints(preSecond, m2later), EPSILON); + Assert.assertEquals(18, leaderboard.getNetPoints(preThird, m2later), EPSILON); + Assert.assertEquals(24, leaderboard.getNetPoints(preFourth, m2later), EPSILON); + + List> afterFinalResults = createCompetitorResultForTimestamp(m2later, leaderboard); + assertNonFinalistsAreBehindFinalistsAndNotChanged(preSeriesScoreRankResult, afterFinalResults); + } + + /** + * Using normal lowpoints, the racecolum factor is expected to be 2 instead of 1. + */ + @Test + public void negativeTestLastWinsTwoRaces() throws NoWindException { + series = new ArrayList(); + // -------- qualification series ------------ + setupQualificationSeriesWithOneRaceColumn(); + setupMedalSeriesWithCarryOverAndFourRaceColumns(); + Regatta regatta = setupRegatta(false); + List competitors = createCompetitors(12); + List yellow = new ArrayList<>(competitors.subList(0, 6)); + List blue = new ArrayList<>(competitors); + blue.removeAll(yellow); + Collections.shuffle(yellow); + Collections.shuffle(blue); + + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + TimePoint now = MillisecondsTimePoint.now(); + TimePoint later = new MillisecondsTimePoint(now.asMillis() + 1000); + executePreSeries(yellow, blue, now); + + List preSeriesRankResult = leaderboard.getCompetitorsFromBestToWorst(later); + List medalCompetitorsBestToWorst = preSeriesRankResult.subList(0, 4); + List> preSeriesScoreRankResult = createCompetitorResultForTimestamp(later, + leaderboard); + + manuallyTransferCarry(leaderboard, medalCompetitorsBestToWorst); + + Competitor preFirst = medalCompetitorsBestToWorst.get(0); + Competitor preSecond = medalCompetitorsBestToWorst.get(1); + Competitor preThird = medalCompetitorsBestToWorst.get(2); + Competitor preFourth = medalCompetitorsBestToWorst.get(3); + + RaceColumn m1 = leaderboard.getRaceColumnByName("M1"); + + ArrayList m1Results = new ArrayList<>(); + m1Results.add(preFourth); + m1Results.add(preFirst); + m1Results.add(preSecond); + m1Results.add(preThird); + + TimePoint m1now = new MillisecondsTimePoint(later.asMillis() + 1000); + TimePoint m1later = new MillisecondsTimePoint(m1now.asMillis() + 1000); + + TrackedRace m1Default = new MockedTrackedRaceWithStartTimeAndRanks(now, m1Results); + m1.setTrackedRace(m1.getFleetByName("Default"), m1Default); + + Assert.assertEquals(10, leaderboard.getNetPoints(preFourth, later), EPSILON); + Assert.assertEquals(6.0, leaderboard.getNetPoints(preFirst, later), EPSILON); + Assert.assertEquals(10, leaderboard.getNetPoints(preSecond, later), EPSILON); + Assert.assertEquals(14, leaderboard.getNetPoints(preThird, later), EPSILON); + + List res = leaderboard.getCompetitorsFromBestToWorst(later); + Assert.assertEquals(res.get(0), preFirst); + Assert.assertEquals(res.get(1), preFourth); + Assert.assertEquals(res.get(2), preSecond); + + RaceColumn m2 = leaderboard.getRaceColumnByName("M2"); + + ArrayList m2Results = new ArrayList<>(); + m2Results.add(preFourth); + m2Results.add(preFirst); + m2Results.add(preSecond); + m2Results.add(preThird); + + TimePoint m2now = new MillisecondsTimePoint(m1later.asMillis() + 1000); + TimePoint m2later = new MillisecondsTimePoint(m2now.asMillis() + 1000); + TrackedRace m2Default = new MockedTrackedRaceWithStartTimeAndRanks(m2now, m2Results); + m2.setTrackedRace(m1.getFleetByName("Default"), m2Default); + + Assert.assertEquals(12, leaderboard.getNetPoints(preFourth, m2later), EPSILON); + Assert.assertEquals(10, leaderboard.getNetPoints(preFirst, m2later), EPSILON); + Assert.assertEquals(16, leaderboard.getNetPoints(preSecond, m2later), EPSILON); + Assert.assertEquals(22, leaderboard.getNetPoints(preThird, m2later), EPSILON); + + List> afterFinalResults = createCompetitorResultForTimestamp(m2later, leaderboard); + assertNonFinalistsAreBehindFinalistsAndNotChanged(preSeriesScoreRankResult, afterFinalResults); + } + + /** + * Using normal lowpoints, tie braking is done with manual carry decimals. + */ + @Test + public void negativeNoTwoWinsAbortAfterTwoRacesAndTieBreaker() throws NoWindException { + series = new ArrayList(); + // -------- qualification series ------------ + setupQualificationSeriesWithOneRaceColumn(); + setupMedalSeriesWithCarryOverAndFourRaceColumns(); + Regatta regatta = setupRegatta(false); + List competitors = createCompetitors(12); + List yellow = new ArrayList<>(competitors.subList(0, 6)); + List blue = new ArrayList<>(competitors); + blue.removeAll(yellow); + Collections.shuffle(yellow); + Collections.shuffle(blue); + + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + TimePoint now = MillisecondsTimePoint.now(); + TimePoint later = new MillisecondsTimePoint(now.asMillis() + 1000); + executePreSeries(yellow, blue, now); + + List preSeriesRankResult = leaderboard.getCompetitorsFromBestToWorst(later); + List medalCompetitorsBestToWorst = preSeriesRankResult.subList(0, 4); + List> preSeriesScoreRankResult = createCompetitorResultForTimestamp(later, + leaderboard); + manuallyTransferCarry(leaderboard, medalCompetitorsBestToWorst); + + Competitor preFirst = medalCompetitorsBestToWorst.get(0); + Competitor preSecond = medalCompetitorsBestToWorst.get(1); + Competitor preThird = medalCompetitorsBestToWorst.get(2); + Competitor preFourth = medalCompetitorsBestToWorst.get(3); + + RaceColumn m1 = leaderboard.getRaceColumnByName("M1"); + + ArrayList m1Results = new ArrayList<>(); + m1Results.add(preFourth); + m1Results.add(preFirst); + m1Results.add(preSecond); + m1Results.add(preThird); + + TimePoint m1now = new MillisecondsTimePoint(later.asMillis() + 1000); + TimePoint m1later = new MillisecondsTimePoint(m1now.asMillis() + 1000); + + TrackedRace m1Default = new MockedTrackedRaceWithStartTimeAndRanks(now, m1Results); + m1.setTrackedRace(m1.getFleetByName("Default"), m1Default); + + Assert.assertEquals(10, leaderboard.getNetPoints(preFourth, later), EPSILON); + Assert.assertEquals(6.0, leaderboard.getNetPoints(preFirst, later), EPSILON); + Assert.assertEquals(10, leaderboard.getNetPoints(preSecond, later), EPSILON); + Assert.assertEquals(14, leaderboard.getNetPoints(preThird, later), EPSILON); + + List res = leaderboard.getCompetitorsFromBestToWorst(later); + Assert.assertEquals(res.get(0), preFirst); + Assert.assertEquals(res.get(1), preFourth); + Assert.assertEquals(res.get(2), preSecond); + + RaceColumn m2 = leaderboard.getRaceColumnByName("M2"); + + ArrayList m2Results = new ArrayList<>(); + m2Results.add(preThird); + m2Results.add(preFirst); + m2Results.add(preSecond); + m2Results.add(preFourth); + + TimePoint m2now = new MillisecondsTimePoint(m1later.asMillis() + 1000); + TimePoint m2later = new MillisecondsTimePoint(m2now.asMillis() + 1000); + TrackedRace m2Default = new MockedTrackedRaceWithStartTimeAndRanks(m2now, m2Results); + m2.setTrackedRace(m1.getFleetByName("Default"), m2Default); + + Assert.assertEquals(18, leaderboard.getNetPoints(preFourth, m2later), EPSILON); + Assert.assertEquals(10, leaderboard.getNetPoints(preFirst, m2later), EPSILON); + Assert.assertEquals(16, leaderboard.getNetPoints(preSecond, m2later), EPSILON); + Assert.assertEquals(16, leaderboard.getNetPoints(preThird, m2later), EPSILON); + + List res2 = leaderboard.getCompetitorsFromBestToWorst(m2later); + Assert.assertEquals(res2.get(0), preFirst); + Assert.assertEquals(res2.get(1), preThird); + Assert.assertEquals(res2.get(2), preSecond); + Assert.assertEquals(res2.get(3), preFourth); + + List> afterFinalResults = createCompetitorResultForTimestamp(m2later, leaderboard); + assertNonFinalistsAreBehindFinalistsAndNotChanged(preSeriesScoreRankResult, afterFinalResults); + } + +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/OfflineSerializationTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/OfflineSerializationTest.java index c9b8983bc9e..214b0952b15 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/OfflineSerializationTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/OfflineSerializationTest.java @@ -137,8 +137,8 @@ public class OfflineSerializationTest extends AbstractSerializationTest { DomainFactory receiverDomainFactory = new DomainFactoryImpl((srlid)->null); LeaderboardGroup clone = cloneBySerialization(leaderboardGroup, receiverDomainFactory); - assertEquals(FACTOR, overallLeaderboard.getRaceColumnByName("Test Leaderboard").getFactor(), 0.00000001); - assertEquals(FACTOR, clone.getOverallLeaderboard().getRaceColumnByName("Test Leaderboard").getFactor(), 0.00000001); + assertEquals(FACTOR, overallLeaderboard.getScoringScheme().getScoreFactor(overallLeaderboard.getRaceColumnByName("Test Leaderboard")), 0.00000001); + assertEquals(FACTOR, clone.getOverallLeaderboard().getScoringScheme().getScoreFactor(clone.getOverallLeaderboard().getRaceColumnByName("Test Leaderboard")), 0.00000001); } @Test diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/RaceColumn.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/RaceColumn.java index 8f6c1afbf14..253ab268a12 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/RaceColumn.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/RaceColumn.java @@ -162,14 +162,6 @@ public interface RaceColumn extends Named { */ void releaseTrackedRace(Fleet fleet); - /** - * Usually, the scores in each leaderboard column count as they are for the overall score. However, if a column is - * a medal race column it usually counts double. Under certain circumstances, columns may also count with factors different - * from 1 or 2. For example, we've seen cases in the Extreme Sailing Series where the race committee defined that in the - * overall series leaderboard the last two columns each count 1.5 times their scores. - */ - double getFactor(); - /** * By default, a competitor's total score is computed by summing up the non-discarded total points of each race * across the leaderboard, considering the {@link RaceColumn#getFactor() column factors}. Some race columns, diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DomainFactoryImpl.java index f86247656d9..01e73aeacfe 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DomainFactoryImpl.java @@ -58,6 +58,7 @@ import com.sap.sailing.domain.leaderboard.impl.HighPointWinnerGetsEightAndInterp import com.sap.sailing.domain.leaderboard.impl.HighPointWinnerGetsFive; import com.sap.sailing.domain.leaderboard.impl.HighPointWinnerGetsSix; import com.sap.sailing.domain.leaderboard.impl.LowPoint; +import com.sap.sailing.domain.leaderboard.impl.LowPointFirstToWinTwoRaces; import com.sap.sailing.domain.leaderboard.impl.LowPointForLeagueOverallLeaderboard; import com.sap.sailing.domain.leaderboard.impl.LowPointTieBreakBasedOnLastSeriesOnly; import com.sap.sailing.domain.leaderboard.impl.LowPointWinnerGetsZero; @@ -144,6 +145,10 @@ public class DomainFactoryImpl extends SharedDomainFactoryImpl implements Domain return new LowPointTieBreakBasedOnLastSeriesOnly(); case LOW_POINT_WITH_AUTOMATIC_RDG: return new LowPointWithAutomaticRDG(); + case LOW_POINT_FIRST_TO_WIN_TWO_RACES: + return new LowPointFirstToWinTwoRaces(); + default: + break; } throw new RuntimeException("Unknown scoring scheme type "+scoringSchemeType.name()); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SimpleAbstractRaceColumn.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SimpleAbstractRaceColumn.java index e76b1f77ca8..19966720fd0 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SimpleAbstractRaceColumn.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/SimpleAbstractRaceColumn.java @@ -20,13 +20,6 @@ public abstract class SimpleAbstractRaceColumn implements RaceColumn { private static final long serialVersionUID = -3590156714385187908L; private final RaceColumnListeners raceColumnListeners; - /** - * The factor by which a medal race score is multiplied by default in the overall point scheme. - * - * @see #getFactor() - */ - private static final double DEFAULT_MEDAL_RACE_FACTOR = 2.0; - /** * If null, the {@link #getFactor() factor} defaults to 1 for non-medal and {@link #DEFAULT_MEDAL_RACE_FACTOR} for * medal races. Otherwise, the explicit factor is used. @@ -66,11 +59,6 @@ public abstract class SimpleAbstractRaceColumn implements RaceColumn { return result; } - @Override - public double getFactor() { - return explicitFactor == null ? (isMedalRace() ? DEFAULT_MEDAL_RACE_FACTOR : 1.) : explicitFactor; - } - @Override public void setFactor(Double factor) { Double oldExplicitFactor = getExplicitFactor(); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java index 098bdb90cf9..1149ec9004c 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/Leaderboard.java @@ -604,4 +604,22 @@ public interface Leaderboard extends LeaderboardBase, HasRaceColumns { * leaderboard. */ boolean hasScores(Competitor competitor, TimePoint timePoint); -} + + /** + * Returns true if a racecolumn evaluates to be a win for the given competitor at the given timepoint. + * If the competitor is not scored for this race, false is returned + */ + default boolean isWin(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + final Double points = getTotalPoints(competitor, raceColumn, timePoint); + final boolean result; + if (points == null) { + result = false; + } else if (getScoringScheme().isHigherBetter()) { + double competitorCount = Util.size(getCompetitors()); + result = points >= (competitorCount - 0.05); + } else { + result = points <= 1.05; + } + return result; + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoringScheme.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoringScheme.java index ec65a4d43be..8964a073e76 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoringScheme.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoringScheme.java @@ -8,6 +8,7 @@ import java.util.concurrent.Callable; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.RaceColumn; import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.ScoringSchemeType; @@ -25,6 +26,13 @@ import com.sap.sse.common.Util; * */ public interface ScoringScheme extends Serializable { + /** + * The factor by which a medal race score is multiplied by default in the overall point scheme. + * + * @see #getFactor() + */ + static final double DEFAULT_MEDAL_RACE_FACTOR = 2.0; + /** * If this returns true, a higher score is better. For example, the Extreme Sailing Series uses this * scoring scheme, as opposed to the olympic sailing classes which use a low-point system. @@ -119,4 +127,51 @@ public interface ScoringScheme extends Serializable { * @throws NoWindException */ int compareByLatestRegattaInMetaLeaderboard(Leaderboard leaderboard, Competitor o1, Competitor o2, TimePoint timePoint); + + /** + * Returning {@code true} makes the number of wins in a medal series the primary ranking criteria. + * The number of wins that makes a competitor the overall winner must be returned by {@link #getTargetAmountOfMedalRaceWins()}. + */ + default boolean isMedalWinAmountCriteria() { + return false; + } + + /** + * Returning {@code true} makes the {@link RaceColumn#isCarryForward() carry forward score} in a + * {@link Series#isMedal() medal series} a secondary ranking criteria for competitors that have an equal overall + * score. + */ + default boolean isCarryForwardInMedalsCriteria() { + return false; + } + + /** + * Returning {@code true} makes the last medal race (having valid scores) a secondary ranking criteria for + * competitors that have an equal overall score. + */ + default boolean isLastMedalRaceCriteria() { + return false; + } + + /** + * If {@link #isMedalWinAmountCriteria()} returns {@code true}, this will be the amount of races that must be won, + * in order to win the medal series instantly + */ + default int getTargetAmountOfMedalRaceWins() { + throw new IllegalStateException("This call is not valid for this scoringSheme"); + } + + /** + * Usually, the scores in each leaderboard column count as they are for the overall score. However, if a column is a + * medal race column it usually counts double. Under certain circumstances, columns may also count with factors + * different from 1 or 2. For example, we've seen cases in the Extreme Sailing Series where the race committee + * defined that in the overall series leaderboard the last two columns each count 1.5 times their scores. + */ + default double getScoreFactor(RaceColumn raceColumn) { + Double factor = raceColumn.getExplicitFactor(); + if (factor == null) { + factor = raceColumn.isMedalRace() ? DEFAULT_MEDAL_RACE_FACTOR : 1.0; + } + return factor; + } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointExtremeSailingSeriesOverall.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointExtremeSailingSeriesOverall.java index 36237e81bf8..48d0964a3b9 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointExtremeSailingSeriesOverall.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointExtremeSailingSeriesOverall.java @@ -72,7 +72,7 @@ public abstract class AbstractHighPointExtremeSailingSeriesOverall extends HighP private int getWins(List> scores) { int wins = 0; for (com.sap.sse.common.Util.Pair score : scores) { - if (Math.abs(score.getB() - maxPoints * score.getA().getFactor()) < 0.0000001) { + if (Math.abs(score.getB() - maxPoints * getScoreFactor(score.getA())) < 0.0000001) { wins++; } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie.java index ff804e487f6..800f4125459 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie.java @@ -46,8 +46,8 @@ public abstract class AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie extends private double getHighestScore(List> scores) { double highestScore = 0; for (com.sap.sse.common.Util.Pair score : scores) { - if ((score.getB() * score.getA().getFactor()) > highestScore) { - highestScore = score.getB() * score.getA().getFactor(); + if ((score.getB() * getScoreFactor(score.getA())) > highestScore) { + highestScore = score.getB() * getScoreFactor(score.getA()); } } return highestScore; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardImpl.java old mode 100755 new mode 100644 index a49dc0d95bd..c2d8203bbb5 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardImpl.java @@ -281,5 +281,5 @@ public abstract class AbstractLeaderboardImpl extends AbstractSimpleLeaderboardI RegattaLogBoatDeregistrator deregisterer = new RegattaLogBoatDeregistrator<>(regattaLog, boats, regattaLogEventAuthorForAbstractLeaderboard); deregisterer.deregister(deregisterer.analyze()); } - + } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardWithCache.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardWithCache.java index 7bc60eb8f96..c4143993c59 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardWithCache.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardWithCache.java @@ -406,7 +406,7 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { } // Note: the RaceColumnDTO won't be created by the following addRace call because it has been created // above by the result.createEmptyRaceColumn call - result.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(), + result.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), getScoringScheme().getScoreFactor(raceColumn), raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, fleetDTO, raceColumn.isMedalRace(), raceIdentifier, race, isMetaLeaderboardColumn); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractScoringSchemeImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractScoringSchemeImpl.java index cc53f6ebf81..cf443cfb28c 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractScoringSchemeImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractScoringSchemeImpl.java @@ -129,11 +129,11 @@ public abstract class AbstractScoringSchemeImpl implements ScoringScheme { // needs to compare net points; therefore, divide the total points by the column factor for comparison: List o1NetScores = new ArrayList<>(); for (com.sap.sse.common.Util.Pair o1ColumnAndScore : o1Scores) { - o1NetScores.add(o1ColumnAndScore.getB()/o1ColumnAndScore.getA().getFactor()); + o1NetScores.add(o1ColumnAndScore.getB() / getScoreFactor(o1ColumnAndScore.getA())); } List o2NetScores = new ArrayList<>(); for (com.sap.sse.common.Util.Pair o2ColumnAndScore : o2Scores) { - o2NetScores.add(o2ColumnAndScore.getB()/o2ColumnAndScore.getA().getFactor()); + o2NetScores.add(o2ColumnAndScore.getB() / getScoreFactor(o2ColumnAndScore.getA())); } Collections.sort(o1NetScores, pureScoreComparator); Collections.sort(o2NetScores, pureScoreComparator); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java index d9577814e88..992edf20d05 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java @@ -475,7 +475,7 @@ public abstract class AbstractSimpleLeaderboardImpl extends AbstractLeaderboardW if (totalPoints == null) { result = null; } else { - result = raceColumn.getFactor() * totalPoints; + result = getScoringScheme().getScoreFactor(raceColumn) * totalPoints; } } return result; @@ -663,7 +663,7 @@ public abstract class AbstractSimpleLeaderboardImpl extends AbstractLeaderboardW return new EntryImpl(trackedRankProvider, correctedScore, () -> correctedResults.getUncorrectedScore(), correctedResults.isCorrected(), discarded ? DOUBLE_0 - : correctedScore == null ? null : Double.valueOf(correctedScore * race.getFactor()), + : correctedScore == null ? null : Double.valueOf(correctedScore * getScoringScheme().getScoreFactor(race)), correctedResults.getMaxPointsReason(), discarded, race.getFleetOfCompetitor(competitor)); } @@ -738,7 +738,7 @@ public abstract class AbstractSimpleLeaderboardImpl extends AbstractLeaderboardW () -> correctedResults.getUncorrectedScore(), correctedResults.isCorrected(), discarded ? DOUBLE_0 : (correctedScore == null ? null - : Double.valueOf((correctedScore * raceColumn.getFactor()))), + : Double.valueOf((correctedScore * getScoringScheme().getScoreFactor(raceColumn)))), correctedResults.getMaxPointsReason(), discarded, raceColumn.getFleetOfCompetitor(competitor)); result.put(new com.sap.sse.common.Util.Pair(competitor, raceColumn), entry); } @@ -959,5 +959,7 @@ public abstract class AbstractSimpleLeaderboardImpl extends AbstractLeaderboardW return Util.getDominantObject(StreamSupport.stream(allBoats.spliterator(), /* parallel */ false) .map(b -> b.getBoatClass()).collect(Collectors.toList())); } + + } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LeaderboardTotalRankComparator.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LeaderboardTotalRankComparator.java old mode 100755 new mode 100644 index b6e1e2af35c..5a94b862a3e --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LeaderboardTotalRankComparator.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LeaderboardTotalRankComparator.java @@ -111,6 +111,10 @@ public class LeaderboardTotalRankComparator implements Comparator { double o2ScoreSum = getLeaderboard().getCarriedPoints(o2); Double o1MedalRaceScore = 0.0; Double o2MedalRaceScore = 0.0; + Double o1CarryForwardScoreInMedals = null; + Double o2CarryForwardScoreInMedals = null; + Double o1LastMedalScore = null; + Double o2LastMedalScore = null; // When a column has isStartsWithZeroScore, the competitor's score only need to be reset to zero if from there on // the competitor scored in this or any subsequent columns boolean needToResetO1ScoreUponNextValidResult = false; @@ -124,6 +128,8 @@ public class LeaderboardTotalRankComparator implements Comparator { // it is guaranteed to have equal ordering as all fleets that the competitor is assigned to within the key series. final Map> fleetWithCorrectOrderingForCompetitorBySeries = new HashMap<>(); int defaultFleetBasedComparisonResult = 0; // relevant if no authoritative fleet-based comparison result was determined; based on extreme fleet vs. no fleet comparison + int numberOfMedalRacesWonO1 = 0; + int numberOfMedalRacesWonO2 = 0; for (RaceColumn raceColumn : getLeaderboard().getRaceColumns()) { needToResetO1ScoreUponNextValidResult = raceColumn.isStartsWithZeroScore(); needToResetO2ScoreUponNextValidResult = raceColumn.isStartsWithZeroScore(); @@ -178,10 +184,21 @@ public class LeaderboardTotalRankComparator implements Comparator { if (o2Score != null) { o2MedalRaceScore += o2Score; } + if (o1Score != null || o2Score != null) { + o1LastMedalScore = o1Score; + o2LastMedalScore = o2Score; + } + } else { + o1CarryForwardScoreInMedals = o1Score; + o2CarryForwardScoreInMedals = o2Score; } // similar to compareByFleet, however, tracking is not required; having medal race column points // (tracked or manual) is sufficient preemptiveColumnResult = compareByMedalRaceParticipation(o1Score, o2Score); + if (scoringScheme.isMedalWinAmountCriteria()) { + numberOfMedalRacesWonO1 += leaderboard.isWin(o1, raceColumn, timePoint) ? 1 : 0; + numberOfMedalRacesWonO2 += leaderboard.isWin(o2, raceColumn, timePoint) ? 1 : 0; + } } if (preemptiveColumnResult == 0 && raceColumn.isTotalOrderDefinedByFleet()) { final FleetComparisonResult compareByFleetResult = compareByFleet(raceColumn, o1, o2, fleetWithCorrectOrderingForCompetitorBySeries); @@ -205,18 +222,35 @@ public class LeaderboardTotalRankComparator implements Comparator { // break tie by sorting scores and looking for the first score difference. int result = compareByNumberOfRacesScored(o1Scores.size(), o2Scores.size()); if (result == 0) { - result = compareByScoreSum(o1ScoreSum, o2ScoreSum); + if (scoringScheme.isMedalWinAmountCriteria()) { + // if one reaches the target amount of races won then this has priority, else proceed with normal low + // points scoring (e.g., not enough races yet) + result = compareByMedalRacesWon(numberOfMedalRacesWonO1, numberOfMedalRacesWonO2); + } if (result == 0) { - result = compareByMedalRaceScore(o1MedalRaceScore, o2MedalRaceScore); + result = compareByScoreSum(o1ScoreSum, o2ScoreSum); if (result == 0) { - result = compareByBetterScore(o1, Collections.unmodifiableList(o1Scores), o2, Collections.unmodifiableList(o2Scores), timePoint); + if (scoringScheme.isCarryForwardInMedalsCriteria()) { + result = compareBySingleRaceColumnScore(o1CarryForwardScoreInMedals, o2CarryForwardScoreInMedals); + } if (result == 0) { - // compare by last race: - result = scoringScheme.compareByLastRace(o1TotalPoints, o2TotalPoints, nullScoresAreBetter, o1, o2); + if (scoringScheme.isLastMedalRaceCriteria()) { + result = compareBySingleRaceColumnScore(o1LastMedalScore, o2LastMedalScore); + } if (result == 0) { - result = scoringScheme.compareByLatestRegattaInMetaLeaderboard(getLeaderboard(), o1, o2, timePoint); + result = compareByMedalRaceScore(o1MedalRaceScore, o2MedalRaceScore); if (result == 0) { - result = compareByArbitraryButStableCriteria(o1, o2); + result = compareByBetterScore(o1, Collections.unmodifiableList(o1Scores), o2, Collections.unmodifiableList(o2Scores), timePoint); + if (result == 0) { + // compare by last race: + result = scoringScheme.compareByLastRace(o1TotalPoints, o2TotalPoints, nullScoresAreBetter, o1, o2); + if (result == 0) { + result = scoringScheme.compareByLatestRegattaInMetaLeaderboard(getLeaderboard(), o1, o2, timePoint); + if (result == 0) { + result = compareByArbitraryButStableCriteria(o1, o2); + } + } + } } } } @@ -226,6 +260,33 @@ public class LeaderboardTotalRankComparator implements Comparator { return result; } + /** + * Compares by the number of races won in the medal series. If one of the competitors reached the target of races + * won (defined by {@link ScoringScheme#getTargetAmountOfMedalRaceWins()} it is ranked better than the other. If + * none of the competitors reached the target, they are ranked equally even if one has more wins. This is because + * only reaching the exact number of wins counts for this criteria but not the general comparison by the number of + * wins. If both competitors exactly reached the target, they are also ranked equally (this can e.g. occur while + * entering score corrections). + */ + private int compareByMedalRacesWon(int numberOfMedalRacesWonO1, int numberOfMedalRacesWonO2) { + final int result; + final int targetAmount = scoringScheme.getTargetAmountOfMedalRaceWins(); + if (numberOfMedalRacesWonO1 >= targetAmount || numberOfMedalRacesWonO2 >= targetAmount) { + result = Integer.compare(numberOfMedalRacesWonO2, numberOfMedalRacesWonO1); + } else { + result = 0; + } + return result; + } + + /** + * Compares by the scores of a single race column. If only one of the competitors has a result this competitor is + * ranked better than the other one. + */ + private int compareBySingleRaceColumnScore(Double o1Score, Double o2Score) { + return Comparator.nullsLast((Double o1s, Double o2s)->getScoreComparator().compare(o1s, o2s)).compare(o1Score, o2Score); + } + private int compareByArbitraryButStableCriteria(Competitor o1, Competitor o2) { return o1.getName().compareTo(o2.getName()); } @@ -236,11 +297,13 @@ public class LeaderboardTotalRankComparator implements Comparator { */ private int compareByMedalRaceScore(Double o1MedalRaceScore, Double o2MedalRaceScore) { assert o1MedalRaceScore != null || o2MedalRaceScore == null; + final int result; if (o1MedalRaceScore != null) { - return getScoreComparator().compare(o1MedalRaceScore, o2MedalRaceScore); + result = getScoreComparator().compare(o1MedalRaceScore, o2MedalRaceScore); } else { - return 0; + result = 0; } + return result; } private static class FleetComparisonResult { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LowPointFirstToWinTwoRaces.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LowPointFirstToWinTwoRaces.java new file mode 100644 index 00000000000..575e652f03b --- /dev/null +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LowPointFirstToWinTwoRaces.java @@ -0,0 +1,57 @@ +package com.sap.sailing.domain.leaderboard.impl; + +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.common.ScoringSchemeType; + +/** + * This class is a derivation of the LowPointScoring scheme. The following additional rules apply: + *
    + *
  1. The first participant in the medal series, that wins two races is ranked as the first.
  2. + *
  3. Having 1 point in the carry forward column of the medal series explicitly counts as an additional win which + * requires the winner of the qualification to only need one additional win in the medals.
  4. + *
  5. If none of the participants in the medal series reached the target amount of 2 wins yet, the ranking is done + * based on the points only.
  6. + *
  7. If there is a carry forward column in the medal series, the carry forward points are used as the primary tie + * breaking criteria in case two participants in the medal series have the same overall score.
  8. + *
  9. If there is no carry forward column in the medal series or the carry forward score is not sufficient, the points + * of the last medal race are used as a secondary tie breaking criteria.
  10. + *
  11. This Scheme will change the default scoring factor for medal races to 1, usually 2 is used
  12. + *
      + */ +public class LowPointFirstToWinTwoRaces extends LowPoint { + private static final long serialVersionUID = 7072175334160798617L; + + @Override + public ScoringSchemeType getType() { + return ScoringSchemeType.LOW_POINT_FIRST_TO_WIN_TWO_RACES; + } + + @Override + public boolean isMedalWinAmountCriteria() { + return true; + } + + @Override + public boolean isCarryForwardInMedalsCriteria() { + return true; + } + + @Override + public boolean isLastMedalRaceCriteria() { + return true; + } + + @Override + public int getTargetAmountOfMedalRaceWins() { + return 2; + } + + @Override + public double getScoreFactor(RaceColumn raceColumn) { + Double factor = raceColumn.getExplicitFactor(); + if (factor == null) { + factor = 1.0; + } + return factor; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 045defa6595..cc7951e05cc 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -434,6 +434,8 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String insertWaypointAfterSelected(); String addGate(); String scoringSchemeLowPointSystem(); + String scoringSchemeLowPointSystemFirstTwoWins(); + String scoringSchemeLowPointSystemFirstTwoWinsDescription(); String scoringSchemeHighPointSystem(); String scoringSchemeLowPointWinnerGetsZero(); String scoringSchemeWinnerGetsFive(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index 08bfa3ed598..8dd85fc710c 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -484,6 +484,8 @@ scoringSchemeHighPointFirstGetsTwelveOrEight=High Point, winner gets 12 points ( scoringSchemeHighPointFirstGetsTwelveOrEightDescription=Score in points. Winner scores 12 points, 2nd - 11 points, .... A tie is broken in favour of the competitor with the highest score, if tie remains then tie is broken by the rank in the last race scored. scoringSchemeHighPointFirstGetsTwelveOrEight2017=High Point ESS 2017, winner gets 12 points (or 8) scoringSchemeHighPointFirstGetsTwelveOrEight2017Description=Score in points. Winner scores 12 points, 2nd - 11 points, .... A tie is broken in favour of the competitor with the highest score, if tie remains then tie is broken by the rank in the last race scored. +scoringSchemeLowPointSystemFirstTwoWins=Low Point System, First with two wins in medals is winner +scoringSchemeLowPointSystemFirstTwoWinsDescription=Low Point System. The first in the medal series to win two races wins the medal series. Being the best in the pre medal series counts as one win for this. scoringSystem=Scoring system createFlexibleLeaderboard=Create flexible leaderboard createRegattaLeaderboard=Create regatta leaderboard diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index e106727b223..48ef82f8c55 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -474,6 +474,8 @@ scoringSchemeLowPointForLeagueOverallLeaderboardDescription=Bei Punktegleichstan scoringSchemeLowPointTieBreakBasedOnLastSeriesOnly=Low Point System, bei Punktegleichstand zählt letzten Serie scoringSchemeLowPointTieBreakBasedOnLastSeriesOnlyDescription=Bei Punktegleichstand entscheidet die Punktesumme aller (auch der gestrichenen) Wettfahrten der letzten Serie scoringSchemeLowPointWithAutomaticRdg=Low Point System, RDG erhält Durchschnitt aller nicht-RDG-Wertungen als Vorschlag +scoringSchemeLowPointSystemFirstTwoWins=Low Point System, Erster mit zwei Siegen in Medaillen-Rennen gewinnt +scoringSchemeLowPointSystemFirstTwoWinsDescription=Low Point System. Der Erste, der zwei Rennen in den Medaillen-Rennen gewinnt, zählt als Sieger der Medaillen-Serie. Der Beste der Vorserien bekommt einen zusätzlichen Sieg angerechnet. scoringSystem=Bepunktungssystem createFlexibleLeaderboard=Flexible Rangliste anlegen createRegattaLeaderboard=Rangliste für Regatta anlegen diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ScoringSchemeTypeFormatter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ScoringSchemeTypeFormatter.java index d0d295a59d9..b5ad2383a81 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ScoringSchemeTypeFormatter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ScoringSchemeTypeFormatter.java @@ -46,6 +46,8 @@ public class ScoringSchemeTypeFormatter { return stringMessages.scoringSchemeLowPointTieBreakBasedOnLastSeriesOnly(); case LOW_POINT_WITH_AUTOMATIC_RDG: return stringMessages.scoringSchemeLowPointWithAutomaticRdg(); + case LOW_POINT_FIRST_TO_WIN_TWO_RACES: + return stringMessages.scoringSchemeLowPointSystemFirstTwoWins(); } return null; } @@ -72,6 +74,8 @@ public class ScoringSchemeTypeFormatter { return stringMessages.scoringSchemeHighPointMatchRacingDescription(); case LOW_POINT_TIE_BREAK_BASED_ON_LAST_SERIES_ONLY: return stringMessages.scoringSchemeLowPointTieBreakBasedOnLastSeriesOnlyDescription(); + case LOW_POINT_FIRST_TO_WIN_TWO_RACES: + return stringMessages.scoringSchemeLowPointSystemFirstTwoWinsDescription(); default: return format(scoringSchemeType, stringMessages); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 7c2d018b39c..03a47b7418d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -2603,7 +2603,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } } final FleetDTO fleetDTO = baseDomainFactory.convertToFleetDTO(fleet); - RaceColumnDTO raceColumnDTO = leaderboardDTO.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(), + RaceColumnDTO raceColumnDTO = leaderboardDTO.addRace(raceColumn.getName(), + raceColumn.getExplicitFactor(), leaderboard.getScoringScheme().getScoreFactor(raceColumn), raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, fleetDTO, raceColumn.isMedalRace(), raceIdentifier, raceDTO, raceColumn instanceof MetaLeaderboardColumn); diff --git a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingLeaderboards.java b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingLeaderboards.java index 6e568a81c19..640f9d915f8 100755 --- a/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingLeaderboards.java +++ b/java/com.sap.sailing.mongodb.test/src/com/sap/sailing/mongodb/test/TestStoringAndRetrievingLeaderboards.java @@ -68,9 +68,9 @@ public class TestStoringAndRetrievingLeaderboards extends AbstractMongoDBTest { r3.setFactor(2.5); new MongoObjectFactoryImpl(db).storeLeaderboard(leaderboard); Leaderboard loadedLeaderboard = new DomainObjectFactoryImpl(db, DomainFactory.INSTANCE).loadLeaderboard(leaderboardName, /* regattaRegistry */ null, /* leaderboardRegistry */ null); - assertEquals(1.0, loadedLeaderboard.getRaceColumnByName("R1").getFactor(), 0.000000001); - assertEquals(1.5, loadedLeaderboard.getRaceColumnByName("R2").getFactor(), 0.000000001); - assertEquals(2.5, loadedLeaderboard.getRaceColumnByName("R3").getFactor(), 0.000000001); + assertEquals(1.0, loadedLeaderboard.getScoringScheme().getScoreFactor(loadedLeaderboard.getRaceColumnByName("R1")), 0.000000001); + assertEquals(1.5, loadedLeaderboard.getScoringScheme().getScoreFactor(loadedLeaderboard.getRaceColumnByName("R2")), 0.000000001); + assertEquals(2.5, loadedLeaderboard.getScoringScheme().getScoreFactor(loadedLeaderboard.getRaceColumnByName("R3")), 0.000000001); } @Test diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupFactory.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupFactory.java index 5909486602d..3d719143ebe 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupFactory.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/RaceGroupFactory.java @@ -31,6 +31,7 @@ import com.sap.sailing.domain.common.TargetTimeInfo; import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; import com.sap.sailing.domain.leaderboard.Leaderboard; import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.ScoringScheme; import com.sap.sailing.domain.polars.NotEnoughDataHasBeenAddedException; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sse.common.TimePoint; @@ -74,8 +75,9 @@ public class RaceGroupFactory { Map> seriesToRaceColumns = getSeriesToRaceColumns(leaderboard); Collection seriesWithRows = new ArrayList<>(); for (Series series : getSeriesIterable(leaderboard, seriesToRaceColumns)) { - seriesWithRows.add(new SeriesWithRowsImpl(series.getName(), series.isMedal(), series.isFleetsCanRunInParallel(), getRows(series, - seriesToRaceColumns.get(series)))); + seriesWithRows + .add(new SeriesWithRowsImpl(series.getName(), series.isMedal(), series.isFleetsCanRunInParallel(), + getRows(series, seriesToRaceColumns.get(series), leaderboard.getScoringScheme()))); } return seriesWithRows; } @@ -89,14 +91,16 @@ public class RaceGroupFactory { } /** - * @param raceColumns must be provided in the same order in which they appear in the series / leaderboard + * @param raceColumns + * must be provided in the same order in which they appear in the series / leaderboard */ - private Collection getRows(Series series, List raceColumns) { + private Collection getRows(Series series, List raceColumns, ScoringScheme scoringScheme) { Collection rows = new ArrayList<>(); for (Fleet fleet : series.getFleets()) { // We are taking the fleet name because there might be several "default fleet" // objects when TrackedRaces are linked onto this Leaderboard - rows.add(new RaceRowImpl(fleet, getCells(fleet.getName(), raceColumns, isFirstRaceColumnVirtual(series)))); + rows.add(new RaceRowImpl(fleet, + getCells(fleet.getName(), raceColumns, isFirstRaceColumnVirtual(series), scoringScheme))); } return rows; } @@ -111,7 +115,7 @@ public class RaceGroupFactory { * {@link TrackedRace#getEstimatedTimeToComplete(TimePoint) estimation} for the current point in time, when the * request is being made, using this time point to determine the wind speed and direction for each leg. */ - private List getCells(String fleetName, List raceColumns, boolean isFirstRaceColumnVirtual) { + private List getCells(String fleetName, List raceColumns, boolean isFirstRaceColumnVirtual, ScoringScheme scoringScheme) { boolean skippedFirst = false; List cells = new ArrayList<>(); if (raceColumns != null) { @@ -131,7 +135,8 @@ public class RaceGroupFactory { targetTime = null; } cells.add(new RaceCellImpl(raceColumn.getName(), raceColumn.getRaceLog(fleet), - raceColumn.getFactor(), raceColumn.getExplicitFactor(), zeroBasedIndexOfRaceInFleet++, targetTime)); + scoringScheme.getScoreFactor(raceColumn), raceColumn.getExplicitFactor(), + zeroBasedIndexOfRaceInFleet++, targetTime)); } } } diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java index 8c87df63b6f..e6fee705739 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java @@ -1200,7 +1200,7 @@ public class LeaderboardsResource extends AbstractLeaderboardsResource { raceColumnsAsJson.add(raceColumnAsJson); raceColumnAsJson.put(RaceColumnConstants.RACE_COLUMN_NAME, rc.getName()); raceColumnAsJson.put(RaceColumnConstants.EXPLICIT_FACTOR, rc.getExplicitFactor()); - raceColumnAsJson.put(RaceColumnConstants.FACTOR, rc.getFactor()); + raceColumnAsJson.put(RaceColumnConstants.FACTOR, leaderboard.getScoringScheme().getScoreFactor(rc)); } return json; } diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java index eb84a8de7ab..1c79016d831 100644 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java @@ -411,7 +411,7 @@ public class MasterDataImportTest { raceColumnOnTarget.setTrackedRace(fleet1OnTarget, trackedRaceForTarget); - Assert.assertEquals(factor, raceColumnOnTarget.getFactor()); + Assert.assertEquals(factor, leaderboard.getScoringScheme().getScoreFactor(raceColumnOnTarget)); Iterable competitorsOnTarget = leaderboardOnTarget.getAllCompetitors(); Iterator competitorIterator = competitorsOnTarget.iterator(); @@ -423,10 +423,8 @@ public class MasterDataImportTest { scoreCorrection, leaderboardOnTarget.getScoreCorrection().getExplicitScoreCorrection(competitorOnTarget, raceColumnOnTarget)); - Assert.assertEquals( - maxPointsReason, - leaderboardOnTarget.getScoreCorrection().getMaxPointsReason(competitorOnTarget, raceColumnOnTarget, - MillisecondsTimePoint.now())); + Assert.assertEquals(maxPointsReason, leaderboardOnTarget.getScoreCorrection() + .getMaxPointsReason(competitorOnTarget, raceColumnOnTarget, MillisecondsTimePoint.now())); // Check for carried points Assert.assertEquals(carriedPoints, leaderboardOnTarget.getCarriedPoints(competitorOnTarget)); @@ -2093,7 +2091,7 @@ public class MasterDataImportTest { RaceColumn metaColumn = metaColumns.iterator().next(); Assert.assertNotNull(metaColumn); - Assert.assertEquals(factor, metaColumn.getFactor()); + Assert.assertEquals(factor, overallLeaderboard.getScoringScheme().getScoreFactor(metaColumn)); // Verify that overall leaderboard data has been persisted RacingEventService persistenceVerifier = new RacingEventServiceImplMock(){}; @@ -2115,7 +2113,7 @@ public class MasterDataImportTest { metaColumn = metaColumns.iterator().next(); Assert.assertNotNull(metaColumn); - Assert.assertEquals(factor, metaColumn.getFactor()); + Assert.assertEquals(factor, overallLeaderboard.getScoringScheme().getScoreFactor(metaColumn)); } @@ -2289,17 +2287,15 @@ public class MasterDataImportTest { raceColumnOnTarget.setTrackedRace(defaultFleetOnTarget, trackedRaceForTarget); - Assert.assertEquals(factor, raceColumnOnTarget.getFactor()); + Assert.assertEquals(factor, leaderboardOnTarget.getScoringScheme().getScoreFactor(raceColumnOnTarget)); // Check for score corrections Assert.assertEquals( scoreCorrection, leaderboardOnTarget.getScoreCorrection().getExplicitScoreCorrection(competitorOnTarget, raceColumnOnTarget)); - Assert.assertEquals( - maxPointsReason, - leaderboardOnTarget.getScoreCorrection().getMaxPointsReason(competitorOnTarget, raceColumnOnTarget, - MillisecondsTimePoint.now())); + Assert.assertEquals(maxPointsReason, leaderboardOnTarget.getScoreCorrection() + .getMaxPointsReason(competitorOnTarget, raceColumnOnTarget, MillisecondsTimePoint.now())); // Check for carried points Assert.assertEquals(carriedPoints, leaderboardOnTarget.getCarriedPoints(competitorOnTarget));