The following code does work how I need it to, but it's ugly, excessive or a number of other things. I've looked at formulas and attempted to write a few solutions, but I end up with a similar amount of statements.
Is there a type of math formula that would benefit me in this instance or are 16 if statements acceptable?
To explain the code, it's for a kind of simultaneous-turn-based game.. two players have four action buttons each and the results come from an array (0-3), but the variables 'one' & 'two' can be assigned anything if this helps. The result is, 0 = neither win, 1 = p1 wins, 2 = p2 wins, 3 = both win.
public int fightMath(int one, int two) {
if(one == 0 && two == 0) { result = 0; }
else if(one == 0 && two == 1) { result = 0; }
else if(one == 0 && two == 2) { result = 1; }
else if(one == 0 && two == 3) { result = 2; }
else if(one == 1 && two == 0) { result = 0; }
else if(one == 1 && two == 1) { result = 0; }
else if(one == 1 && two == 2) { result = 2; }
else if(one == 1 && two == 3) { result = 1; }
else if(one == 2 && two == 0) { result = 2; }
else if(one == 2 && two == 1) { result = 1; }
else if(one == 2 && two == 2) { result = 3; }
else if(one == 2 && two == 3) { result = 3; }
else if(one == 3 && two == 0) { result = 1; }
else if(one == 3 && two == 1) { result = 2; }
else if(one == 3 && two == 2) { result = 3; }
else if(one == 3 && two == 3) { result = 3; }
return result;
}
Other people have already suggested my initial idea, the matrix method, but in addition to consolidating the if statements you can avoid some of what you have by making sure the arguments supplied are in the expected range and by using in-place returns (some coding standards I've seen enforce one-point-of-exit for functions, but I've found that multiple returns are very useful for avoiding arrow coding and with the prevalence of exceptions in Java there's not much point in strictly enforcing such a rule anyway as any uncaught exception thrown inside the method is a possible point of exit anyway). Nesting switch statements is a possibility, but for the small range of values you're checking here I find if statements to be more compact and not likely to result in much of a performance difference, especially if your program is turn-based rather than real-time.
This does end up being less readable than it might otherwise be due to the irregularity of parts of the input->result mapping. I favor the matrix style instead due to its simplicity and how you can set up the matrix to make sense visually (though that is in part influenced by my memories of Karnaugh maps):
Update: Given your mention of blocking/hitting, here's a more radical change to the function that utilizes propertied/attribute-holding enumerated types for inputs and the result and also modifies the result a little to account for blocking, which should result in a more readable function.
You don't even have to change the function itself if you want to add blocks/attacks of more heights, just the enums; adding additional types of moves will probably require modification of the function, though. Also,
EnumSet
s might be more extensible than using extra enums as properties of the main enum, e.g.EnumSet<Move> attacks = EnumSet.of(Move.ATTACK_HIGH, Move.ATTACK_LOW, ...);
and thenattacks.contains(move)
rather thanmove.type == MoveType.ATTACK
, though usingEnumSet
s will probably be slightly slower than direct equals checks.For the case where a successful block results in a counter, you can replace
if (one.height == two.height) return LandedHit.NEITHER;
withAlso, replacing some of the
if
statements with usage of the ternary operator (boolean_expression ? result_if_true : result_if_false
) could make the code more compact (for example, the code in the preceding block would becomereturn one.isAttack() ? LandedHit.PLAYER_TWO : LandedHit.PLAYER_ONE;
), but that can lead to harder-to-read oneliners so I wouldn't recommend it for more complex branching.Since your data set is so small, you can compress everything into 1 long integer and turn it into a formula
More bitwise variant:
This makes use of the fact everything is a multiple of 2
The Origin of the Magic Constant
What can I say? The world needs magic, sometimes the possibility of something calls for its creation.
The essence of the function that solves OP's problem is a map from 2 numbers (one,two), domain {0,1,2,3} to the range {0,1,2,3}. Each of the answers has approached how to implement that map.
Also, you can see in a number of the answers a restatement of the problem as a map of 1 2-digit base 4 number N(one,two) where one is digit 1, two is digit 2, and N = 4*one + two; N = {0,1,2,...,15} -- sixteen different values, that's important. The output of the function is one 1-digit base 4 number {0,1,2,3} -- 4 different values, also important.
Now, a 1-digit base 4 number can be expressed as a 2-digit base 2 number; {0,1,2,3} = {00,01,10,11}, and so each output can be encoded with only 2 bits. From above, there are only 16 different outputs possible, so 16*2 = 32 bits is all that is necessary to encode the entire map; this can all fit into 1 integer.
The constant M is an encoding of the map m where m(0) is encoded in bits M[0:1], m(1) is encoded in bits M[2:3], and m(n) is encoded in bits M[n*2:n*2+1].
All that remains is indexing and returning the right part of the constant, in this case you can shift M right 2*N times and take the 2 least significant bits, that is (M >> 2*N) & 0x3. The expressions (one << 3) and (two << 1) are just multiplying things out while noting that 2*x = x << 1 and 8*x = x << 3.
Let's see what we know
1: your answers are symmetrical for P1 (player one) and P2 (player two). This makes sense for a fighting game but is also something you can take advantage of to improve your logic.
2: 3 beats 0 beats 2 beats 1 beats 3. The only cases not covered by these cases are combinations of 0 vs 1 and 2 vs 3. To put it another way the unique victory table looks like this: 0 beats 2, 1 beats 3, 2 beats 1, 3 beats 0.
3: If 0/1 go up against each other then there's a hitless draw but if 2/3 go up against each then both hit
First, let us build a one-way function telling us if we won:
We can then use this function to compose the final result:
While this is arguably more complex and probably slower than the table lookup offered in many answers I believe it is a superior method because it actually encapsulates the logic of your code and describes it to anyone who's reading your code. I think this makes it a better implementation.
(It's been a while since I did any Java so apologies if the syntax is off, hopefully it is still intelligible if I've got it slightly wrong)
By the way, 0-3 clearly mean something; they're not arbitrary values so it would help to name them.
A good point would be to define the rules as text, you can easier derive the correct formula then. This is extracted from laalto's nice array representation:
And here we go with some general comments, but you should describe them in rule terms:
You could of course crunch this down to less code, but it is generally a good idea to understand what you code rather than finding a compact solution.
Some explanation on the complicated p1/p2 hits would be great, looks interesting!
Since you prefer nested
if
conditionals , here's another way.Note that it doesn't use the
result
member and it doesn't change any state.