Skip to content

Commit

Permalink
Added several unit tests and changed the raid stats calculation slightly
Browse files Browse the repository at this point in the history
The raid stats are not controlled and are done in the database side
instead of being done in the server-side. We re using aggregation pipelines
for calculations
  • Loading branch information
Daniel Villavicencio authored and Daniel Villavicencio committed Mar 1, 2024
1 parent 68aad78 commit 5a01924
Show file tree
Hide file tree
Showing 6 changed files with 496 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.danielvm.destiny2bot.controller;

import com.danielvm.destiny2bot.dto.UserChoiceValue;
import com.danielvm.destiny2bot.dto.destiny.RaidStatistics;
import com.danielvm.destiny2bot.service.RaidStatsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@Slf4j
public class InsertUserController {

private final RaidStatsService raidStatsService;

public InsertUserController(
RaidStatsService raidStatsService) {
this.raidStatsService = raidStatsService;
}

/**
* Inserts a user manually into the Mongo Database
*
* @param userId The userId
* @return the saved entity
*/
@PostMapping("/user")
public Flux<RaidStatistics> insertNewUser(@RequestParam String membershipId,
@RequestParam Integer membershipType, @RequestParam String userId,
@RequestParam Integer userTag) {
UserChoiceValue parsedValues = new UserChoiceValue(membershipId, membershipType, userId,
userTag, null);
return raidStatsService.calculateRaidStats(parsedValues);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
public class RaidStatistics {

/**
* The name of the raid
* The _Id of the MongoDB aggregation should be the name of the raid
*/
private String raidName;
private String _id;

/**
* Total amount of kills done for a raid
Expand All @@ -29,11 +29,6 @@ public class RaidStatistics {
*/
private Integer fastestTime;

/**
* The average time a player finishes the raid
*/
private Integer averageTime;

/**
* The number of completed raids that user has for a specific raid
*/
Expand All @@ -49,16 +44,15 @@ public class RaidStatistics {
*/
private Integer fullClears;

public RaidStatistics(String raidName) {
this.raidName = raidName;
this.totalKills = 0;
this.totalDeaths = 0;
this.fastestTime = Integer.MAX_VALUE;
this.averageTime = 0;
this.partialClears = 0;
this.totalClears = 0;
this.fullClears = 0;
}
/**
* The total amount of normal-mode raid clears for this raid
*/
private Integer masterClears;

/**
* The total amount of master-mode raid clears for this raid
*/
private Integer normalClears;

/**
* This toString uses a StringBuilder to manipulate the actual output to send through Discord
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.danielvm.destiny2bot.config.DiscordConfiguration;
import com.danielvm.destiny2bot.dto.UserChoiceValue;
import com.danielvm.destiny2bot.dto.destiny.MemberGroupResponse;
import com.danielvm.destiny2bot.dto.destiny.RaidStatistics;
import com.danielvm.destiny2bot.dto.destiny.UserGlobalSearchBody;
import com.danielvm.destiny2bot.dto.destiny.UserSearchResult;
import com.danielvm.destiny2bot.dto.discord.Choice;
Expand Down Expand Up @@ -112,6 +113,7 @@ private Mono<InteractionResponseData> processRaidsResponseUser(Interaction inter
UserChoiceValue parsedData = ((UserChoiceValue) interaction.getData().getOptions().get(0)
.getValue());
return raidStatsService.calculateRaidStats(parsedData)
.collectMap(RaidStatistics::get_id)
.map(response -> response.entrySet().stream()
.map(entry -> EmbeddedField.builder()
.name(entry.getKey())
Expand Down
123 changes: 97 additions & 26 deletions src/main/java/com/danielvm/destiny2bot/service/RaidStatsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,113 @@
import com.danielvm.destiny2bot.dto.UserChoiceValue;
import com.danielvm.destiny2bot.dto.destiny.RaidStatistics;
import com.danielvm.destiny2bot.entity.UserDetails;
import com.danielvm.destiny2bot.entity.UserRaidDetails;
import com.danielvm.destiny2bot.enums.RaidDifficulty;
import java.time.Instant;
import java.util.Map;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators;
import org.springframework.data.mongodb.core.aggregation.Fields;
import org.springframework.data.mongodb.core.aggregation.GroupOperation;
import org.springframework.data.mongodb.core.aggregation.MatchOperation;
import org.springframework.data.mongodb.core.aggregation.UnwindOperation;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
@Slf4j
public class RaidStatsService {

private static final List<String> RAIDS_WITH_MASTER_MODE = List.of(
"Vault of Glass", "Vow of the Disciple", "King's Fall", "Root of Nightmares", "Crota's End"
);
private static final String RAID_NAME = "userRaidDetails.raidName";
private static final String IS_COMPLETED = "userRaidDetails.isCompleted";
private static final String RAID_DIFFICULTY = "userRaidDetails.raidDifficulty";
private static final String TOTAL_KILLS = "userRaidDetails.totalKills";
private static final String TOTAL_DEATHS = "userRaidDetails.totalDeaths";
private static final String FROM_BEGINNING = "userRaidDetails.fromBeginning";

private final UserRaidDetailsService userRaidDetailsService;
private final ReactiveMongoTemplate reactiveMongoTemplate;

public RaidStatsService(
UserRaidDetailsService userRaidDetailsService) {
UserRaidDetailsService userRaidDetailsService,
ReactiveMongoTemplate reactiveMongoTemplate) {
this.userRaidDetailsService = userRaidDetailsService;
this.reactiveMongoTemplate = reactiveMongoTemplate;
}

private static RaidStatistics reduceStatistics(RaidStatistics stats,
UserRaidDetails details) {
stats.setTotalKills(stats.getTotalKills() + details.getTotalKills());
stats.setTotalDeaths(stats.getTotalDeaths() + details.getTotalDeaths());
if (details.getIsCompleted()) {
stats.setTotalClears(stats.getTotalClears() + 1);
if (details.getFromBeginning()) {
stats.setFastestTime(Math.min(stats.getFastestTime(), details.getDurationSeconds()));
stats.setFullClears(stats.getFullClears() + 1);
}
}
return stats;
private static Aggregation raidStatisticsAggregationPipeline(String userId) {
MatchOperation userIdMatch = Aggregation.match(Criteria.where("_id").is(userId));

UnwindOperation unwindRaids = new UnwindOperation(Fields.field("userRaidDetails"));

Criteria fastestTimeCriteria = new Criteria();
fastestTimeCriteria.andOperator(
Criteria.where(FROM_BEGINNING).is(true),
Criteria.where(IS_COMPLETED).is(true)
);

Criteria normalModeClearsCriteria = new Criteria();
normalModeClearsCriteria.andOperator(
Criteria.where(RAID_NAME).in(RAIDS_WITH_MASTER_MODE),
Criteria.where(IS_COMPLETED).is(true),
Criteria.where(RAID_DIFFICULTY).is(RaidDifficulty.NORMAL.name())
);

Criteria masterModeClearsCriteria = new Criteria();
masterModeClearsCriteria.andOperator(
Criteria.where(RAID_NAME).in(RAIDS_WITH_MASTER_MODE),
Criteria.where(IS_COMPLETED).is(true),
Criteria.where(RAID_DIFFICULTY).is(RaidDifficulty.MASTER.name())
);

Criteria raidClearCriteria = Criteria.where(IS_COMPLETED).is(true);

Criteria fullRaidClearCriteria = new Criteria();
fullRaidClearCriteria.andOperator(
Criteria.where(IS_COMPLETED).is(true),
Criteria.where(FROM_BEGINNING).is(true)
);

Criteria partialRaidClearCriteria = new Criteria();
partialRaidClearCriteria.andOperator(
Criteria.where(IS_COMPLETED).is(true),
Criteria.where(FROM_BEGINNING).is(false)
);
GroupOperation groupByRaidName = Aggregation.group(RAID_NAME)
.sum(TOTAL_KILLS).as("totalKills")
.sum(TOTAL_DEATHS).as("totalDeaths")
.min(ConditionalOperators
.when(fastestTimeCriteria)
.then("$userRaidDetails.durationSeconds")
.otherwise("0")).as("fastestTime")
.sum(ConditionalOperators
.when(raidClearCriteria)
.then(1)
.otherwise(0)).as("totalClears")
.sum(ConditionalOperators
.when(partialRaidClearCriteria)
.then(1)
.otherwise(0)).as("partialClears")
.sum(ConditionalOperators
.when(fullRaidClearCriteria)
.then(1)
.otherwise(0)).as("fullClears")
.sum(ConditionalOperators
.when(normalModeClearsCriteria)
.then(1)
.otherwise(0)).as("normalClears")
.sum(ConditionalOperators
.when(masterModeClearsCriteria)
.then(1)
.otherwise(0)).as("masterClears");

return Aggregation.newAggregation(userIdMatch, unwindRaids, groupByRaidName);
}

/**
Expand All @@ -42,7 +119,7 @@ private static RaidStatistics reduceStatistics(RaidStatistics stats,
* @param parsedData The parsed data needed to retrieve Raid Statistics for a player
* @return Map of Raid Statistics grouped by raid name
*/
public Mono<Map<String, RaidStatistics>> calculateRaidStats(UserChoiceValue parsedData) {
public Flux<RaidStatistics> calculateRaidStats(UserChoiceValue parsedData) {
Instant now = Instant.now(); // Timestamp for this action
String userId = parsedData.getBungieDisplayName() + "#" + parsedData.getBungieDisplayCode();

Expand All @@ -54,17 +131,11 @@ public Mono<Map<String, RaidStatistics>> calculateRaidStats(UserChoiceValue pars
.doOnSubscribe(subscription -> log.info("Update action initiated for user [{}]", userId))
.doOnSuccess(userDetails -> log.info("Update action finished for user [{}]", userId));

Aggregation aggregation = raidStatisticsAggregationPipeline(userId);

return userRaidDetailsService.existsById(userId)
.flatMap(exists -> exists ? updateAction : createAction)
.flatMapIterable(UserDetails::getUserRaidDetails)
.groupBy(UserRaidDetails::getRaidName)
.flatMap(group -> group.reduce(new RaidStatistics(group.key()),
RaidStatsService::reduceStatistics))
.collectMap(RaidStatistics::getRaidName, raidStatistics -> {
if (raidStatistics.getFastestTime() == Integer.MAX_VALUE) {
raidStatistics.setFastestTime(0);
}
return raidStatistics;
});
.flatMapMany(userDetails -> reactiveMongoTemplate.aggregate(aggregation,
UserDetails.class, RaidStatistics.class));
}
}
Loading

0 comments on commit 5a01924

Please sign in to comment.