Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite tool_dig_to_pos2 (part 2) #2845

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

eddebaby
Copy link
Contributor

@eddebaby eddebaby commented Dec 17, 2023

Rewrite tool_dig_to_pos2 and its children.

Cleanly isolates tool_dig_to_pos2 (and its children) in to 3 main parts:

  • the maze-solving algorithm which plots a path through a 2d grid made up of passable tiles and impassable tiles (a.k.a. wall tiles or obstacle tiles)
  • the use of the terrain in KeeperFX to decide whether a tile is (or can be made) passable
  • the "dig to" CPU player tasks (which call the above algorithm, passing "dig flags" to influence how KeeperFX terrain is interpreted when testing if a tile is passable or not)

This PR resolves the chicken and egg situation of "needing requirements to start implementation" and "needing to know the intended design of the implementation to write the requirements" - I have reverse-engineered the design and implementation of DK/KFX mark for digging path finding by observing the behaviour in both DK and KFX (they are ~95% identical), and then confirming my assumptions in the KFX codebase.

So if the requirements for the "dig to" CPU player tasks are different to the requirements fulfilled in this PR, then this thorough explanation of "how it works in master at the current time" should allow said requirements to be written so that programmers may work sanely.

The "dig to" CPU player tasks

tool_dig_to_pos2 is called by simulate_dig_to, task_dig_room_passage, task_dig_to_entrance, task_dig_to_gold, task_dig_to_attack, and task_dig_to_neutral

tool_dig_to_pos2 traces a path, slab by slab, from the beginning slab, to the destination slab (struct ComputerDig) - using the ariadne wallhug algorithm (see below).

tool_dig_to_pos2 returns when a slab has been marked for digging (or another action has been triggered).

tool_dig_to_pos2 (and the calling task function above) is called again and again (e.g. after each slab marked for digging) until the task is marked as done.

Checking slab terrain (and dig flags)

tool_dig_to_pos2 is passed "dig flags" that indicate the actions allowed by the dig task.

Examples of dig flags are: "dig dirt", "dig gold", "bridge liquid".

These "dig flags" are used to "Check the Slab" - to see if the slab is: the destination, passable, impassable, needs an action to make it passable, or needs a queued action to make it passable:

  • check_slab - Call slab_do_actions, then check if slab is destination and return the slab status
  • slab_do_actions - Call slab_check_for_actions, then carry out any actions on slab (e.g. mark slab for digging) and finalise slab status
  • slab_check_for_actions - Check slab properties and "dig flags" to check if a slab is passable or impassable (noting any actions required).

tool_dig_to_pos2 returns the "result of the last processed slab".

task_dig_... handles the "result of the last processed slab" and decides whether to continue the dig task, wait for a sub-task, or end the dig task.

The "result of the last processed slab" is:

  • 0 for "impassable"
  • >= 1 for "passable"
  • < 0 for "exit tool_dig_to_pos2 and return to task_dig_... " (e.g. destination reached, slab marked for digging, error).


Reimplement the DK "ariadne_wallhug" algorithm - without the decompiled C.

The "ariadne wallhug" algorithm in Dungeon Keeper is a maze-solving algorithm with similarities to the Pledge algorithm and the Chain algorithm.

  • Starting at a known starting tile, step towards the known destination by 1 step (by manhattan distance). The tile to move to is selected based on the euclidian distance between the current tile and the destination.
  • If the tile to step in to is an obstacle then we need to hug the wall. Remember the distance between the current tile and the destination as "best distance".
  • Setup 2 look-ahead robots: one follows the wall with the left-hand rule, the other follows the wall with the right-hand rule.
    • Allow both robots to run until one of them reaches the destination.
    • The look ahead robots will stop following the wall when the tile in front of them is closer to the destination than the "best distance".
    • Once a look-ahead robot stops following the wall, it moves directly towards the destination, in the same way as above.
    • If a look-ahead robot that has stopped following the wall reaches another wall, then it stops functioning. i.e. when the robot would need to create it's own set of 2 look-ahead robots, it instead says "can't reach the destination".
    • The look-ahead robots are limited to a 150 tile path.
    • If the left-hand rule robot reaches the destination first, then the left-hand rule is chosen.
    • If the right-hand rule robot reaches the destination first, then the right-hand rule is chosen. If neither robot reaches the destination, then the right-hand rule is chosen.
  • Using the chosen rule, follow the wall for a single tile.
  • Stop following the wall if the distance between the current tile and the destination is less than the "best distance".
  • Repeat the above step by step, tile by tile, until the destination is reached. The max path length is the sum of the tiles in the current map.
  • If the current tile is a "wall" go back to the previous tile (fails up to 10 times after this, then errors).

Function overview

  • ariadne_wallhug - loop, return on slab = (destination/action complete/action queued/error)
    • ariadne_wallhug_single_step - either step towards destination, or return the result of wall_follow_single_step
      • wall_follow_single_step - follow the wall (using the passed rule: left-hand or right-hand)
      • setup_look_ahead_robots - sets up a left-hand rule following robot, and a right-hand rule following robot, each robot loops through ariadne_wallhug_single_step providing a breadth-first approach to decide whether the left-hand rule is best, or the right-hand rule is best.

ariadne_wallhug is called by tool_dig_to_pos2_f and requires a properly setup struct ComputerDig to function.

ariadne_wallhug_single_step calls on small_index_towards_destination (and small_index_towards_direction for the robots) to measure the distance between the current slab and the destination slab.

Both ariadne_wallhug_single_step and wall_follow_single_step call on check_slab to check whether a slab is "good for a computer dig path" and uses this slab status to filter behaviour.

Algorithm psuedocode

ariadne_wallhug()
{
  do
  {
    slab = ariadne_wallhug_single_step();
  } until (slab == destination);
}

ariadne_wallhug_single_step()
{
  slab = check_slab_towards_destination();
  dist = distance between slab and destination;
  if (dist < best_dist)
  {
    if (slab is passable)
    {
      move_forwards;
      best_dist = dist;
      return slab;
    }
    else
    {
      if (not look-ahead robot)
      {
        setup_look_ahead_robots();
        do
        {
          if (robot A active)
          {
            slab = ariadne_wallhug_single_step(robot A);
          }
          if (robot B active)
          {
            slab = ariadne_wallhug_single_step(robot B);
          }
       } until (best route found);
      set rule to left-hand or right-hand depending on best route;
    }
  }
  return wall_follow_single_step(rule);
}

@eddebaby eddebaby marked this pull request as draft December 17, 2023 22:30
@eddebaby eddebaby self-assigned this Dec 18, 2023
might be more stuff to gather from my notes.

Just want it in a commit finally.
This reverts commit 0454396.
@eddebaby
Copy link
Contributor Author

eddebaby commented Dec 27, 2023

In the future

  • Except for fixing any bugs in my rewrite, there is little point in modifying the "ariadne wallhug" algorithm - i.e. ariadne_wallhug, ariadne_wallhug_single_step , wall_follow_single_step , and setup_look_ahead_robots. Instead the algorithm should be replaced wholesale.
  • The "ariadne wallhug" algorithm can be replaced with another maze-finding algorithm without touching simulate_dig_to, task_dig_room_passage, task_dig_to_entrance, task_dig_to_gold, task_dig_to_attack, task_dig_to_neutral, check_slab, slab_do_actions, and slab_check_for_actions.
  • The AI mark for digging behaviour can be enhanced without touching ariadne_wallhug, ariadne_wallhug_single_step , wall_follow_single_step , and setup_look_ahead_robots.
  • The rewrite of the algorithm should serve as a good example for how to apply any abstract algorithm for solving 2d grid mazes to the specific use case of the "dig to" CPU player tasks in KeeperFX.
  • A star (A*):
    • The implementation a graph based path-finding algorithm for the "dig to" CPU player tasks in KeeperFX will require a "map of the maze", i.e. it will require "the terrain of the grid" to be expressed as a graph.
    • So please note that a maze-solving algorithm is one of the most efficient ways to generate a "map of the maze", and that if we do not design an effective way to "weight the KFX terrain" i.e. "apply movement cost to each terrain type" then the paths generated by e.g. A* will not be any better than "an ideal maze-solving" algorithm, the path will likely take longer to calculate, and require an up-to-date graph ready to go (or more compute is required at path-time, to regenerate the graph).
    • A player can only complete 1 map action per frame, e.g. one slab marked for digging per frame, otherwise the expectations of the state machines and the "actual game output" desynchronise, which while often irrelevant to the actual game play - it is the bane of multiplayer.
    • Once a frame has passed, and the path continues to be followed, the "navigation map" could be meaningfully different ands so the path may need to change course.
    • So to implement A* we need to design an effective graph that serves as a "navigation map" at the slab level. This might "come for free" with a rewrite of the creature pathfinding algorithm.
  • I have an incomplete idea for an implementation combining "ariadne wallhug" with the Chain algorithm, the modifications to "ariadne wallhug" would be:
    • Each look-ahead robot has their own pair of look-ahead robots (i.e. full breadth-first search capabilities through recursion)
    • Mimic the chain algorithm - Have the look-ahead robots say "choose my hand rule" when they "intersect with the perfect line from the path's start, to the path's destination at a point that is further along the line than the furthest point along this line reached by the real traveller". (The look-ahead robots currently only say "choose my hand rule" if they reach the destination point.) See distance equals intersect.ods for my mad ramblings towards a solution to detect the intersection without keeping a list of the tiles that make the "perfect line" that would then have to be searched to see if it contains the current tile. TLDR I have a solution that works great with straighter paths, i.e. the 8 compass roses, and maybe up to 16 or 32 divisions, but any line that is more "wonky" than that can only be estimated as a shape - see the default values of the spreadsheet and the path/shape it draws with green squares for an example wonky path.
    • Modify the decision of "when to stop wallhugging"... (depends on the type of paths we want to output, no "perfect solution" occurs to me, both pledge's algorithm and the chain algorithm have alternatives that could be tested with relative ease)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant