jarlyyn 发表于 昨天 04:32 PM

C#版本的地图路径规划代码

C#的,代码比较简单,没什么优化和花活,加了适当注释,供参考。

计算分为3个部分

1.mapfile,放所有的房间/出口/飞行的信息

2.context,游戏中的当前状态上下文,包括的出口标签匹配,临时房间,出口等等

3.option 移动的选项,可以限制移动的最大的消耗,避免计算过远的路径,或者禁用飞行信息

jarlyyn 发表于 昨天 04:34 PM

路径规划器,能计算路径,动态规划遍历,动态规划顺序移动

https://github.com/hellclient-scripts/hellmapmanager/blob/main/code/HellMapManager/Helpers/Mapper.cs

using HellMapManager.Models;
using System.Collections.Generic;
// 路径规划模块
namespace HellMapManager.Helpers;

// 规划中的每一个步骤
public class WalkingStep
{
    //从 出口信息.上一步,出发点,移动消耗,移动总消耗生成一个新的规划步骤
    //prev是上一个步骤,为空说明是第一步
    //from是移动起点,因为exit可以在多个房间复用,并不包含起点信息
    //exit 具体的房间出口信息
    //cost 这个步骤有多少移动消耗
    //totalcost 总的移动消耗,如果消耗超过Context中最大的移动消耗,路径规划就会失败
    public static WalkingStep FromExit(WalkingStep? prev, string from, Exit exit, int cost, int TotalCost)
    {
      return new WalkingStep()
      {
            Prev = prev,
            From = from,
            To = exit.To,
            Command = exit.Command,
            Cost = cost,
            TotalCost = TotalCost + cost,
            Remain = cost - 1,
      };
    }
    public Step ToStep()
    {
      return new Step(Command, To, Cost);
    }
    //上一个步骤,为空说明是第一步
    public WalkingStep? Prev { get; set; } = null;
    public string From = "";
    //移动起点,因为exit可以在多个房间复用,并不包含起点信息
    public string To { get; set; } = "";
    //目的地
    public string Command { get; set; } = "";
    //移动消耗
    public int Cost { get; set; } = 0;
    //总移动消耗
    public int TotalCost { get; set; } = 0;
    //剩余步数,每个计算回合-1,为0说明这一步移动完成。
    public int Remain { get; set; } = 0;
}

//规划类
public class Walking(Mapper mapper)
{
    //已经移动过的房间信息
    private Dictionary<string, WalkingStep> Walked = new();
    //对应的mapper
    public Mapper Mapper { get; } = mapper;
    //根据最后成功的最后一步移动(last),生成移动结果。targets为目标列表,会在去除移动目标并在结果里记录剩余目标
    private static QueryResult BuildResult(WalkingStep last, List<string> targets)
    {
      var result = new QueryResult();

      WalkingStep current = last;
      //没有prev说明是第一步
      while (current.Prev is not null)
      {
            result.Steps.Add(current.ToStep());
            current = current.Prev;
      }
      //第一步也要加入
      result.Steps.Add(current.ToStep());
      //逆向一下,最早的步骤应该在最前面
      result.Steps.Reverse();
      result.From = current.From;
      result.To = last.To;
      //统计剩余目标(移动支持多目标)
      foreach (var target in targets)
      {
            if (target != result.To)
            {
                result.Unvisited.Add(target);
            }
      }
      //计算消耗
      result.Cost = last.TotalCost;
      return result;
    }
    //在多个起点/终点之间规划一个最近路线
    //from 起点列表
    //target 重点目标
    //initTotalCost 初始移动总消耗,用于缺点多步逼近规划里限制总消耗
    public QueryResult QueryPathAny(List<string> from, List<string> target, int initTotalCost)
    {
      from.RemoveAll(x => x == "");
      target.RemoveAll(x => x == "");
      if (from.Count == 0 || target.Count == 0)
      {
            return QueryResult.Fail;
      }
      Walked = new();
      //从列表转字典
      var targets = new Dictionary<string, bool>();
      List<WalkingStep> current;
      List<WalkingStep> pending = [];
      foreach (var t in target)
      {
            targets = true;
      }
      //起点准备
      foreach (var f in from)
      {
            //终点包含起点则直接到达
            if (targets.ContainsKey(f))
            {
                var result = new QueryResult();
                result.From = f;
                result.To = f;
                result.Cost = initTotalCost;
                foreach (var to in target)
                {
                  if (to != f)
                  {
                        result.Unvisited.Add(to);
                  }
                }
                return result;
            }
            //标记起点
            Walked = new WalkingStep()
            {
                From = "",
                Command = "",
            };
            //加入移动中列表
            Mapper.AddRoomWalkingSteps(null, pending, f, initTotalCost);
      }
      //移动一轮
      while (pending.Count > 0)
      {
            current = pending;
            pending = [];
            //循环每个待移动步骤
            foreach (var step in current)
            {
                //确定这个步骤还需要计算。
                //如果目标已经到达,则放弃
                if (!Walked.ContainsKey(step.To))
                {
                  
                  if (step.Remain <= 1)
                  {
                        //移动到达
                        if (targets.ContainsKey(step.To))
                        {
                            //到达终点
                            return BuildResult(step, target);
                        }
                        Walked = step;
                        //还没走完,延迟到下一轮判断
                        Mapper.AddRoomWalkingSteps(step, pending, step.To, step.TotalCost);
                  }
                  else
                  {
                        //移动中
                        step.Remain--;
                        pending.Add(step);
                  }
                }
            }
      }
      //计算失败
      return QueryResult.Fail;
    }
    //膨胀,在给定的房间边上膨胀iterations个房间
    //一般用于npc会多步随机移动时,遍历目标和目标周边房间
    public List<string> Dilate(List<string> src, int iterations)
    {
      Walked = new();
      List<WalkingStep> current;
      List<WalkingStep> pending = [];
      foreach (var f in src)
      {
            //将有效房间加入待计算列表
            if (Mapper.GetRoom(f) is not null)
            {

                Walked = new WalkingStep()
                {
                  From = "",
                  Command = "",
                };
                Mapper.AddRoomWalkingSteps(null, pending, f, 0);
            }
      }
      var i = 0;
      while (pending.Count > 0 && i < iterations)
      {
            current = pending;
            pending = [];
            foreach (var step in current)
            {
                //未到达过的新房间加入下一步计算列表
                if (!Walked.ContainsKey(step.To))
                {
                  Walked = step;
                  Mapper.AddRoomWalkingSteps(step, pending, step.To, step.TotalCost);
                }
            }
            i++;
      }
      return [.. Walked.Keys];
    }
    //通过模拟多步行走的方式,动态计算全遍历路径
    public QueryResult QueryPathAll(string start, List<string> target)
    {
      target.RemoveAll(x => x == "");
      if (target.Count == 0 || start == "")
      {
            return QueryResult.Fail;
      }
      var result = new QueryResult
      {
            From = start,
            To = start,
      };
      //未走到的房间列表
      var pending = target;
      //循环移动,直到全部走完
      while (pending.Count > 0)
      {
            //计算单步
            var next = QueryPathAny(, pending, result.Cost);
            if (next.IsSuccess())
            {
                // 累积移动结果,循环下一轮
                result.Steps.AddRange(next.Steps);
                result.Cost = next.Cost;
                result.Unvisited = next.Unvisited;
                //更新新的计算列表
                pending = result.Unvisited;
                result.To = next.To;
            }
            else
            {
                //移动失败。结束
                pending = [];
            }
      }
      //如果一步也没动过,就是完全失败
      if (result.Steps.Count == 0)
      {
            return QueryResult.Fail;
      }
      return result;
    }
    //通过模拟多步行走的方式,动态计算顺序路径
    //targets为带顺序的房间列表
    //如果有部分房间无法进入,会跳过无法进入的房间。
    //用于在有某些限制条件的情况下,是指最新的上下文,然后尽可能多的遍历原路径中的房间
    public QueryResult QueryPathOrdered(string start, List<string> target)
    {
      target.RemoveAll(x => x == "");
      if (target.Count == 0 || start == "")
      {
            return QueryResult.Fail;
      }
      var result = new QueryResult
      {
            From = start,
            To = start,
      };
      for (var i = 0; i < target.Count; i++)
      {
            var next = QueryPathAny(, ], result.Cost);
            //成功则记录路径,不成功则跳过房间,将失败的房间加入Unvisited,继续计算
            if (next.IsSuccess())
            {
                result.Steps.AddRange(next.Steps);
                result.Cost = next.Cost;
                result.To = next.To;
            }
            else
            {
                result.Unvisited.Add(target);
            }
      }
      //如果一步也没动过,就是完全失败
      if (result.Steps.Count == 0)
      {
            return QueryResult.Fail;
      }
      return result;
    }
}

//地图计算器
//mapfile为地图信息
//context为移动上下文,多个移动可能使用相同的上下文
//options为移动选项,每次移动都可能不一样
public class Mapper(MapFile mapFile, Context context, MapperOptions options)
{
    public Context Context { get; } = context;
    public MapFile MapFile { get; } = mapFile;
    public MapperOptions Options { get; } = options;
    //通过key获取有效房间信息
    public Room? GetRoom(string key)
    {
      if (!Context.Rooms.TryGetValue(key, out var room))
      {
            if (!MapFile.Cache.Rooms.TryGetValue(key, out room))
            {
                return null;
            }
      }
      return room;
    }
    //更具上下文,获取出口的移动消耗
    public int GetExitCost(Exit exit)
    {
      //判断上下文中是否有单独的出口消耗定义覆盖原始值
      if (Context.CommandCosts.TryGetValue(exit.Command, out var costs))
      {
            if (costs.TryGetValue(exit.To, out var cost))
            {
                return cost;
            }
      }
      return exit.Cost;
    }
    //获取房间出口信息
    public List<Exit> GetRoomExits(Room room)
    {
      List<Exit> result = [.. room.Exits];
      //加入上下文中的临时出口
      if (Context.Paths.TryGetValue(room.Key, out var list))
      {
            result.AddRange(list);
      }
      //判断是否禁用捷径(飞行)出口
      if (!Options.DisableShortcuts)
      {
            //原始捷径信息
            MapFile.Map.Shortcuts.ForEach(e =>
            {
                //符合条件则加入列表
                if (ValueTag.ValidteConditions(room.Tags, e.RoomConditions))
                {
                  result.Add(e);
                }
            });
            //上下文的中的临时捷径
            Context.Shortcuts.ForEach(e =>
            {
                //符合条件则加入列表
                if (ValueTag.ValidteConditions(room.Tags, e.RoomConditions))
                {
                  result.Add(e);
                }
            });
      }
      return result;
    }
    //验证出口
    //start为出发房间
    //exit是出口信息
    //cost为移动消耗
    public bool ValidateExit(string start, Exit exit, int cost)
    {
      //判断房间有效
      var room = GetRoom(exit.To);
      if (room == null)
      {
            return false;
      }
      //验证房间符合当前移动的上下文设置
      if (!ValidateRoom(room))
      {
            return false;
      }
      //判断房间不在上下文中的拦截名单里
      if (Context.IsBlocked(start, exit.To))
      {
            return false;
      }
      //判断出口不超时
      if (Options.MaxExitCost > 0 && cost > Options.MaxExitCost)
      {
            return false;
      }
      //判断出口不超过整个移动的最大消耗
      if (Options.MaxTotalCost > 0 && cost > Options.MaxTotalCost)
      {
            return false;
      }
      //判断出口的条件是否匹配当前上下文
      if (!Context.ValidteConditions(exit.Conditions))
      {
            return false;
      }
      return true;
    }
    //验证房间
    public bool ValidateRoom(Room room)
    {
      //验证房间是否在黑名单
      if (Context.Blacklist.ContainsKey(room.Key))
      {
            return false;
      }
      //验证如果启用白名单的话,房间是否在白名单内
      if (Context.Whitelist.Count > 0 && !Context.Whitelist.ContainsKey(room.Key))
      {
            return false;
      }
      //验证房间的标签是否匹配上下文中的房间条件
      if (!ValueTag.ValidteConditions(room.Tags, Context.RoomConditions))
      {
            return false;
      }
      return true;
    }
    //验证路径
    public bool ValidatePath(string start, Exit exit)
    {
      //验证路径是否在黑名单
      if (Context.IsBlocked(start, exit.To))
      {
            return false;
      }
      return ValidateExit(start, exit, GetExitCost(exit));
    }
    //验证并转换路径
    //如果路径无效,返回空
    public WalkingStep? ValidateToWalkingStep(WalkingStep? prev, string from, Exit exit, int TotalCost)
    {
      if (exit.To == "" || exit.To == from)
      {
            return null;
      }
      var cost = GetExitCost(exit);
      //验证出口
      if (!ValidateExit(from, exit, cost))
      {
            return null;
      }
      //判断最大消耗
      if (Options.MaxTotalCost > 0 && Options.MaxTotalCost < (cost + TotalCost))
      {
            return null;
      }
      //转换
      return WalkingStep.FromExit(prev, from, exit, cost, TotalCost);
    }
    public void AddRoomWalkingSteps(WalkingStep? prev, List<WalkingStep> list, string from, int TotalCost)
    {
      var room = GetRoom(from);
      if (room is not null)
      {
            foreach (var exit in GetRoomExits(room))
            {
                var step = ValidateToWalkingStep(prev, from, exit, TotalCost);
                if (step is not null)
                {
                  list.Add(step);
                }
            }
      }
    }
}

jarlyyn 发表于 昨天 04:35 PM

移动上下文

游戏中和移动计算相关的动态信息

https://github.com/hellclient-scripts/hellmapmanager/blob/main/code/HellMapManager/Models/Context.cs

public class Context
{
    //标签列表
    public Dictionary<string, int> Tags = [];
    //房间条件列表
    public List<ValueCondition> RoomConditions = [];
    //临时房间
    public Dictionary<string, Room> Rooms = [];
    //白名单,为空忽略
    public Dictionary<string, bool> Whitelist = [];
    //黑名单
    public Dictionary<string, bool> Blacklist = [];
    //临时捷径列表
    public List<RoomConditionExit> Shortcuts = [];
    //临时出口列表
    public Dictionary<string, List<Path>> Paths = [];
    //拦截路径列表
    public Dictionary<string, Dictionary<string, bool>> BlockedLinks = [];
    //临时指令消耗列表
    public Dictionary<string, Dictionary<string, int>> CommandCosts = [];
}

jarlyyn 发表于 昨天 04:36 PM

移动选项

单次计算移动的信息

https://github.com/hellclient-scripts/hellmapmanager/blob/main/code/HellMapManager/Models/MapperOption.cs

public class MapperOptions
{
    //最大单步消耗,为0不限制
    public int MaxExitCost = 0;
    //最大总消耗,为0不限制
    public int MaxTotalCost = 0;
    //是否禁用捷径
    public bool DisableShortcuts = false;
}

jarlyyn 发表于 昨天 04:38 PM

https://github.com/hellclient-scripts/hellmapmanager.ts

转写的ts版本的

https://github.com/hellclient-scripts/hellmapmanager.ts/blob/main/src/helpers/mapper.ts

https://github.com/hellclient-scripts/hellmapmanager.ts/blob/main/src/models/context.ts

https://github.com/hellclient-scripts/hellmapmanager.ts/blob/main/src/models/mapperoption.ts

通过npm能编译成js版本的和lua版本的

js版本的正在稳定性和功能测试

lua的……暂时没空和没脚本测试。

jarlyyn 发表于 昨天 04:46 PM

数据结构大体参考这个

https://www.pkuxkx.net/forum/thread-50071-1-1.html
页: [1]
查看完整版本: C#版本的地图路径规划代码