Skip to content

Instantly share code, notes, and snippets.

@sdebaun
Last active August 18, 2020 20:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sdebaun/638a47cff32beeb70e6393c4d00156aa to your computer and use it in GitHub Desktop.
Save sdebaun/638a47cff32beeb70e6393c4d00156aa to your computer and use it in GitHub Desktop.

Patterns

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.

Thinking in List and List Operations

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();

Insertion Special Case

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 ^^^

Shorten the Mapping?

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());

Next Opportunities: Similarities between from reserve and onboard moves

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

What About Vanguard?

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

And beyond

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...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment