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.

public int fightMath(int one, int two) { if (one > 3 || one < 0 || two > 3 || two < 0) { throw new IllegalArgumentException("Result is undefined for arguments outside the range [0, 3]"); } if (one <= 1) { if (two <= 1) return 0; if (two - one == 2) return 1; return 2; // two can only be 3 here, no need for an explicit conditional } // one >= 2 if (two >= 2) return 3; if (two == 1) return 1; return 2; // two can only be 0 here }

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):

int[][] results = {{0, 0, 1, 2}, {0, 0, 2, 1}, {2, 1, 3, 3}, {2, 1, 3, 3}};

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.

enum MoveType { ATTACK, BLOCK; } enum MoveHeight { HIGH, LOW; } enum Move { // Enum members can have properties/attributes/data members of their own ATTACK_HIGH(MoveType.ATTACK, MoveHeight.HIGH), ATTACK_LOW(MoveType.ATTACK, MoveHeight.LOW), BLOCK_HIGH(MoveType.BLOCK, MoveHeight.HIGH), BLOCK_LOW(MoveType.BLOCK, MoveHeight.LOW); public final MoveType type; public final MoveHeight height; private Move(MoveType type, MoveHeight height) { this.type = type; this.height = height; } /** Makes the attack checks later on simpler. */ public boolean isAttack() { return this.type == MoveType.ATTACK; } } enum LandedHit { NEITHER, PLAYER_ONE, PLAYER_TWO, BOTH; } LandedHit fightMath(Move one, Move two) { // One is an attack, the other is a block if (one.type != two.type) { // attack at some height gets blocked by block at same height if (one.height == two.height) return LandedHit.NEITHER; // Either player 1 attacked or player 2 attacked; whoever did // lands a hit if (one.isAttack()) return LandedHit.PLAYER_ONE; return LandedHit.PLAYER_TWO; } // both attack if (one.isAttack()) return LandedHit.BOTH; // both block return LandedHit.NEITHER; }

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 then attacks.contains(move) rather than move.type == MoveType.ATTACK , though using EnumSet 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; with

if (one.height == two.height) { // Successful block results in a counter against the attacker if (one.isAttack()) return LandedHit.PLAYER_TWO; return LandedHit.PLAYER_ONE; }