I am learning to work with optaplanner. I need to spread exams of one student. The less time between two exams of one student, the more penalty I give.
I need the Integer List ExamIds of my Student class because there are all the exams for that one student.
Then I need to check all these Exams planned Timeslots with eachother to give them more time between.
What i tried is following code:
`
Constraint spaceBetweenExams(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Student.class)
.join(Exam.class)
.join(Exam.class)
.filter((student, exam1, exam2) ->{
if(student.getExamIds().contains(exam1.getID()) && student.getExamIds().contains(exam2.getID())){
if(exam1.getID() < exam2.getID()){
int timeDifference = Math.abs(exam1.getTimeslot().getID() - exam2.getTimeslot().getID());
if (timeDifference == 1) {
penalty = 16;
} else if (timeDifference == 2) {
penalty = 8;
} else if (timeDifference == 3) {
penalty = 4;
} else if (timeDifference == 4) {
penalty = 2;
} else if (timeDifference == 5) {
penalty = 1;
}
return true;
}
}
return false;
})
.penalize("Max time between exams", HardSoftScore.ofSoft(penalty));
`
The result I get is 24645 soft penalties but optaplanner doesn't even try to fix them.
I think that the way I check the exams in the code above is not fully correct.
This is my constraint class :
public class ExamTableConstraintProvider implements ConstraintProvider {
int penalty = 0;
List<Integer> studentExamIds;
#Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[]{
// Hard constraints
twoExamForStudentConflict(constraintFactory),
// Soft constraints
spaceBetweenExams(constraintFactory)
};
}
private Constraint twoExamForStudentConflict(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Exam.class)
.join(Exam.class,
Joiners.equal(Exam::getTimeslot),
Joiners.lessThan(Exam::getID))
.filter((exam1, exam2) -> {
List<Integer> result = new ArrayList<>(exam1.getSID());
result.retainAll(exam2.getSID());
return result.size() > 0;
})
.penalize("Student conflict", HardSoftScore.ONE_HARD);
}
Constraint spaceBetweenExams(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Student.class)
.join(Exam.class)
.join(Exam.class)
.filter((student, exam1, exam2) ->{
if(student.getExamIds().contains(exam1.getID()) && student.getExamIds().contains(exam2.getID())){
if(exam1.getID() < exam2.getID()){
int timeDifference = Math.abs(exam1.getTimeslot().getID() - exam2.getTimeslot().getID());
if (timeDifference == 1) {
penalty = 16;
} else if (timeDifference == 2) {
penalty = 8;
} else if (timeDifference == 3) {
penalty = 4;
} else if (timeDifference == 4) {
penalty = 2;
} else if (timeDifference == 5) {
penalty = 1;
}
return true;
}
}
return false;
})
.penalize("Max time between exams", HardSoftScore.ofSoft(penalty));
}
/*Constraint spaceBetweenExams(ConstraintFactory constraintFactory) {
penalty = 0;
return constraintFactory.forEach(Student.class)
.join(Exam.class,
equal(Student::getExamIds, Exam::getID),
filtering((student, exam1) -> exam1.getTimeslot() != null))
.join(Exam.class,
equal((student, exam1) -> student.getExamIds(), Exam::getID),
equal((student, exam1) -> exam1.getTimeslot(), Exam::getTimeslot),
filtering((student, exam1, exam2) -> {
int timeDifference = getPeriodBetweenExams(exam1, exam2);
if (timeDifference == 1) {
penalty += 16;
} else if (timeDifference == 2) {
penalty += 8;
} else if (timeDifference == 3) {
penalty += 4;
} else if (timeDifference == 4) {
penalty += 2;
} else if (timeDifference == 5) {
penalty += 1;
}
if(penalty == 0){
return false;
}
return true;
}))
.penalize("Max time between exams", HardSoftScore.ONE_SOFT);
}*/
}
And this is the class where I start the program:
public class ExamTableApp {
private static final Logger LOGGER = LoggerFactory.getLogger(ExamTableApp.class);
public static void main(String[] args) {
SolverFactory<ExamTable> solverFactory = SolverFactory.create(new SolverConfig()
.withSolutionClass(ExamTable.class)
.withEntityClasses(Exam.class)
.withConstraintProviderClass(ExamTableConstraintProvider.class)
// The solver runs only for 5 seconds on this small dataset.
// It's recommended to run for at least 5 minutes ("5m") otherwise.
.withTerminationSpentLimit(Duration.ofSeconds(30)));
// Load the problem
ExamTable problem = getData();
// Solve the problem
Solver<ExamTable> solver = solverFactory.buildSolver();
ExamTable solution = solver.solve(problem);
// Visualize the solution
printTimetable(solution);
}
public static ExamTable getData(){
DataReader parser = new DataReader("benchmarks/sta-f-83.crs", "benchmarks/sta-f-83.stu");
List<Room> roomList = new ArrayList<>(1);
roomList.add(new Room(1,"Room A"));
// roomList.add(new Room(2,"Room B"));
// roomList.add(new Room(3,"Room C"));
List<Exam> examList = new ArrayList<>();
HashMap<Integer, Exam> exams = parser.getExams();
Set<Integer> keys = exams.keySet();
for (Integer i : keys) {
Exam exam = exams.get(i);
examList.add(new Exam(exam.getID(), exam.getSID()));
}
List<Student> studentList = new ArrayList<>();
HashMap<Integer, Student> students = parser.getStudents();
Set<Integer> keys2 = students.keySet();
for (Integer i : keys2) {
Student student = students.get(i);
studentList.add(new Student(student.getID(), student.getExamIds()));
}
return new ExamTable(parser.getTimeslots(), roomList, examList, studentList);
}
private static void printTimetable(ExamTable examTable) {
LOGGER.info("");
List<Room> roomList = examTable.getRoomList();
List<Exam> examList = examTable.getExamList();
Map<TimeSlot, Map<Room, List<Exam>>> examMap = examList.stream()
.filter(exam -> exam.getTimeslot() != null && exam.getRoom() != null)
.collect(Collectors.groupingBy(Exam::getTimeslot, Collectors.groupingBy(Exam::getRoom)));
LOGGER.info("| | " + roomList.stream()
.map(room -> String.format("%-10s", room.getName())).collect(Collectors.joining(" | ")) + " |");
LOGGER.info("|" + "------------|".repeat(roomList.size() + 1));
for (TimeSlot timeslot : examTable.getTimeslotList()) {
List<List<Exam>> cellList = roomList.stream()
.map(room -> {
Map<Room, List<Exam>> byRoomMap = examMap.get(timeslot);
if (byRoomMap == null) {
return Collections.<Exam>emptyList();
}
List<Exam> cellLessonList = byRoomMap.get(room);
if (cellLessonList == null) {
return Collections.<Exam>emptyList();
}
return cellLessonList;
})
.collect(Collectors.toList());
LOGGER.info("| " + String.format("%-10s",
timeslot.getID() + " " + " | "
+ cellList.stream().map(cellLessonList -> String.format("%-10s",
cellLessonList.stream().map(Exam::getName).collect(Collectors.joining(", "))))
.collect(Collectors.joining(" | "))
+ " |"));
LOGGER.info("|" + "------------|".repeat(roomList.size() + 1));
}
List<Exam> unassignedExams = examList.stream()
.filter(exam -> exam.getTimeslot() == null || exam.getRoom() == null)
.collect(Collectors.toList());
if (!unassignedExams.isEmpty()) {
LOGGER.info("");
LOGGER.info("Unassigned lessons");
for (Exam exam : unassignedExams) {
LOGGER.info(" " + exam.getName() + " - " + exam.getNumberOfStudents() + " - " + exam.getSID());
}
}
}
}
Could someone help me out with this?
Thanks in advance.
The code you provided shows one major misunderstanding of how Constraint Streams work, which in turn makes it fundamentally broken.
Any instance of ConstraintProvider must be stateless. Even though you technically can have fields in that class, there is no use for it, as the constraints - when run - need to be without side effects. This way, you have introduced score corruptions and probably have not even noticed.
Also, the constraint weight HardScore.ofSoft(...) is only computed once during constraint creation, not at solver runtime, therefore defining it like you did would have been pointless anyway. (Its value will be whatever penalty was at instantiation, therefore 0.) What you need to use instead is a match weight, which is computed at runtime. Making just that modification, the constraint in question would look like this:
Constraint spaceBetweenExams(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Student.class)
.join(Exam.class)
.join(Exam.class)
.filter((student, exam1, exam2) -> {
if(student.getExamIds().contains(exam1.getID()) && student.getExamIds().contains(exam2.getID())){
if(exam1.getID() < exam2.getID()){
return true;
}
}
return false;
})
.penalize("Max time between exams", HardSoftScore.ONE_SOFT,
(student, exam1, exam2) ->{
int timeDifference = Math.abs(exam1.getTimeslot().getID() - exam2.getTimeslot().getID());
if (timeDifference == 1) {
return 16;
} else if (timeDifference == 2) {
return 8;
} else if (timeDifference == 3) {
return 4;
} else if (timeDifference == 4) {
return 2;
} else {
return 1;
}
}
});
}
The above code will at least make this code run properly, but it will likely be slow. See my other recent answer for inspiration on how to change your domain model to improve performance of your constraints. It may not fit exactly, but the idea seems valid to your use case as well.
Related
The class AnagramGameDefault simulates an anagram game.
The submitScore() should recalculate the positions, the one with highest score has position 1, there can be several players on the same position.
The getLeaderBoard() fetches the entry for a user plus two above and two below.
The concerns I have :
I tested the code for multiple threads and I guess it's working but I would like to know if there are some race conditions or failure in sharing state in the code
I have used quite a stringent mutually exclusive locking by using 'synchronized'. I don't think this can be avoided as submitScore() and getLeaderBoard() rely heavily on sorting and correct positions of score but is there a possibility ? I read a bit about ReentrantLock but it's more suitable where there are multiple reads and lesser writes, in this case, even the reads need calculations.
public enum AnagramGameDefault{
INSTANCE;
private Map<String, Entry> leaderBoardUserEntryMap;
{
leaderBoardUserEntryMap = new LinkedHashMap<>();
}
public int calculateScore(String word, String anagram) {
if (word == null || anagram == null) {
throw new NullPointerException("Both, word and anagram, must be non-null");
}
char[] wordArray = word.trim().toLowerCase().toCharArray();
char[] anagramArray = anagram.trim().toLowerCase().toCharArray();
int[] alphabetCountArray = new int[26];
int reference = 'a';
for (int i = 0; i < wordArray.length; i++) {
if (!Character.isWhitespace(wordArray[i])) {
alphabetCountArray[wordArray[i] - reference]++;
}
}
for (int i = 0; i < anagramArray.length; i++) {
if (!Character.isWhitespace(anagramArray[i])) {
alphabetCountArray[anagramArray[i] - reference]--;
}
}
for (int i = 0; i < 26; i++)
if (alphabetCountArray[i] != 0)
return 0;
return word.length();
}
public void submitScore(String uid, int score) {
Entry newEntry = new Entry(uid, score);
sortLeaderBoard(newEntry);
}
private void sortLeaderBoard(Entry newEntry) {
synchronized (leaderBoardUserEntryMap) {
leaderBoardUserEntryMap.put(newEntry.getUid(), newEntry);
// System.out.println("Sorting for " + newEntry);
List<Map.Entry<String, Entry>> list = leaderBoardUserEntryMap.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Collections.reverseOrder())).collect(Collectors.toList());
leaderBoardUserEntryMap.clear();
int position = 0;
int previousPosition = 0;
int currentPosition = 0;
for (Map.Entry<String, Entry> entry : list) {
currentPosition = entry.getValue().getScore();
if (!(currentPosition == previousPosition))
position++;
entry.getValue().setPosition(position);
leaderBoardUserEntryMap.put(entry.getKey(), entry.getValue());
previousPosition = currentPosition;
}
}
}
public List<Entry> getLeaderBoard(String uid) {
final int maxEntriesAroundAnEntry = 2;
if (!leaderBoardUserEntryMap.containsKey(uid))
return Collections.emptyList();
Entry userEntry = null;
final List<Entry> leaderBoard = new ArrayList<>();
List<Entry> lowerEntries = null;
List<Entry> higherEntries = null;
synchronized (leaderBoardUserEntryMap) {
printBoard();
userEntry = leaderBoardUserEntryMap.get(uid);
int userPosition = userEntry.getPosition();
int upperPosition = userPosition - maxEntriesAroundAnEntry;
int lowerPosition = userPosition + maxEntriesAroundAnEntry;
// Higher entries
higherEntries = leaderBoardUserEntryMap.values().stream()
.filter(entry -> (entry.getPosition() < userPosition && entry.getPosition() >= upperPosition))
.map(entry -> new Entry(entry.getUid(), entry.getScore(), entry.getPosition()))
.collect(Collectors.toList());
// Lower entries
lowerEntries = leaderBoardUserEntryMap.values().stream()
.filter(entry -> (entry.getPosition() > userPosition && entry.getPosition() <= lowerPosition))
.map(entry -> new Entry(entry.getUid(), entry.getScore(), entry.getPosition()))
.collect(Collectors.toList());
userEntry = new Entry(userEntry.getUid(), userEntry.getScore(), userEntry.getPosition());
// }
if (higherEntries != null && !higherEntries.isEmpty()) {
if (higherEntries.size() >= maxEntriesAroundAnEntry) {
higherEntries = higherEntries.subList(higherEntries.size() - maxEntriesAroundAnEntry,
higherEntries.size());
}
leaderBoard.addAll(higherEntries);
}
leaderBoard.add(userEntry);
if (lowerEntries != null && !lowerEntries.isEmpty()) {
if (lowerEntries.size() >= maxEntriesAroundAnEntry) {
lowerEntries = lowerEntries.subList(0, maxEntriesAroundAnEntry);
}
leaderBoard.addAll(lowerEntries);
}
}
return leaderBoard;
}
public void printBoard() {
System.out.println("---------Start : Current leader board---------");
leaderBoardUserEntryMap.forEach((key, value) -> {
System.out.println("BOARD ENTRY : " + key + " : " + value);
});
System.out.println("---------End : Current leader board---------");
}
}
The Entry POJO :
public class Entry implements Comparable<Entry> {
private String uid;
private int score;
private int position;
public Entry(String uid, int score) {
this.uid = uid;
this.score = score;
}
public Entry(String uid, int score, int position) {
this.uid = uid;
this.score = score;
this.position = position;
}
public Entry() {
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
public int getPosition() {
return position;
}
public void setPosition(int position) {
this.position = position;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + score;
result = prime * result + ((uid == null) ? 0 : uid.hashCode());
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Entry other = (Entry) obj;
if (score != other.score)
return false;
if (uid == null) {
if (other.uid != null)
return false;
} else if (!uid.equals(other.uid))
return false;
return true;
}
#Override
public String toString() {
return "Entry [uid=" + uid + ", score=" + score + ", position=" + position + "]";
}
#Override
public int compareTo(Entry o) {
// TODO Auto-generated method stub
if (o == null)
return -1;
return Integer.compare(score, o.getScore());
}
}
The tester class :
public class AnagramGameDefaultDemo {
public static void main(String[] args) {
if (args == null || args.length < 1) {
System.out.println("Enter testing approach - 1 for single threaded, 2 for multi-threaded");
return;
}
switch (args[0]) {
case "1": {
new AnagramGameDefaultDemo().testSingleThreaded();
break;
}
case "2": {
new AnagramGameDefaultDemo().testMultithreaded();
break;
}
default: {
System.out.println("Enter proper option(1 or 2)");
break;
}
}
}
private void testMultithreaded() {
Map<String, String> stringAnagramMap = new HashMap<>();
CountDownLatch countDownLatchOne = new CountDownLatch(1);
stringAnagramMap.put("raw", "war");
stringAnagramMap.put("raw", "wars");
AnagramGamePlayer jake = new AnagramGamePlayer("jake", stringAnagramMap, countDownLatchOne);
new Thread(jake, "jake").start();
stringAnagramMap.clear();
stringAnagramMap.put("tool", "loot");
AnagramGamePlayer ace = new AnagramGamePlayer("ace", stringAnagramMap, countDownLatchOne);
new Thread(ace, "ace").start();
stringAnagramMap.clear();
stringAnagramMap.put("William Shakespeare", "I am a weakish speller");
AnagramGamePlayer max = new AnagramGamePlayer("max", stringAnagramMap, countDownLatchOne);
new Thread(max, "max").start();
stringAnagramMap.clear();
stringAnagramMap.put("School master", "The classroom");
AnagramGamePlayer tBone = new AnagramGamePlayer("tBone", stringAnagramMap, countDownLatchOne);
new Thread(tBone, "tBone").start();
stringAnagramMap.clear();
countDownLatchOne.countDown();
CountDownLatch countDownLatchTwo = new CountDownLatch(1);
stringAnagramMap.put("Punishments", "Nine Thumps");
AnagramGamePlayer razor = new AnagramGamePlayer("razor", stringAnagramMap, countDownLatchTwo);
new Thread(razor, "razor").start();
stringAnagramMap.clear();
stringAnagramMap.put("Dormitory", "Dirty Room");
AnagramGamePlayer chip = new AnagramGamePlayer("chip", stringAnagramMap, countDownLatchTwo);
new Thread(chip, "chip").start();
stringAnagramMap.clear();
countDownLatchTwo.countDown();
CountDownLatch countDownLatchThree = new CountDownLatch(1);
stringAnagramMap.put("Mother in law", "Hitler woman");
AnagramGamePlayer dale = new AnagramGamePlayer("dale", stringAnagramMap, countDownLatchThree);
new Thread(dale, "dale").start();
countDownLatchThree.countDown();
stringAnagramMap.clear();
}
private final class AnagramGamePlayer implements Runnable {
private Map<String, String> stringAnagramMap = new HashMap<>();
private String uid;
private CountDownLatch countDownLatch;
public AnagramGamePlayer(String uid, Map<String, String> stringAnagramMap, CountDownLatch countDownLatch) {
this.stringAnagramMap.putAll(stringAnagramMap);
this.uid = uid;
this.countDownLatch = countDownLatch;
}
#Override
public void run() {
AnagramGameDefault anagramGameDefault = AnagramGameDefault.INSTANCE;
try {
countDownLatch.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("Player " + uid + " started playing with " + stringAnagramMap);
stringAnagramMap.entrySet().forEach(entry -> {
anagramGameDefault.submitScore(uid,
anagramGameDefault.calculateScore(entry.getKey(), entry.getValue()));
printLeaderBoard(uid, anagramGameDefault.getLeaderBoard(uid));
});
System.out.println("Player " + uid + " completed playing");
}
}
private void testSingleThreaded() {
AnagramGameDefault anagramGameDefault = AnagramGameDefault.INSTANCE;
anagramGameDefault.submitScore("Jake", 3);
anagramGameDefault.submitScore("Ace", 7);
anagramGameDefault.submitScore("Max", 1);
anagramGameDefault.submitScore("T-Bone", 14);
anagramGameDefault.submitScore("Razor", 6);
anagramGameDefault.submitScore("Razor", 7);
anagramGameDefault.submitScore("He-Man", 4);
anagramGameDefault.submitScore("Men-at-Arms", 8);
anagramGameDefault.submitScore("BattleCat", 3);
anagramGameDefault.submitScore("Jake", 2);
anagramGameDefault.submitScore("BattleCat", 3);
anagramGameDefault.printBoard();
anagramGameDefault.submitScore("Men-at-Arms", 21);
anagramGameDefault.submitScore("Orko", 20);
anagramGameDefault.submitScore("Jake", 4);
anagramGameDefault.printBoard();
System.out.println();
printLeaderBoard("user5", anagramGameDefault.getLeaderBoard("user5"));
System.out.println();
printLeaderBoard("user4", anagramGameDefault.getLeaderBoard("user4"));
System.out.println();
printLeaderBoard("user15", anagramGameDefault.getLeaderBoard("user15"));
System.out.println();
List<Entry> entries = anagramGameDefault.getLeaderBoard("user1");
printLeaderBoard("user1", entries);
System.out.println("Changing state of the received entries");
entries.forEach(entry -> {
entry.setPosition(1);
entry.setScore(0);
});
anagramGameDefault.printBoard();
printLeaderBoard("user1", anagramGameDefault.getLeaderBoard("user1"));
}
private static void printLeaderBoard(String user, List<Entry> leaderBoard) {
if (user == null || leaderBoard.isEmpty()) {
System.out.println("Either user " + user + " doesn't exist or leader board is empty " + leaderBoard);
}
System.out.println("**********Printing leader board for " + user);
leaderBoard.forEach(System.out::println);
System.out.println("**********");
}
}
It seems the only shared state you have in the whole thing is leaderBoardUserEntryMap. You synchronize on it when updating in sortLeaderBoard. In getLeaderBoard for some reason you don't yet synchronize on it when checking if (!leaderBoardUserEntryMap.containsKey(uid)). That's minor, but you should do it as well. Later, you synchronize on it when constructing the leader board.
From this point of view your synchronization seems adequate.
What I find a bit problematic is that your Entry is mutable and stores position. This makes your update operation more problematic. You have to re-sort and re-set positions on every update. And you're locking all other update or even read operations. I'd avoid mutable objects in multithreaded code.
I'd use a SortedSet instead and let the implementation handle sorting. To find out the position of the element you'd just do set.headSet(element).size() + 1. So no need to store position at all.
I initially wanted to suggest using concurrent collection implementations like ConcurrentHashSet which would allow "full concurrency of retrievals and adjustable expected concurrency for updates". Basically, retrievals could be non-blocking, only updates were.
However, this won't help much as your "retrieval" logic (creating the leader board around target entry) is not so simple and involves several reads. So I think it's better not use concurrent collections but instead actually synchronize on the collection and make updates and retrievals as compact as possible. If you give up on the idea of having position in Entry then update is a trivial add. Then you'll only need to read entries around the entry as fast as possible (within the synchronized block). This should be quite fast with tree sets.
I am a new in java and programming in general.
I am currently doing complex numbers. I know that there might be an answer for this online, but it will also reveal if I used the correct algorithm and so on, so I am trying to avoid other answers around here.
My main issue is that I am having trouble with the Divide class that I made, since in complex number division we are going to return a fraction as an answer, I can't figure out how to have the program return the 2 statements that it calculate, can someone advise what can be done? Here is the part of the division code, it works fine when I check both part 1 and then part 2, but how can i get it to return both of them when calling using that class.
I attached my full code that I made, I know it can be tweaked to have less coding, but that is not my current concern.
Thanks for your help in advance.
class complexN {
int R, I;
complexN(int R, int I) {
this.R = R;
this.I = I;
}
complexN AddCN(complexN A) {
return new complexN(this.R + A.R, this.I + A.I);
}
complexN SubCN(complexN A) {
int AnsR = this.R - A.R;
int AnsI = this.I - A.I;
complexN Sum = new complexN(AnsR, AnsI);
return Sum;
}
complexN MulCN(complexN A) {
int AnsI = (this.R * A.I) + (this.I * A.R);
int AnsR = (this.R * A.R) - (this.I * A.I);
complexN Sum = new complexN(AnsR, AnsI);
return Sum;
}
complexN DivCN(complexN A) {
complexN ComCon = new complexN(A.R, (A.I * -1));
complexN part1 = new complexN(this.R, this.I).MulCN(ComCon);
complexN part2 = A.MulCN(ComCon);
return part1;
}
void print() {
String i = (this.I == 1 ? "" : (this.I == -1 ? "-" : "" + this.I));
if (this.R != 0 && this.I > 0) {
System.out.println(this.R + "+" + i + "i");
}
if (this.R != 0 && this.I < 0) {
System.out.println(this.R + i + "i");
}
if (this.R != 0 && this.I == 0) {
System.out.println(this.R);
}
if (this.R == 0 && this.I != 0) {
System.out.println(i + "i");
}
if (this.R == 0 && this.I == 0) {
System.out.println("0");
}
}
}
class complex {
public static void main(String[] args) {
// TODO Auto-generated method stub
complexN z1 = new complexN(5, 2);
complexN z2 = new complexN(3, -4);
System.out.print("z1 = ");
z1.print();
System.out.print("z2 = ");
z2.print();
System.out.println("---------");
z1.DivCN(z2).print();
}
}
I plan in JavaFX a new Game 'Number-Shape-System'. Basically its a little memory game where pictures are associated with numbers. So '2'='Swan', '5'='Hand(Fingers)' and so on. So the player see the exercise 'Swan + Fingers = ?'.
What I want is all possible mathematically operations following rules:
/*
* Generate all possible mathematical operations to the console with the numbers
* 0-12, where every result is (>= 0 && <= 12).
* - Mathematical operations are '+', '-', '*' and '/'.
* - The rule 'dot before line' shouldn't be used, instead the operations will
* be executed from left to right.
* - Every among result must be between (>= 0 && <= 12) and a whole number.
* - Only different numbers are allowed for the operations (an operation have
* 2 numbers). For example 2+3 is allowed, 3*3 not.
*
* A solution with recursive methods would be preferred. I want the output for
* the length 2-10.
*
* Example output with different length:
* - Length 3: 2+3(=5)*2(=10)
* - Length 5: 2+3(=5)*2(=10)+2(=12)/4(=3)
*/
I have prepared a example implementation, but I don't know how to convert it to a recursive functionality.
import java.util.ArrayList;
import java.util.List;
public class Generator {
private static final List<Double> numbers = new ArrayList<>();
private static final List<String> operations = new ArrayList<>();
static {
numbers.add(0.0);
numbers.add(1.0);
numbers.add(2.0);
numbers.add(3.0);
numbers.add(4.0);
numbers.add(5.0);
numbers.add(6.0);
numbers.add(7.0);
numbers.add(8.0);
numbers.add(9.0);
numbers.add(10.0);
numbers.add(11.0);
numbers.add(12.0);
operations.add("+");
operations.add("-");
operations.add("*");
operations.add("/");
}
private int lineCounter = 0;
public Generator() {
this.init();
}
private void init() {
}
public void generate() {
// Length 2 ###########################################################
boolean okay = false;
int lineCounter = 0;
StringBuilder sbDouble = new StringBuilder();
for (Double first : numbers) {
for (Double second : numbers) {
for (String operation : operations) {
if (first == second) {
continue;
}
if (operation.equals("/") && (first == 0.0 || second == 0.0)) {
continue;
}
double result = perform(first, operation, second);
okay = this.check(result, operation);
if (okay) {
++lineCounter;
sbDouble = new StringBuilder();
this.computeResultAsString(sbDouble, first, operation, second, result);
System.out.println(sbDouble.toString());
}
}
}
}
System.out.println("Compute with length 2: " + lineCounter + " lines");
// Length 2 ###########################################################
// Length 3 ###########################################################
okay = false;
lineCounter = 0;
sbDouble = new StringBuilder();
for (Double first : numbers) {
for (Double second : numbers) {
for (String operation1 : operations) {
if (first == second) {
continue;
}
if (operation1.equals("/") && (first == 0.0 || second == 0.0)) {
continue;
}
double result1 = perform(first, operation1, second);
okay = this.check(result1, operation1);
if (okay) {
for (Double third : numbers) {
for (String operation2 : operations) {
if (second == third) {
continue;
}
if (operation2.equals("/") && third == 0.0) {
continue;
}
double result2 = perform(result1, operation2, third);
okay = this.check(result2, operation2);
if (okay) {
++lineCounter;
sbDouble = new StringBuilder();
this.computeResultAsString(sbDouble, first, operation1, second, result1);
this.computeResultAsString(sbDouble, operation2, third, result2);
System.out.println(sbDouble.toString());
}
}
}
}
}
}
}
System.out.println("Compute with length 3: " + lineCounter + " lines");
// Length 3 ###########################################################
// Length 4 ###########################################################
okay = false;
lineCounter = 0;
sbDouble = new StringBuilder();
for (Double first : numbers) {
for (Double second : numbers) {
for (String operation1 : operations) {
if (first == second) {
continue;
}
if (operation1.equals("/") && (first == 0.0 || second == 0.0)) {
continue;
}
double result1 = perform(first, operation1, second);
okay = this.check(result1, operation1);
if (okay) {
for (Double third : numbers) {
for (String operation2 : operations) {
if (second == third) {
continue;
}
if (operation2.equals("/") && third == 0.0) {
continue;
}
double result2 = perform(result1, operation2, third);
okay = this.check(result2, operation2);
if (okay) {
for (Double forth : numbers) {
for (String operation3 : operations) {
if (third == forth) {
continue;
}
if (operation3.equals("/") && forth == 0.0) {
continue;
}
double result3 = perform(result2, operation3, forth);
okay = this.check(result3, operation3);
if (okay) {
++lineCounter;
sbDouble = new StringBuilder();
this.computeResultAsString(sbDouble, first, operation1, second, result1);
this.computeResultAsString(sbDouble, operation2, third, result2);
this.computeResultAsString(sbDouble, operation3, forth, result3);
System.out.println(sbDouble.toString());
}
}
}
}
}
}
}
}
}
}
System.out.println("Compute with length 4: " + lineCounter + " lines");
// Length 4 ###########################################################
}
private boolean check(double result, String operation) {
switch (operation) {
case "+":
case "-":
case "*": {
if (result > 0 && result <= 12) {
return true;
}
break;
}
case "/": {
if (
(Math.floor(result) == result)
&& (result >= 0 && result <= 12)
) {
return true;
}
break;
}
}
return false;
}
private double perform(double first, String operation, double second) {
double result = 0.0;
switch (operation) {
case "+": { result = first + second; break; }
case "-": { result = first - second; break; }
case "*": { result = first * second; break; }
case "/": { result = first / second; break; }
}
return result;
}
private void computeResultAsString(StringBuilder sbDouble, String operation, double second, double result) {
sbDouble.append(operation);
sbDouble.append(second);
sbDouble.append("(=");
sbDouble.append(result);
sbDouble.append(")");
}
private void computeResultAsString(StringBuilder sbDouble, double first, String operation, double second, double result) {
sbDouble.append(first);
sbDouble.append(operation);
sbDouble.append(second);
sbDouble.append("(=");
sbDouble.append(result);
sbDouble.append(")");
}
public static void main(String[] args) {
final Generator generator = new Generator();
generator.generate();
}
}
As you can see in your own code, for each increase in "length", you have to nest another block of the same code. With a dynamic length value, you can't do that.
Therefore, you move the block of code into a method, and pass in a parameter of how many more times it has to "nest", i.e. a remainingLength. Then the method can call itself with a decreasing value of remainingLength, until you get to 0.
Here is an example, using an enum for the operator.
public static void generate(int length) {
if (length <= 0)
throw new IllegalArgumentException();
StringBuilder expr = new StringBuilder();
for (int number = 0; number <= 12; number++) {
expr.append(number);
generate(expr, number, length - 1);
expr.setLength(0);
}
}
private static void generate(StringBuilder expr, int exprTotal, int remainingLength) {
if (remainingLength == 0) {
System.out.println(expr);
return;
}
final int exprLength = expr.length();
for (int number = 0; number <= 12; number++) {
if (number != exprTotal) {
for (Operator oper : Operator.values()) {
int total = oper.method.applyAsInt(exprTotal, number);
if (total >= 0 && total <= 12) {
expr.append(oper.symbol).append(number)
.append("(=").append(total).append(")");
generate(expr, total, remainingLength - 1);
expr.setLength(exprLength);
}
}
}
}
}
private enum Operator {
PLUS ('+', Math::addExact),
MINUS ('-', Math::subtractExact),
MULTIPLY('*', Math::multiplyExact),
DIVIDE ('/', Operator::divide);
final char symbol;
final IntBinaryOperator method;
private Operator(char symbol, IntBinaryOperator method) {
this.symbol = symbol;
this.method = method;
}
private static int divide(int left, int right) {
if (right == 0 || left % right != 0)
return -1/*No exact integer value*/;
return left / right;
}
}
Be aware that the number of permutations grow fast:
1: 13
2: 253
3: 5,206
4: 113,298
5: 2,583,682
6: 61,064,003
7: 1,480,508,933
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 8 years ago.
Improve this question
Im doing a tutorial in Java and I always read that I must try not to be repetitive and I noticed this is very repetitive; So if anyone can give me some tips to make it less repetitive or somehow better it is much appriciated. Thanks ;) (This isn't part of the the tutorial, I made it just for fun because of what I am learning in science at school)
Run.java file:
package scientificFormula;
public class Run {
public static void main(String[] args) {
Formula formula = new Formula();
formula.compound1 = args[0];
formula.compound2 = args[1];
String theFormula = formula.createFormula();
System.out.println("Molecule: " + args[0] + " " + args[1] + " = "
+ theFormula);
}
}
Formula.java file:
package scientificFormula;
import java.util.HashMap;
import java.util.Map;
public class Formula {
String compound1;
String compound2;
static private Map<String, String> map = new HashMap<String, String>();
static private void initiateIons() {
// 1+
map.put("Hydrogen", "H^1+");
map.put("Lithium", "Li^1+");
map.put("Sodium", "Na^1+");
map.put("Potassium", "K^1+");
map.put("Rubidium", "Rb^1+");
// 2+
map.put("Magnesium", "Mg^2+");
map.put("Calcium", "Ca^2+");
map.put("Strontium", "Sr^2+");
// 3+
map.put("Aluminium", "Al^3+");
// 3-
map.put("Nitrogem", "N^-3");
map.put("Phosphorus", "P^-3");
// 2-
map.put("Oxygen", "O^-2");
map.put("Sulfur", "S^-2");
map.put("Selenium", "Se^-2");
// 1-
map.put("Fluorine", "F^-1");
map.put("Chlorine", "Cl^-1");
map.put("Bromine", "Br^-1");
map.put("Iodine", "I^-1");
}
String createFormula() {
initiateIons();
// Example1: Input = Calcium Iodine:
// 2x + -1y = 0
// x = 1 and y = 2
// Output = CaI2
//
// Example2: Input = Sulfur Iodine
// Output = Molecule: Sulfur Iodine = SI2
String symbol1 = map.get(compound1);
String symbol2 = map.get(compound2);
int charge1 = Integer.parseInt(symbol1.replace("+", "").substring(
symbol1.length() - 2));
int charge2 = Integer.parseInt(symbol2.replace("+", "").substring(
symbol2.length() - 2));
String letter1 = null;
String letter2 = null;
if (symbol1.length() == 5) {
letter1 = symbol1.substring(0, 2);
} else if (symbol1.length() == 4) {
letter1 = symbol1.substring(0, 1);
}
if (symbol2.length() == 5) {
letter2 = symbol2.substring(0, 2);
} else if (symbol2.length() == 4) {
letter2 = symbol2.substring(0, 1);
}
int possitive1 = (int) Math.sqrt(charge1 * charge1);
int possitive2 = (int) Math.sqrt(charge2 * charge2);
if ((possitive1 == 1) & (possitive2 == 1)) {
return letter1 + letter2;
} else if (possitive1 == 1) {
return letter1 + possitive2 + letter2;
} else if (possitive2 == 1) {
return letter1 + letter2 + possitive1;
}
if (possitive1 == 0) {
possitive1 = -(charge1);
}
if (possitive2 == 0) {
possitive2 = -(charge2);
}
return letter1 + possitive2 + letter2 + possitive1;
}
}
I highly recommend reading the book clean code which mainly deals with refactoring.
Duplicating code is just one (important) issue when you start refactoring (also called DRY - Don't Repeat Yourself). There are many other principles and I'll try describe a few of them which I find most important:
One important "rule of thumb" is the SRP: Single Responsibility Principle, which says that each class should have only one responsibility, and if we apply the same idea to methods - each method should do only one thing! It might sound very strict, but when you'll start applying it - your code will become clearer to read and easier to maintain.
Another one is using meaningful names (classes/methods/variables):
return letter1 + possitive2 + letter2; // you probably meant positive with one 's' (typo!)
might mean something to you - but it will not mean much to another reader - now, of course you can solve it by adding code comments, but that's patching the problem instead of solving it. Further, code comments get stale - either become irrelevant or even worse - might mislead the reader when the code changes and the comment doesn't.
And last (for now), keep clear order of execution, let's take a piece of code that you posted and improve it:
String symbol1 = map.get(compound1);
String symbol2 = map.get(compound2);
int charge1 = Integer.parseInt(symbol1.replace("+", "").substring(
symbol1.length() - 2));
int charge2 = Integer.parseInt(symbol2.replace("+", "").substring(
symbol2.length() - 2));
String letter1 = null;
String letter2 = null;
if (symbol1.length() == 5) {
letter1 = symbol1.substring(0, 2);
} else if (symbol1.length() == 4) {
letter1 = symbol1.substring(0, 1);
}
if (symbol2.length() == 5) {
letter2 = symbol2.substring(0, 2);
} else if (symbol2.length() == 4) {
letter2 = symbol2.substring(0, 1);
}
int possitive1 = (int) Math.sqrt(charge1 * charge1);
int possitive2 = (int) Math.sqrt(charge2 * charge2);
As we can see: positive depends on charge which depends on symbol which depends on compound. And totaly unrelated to it: letter depends on symbol which depends on compound.
let's split it to separate methods:
int getPositive(String compound) { // I have no idea what "positive", "symbol" and compound represent, consider better names please
String symbol = map.get(compound);
int charge = Integer.parseInt(symbol.replace("+", "").substring(
symbol.length() - 2));
return (int) Math.sqrt(charge2 * charge2);
}
And now we can apply the same to getLetter(String compound) {...} etc.
I would throw some of that parsing into its own class, maybe you can even offload more into there when you build out more functionallity.
public class Symbol {
final int charge;
final String letter;
public Symbol(String str) {
int sepIndex = str.indexOf('^');
if(sepIndex != -1) {
letter = str.substring(0, sepIndex);
charge = Integer.parseInt(str.substring(sepIndex+1).replace("+", ""));
} else {
throw new IllegalArgumentException(str + " isnt a valid Symbol, no ^ found");
}
}
}
public class Formula {
String compound1;
String compound2;
static private Map<String, String> map = new HashMap<String, String>();
// make this a static block so its only called once.
static {
// 1+
map.put("Hydrogen", "H^1+");
map.put("Lithium", "Li^1+");
map.put("Sodium", "Na^1+");
map.put("Potassium", "K^1+");
map.put("Rubidium", "Rb^1+");
// 2+
map.put("Magnesium", "Mg^2+");
map.put("Calcium", "Ca^2+");
map.put("Strontium", "Sr^2+");
// 3+
map.put("Aluminium", "Al^3+");
// 3-
map.put("Nitrogem", "N^-3");
map.put("Phosphorus", "P^-3");
// 2-
map.put("Oxygen", "O^-2");
map.put("Sulfur", "S^-2");
map.put("Selenium", "Se^-2");
// 1-
map.put("Fluorine", "F^-1");
map.put("Chlorine", "Cl^-1");
map.put("Bromine", "Br^-1");
map.put("Iodine", "I^-1");
}
String createFormula() {
// Example1: Input = Calcium Iodine:
// 2x + -1y = 0
// x = 1 and y = 2
// Output = CaI2
//
// Example2: Input = Sulfur Iodine
// Output = Molecule: Sulfur Iodine = SI2
Symbol symbol1 = new Symbol(map.get(compound1));
Symbol symbol2 = new Symbol(map.get(compound2));
int possitive1 = Math.abs(symbol1.charge); // sqrt(a*a) == abs(a)
int possitive2 = Math.abs(symbol1.charge);
if ((possitive1 == 1) & (possitive2 == 1)) {
return symbol1.letter + symbol1.letter;
} else if (possitive1 == 1) {
return symbol1.letter + possitive2 + symbol2.letter;
} else if (possitive2 == 1) {
return symbol1.letter + symbol2.letter + possitive1;
}
// dead code, if positive1 is 0 then setting it to -0 does nothing
/*if (possitive1 == 0) {
possitive1 = -(symbol1.charge);
}
if (possitive2 == 0) {
possitive2 = -(symbol2.charge);
}*/
return symbol1.letter + possitive2 + symbol2.letter + possitive1;
}
}
First method:
getCharge(String symbol){
return Integer.parseInt(symbol.replace("+", "").substring(symbol.length() - 2));
}
Second Method:
getLetter(String symbol){
if (symbol.length() == 5) {
return symbol.substring(0, 2);
} else if (symbol.length() == 4) {
return symbol.substring(0, 1);
}
}
I believe this is equivalent to your posted code -
private static String checkCompound(String symbol) {
if (symbol.length() == 5) {
return symbol.substring(0, 2);
} else if (symbol.length() == 4) {
return symbol.substring(0, 1);
}
return "";
}
Then
String letter1 = checkCompound(symbol1);
String letter2 = checkCompound(symbol2);
if (charge1 > 0) {
if (charge2 > 0) {
return letter1 + letter2;
}
return letter1 + charge2 + letter2;
} else if (charge2 > 0) {
return letter1 + letter2 + charge1;
}
return letter1 + charge2 + letter2 + charge1;
Finally, this
if (possitive1 == 0) {
possitive1 = -(charge1);
}
was removed because it's -0 which is 0.
Well, first let's rearrange your code so that all of the ...1 variables are together. Judging by your naming convention, you use letter1 to calculate symbol1, symbol1 to calculate charge1, etc... I'm just going to focus on createFormula() since that's the part you want to reduce in size.
String createFormula() {
initiateIons();
//Calculate symbol1, charge1, letter1, possitive1
String symbol1 = map.get(compound1);
int charge1 = Integer.parseInt(symbol1.replace("+", "").substring(
symbol1.length() - 2));
String letter1 = null;
if (symbol1.length() == 5) {
letter1 = symbol1.substring(0, 2);
} else if (symbol1.length() == 4) {
letter1 = symbol1.substring(0, 1);
}
int possitive1 = (int) Math.sqrt(charge1 * charge1);
//calculate symbol2, charge2, letter2, possitive2
String symbol2 = map.get(compound2);
int charge2 = Integer.parseInt(symbol2.replace("+", "").substring(
symbol2.length() - 2));
String letter2 = null;
if (symbol2.length() == 5) {
letter2 = symbol2.substring(0, 2);
} else if (symbol2.length() == 4) {
letter2 = symbol2.substring(0, 1);
}
int possitive2 = (int) Math.sqrt(charge2 * charge2);
//Returns
if ((possitive1 == 1) & (possitive2 == 1)) {
return letter1 + letter2;
} else if (possitive1 == 1) {
return letter1 + possitive2 + letter2;
} else if (possitive2 == 1) {
return letter1 + letter2 + possitive1;
}
if (possitive1 == 0) {
possitive1 = -(charge1);
}
if (possitive2 == 0) {
possitive2 = -(charge2);
}
return letter1 + possitive2 + letter2 + possitive1;
}
I agree, the calculation step seems redundant. It would be amazing to have a function that calculates those for and returns a tuple of them, but (as far as I know) Java doesn't yet have tuples. We can do a string array though, and parse the ints back out of the strings. Here's a less redundant revision of your code.
String[] calculatePieces(String compound){
String symbol = map.get(compound);
int charge = Integer.parseInt(symbol.replace("+", "").substring(
symbol.length() - 2));
String letter = null;
if (symbol.length() == 5) {
letter = symbol1.substring(0, 2);
} else if (symbol1.length() == 4) {
letter = symbol1.substring(0, 1);
}
int possitive = (int) Math.sqrt(charge * charge);
pieces = new String[4];
pieces[0] = symbol;
pieces[1] = charge + "";
pieces[2] = letter;
pieces[3] = possitive + "";
return pieces;
}
String createFormula() {
initiateIons();
String[] pieces1 = calculatePieces(compound1);
int charge1 = Integer.parseInt(pieces1[1]);
int possitive1 = Integer.parseInt(pieces1[3]);
String[] pieces2 = calculatePieces(compound2);
int charge2 = Integer.parseInt(pieces2[1]);
int possitive2 = Integer.parseInt(pieces2[3]);
//Returns
if ((possitive1 == 1) & (possitive2 == 1)) {
return pieces1[2] + pieces2[2];
} else if (possitive1 == 1) {
return pieces1[2] + possitive2 + pieces2[2];
} else if (possitive2 == 1) {
return pieces1[2] + pieces2[2] + possitive1;
}
if (possitive1 == 0) {
possitive1 = -(charge1);
}
if (possitive2 == 0) {
possitive2 = -(charge2);
}
return pieces1[2] + possitive2 + pieces2[2] + possitive1;
}
It's a bit better, but Java's restriction on returning a single object limits how clean this can get. The way to make it even cleaner is to make a wrapper object that basically acts as a tuple of (string, int, string, int), but if you need this to be fast that's not the way to go.
OK, I don't know how to word this question, but maybe my code will spell out the problem:
public class ControllerTest
{
public static void main(String [] args)
{
GamePadController rockbandDrum = new GamePadController();
DrumMachine drum = new DrumMachine();
while(true)
{
try{
rockbandDrum.poll();
if(rockbandDrum.isButtonPressed(1)) //BLUE PAD HhiHat)
{
drum.playSound("hiHat.wav");
Thread.sleep(50);
}
if(rockbandDrum.isButtonPressed(2)) //GREEN PAD (Crash)
{
//Todo: Change to Crash
drum.playSound("hiHat.wav");
Thread.sleep(50);
}
//Etc....
}
}
}
public class DrumMachine
{
InputStream soundPlayer = null;
AudioStream audio = null;
static boolean running = true;
public void playSound(String soundFile)
{
//Tak a sound file as a paramater and then
//play that sound file
try{
soundPlayer = new FileInputStream(soundFile);
audio = new AudioStream(soundPlayer);
}
catch(FileNotFoundException e){
e.printStackTrace();
}
catch(IOException e){
e.printStackTrace();
}
AudioPlayer.player.start(audio);
}
//Etc... Methods for multiple audio clip playing
}
Now the problem is, if I lower the delay in the
Thread.sleep(50)
then the sound plays multiple times a second, but if I keep at this level or any higher, I could miss sounds being played...
It's an odd problem, where if the delay is too low, the sound loops. But if it's too high it misses playing sounds. Is this just a problem where I would need to tweak the settings, or is there any other way to poll the controller without looping sound?
Edit: If I need to post the code for polling the controller I will...
import java.io.*;
import net.java.games.input.*;
import net.java.games.input.Component.POV;
public class GamePadController
{
public static final int NUM_BUTTONS = 13;
// public stick and hat compass positions
public static final int NUM_COMPASS_DIRS = 9;
public static final int NW = 0;
public static final int NORTH = 1;
public static final int NE = 2;
public static final int WEST = 3;
public static final int NONE = 4; // default value
public static final int EAST = 5;
public static final int SW = 6;
public static final int SOUTH = 7;
public static final int SE = 8;
private Controller controller;
private Component[] comps; // holds the components
// comps[] indices for specific components
private int xAxisIdx, yAxisIdx, zAxisIdx, rzAxisIdx;
// indices for the analog sticks axes
private int povIdx; // index for the POV hat
private int buttonsIdx[]; // indices for the buttons
private Rumbler[] rumblers;
private int rumblerIdx; // index for the rumbler being used
private boolean rumblerOn = false; // whether rumbler is on or off
public GamePadController()
{
// get the controllers
ControllerEnvironment ce =
ControllerEnvironment.getDefaultEnvironment();
Controller[] cs = ce.getControllers();
if (cs.length == 0) {
System.out.println("No controllers found");
System.exit(0);
}
else
System.out.println("Num. controllers: " + cs.length);
// get the game pad controller
controller = findGamePad(cs);
System.out.println("Game controller: " +
controller.getName() + ", " +
controller.getType());
// collect indices for the required game pad components
findCompIndices(controller);
findRumblers(controller);
} // end of GamePadController()
private Controller findGamePad(Controller[] cs)
/* Search the array of controllers until a suitable game pad
controller is found (eith of type GAMEPAD or STICK).
*/
{
Controller.Type type;
int i = 0;
while(i < cs.length) {
type = cs[i].getType();
if ((type == Controller.Type.GAMEPAD) ||
(type == Controller.Type.STICK))
break;
i++;
}
if (i == cs.length) {
System.out.println("No game pad found");
System.exit(0);
}
else
System.out.println("Game pad index: " + i);
return cs[i];
} // end of findGamePad()
private void findCompIndices(Controller controller)
/* Store the indices for the analog sticks axes
(x,y) and (z,rz), POV hat, and
button components of the controller.
*/
{
comps = controller.getComponents();
if (comps.length == 0) {
System.out.println("No Components found");
System.exit(0);
}
else
System.out.println("Num. Components: " + comps.length);
// get the indices for the axes of the analog sticks: (x,y) and (z,rz)
xAxisIdx = findCompIndex(comps, Component.Identifier.Axis.X, "x-axis");
yAxisIdx = findCompIndex(comps, Component.Identifier.Axis.Y, "y-axis");
zAxisIdx = findCompIndex(comps, Component.Identifier.Axis.Z, "z-axis");
rzAxisIdx = findCompIndex(comps, Component.Identifier.Axis.RZ, "rz-axis");
// get POV hat index
povIdx = findCompIndex(comps, Component.Identifier.Axis.POV, "POV hat");
findButtons(comps);
} // end of findCompIndices()
private int findCompIndex(Component[] comps,
Component.Identifier id, String nm)
/* Search through comps[] for id, returning the corresponding
array index, or -1 */
{
Component c;
for(int i=0; i < comps.length; i++) {
c = comps[i];
if ((c.getIdentifier() == id) && !c.isRelative()) {
System.out.println("Found " + c.getName() + "; index: " + i);
return i;
}
}
System.out.println("No " + nm + " component found");
return -1;
} // end of findCompIndex()
private void findButtons(Component[] comps)
/* Search through comps[] for NUM_BUTTONS buttons, storing
their indices in buttonsIdx[]. Ignore excessive buttons.
If there aren't enough buttons, then fill the empty spots in
buttonsIdx[] with -1's. */
{
buttonsIdx = new int[NUM_BUTTONS];
int numButtons = 0;
Component c;
for(int i=0; i < comps.length; i++) {
c = comps[i];
if (isButton(c)) { // deal with a button
if (numButtons == NUM_BUTTONS) // already enough buttons
System.out.println("Found an extra button; index: " + i + ". Ignoring it");
else {
buttonsIdx[numButtons] = i; // store button index
System.out.println("Found " + c.getName() + "; index: " + i);
numButtons++;
}
}
}
// fill empty spots in buttonsIdx[] with -1's
if (numButtons < NUM_BUTTONS) {
System.out.println("Too few buttons (" + numButtons +
"); expecting " + NUM_BUTTONS);
while (numButtons < NUM_BUTTONS) {
buttonsIdx[numButtons] = -1;
numButtons++;
}
}
} // end of findButtons()
private boolean isButton(Component c)
/* Return true if the component is a digital/absolute button, and
its identifier name ends with "Button" (i.e. the
identifier class is Component.Identifier.Button).
*/
{
if (!c.isAnalog() && !c.isRelative()) { // digital and absolute
String className = c.getIdentifier().getClass().getName();
// System.out.println(c.getName() + " identifier: " + className);
if (className.endsWith("Button"))
return true;
}
return false;
} // end of isButton()
private void findRumblers(Controller controller)
/* Find the rumblers. Use the last rumbler for making vibrations,
an arbitrary decision. */
{
// get the game pad's rumblers
rumblers = controller.getRumblers();
if (rumblers.length == 0) {
System.out.println("No Rumblers found");
rumblerIdx = -1;
}
else {
System.out.println("Rumblers found: " + rumblers.length);
rumblerIdx = rumblers.length-1; // use last rumbler
}
} // end of findRumblers()
// ----------------- polling and getting data ------------------
public void poll()
// update the component values in the controller
{
controller.poll();
}
public int getXYStickDir()
// return the (x,y) analog stick compass direction
{
if ((xAxisIdx == -1) || (yAxisIdx == -1)) {
System.out.println("(x,y) axis data unavailable");
return NONE;
}
else
return getCompassDir(xAxisIdx, yAxisIdx);
} // end of getXYStickDir()
public int getZRZStickDir()
// return the (z,rz) analog stick compass direction
{
if ((zAxisIdx == -1) || (rzAxisIdx == -1)) {
System.out.println("(z,rz) axis data unavailable");
return NONE;
}
else
return getCompassDir(zAxisIdx, rzAxisIdx);
} // end of getXYStickDir()
private int getCompassDir(int xA, int yA)
// Return the axes as a single compass value
{
float xCoord = comps[ xA ].getPollData();
float yCoord = comps[ yA ].getPollData();
// System.out.println("(x,y): (" + xCoord + "," + yCoord + ")");
int xc = Math.round(xCoord);
int yc = Math.round(yCoord);
// System.out.println("Rounded (x,y): (" + xc + "," + yc + ")");
if ((yc == -1) && (xc == -1)) // (y,x)
return NW;
else if ((yc == -1) && (xc == 0))
return NORTH;
else if ((yc == -1) && (xc == 1))
return NE;
else if ((yc == 0) && (xc == -1))
return WEST;
else if ((yc == 0) && (xc == 0))
return NONE;
else if ((yc == 0) && (xc == 1))
return EAST;
else if ((yc == 1) && (xc == -1))
return SW;
else if ((yc == 1) && (xc == 0))
return SOUTH;
else if ((yc == 1) && (xc == 1))
return SE;
else {
System.out.println("Unknown (x,y): (" + xc + "," + yc + ")");
return NONE;
}
} // end of getCompassDir()
public int getHatDir()
// Return the POV hat's direction as a compass direction
{
if (povIdx == -1) {
System.out.println("POV hat data unavailable");
return NONE;
}
else {
float povDir = comps[povIdx].getPollData();
if (povDir == POV.CENTER) // 0.0f
return NONE;
else if (povDir == POV.DOWN) // 0.75f
return SOUTH;
else if (povDir == POV.DOWN_LEFT) // 0.875f
return SW;
else if (povDir == POV.DOWN_RIGHT) // 0.625f
return SE;
else if (povDir == POV.LEFT) // 1.0f
return WEST;
else if (povDir == POV.RIGHT) // 0.5f
return EAST;
else if (povDir == POV.UP) // 0.25f
return NORTH;
else if (povDir == POV.UP_LEFT) // 0.125f
return NW;
else if (povDir == POV.UP_RIGHT) // 0.375f
return NE;
else { // assume center
System.out.println("POV hat value out of range: " + povDir);
return NONE;
}
}
} // end of getHatDir()
public boolean[] getButtons()
/* Return all the buttons in a single array. Each button value is
a boolean. */
{
boolean[] buttons = new boolean[NUM_BUTTONS];
float value;
for(int i=0; i < NUM_BUTTONS; i++) {
value = comps[ buttonsIdx[i] ].getPollData();
buttons[i] = ((value == 0.0f) ? false : true);
}
return buttons;
} // end of getButtons()
public boolean isButtonPressed(int pos)
/* Return the button value (a boolean) for button number 'pos'.
pos is in the range 1-NUM_BUTTONS to match the game pad
button labels.
*/
{
if ((pos < 1) || (pos > NUM_BUTTONS)) {
System.out.println("Button position out of range (1-" +
NUM_BUTTONS + "): " + pos);
return false;
}
if (buttonsIdx[pos-1] == -1) // no button found at that pos
return false;
float value = comps[ buttonsIdx[pos-1] ].getPollData();
// array range is 0-NUM_BUTTONS-1
return ((value == 0.0f) ? false : true);
} // end of isButtonPressed()
// ------------------- Trigger a rumbler -------------------
public void setRumbler(boolean switchOn)
// turn the rumbler on or off
{
if (rumblerIdx != -1) {
if (switchOn)
rumblers[rumblerIdx].rumble(0.8f); // almost full on for last rumbler
else // switch off
rumblers[rumblerIdx].rumble(0.0f);
rumblerOn = switchOn; // record rumbler's new status
}
} // end of setRumbler()
public boolean isRumblerOn()
{ return rumblerOn; }
} // end of GamePadController class
I think you are using the wrong design pattern here. You should use the observer pattern for this type of thing.
A polling loop not very efficient, and as you've noticed doesn't really yield the desired results.
I'm not sure what you are using inside your objects to detect if a key is pressed, but if it's a GUI architecture such as Swing or AWT it will be based on the observer pattern via the use of EventListeners, etc.
Here is a (slightly simplified) Observer-pattern
applied to your situation.
The advantage of this design is that when a button
is pressed and hold, method 'buttonChanged' will
still only be called once, instead of start
'repeating' every 50 ms.
public static final int BUTTON_01 = 0x00000001;
public static final int BUTTON_02 = 0x00000002;
public static final int BUTTON_03 = 0x00000004;
public static final int BUTTON_04 = 0x00000008; // hex 8 == dec 8
public static final int BUTTON_05 = 0x00000010; // hex 10 == dec 16
public static final int BUTTON_06 = 0x00000020; // hex 20 == dec 32
public static final int BUTTON_07 = 0x00000040; // hex 40 == dec 64
public static final int BUTTON_08 = 0x00000080; // etc.
public static final int BUTTON_09 = 0x00000100;
public static final int BUTTON_10 = 0x00000200;
public static final int BUTTON_11 = 0x00000400;
public static final int BUTTON_12 = 0x00000800;
private int previousButtons = 0;
void poll()
{
rockbandDrum.poll();
handleButtons();
}
private void handleButtons()
{
boolean[] buttons = getButtons();
int pressedButtons = getPressedButtons(buttons);
if (pressedButtons != previousButtons)
{
buttonChanged(pressedButtons); // Notify 'listener'.
previousButtons = pressedButtons;
}
}
public boolean[] getButtons()
{
// Return all the buttons in a single array. Each button-value is a boolean.
boolean[] buttons = new boolean[MAX_NUMBER_OF_BUTTONS];
float value;
for (int i = 0; i < MAX_NUMBER_OF_BUTTONS-1; i++)
{
int index = buttonsIndex[i];
if (index < 0) { continue; }
value = comps[index].getPollData();
buttons[i] = ((value == 0.0f) ? false : true);
}
return buttons;
}
private int getPressedButtons(boolean[] array)
{
// Mold all pressed buttons into a single number by OR-ing their values.
int pressedButtons = 0;
int i = 1;
for (boolean isBbuttonPressed : array)
{
if (isBbuttonPressed) { pressedButtons |= getOrValue(i); }
i++;
}
return pressedButtons;
}
private int getOrValue(int btnNumber) // Get a value to 'OR' with.
{
int btnValue = 0;
switch (btnNumber)
{
case 1 : btnValue = BUTTON_01; break;
case 2 : btnValue = BUTTON_02; break;
case 3 : btnValue = BUTTON_03; break;
case 4 : btnValue = BUTTON_04; break;
case 5 : btnValue = BUTTON_05; break;
case 6 : btnValue = BUTTON_06; break;
case 7 : btnValue = BUTTON_07; break;
case 8 : btnValue = BUTTON_08; break;
case 9 : btnValue = BUTTON_09; break;
case 10 : btnValue = BUTTON_10; break;
case 11 : btnValue = BUTTON_11; break;
case 12 : btnValue = BUTTON_12; break;
default : assert false : "Invalid button-number";
}
return btnValue;
}
public static boolean checkButton(int pressedButtons, int buttonToCheckFor)
{
return (pressedButtons & buttonToCheckFor) == buttonToCheckFor;
}
public void buttonChanged(int buttons)
{
if (checkButton(buttons, BUTTON_01)
{
drum.playSound("hiHat.wav");
}
if (checkButton(buttons, BUTTON_02)
{
drum.playSound("crash.wav");
}
}
Please post more information about the GamePadController class that you are using.
More than likely, that same library will offer an "event" API, where a "callback" that you register with a game pad object will be called as soon as the user presses a button. With this kind of setup, the "polling" loop is in the framework, not your application, and it can be much more efficient, because it uses signals from the hardware rather than a busy-wait polling loop.
Okay, I looked at the JInput API, and it is not really event-driven; you have to poll it as you are doing. Does the sound stop looping when you release the button? If so, is your goal to have the sound play just once, and not again until the button is release and pressed again? In that case, you'll need to track the previous button state each time through the loop.
Human response time is about 250 ms (for an old guy like me, anyway). If you are polling every 50 ms, I'd expect the controller to report the button depressed for several iterations of the loop. Can you try something like this:
boolean played = false;
while (true) {
String sound = null;
if (controller.isButtonPressed(1))
sound = "hiHat.wav";
if (controller.isButtonPressed(2))
sound = "crash.wav";
if (sound != null) {
if (!played) {
drum.playSound(sound);
played = true;
}
} else {
played = false;
}
Thread.sleep(50);
}