#Suggestions on making a traversal reusable in a game (Java)

7 messages · Page 1 of 1 (latest)

verbal shoal
#

Hi guys, im making a small game in java where i have these sorts of overlays (e.g. a unit is clicked and it shows an overlay of reachable tiles based on the units movement points, or a city is clicked and it shows the supply range). I have attached an image of what i mean by "overlay" (where the reachable tiles are shaded light gray and the unreachable ones are dark gray).

I have an issue where i have these different types of overlays to compute, and the traversal logic is very similar, but there have to be little changes inside the traversal based on what overlay i'm computing.

Currently, the traversal computes a Hashmap of reachable tiles and their costs, as well as a Hashmap called parent which allows rebuilding the shortest path to a reachable tile. As far as im aware, all the overlays should have at least these two.

However, the traversal for unit movement does something custom where it excludes hexes from the reachable hashmap if they have an enemy unit on them, and instead add them to an attackable hashmap. The issue is that this happens right in the middle of the traversal, and it is also not something that is needed when calculating a traversal for, e.g., supply range.

My question is, how would it be best to extract the core traversal to be reusable and not need to rewrite it each time, while also accounting for the quirks that the different types of overlays require?

swift craterBOT
#

-# Java @devout rain

verbal shoal
#

As an example, here is the current setup:

    public static MovementOverlay compute(Axial startCoordinate, int movementPointsLeft, HashMap<Axial, Hex> hexMap, BattleField battleField, Faction playerFaction) {

        HashMap<Axial, Integer> cost = new HashMap<>();
        HashMap<Axial, Axial> parent = new HashMap<>();
        HashMap<Axial, Integer> attackable = new HashMap<>();

        PriorityQueue<Node> priorityQueue = new PriorityQueue<>(Comparator.comparingInt(node -> node.cost));

        cost.put(startCoordinate, 0);
        priorityQueue.add(new Node(startCoordinate, 0));

        while (!priorityQueue.isEmpty()) {

            Node currentNode = priorityQueue.poll();
            if (currentNode.cost() != cost.get(currentNode.pos()))
                continue;

            for (int[] direction : hexNeighborDirections) {

                Axial neighborDirection = new Axial(currentNode.pos().q() + direction[0],
                        currentNode.pos().r() + direction[1]);

                if (!hexMap.containsKey(neighborDirection))
                    continue;
                if (battleField.getOccupiedHexes().containsKey(neighborDirection)) {

                    //In this if statement there is the custom logic for movement, all else is the same

                    if (battleField.unitAt(neighborDirection).getFaction() == playerFaction) {
                        continue;
                    } else {

                        int attackCost = currentNode.cost + hexMap.get(neighborDirection).getMoveCost() + 1;
                        if (attackCost <= movementPointsLeft) {
                            attackable.merge(neighborDirection, attackCost, Math::min);
                        }
                        continue;
                    }

                }

                int neighborToEnterCost = hexMap.get(neighborDirection).getMoveCost();
                int newCost = currentNode.cost() + neighborToEnterCost;
                if (newCost > movementPointsLeft)
                    continue;

                Integer bestPath = cost.get(neighborDirection);
                if (bestPath == null || newCost < bestPath) {

                    cost.put(neighborDirection, newCost);
                    parent.put(neighborDirection, currentNode.pos());
                    priorityQueue.add(new Node(neighborDirection, newCost));

                }
            }
        }
              
        return new MovementOverlay(startCoordinate, movementPointsLeft, cost, attackable, parent);
    }

}

I have some ideas on how to approach this, but all of them feel a bit wasteful.

The first option i see is having the traversal method take some kind of method that runs in place of where the add attackers stuff is.
The second option i guess would be to have one "overall" method, and then override it for movement, but then i think i'd have to rewrite the rest of the traversal
The third option would be to have some sort of enum like MOVEMENT or SUPPLY where it does a switch statement and computes what it should
The fourth option would be similar to the second, but to split up the traversal into 3 sections or so, where first section is setup, second section is anything custom like "addAttackable", and the third is the enqueue?

crisp prawn
#

Don’t have too much time to look at the code, can look later today if you want. But my recommendation would be potentially to write an interface for any specific logic, and keep your higher level class a common one. Make implementations of the interface depending on your usecases and construct the class with the implementations you require housing any specific logic

#

I’d have to look into your code more to provide better advice. I can later:)

verbal shoal
#

or the implementation would act as a callback*