Data structure for Statistics and Achievements system - java

I'm implementing an Statistics + Achievements system. Basically the structure is:
An Achievement have many related Statistics, this relations must associate each Achievement with the required Statistics (and it value). For example the Achievement1 needs the Statistic1 with value 50 (or greater) and the Statistic2 with value 100 (or greater).
Given an Statistic I need also know what are the related Achievements (in order to check them when the Statistic changes.
Both Stats and Achievements have an unique id.
My problem is I donĀ“t know whats the best data(s) structure(s) for representing that. By the way I'm using:
SparseArray<HashMap<Statistic, Integer>> statisticsForAnAchievement;
For the first point where the index of the array is the Achievement ID and the HashMap contains the Statistic/TargetValue pairs. And a:
SparseArray<Collection<Achievement>> achievementsRelatedToAStatistic;
For the second point where the index is the StatisticID and the item is the collection of Achievements related.
Then I need to handle both objects keeping it coherence.
Is there an easier way of representing that? Thanks

As a Statistic(or a group of Statistics) describes an Achievement shouldn't that/those Statistic/s be stored in the Achievement class? For example, an improved Achievement class:
public class Achievement {
SparseArray<Statistic> mStatistics = new SparseArray<Statistic>();
// to get a reference to the statisctics that make this achievement
public SparseArray<Statistic> getStatics() {
return mStatistics;
}
// add a new Statistic to these Achievement
public void addStatistic(int statisticId, Statistic newStat) {
// if we don't already have this particular statistic, add it
// or maybe update the underlining Statistic?!?
if (mStatistics.get(statisticId) == null) {
mStatistic.add(newStat);
}
}
// remove the Statistic
public void removeStatistic(int statisticId) {
mStatistic.delete(statisticId);
}
// check to see if this achievment has a statistic with this id
public boolean hasStatistics(int statisticId) {
return mStatistic.get(statisticId) == null ? false : true;
}
// rest of your code
}
Also, the Statistic class should store its target(the 50 value for Statistic1) value in it as a field.
An Achievement have many related Statistics, this relations must
associate each Achievement with the required Statistics (and it
value). For example the Achievement1 needs the Statistic1 with value
50 (or greater) and the Statistic2 with value 100 (or greater).
The statistics are already stored in the achievments so all you have to do is store an array/list of ids of the achievments(or the achievements them self) and with this you'll have access to the statistics that made those achievements.
Given an Statistic I need also know what are the related Achievements
(in order to check them when the Statistic changes.
You would use the above array/list of achievements, iterate them and check to see if the achievement holds that particular Statistic:
ArrayList<Achievement> relatedAchievements = new ArrayList<Achievement>();
for (Achievement a : theListOfAchievments) {
if (a.hasStatistics(targetStatistic)) {
relatedAchievements.add(a); // at the end this will store the achievements related(that contain) the targetStatistic
}
}
Another option is to have somewhere a static mapping that stores which achievements have a Statistic, mapping that would get updated each time one of the addStatictic or removeStatistic methods gets called.
Regarding your code, if you don't need the Statistic object and are happy with just holding a reference to its id then you could improve the statisticsForAnAchievement with:
SparseArray<SparseIntArray> statisticsForAnAchievement;
// the index of the SparseArray is the Achievement's id
// the index of the SparseIntArray is the Statistic's id
// the value of the SparseIntArray is the Statistic's value

Related

Checking if multiple hashmaps are empty

I'm trying to find if multiple HashMaps are empty.
To give some context. I have a hashmap declared here.
static Map<Integer, College> tblColleges = new HashMap<Integer, College>();
For each college object:
Map<Integer, Department> tblDepartments = new HashMap<Integer, Department>();
I'm trying to add a major. Majors can only exist as an attribute of Department.
Here's what I have right now.
int numberofColleges = Databases.tblColleges.size();
int emptyColleges = 0;
for(int key: Databases.tblColleges.keySet()) {
if(Databases.getTblColleges(key).tblDepartments.isEmpty()) {
emptyColleges++;
}
}
if(numberofColleges == emptyColleges) {
System.out.println("Invalid. Requires at least 1 department.");
}
I should only be able to create a Major if at least 1 college has a department.
Essentially for each college object that exists in the tblColleges, I'm checking to see if it's department hashmap is empty. If it is empty, then I increment the number of empty colleges.
Afterward, I compare the number of college objects with empty college objects found, if they are equal then I print an error.
I was wondering if there was a better more efficient way to do this, maybe with some function that exists that I'm not familiar with rather than using variables.
Q: Can you do the check "more efficiently"?
A: You could optimize it a bit:
boolean nonEmptyColleges = false;
for (int key: Databases.tblColleges.keySet()) {
if (!Databases.getTblColleges(key).tblDepartments.isEmpty()) {
nonEmptyColleges = true;
break;
}
}
The above short circuits as soon as it finds a College with a Department. That will be a substantial improvement in a lot of cases.
Then, assuming that Databases.tblColleges is a Map:
boolean nonEmptyColleges = false;
for (int college: Databases.tblColleges.values()) {
if (!college.tblDepartments.isEmpty()) {
nonEmptyColleges = true;
break;
}
}
Q: Can you do the check with less code?
A: Using Java 8 streams you could write the last as:
boolean nonEmptyColleges = Databases.tblColleges.values().stream()
.anyMatch(c -> !c.tblDepartments.isEmpty());
(I think ...)
Q: But is this the right approach?
A: IMO, no.
It seems that you intend to do this check each time you add a major. That's not necessary.
Majors can only exist as an attribute of Department.
The key thing that you need to check is that the Department you want to add the major for exists.
If the Department doesn't exist you can't add the major to it.
If the Department does exist you can the major to it, whether or not it is currently a department of a college1.
The bigger point here is that any data model is going to have a variety of data integrity rules / constraints on it. But that does mean that you need to explicitly check all of them each time the model is changed. You only need to check the preconditions for the change (e.g. that the Department exists) and any constraints that could be invalidated by the change.
1 - The "not" case assumes that there may be some other way of finding a Department. It could be a separate table of Department objects, or it could be that you are in the process of creating and building a new Department and haven't added it to its College yet.

Teleport to next player

I am working on a Spigot 1.8.9 plugin and am trying to add a feature when a staff right-clicks an item it teleports them to the next player that isn't in vanish and not themselves and if there aren't any it should return null.
On click I attempted to add all possible users to a list using
public static List<User> getPossibleUsers(User user){
List<User> result = new ArrayList<>();
for(User target : users)
if(!target.isVanished() && !user.getUUID().equals(target.getUUID()))
result.add(target);
return result;
}
The staff is also assigned an int called nextPlayer which is set to 0 when they login. Then when they click I add one to the int so next time they click it can get the next user.
private User getNextPlayer(User user) {
int next = user.nextPlayer;
List<User> users = getPossibleUsers(user);
if(users.size() == 0)
return null;
int current = 0;
for(User target : users) {
if(current == next){
return target;
}
current++;
}
user.nextPlayer = next;
}
The problem is I don't know how to make the getNextPlayer method correctly and make it efficient. I also would like to also to make it so once it hits the last player it loops back to the first player.
I'd suggest thinking about your problem entirely differently if you want it to be efficient, but efficiency really isn't a concern in this situation, so I'm opting to not pre-maturely optimize and instead work with the code you already have.
public static List<User> getPossibleUsers(User user){
List<User> result = new ArrayList<>();
for(User target : users)
if(!target.isVanished() && !user.getUUID().equals(target.getUUID()))
result.add(target);
return result;
}
This currently returns the Users in the same order, as they are defined on users.
This better have a natural sort order, otherwise you are going to have issues when people join / leave the server, as it will cause people to change their ordering in the list.
Now let's get back to first principals.
int next = user.nextPlayer;
Looks like you are storing the index of the player in the list you have already been in on the 'user'.
Once you have this, you can access that index directly from the list.
https://docs.oracle.com/javase/8/docs/api/java/util/List.html#get-int-
E get(int index)
So, doing users.get(next++); is all you need to do to 'fix' the code you have above. it increments next, and gets the user at that position (assuming the ordering is consistent, and hasn't changed) However, it may throw an exception if it's out of range of the list, so we wrap it in
if(next <= users.length) {
users.get(next++);
} else return null;
This will change it to returning null, if it would otherwise throw an exception.
BUT all of this still has a fatal flaw, that if the list is mutated between calls, that you could be potentially skipping or changing the order around.
A far better solution to this, is to instead cache the visited users, as well as the last visited user.
If the users are ordered, and you store the last visited user, instead of the index, you are storing data that is much more resilient to change, and more closely matches the behavior you want.
To more closely match your needs, you are asking that.
Generate a predictable, ordered list of users that don't include the admin, or anyone else that is vanished, to aid the admin in predicting where they are going.
Rotate through this list, by right clicking with a tool, (Note this is async, so all the state needs to be saved)
Ensure that all visited users are visited before repeating the sequence.
public class TeleportTooldata {
private ListIterator<UUID> cursor;
private List<UUID> cachedOrder;
public TeleportTooldata(List<UUID> applicableUsers) {
cachedOrder = applicableUsers;
}
#Nullable
public UUID next() {
if (!cursor.hasNext()) return null;
UUID next = cursor.next();
if (!cachedOrder.contains(next)) {
cachedOrder.add(next);
}
return next;
}
public void Update(List<UUID> applicableUsers) {
applicableUsers.removeAll(cachedOrder);
cachedOrder.addAll(applicableUsers);
}
}
public class TeleportToolUtil {
YourPluginUserRepo repo;
Map<User, TeleportTooldata> storage; //This could be a cache, make sure to remove if they log out, or maybe timed as well.
public List<UUID> getApplicableUsers() {
return repo.getOnlineUsers().stream()
.filter(User::isVanish)
.sorted(Comparator.comparing(User::getId)) // You can change the sort order
.map(User::getId)
.collect(Collectors.toList());
}
public void onToolUse(User user) {
TeleportTooldata data = storage.computeIfAbsent(user, x -> new TeleportTooldata(getApplicableUsers()));
UUID next = data.next();
if (next == null) {
data.Update(getApplicableUsers());
next = data.next();
if(next == null) {
storage.put(user, new TeleportTooldata(getApplicableUsers()));
next = data.next();
}
}
user.teleportTo(next);
}
}
A few changes.
We are now caching the ordering, so that you could conceptually also let the user go backwards through the list.
We are using ListIterator. ListIterator is an object that loops through lists, and stores the current position for you! Much like you were doing before, but without indexes.
We now have the possibility to update the data, in case a player joins late, or someone unvanishes they will be put at the back of the list if they are not already inside it.
when we run out of users, we attempt an update, if we are really out, we start again with a brand new list. (note this won't guarantee the same order every time (people will be 'properly' sorted when it updates if they were previously appended, but it's close enough for this usecase)
However! We still need to be mindful of memory leaks. using UUID's rather then players or users, means this class is very light weight, we should be pretty safe from memory leaks in the list of UUID AS LONG as the TeleportTooldata doesn't live too long.
You can replace the Map of TeleportTooldata with a cache (maybe from Guava?) to remove the data some time after the admin leaves the game.
If TeleportTooldata was expected to be long-lived, we would want to seriously consider removing UUID's from the history.
Also, not handled in my example, is the possibility of the users going offline after the order is cached.
To handle this, before teleporting the player, check if the uuid is online, otherwise go to the 'next' and follow all the same logic again.

Java Multiple Sets within a Map

I am reading from a CSV file which contains data about Hills, all the reading etc works fine but now I want to create a map which associates the "County Name" (A column header in the file) with all the hills that have the county name in it.
I am along the right path, because my code works for the first countyName, but the problem is when my for loop If statement goes to the else statement, I want it to technically create another set (clearing the previous values) and assigning the new hill data with the county name
My output is:
{Perth and Kinross=[16,Knock of Crieff,Perth and Kinross,279.0,56.389329,-3.826973, 3,Creag Uchdag,Perth and Kinross,879.0,56.465278,-4.098107]}
So it has all the hills with "Perth and Kinross" in it. So now my next county name is "Stirling" for example so the map should end up like
{Perth and Kinross=[16,Knock of Crieff,Perth and Kinross,279.0,56.389329,-3.826973, 3,Creag Uchdag,Perth and Kinross,879.0,56.465278,-4.098107], Stirling=[7,Meall Buidhe,Stirling,719.0,56.419004,-4.308645]}
What I'm unsure about is how to create another set without clearing the values stored for Perth and Kinross
My code is:
Map<String, Set<Hill>> hillsByCounty = new HashMap<>();
if (h.getCounty().equals(countyName)) {
hillsByCounty.get(countyName);
currentSet.add(h);
hillsByCounty.put(countyName, currentSet);
} else {
countyName = h.getCounty();
currentSet.clear();
currentSet.add(h);
}
}
return hillsByCounty;
}
Where exactly is it going wrong. I have a feeling it is the clear function but i'm not sure how else i would do this.
My current code prints out:
{Perth and Kinross=[7,Meall Buidhe,Stirling,719.0,56.419004,-4.308645], Stirling=[7,Meall Buidhe,Stirling,719.0,56.419004,-4.308645]}
As its overwriting the set. What would be my workaround?
You reuse the same set. So if your county changes, you clear the set and fill it with the next county. Instead you should create a new set. As long as you reuse the same set, well, you reuse the same set, empty it and fill something else in it. hillsByCounty.put(...) does not clone the set, but just stores a reference.
The value for each entry of the map is the same set so every modification on one set will also appear at the other entries.
You can check for an existing set in the loop body:
for (Hill h : hills) {
Set<Hill> currentSet = hillsByCountry.get(h.getCountry());
if (currentSet == null) {
currentSet = new HashSet<>();
hillsByCountry.put(h.getCountry(), currentSet);
}
currentSet.add(h);
}

Comparing objects: using enhanced for loop

I am iterating through an array of objects (different website types). But I want to be able to show which website has the highest amount of members. Initially my code shows the amount of members of each website, but my code isn't complete as im not sure how to compare the objects to see which has the highest amount of members. Current code:
public static void mostUsers(){
for (Website w: websiteTypes){
// check if the array index is not null
if (w!=null){
System.out.println(w.getType() + "has:" + w.getMembers());
System.out.println(); // line break
**//Code to compare amount of users on each site and display highest**
} else {
// no more records so quit loop
break;
}
}
}
You need to keep track of the maximum number of users while your iterating.
Initialize a variable called maxWebsite to null before your loop
Initialize an integer called max to Integer.MIN_VALUE before your loop
While your iterating, test if w.getMaxUsers() is superior than the max value.
If it is, update accordingly the maxWebsite and max variables
Note : remove the else statement in your loop, you have to iterate all the elements of your array.
If you're using Collections, that could be simple using Collections.max with a custom comparator :
List<Website> l = .......
l.removeAll(Collections.singleton(null)); //remove null elements from the list
Website maxWebsite = Collections.max(l, new Comparator<Website>(){
#Override
public int compare(Website arg0, Website arg1) {
return Integer.compare(arg0.getMaxUsers(), arg1.getMaxUsers());
}
});
Use a Comparator, or have Website extend Comparable.
Then, sort your collection of Websites using sort. The websites with the most views will then be at the beginning of your collection.

Trying to compare a HashSet element with an element in a List

I have a HashSet that I created and this is what it contains. It will contain more later on, this is pasted from standard out when I did a toString on it. Just to show the contents.
foo.toString(): Abstractfoo [id=2, serial=1d21d, value=1.25, date=2012-09-02 12:00:00.0]
INFO [STDOUT] price.toString(): Abstractfoo [id=1, serial=1d24d, value=1.30, date=2012-09-19 12:00:00.0]
I have a List that I also have and I need to compare the two. One of the elements in List is:
Bar.toString(): Bar [id=1d21d, name=Dell, description=Laptop, ownerId=null]
Here is what I am trying to do...
Bar contains all of the elements I want foo to have. There will only be one unique serial. I would like my program to see if an element in the list that is in HashSet contains the id for bar. So serial == id.
Here is what I've been trying to do
Removed code and added clearer code below
I've verified the data is getting entered into the HashSet and List correctly by viewing it through the debugger.
foo is being pulled from a database through hibernate, and bar is coming from a different source. If there is an element in bar I need to add it to a list and I'm passing it back to my UI where I'll enter some additional data and then commit it to the database.
Let me know if this makes sense and if I can provide anymore information.
Thanks
EDIT: Here is the class
#RequestMapping(value = "/system", method = RequestMethod.GET)
public #ResponseBody
List<AbstractSystem> SystemList() {
// Retrieve system list from database
HashSet<AbstractSystem> systemData = new HashSet<AbstractSystem>(
systemService.getSystemData());
// Retrieve system info from cloud API
List<SystemName> systemName= null;
try {
systemName = cloudClass.getImages();
} catch (Exception e) {
LOG.warn("Unable to get status", e);
}
// Tried this but, iter2 only has two items and iter has many more.
// In production it will be the other way around, but I need to not
// Have to worry about that
Iterator<SystemName> iter = systemName.iterator();
Iterator<AbstractSystem> iter2 = systemData .iterator();
while(iter.hasNext()){
Image temp = iter.next();
while(iter2.hasNext()){
AbstractPricing temp2 = iter2.next();
System.out.println("temp2.getSerial(): " + temp2.getSerial());
System.out.println("temp.getId(): " + temp.getId());
if(temp2.getSerial().equals(temp.getId())){
System.out.println("This will be slow...");
}
}
}
return systemData;
}
If N is the number of items in systemName and M is the number of items in systemData, then you've effectively built an O(N*M) method.
If you instead represent your systemData as a HashMap of AbstractSystem by AbstractSystem.getSerial() values, then you just loop through the systemName collection and lookup by systemName.getId(). This becomes more like O(N+M).
(You might want to avoid variables like iter, iter2, temp2, etc., since those make the code harder to read.)
EDIT - here's what I mean:
// Retrieve system list from database
HashMap<Integer, AbstractSystem> systemDataMap = new HashMap<AbstractSystem>(
systemService.getSystemDataMap());
// Retrieve system info from cloud API
List<SystemName> systemNames = cloudClass.getImages();
for (SystemName systemName : systemNames) {
if (systemDataMap.containsKey(systemName.getId()) {
System.out.println("This will be slow...");
}
}
I used Integer because I can't tell from your code what the type of AbstractSystem.getSerial() or SystemName.getId() are. This assumes that you store the system data as a Map elsewhere. If not, you could construct the map yourself here.

Categories

Resources