working towards bug 3798: implemented scoring specials, optionally limiting the number of discards in a series

Change-Id: Ifa5ec9e14791aac6bfc9648450d91e80a49039ad
This commit is contained in:
Axel Uhl committed 2016-07-20 16:46:15 +02:00
1 parent 0450b59fa0
commit 60dbb42d67
4 files changed
+164 -5

No files matched your search

@@ -36,6 +36,7 @@ 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.RaceColumnInSeries;
import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.base.Series;
import com.sap.sailing.domain.base.Waypoint;
@@ -2101,4 +2102,105 @@ public class LeaderboardScoringAndRankingTest extends LeaderboardScoringAndRanki
}
}
}
/**
* See bug 3798: special discarding rule that limits the number of discards for the final series; configured
* by {@link Series#getMaximumNumberOfDiscards()}.
*/
@Test
public void testDiscardingWithLimitOnFinalSeries() throws NoWindException {
series = new ArrayList<Series>();
// -------- qualification series ------------
{
List<Fleet> qualificationFleets = new ArrayList<Fleet>();
for (String qualificationFleetName : new String[] { "Yellow", "Blue" }) {
qualificationFleets.add(new FleetImpl(qualificationFleetName));
}
List<String> qualificationRaceColumnNames = new ArrayList<String>();
for (int q=1; q<=7; q++) {
qualificationRaceColumnNames.add("Q"+q);
}
Series qualificationSeries = new SeriesImpl("Qualification", /* isMedal */false, /* isFleetsCanRunInParallel */ true, qualificationFleets, qualificationRaceColumnNames, /* trackedRegattaRegistry */ null);
series.add(qualificationSeries);
}
// -------- final series ------------
{
List<Fleet> finalFleets = new ArrayList<Fleet>();
int fleetOrdering = 1;
for (String finalFleetName : new String[] { "Gold", "Silver" }) {
finalFleets.add(new FleetImpl(finalFleetName, fleetOrdering++));
}
List<String> finalRaceColumnNames = new ArrayList<String>();
for (int f=1; f<=7; f++) {
finalRaceColumnNames.add("F"+f);
}
Series finalSeries = new SeriesImpl("Final", /* isMedal */false, /* isFleetsCanRunInParallel */ true, finalFleets, finalRaceColumnNames, /* trackedRegattaRegistry */ null);
finalSeries.setMaximumNumberOfDiscards(1);
series.add(finalSeries);
}
final BoatClass boatClass = DomainFactory.INSTANCE.getOrCreateBoatClass("470", /* typicallyStartsUpwind */ true);
Regatta regatta = new RegattaImpl(RegattaImpl.getDefaultName("Test Regatta", boatClass.getName()), boatClass, /*startDate*/ null, /*endDate*/ null,
series, /* persistent */false, DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT), "123", /* course area */null, OneDesignRankingMetric::new);
final int TOTAL_NUMBER_OF_COMPETITORS = 100;
List<Competitor> competitors = createCompetitors(TOTAL_NUMBER_OF_COMPETITORS);
final int firstYellowCompetitorIndex = TOTAL_NUMBER_OF_COMPETITORS/4;
List<Competitor> yellow = new ArrayList<>(competitors.subList(firstYellowCompetitorIndex, firstYellowCompetitorIndex+TOTAL_NUMBER_OF_COMPETITORS/2));
List<Competitor> blue = new ArrayList<>(competitors);
blue.removeAll(yellow);
Collections.shuffle(yellow);
Collections.shuffle(blue);
final int firstGoldCompetitorIndex = TOTAL_NUMBER_OF_COMPETITORS/3;
List<Competitor> gold = new ArrayList<>(competitors.subList(firstGoldCompetitorIndex, firstGoldCompetitorIndex+TOTAL_NUMBER_OF_COMPETITORS/2));
List<Competitor> silver = new ArrayList<>(competitors);
silver.removeAll(gold);
Collections.shuffle(gold);
Collections.shuffle(silver);
Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[] { 5, 10 });
TimePoint now = MillisecondsTimePoint.now();
TimePoint later = new MillisecondsTimePoint(now.asMillis()+1000);
for (int q = 1; q <= 7; q++) {
RaceColumn qColumn = series.get(0).getRaceColumnByName("Q"+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);
}
for (int f = 1; f <= 7; f++) {
RaceColumn fColumn = series.get(1).getRaceColumnByName("F"+f);
TrackedRace f1Gold = new MockedTrackedRaceWithStartTimeAndRanks(now, gold);
fColumn.setTrackedRace(fColumn.getFleetByName("Gold"), f1Gold);
TrackedRace f1Silver = new MockedTrackedRaceWithStartTimeAndRanks(now, silver);
fColumn.setTrackedRace(fColumn.getFleetByName("Silver"), f1Silver);
}
for (final Competitor c : competitors) {
int totalDiscards = 0;
int discardsInFinalSeries = 0;
int numberOfRacesInFinalThatWereWorseThanWorstQualificationRace = 0;
double worstQualificationScore = 0;
for (final RaceColumn rc : leaderboard.getRaceColumns()) {
double score = leaderboard.getTotalPoints(c, rc, later);
if (rc instanceof RaceColumnInSeries && ((RaceColumnInSeries) rc).getSeries() == regatta.getSeriesByName("Qualification")) {
if (score > worstQualificationScore) {
worstQualificationScore = score;
}
} else {
if (score > worstQualificationScore) {
numberOfRacesInFinalThatWereWorseThanWorstQualificationRace++;
}
}
if (leaderboard.isDiscarded(c, rc, later)) {
totalDiscards++;
if (rc instanceof RaceColumnInSeries && ((RaceColumnInSeries) rc).getSeries() == regatta.getSeriesByName("Final")) {
discardsInFinalSeries++;
}
}
}
assertEquals(2, totalDiscards);
assertTrue(discardsInFinalSeries <= 1);
assertTrue(numberOfRacesInFinalThatWereWorseThanWorstQualificationRace == 0 || discardsInFinalSeries > 0);
}
}
}
@@ -71,6 +71,18 @@ public interface Series extends SeriesBase {
void setResultDiscardingRule(ThresholdBasedResultDiscardingRule resultDiscardingRule);
/**
* If not {@code null}, defines an upper inclusive limit for the number of races that may be discarded from
* this series. For example, when setting this to {@code 1} for a final series in a regatta that has a
* qualification and a final series, when the second discard becomes available and the series don't define
* their own discarding rules, two discards may be picked from the qualification series, but at most one
* could be selected in the final even if another final race has a score worse than that of all
* qualification races.
*/
Integer getMaximumNumberOfDiscards();
void setMaximumNumberOfDiscards(Integer maximumNumberOfDiscards);
Regatta getRegatta();
/**
@@ -118,4 +130,5 @@ public interface Series extends SeriesBase {
boolean hasSplitFleetContiguousScoring();
void setSplitFleetContiguousScoring(boolean hasSplitFleetScore);
}
@@ -40,6 +40,16 @@ public class SeriesImpl extends RenamableImpl implements Series, RaceColumnListe
private Regatta regatta;
private final RaceColumnListeners raceColumnListeners;
private ThresholdBasedResultDiscardingRule resultDiscardingRule;
/**
* If not {@code null}, defines an upper inclusive limit for the number of races that may be discarded from
* this series. For example, when setting this to {@code 1} for a final series in a regatta that has a
* qualification and a final series, when the second discard becomes available and the series don't define
* their own discarding rules, two discards may be picked from the qualification series, but at most one
* could be selected in the final even if another final race has a score worse than that of all
* qualification races.
*/
private Integer maximumNumberOfDiscards;
/**
* If set, the series doesn't take over the scores from any previous series but starts with zero scores for all its
@@ -391,6 +401,16 @@ public class SeriesImpl extends RenamableImpl implements Series, RaceColumnListe
this.resultDiscardingRule = resultDiscardingRule;
}
@Override
public Integer getMaximumNumberOfDiscards() {
return maximumNumberOfDiscards;
}
@Override
public void setMaximumNumberOfDiscards(Integer maximumNumberOfDiscards) {
this.maximumNumberOfDiscards = maximumNumberOfDiscards;
}
@Override
public boolean definesSeriesDiscardThresholds() {
return getResultDiscardingRule() != null;
@@ -16,6 +16,8 @@ import java.util.Set;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.RaceColumn;
import com.sap.sailing.domain.base.RaceColumnInSeries;
import com.sap.sailing.domain.base.Series;
import com.sap.sailing.domain.common.MaxPointsReason;
import com.sap.sailing.domain.leaderboard.Leaderboard;
import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule;
@@ -77,7 +79,7 @@ public class ThresholdBasedResultDiscardingRuleImpl implements ThresholdBasedRes
public Set<RaceColumn> getDiscardedRaceColumns(final Competitor competitor, final Leaderboard leaderboard,
Iterable<RaceColumn> raceColumnsToConsider, final TimePoint timePoint) {
int resultsToDiscard = getNumberOfResultsToDiscard(competitor, raceColumnsToConsider, leaderboard, timePoint);
Set<RaceColumn> result;
final Set<RaceColumn> result;
if (resultsToDiscard > 0) {
final Map<RaceColumn, Double> totalPointsForCompetitorPerColumn = new HashMap<>();
List<RaceColumn> sortedRaces = new ArrayList<RaceColumn>();
@@ -101,14 +103,12 @@ public class ThresholdBasedResultDiscardingRuleImpl implements ThresholdBasedRes
}
};
Collections.sort(sortedRaces, comparator);
int i=0;
Iterator<RaceColumn> badRacesIter = sortedRaces.iterator();
while (badRacesIter.hasNext() && i<resultsToDiscard) {
while (badRacesIter.hasNext() && result.size()<resultsToDiscard) {
final RaceColumn badRace = badRacesIter.next();
final MaxPointsReason maxPointsReason = leaderboard.getMaxPointsReason(competitor, badRace, timePoint);
if (maxPointsReason == null || maxPointsReason.isDiscardable()) {
result.add(badRace);
i++;
addDiscardIfWithinSeriesLimits(result, badRace);
}
}
} else {
@@ -117,6 +117,30 @@ public class ThresholdBasedResultDiscardingRuleImpl implements ThresholdBasedRes
return result;
}
/**
* Only adds the discard if the {@link Series} to which the {@code badRace} {@link RaceColumn} belongs
* does not define a separate limit for the number of its races that may be discarded, or if that number
* is less than the number of races discarded so far from that series.
*/
private void addDiscardIfWithinSeriesLimits(Set<RaceColumn> result, RaceColumn badRace) {
final Series series;
if (!(badRace instanceof RaceColumnInSeries) ||
(series = ((RaceColumnInSeries) badRace).getSeries()).getMaximumNumberOfDiscards() == null ||
getRacesDiscardedFromSeries(result, series) < series.getMaximumNumberOfDiscards()) {
result.add(badRace);
}
}
private int getRacesDiscardedFromSeries(Set<RaceColumn> result, Series series) {
int count = 0;
for (final RaceColumn rc : result) {
if (rc instanceof RaceColumnInSeries && ((RaceColumnInSeries) rc).getSeries() == series) {
count++;
}
}
return count;
}
private int getNumberOfResultsToDiscard(Competitor competitor, Iterable<RaceColumn> raceColumnsToConsider,
Leaderboard leaderboard, TimePoint timePoint) {
int numberOfResultsToDiscard;