From 1c4a68dde4a092f128bd967beaa6ff43a1cc69f8 Mon Sep 17 00:00:00 2001 From: tcd <1369103595@qq.com> Date: Tue, 21 Apr 2026 20:13:24 +0800 Subject: [PATCH 01/12] feat(lua): add train-decision endpoint --- src/lua/blackboard.lua | 13 +- src/lua/endpoint/test.lua | 63 ---- src/lua/endpoint/train-decision.lua | 320 +++++++++++++++++++ src/lua/intent/escape-to-home.lua | 30 ++ src/lua/intent/guard-home.lua | 0 src/lua/intent/keep-cruise.lua | 21 ++ src/lua/intent/start-cruise.lua | 52 +++ src/lua/task/crossing-fluctuant-road.lua | 69 ++++ src/lua/task/crossing-road-zone.lua | 32 +- src/lua/task/cruise-in-central-highlands.lua | 71 ++++ src/lua/task/fluctuant-stuck-self-rescue.lua | 119 +++++++ src/lua/task/navigate-to-point.lua | 51 +++ src/lua/task/occupy-fortress.lua | 28 ++ src/lua/task/stuck-self-rescue.lua | 246 ++++++++++++++ src/lua/task/supply-ammunition.lua | 0 src/lua/task/switch-mode.lua | 0 16 files changed, 1047 insertions(+), 68 deletions(-) delete mode 100644 src/lua/endpoint/test.lua create mode 100644 src/lua/endpoint/train-decision.lua create mode 100644 src/lua/intent/guard-home.lua create mode 100644 src/lua/intent/keep-cruise.lua create mode 100644 src/lua/intent/start-cruise.lua create mode 100644 src/lua/task/crossing-fluctuant-road.lua create mode 100644 src/lua/task/cruise-in-central-highlands.lua create mode 100644 src/lua/task/fluctuant-stuck-self-rescue.lua create mode 100644 src/lua/task/navigate-to-point.lua create mode 100644 src/lua/task/occupy-fortress.lua create mode 100644 src/lua/task/stuck-self-rescue.lua create mode 100644 src/lua/task/supply-ammunition.lua create mode 100644 src/lua/task/switch-mode.lua diff --git a/src/lua/blackboard.lua b/src/lua/blackboard.lua index dea3bcb..b2b3faf 100644 --- a/src/lua/blackboard.lua +++ b/src/lua/blackboard.lua @@ -33,10 +33,10 @@ local function create_default_blackboard() -- 状态类规则 - health_limit = 0, - health_ready = 0, - bullet_limit = 0, - bullet_ready = 0, + health_limit = 210, + health_ready = 400, + bullet_limit = 40, + bullet_ready = 300, -- 坐标类规则 -- 定义顺序:ours = 0,them = 1 @@ -50,6 +50,9 @@ local function create_default_blackboard() launch_ramp_final = PointPair { { 0, 0 }, { 0, 0 } }, outpost_resupply = PointPair { { 0, 0 }, { 0, 0 } }, -- 前哨站补给点 assembly_zone = PointPair { { 0, 0 }, { 0, 0 } }, + central_highland_near_crossing_road = PointPair { { 0, 0 }, { 0, 0 } }, -- 中央高地靠近公路一侧 + central_highland_near_doghole = PointPair { { 0, 0 }, { 0, 0 } }, --中央高地靠近狗洞一侧 + central_highland_two_steps = PointPair { { 0, 0 }, { 0, 0 } }, --中央高地靠近二级台阶(二级台阶增益点前) -- 特殊跨越地形坐标 road_tunnel_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 公路隧道 @@ -58,6 +61,8 @@ local function create_default_blackboard() one_step_final = PointPair { { 0, 0 }, { 0, 0 } }, two_step_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 二级台阶 two_step_final = PointPair { { 0, 0 }, { 0, 0 } }, + fluctuant_road_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 起伏路段 + fluctuant_road_final = PointPair { { 0, 0 }, { 0, 0 } }, common_elevated_ground_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 普通高地(飞坡起点那个高地) common_elevated_ground_final = PointPair { { 0, 0 }, { 0, 0 } }, }, diff --git a/src/lua/endpoint/test.lua b/src/lua/endpoint/test.lua deleted file mode 100644 index 9c77640..0000000 --- a/src/lua/endpoint/test.lua +++ /dev/null @@ -1,63 +0,0 @@ ---- ---- Local Context ---- - -local action = require("action") -local ascii = require("util.ascii_art") -local clock = require("util.clock") -local edges = require("util.edge").new() - -local Scheduler = require("util.scheduler") -local scheduler = Scheduler.new() -local request = Scheduler.request - -local restart_navigation = function() - action:info("导航即将重启") - action:restart_navigation { - global_map = "empty", - launch_livox = true, - launch_odin1 = false, - use_sim_time = false, - } -end - ---- ---- Export Context ---- - -blackboard = require("blackboard").singleton() - -on_init = function() - action:bind(scheduler) - action:info(ascii.banner) - action:warn("⚠️ TEST 模式,别上场哦") - - clock:reset(blackboard.meta.timestamp) - action:switch_topic_forward(true) - - restart_navigation() - edges:on(blackboard.getter.rswitch, "UP", restart_navigation) - - scheduler:append_task(function() - while true do - request:sleep(1) - -- action:info("limit: " .. blackboard.user.chassis_power_limit) - end - end) -end - -on_tick = function() - clock:update(blackboard.meta.timestamp) - - edges:spin() - scheduler:spin_once() -end - -on_exit = function() - action:stop_navigation() -end - ---- 由 NAV2 发布的目标速度值,在此处理回调 -on_control = function(x, y, _) - action:update_chassis_vel(x, y) -end diff --git a/src/lua/endpoint/train-decision.lua b/src/lua/endpoint/train-decision.lua new file mode 100644 index 0000000..862dd50 --- /dev/null +++ b/src/lua/endpoint/train-decision.lua @@ -0,0 +1,320 @@ +--- +--- Local Context +--- + +local action = require("action") +local ascii = require("util.ascii_art") +local clock = require("util.clock") +local fsm = require("util.fsm") +local option = require("option") + +local start_cruise = require("intent.start-cruise") +local keep_cruise = require("intent.keep-cruise") +local escape_to_home = require("intent.escape-to-home") + +local Scheduler = require("util.scheduler") +local scheduler = Scheduler.new() +local request = Scheduler.request + +local edges = require("util.edge").new() + +--- +--- Export Context +--- + +blackboard = require("blackboard").singleton() + +local runtime = { + ours_zone = true, + switch_interval = 2.0, + current_state = "idle", +} + +local requests = { + start = false, +} + +local job = { + handle = nil, + name = nil, + done = false, + success = false, +} + +local function read_option(name, fallback) + local value = rawget(option, name) + if value == nil then + return fallback + end + return value +end + +local function configure_test_rule() + local rule = blackboard.rule + + rule.health_limit = read_option("fsm_health_limit", 210) + rule.health_ready = read_option("fsm_health_ready", 400) + rule.bullet_limit = read_option("fsm_bullet_limit", 40) + rule.bullet_ready = read_option("fsm_bullet_ready", 300) + + -- Ours side sample points + rule.resupply_zone.ours = { x = 0.0, y = 0.0 } + rule.road_zone_begin.ours = { x = 0.0, y = 0.0 } + rule.road_zone_final.ours = { x = 0.0, y = 0.0 } + rule.fortress.ours = { x = 0.0, y = 0.0 } + rule.central_highland_near_crossing_road.ours = { x = 0.0, y = 0.0 } + rule.central_highland_near_doghole.ours = { x = 0.0, y = 0.0 } + + -- Them side sample points + rule.resupply_zone.them = { x = 0.0, y = 0.0 } + rule.road_zone_begin.them = { x = 0.0, y = 0.0 } + rule.road_zone_final.them = { x = 0.0, y = 0.0 } + rule.fortress.them = { x = 0.0, y = 0.0 } + rule.central_highland_near_crossing_road.them = { x = 0.0, y = 0.0 } + rule.central_highland_near_doghole.them = { x = 0.0, y = 0.0 } +end + +local function reset_job_status() + job.done = false + job.success = false +end + +local function cancel_job() + if job.handle ~= nil then + job.handle.cancel() + job.handle = nil + end + job.name = nil + reset_job_status() +end + +local function run_job(name, fn) + cancel_job() + job.name = name + reset_job_status() + + job.handle = scheduler:append_task(function() + local ok, result = xpcall(fn, debug.traceback) + job.handle = nil + job.name = nil + job.done = true + + if not ok then + job.success = false + action:fuck(string.format("fsm job '%s' failed:\n%s", name, result)) + return + end + + job.success = (result ~= false) + if not job.success then + action:warn(string.format("fsm job '%s' finished with false", name)) + end + end) +end + +local function take_request(name) + local value = requests[name] + requests[name] = false + return value +end + +local function set_state(name) + runtime.current_state = name + action:info("fsm state -> " .. name) +end + +local function start_navigation() + local ok, message = action:restart_navigation({ + global_map = read_option("global_map", "rmuc"), + launch_livox = read_option("launch_livox", true), + launch_odin1 = read_option("launch_odin1", false), + use_sim_time = read_option("use_sim_time", false), + }) + if not ok then + action:fuck("restart_navigation 触发失败: " .. tostring(message)) + end + + return ok, message +end + +local function setup_edges() + edges:on(blackboard.getter.rswitch, "UP", function() + requests.start = true + end) +end + +local function create_intent_fsm() + local State = { + idle = "idle", + start_cruise = "start_cruise", + keep_cruise = "keep_cruise", + escape = "escape", + recover = "recover", + } + + local condition = blackboard.condition + local intent_fsm = fsm:new(State.idle) + + intent_fsm:use({ + state = State.idle, + enter = function() + cancel_job() + set_state(State.idle) + end, + event = function(handle) + if take_request("start") then + start_navigation() + end + + if blackboard.game.stage == "STARTED" then + handle:set_next(State.start_cruise) + end + end, + }) + + intent_fsm:use({ + state = State.start_cruise, + enter = function() + set_state(State.start_cruise) + run_job("start_cruise", function() + return start_cruise(runtime.ours_zone) + end) + end, + event = function(handle) + if condition.low_health() or condition.low_bullet() then + cancel_job() + handle:set_next(State.escape) + return + end + + if job.done then + if job.success then + handle:set_next(State.keep_cruise) + else + action:warn("fsm(start_cruise): 导航失败") + end + end + end, + }) + + intent_fsm:use({ + state = State.keep_cruise, + enter = function() + set_state(State.keep_cruise) + run_job("keep_cruise", function() + return keep_cruise(runtime.ours_zone, runtime.switch_interval) + end) + end, + event = function(handle) + if condition.low_health() or condition.low_bullet() then + cancel_job() + handle:set_next(State.escape) + return + end + + if job.done and not job.success then + action:warn("fsm(keep_cruise): 导航失败") + end + end, + }) + + intent_fsm:use({ + state = State.escape, + enter = function() + set_state(State.escape) + run_job("escape_to_home", function() + return escape_to_home(runtime.ours_zone) + end) + end, + event = function(handle) + if job.done then + if job.success then + handle:set_next(State.recover) + else + action:warn("fsm(escape): 导航失败") + end + end + end, + }) + + intent_fsm:use({ + state = State.recover, + enter = function() + cancel_job() + set_state(State.recover) + end, + event = function(handle) + if condition.low_health() or condition.low_bullet() then + return + end + + if condition.health_ready() and condition.bullet_ready() then + handle:set_next(State.start_cruise) + end + end, + }) + + assert(intent_fsm:init_ready(State), "intent fsm init_ready failed") + return intent_fsm +end + +on_init = function() + clock:reset(blackboard.meta.timestamp) + + option:set_handler(function(error) + action:warn("while fetch option: " .. error) + end) + + runtime.ours_zone = read_option("fsm_ours_zone", true) + runtime.switch_interval = read_option("fsm_switch_interval", 2.0) + + configure_test_rule() + setup_edges() + + if read_option("enable_goal_topic_forward", false) then + action:switch_topic_forward(true) + end + action:bind(scheduler) + + local intent_fsm = create_intent_fsm() + scheduler:append_task(function() + while true do + intent_fsm:spin_once() + request:yield() + end + end) + + scheduler:append_task(function() + while true do + request:sleep(1.0) + action:info(string.format( + "fsm=%s stage=%s hp=%s bullet=%s rs=%s ls=%s", + runtime.current_state, + blackboard.game.stage, + tostring(blackboard.user.health), + tostring(blackboard.user.bullet), + blackboard.play.rswitch, + blackboard.play.lswitch + )) + end + end) + + action:info(ascii.banner) + action:warn("FSM test endpoint loaded") +end + +on_tick = function() + clock:update(blackboard.meta.timestamp) + edges:spin() + scheduler:spin_once() +end + +on_exit = function() + cancel_job() + action:stop_navigation() +end + +--- Callback for velocity topic from Nav2. +on_control = function(vx, vy, _) + action:update_chassis_vel(vx, vy) +end diff --git a/src/lua/intent/escape-to-home.lua b/src/lua/intent/escape-to-home.lua index e69de29..40a9c18 100644 --- a/src/lua/intent/escape-to-home.lua +++ b/src/lua/intent/escape-to-home.lua @@ -0,0 +1,30 @@ +local blackboard = require("blackboard").singleton() +local action = require("action") +local navigate_to_point = require("task.navigate-to-point") + +--- 回家补给:从当前位置直接导航至补给点。 +--- @param ours_zone boolean +--- @return boolean is_success +return function(ours_zone) + assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") + + local rule = blackboard.rule + local resupply_zone + if ours_zone then + resupply_zone = rule.resupply_zone.ours + else + resupply_zone = rule.resupply_zone.them + end + + local is_success = navigate_to_point(resupply_zone, { + tolerance = 0.15, + timeout = 10, + }) + if not is_success then + action:warn("escape-to-home: 导航到补给点失败(超时)") + return false + end + + action:info("escape-to-home: 已抵达补给点") + return true +end diff --git a/src/lua/intent/guard-home.lua b/src/lua/intent/guard-home.lua new file mode 100644 index 0000000..e69de29 diff --git a/src/lua/intent/keep-cruise.lua b/src/lua/intent/keep-cruise.lua new file mode 100644 index 0000000..22687f5 --- /dev/null +++ b/src/lua/intent/keep-cruise.lua @@ -0,0 +1,21 @@ +local action = require("action") +local cruise_in_central_highlands = require("task.cruise-in-central-highlands") + +--- 持续巡航:在中央高地两点间循环巡航(通常为长期运行)。 +--- @param ours_zone boolean +--- @param switch_interval number 中央高地巡航切换周期(秒) +--- @return boolean is_success +return function(ours_zone, switch_interval) + assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") + assert(type(switch_interval) == "number", "switch_interval should be a number") + assert(switch_interval > 0, "switch_interval should be positive") + + action:info("keep-cruise: 进入中央高地持续巡航") + local ok = cruise_in_central_highlands(ours_zone, switch_interval) + if not ok then + action:warn("keep-cruise: 中央高地巡航导航失败") + return false + end + + return true +end diff --git a/src/lua/intent/start-cruise.lua b/src/lua/intent/start-cruise.lua new file mode 100644 index 0000000..fc03b6a --- /dev/null +++ b/src/lua/intent/start-cruise.lua @@ -0,0 +1,52 @@ +local blackboard = require("blackboard").singleton() +local action = require("action") +local navigate_to_point = require("task.navigate-to-point") +local crossing_road_zone = require("task.crossing-road-zone") + +--- 开始巡航: +--- 1) 从补给点移动到公路区起点 +--- 2) 正向通过公路区 +--- 3) 完成持续巡航前的机动准备 +--- @param ours_zone boolean +--- @return boolean is_success +return function(ours_zone) + assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") + + local rule = blackboard.rule + local resupply + local road_begin + if ours_zone then + resupply = rule.resupply_zone.ours + road_begin = rule.road_zone_begin.ours + else + resupply = rule.resupply_zone.them + road_begin = rule.road_zone_begin.them + end + + local ok = navigate_to_point(resupply, { + tolerance = 0.08, + timeout = 10, + }) + if not ok then + action:warn("start-cruise: 导航到补给点失败(超时)") + return false + end + + ok = navigate_to_point(road_begin, { + tolerance = 0.15, + timeout = 10, + }) + if not ok then + action:warn("start-cruise: 导航到公路区起点失败(超时)") + return false + end + + ok = crossing_road_zone(ours_zone, true) + if not ok then + action:warn("start-cruise: 通过公路区导航失败") + return false + end + + action:info("start-cruise: 已通过公路区,准备进入持续巡航") + return true +end diff --git a/src/lua/task/crossing-fluctuant-road.lua b/src/lua/task/crossing-fluctuant-road.lua new file mode 100644 index 0000000..80fdd7a --- /dev/null +++ b/src/lua/task/crossing-fluctuant-road.lua @@ -0,0 +1,69 @@ +local blackboard = require("blackboard").singleton() +local request = require("util.scheduler").request +local action = require("action") + +--- @param ours_zone boolean +--- @param forward_center boolean +--- @return boolean is_success +return function(ours_zone, forward_center) + action:info("开始crossing-fluctuant-road") + + local x = blackboard.user.x + local y = blackboard.user.y + + local rule = blackboard.rule + local begin, final + if ours_zone then + begin = rule.fluctuant_road_begin.ours + final = rule.fluctuant_road_final.ours + else + begin = rule.fluctuant_road_begin.them + final = rule.fluctuant_road_final.them + end + + local from, to + if forward_center then + from = begin + to = final + else + from = final + to = begin + end + + local condition = blackboard.condition + + action:navigate(from) + local is_timeout = request:wait_until { + monitor = function() + return condition.near(from, 0.1) + end, + timeout = 10, + } + + if is_timeout then + action:warn(string.format( + "crossing-fluctuant-road: 导航到起点失败 (x=%.2f, y=%.2f)", + from.x, + from.y + )) + return false + end + + action:navigate(to) + local is_timeout = request:wait_until { + monitor = function() + return condition.near(to, 0.1) + end, + timeout = 10, + } + if is_timeout then + action:warn(string.format( + "crossing-fluctuant-road: 导航到终点失败 (x=%.2f, y=%.2f)", + to.x, + to.y + )) + return false + end + + return not is_timeout +end diff --git a/src/lua/task/crossing-road-zone.lua b/src/lua/task/crossing-road-zone.lua index aa72fcc..58c54b5 100644 --- a/src/lua/task/crossing-road-zone.lua +++ b/src/lua/task/crossing-road-zone.lua @@ -4,7 +4,10 @@ local action = require("action") --- @param ours_zone boolean --- @param forward_center boolean +--- @return boolean is_success return function(ours_zone, forward_center) + action:info("开始crossing-road-zone") + local x = blackboard.user.x local y = blackboard.user.y @@ -30,10 +33,37 @@ return function(ours_zone, forward_center) local condition = blackboard.condition action:navigate(from) - local timeout = request:wait_until { + local is_timeout = request:wait_until { monitor = function() return condition.near(from, 0.1) end, timeout = 10, } + + if is_timeout then + action:warn(string.format( + "crossing-road-zone: 导航到起点失败 (x=%.2f, y=%.2f)", + from.x, + from.y + )) + return false + end + + action:navigate(to) + local is_timeout = request:wait_until { + monitor = function() + return condition.near(to, 0.1) + end, + timeout = 10, + } + if is_timeout then + action:warn(string.format( + "crossing-road-zone: 导航到终点失败 (x=%.2f, y=%.2f)", + to.x, + to.y + )) + return false + end + + return not is_timeout end diff --git a/src/lua/task/cruise-in-central-highlands.lua b/src/lua/task/cruise-in-central-highlands.lua new file mode 100644 index 0000000..8c1d8e4 --- /dev/null +++ b/src/lua/task/cruise-in-central-highlands.lua @@ -0,0 +1,71 @@ +local blackboard = require("blackboard").singleton() +local clock = require("util.clock") +local request = require("util.scheduler").request +local action = require("action") + +local function distance_to(target) + local dx = target.x - blackboard.user.x + local dy = target.y - blackboard.user.y + return math.sqrt(dx * dx + dy * dy) +end + +--- 中央高地巡航:在“靠近公路侧”与“靠近狗洞侧”之间按固定周期切换导航目标。 +--- @param ours_zone boolean +--- @param switch_interval number 切换周期(秒) +return function(ours_zone, switch_interval) + assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") + assert(type(switch_interval) == "number", "switch_interval should be a number") + assert(switch_interval > 0, "switch_interval should be positive") + action:info("开始cruise-in-central-highlands") + + local rule = blackboard.rule + local condition = blackboard.condition + local navigation_timeout = math.max(10.0, switch_interval * 2.0) + local near_crossing_road, near_doghole + if ours_zone then + near_crossing_road = rule.central_highland_near_crossing_road.ours + near_doghole = rule.central_highland_near_doghole.ours + else + near_crossing_road = rule.central_highland_near_crossing_road.them + near_doghole = rule.central_highland_near_doghole.them + end + + -- 首次优先去更近的点,减少无效折返。 + local go_crossing_road_first = distance_to(near_crossing_road) <= distance_to(near_doghole) + local target = go_crossing_road_first and near_crossing_road or near_doghole + + while true do + local phase_start = clock:now() + action:navigate(target) + local is_timeout = request:wait_until { + monitor = function() + return condition.near(target, 0.15) + end, + timeout = navigation_timeout, + } + if is_timeout then + action:warn(string.format( + "cruise-in-central-highlands: 导航到巡航点失败 (x=%.2f, y=%.2f, timeout=%.2fs)", + target.x, + target.y, + navigation_timeout + )) + return false + end + + -- 保持固定切换周期:若提前到达,则驻留到本周期结束后再切点。 + local elapsed = clock:now() - phase_start + local remain = switch_interval - elapsed + if remain > 0 then + request:sleep(remain) + end + + if target == near_crossing_road then + target = near_doghole + else + target = near_crossing_road + end + end + + return true +end diff --git a/src/lua/task/fluctuant-stuck-self-rescue.lua b/src/lua/task/fluctuant-stuck-self-rescue.lua new file mode 100644 index 0000000..d4fa50b --- /dev/null +++ b/src/lua/task/fluctuant-stuck-self-rescue.lua @@ -0,0 +1,119 @@ +local blackboard = require("blackboard").singleton() +local request = require("util.scheduler").request +local action = require("action") +local navigate_to_point = require("task.navigate-to-point") +local stuck_self_rescue = require("task.stuck-self-rescue") + +--- @class FluctuantStuckSelfRescueOptions +--- @field max_cycles? number +--- @field settle_time? number +--- @field retreat_timeout? number +--- @field retreat_tolerance? number|{x: number, y: number} +--- @field retry_forward_timeout? number +--- @field goal_tolerance? number|{x: number, y: number} +--- @field fallback_timeout? number +--- @field fallback_max_rescue_attempts? number + +local function normalize_options(options) + options = options or {} + assert(type(options) == "table", "options should be a table") + + local result = { + max_cycles = options.max_cycles or 2, + settle_time = options.settle_time or 0.25, + retreat_timeout = options.retreat_timeout or 2.5, + retreat_tolerance = options.retreat_tolerance or 0.2, + retry_forward_timeout = options.retry_forward_timeout or 5.0, + goal_tolerance = options.goal_tolerance or 0.2, + fallback_timeout = options.fallback_timeout or 8.0, + fallback_max_rescue_attempts = options.fallback_max_rescue_attempts or 2, + } + + assert(type(result.max_cycles) == "number" and result.max_cycles >= 0, "max_cycles should be non-negative") + assert(result.max_cycles % 1 == 0, "max_cycles should be an integer") + assert(type(result.settle_time) == "number" and result.settle_time >= 0, "settle_time should be non-negative") + assert(type(result.retreat_timeout) == "number" and result.retreat_timeout > 0, "retreat_timeout should be positive") + assert(type(result.retry_forward_timeout) == "number" and result.retry_forward_timeout > 0, "retry_forward_timeout should be positive") + assert(type(result.fallback_timeout) == "number" and result.fallback_timeout > 0, "fallback_timeout should be positive") + assert( + type(result.fallback_max_rescue_attempts) == "number" and result.fallback_max_rescue_attempts >= 0, + "fallback_max_rescue_attempts should be non-negative" + ) + assert(result.fallback_max_rescue_attempts % 1 == 0, "fallback_max_rescue_attempts should be an integer") + + return result +end + +local function resolve_fluctuant_points(ours_zone, forward_center) + assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") + assert(type(forward_center) == "boolean", "forward_center should be a boolean") + + local rule = blackboard.rule + local begin, final + if ours_zone then + begin = rule.fluctuant_road_begin.ours + final = rule.fluctuant_road_final.ours + else + begin = rule.fluctuant_road_begin.them + final = rule.fluctuant_road_final.them + end + + if forward_center then + return begin, final + end + return final, begin +end + +--- 起伏路段专用脱困:优先回退至稳定区再重试;失败后回落通用脱困。 +--- @param ours_zone boolean +--- @param forward_center boolean +--- @param options? FluctuantStuckSelfRescueOptions +--- @return boolean is_success +return function(ours_zone, forward_center, options) + action:info("开始fluctuant-stuck-self-rescue") + + local config = normalize_options(options) + local condition = blackboard.condition + + local from, to = resolve_fluctuant_points(ours_zone, forward_center) + if condition.near(to, config.goal_tolerance) then + return true + end + + for cycle = 1, config.max_cycles do + action:warn("起伏路段疑似悬空卡住,执行回退脱困流程") + + local back_ok = navigate_to_point(from, { + tolerance = config.retreat_tolerance, + timeout = config.retreat_timeout, + }) + if not back_ok then + action:warn("起伏路段回退失败,准备回落通用脱困") + break + end + + if config.settle_time > 0 then + request:sleep(config.settle_time) + end + + local forward_ok = navigate_to_point(to, { + tolerance = config.goal_tolerance, + timeout = config.retry_forward_timeout, + }) + if forward_ok then + return true + end + + action:warn("起伏路段重试通过失败,继续下一轮") + end + + action:warn("起伏路段专用回退未恢复,回落通用 stuck-self-rescue") + return stuck_self_rescue(to, { + timeout = config.fallback_timeout, + goal_tolerance = config.goal_tolerance, + max_rescue_attempts = config.fallback_max_rescue_attempts, + rescue_side_offset = 0.35, + rescue_backtrack = 0.45, + rescue_point_timeout = 2.0, + }) +end diff --git a/src/lua/task/navigate-to-point.lua b/src/lua/task/navigate-to-point.lua new file mode 100644 index 0000000..ac073e1 --- /dev/null +++ b/src/lua/task/navigate-to-point.lua @@ -0,0 +1,51 @@ +local blackboard = require("blackboard").singleton() +local request = require("util.scheduler").request +local action = require("action") + +--- @class NavigateToPointOptions +--- @field tolerance? number|{x: number, y: number} +--- @field timeout? number + +local function normalize_options(options) + options = options or {} + assert(type(options) == "table", "options should be a table") + + local tolerance = options.tolerance or 0.1 + if type(tolerance) == "number" then + assert(tolerance >= 0, "tolerance should be non-negative") + else + assert(type(tolerance) == "table", "tolerance should be number or {x, y}") + assert(type(tolerance.x) == "number", "tolerance.x should be a number") + assert(type(tolerance.y) == "number", "tolerance.y should be a number") + assert(tolerance.x >= 0 and tolerance.y >= 0, "tolerance.{x,y} should be non-negative") + end + + local timeout = options.timeout or 10 + assert(type(timeout) == "number", "timeout should be a number") + assert(timeout >= 0, "timeout should be non-negative") + + return tolerance, timeout +end + +--- 普通点位导航:设置目标点并等待到达(或超时)。 +--- @param point {x: number, y: number} +--- @param options? NavigateToPointOptions +--- @return boolean is_success +return function(point, options) + assert(type(point) == "table", "point should be a table") + assert(type(point.x) == "number", "point.x should be a number") + assert(type(point.y) == "number", "point.y should be a number") + action:info("开始navigate-to-point") + + local tolerance, timeout = normalize_options(options) + local condition = blackboard.condition + + action:navigate(point) + local is_timeout = request:wait_until { + monitor = function() + return condition.near(point, tolerance) + end, + timeout = timeout, + } + return not is_timeout +end diff --git a/src/lua/task/occupy-fortress.lua b/src/lua/task/occupy-fortress.lua new file mode 100644 index 0000000..aaa9a6f --- /dev/null +++ b/src/lua/task/occupy-fortress.lua @@ -0,0 +1,28 @@ +local blackboard = require("blackboard").singleton() +local action = require("action") +local navigate_to_point = require("task.navigate-to-point") + +--- @param ours_zone boolean +--- @return boolean is_success +return function(ours_zone) + action:info("开始occupy-fortress") + + local rule = blackboard.rule + local fortress + if ours_zone then + fortress = rule.fortress.ours + else + fortress = rule.fortress.them + end + + local is_success = navigate_to_point(fortress, { + tolerance = 0.1, + timeout = 10, + }) + if not is_success then + action:warn("前往堡垒点超时") + return false + end + + return true +end diff --git a/src/lua/task/stuck-self-rescue.lua b/src/lua/task/stuck-self-rescue.lua new file mode 100644 index 0000000..aa46a90 --- /dev/null +++ b/src/lua/task/stuck-self-rescue.lua @@ -0,0 +1,246 @@ +local blackboard = require("blackboard").singleton() +local clock = require("util.clock") +local request = require("util.scheduler").request +local util = require("util.math") +local action = require("action") +local navigate_to_point = require("task.navigate-to-point") + +local function distance(a, b) + local dx = a.x - b.x + local dy = a.y - b.y + return math.sqrt(dx * dx + dy * dy) +end + +local function vector_length(x, y) + return math.sqrt(x * x + y * y) +end + +--- @class StuckSelfRescueOptions +--- @field timeout? number +--- @field monitor_interval? number +--- @field stall_window? number +--- @field min_progress? number +--- @field goal_tolerance? number|{x: number, y: number} +--- @field stuck_ignore_tolerance? number +--- @field max_rescue_attempts? number +--- @field rescue_side_offset? number +--- @field rescue_backtrack? number +--- @field rescue_point_timeout? number +--- @field rescue_point_tolerance? number|{x: number, y: number} + +local function normalize_options(options) + options = options or {} + assert(type(options) == "table", "options should be a table") + + local result = { + timeout = options.timeout or 12.0, + monitor_interval = options.monitor_interval or 0.1, + stall_window = options.stall_window or 1.2, + min_progress = options.min_progress or 0.05, + goal_tolerance = options.goal_tolerance or 0.15, + stuck_ignore_tolerance = options.stuck_ignore_tolerance or 0.35, + max_rescue_attempts = options.max_rescue_attempts or 2, + rescue_side_offset = options.rescue_side_offset or 0.6, + rescue_backtrack = options.rescue_backtrack or 0.4, + rescue_point_timeout = options.rescue_point_timeout or 2.5, + rescue_point_tolerance = options.rescue_point_tolerance or 0.15, + } + + assert(type(result.timeout) == "number" and result.timeout > 0, "timeout should be positive") + assert( + type(result.monitor_interval) == "number" and result.monitor_interval > 0, + "monitor_interval should be positive" + ) + assert( + type(result.stall_window) == "number" and result.stall_window > 0, + "stall_window should be positive" + ) + assert( + type(result.min_progress) == "number" and result.min_progress >= 0, + "min_progress should be non-negative" + ) + assert( + type(result.stuck_ignore_tolerance) == "number" and result.stuck_ignore_tolerance >= 0, + "stuck_ignore_tolerance should be non-negative" + ) + assert( + type(result.max_rescue_attempts) == "number" and result.max_rescue_attempts >= 0, + "max_rescue_attempts should be non-negative" + ) + assert( + type(result.max_rescue_attempts) == "number" and result.max_rescue_attempts % 1 == 0, + "max_rescue_attempts should be an integer" + ) + assert( + type(result.rescue_side_offset) == "number" and result.rescue_side_offset >= 0, + "rescue_side_offset should be non-negative" + ) + assert( + type(result.rescue_backtrack) == "number" and result.rescue_backtrack >= 0, + "rescue_backtrack should be non-negative" + ) + assert( + type(result.rescue_point_timeout) == "number" and result.rescue_point_timeout > 0, + "rescue_point_timeout should be positive" + ) + + return result +end + +local function build_rescue_targets(goal, side_offset, backtrack) + local current = { + x = blackboard.user.x, + y = blackboard.user.y, + } + + local dx = goal.x - current.x + local dy = goal.y - current.y + local norm = vector_length(dx, dy) + if norm < 1e-6 then + dx = math.cos(blackboard.user.yaw) + dy = math.sin(blackboard.user.yaw) + norm = vector_length(dx, dy) + end + + if norm < 1e-6 then + dx = 1.0 + dy = 0.0 + norm = 1.0 + end + + local ux = dx / norm + local uy = dy / norm + local sx = -uy + local sy = ux + + return { + { + x = current.x + sx * side_offset, + y = current.y + sy * side_offset, + }, + { + x = current.x - sx * side_offset, + y = current.y - sy * side_offset, + }, + { + x = current.x - ux * backtrack, + y = current.y - uy * backtrack, + }, + } +end + +--- @param goal {x: number, y: number} +--- @param config StuckSelfRescueOptions +--- @param deadline number +--- @return "success" | "stuck" | "timeout" +local function monitor_progress(goal, config, deadline) + local condition = blackboard.condition + local previous = { + x = blackboard.user.x, + y = blackboard.user.y, + } + local stall_begin = nil + + while true do + if condition.near(goal, config.goal_tolerance) then + return "success" + end + + local now = clock:now() + if now >= deadline then + return "timeout" + end + + local current = { + x = blackboard.user.x, + y = blackboard.user.y, + } + local moved = distance(current, previous) + previous = current + + if moved >= config.min_progress then + stall_begin = nil + else + stall_begin = stall_begin or now + if now - stall_begin >= config.stall_window then + if not condition.near(goal, config.stuck_ignore_tolerance) then + return "stuck" + end + end + end + + request:sleep(config.monitor_interval) + end +end + +--- @param goal {x: number, y: number} +--- @param config StuckSelfRescueOptions +--- @param deadline number +--- @return boolean rescued +local function execute_rescue(goal, config, deadline) + local targets = build_rescue_targets(goal, config.rescue_side_offset, config.rescue_backtrack) + for _, target in ipairs(targets) do + local now = clock:now() + local remaining = deadline - now + if remaining <= 0 then + return false + end + + local timeout = math.min(config.rescue_point_timeout, remaining) + local ok = navigate_to_point(target, { + tolerance = config.rescue_point_tolerance, + timeout = timeout, + }) + if ok then + return true + end + end + + return false +end + +--- 通用卡死自救:检测无进展并执行侧向/回退子目标脱困。 +--- @param goal {x: number, y: number} +--- @param options? StuckSelfRescueOptions +--- @return boolean is_success +return function(goal, options) + assert(type(goal) == "table", "goal should be a table") + assert(type(goal.x) == "number", "goal.x should be a number") + assert(type(goal.y) == "number", "goal.y should be a number") + assert(not util.check_nan(goal.x, goal.y), "goal should not be NaN") + action:info("开始stuck-self-rescue") + + local config = normalize_options(options) + local condition = blackboard.condition + local deadline = clock:now() + config.timeout + + if condition.near(goal, config.goal_tolerance) then + return true + end + + local rescue_count = 0 + while true do + action:navigate(goal) + local status = monitor_progress(goal, config, deadline) + if status == "success" then + return true + end + if status == "timeout" then + action:warn("stuck-self-rescue 超时,未能抵达目标") + return false + end + + rescue_count = rescue_count + 1 + if rescue_count > config.max_rescue_attempts then + action:warn("stuck-self-rescue 超过最大脱困次数") + return false + end + + action:warn("检测到无进展,开始执行脱困子目标") + local rescued = execute_rescue(goal, config, deadline) + if not rescued then + action:warn("脱困子目标执行失败") + return false + end + end +end diff --git a/src/lua/task/supply-ammunition.lua b/src/lua/task/supply-ammunition.lua new file mode 100644 index 0000000..e69de29 diff --git a/src/lua/task/switch-mode.lua b/src/lua/task/switch-mode.lua new file mode 100644 index 0000000..e69de29 From 5a122838d081727f2ddb3bc0e450838309154f21 Mon Sep 17 00:00:00 2001 From: tcd <1369103595@qq.com> Date: Tue, 21 Apr 2026 23:07:26 +0800 Subject: [PATCH 02/12] feat(nav-map): add train_map occupancy map and metadata --- maps/train_map.png | Bin 0 -> 2956 bytes maps/train_map.yaml | 7 +++++++ 2 files changed, 7 insertions(+) create mode 100644 maps/train_map.png create mode 100644 maps/train_map.yaml diff --git a/maps/train_map.png b/maps/train_map.png new file mode 100644 index 0000000000000000000000000000000000000000..4492aaad5836804910e526aa335c324e53188511 GIT binary patch literal 2956 zcmV;73v={|P)YJ+mKX+SGX4%GIVw z;0Oe8t^dCSE|}7~4g>%K1OUz%wyU8(f`_nxvTCLaYDm2L2y}KA3D$I-RxH_I%y$_9 za?UDlNWAKe-oRLgK8vAD)V6aE-qU5kmX>#wMeo3r%@-tln8iq|IvPA%Jd{T~_ z5YPFb)zd;mvy1^yVm(k${Khp!nD%rr_jLGM#2s)ZJ&`I=%8O)Z&YUiQQ%%N+U%rfn z;NeQ^X;i+>t|kfomc_V4myJOmWkX^)L{3i4I8;u*La#Y$^OTLRH_hgw zUIme}L)D9ZHwYBT>VgI4$MA$IwxxJ~W)L{KJ@p1Z_qC}>o~>Y=_33w@E>pG`N$)gO zvqT^b*!E>Bqrwyb0Pw6GeYV^?0jnzH!`^f2Ot^<#nCM8I#OntBP4R-ts9cCxzbs8W zmh*qBx$A?Hp=FFUYgJ*5@c1Fp$frU@6(7p+k03ZS>*WW8`ejW$^iEpsiiM%r@LUAa zXZZyR#yt=35T-bxpsg@CurY*Si}CrYz2X+Daho|>=d7LX`k8ZxHb2HoLv78S*ET+5 zL)A%`^@tRVgg|Lf-Jp82qGerzE8!(KRGGFLAl&@mG2X%Ha9p%KVNGwwe-&tyg)FY# zTwF#ZBBC*xAX|Xe9>4>;&zoUIFyfknox(+nq{-at4rlczOagDL{p^r;*_?5QN$mBi zSh$tj*52U$obOM>nF0r0hQ;>y06aWMAXntQp&cko3VU908*e8!-_6hme`CeN$fx4} z$~wD6Ry4{rpH&O`uNi%py}e!mnkiz3MiTO)DBB5low9+h`j19FT;tFVdmVf%(To{g z7O?^c13)t!xlXrApivJm4il8kuSQ=gMsTJ4n|;}fvk6`H%%C+5zdUcS9L&0xkKgUZ z7653JjoIhU1}z#T(v#Wc#}>GQklQ^wSYkKgB01-dkxx)-#e1cAam(R6Aq7s5X2W-Wyi?NV{J|- zqMak0)p3p$eT?8c>V%bfEM;unbj!1iw%=hY5n+eogvnTCTqThPbj!5|b>oX)rnMmM z9Ah~jxHfll1p`~0wZoPRx)XFkO!-Cu+~|7v?-!O_j>8v1Yx3(G-8-yx+CJTZFC_j!-xqxK)6$pC)5r0;xCoV!{> zH{irb9M02u9YAn1Kk9&$xeBsa?foTU2)E3DCAxh$`{AuDM;2gxFb29$llE>k1^4QF zI-)rX{uaD;SvmR{`FnC^ifQ!dlPmiO&O*Om_B19m=pGL*3y-I9#=(m+nH+qmtNoo0dW0*leae0oS;D0o)>%!OvqH&Tv*p8x2t~6Oy^8_yuJ?L5LpE zdS}NgVmU22w{6KeNADeho${!&O zU&Z)kpAJg@m_BM6mSq`s#Nfe<3;NR&1C((cJ%>nbG2n2PGfgqki&=cdF1t{zclngn z{RS11Oe%>o0UP5=cQ9&9W#fDy#VgK#`Myd=-zD(UX-1XAUBJ5j)mZq=7lcAPw0&O4 zO6b9+@)o&fe$VxLT0*Fed23>97}+N)rp6(H6`SH*fPq!pDAGD;aG9N4xNfM~MkpJU zac()ihY+3U;}al(3_NukH`%Ap`9<+0m26ak@Wl<|(tiYRxKsIlnUYf`sbhY()Fn4{c^ zrdk_gXauZ`_2v0x0x&(mv|<0_>7yO>I>hDe7&ylW$^tcG^%4~rgW}R`$C2;AP2yLh zuJq+fYWwg3PC)gRDv2x^+nQ=szZv@Kc=p4Rg!ftK~Eo!{GrF`XC> z<0wjfSEMAG#m@&Q+vXp=RvDp)WU*yrcGU7X$I(XEj>nV+UI+eVdb`SYn0J1woiJCuLN z+30#ktvzzK)9+)c<_Fcg3@FBKnxHJ(WvsU-OGDyc(p7{Ft9cKU5)Z3kvvMa`0!@+r z)A9489XoeS3M!RmrTJ)e1+gk6zX(Nl5>?f@%LdHXu2f-O>bOcpAzrss$(8O>rdqZS zNol_5=l%Q8Ndz0FG`EDoPgo3iZ~o3sGyC#2l3y0HYR-zbV+RHO~co&+(k-jr_hMo#pB zWk&nd$k=vNLNU^}Vf8r2Q_3oJcki)s*}UgkuWR|0F>o!(Ce_YaWq;n`t@cex55$dr zpE%4KW^sL1B|4WAYO^+QT zg%!&FeLTKtm+9CneKLptgR-w0=NAp-f5A8Dvi}5slzkljDEm14QTB28qwM4GN7={W zyB_Yh+xayYeAO;P_V;`7O_X8r|4Z>rlzqEIJn;_?5BSfbNNvIZ0000 Date: Tue, 21 Apr 2026 23:07:47 +0800 Subject: [PATCH 03/12] feat(lua): add train-specific road-crossing and start-cruise intents --- src/lua/intent/start-cruise-train.lua | 20 +++++++++++ src/lua/task/crossing-road-zone-train.lua | 41 +++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 src/lua/intent/start-cruise-train.lua create mode 100644 src/lua/task/crossing-road-zone-train.lua diff --git a/src/lua/intent/start-cruise-train.lua b/src/lua/intent/start-cruise-train.lua new file mode 100644 index 0000000..1b1a893 --- /dev/null +++ b/src/lua/intent/start-cruise-train.lua @@ -0,0 +1,20 @@ +local action = require("action") +local crossing_road_zone_train = require("task.crossing-road-zone-train") + +--- 训练专用: +--- 直接调用 crossing-road-zone-train 通过公路区 +--- @param ours_zone boolean +--- @return boolean is_success +return function(ours_zone) + assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") + action:info("开始start-cruise-train") + + local ok = crossing_road_zone_train(ours_zone) + if not ok then + action:warn("start-cruise-train: 通过公路区训练路线失败") + return false + end + + action:info("start-cruise-train: 已完成公路区训练路线") + return true +end diff --git a/src/lua/task/crossing-road-zone-train.lua b/src/lua/task/crossing-road-zone-train.lua new file mode 100644 index 0000000..e05dd8d --- /dev/null +++ b/src/lua/task/crossing-road-zone-train.lua @@ -0,0 +1,41 @@ +local blackboard = require("blackboard").singleton() +local action = require("action") +local navigate_to_point = require("task.navigate-to-point") + +--- 训练专用:从当前点导航到公路区起点,再导航到公路区终点。 +--- @param ours_zone boolean +--- @return boolean is_success +return function(ours_zone) + assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") + action:info("开始crossing-road-zone-train") + + local rule = blackboard.rule + local road_begin, road_final + if ours_zone then + road_begin = rule.road_zone_begin.ours + road_final = rule.road_zone_final.ours + else + road_begin = rule.road_zone_begin.them + road_final = rule.road_zone_final.them + end + + local ok = navigate_to_point(road_begin, { + tolerance = 0.1, + timeout = 10, + }) + if not ok then + action:warn("crossing-road-zone-train: 导航到公路区起点失败(超时)") + return false + end + + ok = navigate_to_point(road_final, { + tolerance = 0.1, + timeout = 10, + }) + if not ok then + action:warn("crossing-road-zone-train: 导航到公路区终点失败(超时)") + return false + end + + return true +end From 49c4288c4109451e144660cd4c5da27e89b43125 Mon Sep 17 00:00:00 2001 From: tcd <1369103595@qq.com> Date: Tue, 21 Apr 2026 23:08:04 +0800 Subject: [PATCH 04/12] refactor(lua): simplify cruise entry and rework crossing-road-zone flow --- src/lua/intent/start-cruise.lua | 36 +-------------- src/lua/task/crossing-road-zone.lua | 71 ++++++++++++----------------- 2 files changed, 32 insertions(+), 75 deletions(-) diff --git a/src/lua/intent/start-cruise.lua b/src/lua/intent/start-cruise.lua index fc03b6a..bf1afd6 100644 --- a/src/lua/intent/start-cruise.lua +++ b/src/lua/intent/start-cruise.lua @@ -1,47 +1,15 @@ -local blackboard = require("blackboard").singleton() local action = require("action") -local navigate_to_point = require("task.navigate-to-point") local crossing_road_zone = require("task.crossing-road-zone") --- 开始巡航: ---- 1) 从补给点移动到公路区起点 +--- 1) 从当前位置直接进入公路区跨越流程 --- 2) 正向通过公路区 ---- 3) 完成持续巡航前的机动准备 --- @param ours_zone boolean --- @return boolean is_success return function(ours_zone) assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") - local rule = blackboard.rule - local resupply - local road_begin - if ours_zone then - resupply = rule.resupply_zone.ours - road_begin = rule.road_zone_begin.ours - else - resupply = rule.resupply_zone.them - road_begin = rule.road_zone_begin.them - end - - local ok = navigate_to_point(resupply, { - tolerance = 0.08, - timeout = 10, - }) - if not ok then - action:warn("start-cruise: 导航到补给点失败(超时)") - return false - end - - ok = navigate_to_point(road_begin, { - tolerance = 0.15, - timeout = 10, - }) - if not ok then - action:warn("start-cruise: 导航到公路区起点失败(超时)") - return false - end - - ok = crossing_road_zone(ours_zone, true) + local ok = crossing_road_zone(ours_zone, true) if not ok then action:warn("start-cruise: 通过公路区导航失败") return false diff --git a/src/lua/task/crossing-road-zone.lua b/src/lua/task/crossing-road-zone.lua index 58c54b5..89f9761 100644 --- a/src/lua/task/crossing-road-zone.lua +++ b/src/lua/task/crossing-road-zone.lua @@ -1,69 +1,58 @@ local blackboard = require("blackboard").singleton() -local request = require("util.scheduler").request local action = require("action") +local navigate_to_point = require("task.navigate-to-point") +local crossing_fluctuant_road = require("task.crossing-fluctuant-road") --- @param ours_zone boolean --- @param forward_center boolean --- @return boolean is_success return function(ours_zone, forward_center) + assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") + assert(type(forward_center) == "boolean", "forward_center should be a boolean") action:info("开始crossing-road-zone") - local x = blackboard.user.x - local y = blackboard.user.y - local rule = blackboard.rule - local begin, final + local road_begin, road_final if ours_zone then - begin = rule.road_zone_begin.ours - final = rule.road_zone_final.ours + road_begin = rule.road_zone_begin.ours + road_final = rule.road_zone_final.ours else - begin = rule.road_zone_begin.them - final = rule.road_zone_final.them + road_begin = rule.road_zone_begin.them + road_final = rule.road_zone_final.them end local from, to if forward_center then - from = begin - to = final + from = road_begin + to = road_final else - from = final - to = begin + from = road_final + to = road_begin end - local condition = blackboard.condition - - action:navigate(from) - local is_timeout = request:wait_until { - monitor = function() - return condition.near(from, 0.1) - end, + local ok = navigate_to_point(from, { + tolerance = 0.1, timeout = 10, - } - - if is_timeout then - action:warn(string.format( - "crossing-road-zone: 导航到起点失败 (x=%.2f, y=%.2f)", - from.x, - from.y - )) + }) + if not ok then + action:warn("crossing-road-zone: 导航到公路区入口失败(超时)") return false end - action:navigate(to) - local is_timeout = request:wait_until { - monitor = function() - return condition.near(to, 0.1) - end, + ok = navigate_to_point(to, { + tolerance = 0.1, timeout = 10, - } - if is_timeout then - action:warn(string.format( - "crossing-road-zone: 导航到终点失败 (x=%.2f, y=%.2f)", - to.x, - to.y - )) + }) + if not ok then + action:warn("crossing-road-zone: 导航到公路区出口失败(超时)") + return false + end + + ok = crossing_fluctuant_road(ours_zone, forward_center) + if not ok then + action:warn("crossing-road-zone: 通过起伏路段失败") return false end - return not is_timeout + return true end From b6687e277c611e5aaa9c65bea62a7a922af6ca25 Mon Sep 17 00:00:00 2001 From: tcd <1369103595@qq.com> Date: Tue, 21 Apr 2026 23:08:35 +0800 Subject: [PATCH 05/12] feat(lua): add one-step descent stage before escape-to-home --- src/lua/blackboard.lua | 4 +-- src/lua/intent/escape-to-home.lua | 9 ++++++- src/lua/task/go-down-onestep.lua | 41 +++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 src/lua/task/go-down-onestep.lua diff --git a/src/lua/blackboard.lua b/src/lua/blackboard.lua index b2b3faf..81cbc6c 100644 --- a/src/lua/blackboard.lua +++ b/src/lua/blackboard.lua @@ -57,8 +57,8 @@ local function create_default_blackboard() -- 特殊跨越地形坐标 road_tunnel_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 公路隧道 road_tunnel_final = PointPair { { 0, 0 }, { 0, 0 } }, - one_step_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 一级台阶 - one_step_final = PointPair { { 0, 0 }, { 0, 0 } }, + one_step_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 一级台阶高点 + one_step_final = PointPair { { 0, 0 }, { 0, 0 } }, -- 一级台阶低点 two_step_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 二级台阶 two_step_final = PointPair { { 0, 0 }, { 0, 0 } }, fluctuant_road_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 起伏路段 diff --git a/src/lua/intent/escape-to-home.lua b/src/lua/intent/escape-to-home.lua index 40a9c18..0a8e0a2 100644 --- a/src/lua/intent/escape-to-home.lua +++ b/src/lua/intent/escape-to-home.lua @@ -1,5 +1,6 @@ local blackboard = require("blackboard").singleton() local action = require("action") +local go_down_onestep = require("task.go-down-onestep") local navigate_to_point = require("task.navigate-to-point") --- 回家补给:从当前位置直接导航至补给点。 @@ -16,7 +17,13 @@ return function(ours_zone) resupply_zone = rule.resupply_zone.them end - local is_success = navigate_to_point(resupply_zone, { + local is_success = go_down_onestep(ours_zone) + if not is_success then + action:warn("escape-to-home: 下一级台阶失败") + return false + end + + is_success = navigate_to_point(resupply_zone, { tolerance = 0.15, timeout = 10, }) diff --git a/src/lua/task/go-down-onestep.lua b/src/lua/task/go-down-onestep.lua new file mode 100644 index 0000000..72fa668 --- /dev/null +++ b/src/lua/task/go-down-onestep.lua @@ -0,0 +1,41 @@ +local blackboard = require("blackboard").singleton() +local action = require("action") +local navigate_to_point = require("task.navigate-to-point") + +--- 从当前位置依次经过一级台阶高点与低点。 +--- @param ours_zone boolean +--- @return boolean is_success +return function(ours_zone) + assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") + action:info("开始go-down-onestep") + + local rule = blackboard.rule + local one_step_high, one_step_low + if ours_zone then + one_step_high = rule.one_step_begin.ours + one_step_low = rule.one_step_final.ours + else + one_step_high = rule.one_step_begin.them + one_step_low = rule.one_step_final.them + end + + local ok = navigate_to_point(one_step_high, { + tolerance = 0.1, + timeout = 10, + }) + if not ok then + action:warn("go-down-onestep: 导航到一级台阶高点失败(超时)") + return false + end + + ok = navigate_to_point(one_step_low, { + tolerance = 0.1, + timeout = 10, + }) + if not ok then + action:warn("go-down-onestep: 导航到一级台阶低点失败(超时)") + return false + end + + return true +end From 97910d63fa372d61e6c0ac6e74eaa7f7a41ff121 Mon Sep 17 00:00:00 2001 From: tcd <1369103595@qq.com> Date: Tue, 21 Apr 2026 23:08:47 +0800 Subject: [PATCH 06/12] feat(lua): add decision test endpoint and harden train-decision FSM --- src/lua/endpoint/test.lua | 346 ++++++++++++++++++++++++++++ src/lua/endpoint/train-decision.lua | 109 ++++++--- 2 files changed, 417 insertions(+), 38 deletions(-) create mode 100644 src/lua/endpoint/test.lua diff --git a/src/lua/endpoint/test.lua b/src/lua/endpoint/test.lua new file mode 100644 index 0000000..226de38 --- /dev/null +++ b/src/lua/endpoint/test.lua @@ -0,0 +1,346 @@ +--- +--- Local Context +--- + +local action = require("action") +local ascii = require("util.ascii_art") +local clock = require("util.clock") +local fsm = require("util.fsm") +local option = require("option") + +local start_cruise = require("intent.start-cruise") +local keep_cruise = require("intent.keep-cruise") +local escape_to_home = require("intent.escape-to-home") + +local Scheduler = require("util.scheduler") +local scheduler = Scheduler.new() +local request = Scheduler.request + +local edges = require("util.edge").new() + +--- +--- Export Context +--- + +blackboard = require("blackboard").singleton() + +local runtime = { + ours_zone = true, + switch_interval = 2.0, + current_state = "idle", + navigation_ready = false, + escape_retry_delay = 20.0, + escape_retry_deadline = nil, +} + +local requests = { + start = false, +} + +local job = { + handle = nil, + name = nil, + done = false, + success = false, +} + +local function read_option(name, fallback) + local value = rawget(option, name) + if value == nil then + return fallback + end + return value +end + +local function configure_test_rule() + local rule = blackboard.rule + + rule.health_limit = read_option("fsm_health_limit", 210) + rule.health_ready = read_option("fsm_health_ready", 400) + rule.bullet_limit = read_option("fsm_bullet_limit", 40) + rule.bullet_ready = read_option("fsm_bullet_ready", 300) + + -- 坐标点位由地图/配置提供,这里不再覆写为 (0,0)。 +end + +local function reset_job_status() + job.done = false + job.success = false +end + +local function cancel_job() + if job.handle ~= nil then + job.handle.cancel() + job.handle = nil + end + job.name = nil + reset_job_status() +end + +local function run_job(name, fn) + cancel_job() + job.name = name + reset_job_status() + + job.handle = scheduler:append_task(function() + local ok, result = xpcall(fn, debug.traceback) + job.handle = nil + job.name = nil + job.done = true + + if not ok then + job.success = false + action:fuck(string.format("fsm job '%s' failed:\n%s", name, result)) + return + end + + job.success = (result ~= false) + if not job.success then + action:warn(string.format("fsm job '%s' finished with false", name)) + end + end) +end + +local function take_request(name) + local value = requests[name] + requests[name] = false + return value +end + +local function set_state(name) + runtime.current_state = name + action:info("fsm state -> " .. name) +end + +local function arm_escape_retry(reason) + runtime.escape_retry_deadline = clock:now() + runtime.escape_retry_delay + action:warn(string.format( + "%s,%.1f 秒后重试 escape-to-home", + reason, + runtime.escape_retry_delay + )) +end + +local function start_navigation() + local ok, message = action:restart_navigation({ + global_map = read_option("global_map", "rmuc"), + launch_livox = read_option("launch_livox", true), + launch_odin1 = read_option("launch_odin1", false), + use_sim_time = read_option("use_sim_time", false), + }) + if not ok then + action:fuck("restart_navigation 触发失败: " .. tostring(message)) + end + + return ok, message +end + +local function setup_edges() + edges:on(blackboard.getter.rswitch, "UP", function() + requests.start = true + end) +end + +local function create_intent_fsm() + local State = { + idle = "idle", + start_cruise = "start_cruise", + keep_cruise = "keep_cruise", + escape = "escape", + wait_escape = "wait_escape", + recover = "recover", + } + + local condition = blackboard.condition + local intent_fsm = fsm:new(State.idle) + + intent_fsm:use({ + state = State.idle, + enter = function() + cancel_job() + set_state(State.idle) + runtime.navigation_ready = false + runtime.escape_retry_deadline = nil + end, + event = function(handle) + if take_request("start") then + local ok = start_navigation() + runtime.navigation_ready = ok + end + + if runtime.navigation_ready and blackboard.game.stage == "STARTED" then + handle:set_next(State.start_cruise) + end + end, + }) + + intent_fsm:use({ + state = State.start_cruise, + enter = function() + set_state(State.start_cruise) + run_job("start_cruise", function() + return start_cruise(runtime.ours_zone) + end) + end, + event = function(handle) + if condition.low_health() or condition.low_bullet() then + cancel_job() + handle:set_next(State.escape) + return + end + + if job.done then + if job.success then + handle:set_next(State.keep_cruise) + else + arm_escape_retry("fsm(start_cruise): 导航失败") + handle:set_next(State.wait_escape) + end + end + end, + }) + + intent_fsm:use({ + state = State.keep_cruise, + enter = function() + set_state(State.keep_cruise) + run_job("keep_cruise", function() + return keep_cruise(runtime.ours_zone, runtime.switch_interval) + end) + end, + event = function(handle) + if condition.low_health() or condition.low_bullet() then + cancel_job() + handle:set_next(State.escape) + return + end + + if job.done and not job.success then + arm_escape_retry("fsm(keep_cruise): 导航失败") + handle:set_next(State.wait_escape) + end + end, + }) + + intent_fsm:use({ + state = State.escape, + enter = function() + set_state(State.escape) + run_job("escape_to_home", function() + return escape_to_home(runtime.ours_zone) + end) + end, + event = function(handle) + if job.done then + if job.success then + handle:set_next(State.recover) + else + arm_escape_retry("fsm(escape): 导航失败") + handle:set_next(State.wait_escape) + end + end + end, + }) + + intent_fsm:use({ + state = State.wait_escape, + enter = function() + cancel_job() + set_state(State.wait_escape) + if runtime.escape_retry_deadline == nil then + runtime.escape_retry_deadline = clock:now() + runtime.escape_retry_delay + end + end, + event = function(handle) + if clock:now() >= runtime.escape_retry_deadline then + runtime.escape_retry_deadline = nil + handle:set_next(State.escape) + end + end, + }) + + intent_fsm:use({ + state = State.recover, + enter = function() + cancel_job() + set_state(State.recover) + runtime.escape_retry_deadline = nil + end, + event = function(handle) + if condition.low_health() or condition.low_bullet() then + return + end + + if condition.health_ready() and condition.bullet_ready() then + handle:set_next(State.start_cruise) + end + end, + }) + + assert(intent_fsm:init_ready(State), "intent fsm init_ready failed") + return intent_fsm +end + +on_init = function() + clock:reset(blackboard.meta.timestamp) + + option:set_handler(function(error) + action:warn("while fetch option: " .. error) + end) + + runtime.ours_zone = read_option("fsm_ours_zone", true) + runtime.switch_interval = read_option("fsm_switch_interval", 2.0) + runtime.escape_retry_delay = read_option("fsm_escape_retry_delay", 20.0) + assert(type(runtime.escape_retry_delay) == "number", "fsm_escape_retry_delay should be a number") + assert(runtime.escape_retry_delay >= 0, "fsm_escape_retry_delay should be non-negative") + + configure_test_rule() + setup_edges() + + if read_option("enable_goal_topic_forward", false) then + action:switch_topic_forward(true) + end + action:bind(scheduler) + + local intent_fsm = create_intent_fsm() + scheduler:append_task(function() + while true do + intent_fsm:spin_once() + request:yield() + end + end) + + scheduler:append_task(function() + while true do + request:sleep(1.0) + action:info(string.format( + "fsm=%s stage=%s hp=%s bullet=%s rs=%s ls=%s", + runtime.current_state, + blackboard.game.stage, + tostring(blackboard.user.health), + tostring(blackboard.user.bullet), + blackboard.play.rswitch, + blackboard.play.lswitch + )) + end + end) + + action:info(ascii.banner) + action:warn("FSM test endpoint loaded") +end + +on_tick = function() + clock:update(blackboard.meta.timestamp) + edges:spin() + scheduler:spin_once() +end + +on_exit = function() + cancel_job() + action:stop_navigation() +end + +--- Callback for velocity topic from Nav2. +on_control = function(vx, vy, _) + action:update_chassis_vel(vx, vy) +end diff --git a/src/lua/endpoint/train-decision.lua b/src/lua/endpoint/train-decision.lua index 862dd50..9630e34 100644 --- a/src/lua/endpoint/train-decision.lua +++ b/src/lua/endpoint/train-decision.lua @@ -8,7 +8,7 @@ local clock = require("util.clock") local fsm = require("util.fsm") local option = require("option") -local start_cruise = require("intent.start-cruise") +local start_cruise = require("intent.start-cruise-train") local keep_cruise = require("intent.keep-cruise") local escape_to_home = require("intent.escape-to-home") @@ -28,6 +28,9 @@ local runtime = { ours_zone = true, switch_interval = 2.0, current_state = "idle", + navigation_ready = false, + escape_retry_delay = 20.0, + escape_retry_deadline = nil, } local requests = { @@ -58,20 +61,13 @@ local function configure_test_rule() rule.bullet_ready = read_option("fsm_bullet_ready", 300) -- Ours side sample points - rule.resupply_zone.ours = { x = 0.0, y = 0.0 } - rule.road_zone_begin.ours = { x = 0.0, y = 0.0 } - rule.road_zone_final.ours = { x = 0.0, y = 0.0 } - rule.fortress.ours = { x = 0.0, y = 0.0 } - rule.central_highland_near_crossing_road.ours = { x = 0.0, y = 0.0 } - rule.central_highland_near_doghole.ours = { x = 0.0, y = 0.0 } - - -- Them side sample points - rule.resupply_zone.them = { x = 0.0, y = 0.0 } - rule.road_zone_begin.them = { x = 0.0, y = 0.0 } - rule.road_zone_final.them = { x = 0.0, y = 0.0 } - rule.fortress.them = { x = 0.0, y = 0.0 } - rule.central_highland_near_crossing_road.them = { x = 0.0, y = 0.0 } - rule.central_highland_near_doghole.them = { x = 0.0, y = 0.0 } + rule.resupply_zone.ours = { x = 0.0, y = 0.0 } --家 + rule.road_zone_begin.ours = { x = 0.0, y = 0.0 } --公路区起点 + rule.road_zone_final.ours = { x = 0.0, y = 0.0 } --公路区终点 + rule.one_step_begin.ours = { x = 0.0, y = 0.0 } --一级台阶高点(先随便标个回家路上的点) + rule.one_step_final.ours = { x = 0.0, y = 0.0 } --一级台阶低点(先随便标个回家路上的点) + rule.central_highland_near_crossing_road.ours = { x = 0.0, y = 0.0 } --高地靠近公路 + rule.central_highland_near_doghole.ours = { x = 0.0, y = 0.0 } --高地靠近狗洞 end local function reset_job_status() @@ -123,6 +119,15 @@ local function set_state(name) action:info("fsm state -> " .. name) end +local function arm_escape_retry(reason) + runtime.escape_retry_deadline = clock:now() + runtime.escape_retry_delay + action:warn(string.format( + "%s,%.1f 秒后重试 escape-to-home", + reason, + runtime.escape_retry_delay + )) +end + local function start_navigation() local ok, message = action:restart_navigation({ global_map = read_option("global_map", "rmuc"), @@ -149,6 +154,7 @@ local function create_intent_fsm() start_cruise = "start_cruise", keep_cruise = "keep_cruise", escape = "escape", + wait_escape = "wait_escape", recover = "recover", } @@ -160,13 +166,16 @@ local function create_intent_fsm() enter = function() cancel_job() set_state(State.idle) + runtime.navigation_ready = false + runtime.escape_retry_deadline = nil end, event = function(handle) if take_request("start") then - start_navigation() + local ok = start_navigation() + runtime.navigation_ready = ok end - if blackboard.game.stage == "STARTED" then + if runtime.navigation_ready and blackboard.game.stage == "STARTED" then handle:set_next(State.start_cruise) end end, @@ -187,15 +196,16 @@ local function create_intent_fsm() return end - if job.done then - if job.success then - handle:set_next(State.keep_cruise) - else - action:warn("fsm(start_cruise): 导航失败") + if job.done then + if job.success then + handle:set_next(State.keep_cruise) + else + arm_escape_retry("fsm(start_cruise): 导航失败") + handle:set_next(State.wait_escape) + end end - end - end, - }) + end, + }) intent_fsm:use({ state = State.keep_cruise, @@ -212,11 +222,12 @@ local function create_intent_fsm() return end - if job.done and not job.success then - action:warn("fsm(keep_cruise): 导航失败") - end - end, - }) + if job.done and not job.success then + arm_escape_retry("fsm(keep_cruise): 导航失败") + handle:set_next(State.wait_escape) + end + end, + }) intent_fsm:use({ state = State.escape, @@ -227,21 +238,40 @@ local function create_intent_fsm() end) end, event = function(handle) - if job.done then - if job.success then - handle:set_next(State.recover) - else - action:warn("fsm(escape): 导航失败") + if job.done then + if job.success then + handle:set_next(State.recover) + else + arm_escape_retry("fsm(escape): 导航失败") + handle:set_next(State.wait_escape) + end end - end - end, - }) + end, + }) + + intent_fsm:use({ + state = State.wait_escape, + enter = function() + cancel_job() + set_state(State.wait_escape) + if runtime.escape_retry_deadline == nil then + runtime.escape_retry_deadline = clock:now() + runtime.escape_retry_delay + end + end, + event = function(handle) + if clock:now() >= runtime.escape_retry_deadline then + runtime.escape_retry_deadline = nil + handle:set_next(State.escape) + end + end, + }) intent_fsm:use({ state = State.recover, enter = function() cancel_job() set_state(State.recover) + runtime.escape_retry_deadline = nil end, event = function(handle) if condition.low_health() or condition.low_bullet() then @@ -267,6 +297,9 @@ on_init = function() runtime.ours_zone = read_option("fsm_ours_zone", true) runtime.switch_interval = read_option("fsm_switch_interval", 2.0) + runtime.escape_retry_delay = read_option("fsm_escape_retry_delay", 20.0) + assert(type(runtime.escape_retry_delay) == "number", "fsm_escape_retry_delay should be a number") + assert(runtime.escape_retry_delay >= 0, "fsm_escape_retry_delay should be non-negative") configure_test_rule() setup_edges() From c246349f173a5e3759659fccba03a1cad5bc8903 Mon Sep 17 00:00:00 2001 From: tcd <1369103595@qq.com> Date: Thu, 23 Apr 2026 16:41:07 +0800 Subject: [PATCH 07/12] feat(lua): add navigation history queue and improve escape-to-home routing --- src/lua/blackboard.lua | 40 +++++++++++++ src/lua/endpoint/test.lua | 1 + src/lua/endpoint/train-decision.lua | 1 + src/lua/intent/escape-to-home.lua | 63 ++++++++++++++++++-- src/lua/task/crossing-fluctuant-road.lua | 34 ++++------- src/lua/task/crossing-road-zone-train.lua | 4 +- src/lua/task/crossing-road-zone.lua | 16 ++--- src/lua/task/cruise-in-central-highlands.lua | 13 ++-- src/lua/task/go-down-onestep.lua | 4 +- src/lua/task/navigate-to-point.lua | 1 + 10 files changed, 129 insertions(+), 48 deletions(-) diff --git a/src/lua/blackboard.lua b/src/lua/blackboard.lua index 81cbc6c..00dbd2a 100644 --- a/src/lua/blackboard.lua +++ b/src/lua/blackboard.lua @@ -25,6 +25,9 @@ local function create_default_blackboard() }, meta = { timestamp = 0, -- 秒 + fsm_state = "unknown", + navigate_point_queue = {}, + navigate_point_queue_max = 200, }, -- Static Information @@ -74,6 +77,43 @@ local function create_default_blackboard() end, } + --- @param point {x: number, y: number} + --- @param tag? string + function result.enqueue_navigate_point(point, tag) + assert(type(point) == "table", "point should be a table") + assert(type(point.x) == "number", "point.x should be a number") + assert(type(point.y) == "number", "point.y should be a number") + + local queue = result.meta.navigate_point_queue + if type(queue) ~= "table" then + queue = {} + result.meta.navigate_point_queue = queue + end + + local max = tonumber(result.meta.navigate_point_queue_max) or 200 + if max < 1 then + max = 1 + end + + queue[#queue + 1] = { + ts = result.meta.timestamp, + x = point.x, + y = point.y, + hp = result.user.health, + bullet = result.user.bullet, + stage = result.game.stage, + state = result.meta.fsm_state, + tag = tag, + } + + while #queue > max do + table.remove(queue, 1) + end + end + + -- Backward-compatible alias. + result.push_navigate_point = result.enqueue_navigate_point + result.condition = { low_health = function() return result.user.health < result.rule.health_limit diff --git a/src/lua/endpoint/test.lua b/src/lua/endpoint/test.lua index 226de38..53635b4 100644 --- a/src/lua/endpoint/test.lua +++ b/src/lua/endpoint/test.lua @@ -109,6 +109,7 @@ end local function set_state(name) runtime.current_state = name + blackboard.meta.fsm_state = name action:info("fsm state -> " .. name) end diff --git a/src/lua/endpoint/train-decision.lua b/src/lua/endpoint/train-decision.lua index 9630e34..3afbbcd 100644 --- a/src/lua/endpoint/train-decision.lua +++ b/src/lua/endpoint/train-decision.lua @@ -116,6 +116,7 @@ end local function set_state(name) runtime.current_state = name + blackboard.meta.fsm_state = name action:info("fsm state -> " .. name) end diff --git a/src/lua/intent/escape-to-home.lua b/src/lua/intent/escape-to-home.lua index 0a8e0a2..4d13af8 100644 --- a/src/lua/intent/escape-to-home.lua +++ b/src/lua/intent/escape-to-home.lua @@ -3,7 +3,51 @@ local action = require("action") local go_down_onestep = require("task.go-down-onestep") local navigate_to_point = require("task.navigate-to-point") ---- 回家补给:从当前位置直接导航至补给点。 +local function is_same_point(a, b) + return a.x == b.x and a.y == b.y +end + +local function decide_escape_route(ours_zone) + local rule = blackboard.rule + local exit_point + local resupply_point + if ours_zone then + exit_point = rule.fluctuant_road_final.ours + resupply_point = rule.resupply_zone.ours + else + exit_point = rule.fluctuant_road_final.them + resupply_point = rule.resupply_zone.them + end + + local queue = blackboard.meta.navigate_point_queue + if type(queue) ~= "table" then + return "direct" + end + + -- 从队尾(最近一次)开始回溯查询。 + for index = #queue, 1, -1 do + local item = queue[index] + if type(item) == "table" and type(item.x) == "number" and type(item.y) == "number" then + -- 1. 先遇到补给点:直接回补给点 + if is_same_point(item, resupply_point) then + return "direct" + end + + -- 2. 先遇到起伏路段出口:先下台阶再回补给点 + if is_same_point(item, exit_point) then + return "onestep" + end + end + end + + -- 3. 全队列都没遇到起伏路段出口或补给点:直接回补给点 + return "direct" +end + +--- 回家补给: +--- - 先遇到补给点 resupply_zone:直接从当前位置回补给点。 +--- - 先遇到起伏路段出口 fluctuant_road_final:先下一级台阶,再回补给点。 +--- - 全队列都没遇到起伏路段出口或补给点:直接从当前位置回补给点。 --- @param ours_zone boolean --- @return boolean is_success return function(ours_zone) @@ -17,10 +61,17 @@ return function(ours_zone) resupply_zone = rule.resupply_zone.them end - local is_success = go_down_onestep(ours_zone) - if not is_success then - action:warn("escape-to-home: 下一级台阶失败") - return false + local route = decide_escape_route(ours_zone) + local is_success = true + if route == "onestep" then + action:info("escape-to-home: 历史队列先命中起伏路段出口,按原有逻辑回家") + is_success = go_down_onestep(ours_zone) + if not is_success then + action:warn("escape-to-home: 下一级台阶失败") + return false + end + else + action:info("escape-to-home: 历史队列先命中补给点或未命中关键点,直接从当前位置回家") end is_success = navigate_to_point(resupply_zone, { @@ -28,7 +79,7 @@ return function(ours_zone) timeout = 10, }) if not is_success then - action:warn("escape-to-home: 导航到补给点失败(超时)") + action:warn("escape-to-home: 导航到补给点失败") return false end diff --git a/src/lua/task/crossing-fluctuant-road.lua b/src/lua/task/crossing-fluctuant-road.lua index 80fdd7a..7cfc63a 100644 --- a/src/lua/task/crossing-fluctuant-road.lua +++ b/src/lua/task/crossing-fluctuant-road.lua @@ -1,16 +1,15 @@ local blackboard = require("blackboard").singleton() -local request = require("util.scheduler").request local action = require("action") +local navigate_to_point = require("task.navigate-to-point") --- @param ours_zone boolean --- @param forward_center boolean --- @return boolean is_success return function(ours_zone, forward_center) + assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") + assert(type(forward_center) == "boolean", "forward_center should be a boolean") action:info("开始crossing-fluctuant-road") - local x = blackboard.user.x - local y = blackboard.user.y - local rule = blackboard.rule local begin, final if ours_zone then @@ -30,17 +29,11 @@ return function(ours_zone, forward_center) to = begin end - local condition = blackboard.condition - - action:navigate(from) - local is_timeout = request:wait_until { - monitor = function() - return condition.near(from, 0.1) - end, + local ok = navigate_to_point(from, { + tolerance = 0.1, timeout = 10, - } - - if is_timeout then + }) + if not ok then action:warn(string.format( "crossing-fluctuant-road: 导航到起点失败 (x=%.2f, y=%.2f)", from.x, @@ -49,14 +42,11 @@ return function(ours_zone, forward_center) return false end - action:navigate(to) - local is_timeout = request:wait_until { - monitor = function() - return condition.near(to, 0.1) - end, + ok = navigate_to_point(to, { + tolerance = 0.1, timeout = 10, - } - if is_timeout then + }) + if not ok then action:warn(string.format( "crossing-fluctuant-road: 导航到终点失败 (x=%.2f, y=%.2f)", to.x, @@ -65,5 +55,5 @@ return function(ours_zone, forward_center) return false end - return not is_timeout + return true end diff --git a/src/lua/task/crossing-road-zone-train.lua b/src/lua/task/crossing-road-zone-train.lua index e05dd8d..b7f79a1 100644 --- a/src/lua/task/crossing-road-zone-train.lua +++ b/src/lua/task/crossing-road-zone-train.lua @@ -24,7 +24,7 @@ return function(ours_zone) timeout = 10, }) if not ok then - action:warn("crossing-road-zone-train: 导航到公路区起点失败(超时)") + action:warn("crossing-road-zone-train: 导航到公路区起点失败") return false end @@ -33,7 +33,7 @@ return function(ours_zone) timeout = 10, }) if not ok then - action:warn("crossing-road-zone-train: 导航到公路区终点失败(超时)") + action:warn("crossing-road-zone-train: 导航到公路区终点失败") return false end diff --git a/src/lua/task/crossing-road-zone.lua b/src/lua/task/crossing-road-zone.lua index 89f9761..20061db 100644 --- a/src/lua/task/crossing-road-zone.lua +++ b/src/lua/task/crossing-road-zone.lua @@ -35,22 +35,22 @@ return function(ours_zone, forward_center) timeout = 10, }) if not ok then - action:warn("crossing-road-zone: 导航到公路区入口失败(超时)") + action:warn("crossing-road-zone: 导航到公路区入口失败") return false end - ok = navigate_to_point(to, { - tolerance = 0.1, - timeout = 10, - }) + ok = crossing_fluctuant_road(ours_zone, forward_center) if not ok then - action:warn("crossing-road-zone: 导航到公路区出口失败(超时)") + action:warn("crossing-road-zone: 通过起伏路段失败") return false end - ok = crossing_fluctuant_road(ours_zone, forward_center) + ok = navigate_to_point(to, { + tolerance = 0.1, + timeout = 10, + }) if not ok then - action:warn("crossing-road-zone: 通过起伏路段失败") + action:warn("crossing-road-zone: 导航到公路区出口失败") return false end diff --git a/src/lua/task/cruise-in-central-highlands.lua b/src/lua/task/cruise-in-central-highlands.lua index 8c1d8e4..abf9b37 100644 --- a/src/lua/task/cruise-in-central-highlands.lua +++ b/src/lua/task/cruise-in-central-highlands.lua @@ -2,6 +2,7 @@ local blackboard = require("blackboard").singleton() local clock = require("util.clock") local request = require("util.scheduler").request local action = require("action") +local navigate_to_point = require("task.navigate-to-point") local function distance_to(target) local dx = target.x - blackboard.user.x @@ -19,7 +20,6 @@ return function(ours_zone, switch_interval) action:info("开始cruise-in-central-highlands") local rule = blackboard.rule - local condition = blackboard.condition local navigation_timeout = math.max(10.0, switch_interval * 2.0) local near_crossing_road, near_doghole if ours_zone then @@ -36,14 +36,11 @@ return function(ours_zone, switch_interval) while true do local phase_start = clock:now() - action:navigate(target) - local is_timeout = request:wait_until { - monitor = function() - return condition.near(target, 0.15) - end, + local ok = navigate_to_point(target, { + tolerance = 0.15, timeout = navigation_timeout, - } - if is_timeout then + }) + if not ok then action:warn(string.format( "cruise-in-central-highlands: 导航到巡航点失败 (x=%.2f, y=%.2f, timeout=%.2fs)", target.x, diff --git a/src/lua/task/go-down-onestep.lua b/src/lua/task/go-down-onestep.lua index 72fa668..0bf5005 100644 --- a/src/lua/task/go-down-onestep.lua +++ b/src/lua/task/go-down-onestep.lua @@ -24,7 +24,7 @@ return function(ours_zone) timeout = 10, }) if not ok then - action:warn("go-down-onestep: 导航到一级台阶高点失败(超时)") + action:warn("go-down-onestep: 导航到一级台阶高点失败") return false end @@ -33,7 +33,7 @@ return function(ours_zone) timeout = 10, }) if not ok then - action:warn("go-down-onestep: 导航到一级台阶低点失败(超时)") + action:warn("go-down-onestep: 导航到一级台阶低点失败") return false end diff --git a/src/lua/task/navigate-to-point.lua b/src/lua/task/navigate-to-point.lua index ac073e1..29e7055 100644 --- a/src/lua/task/navigate-to-point.lua +++ b/src/lua/task/navigate-to-point.lua @@ -40,6 +40,7 @@ return function(point, options) local tolerance, timeout = normalize_options(options) local condition = blackboard.condition + blackboard.enqueue_navigate_point(point, "navigate-to-point") action:navigate(point) local is_timeout = request:wait_until { monitor = function() From e66bd5fc6fd8074b70a6a496cfbff93513f98514 Mon Sep 17 00:00:00 2001 From: tcd <1369103595@qq.com> Date: Thu, 23 Apr 2026 20:32:30 +0800 Subject: [PATCH 08/12] refactor(lua): unify endpoint FSM retry flow, remove wait logic, and clear nav history on STARTED --- src/lua/endpoint/test.lua | 130 +++++++++++++-------------- src/lua/endpoint/train-decision.lua | 132 ++++++++++++++-------------- 2 files changed, 131 insertions(+), 131 deletions(-) diff --git a/src/lua/endpoint/test.lua b/src/lua/endpoint/test.lua index 53635b4..32ec287 100644 --- a/src/lua/endpoint/test.lua +++ b/src/lua/endpoint/test.lua @@ -29,8 +29,6 @@ local runtime = { switch_interval = 2.0, current_state = "idle", navigation_ready = false, - escape_retry_delay = 20.0, - escape_retry_deadline = nil, } local requests = { @@ -113,15 +111,6 @@ local function set_state(name) action:info("fsm state -> " .. name) end -local function arm_escape_retry(reason) - runtime.escape_retry_deadline = clock:now() + runtime.escape_retry_delay - action:warn(string.format( - "%s,%.1f 秒后重试 escape-to-home", - reason, - runtime.escape_retry_delay - )) -end - local function start_navigation() local ok, message = action:restart_navigation({ global_map = read_option("global_map", "rmuc"), @@ -142,18 +131,44 @@ local function setup_edges() end) end +local function clear_navigate_history() + local queue = blackboard.meta.navigate_point_queue + if type(queue) ~= "table" then + blackboard.meta.navigate_point_queue = {} + return + end + + for i = #queue, 1, -1 do + queue[i] = nil + end +end + local function create_intent_fsm() local State = { idle = "idle", start_cruise = "start_cruise", keep_cruise = "keep_cruise", escape = "escape", - wait_escape = "wait_escape", recover = "recover", } local condition = blackboard.condition local intent_fsm = fsm:new(State.idle) + local function restart_start_cruise_job() + run_job("start_cruise", function() + return start_cruise(runtime.ours_zone) + end) + end + local function restart_keep_cruise_job() + run_job("keep_cruise", function() + return keep_cruise(runtime.ours_zone, runtime.switch_interval) + end) + end + local function restart_escape_job() + run_job("escape_to_home", function() + return escape_to_home(runtime.ours_zone) + end) + end intent_fsm:use({ state = State.idle, @@ -161,7 +176,6 @@ local function create_intent_fsm() cancel_job() set_state(State.idle) runtime.navigation_ready = false - runtime.escape_retry_deadline = nil end, event = function(handle) if take_request("start") then @@ -170,6 +184,7 @@ local function create_intent_fsm() end if runtime.navigation_ready and blackboard.game.stage == "STARTED" then + clear_navigate_history() handle:set_next(State.start_cruise) end end, @@ -179,9 +194,7 @@ local function create_intent_fsm() state = State.start_cruise, enter = function() set_state(State.start_cruise) - run_job("start_cruise", function() - return start_cruise(runtime.ours_zone) - end) + restart_start_cruise_job() end, event = function(handle) if condition.low_health() or condition.low_bullet() then @@ -190,24 +203,25 @@ local function create_intent_fsm() return end - if job.done then - if job.success then - handle:set_next(State.keep_cruise) - else - arm_escape_retry("fsm(start_cruise): 导航失败") - handle:set_next(State.wait_escape) - end - end - end, - }) + if not job.done then + return + end + + if job.success then + handle:set_next(State.keep_cruise) + return + end + + action:warn("fsm(start_cruise): 导航失败,重试当前状态") + restart_start_cruise_job() + end, + }) intent_fsm:use({ state = State.keep_cruise, enter = function() set_state(State.keep_cruise) - run_job("keep_cruise", function() - return keep_cruise(runtime.ours_zone, runtime.switch_interval) - end) + restart_keep_cruise_job() end, event = function(handle) if condition.low_health() or condition.low_bullet() then @@ -216,47 +230,37 @@ local function create_intent_fsm() return end - if job.done and not job.success then - arm_escape_retry("fsm(keep_cruise): 导航失败") - handle:set_next(State.wait_escape) - end - end, - }) + if not job.done then + return + end + + if job.success then + return + end + + action:warn("fsm(keep_cruise): 导航失败,重试当前状态") + restart_keep_cruise_job() + end, + }) intent_fsm:use({ state = State.escape, enter = function() set_state(State.escape) - run_job("escape_to_home", function() - return escape_to_home(runtime.ours_zone) - end) + restart_escape_job() end, event = function(handle) - if job.done then - if job.success then - handle:set_next(State.recover) - else - arm_escape_retry("fsm(escape): 导航失败") - handle:set_next(State.wait_escape) - end - end - end, - }) - - intent_fsm:use({ - state = State.wait_escape, - enter = function() - cancel_job() - set_state(State.wait_escape) - if runtime.escape_retry_deadline == nil then - runtime.escape_retry_deadline = clock:now() + runtime.escape_retry_delay + if not job.done then + return end - end, - event = function(handle) - if clock:now() >= runtime.escape_retry_deadline then - runtime.escape_retry_deadline = nil - handle:set_next(State.escape) + + if job.success then + handle:set_next(State.recover) + return end + + action:warn("fsm(escape): 导航失败,重试当前状态") + restart_escape_job() end, }) @@ -265,7 +269,6 @@ local function create_intent_fsm() enter = function() cancel_job() set_state(State.recover) - runtime.escape_retry_deadline = nil end, event = function(handle) if condition.low_health() or condition.low_bullet() then @@ -291,9 +294,6 @@ on_init = function() runtime.ours_zone = read_option("fsm_ours_zone", true) runtime.switch_interval = read_option("fsm_switch_interval", 2.0) - runtime.escape_retry_delay = read_option("fsm_escape_retry_delay", 20.0) - assert(type(runtime.escape_retry_delay) == "number", "fsm_escape_retry_delay should be a number") - assert(runtime.escape_retry_delay >= 0, "fsm_escape_retry_delay should be non-negative") configure_test_rule() setup_edges() diff --git a/src/lua/endpoint/train-decision.lua b/src/lua/endpoint/train-decision.lua index 3afbbcd..9b031be 100644 --- a/src/lua/endpoint/train-decision.lua +++ b/src/lua/endpoint/train-decision.lua @@ -29,8 +29,6 @@ local runtime = { switch_interval = 2.0, current_state = "idle", navigation_ready = false, - escape_retry_delay = 20.0, - escape_retry_deadline = nil, } local requests = { @@ -120,18 +118,9 @@ local function set_state(name) action:info("fsm state -> " .. name) end -local function arm_escape_retry(reason) - runtime.escape_retry_deadline = clock:now() + runtime.escape_retry_delay - action:warn(string.format( - "%s,%.1f 秒后重试 escape-to-home", - reason, - runtime.escape_retry_delay - )) -end - local function start_navigation() local ok, message = action:restart_navigation({ - global_map = read_option("global_map", "rmuc"), + global_map = read_option("global_map", "train_map"), launch_livox = read_option("launch_livox", true), launch_odin1 = read_option("launch_odin1", false), use_sim_time = read_option("use_sim_time", false), @@ -149,18 +138,44 @@ local function setup_edges() end) end +local function clear_navigate_history() + local queue = blackboard.meta.navigate_point_queue + if type(queue) ~= "table" then + blackboard.meta.navigate_point_queue = {} + return + end + + for i = #queue, 1, -1 do + queue[i] = nil + end +end + local function create_intent_fsm() local State = { idle = "idle", start_cruise = "start_cruise", keep_cruise = "keep_cruise", escape = "escape", - wait_escape = "wait_escape", recover = "recover", } local condition = blackboard.condition local intent_fsm = fsm:new(State.idle) + local function restart_start_cruise_job() + run_job("start_cruise", function() + return start_cruise(runtime.ours_zone) + end) + end + local function restart_keep_cruise_job() + run_job("keep_cruise", function() + return keep_cruise(runtime.ours_zone, runtime.switch_interval) + end) + end + local function restart_escape_job() + run_job("escape_to_home", function() + return escape_to_home(runtime.ours_zone) + end) + end intent_fsm:use({ state = State.idle, @@ -168,7 +183,6 @@ local function create_intent_fsm() cancel_job() set_state(State.idle) runtime.navigation_ready = false - runtime.escape_retry_deadline = nil end, event = function(handle) if take_request("start") then @@ -177,6 +191,7 @@ local function create_intent_fsm() end if runtime.navigation_ready and blackboard.game.stage == "STARTED" then + clear_navigate_history() handle:set_next(State.start_cruise) end end, @@ -186,9 +201,7 @@ local function create_intent_fsm() state = State.start_cruise, enter = function() set_state(State.start_cruise) - run_job("start_cruise", function() - return start_cruise(runtime.ours_zone) - end) + restart_start_cruise_job() end, event = function(handle) if condition.low_health() or condition.low_bullet() then @@ -197,24 +210,25 @@ local function create_intent_fsm() return end - if job.done then - if job.success then - handle:set_next(State.keep_cruise) - else - arm_escape_retry("fsm(start_cruise): 导航失败") - handle:set_next(State.wait_escape) - end - end - end, - }) + if not job.done then + return + end + + if job.success then + handle:set_next(State.keep_cruise) + return + end + + action:warn("fsm(start_cruise): 导航失败,重试当前状态") + restart_start_cruise_job() + end, + }) intent_fsm:use({ state = State.keep_cruise, enter = function() set_state(State.keep_cruise) - run_job("keep_cruise", function() - return keep_cruise(runtime.ours_zone, runtime.switch_interval) - end) + restart_keep_cruise_job() end, event = function(handle) if condition.low_health() or condition.low_bullet() then @@ -223,47 +237,37 @@ local function create_intent_fsm() return end - if job.done and not job.success then - arm_escape_retry("fsm(keep_cruise): 导航失败") - handle:set_next(State.wait_escape) - end - end, - }) + if not job.done then + return + end + + if job.success then + return + end + + action:warn("fsm(keep_cruise): 导航失败,重试当前状态") + restart_keep_cruise_job() + end, + }) intent_fsm:use({ state = State.escape, enter = function() set_state(State.escape) - run_job("escape_to_home", function() - return escape_to_home(runtime.ours_zone) - end) + restart_escape_job() end, event = function(handle) - if job.done then - if job.success then - handle:set_next(State.recover) - else - arm_escape_retry("fsm(escape): 导航失败") - handle:set_next(State.wait_escape) - end - end - end, - }) - - intent_fsm:use({ - state = State.wait_escape, - enter = function() - cancel_job() - set_state(State.wait_escape) - if runtime.escape_retry_deadline == nil then - runtime.escape_retry_deadline = clock:now() + runtime.escape_retry_delay + if not job.done then + return end - end, - event = function(handle) - if clock:now() >= runtime.escape_retry_deadline then - runtime.escape_retry_deadline = nil - handle:set_next(State.escape) + + if job.success then + handle:set_next(State.recover) + return end + + action:warn("fsm(escape): 导航失败,重试当前状态") + restart_escape_job() end, }) @@ -272,7 +276,6 @@ local function create_intent_fsm() enter = function() cancel_job() set_state(State.recover) - runtime.escape_retry_deadline = nil end, event = function(handle) if condition.low_health() or condition.low_bullet() then @@ -298,9 +301,6 @@ on_init = function() runtime.ours_zone = read_option("fsm_ours_zone", true) runtime.switch_interval = read_option("fsm_switch_interval", 2.0) - runtime.escape_retry_delay = read_option("fsm_escape_retry_delay", 20.0) - assert(type(runtime.escape_retry_delay) == "number", "fsm_escape_retry_delay should be a number") - assert(runtime.escape_retry_delay >= 0, "fsm_escape_retry_delay should be non-negative") configure_test_rule() setup_edges() From a4e1131208a45c6d9d0da9e24d1b70195c87df0a Mon Sep 17 00:00:00 2001 From: tcd <1369103595@qq.com> Date: Fri, 24 Apr 2026 21:51:47 +0800 Subject: [PATCH 09/12] feat(lua): route escape-to-home via fluctuant road and set traversal chassis modes --- src/lua/endpoint/test.lua | 30 ++++----- src/lua/endpoint/train-decision.lua | 32 +++++----- src/lua/intent/escape-to-home.lua | 65 ++++++++++++-------- src/lua/task/crossing-fluctuant-road.lua | 10 ++- src/lua/task/crossing-road-zone-train.lua | 1 + src/lua/task/crossing-road-zone.lua | 2 + src/lua/task/cruise-in-central-highlands.lua | 2 +- src/lua/task/go-down-onestep.lua | 9 +++ 8 files changed, 92 insertions(+), 59 deletions(-) diff --git a/src/lua/endpoint/test.lua b/src/lua/endpoint/test.lua index 32ec287..8b77940 100644 --- a/src/lua/endpoint/test.lua +++ b/src/lua/endpoint/test.lua @@ -25,8 +25,8 @@ local edges = require("util.edge").new() blackboard = require("blackboard").singleton() local runtime = { - ours_zone = true, - switch_interval = 2.0, + ours_zone = nil, + switch_interval = nil, current_state = "idle", navigation_ready = false, } @@ -154,21 +154,21 @@ local function create_intent_fsm() local condition = blackboard.condition local intent_fsm = fsm:new(State.idle) - local function restart_start_cruise_job() + local function run_start_cruise_job() run_job("start_cruise", function() return start_cruise(runtime.ours_zone) end) end - local function restart_keep_cruise_job() + local function run_keep_cruise_job() run_job("keep_cruise", function() return keep_cruise(runtime.ours_zone, runtime.switch_interval) end) end - local function restart_escape_job() - run_job("escape_to_home", function() - return escape_to_home(runtime.ours_zone) - end) - end + local function run_escape_job() + run_job("escape_to_home", function() + return escape_to_home() + end) + end intent_fsm:use({ state = State.idle, @@ -194,7 +194,7 @@ local function create_intent_fsm() state = State.start_cruise, enter = function() set_state(State.start_cruise) - restart_start_cruise_job() + run_start_cruise_job() end, event = function(handle) if condition.low_health() or condition.low_bullet() then @@ -213,7 +213,7 @@ local function create_intent_fsm() end action:warn("fsm(start_cruise): 导航失败,重试当前状态") - restart_start_cruise_job() + run_start_cruise_job() end, }) @@ -221,7 +221,7 @@ local function create_intent_fsm() state = State.keep_cruise, enter = function() set_state(State.keep_cruise) - restart_keep_cruise_job() + run_keep_cruise_job() end, event = function(handle) if condition.low_health() or condition.low_bullet() then @@ -239,7 +239,7 @@ local function create_intent_fsm() end action:warn("fsm(keep_cruise): 导航失败,重试当前状态") - restart_keep_cruise_job() + run_keep_cruise_job() end, }) @@ -247,7 +247,7 @@ local function create_intent_fsm() state = State.escape, enter = function() set_state(State.escape) - restart_escape_job() + run_escape_job() end, event = function(handle) if not job.done then @@ -260,7 +260,7 @@ local function create_intent_fsm() end action:warn("fsm(escape): 导航失败,重试当前状态") - restart_escape_job() + run_escape_job() end, }) diff --git a/src/lua/endpoint/train-decision.lua b/src/lua/endpoint/train-decision.lua index 9b031be..bddf93d 100644 --- a/src/lua/endpoint/train-decision.lua +++ b/src/lua/endpoint/train-decision.lua @@ -25,8 +25,8 @@ local edges = require("util.edge").new() blackboard = require("blackboard").singleton() local runtime = { - ours_zone = true, - switch_interval = 2.0, + ours_zone = nil, + switch_interval = nil, current_state = "idle", navigation_ready = false, } @@ -161,21 +161,21 @@ local function create_intent_fsm() local condition = blackboard.condition local intent_fsm = fsm:new(State.idle) - local function restart_start_cruise_job() + local function run_start_cruise_job() run_job("start_cruise", function() return start_cruise(runtime.ours_zone) end) end - local function restart_keep_cruise_job() + local function run_keep_cruise_job() run_job("keep_cruise", function() return keep_cruise(runtime.ours_zone, runtime.switch_interval) end) end - local function restart_escape_job() - run_job("escape_to_home", function() - return escape_to_home(runtime.ours_zone) - end) - end + local function run_escape_job() + run_job("escape_to_home", function() + return escape_to_home() + end) + end intent_fsm:use({ state = State.idle, @@ -201,7 +201,7 @@ local function create_intent_fsm() state = State.start_cruise, enter = function() set_state(State.start_cruise) - restart_start_cruise_job() + run_start_cruise_job() end, event = function(handle) if condition.low_health() or condition.low_bullet() then @@ -220,7 +220,7 @@ local function create_intent_fsm() end action:warn("fsm(start_cruise): 导航失败,重试当前状态") - restart_start_cruise_job() + run_start_cruise_job() end, }) @@ -228,7 +228,7 @@ local function create_intent_fsm() state = State.keep_cruise, enter = function() set_state(State.keep_cruise) - restart_keep_cruise_job() + run_keep_cruise_job() end, event = function(handle) if condition.low_health() or condition.low_bullet() then @@ -246,7 +246,7 @@ local function create_intent_fsm() end action:warn("fsm(keep_cruise): 导航失败,重试当前状态") - restart_keep_cruise_job() + run_keep_cruise_job() end, }) @@ -254,7 +254,7 @@ local function create_intent_fsm() state = State.escape, enter = function() set_state(State.escape) - restart_escape_job() + run_escape_job() end, event = function(handle) if not job.done then @@ -267,7 +267,7 @@ local function create_intent_fsm() end action:warn("fsm(escape): 导航失败,重试当前状态") - restart_escape_job() + run_escape_job() end, }) @@ -300,7 +300,7 @@ on_init = function() end) runtime.ours_zone = read_option("fsm_ours_zone", true) - runtime.switch_interval = read_option("fsm_switch_interval", 2.0) + runtime.switch_interval = read_option("fsm_switch_interval", 7.0) configure_test_rule() setup_edges() diff --git a/src/lua/intent/escape-to-home.lua b/src/lua/intent/escape-to-home.lua index 4d13af8..593c5dc 100644 --- a/src/lua/intent/escape-to-home.lua +++ b/src/lua/intent/escape-to-home.lua @@ -7,24 +7,18 @@ local function is_same_point(a, b) return a.x == b.x and a.y == b.y end -local function decide_escape_route(ours_zone) +local function decide_escape_route() local rule = blackboard.rule - local exit_point - local resupply_point - if ours_zone then - exit_point = rule.fluctuant_road_final.ours - resupply_point = rule.resupply_zone.ours - else - exit_point = rule.fluctuant_road_final.them - resupply_point = rule.resupply_zone.them - end + local entry_point = rule.fluctuant_road_begin.ours + local exit_point = rule.fluctuant_road_final.ours + local resupply_point = rule.resupply_zone.ours local queue = blackboard.meta.navigate_point_queue if type(queue) ~= "table" then return "direct" end - -- 从队尾(最近一次)开始回溯查询。 + -- 从队尾开始回溯查询。 for index = #queue, 1, -1 do local item = queue[index] if type(item) == "table" and type(item.x) == "number" and type(item.y) == "number" then @@ -37,43 +31,62 @@ local function decide_escape_route(ours_zone) if is_same_point(item, exit_point) then return "onestep" end + + -- 3. 先遇到起伏路段入口:先通过起伏路再回补给点 + if is_same_point(item, entry_point) then + return "fluctuant_road" + end end end - -- 3. 全队列都没遇到起伏路段出口或补给点:直接回补给点 + -- 4. 全队列都没遇到起伏路段入口/出口或补给点:直接回补给点 return "direct" end --- 回家补给: --- - 先遇到补给点 resupply_zone:直接从当前位置回补给点。 --- - 先遇到起伏路段出口 fluctuant_road_final:先下一级台阶,再回补给点。 ---- - 全队列都没遇到起伏路段出口或补给点:直接从当前位置回补给点。 ---- @param ours_zone boolean +--- - 先遇到起伏路段入口 fluctuant_road_begin:原地开跟随到fluctuant_road_begin.ours回家。 +--- - 全队列都没遇到起伏路段入口/出口或补给点:直接从当前位置回补给点。 --- @return boolean is_success -return function(ours_zone) - assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") - +return function() local rule = blackboard.rule - local resupply_zone - if ours_zone then - resupply_zone = rule.resupply_zone.ours - else - resupply_zone = rule.resupply_zone.them - end + local entry_point = rule.fluctuant_road_begin.ours + local resupply_zone = rule.resupply_zone.ours - local route = decide_escape_route(ours_zone) + local route = decide_escape_route() local is_success = true if route == "onestep" then - action:info("escape-to-home: 历史队列先命中起伏路段出口,按原有逻辑回家") - is_success = go_down_onestep(ours_zone) + action:info("escape-to-home: 历史队列先命中起伏路段出口,下一级台阶回家") + is_success = go_down_onestep(true) if not is_success then action:warn("escape-to-home: 下一级台阶失败") return false end + + elseif route == "fluctuant_road" then + action:info("escape-to-home: 历史队列先命中起伏路段入口,原地开跟随到fluctuant_road_begin.ours回家") + action:update_chassis_mode("LAUNCH_RAMP") + local gimbal_yaw = math.pi + action:info(string.format( + "escape-fluctuant-road: LAUNCH_RAMP 云台朝向=%.3f rad", + gimbal_yaw + )) + action:update_gimbal_direction(gimbal_yaw) + is_success = navigate_to_point(entry_point, { + tolerance = 0.1, + timeout = 10, + }) + + if not is_success then + action:warn("escape-to-home: 通过起伏路失败") + return false + end else action:info("escape-to-home: 历史队列先命中补给点或未命中关键点,直接从当前位置回家") end + action:update_chassis_mode("SPIN") is_success = navigate_to_point(resupply_zone, { tolerance = 0.15, timeout = 10, diff --git a/src/lua/task/crossing-fluctuant-road.lua b/src/lua/task/crossing-fluctuant-road.lua index 7cfc63a..2b535e8 100644 --- a/src/lua/task/crossing-fluctuant-road.lua +++ b/src/lua/task/crossing-fluctuant-road.lua @@ -20,13 +20,15 @@ return function(ours_zone, forward_center) final = rule.fluctuant_road_final.them end - local from, to + local from, to, gimbal_yaw if forward_center then from = begin to = final + gimbal_yaw = 0 else from = final to = begin + gimbal_yaw = math.pi end local ok = navigate_to_point(from, { @@ -42,6 +44,12 @@ return function(ours_zone, forward_center) return false end + action:update_chassis_mode("LAUNCH_RAMP") + action:info(string.format( + "crossing-fluctuant-road: LAUNCH_RAMP 云台朝向=%.3f rad", + gimbal_yaw + )) + action:update_gimbal_direction(gimbal_yaw) ok = navigate_to_point(to, { tolerance = 0.1, timeout = 10, diff --git a/src/lua/task/crossing-road-zone-train.lua b/src/lua/task/crossing-road-zone-train.lua index b7f79a1..8fad1fe 100644 --- a/src/lua/task/crossing-road-zone-train.lua +++ b/src/lua/task/crossing-road-zone-train.lua @@ -19,6 +19,7 @@ return function(ours_zone) road_final = rule.road_zone_final.them end + action:update_chassis_mode("SPIN") local ok = navigate_to_point(road_begin, { tolerance = 0.1, timeout = 10, diff --git a/src/lua/task/crossing-road-zone.lua b/src/lua/task/crossing-road-zone.lua index 20061db..0ec42d3 100644 --- a/src/lua/task/crossing-road-zone.lua +++ b/src/lua/task/crossing-road-zone.lua @@ -30,6 +30,7 @@ return function(ours_zone, forward_center) to = road_begin end + action:update_chassis_mode("SPIN") local ok = navigate_to_point(from, { tolerance = 0.1, timeout = 10, @@ -45,6 +46,7 @@ return function(ours_zone, forward_center) return false end + action:update_chassis_mode("SPIN") ok = navigate_to_point(to, { tolerance = 0.1, timeout = 10, diff --git a/src/lua/task/cruise-in-central-highlands.lua b/src/lua/task/cruise-in-central-highlands.lua index abf9b37..88a4170 100644 --- a/src/lua/task/cruise-in-central-highlands.lua +++ b/src/lua/task/cruise-in-central-highlands.lua @@ -37,7 +37,7 @@ return function(ours_zone, switch_interval) while true do local phase_start = clock:now() local ok = navigate_to_point(target, { - tolerance = 0.15, + tolerance = 0.1, timeout = navigation_timeout, }) if not ok then diff --git a/src/lua/task/go-down-onestep.lua b/src/lua/task/go-down-onestep.lua index 0bf5005..199a064 100644 --- a/src/lua/task/go-down-onestep.lua +++ b/src/lua/task/go-down-onestep.lua @@ -28,6 +28,14 @@ return function(ours_zone) return false end + action:update_chassis_mode("LAUNCH_RAMP") + local gimbal_yaw = math.pi / 2 + action:info(string.format( + "go-down-onestep: 云台朝向=%.3f rad", + gimbal_yaw + )) + action:update_gimbal_direction(gimbal_yaw) + ok = navigate_to_point(one_step_low, { tolerance = 0.1, timeout = 10, @@ -36,6 +44,7 @@ return function(ours_zone) action:warn("go-down-onestep: 导航到一级台阶低点失败") return false end + action:update_chassis_mode("SPIN") return true end From f9da1184a6684755d20cbdf9db1a7800ec84df44 Mon Sep 17 00:00:00 2001 From: tcd <1369103595@qq.com> Date: Sat, 25 Apr 2026 15:07:23 +0800 Subject: [PATCH 10/12] refactor(lua): navigation lua decision flow for fluctuant-road routing --- src/lua/api.lua | 2 +- src/lua/blackboard.lua | 41 +-- src/lua/endpoint/test.lua | 332 ++---------------- src/lua/endpoint/train-decision.lua | 104 ++++-- src/lua/intent/escape-to-home.lua | 78 +--- src/lua/intent/start-cruise-train.lua | 20 -- src/lua/intent/start-cruise.lua | 20 -- ...uant-road.lua => cross-fluctuant-road.lua} | 26 +- ...sing-road-zone.lua => cross-road-zone.lua} | 15 +- src/lua/task/crossing-road-zone-train.lua | 42 --- src/lua/task/cruise-in-central-highlands.lua | 16 +- src/lua/task/fluctuant-stuck-self-rescue.lua | 119 ------- src/lua/task/navigate-to-fluctuant-begin.lua | 24 ++ src/lua/task/navigate-to-point.lua | 1 - src/lua/task/stuck-self-rescue.lua | 246 ------------- 15 files changed, 145 insertions(+), 941 deletions(-) delete mode 100644 src/lua/intent/start-cruise-train.lua delete mode 100644 src/lua/intent/start-cruise.lua rename src/lua/task/{crossing-fluctuant-road.lua => cross-fluctuant-road.lua} (67%) rename src/lua/task/{crossing-road-zone.lua => cross-road-zone.lua} (68%) delete mode 100644 src/lua/task/crossing-road-zone-train.lua delete mode 100644 src/lua/task/fluctuant-stuck-self-rescue.lua create mode 100644 src/lua/task/navigate-to-fluctuant-begin.lua delete mode 100644 src/lua/task/stuck-self-rescue.lua diff --git a/src/lua/api.lua b/src/lua/api.lua index e7cef9e..d56a548 100644 --- a/src/lua/api.lua +++ b/src/lua/api.lua @@ -13,7 +13,7 @@ local util = require("util.native") --- @field fuck fun(message: string) --- --- @field send_target fun(x: number, y: number) ---- @field update_gimbal_direction fun(angle: number) +--- @field update_gimbal_direction fun(angle: number) --- @field update_gimbal_dominator fun(name: string) --- @field update_chassis_mode fun(mode: string) --- @field update_chassis_vel fun(x: number, y: number) diff --git a/src/lua/blackboard.lua b/src/lua/blackboard.lua index 00dbd2a..9a8f179 100644 --- a/src/lua/blackboard.lua +++ b/src/lua/blackboard.lua @@ -26,8 +26,6 @@ local function create_default_blackboard() meta = { timestamp = 0, -- 秒 fsm_state = "unknown", - navigate_point_queue = {}, - navigate_point_queue_max = 200, }, -- Static Information @@ -53,7 +51,7 @@ local function create_default_blackboard() launch_ramp_final = PointPair { { 0, 0 }, { 0, 0 } }, outpost_resupply = PointPair { { 0, 0 }, { 0, 0 } }, -- 前哨站补给点 assembly_zone = PointPair { { 0, 0 }, { 0, 0 } }, - central_highland_near_crossing_road = PointPair { { 0, 0 }, { 0, 0 } }, -- 中央高地靠近公路一侧 + central_highland_near_fluctuant_road = PointPair { { 0, 0 }, { 0, 0 } }, -- 中央高地靠近起伏路一侧 central_highland_near_doghole = PointPair { { 0, 0 }, { 0, 0 } }, --中央高地靠近狗洞一侧 central_highland_two_steps = PointPair { { 0, 0 }, { 0, 0 } }, --中央高地靠近二级台阶(二级台阶增益点前) @@ -77,43 +75,6 @@ local function create_default_blackboard() end, } - --- @param point {x: number, y: number} - --- @param tag? string - function result.enqueue_navigate_point(point, tag) - assert(type(point) == "table", "point should be a table") - assert(type(point.x) == "number", "point.x should be a number") - assert(type(point.y) == "number", "point.y should be a number") - - local queue = result.meta.navigate_point_queue - if type(queue) ~= "table" then - queue = {} - result.meta.navigate_point_queue = queue - end - - local max = tonumber(result.meta.navigate_point_queue_max) or 200 - if max < 1 then - max = 1 - end - - queue[#queue + 1] = { - ts = result.meta.timestamp, - x = point.x, - y = point.y, - hp = result.user.health, - bullet = result.user.bullet, - stage = result.game.stage, - state = result.meta.fsm_state, - tag = tag, - } - - while #queue > max do - table.remove(queue, 1) - end - end - - -- Backward-compatible alias. - result.push_navigate_point = result.enqueue_navigate_point - result.condition = { low_health = function() return result.user.health < result.rule.health_limit diff --git a/src/lua/endpoint/test.lua b/src/lua/endpoint/test.lua index 8b77940..ac8fd91 100644 --- a/src/lua/endpoint/test.lua +++ b/src/lua/endpoint/test.lua @@ -5,18 +5,21 @@ local action = require("action") local ascii = require("util.ascii_art") local clock = require("util.clock") -local fsm = require("util.fsm") -local option = require("option") - -local start_cruise = require("intent.start-cruise") -local keep_cruise = require("intent.keep-cruise") -local escape_to_home = require("intent.escape-to-home") +local edges = require("util.edge").new() local Scheduler = require("util.scheduler") local scheduler = Scheduler.new() local request = Scheduler.request -local edges = require("util.edge").new() +local restart_navigation = function() + action:info("导航即将重启") + action:restart_navigation { + global_map = "empty", + launch_livox = true, + launch_odin1 = false, + use_sim_time = false, + } +end --- --- Export Context @@ -24,324 +27,37 @@ local edges = require("util.edge").new() blackboard = require("blackboard").singleton() -local runtime = { - ours_zone = nil, - switch_interval = nil, - current_state = "idle", - navigation_ready = false, -} - -local requests = { - start = false, -} - -local job = { - handle = nil, - name = nil, - done = false, - success = false, -} - -local function read_option(name, fallback) - local value = rawget(option, name) - if value == nil then - return fallback - end - return value -end - -local function configure_test_rule() - local rule = blackboard.rule - - rule.health_limit = read_option("fsm_health_limit", 210) - rule.health_ready = read_option("fsm_health_ready", 400) - rule.bullet_limit = read_option("fsm_bullet_limit", 40) - rule.bullet_ready = read_option("fsm_bullet_ready", 300) - - -- 坐标点位由地图/配置提供,这里不再覆写为 (0,0)。 -end - -local function reset_job_status() - job.done = false - job.success = false -end - -local function cancel_job() - if job.handle ~= nil then - job.handle.cancel() - job.handle = nil - end - job.name = nil - reset_job_status() -end - -local function run_job(name, fn) - cancel_job() - job.name = name - reset_job_status() - - job.handle = scheduler:append_task(function() - local ok, result = xpcall(fn, debug.traceback) - job.handle = nil - job.name = nil - job.done = true - - if not ok then - job.success = false - action:fuck(string.format("fsm job '%s' failed:\n%s", name, result)) - return - end - - job.success = (result ~= false) - if not job.success then - action:warn(string.format("fsm job '%s' finished with false", name)) - end - end) -end - -local function take_request(name) - local value = requests[name] - requests[name] = false - return value -end - -local function set_state(name) - runtime.current_state = name - blackboard.meta.fsm_state = name - action:info("fsm state -> " .. name) -end - -local function start_navigation() - local ok, message = action:restart_navigation({ - global_map = read_option("global_map", "rmuc"), - launch_livox = read_option("launch_livox", true), - launch_odin1 = read_option("launch_odin1", false), - use_sim_time = read_option("use_sim_time", false), - }) - if not ok then - action:fuck("restart_navigation 触发失败: " .. tostring(message)) - end - - return ok, message -end - -local function setup_edges() - edges:on(blackboard.getter.rswitch, "UP", function() - requests.start = true - end) -end - -local function clear_navigate_history() - local queue = blackboard.meta.navigate_point_queue - if type(queue) ~= "table" then - blackboard.meta.navigate_point_queue = {} - return - end - - for i = #queue, 1, -1 do - queue[i] = nil - end -end - -local function create_intent_fsm() - local State = { - idle = "idle", - start_cruise = "start_cruise", - keep_cruise = "keep_cruise", - escape = "escape", - recover = "recover", - } - - local condition = blackboard.condition - local intent_fsm = fsm:new(State.idle) - local function run_start_cruise_job() - run_job("start_cruise", function() - return start_cruise(runtime.ours_zone) - end) - end - local function run_keep_cruise_job() - run_job("keep_cruise", function() - return keep_cruise(runtime.ours_zone, runtime.switch_interval) - end) - end - local function run_escape_job() - run_job("escape_to_home", function() - return escape_to_home() - end) - end - - intent_fsm:use({ - state = State.idle, - enter = function() - cancel_job() - set_state(State.idle) - runtime.navigation_ready = false - end, - event = function(handle) - if take_request("start") then - local ok = start_navigation() - runtime.navigation_ready = ok - end - - if runtime.navigation_ready and blackboard.game.stage == "STARTED" then - clear_navigate_history() - handle:set_next(State.start_cruise) - end - end, - }) - - intent_fsm:use({ - state = State.start_cruise, - enter = function() - set_state(State.start_cruise) - run_start_cruise_job() - end, - event = function(handle) - if condition.low_health() or condition.low_bullet() then - cancel_job() - handle:set_next(State.escape) - return - end - - if not job.done then - return - end - - if job.success then - handle:set_next(State.keep_cruise) - return - end - - action:warn("fsm(start_cruise): 导航失败,重试当前状态") - run_start_cruise_job() - end, - }) - - intent_fsm:use({ - state = State.keep_cruise, - enter = function() - set_state(State.keep_cruise) - run_keep_cruise_job() - end, - event = function(handle) - if condition.low_health() or condition.low_bullet() then - cancel_job() - handle:set_next(State.escape) - return - end - - if not job.done then - return - end - - if job.success then - return - end - - action:warn("fsm(keep_cruise): 导航失败,重试当前状态") - run_keep_cruise_job() - end, - }) - - intent_fsm:use({ - state = State.escape, - enter = function() - set_state(State.escape) - run_escape_job() - end, - event = function(handle) - if not job.done then - return - end - - if job.success then - handle:set_next(State.recover) - return - end - - action:warn("fsm(escape): 导航失败,重试当前状态") - run_escape_job() - end, - }) - - intent_fsm:use({ - state = State.recover, - enter = function() - cancel_job() - set_state(State.recover) - end, - event = function(handle) - if condition.low_health() or condition.low_bullet() then - return - end - - if condition.health_ready() and condition.bullet_ready() then - handle:set_next(State.start_cruise) - end - end, - }) - - assert(intent_fsm:init_ready(State), "intent fsm init_ready failed") - return intent_fsm -end - on_init = function() - clock:reset(blackboard.meta.timestamp) - - option:set_handler(function(error) - action:warn("while fetch option: " .. error) - end) - - runtime.ours_zone = read_option("fsm_ours_zone", true) - runtime.switch_interval = read_option("fsm_switch_interval", 2.0) - - configure_test_rule() - setup_edges() - - if read_option("enable_goal_topic_forward", false) then - action:switch_topic_forward(true) - end action:bind(scheduler) + action:info(ascii.banner) + action:warn("⚠️ TEST 模式,别上场哦") - local intent_fsm = create_intent_fsm() - scheduler:append_task(function() - while true do - intent_fsm:spin_once() - request:yield() - end - end) + clock:reset(blackboard.meta.timestamp) + action:switch_topic_forward(true) + + restart_navigation() + edges:on(blackboard.getter.rswitch, "UP", restart_navigation) scheduler:append_task(function() while true do - request:sleep(1.0) - action:info(string.format( - "fsm=%s stage=%s hp=%s bullet=%s rs=%s ls=%s", - runtime.current_state, - blackboard.game.stage, - tostring(blackboard.user.health), - tostring(blackboard.user.bullet), - blackboard.play.rswitch, - blackboard.play.lswitch - )) + request:sleep(1) + -- action:info("limit: " .. blackboard.user.chassis_power_limit) end end) - - action:info(ascii.banner) - action:warn("FSM test endpoint loaded") end on_tick = function() clock:update(blackboard.meta.timestamp) + edges:spin() scheduler:spin_once() end on_exit = function() - cancel_job() action:stop_navigation() end ---- Callback for velocity topic from Nav2. -on_control = function(vx, vy, _) - action:update_chassis_vel(vx, vy) -end +--- 由 NAV2 发布的目标速度值,在此处理回调 +on_control = function(x, y, _) + action:update_chassis_vel(x, y) +end \ No newline at end of file diff --git a/src/lua/endpoint/train-decision.lua b/src/lua/endpoint/train-decision.lua index bddf93d..1d15f23 100644 --- a/src/lua/endpoint/train-decision.lua +++ b/src/lua/endpoint/train-decision.lua @@ -8,9 +8,10 @@ local clock = require("util.clock") local fsm = require("util.fsm") local option = require("option") -local start_cruise = require("intent.start-cruise-train") local keep_cruise = require("intent.keep-cruise") local escape_to_home = require("intent.escape-to-home") +local cross_fluctuant_road = require("task.cross-fluctuant-road") +local navigate_to_fluctuant_begin = require("task.navigate-to-fluctuant-begin") local Scheduler = require("util.scheduler") local scheduler = Scheduler.new() @@ -27,6 +28,7 @@ blackboard = require("blackboard").singleton() local runtime = { ours_zone = nil, switch_interval = nil, + escape_route = nil, current_state = "idle", navigation_ready = false, } @@ -60,11 +62,11 @@ local function configure_test_rule() -- Ours side sample points rule.resupply_zone.ours = { x = 0.0, y = 0.0 } --家 - rule.road_zone_begin.ours = { x = 0.0, y = 0.0 } --公路区起点 - rule.road_zone_final.ours = { x = 0.0, y = 0.0 } --公路区终点 + rule.fluctuant_road_begin.ours = { x = 0.0, y = 0.0 } --起伏路段起点 + rule.fluctuant_road_final.ours = { x = 0.0, y = 0.0 } --起伏路段终点 rule.one_step_begin.ours = { x = 0.0, y = 0.0 } --一级台阶高点(先随便标个回家路上的点) rule.one_step_final.ours = { x = 0.0, y = 0.0 } --一级台阶低点(先随便标个回家路上的点) - rule.central_highland_near_crossing_road.ours = { x = 0.0, y = 0.0 } --高地靠近公路 + rule.central_highland_near_fluctuant_road.ours = { x = 0.0, y = 0.0 } --高地靠近起伏路 rule.central_highland_near_doghole.ours = { x = 0.0, y = 0.0 } --高地靠近狗洞 end @@ -138,22 +140,11 @@ local function setup_edges() end) end -local function clear_navigate_history() - local queue = blackboard.meta.navigate_point_queue - if type(queue) ~= "table" then - blackboard.meta.navigate_point_queue = {} - return - end - - for i = #queue, 1, -1 do - queue[i] = nil - end -end - local function create_intent_fsm() local State = { idle = "idle", - start_cruise = "start_cruise", + navigate_to_fluctuant_begin = "navigate_to_fluctuant_begin", + cross_fluctuant = "cross_fluctuant", keep_cruise = "keep_cruise", escape = "escape", recover = "recover", @@ -161,9 +152,14 @@ local function create_intent_fsm() local condition = blackboard.condition local intent_fsm = fsm:new(State.idle) - local function run_start_cruise_job() - run_job("start_cruise", function() - return start_cruise(runtime.ours_zone) + local function run_navigate_to_fluctuant_begin_job() + run_job("navigate_to_fluctuant_begin", function() + return navigate_to_fluctuant_begin(runtime.ours_zone, true) + end) + end + local function run_cross_fluctuant_job() + run_job("cross_fluctuant", function() + return cross_fluctuant_road(runtime.ours_zone, true) end) end local function run_keep_cruise_job() @@ -171,16 +167,24 @@ local function create_intent_fsm() return keep_cruise(runtime.ours_zone, runtime.switch_interval) end) end - local function run_escape_job() - run_job("escape_to_home", function() - return escape_to_home() - end) - end + local function run_escape_job() + assert(type(runtime.escape_route) == "string", "escape_route should be set before escape") + run_job("escape_to_home", function() + return escape_to_home(runtime.escape_route) + end) + end + local function enter_escape(handle, route) + assert(type(route) == "string", "route should be a string") + runtime.escape_route = route + cancel_job() + handle:set_next(State.escape) + end intent_fsm:use({ state = State.idle, enter = function() cancel_job() + runtime.escape_route = nil set_state(State.idle) runtime.navigation_ready = false end, @@ -191,22 +195,46 @@ local function create_intent_fsm() end if runtime.navigation_ready and blackboard.game.stage == "STARTED" then - clear_navigate_history() - handle:set_next(State.start_cruise) + handle:set_next(State.navigate_to_fluctuant_begin) + end + end, + }) + + intent_fsm:use({ + state = State.navigate_to_fluctuant_begin, + enter = function() + set_state(State.navigate_to_fluctuant_begin) + run_navigate_to_fluctuant_begin_job() + end, + event = function(handle) + if condition.low_health() or condition.low_bullet() then + enter_escape(handle, "direct") + return end + + if not job.done then + return + end + + if job.success then + handle:set_next(State.cross_fluctuant) + return + end + + action:warn("fsm(navigate_to_fluctuant_begin): 导航失败,重试当前阶段") + run_navigate_to_fluctuant_begin_job() end, }) intent_fsm:use({ - state = State.start_cruise, + state = State.cross_fluctuant, enter = function() - set_state(State.start_cruise) - run_start_cruise_job() + set_state(State.cross_fluctuant) + run_cross_fluctuant_job() end, event = function(handle) if condition.low_health() or condition.low_bullet() then - cancel_job() - handle:set_next(State.escape) + enter_escape(handle, "fluctuant_road") return end @@ -219,8 +247,8 @@ local function create_intent_fsm() return end - action:warn("fsm(start_cruise): 导航失败,重试当前状态") - run_start_cruise_job() + action:warn("fsm(cross_fluctuant): 导航失败,重试当前阶段") + run_cross_fluctuant_job() end, }) @@ -232,8 +260,7 @@ local function create_intent_fsm() end, event = function(handle) if condition.low_health() or condition.low_bullet() then - cancel_job() - handle:set_next(State.escape) + enter_escape(handle, "onestep") return end @@ -275,6 +302,7 @@ local function create_intent_fsm() state = State.recover, enter = function() cancel_job() + runtime.escape_route = nil set_state(State.recover) end, event = function(handle) @@ -283,7 +311,7 @@ local function create_intent_fsm() end if condition.health_ready() and condition.bullet_ready() then - handle:set_next(State.start_cruise) + handle:set_next(State.navigate_to_fluctuant_begin) end end, }) @@ -300,7 +328,7 @@ on_init = function() end) runtime.ours_zone = read_option("fsm_ours_zone", true) - runtime.switch_interval = read_option("fsm_switch_interval", 7.0) + runtime.switch_interval = read_option("fsm_switch_interval", 5.0) configure_test_rule() setup_edges() diff --git a/src/lua/intent/escape-to-home.lua b/src/lua/intent/escape-to-home.lua index 593c5dc..5be1c87 100644 --- a/src/lua/intent/escape-to-home.lua +++ b/src/lua/intent/escape-to-home.lua @@ -1,89 +1,35 @@ local blackboard = require("blackboard").singleton() local action = require("action") local go_down_onestep = require("task.go-down-onestep") +local cross_fluctuant_road = require("task.cross-fluctuant-road") local navigate_to_point = require("task.navigate-to-point") -local function is_same_point(a, b) - return a.x == b.x and a.y == b.y -end - -local function decide_escape_route() - local rule = blackboard.rule - local entry_point = rule.fluctuant_road_begin.ours - local exit_point = rule.fluctuant_road_final.ours - local resupply_point = rule.resupply_zone.ours - - local queue = blackboard.meta.navigate_point_queue - if type(queue) ~= "table" then - return "direct" - end - - -- 从队尾开始回溯查询。 - for index = #queue, 1, -1 do - local item = queue[index] - if type(item) == "table" and type(item.x) == "number" and type(item.y) == "number" then - -- 1. 先遇到补给点:直接回补给点 - if is_same_point(item, resupply_point) then - return "direct" - end - - -- 2. 先遇到起伏路段出口:先下台阶再回补给点 - if is_same_point(item, exit_point) then - return "onestep" - end - - -- 3. 先遇到起伏路段入口:先通过起伏路再回补给点 - if is_same_point(item, entry_point) then - return "fluctuant_road" - end - end - end - - -- 4. 全队列都没遇到起伏路段入口/出口或补给点:直接回补给点 - return "direct" -end - ---- 回家补给: ---- - 先遇到补给点 resupply_zone:直接从当前位置回补给点。 ---- - 先遇到起伏路段出口 fluctuant_road_final:先下一级台阶,再回补给点。 ---- - 先遇到起伏路段入口 fluctuant_road_begin:原地开跟随到fluctuant_road_begin.ours回家。 ---- - 全队列都没遇到起伏路段入口/出口或补给点:直接从当前位置回补给点。 +--- @param route "direct"|"onestep"|"fluctuant_road" --- @return boolean is_success -return function() - local rule = blackboard.rule - local entry_point = rule.fluctuant_road_begin.ours - local resupply_zone = rule.resupply_zone.ours +return function(route) + assert(type(route) == "string", "route should be a string") - local route = decide_escape_route() + local resupply_zone = blackboard.rule.resupply_zone.ours local is_success = true + if route == "onestep" then - action:info("escape-to-home: 历史队列先命中起伏路段出口,下一级台阶回家") + action:info("escape-to-home: 开跟随走下台阶路线回家") is_success = go_down_onestep(true) if not is_success then action:warn("escape-to-home: 下一级台阶失败") return false end - elseif route == "fluctuant_road" then - action:info("escape-to-home: 历史队列先命中起伏路段入口,原地开跟随到fluctuant_road_begin.ours回家") - action:update_chassis_mode("LAUNCH_RAMP") - local gimbal_yaw = math.pi - action:info(string.format( - "escape-fluctuant-road: LAUNCH_RAMP 云台朝向=%.3f rad", - gimbal_yaw - )) - action:update_gimbal_direction(gimbal_yaw) - is_success = navigate_to_point(entry_point, { - tolerance = 0.1, - timeout = 10, - }) - + action:info("escape-to-home: 走起伏路路线回家") + is_success = cross_fluctuant_road(true, false) if not is_success then action:warn("escape-to-home: 通过起伏路失败") return false end + elseif route == "direct" then + action:info("escape-to-home: 走直接回家路线") else - action:info("escape-to-home: 历史队列先命中补给点或未命中关键点,直接从当前位置回家") + action:warn("unknown escape route: " .. tostring(route)) end action:update_chassis_mode("SPIN") diff --git a/src/lua/intent/start-cruise-train.lua b/src/lua/intent/start-cruise-train.lua deleted file mode 100644 index 1b1a893..0000000 --- a/src/lua/intent/start-cruise-train.lua +++ /dev/null @@ -1,20 +0,0 @@ -local action = require("action") -local crossing_road_zone_train = require("task.crossing-road-zone-train") - ---- 训练专用: ---- 直接调用 crossing-road-zone-train 通过公路区 ---- @param ours_zone boolean ---- @return boolean is_success -return function(ours_zone) - assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") - action:info("开始start-cruise-train") - - local ok = crossing_road_zone_train(ours_zone) - if not ok then - action:warn("start-cruise-train: 通过公路区训练路线失败") - return false - end - - action:info("start-cruise-train: 已完成公路区训练路线") - return true -end diff --git a/src/lua/intent/start-cruise.lua b/src/lua/intent/start-cruise.lua deleted file mode 100644 index bf1afd6..0000000 --- a/src/lua/intent/start-cruise.lua +++ /dev/null @@ -1,20 +0,0 @@ -local action = require("action") -local crossing_road_zone = require("task.crossing-road-zone") - ---- 开始巡航: ---- 1) 从当前位置直接进入公路区跨越流程 ---- 2) 正向通过公路区 ---- @param ours_zone boolean ---- @return boolean is_success -return function(ours_zone) - assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") - - local ok = crossing_road_zone(ours_zone, true) - if not ok then - action:warn("start-cruise: 通过公路区导航失败") - return false - end - - action:info("start-cruise: 已通过公路区,准备进入持续巡航") - return true -end diff --git a/src/lua/task/crossing-fluctuant-road.lua b/src/lua/task/cross-fluctuant-road.lua similarity index 67% rename from src/lua/task/crossing-fluctuant-road.lua rename to src/lua/task/cross-fluctuant-road.lua index 2b535e8..2397420 100644 --- a/src/lua/task/crossing-fluctuant-road.lua +++ b/src/lua/task/cross-fluctuant-road.lua @@ -8,7 +8,7 @@ local navigate_to_point = require("task.navigate-to-point") return function(ours_zone, forward_center) assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") assert(type(forward_center) == "boolean", "forward_center should be a boolean") - action:info("开始crossing-fluctuant-road") + action:info("开始cross-fluctuant-road") local rule = blackboard.rule local begin, final @@ -20,48 +20,34 @@ return function(ours_zone, forward_center) final = rule.fluctuant_road_final.them end - local from, to, gimbal_yaw + local to, gimbal_yaw if forward_center then - from = begin to = final gimbal_yaw = 0 else - from = final to = begin gimbal_yaw = math.pi end - local ok = navigate_to_point(from, { - tolerance = 0.1, - timeout = 10, - }) - if not ok then - action:warn(string.format( - "crossing-fluctuant-road: 导航到起点失败 (x=%.2f, y=%.2f)", - from.x, - from.y - )) - return false - end - action:update_chassis_mode("LAUNCH_RAMP") action:info(string.format( - "crossing-fluctuant-road: LAUNCH_RAMP 云台朝向=%.3f rad", + "cross-fluctuant-road: LAUNCH_RAMP 云台朝向=%.3f rad", gimbal_yaw )) action:update_gimbal_direction(gimbal_yaw) - ok = navigate_to_point(to, { + local ok = navigate_to_point(to, { tolerance = 0.1, timeout = 10, }) if not ok then action:warn(string.format( - "crossing-fluctuant-road: 导航到终点失败 (x=%.2f, y=%.2f)", + "cross-fluctuant-road: 导航到终点失败 (x=%.2f, y=%.2f)", to.x, to.y )) return false end + action:update_chassis_mode("SPIN") return true end diff --git a/src/lua/task/crossing-road-zone.lua b/src/lua/task/cross-road-zone.lua similarity index 68% rename from src/lua/task/crossing-road-zone.lua rename to src/lua/task/cross-road-zone.lua index 0ec42d3..13fe623 100644 --- a/src/lua/task/crossing-road-zone.lua +++ b/src/lua/task/cross-road-zone.lua @@ -1,7 +1,6 @@ local blackboard = require("blackboard").singleton() local action = require("action") local navigate_to_point = require("task.navigate-to-point") -local crossing_fluctuant_road = require("task.crossing-fluctuant-road") --- @param ours_zone boolean --- @param forward_center boolean @@ -9,7 +8,7 @@ local crossing_fluctuant_road = require("task.crossing-fluctuant-road") return function(ours_zone, forward_center) assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") assert(type(forward_center) == "boolean", "forward_center should be a boolean") - action:info("开始crossing-road-zone") + action:info("开始cross-road-zone") local rule = blackboard.rule local road_begin, road_final @@ -30,29 +29,21 @@ return function(ours_zone, forward_center) to = road_begin end - action:update_chassis_mode("SPIN") local ok = navigate_to_point(from, { tolerance = 0.1, timeout = 10, }) if not ok then - action:warn("crossing-road-zone: 导航到公路区入口失败") + action:warn("cross-road-zone: 导航到公路区起点失败") return false end - ok = crossing_fluctuant_road(ours_zone, forward_center) - if not ok then - action:warn("crossing-road-zone: 通过起伏路段失败") - return false - end - - action:update_chassis_mode("SPIN") ok = navigate_to_point(to, { tolerance = 0.1, timeout = 10, }) if not ok then - action:warn("crossing-road-zone: 导航到公路区出口失败") + action:warn("cross-road-zone: 导航到公路区终点失败") return false end diff --git a/src/lua/task/crossing-road-zone-train.lua b/src/lua/task/crossing-road-zone-train.lua deleted file mode 100644 index 8fad1fe..0000000 --- a/src/lua/task/crossing-road-zone-train.lua +++ /dev/null @@ -1,42 +0,0 @@ -local blackboard = require("blackboard").singleton() -local action = require("action") -local navigate_to_point = require("task.navigate-to-point") - ---- 训练专用:从当前点导航到公路区起点,再导航到公路区终点。 ---- @param ours_zone boolean ---- @return boolean is_success -return function(ours_zone) - assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") - action:info("开始crossing-road-zone-train") - - local rule = blackboard.rule - local road_begin, road_final - if ours_zone then - road_begin = rule.road_zone_begin.ours - road_final = rule.road_zone_final.ours - else - road_begin = rule.road_zone_begin.them - road_final = rule.road_zone_final.them - end - - action:update_chassis_mode("SPIN") - local ok = navigate_to_point(road_begin, { - tolerance = 0.1, - timeout = 10, - }) - if not ok then - action:warn("crossing-road-zone-train: 导航到公路区起点失败") - return false - end - - ok = navigate_to_point(road_final, { - tolerance = 0.1, - timeout = 10, - }) - if not ok then - action:warn("crossing-road-zone-train: 导航到公路区终点失败") - return false - end - - return true -end diff --git a/src/lua/task/cruise-in-central-highlands.lua b/src/lua/task/cruise-in-central-highlands.lua index 88a4170..8a5ebff 100644 --- a/src/lua/task/cruise-in-central-highlands.lua +++ b/src/lua/task/cruise-in-central-highlands.lua @@ -10,7 +10,7 @@ local function distance_to(target) return math.sqrt(dx * dx + dy * dy) end ---- 中央高地巡航:在“靠近公路侧”与“靠近狗洞侧”之间按固定周期切换导航目标。 + --- 中央高地巡航:在“靠近起伏路侧”与“靠近狗洞侧”之间按固定周期切换导航目标。 --- @param ours_zone boolean --- @param switch_interval number 切换周期(秒) return function(ours_zone, switch_interval) @@ -21,18 +21,18 @@ return function(ours_zone, switch_interval) local rule = blackboard.rule local navigation_timeout = math.max(10.0, switch_interval * 2.0) - local near_crossing_road, near_doghole + local near_fluctuant_road, near_doghole if ours_zone then - near_crossing_road = rule.central_highland_near_crossing_road.ours + near_fluctuant_road = rule.central_highland_near_fluctuant_road.ours near_doghole = rule.central_highland_near_doghole.ours else - near_crossing_road = rule.central_highland_near_crossing_road.them + near_fluctuant_road = rule.central_highland_near_fluctuant_road.them near_doghole = rule.central_highland_near_doghole.them end -- 首次优先去更近的点,减少无效折返。 - local go_crossing_road_first = distance_to(near_crossing_road) <= distance_to(near_doghole) - local target = go_crossing_road_first and near_crossing_road or near_doghole + local go_fluctuant_road_first = distance_to(near_fluctuant_road) <= distance_to(near_doghole) + local target = go_fluctuant_road_first and near_fluctuant_road or near_doghole while true do local phase_start = clock:now() @@ -57,10 +57,10 @@ return function(ours_zone, switch_interval) request:sleep(remain) end - if target == near_crossing_road then + if target == near_fluctuant_road then target = near_doghole else - target = near_crossing_road + target = near_fluctuant_road end end diff --git a/src/lua/task/fluctuant-stuck-self-rescue.lua b/src/lua/task/fluctuant-stuck-self-rescue.lua deleted file mode 100644 index d4fa50b..0000000 --- a/src/lua/task/fluctuant-stuck-self-rescue.lua +++ /dev/null @@ -1,119 +0,0 @@ -local blackboard = require("blackboard").singleton() -local request = require("util.scheduler").request -local action = require("action") -local navigate_to_point = require("task.navigate-to-point") -local stuck_self_rescue = require("task.stuck-self-rescue") - ---- @class FluctuantStuckSelfRescueOptions ---- @field max_cycles? number ---- @field settle_time? number ---- @field retreat_timeout? number ---- @field retreat_tolerance? number|{x: number, y: number} ---- @field retry_forward_timeout? number ---- @field goal_tolerance? number|{x: number, y: number} ---- @field fallback_timeout? number ---- @field fallback_max_rescue_attempts? number - -local function normalize_options(options) - options = options or {} - assert(type(options) == "table", "options should be a table") - - local result = { - max_cycles = options.max_cycles or 2, - settle_time = options.settle_time or 0.25, - retreat_timeout = options.retreat_timeout or 2.5, - retreat_tolerance = options.retreat_tolerance or 0.2, - retry_forward_timeout = options.retry_forward_timeout or 5.0, - goal_tolerance = options.goal_tolerance or 0.2, - fallback_timeout = options.fallback_timeout or 8.0, - fallback_max_rescue_attempts = options.fallback_max_rescue_attempts or 2, - } - - assert(type(result.max_cycles) == "number" and result.max_cycles >= 0, "max_cycles should be non-negative") - assert(result.max_cycles % 1 == 0, "max_cycles should be an integer") - assert(type(result.settle_time) == "number" and result.settle_time >= 0, "settle_time should be non-negative") - assert(type(result.retreat_timeout) == "number" and result.retreat_timeout > 0, "retreat_timeout should be positive") - assert(type(result.retry_forward_timeout) == "number" and result.retry_forward_timeout > 0, "retry_forward_timeout should be positive") - assert(type(result.fallback_timeout) == "number" and result.fallback_timeout > 0, "fallback_timeout should be positive") - assert( - type(result.fallback_max_rescue_attempts) == "number" and result.fallback_max_rescue_attempts >= 0, - "fallback_max_rescue_attempts should be non-negative" - ) - assert(result.fallback_max_rescue_attempts % 1 == 0, "fallback_max_rescue_attempts should be an integer") - - return result -end - -local function resolve_fluctuant_points(ours_zone, forward_center) - assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") - assert(type(forward_center) == "boolean", "forward_center should be a boolean") - - local rule = blackboard.rule - local begin, final - if ours_zone then - begin = rule.fluctuant_road_begin.ours - final = rule.fluctuant_road_final.ours - else - begin = rule.fluctuant_road_begin.them - final = rule.fluctuant_road_final.them - end - - if forward_center then - return begin, final - end - return final, begin -end - ---- 起伏路段专用脱困:优先回退至稳定区再重试;失败后回落通用脱困。 ---- @param ours_zone boolean ---- @param forward_center boolean ---- @param options? FluctuantStuckSelfRescueOptions ---- @return boolean is_success -return function(ours_zone, forward_center, options) - action:info("开始fluctuant-stuck-self-rescue") - - local config = normalize_options(options) - local condition = blackboard.condition - - local from, to = resolve_fluctuant_points(ours_zone, forward_center) - if condition.near(to, config.goal_tolerance) then - return true - end - - for cycle = 1, config.max_cycles do - action:warn("起伏路段疑似悬空卡住,执行回退脱困流程") - - local back_ok = navigate_to_point(from, { - tolerance = config.retreat_tolerance, - timeout = config.retreat_timeout, - }) - if not back_ok then - action:warn("起伏路段回退失败,准备回落通用脱困") - break - end - - if config.settle_time > 0 then - request:sleep(config.settle_time) - end - - local forward_ok = navigate_to_point(to, { - tolerance = config.goal_tolerance, - timeout = config.retry_forward_timeout, - }) - if forward_ok then - return true - end - - action:warn("起伏路段重试通过失败,继续下一轮") - end - - action:warn("起伏路段专用回退未恢复,回落通用 stuck-self-rescue") - return stuck_self_rescue(to, { - timeout = config.fallback_timeout, - goal_tolerance = config.goal_tolerance, - max_rescue_attempts = config.fallback_max_rescue_attempts, - rescue_side_offset = 0.35, - rescue_backtrack = 0.45, - rescue_point_timeout = 2.0, - }) -end diff --git a/src/lua/task/navigate-to-fluctuant-begin.lua b/src/lua/task/navigate-to-fluctuant-begin.lua new file mode 100644 index 0000000..320038f --- /dev/null +++ b/src/lua/task/navigate-to-fluctuant-begin.lua @@ -0,0 +1,24 @@ +local blackboard = require("blackboard").singleton() +local action = require("action") +local navigate_to_point = require("task.navigate-to-point") + +--- @param ours_zone boolean +--- @param use_begin boolean +--- @return boolean is_success +return function(ours_zone, use_begin) + assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") + assert(type(use_begin) == "boolean", "use_begin should be a boolean") + + local rule = blackboard.rule + local point + if ours_zone then + point = use_begin and rule.fluctuant_road_begin.ours or rule.fluctuant_road_final.ours + else + point = use_begin and rule.fluctuant_road_begin.them or rule.fluctuant_road_final.them + end + + return navigate_to_point(point, { + tolerance = 0.1, + timeout = 10, + }) +end diff --git a/src/lua/task/navigate-to-point.lua b/src/lua/task/navigate-to-point.lua index 29e7055..ac073e1 100644 --- a/src/lua/task/navigate-to-point.lua +++ b/src/lua/task/navigate-to-point.lua @@ -40,7 +40,6 @@ return function(point, options) local tolerance, timeout = normalize_options(options) local condition = blackboard.condition - blackboard.enqueue_navigate_point(point, "navigate-to-point") action:navigate(point) local is_timeout = request:wait_until { monitor = function() diff --git a/src/lua/task/stuck-self-rescue.lua b/src/lua/task/stuck-self-rescue.lua deleted file mode 100644 index aa46a90..0000000 --- a/src/lua/task/stuck-self-rescue.lua +++ /dev/null @@ -1,246 +0,0 @@ -local blackboard = require("blackboard").singleton() -local clock = require("util.clock") -local request = require("util.scheduler").request -local util = require("util.math") -local action = require("action") -local navigate_to_point = require("task.navigate-to-point") - -local function distance(a, b) - local dx = a.x - b.x - local dy = a.y - b.y - return math.sqrt(dx * dx + dy * dy) -end - -local function vector_length(x, y) - return math.sqrt(x * x + y * y) -end - ---- @class StuckSelfRescueOptions ---- @field timeout? number ---- @field monitor_interval? number ---- @field stall_window? number ---- @field min_progress? number ---- @field goal_tolerance? number|{x: number, y: number} ---- @field stuck_ignore_tolerance? number ---- @field max_rescue_attempts? number ---- @field rescue_side_offset? number ---- @field rescue_backtrack? number ---- @field rescue_point_timeout? number ---- @field rescue_point_tolerance? number|{x: number, y: number} - -local function normalize_options(options) - options = options or {} - assert(type(options) == "table", "options should be a table") - - local result = { - timeout = options.timeout or 12.0, - monitor_interval = options.monitor_interval or 0.1, - stall_window = options.stall_window or 1.2, - min_progress = options.min_progress or 0.05, - goal_tolerance = options.goal_tolerance or 0.15, - stuck_ignore_tolerance = options.stuck_ignore_tolerance or 0.35, - max_rescue_attempts = options.max_rescue_attempts or 2, - rescue_side_offset = options.rescue_side_offset or 0.6, - rescue_backtrack = options.rescue_backtrack or 0.4, - rescue_point_timeout = options.rescue_point_timeout or 2.5, - rescue_point_tolerance = options.rescue_point_tolerance or 0.15, - } - - assert(type(result.timeout) == "number" and result.timeout > 0, "timeout should be positive") - assert( - type(result.monitor_interval) == "number" and result.monitor_interval > 0, - "monitor_interval should be positive" - ) - assert( - type(result.stall_window) == "number" and result.stall_window > 0, - "stall_window should be positive" - ) - assert( - type(result.min_progress) == "number" and result.min_progress >= 0, - "min_progress should be non-negative" - ) - assert( - type(result.stuck_ignore_tolerance) == "number" and result.stuck_ignore_tolerance >= 0, - "stuck_ignore_tolerance should be non-negative" - ) - assert( - type(result.max_rescue_attempts) == "number" and result.max_rescue_attempts >= 0, - "max_rescue_attempts should be non-negative" - ) - assert( - type(result.max_rescue_attempts) == "number" and result.max_rescue_attempts % 1 == 0, - "max_rescue_attempts should be an integer" - ) - assert( - type(result.rescue_side_offset) == "number" and result.rescue_side_offset >= 0, - "rescue_side_offset should be non-negative" - ) - assert( - type(result.rescue_backtrack) == "number" and result.rescue_backtrack >= 0, - "rescue_backtrack should be non-negative" - ) - assert( - type(result.rescue_point_timeout) == "number" and result.rescue_point_timeout > 0, - "rescue_point_timeout should be positive" - ) - - return result -end - -local function build_rescue_targets(goal, side_offset, backtrack) - local current = { - x = blackboard.user.x, - y = blackboard.user.y, - } - - local dx = goal.x - current.x - local dy = goal.y - current.y - local norm = vector_length(dx, dy) - if norm < 1e-6 then - dx = math.cos(blackboard.user.yaw) - dy = math.sin(blackboard.user.yaw) - norm = vector_length(dx, dy) - end - - if norm < 1e-6 then - dx = 1.0 - dy = 0.0 - norm = 1.0 - end - - local ux = dx / norm - local uy = dy / norm - local sx = -uy - local sy = ux - - return { - { - x = current.x + sx * side_offset, - y = current.y + sy * side_offset, - }, - { - x = current.x - sx * side_offset, - y = current.y - sy * side_offset, - }, - { - x = current.x - ux * backtrack, - y = current.y - uy * backtrack, - }, - } -end - ---- @param goal {x: number, y: number} ---- @param config StuckSelfRescueOptions ---- @param deadline number ---- @return "success" | "stuck" | "timeout" -local function monitor_progress(goal, config, deadline) - local condition = blackboard.condition - local previous = { - x = blackboard.user.x, - y = blackboard.user.y, - } - local stall_begin = nil - - while true do - if condition.near(goal, config.goal_tolerance) then - return "success" - end - - local now = clock:now() - if now >= deadline then - return "timeout" - end - - local current = { - x = blackboard.user.x, - y = blackboard.user.y, - } - local moved = distance(current, previous) - previous = current - - if moved >= config.min_progress then - stall_begin = nil - else - stall_begin = stall_begin or now - if now - stall_begin >= config.stall_window then - if not condition.near(goal, config.stuck_ignore_tolerance) then - return "stuck" - end - end - end - - request:sleep(config.monitor_interval) - end -end - ---- @param goal {x: number, y: number} ---- @param config StuckSelfRescueOptions ---- @param deadline number ---- @return boolean rescued -local function execute_rescue(goal, config, deadline) - local targets = build_rescue_targets(goal, config.rescue_side_offset, config.rescue_backtrack) - for _, target in ipairs(targets) do - local now = clock:now() - local remaining = deadline - now - if remaining <= 0 then - return false - end - - local timeout = math.min(config.rescue_point_timeout, remaining) - local ok = navigate_to_point(target, { - tolerance = config.rescue_point_tolerance, - timeout = timeout, - }) - if ok then - return true - end - end - - return false -end - ---- 通用卡死自救:检测无进展并执行侧向/回退子目标脱困。 ---- @param goal {x: number, y: number} ---- @param options? StuckSelfRescueOptions ---- @return boolean is_success -return function(goal, options) - assert(type(goal) == "table", "goal should be a table") - assert(type(goal.x) == "number", "goal.x should be a number") - assert(type(goal.y) == "number", "goal.y should be a number") - assert(not util.check_nan(goal.x, goal.y), "goal should not be NaN") - action:info("开始stuck-self-rescue") - - local config = normalize_options(options) - local condition = blackboard.condition - local deadline = clock:now() + config.timeout - - if condition.near(goal, config.goal_tolerance) then - return true - end - - local rescue_count = 0 - while true do - action:navigate(goal) - local status = monitor_progress(goal, config, deadline) - if status == "success" then - return true - end - if status == "timeout" then - action:warn("stuck-self-rescue 超时,未能抵达目标") - return false - end - - rescue_count = rescue_count + 1 - if rescue_count > config.max_rescue_attempts then - action:warn("stuck-self-rescue 超过最大脱困次数") - return false - end - - action:warn("检测到无进展,开始执行脱困子目标") - local rescued = execute_rescue(goal, config, deadline) - if not rescued then - action:warn("脱困子目标执行失败") - return false - end - end -end From 21fbcc3087a577072a71104543c93f1210184849 Mon Sep 17 00:00:00 2001 From: tcd <1369103595@qq.com> Date: Sat, 25 Apr 2026 18:20:50 +0800 Subject: [PATCH 11/12] feat(lua): reorganize navigation task modules and add forward-press tasks --- src/lua/blackboard.lua | 7 +-- src/lua/endpoint/train-decision.lua | 9 +++- src/lua/intent/escape-to-home.lua | 4 +- src/lua/intent/keep-cruise.lua | 2 +- .../cross-fluctuant-road.lua | 0 .../navigate-to-fluctuant-begin.lua | 0 .../task/{ => cross-road}/cross-road-zone.lua | 1 + .../cruise-in-central-highlands.lua | 1 + .../forward-press-in-one-step.lua | 28 ++++++++++ .../forward-press-in-two-step.lua | 53 +++++++++++++++++++ .../cruise-in-front-of-base.lua} | 0 .../task/{ => guard-home}/occupy-fortress.lua | 1 + .../task/{ => one-step}/go-down-onestep.lua | 0 src/lua/task/supply/supply-ammunition.lua | 0 14 files changed, 98 insertions(+), 8 deletions(-) rename src/lua/task/{ => cross-fluctuant}/cross-fluctuant-road.lua (100%) rename src/lua/task/{ => cross-fluctuant}/navigate-to-fluctuant-begin.lua (100%) rename src/lua/task/{ => cross-road}/cross-road-zone.lua (97%) rename src/lua/task/{ => cruise-in-central-highland}/cruise-in-central-highlands.lua (98%) create mode 100644 src/lua/task/forward-press/forward-press-in-one-step.lua create mode 100644 src/lua/task/forward-press/forward-press-in-two-step.lua rename src/lua/task/{supply-ammunition.lua => guard-home/cruise-in-front-of-base.lua} (100%) rename src/lua/task/{ => guard-home}/occupy-fortress.lua (94%) rename src/lua/task/{ => one-step}/go-down-onestep.lua (100%) create mode 100644 src/lua/task/supply/supply-ammunition.lua diff --git a/src/lua/blackboard.lua b/src/lua/blackboard.lua index 9a8f179..992f860 100644 --- a/src/lua/blackboard.lua +++ b/src/lua/blackboard.lua @@ -52,15 +52,16 @@ local function create_default_blackboard() outpost_resupply = PointPair { { 0, 0 }, { 0, 0 } }, -- 前哨站补给点 assembly_zone = PointPair { { 0, 0 }, { 0, 0 } }, central_highland_near_fluctuant_road = PointPair { { 0, 0 }, { 0, 0 } }, -- 中央高地靠近起伏路一侧 - central_highland_near_doghole = PointPair { { 0, 0 }, { 0, 0 } }, --中央高地靠近狗洞一侧 - central_highland_two_steps = PointPair { { 0, 0 }, { 0, 0 } }, --中央高地靠近二级台阶(二级台阶增益点前) + central_highland_near_doghole = PointPair { { 0, 0 }, { 0, 0 } }, -- 中央高地靠近狗洞一侧 + central_highland_gain_pount = PointPair { { 0, 0 }, { 0, 0 } }, -- 中央高地增益点 + central_highland_near_two_steps_and_outpost = PointPair { { 0, 0 }, { 0, 0 } }, -- 中央高地靠近二级台阶(二级台阶增益点和前哨站中间) -- 特殊跨越地形坐标 road_tunnel_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 公路隧道 road_tunnel_final = PointPair { { 0, 0 }, { 0, 0 } }, one_step_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 一级台阶高点 one_step_final = PointPair { { 0, 0 }, { 0, 0 } }, -- 一级台阶低点 - two_step_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 二级台阶 + two_step_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 二级台阶高点 two_step_final = PointPair { { 0, 0 }, { 0, 0 } }, fluctuant_road_begin = PointPair { { 0, 0 }, { 0, 0 } }, -- 起伏路段 fluctuant_road_final = PointPair { { 0, 0 }, { 0, 0 } }, diff --git a/src/lua/endpoint/train-decision.lua b/src/lua/endpoint/train-decision.lua index 1d15f23..50cc6e3 100644 --- a/src/lua/endpoint/train-decision.lua +++ b/src/lua/endpoint/train-decision.lua @@ -10,8 +10,8 @@ local option = require("option") local keep_cruise = require("intent.keep-cruise") local escape_to_home = require("intent.escape-to-home") -local cross_fluctuant_road = require("task.cross-fluctuant-road") -local navigate_to_fluctuant_begin = require("task.navigate-to-fluctuant-begin") +local cross_fluctuant_road = require("task.cross-fluctuant.cross-fluctuant-road") +local navigate_to_fluctuant_begin = require("task.cross-fluctuant.navigate-to-fluctuant-begin") local Scheduler = require("util.scheduler") local scheduler = Scheduler.new() @@ -61,6 +61,7 @@ local function configure_test_rule() rule.bullet_ready = read_option("fsm_bullet_ready", 300) -- Ours side sample points + -- 暂时全为0 rule.resupply_zone.ours = { x = 0.0, y = 0.0 } --家 rule.fluctuant_road_begin.ours = { x = 0.0, y = 0.0 } --起伏路段起点 rule.fluctuant_road_final.ours = { x = 0.0, y = 0.0 } --起伏路段终点 @@ -68,6 +69,10 @@ local function configure_test_rule() rule.one_step_final.ours = { x = 0.0, y = 0.0 } --一级台阶低点(先随便标个回家路上的点) rule.central_highland_near_fluctuant_road.ours = { x = 0.0, y = 0.0 } --高地靠近起伏路 rule.central_highland_near_doghole.ours = { x = 0.0, y = 0.0 } --高地靠近狗洞 + + rule.central_highland_near_fluctuant_road.them = { x = 0.0, y = 0.0 } --高地靠近起伏路 + rule.central_highland_near_doghole.them = { x = 0.0, y = 0.0 } --高地靠近狗洞 + rule.fluctuant_road_final.them = { x = 0.0, y = 0.0 } --起伏路段终点 end local function reset_job_status() diff --git a/src/lua/intent/escape-to-home.lua b/src/lua/intent/escape-to-home.lua index 5be1c87..4392da4 100644 --- a/src/lua/intent/escape-to-home.lua +++ b/src/lua/intent/escape-to-home.lua @@ -1,7 +1,7 @@ local blackboard = require("blackboard").singleton() local action = require("action") -local go_down_onestep = require("task.go-down-onestep") -local cross_fluctuant_road = require("task.cross-fluctuant-road") +local go_down_onestep = require("task.one-step.go-down-onestep") +local cross_fluctuant_road = require("task.cross-fluctuant.cross-fluctuant-road") local navigate_to_point = require("task.navigate-to-point") --- @param route "direct"|"onestep"|"fluctuant_road" diff --git a/src/lua/intent/keep-cruise.lua b/src/lua/intent/keep-cruise.lua index 22687f5..21e8eed 100644 --- a/src/lua/intent/keep-cruise.lua +++ b/src/lua/intent/keep-cruise.lua @@ -1,5 +1,5 @@ local action = require("action") -local cruise_in_central_highlands = require("task.cruise-in-central-highlands") +local cruise_in_central_highlands = require("task.cruise-in-central-highland.cruise-in-central-highlands") --- 持续巡航:在中央高地两点间循环巡航(通常为长期运行)。 --- @param ours_zone boolean diff --git a/src/lua/task/cross-fluctuant-road.lua b/src/lua/task/cross-fluctuant/cross-fluctuant-road.lua similarity index 100% rename from src/lua/task/cross-fluctuant-road.lua rename to src/lua/task/cross-fluctuant/cross-fluctuant-road.lua diff --git a/src/lua/task/navigate-to-fluctuant-begin.lua b/src/lua/task/cross-fluctuant/navigate-to-fluctuant-begin.lua similarity index 100% rename from src/lua/task/navigate-to-fluctuant-begin.lua rename to src/lua/task/cross-fluctuant/navigate-to-fluctuant-begin.lua diff --git a/src/lua/task/cross-road-zone.lua b/src/lua/task/cross-road/cross-road-zone.lua similarity index 97% rename from src/lua/task/cross-road-zone.lua rename to src/lua/task/cross-road/cross-road-zone.lua index 13fe623..fefa51b 100644 --- a/src/lua/task/cross-road-zone.lua +++ b/src/lua/task/cross-road/cross-road-zone.lua @@ -47,5 +47,6 @@ return function(ours_zone, forward_center) return false end + action:update_chassis_mode("SPIN") return true end diff --git a/src/lua/task/cruise-in-central-highlands.lua b/src/lua/task/cruise-in-central-highland/cruise-in-central-highlands.lua similarity index 98% rename from src/lua/task/cruise-in-central-highlands.lua rename to src/lua/task/cruise-in-central-highland/cruise-in-central-highlands.lua index 8a5ebff..fc11ef4 100644 --- a/src/lua/task/cruise-in-central-highlands.lua +++ b/src/lua/task/cruise-in-central-highland/cruise-in-central-highlands.lua @@ -36,6 +36,7 @@ return function(ours_zone, switch_interval) while true do local phase_start = clock:now() + action:update_chassis_mode("SPIN") local ok = navigate_to_point(target, { tolerance = 0.1, timeout = navigation_timeout, diff --git a/src/lua/task/forward-press/forward-press-in-one-step.lua b/src/lua/task/forward-press/forward-press-in-one-step.lua new file mode 100644 index 0000000..63fd8ec --- /dev/null +++ b/src/lua/task/forward-press/forward-press-in-one-step.lua @@ -0,0 +1,28 @@ +local blackboard = require("blackboard").singleton() +local action = require("action") +local navigate_to_point = require("task.navigate-to-point") + +--- 前压至对方半场起伏路段终点 +--- @return boolean is_success +return function() + action:info("开始forward-press-in-one-step") + + local rule = blackboard.rule + local enemy_fluctuant_road_final = rule.fluctuant_road_final.them + + action:update_chassis_mode("SPIN") + ok = navigate_to_point(enemy_fluctuant_road_final, { + tolerance = 0.1, + timeout = 10, + }) + if not ok then + action:warn(string.format( + "forward-press-in-one-step: 导航到对方起伏路段终点失败 (x=%.2f, y=%.2f)", + enemy_fluctuant_road_final.x, + enemy_fluctuant_road_final.y + )) + return false + end + + return true +end diff --git a/src/lua/task/forward-press/forward-press-in-two-step.lua b/src/lua/task/forward-press/forward-press-in-two-step.lua new file mode 100644 index 0000000..35471fb --- /dev/null +++ b/src/lua/task/forward-press/forward-press-in-two-step.lua @@ -0,0 +1,53 @@ +local blackboard = require("blackboard").singleton() +local clock = require("util.clock") +local request = require("util.scheduler").request +local action = require("action") +local navigate_to_point = require("task.navigate-to-point") + +--- 前压至对方高地在二级台阶侧与高地增益点之间巡航。 +--- @param switch_interval number 切换周期(秒) +--- @return boolean is_success +return function(switch_interval) + assert(type(switch_interval) == "number", "switch_interval should be a number") + assert(switch_interval > 0, "switch_interval should be positive") + action:info("开始forward-press-in-two-step") + + local rule = blackboard.rule + local enemy_gain_point = rule.central_highland_gain_pount.them + local enemy_near_two_steps_and_outpost = rule.central_highland_near_two_steps_and_outpost.them + + local navigation_timeout = math.max(10.0, switch_interval * 2.0) + local target = enemy_gain_point + + while true do + local phase_start = clock:now() + action:update_chassis_mode("SPIN") + ok = navigate_to_point(target, { + tolerance = 0.1, + timeout = navigation_timeout, + }) + if not ok then + action:warn(string.format( + "forward-press-in-two-step: 导航到巡航点失败 (x=%.2f, y=%.2f, timeout=%.2fs)", + target.x, + target.y, + navigation_timeout + )) + return false + end + + local elapsed = clock:now() - phase_start + local remain = switch_interval - elapsed + if remain > 0 then + request:sleep(remain) + end + + if target == enemy_gain_point then + target = enemy_near_two_steps_and_outpost + else + target = enemy_gain_point + end + end + + return true +end diff --git a/src/lua/task/supply-ammunition.lua b/src/lua/task/guard-home/cruise-in-front-of-base.lua similarity index 100% rename from src/lua/task/supply-ammunition.lua rename to src/lua/task/guard-home/cruise-in-front-of-base.lua diff --git a/src/lua/task/occupy-fortress.lua b/src/lua/task/guard-home/occupy-fortress.lua similarity index 94% rename from src/lua/task/occupy-fortress.lua rename to src/lua/task/guard-home/occupy-fortress.lua index aaa9a6f..6c667c3 100644 --- a/src/lua/task/occupy-fortress.lua +++ b/src/lua/task/guard-home/occupy-fortress.lua @@ -15,6 +15,7 @@ return function(ours_zone) fortress = rule.fortress.them end + action:update_chassis_mode("SPIN") local is_success = navigate_to_point(fortress, { tolerance = 0.1, timeout = 10, diff --git a/src/lua/task/go-down-onestep.lua b/src/lua/task/one-step/go-down-onestep.lua similarity index 100% rename from src/lua/task/go-down-onestep.lua rename to src/lua/task/one-step/go-down-onestep.lua diff --git a/src/lua/task/supply/supply-ammunition.lua b/src/lua/task/supply/supply-ammunition.lua new file mode 100644 index 0000000..e69de29 From f0c20d9e74a5101e23df671d1f6fa7979fa85791 Mon Sep 17 00:00:00 2001 From: tcd <1369103595@qq.com> Date: Sat, 25 Apr 2026 21:49:22 +0800 Subject: [PATCH 12/12] feat(lua): add start-cruise intent, supply-health task, and blackboard conditions --- src/lua/blackboard.lua | 83 +++++++++++++++- src/lua/endpoint/train-decision.lua | 135 ++++++++++++++------------ src/lua/intent/keep-cruise.lua | 57 +++++++---- src/lua/intent/start-cruise.lua | 84 ++++++++++++++++ src/lua/task/supply/supply-health.lua | 39 ++++++++ 5 files changed, 319 insertions(+), 79 deletions(-) create mode 100644 src/lua/intent/start-cruise.lua create mode 100644 src/lua/task/supply/supply-health.lua diff --git a/src/lua/blackboard.lua b/src/lua/blackboard.lua index 992f860..1b5f71b 100644 --- a/src/lua/blackboard.lua +++ b/src/lua/blackboard.lua @@ -15,9 +15,32 @@ local function create_default_blackboard() x = 0, y = 0, yaw = 0, + + mode = "UNKNOWN", + }, game = { stage = "UNKNOWN", + + outpost_health = 0, -- 前哨站血量 + base_health = 0, -- 基地血量 + + hero_health = 150, + infantry_1_health = 150, + infantry_2_health = 150, + engineer_health = 250, + + remaining_time = 0, -- 比赛剩余时间 + gold_coin = 0, -- 队伍剩余金币数 + exchangeable_ammunition_quantity = 0, -- 队伍 17mm 允许发弹量的剩余可兑换数 + + our_dart_nmber_of_hits = 0, -- 己方飞镖击中次数 + double_damage_activated = false, -- 雷达双倍易伤是否开启 + fortress_occupied = false, -- 己方堡垒是否被占领 + big_energy_mechanism_activated = false, -- 大能量机关是否被激活 + small_energy_mechanism_activated = false, -- 小能量机关是否被激活 + + }, play = { rswitch = "UNKNOWN", @@ -26,18 +49,48 @@ local function create_default_blackboard() meta = { timestamp = 0, -- 秒 fsm_state = "unknown", + fsm_phase = "none", }, -- Static Information rule = { decision = "auxiliary", - -- 状态类规则 + -- 自身状态类规则 health_limit = 210, health_ready = 400, bullet_limit = 40, bullet_ready = 300, + mode = "movement", + + -- 其他状态类规则 + + -- 比赛相关 + time_of_the_competition = 420, --比赛剩余时间 + + -- 队伍资源相关 + exchangeable_ammunition_quantity = 1000, -- 队伍 17mm 允许发弹量的剩余可兑换数 + gold_coin = 400, -- 队伍剩余金币数 + + -- 前哨站相关 + outpost_health_ready = 1500, + outpost_health_red_line = 1500, + + -- 基地相关 + base_health_ready = 5000, + base_health_red_line = 2000, + + -- 友方机器人相关 + hero_health_ready = 150, + infantry_1_health_ready = 150, + infantry_2_health_ready = 150, + engineer_health_ready = 250, + + hero_health_ready_red_line = 50, + infantry_1_health_ready_red_line = 50, + infantry_2_health_ready_red_line = 50, + engineer_health_ready_red_line = 50, -- 坐标类规则 -- 定义顺序:ours = 0,them = 1 @@ -90,6 +143,34 @@ local function create_default_blackboard() return result.user.bullet >= result.rule.bullet_ready end, + base_in_danger = function () + return result.game.base_health <= result.rule.base_health_red_line + end, + + oupost_survival = function () + return result.game.outpost_health > 0 + end, + + dart_hit_first_time = function () + return result.game.our_dart_nmber_of_hits == 1 + end, + + double_damage_activated = function () + return result.game.double_damage_activated + end, + + fortress_occupied = function () + return result.game.fortress_occupied + end, + + big_energy_mechanism_activated = function () + return result.game.big_energy_mechanism_activated + end, + + small_energy_mechanism_activated = function () + return result.game.small_energy_mechanism_activated + end, + --- @param target {x: number, y: number} --- @param tolerance? number|{x: number, y: number} near = function(target, tolerance) diff --git a/src/lua/endpoint/train-decision.lua b/src/lua/endpoint/train-decision.lua index 50cc6e3..eca2a06 100644 --- a/src/lua/endpoint/train-decision.lua +++ b/src/lua/endpoint/train-decision.lua @@ -8,10 +8,9 @@ local clock = require("util.clock") local fsm = require("util.fsm") local option = require("option") -local keep_cruise = require("intent.keep-cruise") +local KeepCruiseIntent = require("intent.keep-cruise") +local StartCruiseIntent = require("intent.start-cruise") local escape_to_home = require("intent.escape-to-home") -local cross_fluctuant_road = require("task.cross-fluctuant.cross-fluctuant-road") -local navigate_to_fluctuant_begin = require("task.cross-fluctuant.navigate-to-fluctuant-begin") local Scheduler = require("util.scheduler") local scheduler = Scheduler.new() @@ -30,6 +29,8 @@ local runtime = { switch_interval = nil, escape_route = nil, current_state = "idle", + current_phase = "none", + current_intent = nil, navigation_ready = false, } @@ -125,6 +126,23 @@ local function set_state(name) action:info("fsm state -> " .. name) end +local function set_phase(name) + runtime.current_phase = name + blackboard.meta.fsm_phase = name + action:info("fsm phase -> " .. name) +end + +local function sync_intent_phase(intent) + assert(intent ~= nil, "intent should exist before syncing phase") + + if type(intent.phase_name) == "function" then + set_phase(intent:phase_name()) + return + end + + set_phase("none") +end + local function start_navigation() local ok, message = action:restart_navigation({ global_map = read_option("global_map", "train_map"), @@ -148,8 +166,7 @@ end local function create_intent_fsm() local State = { idle = "idle", - navigate_to_fluctuant_begin = "navigate_to_fluctuant_begin", - cross_fluctuant = "cross_fluctuant", + start_cruise = "start_cruise", keep_cruise = "keep_cruise", escape = "escape", recover = "recover", @@ -157,29 +174,36 @@ local function create_intent_fsm() local condition = blackboard.condition local intent_fsm = fsm:new(State.idle) - local function run_navigate_to_fluctuant_begin_job() - run_job("navigate_to_fluctuant_begin", function() - return navigate_to_fluctuant_begin(runtime.ours_zone, true) - end) - end - local function run_cross_fluctuant_job() - run_job("cross_fluctuant", function() - return cross_fluctuant_road(runtime.ours_zone, true) - end) - end - local function run_keep_cruise_job() - run_job("keep_cruise", function() - return keep_cruise(runtime.ours_zone, runtime.switch_interval) - end) - end local function run_escape_job() assert(type(runtime.escape_route) == "string", "escape_route should be set before escape") run_job("escape_to_home", function() return escape_to_home(runtime.escape_route) end) end - local function enter_escape(handle, route) - assert(type(route) == "string", "route should be a string") + + local function create_start_cruise_intent() + runtime.current_intent = StartCruiseIntent.new({ + ours_zone = runtime.ours_zone, + }) + sync_intent_phase(runtime.current_intent) + end + local function create_keep_cruise_intent() + runtime.current_intent = KeepCruiseIntent.new({ + ours_zone = runtime.ours_zone, + switch_interval = runtime.switch_interval, + }) + sync_intent_phase(runtime.current_intent) + end + local function run_current_intent_job() + assert(runtime.current_intent ~= nil, "current intent should exist before running intent job") + sync_intent_phase(runtime.current_intent) + runtime.current_intent:run(run_job) + end + local function enter_escape(handle) + local route = "direct" + if runtime.current_intent ~= nil then + route = runtime.current_intent:escape_route() + end runtime.escape_route = route cancel_job() handle:set_next(State.escape) @@ -190,7 +214,9 @@ local function create_intent_fsm() enter = function() cancel_job() runtime.escape_route = nil + runtime.current_intent = nil set_state(State.idle) + set_phase("none") runtime.navigation_ready = false end, event = function(handle) @@ -200,46 +226,21 @@ local function create_intent_fsm() end if runtime.navigation_ready and blackboard.game.stage == "STARTED" then - handle:set_next(State.navigate_to_fluctuant_begin) - end - end, - }) - - intent_fsm:use({ - state = State.navigate_to_fluctuant_begin, - enter = function() - set_state(State.navigate_to_fluctuant_begin) - run_navigate_to_fluctuant_begin_job() - end, - event = function(handle) - if condition.low_health() or condition.low_bullet() then - enter_escape(handle, "direct") - return + handle:set_next(State.start_cruise) end - - if not job.done then - return - end - - if job.success then - handle:set_next(State.cross_fluctuant) - return - end - - action:warn("fsm(navigate_to_fluctuant_begin): 导航失败,重试当前阶段") - run_navigate_to_fluctuant_begin_job() end, }) intent_fsm:use({ - state = State.cross_fluctuant, + state = State.start_cruise, enter = function() - set_state(State.cross_fluctuant) - run_cross_fluctuant_job() + set_state(State.start_cruise) + create_start_cruise_intent() + run_current_intent_job() end, event = function(handle) if condition.low_health() or condition.low_bullet() then - enter_escape(handle, "fluctuant_road") + enter_escape(handle) return end @@ -248,12 +249,19 @@ local function create_intent_fsm() end if job.success then - handle:set_next(State.keep_cruise) + if runtime.current_intent:advance() then + run_current_intent_job() + else + handle:set_next(State.keep_cruise) + end return end - action:warn("fsm(cross_fluctuant): 导航失败,重试当前阶段") - run_cross_fluctuant_job() + action:warn(string.format( + "fsm(start_cruise:%s): 导航失败,重试当前阶段", + runtime.current_phase + )) + run_current_intent_job() end, }) @@ -261,11 +269,12 @@ local function create_intent_fsm() state = State.keep_cruise, enter = function() set_state(State.keep_cruise) - run_keep_cruise_job() + create_keep_cruise_intent() + run_current_intent_job() end, event = function(handle) if condition.low_health() or condition.low_bullet() then - enter_escape(handle, "onestep") + enter_escape(handle) return end @@ -278,7 +287,7 @@ local function create_intent_fsm() end action:warn("fsm(keep_cruise): 导航失败,重试当前状态") - run_keep_cruise_job() + run_current_intent_job() end, }) @@ -286,6 +295,7 @@ local function create_intent_fsm() state = State.escape, enter = function() set_state(State.escape) + set_phase("none") run_escape_job() end, event = function(handle) @@ -308,7 +318,9 @@ local function create_intent_fsm() enter = function() cancel_job() runtime.escape_route = nil + runtime.current_intent = nil set_state(State.recover) + set_phase("none") end, event = function(handle) if condition.low_health() or condition.low_bullet() then @@ -316,7 +328,7 @@ local function create_intent_fsm() end if condition.health_ready() and condition.bullet_ready() then - handle:set_next(State.navigate_to_fluctuant_begin) + handle:set_next(State.start_cruise) end end, }) @@ -355,8 +367,9 @@ on_init = function() while true do request:sleep(1.0) action:info(string.format( - "fsm=%s stage=%s hp=%s bullet=%s rs=%s ls=%s", + "fsm=%s phase=%s stage=%s hp=%s bullet=%s rs=%s ls=%s", runtime.current_state, + runtime.current_phase, blackboard.game.stage, tostring(blackboard.user.health), tostring(blackboard.user.bullet), diff --git a/src/lua/intent/keep-cruise.lua b/src/lua/intent/keep-cruise.lua index 21e8eed..25ec5f9 100644 --- a/src/lua/intent/keep-cruise.lua +++ b/src/lua/intent/keep-cruise.lua @@ -1,21 +1,44 @@ local action = require("action") local cruise_in_central_highlands = require("task.cruise-in-central-highland.cruise-in-central-highlands") ---- 持续巡航:在中央高地两点间循环巡航(通常为长期运行)。 ---- @param ours_zone boolean ---- @param switch_interval number 中央高地巡航切换周期(秒) ---- @return boolean is_success -return function(ours_zone, switch_interval) - assert(type(ours_zone) == "boolean", "ours_zone should be a boolean") - assert(type(switch_interval) == "number", "switch_interval should be a number") - assert(switch_interval > 0, "switch_interval should be positive") - - action:info("keep-cruise: 进入中央高地持续巡航") - local ok = cruise_in_central_highlands(ours_zone, switch_interval) - if not ok then - action:warn("keep-cruise: 中央高地巡航导航失败") - return false - end - - return true +local KeepCruiseIntent = {} +KeepCruiseIntent.__index = KeepCruiseIntent + +local M = {} + +--- @param args { ours_zone: boolean, switch_interval: number } +--- @return table +function M.new(args) + assert(type(args) == "table", "args should be a table") + assert(type(args.ours_zone) == "boolean", "args.ours_zone should be a boolean") + assert(type(args.switch_interval) == "number", "args.switch_interval should be a number") + assert(args.switch_interval > 0, "args.switch_interval should be positive") + + return setmetatable({ + ours_zone = args.ours_zone, + switch_interval = args.switch_interval, + }, KeepCruiseIntent) +end + +--- @return "onestep" +function KeepCruiseIntent:escape_route() + return "onestep" end + +--- @param run_job fun(name: string, fn: function) +function KeepCruiseIntent:run(run_job) + assert(type(run_job) == "function", "run_job should be a function") + + run_job("keep_cruise", function() + action:info("keep-cruise: 进入中央高地持续巡航") + local ok = cruise_in_central_highlands(self.ours_zone, self.switch_interval) + if not ok then + action:warn("keep-cruise: 中央高地巡航导航失败") + return false + end + + return true + end) +end + +return M diff --git a/src/lua/intent/start-cruise.lua b/src/lua/intent/start-cruise.lua new file mode 100644 index 0000000..ca065f0 --- /dev/null +++ b/src/lua/intent/start-cruise.lua @@ -0,0 +1,84 @@ +local cross_fluctuant_road = require("task.cross-fluctuant.cross-fluctuant-road") +local navigate_to_fluctuant_begin = require("task.cross-fluctuant.navigate-to-fluctuant-begin") + +local StartCruiseIntent = {} +StartCruiseIntent.__index = StartCruiseIntent + +local Phase = { + to_fluctuant_begin = "to_fluctuant_begin", + crossing_fluctuant = "crossing_fluctuant", +} + +local function unknown_phase_error(phase) + error("unknown start-cruise intent phase: " .. tostring(phase)) +end + +local M = { + Phase = Phase, +} + +--- @param args { ours_zone: boolean } +--- @return table +function M.new(args) + assert(type(args) == "table", "args should be a table") + assert(type(args.ours_zone) == "boolean", "args.ours_zone should be a boolean") + + return setmetatable({ + ours_zone = args.ours_zone, + phase = Phase.to_fluctuant_begin, + }, StartCruiseIntent) +end + +--- @return string +function StartCruiseIntent:phase_name() + return self.phase +end + +--- @return "direct"|"fluctuant_road"|"onestep" +function StartCruiseIntent:escape_route() + if self.phase == Phase.to_fluctuant_begin then + return "direct" + end + if self.phase == Phase.crossing_fluctuant then + return "fluctuant_road" + end + + unknown_phase_error(self.phase) +end + +--- @param run_job fun(name: string, fn: function) +function StartCruiseIntent:run(run_job) + assert(type(run_job) == "function", "run_job should be a function") + + if self.phase == Phase.to_fluctuant_begin then + run_job("navigate_to_fluctuant_begin", function() + return navigate_to_fluctuant_begin(self.ours_zone, true) + end) + return + end + + if self.phase == Phase.crossing_fluctuant then + run_job("cross_fluctuant", function() + return cross_fluctuant_road(self.ours_zone, true) + end) + return + end + + unknown_phase_error(self.phase) +end + +--- @return boolean has_next_phase +function StartCruiseIntent:advance() + if self.phase == Phase.to_fluctuant_begin then + self.phase = Phase.crossing_fluctuant + return true + end + + if self.phase == Phase.crossing_fluctuant then + return false + end + + unknown_phase_error(self.phase) +end + +return M diff --git a/src/lua/task/supply/supply-health.lua b/src/lua/task/supply/supply-health.lua new file mode 100644 index 0000000..d57eae5 --- /dev/null +++ b/src/lua/task/supply/supply-health.lua @@ -0,0 +1,39 @@ +local blackboard = require("blackboard").singleton() +local request = require("util.scheduler").request +local action = require("action") + +local POLL_INTERVAL = 0.2 + +--- 在补血点等待,直到血量达到 ready 阈值。 +--- @return boolean is_success +return function() + local condition = blackboard.condition + local health_ready = blackboard.rule.health_ready + assert(type(condition.health_ready) == "function", "blackboard.condition.health_ready should be a function") + assert(type(health_ready) == "number", "blackboard.rule.health_ready should be a number") + + if condition.health_ready() then + action:info(string.format( + "supply-health: 当前已达到补血完成阈值 (health=%s, health_ready=%s)", + tostring(blackboard.user.health), + tostring(health_ready) + )) + return true + end + + action:info(string.format( + "supply-health: 开始等待补血 (health=%s, health_ready=%s)", + tostring(blackboard.user.health), + tostring(health_ready) + )) + + while not condition.health_ready() do + request:sleep(POLL_INTERVAL) + end + + action:info(string.format( + "supply-health: 血量已达到补血完成阈值,结束等待 (health=%s)", + tostring(blackboard.user.health) + )) + return true +end