diff --git a/maps/train_map.png b/maps/train_map.png new file mode 100644 index 0000000..4492aaa Binary files /dev/null and b/maps/train_map.png differ diff --git a/maps/train_map.yaml b/maps/train_map.yaml new file mode 100644 index 0000000..b5f3a7c --- /dev/null +++ b/maps/train_map.yaml @@ -0,0 +1,7 @@ +image: train_map.png +mode: trinary +resolution: 0.05 +origin: [-1.2, -6.3, 0.0] +negate: 0 +occupied_thresh: 0.65 +free_thresh: 0.196 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 dea3bcb..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", @@ -25,18 +48,49 @@ 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, --比赛剩余时间 - health_limit = 0, - health_ready = 0, - bullet_limit = 0, - bullet_ready = 0, + -- 队伍资源相关 + 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 @@ -50,14 +104,20 @@ 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_fluctuant_road = 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 } }, -- 二级台阶 + 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 } }, -- 起伏路段 + 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 } }, }, @@ -83,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/test.lua b/src/lua/endpoint/test.lua index 9c77640..ac8fd91 100644 --- a/src/lua/endpoint/test.lua +++ b/src/lua/endpoint/test.lua @@ -60,4 +60,4 @@ end --- 由 NAV2 发布的目标速度值,在此处理回调 on_control = function(x, y, _) action:update_chassis_vel(x, y) -end +end \ No newline at end of file diff --git a/src/lua/endpoint/train-decision.lua b/src/lua/endpoint/train-decision.lua new file mode 100644 index 0000000..eca2a06 --- /dev/null +++ b/src/lua/endpoint/train-decision.lua @@ -0,0 +1,400 @@ +--- +--- 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 KeepCruiseIntent = require("intent.keep-cruise") +local StartCruiseIntent = require("intent.start-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 = nil, + switch_interval = nil, + escape_route = nil, + current_state = "idle", + current_phase = "none", + current_intent = nil, + 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) + + -- 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 } --起伏路段终点 + 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_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() + 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 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"), + 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) + 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 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) + end + + intent_fsm:use({ + state = State.idle, + 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) + 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) + create_start_cruise_intent() + run_current_intent_job() + end, + event = function(handle) + if condition.low_health() or condition.low_bullet() then + enter_escape(handle) + return + end + + if not job.done then + return + end + + if job.success then + if runtime.current_intent:advance() then + run_current_intent_job() + else + handle:set_next(State.keep_cruise) + end + return + end + + action:warn(string.format( + "fsm(start_cruise:%s): 导航失败,重试当前阶段", + runtime.current_phase + )) + run_current_intent_job() + end, + }) + + intent_fsm:use({ + state = State.keep_cruise, + enter = function() + set_state(State.keep_cruise) + create_keep_cruise_intent() + run_current_intent_job() + end, + event = function(handle) + if condition.low_health() or condition.low_bullet() then + enter_escape(handle) + return + end + + if not job.done then + return + end + + if job.success then + return + end + + action:warn("fsm(keep_cruise): 导航失败,重试当前状态") + run_current_intent_job() + end, + }) + + intent_fsm:use({ + state = State.escape, + enter = function() + set_state(State.escape) + set_phase("none") + 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() + 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 + 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", 5.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 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), + 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..4392da4 100644 --- a/src/lua/intent/escape-to-home.lua +++ b/src/lua/intent/escape-to-home.lua @@ -0,0 +1,47 @@ +local blackboard = require("blackboard").singleton() +local action = require("action") +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" +--- @return boolean is_success +return function(route) + assert(type(route) == "string", "route should be a string") + + local resupply_zone = blackboard.rule.resupply_zone.ours + local is_success = true + + if route == "onestep" then + 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: 走起伏路路线回家") + 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:warn("unknown escape route: " .. tostring(route)) + end + + action:update_chassis_mode("SPIN") + 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..25ec5f9 --- /dev/null +++ b/src/lua/intent/keep-cruise.lua @@ -0,0 +1,44 @@ +local action = require("action") +local cruise_in_central_highlands = require("task.cruise-in-central-highland.cruise-in-central-highlands") + +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/cross-fluctuant/cross-fluctuant-road.lua b/src/lua/task/cross-fluctuant/cross-fluctuant-road.lua new file mode 100644 index 0000000..2397420 --- /dev/null +++ b/src/lua/task/cross-fluctuant/cross-fluctuant-road.lua @@ -0,0 +1,53 @@ +local blackboard = require("blackboard").singleton() +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("开始cross-fluctuant-road") + + 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 to, gimbal_yaw + if forward_center then + to = final + gimbal_yaw = 0 + else + to = begin + gimbal_yaw = math.pi + end + + action:update_chassis_mode("LAUNCH_RAMP") + action:info(string.format( + "cross-fluctuant-road: LAUNCH_RAMP 云台朝向=%.3f rad", + gimbal_yaw + )) + action:update_gimbal_direction(gimbal_yaw) + local ok = navigate_to_point(to, { + tolerance = 0.1, + timeout = 10, + }) + if not ok then + action:warn(string.format( + "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/cross-fluctuant/navigate-to-fluctuant-begin.lua b/src/lua/task/cross-fluctuant/navigate-to-fluctuant-begin.lua new file mode 100644 index 0000000..320038f --- /dev/null +++ b/src/lua/task/cross-fluctuant/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/cross-road/cross-road-zone.lua b/src/lua/task/cross-road/cross-road-zone.lua new file mode 100644 index 0000000..fefa51b --- /dev/null +++ b/src/lua/task/cross-road/cross-road-zone.lua @@ -0,0 +1,52 @@ +local blackboard = require("blackboard").singleton() +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("开始cross-road-zone") + + 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 from, to + if forward_center then + from = road_begin + to = road_final + else + from = road_final + to = road_begin + end + + local ok = navigate_to_point(from, { + tolerance = 0.1, + timeout = 10, + }) + if not ok then + action:warn("cross-road-zone: 导航到公路区起点失败") + return false + end + + ok = navigate_to_point(to, { + tolerance = 0.1, + timeout = 10, + }) + if not ok then + action:warn("cross-road-zone: 导航到公路区终点失败") + 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/crossing-road-zone.lua deleted file mode 100644 index aa72fcc..0000000 --- a/src/lua/task/crossing-road-zone.lua +++ /dev/null @@ -1,39 +0,0 @@ -local blackboard = require("blackboard").singleton() -local request = require("util.scheduler").request -local action = require("action") - ---- @param ours_zone boolean ---- @param forward_center boolean -return function(ours_zone, forward_center) - local x = blackboard.user.x - local y = blackboard.user.y - - local rule = blackboard.rule - local begin, final - if ours_zone then - begin = rule.road_zone_begin.ours - final = rule.road_zone_final.ours - else - begin = rule.road_zone_begin.them - final = rule.road_zone_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 timeout = request:wait_until { - monitor = function() - return condition.near(from, 0.1) - end, - timeout = 10, - } -end diff --git a/src/lua/task/cruise-in-central-highland/cruise-in-central-highlands.lua b/src/lua/task/cruise-in-central-highland/cruise-in-central-highlands.lua new file mode 100644 index 0000000..fc11ef4 --- /dev/null +++ b/src/lua/task/cruise-in-central-highland/cruise-in-central-highlands.lua @@ -0,0 +1,69 @@ +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 + 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 navigation_timeout = math.max(10.0, switch_interval * 2.0) + local near_fluctuant_road, near_doghole + if ours_zone then + near_fluctuant_road = rule.central_highland_near_fluctuant_road.ours + near_doghole = rule.central_highland_near_doghole.ours + else + near_fluctuant_road = rule.central_highland_near_fluctuant_road.them + near_doghole = rule.central_highland_near_doghole.them + end + + -- 首次优先去更近的点,减少无效折返。 + 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() + action:update_chassis_mode("SPIN") + local ok = navigate_to_point(target, { + tolerance = 0.1, + timeout = navigation_timeout, + }) + if not ok 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_fluctuant_road then + target = near_doghole + else + target = near_fluctuant_road + end + end + + return true +end 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/guard-home/cruise-in-front-of-base.lua b/src/lua/task/guard-home/cruise-in-front-of-base.lua new file mode 100644 index 0000000..e69de29 diff --git a/src/lua/task/guard-home/occupy-fortress.lua b/src/lua/task/guard-home/occupy-fortress.lua new file mode 100644 index 0000000..6c667c3 --- /dev/null +++ b/src/lua/task/guard-home/occupy-fortress.lua @@ -0,0 +1,29 @@ +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 + + action:update_chassis_mode("SPIN") + 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/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/one-step/go-down-onestep.lua b/src/lua/task/one-step/go-down-onestep.lua new file mode 100644 index 0000000..199a064 --- /dev/null +++ b/src/lua/task/one-step/go-down-onestep.lua @@ -0,0 +1,50 @@ +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 + + 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, + }) + if not ok then + action:warn("go-down-onestep: 导航到一级台阶低点失败") + return false + end + action:update_chassis_mode("SPIN") + + return true +end 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 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 diff --git a/src/lua/task/switch-mode.lua b/src/lua/task/switch-mode.lua new file mode 100644 index 0000000..e69de29