So because of the refactoring that you started, it's starting to expose patterns in both those functions and related functions.
I am an asshole and using List<Hex>
as a stand in for HashSet<HexTile>
because my fingers are lazy
One thing i noticed: A lot of the stuff in this reserves behavior looks like:
(List<Hex> starts) => List<Hex> dests
Because reserves can 'start' from a list of virtual, "offmap" hexes (same concept for reserve with insertion)
But theyre mostly doing the same sim logic as an onboard move that looks like:
(Hex start) => List<Hex> dests
Theyre just doing it to a set of starts and uniq'ing the flattened results.
So for the simplest case, we can just grab the potential reserve hexes
List<Hex> reserveStarts = map.ReserveHexesFor(unit.owningPlayerId);
And the general pattern to get destinations out of that:
List<Hex> walkDests = reserveStarts.Map(MyWalkDestFinder).Flatten().Uniq();
// or whatever the accursed linq syntax is which is not this
Where MyWalkDestFinder
is aFunc<Hex, List<Hex>>
.
But wait, only input is a Hex? WHAT YOU SAY?
Because it's a (manually) curried function:
private Func<Hex, List> WalkDestFinder(Unit unit) =>
(Hex hex) => // using existing method rn, also this will not work for AH units
// these parameters might not be exactly right but should be same sig
GetHexesInRange(unit.owningPlayerId, hex, 1, (unit.jumpMoves > 0) ? 1 : unit.walkMoves, unit.allTerrain, false, false)
So we end up with:
List<Hex> walkDests = reserveStarts.Map(WalkDestFinder(unit)).Flatten().Uniq();
List<Hex> runDests = reserveStarts.Map(RunDestFinder(unit)).Flatten().Uniq();
List<Hex> jumpDests = reserveStarts.Map(JumpDestFinder(unit)).Flatten().Uniq();
The same pattern, different starts: all hexes that contain a valid beaconing unit.
List<Hex> beaconStarts = map.BeaconHexesFor(turnData, unit.owningPlayerId);
And now a slightly different implementation for destinations:
List<Hex> insertionDests = beaconStarts.Map(InsertionDestFinder()).Flatten().Uniq();
// this is new:
private Func<Hex, List<Hex>> InsertionDestFinder() =>
(Hex hex) =>
GetNeighbors(hex).Filter(Hex.IsEmpty).Filter(Hex.IsOnBoard); // again with normal "Filter" instead of "Where"
// sneaking some sweet sweet fp into that ^^^
There's a lot of repetition in how we're mapping over the starts and narrowing the dests.
List<Hex> walkDests = reserveStarts.Map(WalkDestFinder(unit)).Flatten().Uniq();
List<Hex> insertionDests = beaconStarts.Map(InsertionDestFinder()).Flatten().Uniq();
That could be dry'd up into an extension method:
// in some extension class - or SelectManyUniq or whatever you prefer
public static List<T> FlatMapUniq<T>(this List<T> source, Func<T, List<T>> selector) =>
source.Map(selector).Flatten().Uniq();
// implementation
List<Hex> walkDests = reserveStarts.FlatMapUniq(WalkDestFinder(unit));
List<Hex> insertionDests = beaconStarts.FlatMapUniq(InsertionDestFinder());
Aren't those XXXDestFinders exactly the logic we would use in a regular, onboard move?
One approach could be, in the onboard move logic, would be:
List<Hex> walkDests = WalkDestFinder(unit)(unit.hex);
// it makes me cry that i have to have (x)(y) instead of (x,y)
And then start to think past that... what is really different between a move (not attack) that starts in reserves vs onboard? at least as far as finding potential destination hexes?
List<Hex> footStarts = unit.inReserve ?
map.ReserveHexesFor(unit.owningPlayerId) :
new List<Hex> { unit.hex };
List<Hex> beaconStarts = unit.inReserve && unit.hasInsertion ?
map.BeaconHexesFor(turnData, unit.owningPlayerId) :
new List<Hex>();
And now the same logic generates destinations for either reserve or onboard units
List<Hex> walkDests = footStarts.FlatMapUniq(WalkDestFinder(unit))
now that we're thinking in terms of List => List... lets apply that perspective to vanguard. walkDest is the same. We need to return a List. but our starts are irrelevant! walkDest is always going to be your two home rows - hexes with units.
When you first start asking what destination hexes to return, just check:
if (isVanguard) {
List<Hex> walkDest = map.HomeRowHexesFor(unit.owningPlayerId).Filter(Hex.IsEmpty);
List<Hex> runDest, jumpDest, insertionDest = new List<Hex>();
}
// otherwise the usual
Obviously this same approach can be used in selecting attack targets.
We can also dig deeper into GetHexesInRange and its siblings.
And then start to go up... SelectActionableHexes. What is it really doing? We are returning a set of 6 different semantically meaningful sets of hexes...