diff --git a/logic/ClientTest/program.cs b/logic/ClientTest/program.cs index 6ccfe5f9..a8ffd03e 100644 --- a/logic/ClientTest/program.cs +++ b/logic/ClientTest/program.cs @@ -2,10 +2,9 @@ using Protobuf; // ============================================================================ -// 完整流程测试:召唤角色 → 寻路采集 → 工厂生产 → 返厂装载 → 前往市场售卖 +// 价格科技测试:采资源 → 生产2 Food → 装载 → 卖1 → 升级售价科技 → 卖1 // -// 运行前提:服务端需用 --gameTimeInSecond 60(以上)启动,否则默认 10s 不够一轮循环 -// 参数: [characterId] (characterId 默认 1) +// 运行:dotnet run --project logic/ClientTest -- // ============================================================================ namespace ClientTest @@ -14,12 +13,9 @@ public class Program { private const int CellSize = 1000; private const int CellCenter = 500; - // 到达阈值:300 game-units(< 1 cell),保证落在目标格内 private const double ArrivalRadius = 300.0; + private const GoodsType Food = GoodsType.Food; - // ──────────────────────────────────────────────────────────────────── - // SharedState:由帧读取线程更新,主任务线程只读 - // ──────────────────────────────────────────────────────────────────── private sealed class SharedState { private readonly object _lk = new(); @@ -30,12 +26,12 @@ private sealed class SharedState private bool _hasPos; private int _charX, _charY; - private int _currentLoad; - private int _material; - private bool _factoryCanProduce = true; - // 本队工厂在游戏坐标系中的位置(首帧更新后有效) + private long _computingPower; + private long _teamScore; private int _facX = -1, _facY = -1; + private int _material; private readonly Dictionary _facGoods = new(); + private bool _factoryCanProduce = true; public Task GameStartTask => _gameStartTcs.Task; public Task CharSeenTask => _charSeenTcs.Task; @@ -45,33 +41,23 @@ public void ApplyFrame(MessageToClient frame, long teamId, long charId) if (frame.GameState is GameState.GameStart or GameState.GameRunning) _gameStartTcs.TrySetResult(true); - // ── 队伍经济 ────────────────────────────────────────────── - if (frame.AllMessage != null) - { - int idx = (int)teamId - 1; - if ((uint)idx < (uint)frame.AllMessage.Teams.Count) - lock (_lk) { _material = frame.AllMessage.Teams[idx].Material; } - } - foreach (var obj in frame.ObjMessage) { - // ── 本队工厂 ────────────────────────────────────────── var fac = obj.FactoryMessage; if (fac != null && fac.TeamId == teamId) { lock (_lk) { - _factoryCanProduce = fac.CanProduce; + _computingPower = fac.ComputingPower; _facX = fac.X; _facY = fac.Y; + _factoryCanProduce = fac.CanProduce; _facGoods.Clear(); - // GoodsStack 字段:ProductType (GoodsType) + Quantity (int) foreach (var gs in fac.ProductInventory) _facGoods[gs.ProductType] = gs.Quantity; } } - // ── 本角色 ──────────────────────────────────────────── var ch = obj.CharacterMessage; if (ch != null && ch.TeamId == teamId && ch.PlayerId == charId) { @@ -80,35 +66,46 @@ public void ApplyFrame(MessageToClient frame, long teamId, long charId) _hasPos = true; _charX = ch.X; _charY = ch.Y; - _currentLoad = ch.CurrentLoad; } _charSeenTcs.TrySetResult(true); } } + + if (frame.AllMessage != null) + { + int idx = (int)teamId - 1; + if ((uint)idx < (uint)frame.AllMessage.Teams.Count) + { + lock (_lk) + { + _material = frame.AllMessage.Teams[idx].Material; + _teamScore = frame.AllMessage.Teams[idx].Score; + } + } + } } public bool TryGetPos(out int x, out int y) { lock (_lk) { x = _charX; y = _charY; return _hasPos; } } + public long ComputingPower { get { lock (_lk) return _computingPower; } } + public long TeamScore { get { lock (_lk) return _teamScore; } } public int Material { get { lock (_lk) return _material; } } - public bool FactoryCanProduce { get { lock (_lk) return _factoryCanProduce; } } - public int CurrentLoad { get { lock (_lk) return _currentLoad; } } public bool TryGetFactoryPos(out int x, out int y) { lock (_lk) { x = _facX; y = _facY; return _facX >= 0; } } public int GetFactoryGoods(GoodsType type) { lock (_lk) return _facGoods.GetValueOrDefault(type, 0); } + + public bool FactoryCanProduce { get { lock (_lk) return _factoryCanProduce; } } } - // ──────────────────────────────────────────────────────────────────── - // Main:负责连接、注册、清理;游戏逻辑委托给 RunAsync - // ──────────────────────────────────────────────────────────────────── public static async Task Main(string[] args) { if (args.Length < 2) { - Console.WriteLine("Usage: ClientTest [characterId]"); + Console.WriteLine("Usage: ClientTest "); return; } if (!long.TryParse(args[0], out long playerId) || @@ -117,7 +114,7 @@ public static async Task Main(string[] args) Console.WriteLine("Invalid arguments."); return; } - long charId = args.Length >= 3 && long.TryParse(args[2], out long cid) ? cid : 1L; + long charId = 1; var channel = new Channel("127.0.0.1:8888", ChannelCredentials.Insecure); await channel.ConnectAsync(DateTime.UtcNow.AddSeconds(5)); @@ -140,7 +137,7 @@ public static async Task Main(string[] args) catch (OperationCanceledException) { } catch (Exception ex) { - Console.WriteLine($"[EXCEPTION] {ex.Message}"); + Console.WriteLine($"[EXCEPTION] {ex}"); } finally { @@ -150,9 +147,6 @@ public static async Task Main(string[] args) } } - // ──────────────────────────────────────────────────────────────────── - // 核心流程:7 步完整循环 - // ──────────────────────────────────────────────────────────────────── private static async Task RunAsync( AvailableService.AvailableServiceClient client, SharedState state, @@ -161,13 +155,13 @@ private static async Task RunAsync( { var ct = cts.Token; - // ── [1] 等待游戏开始 ────────────────────────────────────────── + // [1] 等待游戏开始 Log("Waiting for game start..."); if (!await TimeoutTask(state.GameStartTask, 30, ct)) - { Fail(cts, "Game start timeout."); return; } + { Log("[FAIL] Game start timeout."); return; } + Log("[OK] Game started."); - // ── [2] 召唤角色 ────────────────────────────────────────────── - // 诊断:CreateCharacterRID 可取回分配的 PlayerId;这里用 CreateCharacter 固定 Id + // [2] 召唤角色 Log($"Creating Robot (charId={charId})..."); var createRes = client.CreateCharacter(new CreateCharacterMsg { @@ -176,164 +170,163 @@ private static async Task RunAsync( CharacterType = CharacterType.Robot }); if (!createRes.ActSuccess) - { Fail(cts, "CreateCharacter failed (工厂可能无 Material 或未开局)."); return; } + { Log("[FAIL] CreateCharacter failed."); return; } if (!await TimeoutTask(state.CharSeenTask, 10, ct)) - { Fail(cts, "Character not seen in frame within 10s."); return; } - Log(" Character spawned."); + { Log("[FAIL] Character not seen in frame."); return; } + Log("[OK] Character spawned."); - // ── [3] 获取地图,寻路到最近资源 ───────────────────────────── + // [3] 获取地图 var map = client.GetMap(new NullRequest()); - Log("Navigating to nearest resource..."); + + // [4] 寻路到最近资源并采集 + Log("Navigating to nearest Resource..."); if (!await NavigateToType(client, state, map, teamId, charId, PlaceType.Resource, ct)) - { Fail(cts, "Failed to reach any resource."); return; } - - // ── [4] 开始采集 ────────────────────────────────────────────── - // 诊断 A:Harvest 不需要 ResourceId,服务端用 OneForInteract 按位置找最近资源 - // 诊断 B:资源可能在导航途中被耗尽,重试 15 次兜底 - Log("Starting harvest..."); - bool harvesting = false; - for (int i = 0; i < 15 && !harvesting && !ct.IsCancellationRequested; i++) + { Log("[FAIL] Failed to reach Resource."); return; } + Log("[OK] Arrived at Resource."); + + // 持续采集直到 material 足够生产 2 个 Food + Log("Harvesting resources..."); + int targetMaterial = 10 * 2; // Food cost = 10 each (CostFood), need 20 total + var harvestDeadline = DateTime.UtcNow.AddSeconds(30); + while (DateTime.UtcNow < harvestDeadline && !ct.IsCancellationRequested) { - var hr = client.Harvest(new ResourceMsg { TeamId = teamId, PlayerId = charId }); - if (hr.ActSuccess) harvesting = true; - else await Task.Delay(150, ct); - } - if (!harvesting) - { Fail(cts, "Harvest failed(角色可能不在资源旁边,或资源已耗尽)."); return; } - Log(" Harvest running."); - - // ── [5] 等待 Material 足够,下达生产命令 ───────────────────── - // 选择:Semiconductor(成本 10,生产时间 5s,基础价格 80,收益最高) - const GoodsType Product = GoodsType.Semiconductor; - const int ProduceCost = 10; // CostSemiconductor - - Log($"Waiting for material >= {ProduceCost}..."); - // 诊断:material 通过 AllMessage.Teams[idx].Material 推送,约 50ms 延迟 - if (!await WaitFor(() => state.Material >= ProduceCost, 90, ct)) - { Fail(cts, $"Material still {state.Material} after 90s."); return; } - Log($" Material = {state.Material}. Ready to produce."); - - // ── [6] 工厂生产 ────────────────────────────────────────────── - // 诊断 C:必须先等 CanProduce=true;游戏一开始 CanProduce 可能为 false - // (工厂被攻击后有一段 disable 时间) - Log("Issuing Produce command..."); - await WaitFor(() => state.FactoryCanProduce, 15, ct); - - var produceRes = client.Produce(new ProduceGoodsMsg - { - TeamId = teamId, - ProductType = Product, - MaxProduceNum = 1 - }); - Log($" Produce: {(produceRes.ActSuccess ? "OK" : "FAIL")}"); + if (state.Material >= targetMaterial) break; - if (produceRes.ActSuccess) - { - // 诊断 D:Produce 成功后的下一帧 CanProduce 可能仍为 true(旧帧值), - // 加 150ms 延迟让"繁忙帧"先到达,再等恢复 - await Task.Delay(150, ct); - bool finished = await WaitFor(() => state.FactoryCanProduce, 30, ct); - if (!finished) - Log(" WARNING: factory still busy after 30s, continuing anyway."); - - // 同时确认仓库里确实有货(FactoryMessage.ProductInventory 已更新) - await WaitFor(() => state.GetFactoryGoods(Product) >= 1, 10, ct); - Log($" Factory inventory: {Product} x{state.GetFactoryGoods(Product)}"); + var hr = client.Harvest(new ResourceMsg + { + TeamId = teamId, + PlayerId = charId, + ResourceId = 0, + Amount = 0 + }); + await Task.Delay(600, ct); } + Log($"Material after harvest: {state.Material} (target {targetMaterial})"); - // ── [7] 停止采集,导航回本队工厂 ───────────────────────────── - // 诊断 E:EndAllAction 在服务端 ActionLock 内同步将角色状态清为 NULL, - // 所以 Move 在其后立即到达服务端时不会被 HARVESTING 状态拒绝。 - // 100ms 延迟是额外安全余量。 - Log("Stopping harvest, navigating to own factory..."); - client.EndAllAction(new IDMsg { PlayerId = charId, TeamId = teamId }); - await Task.Delay(100, ct); + if (state.Material < targetMaterial) + { Log("[FAIL] Not enough material."); return; } - if (!state.TryGetFactoryPos(out int facX, out int facY)) - { Fail(cts, "Factory position not seen in any frame yet."); return; } + // [5] 生产 2 单位 Food(每单位需等上一次生产完成) + Log("Producing 2x Food at factory..."); + for (int i = 0; i < 2 && !ct.IsCancellationRequested; i++) + { + // 等工厂空闲(CanProduce 由流异步更新,Produce 后需给帧时间) + await WaitFor(refreshMs: 400, timeoutSec: 12, ct, + () => state.FactoryCanProduce, + desc: $"CanProduce before produce #{i + 1}"); - int facRow = facX / CellSize; - int facCol = facY / CellSize; - Log($" Own factory cell: [{facRow}, {facCol}]"); + var pr = client.Produce(new ProduceGoodsMsg + { + TeamId = teamId, + ProductType = Food, + MaxProduceNum = 1 + }); + Log($" Produce #{i + 1}: {(pr.ActSuccess ? "OK" : "FAIL")}"); - if (!await NavigateToCell(client, state, map, teamId, charId, facRow, facCol, ct)) - { Fail(cts, "Failed to reach own factory."); return; } + if (!pr.ActSuccess) continue; - if (!await WaitFor(() => - { - if (!state.TryGetPos(out int x, out int y)) return false; - return Math.Abs(x / CellSize - facRow) <= 1 && Math.Abs(y / CellSize - facCol) <= 1; - }, 8, ct)) - { Fail(cts, "Character is not close enough to own factory for Load."); return; } - - // ── [8] 装载货物 ────────────────────────────────────────────── - // 诊断 F:Load 用 OneForInteract 找"最近工厂",不检查团队归属。 - // 若敌方工厂更近,会从敌方工厂扣货(即使为空也会失败)。 - // 通过导航到 TryGetFactoryPos 返回的坐标,保证本队工厂是最近的。 - // 诊断 G:Load 要求 amount > 0,且工厂仓库 >= amount;若生产未完成则失败。 - Log($"Loading {Product} x1 from factory..."); - bool loaded = false; - for (int i = 0; i < 15 && !loaded && !ct.IsCancellationRequested; i++) + // 等生产完成 + 流推送库存更新 + await WaitFor(refreshMs: 400, timeoutSec: 15, ct, + () => state.FactoryCanProduce && state.GetFactoryGoods(Food) > i, + desc: $"produce #{i + 1} complete"); + } + Log($"Factory Food stock: {state.GetFactoryGoods(Food)}, CP: {state.ComputingPower}"); + + // [6] 导航回工厂装载 2 单位 Food + Log("Navigating to Factory..."); + if (!state.TryGetFactoryPos(out int fx, out int fy)) + { Log("[FAIL] Factory position unknown."); return; } + if (!await NavigateToCell(client, state, teamId, charId, fx / CellSize, fy / CellSize, map, ct)) + { Log("[FAIL] Failed to reach Factory."); return; } + Log("[OK] Arrived at Factory."); + + Log("Loading 2x Food..."); + for (int i = 0; i < 2 && !ct.IsCancellationRequested; i++) { var lr = client.Load(new LoadMsg { TeamId = teamId, PlayerId = charId, - ProductType = Product, + ProductType = Food, ProductAmount = 1 }); - if (lr.ActSuccess) loaded = true; - else - { - Log($" Load attempt {i + 1} failed (factory goods={state.GetFactoryGoods(Product)}, load={state.CurrentLoad})"); - await Task.Delay(200, ct); - } + Log($" Load #{i + 1}: {(lr.ActSuccess ? "OK" : "FAIL")}"); + await Task.Delay(200, ct); } - if (!loaded) - { Fail(cts, "Load failed after 15 attempts."); return; } - Log($" Load succeeded. CurrentLoad={state.CurrentLoad}"); - // ── [9] 寻路到最近市场 ──────────────────────────────────────── - Log("Navigating to nearest market..."); + // [7] 寻路到最近市场 + Log("Navigating to nearest Market..."); if (!await NavigateToType(client, state, map, teamId, charId, PlaceType.Market, ct)) - { Fail(cts, "Failed to reach any market."); return; } - - // ── [10] 售卖货物 ────────────────────────────────────────────── - // 诊断 H:Trade 同样用 OneForInteract 找最近市场,ApproachToInteract 检查 - // 格坐标 Chebyshev 距离 ≤ 1(即角色所在格与目标格相邻即可)。 - // BFS 导航到目标格的相邻格,满足此条件。 - // 诊断 I:市场价格有衰减机制(同类型商品大量交易后价格下降), - // Semiconductor 基础价 80,小市场 x1.1 = 88 分;尽早卖出更划算。 - Log($"Selling {Product} x1..."); - bool sold = false; - for (int i = 0; i < 15 && !sold && !ct.IsCancellationRequested; i++) + { Log("[FAIL] Failed to reach Market."); return; } + Log("[OK] Arrived at Market."); + + // [8] 卖出 1 单位 Food(升级前基准价,看 Score 增长) + long scoreBefore1 = state.TeamScore; + Log($"Selling 1x Food (before price upgrade), Score before: {scoreBefore1}..."); + var tr1 = client.Trade(new TradeMsg { - var tr = client.Trade(new TradeMsg - { - TeamId = teamId, - PlayerId = charId, - ProductType = Product, - ProductAmount = 1, - IsBuy = false // 卖出 - }); - if (tr.ActSuccess) sold = true; - else - { - Log($" Trade attempt {i + 1} failed"); - await Task.Delay(200, ct); - } + TeamId = teamId, + PlayerId = charId, + ProductType = Food, + ProductAmount = 1, + IsBuy = false + }); + await Task.Delay(500, ct); + long scoreAfter1 = state.TeamScore; + long sell1Gain = scoreAfter1 - scoreBefore1; + Log($" Sell #1: {(tr1.ActSuccess ? "OK" : "FAIL")}, Score +{sell1Gain} (CP={state.ComputingPower})"); + + // [9] 攒够 80 CP 来升级(Trade 加的是 Score 不是 CP,CP 靠工厂自然生成) + if (state.ComputingPower < 80) + { + Log($"CP ({state.ComputingPower}) < 80, waiting for factory CP generation..."); + await GrindCP(client, state, teamId, charId, map, ct); + } + + await Task.Delay(400, ct); + long cpBeforeUpgrade = state.ComputingPower; + Log($"CP before upgrade: {cpBeforeUpgrade}"); + + // [10] 升级 INCREASE_PRICE 科技 + Log("Upgrading INCREASE_PRICE tech (cost 80)..."); + var upRes = client.UplevelTech(new UplevelTechMsg + { + TeamId = teamId, + TechType = TechType.IncreasePrice + }); + await Task.Delay(500, ct); + long cpAfterUpgrade = state.ComputingPower; + Log($" Upgrade: {(upRes.ActSuccess ? "OK" : "FAIL")}, CP after: {cpAfterUpgrade}"); + + // [11] 再卖出 1 单位 Food(确保在市场旁,看 Score 增长) + long scoreBefore2 = state.TeamScore; + Log($"Selling 1x Food (after price upgrade), Score before: {scoreBefore2}..."); + if (!await NavigateToType(client, state, map, teamId, charId, PlaceType.Market, ct)) + { Log("[FAIL] Lost market."); return; } + var tr2 = client.Trade(new TradeMsg + { + TeamId = teamId, + PlayerId = charId, + ProductType = Food, + ProductAmount = 1, + IsBuy = false + }); + await Task.Delay(500, ct); + long scoreAfter2 = state.TeamScore; + long sell2Gain = scoreAfter2 - scoreBefore2; + Log($" Sell #2: {(tr2.ActSuccess ? "OK" : "FAIL")}, Score +{sell2Gain} (CP={state.ComputingPower})"); + + if (tr1.ActSuccess && tr2.ActSuccess) + { + Log($" Sell #1 gain: {sell1Gain}, Sell #2 gain: {sell2Gain}"); + Log($" Price tech effect: {(sell2Gain > sell1Gain ? $"+{sell2Gain - sell1Gain} ({(double)sell2Gain / sell1Gain:F2}x)" : "no change")}"); } - // ── 结果汇总 ────────────────────────────────────────────────── Console.WriteLine(); - Console.WriteLine("══════════════════════════════════════"); - Console.WriteLine($" Harvest : OK"); - Console.WriteLine($" Produce : {(produceRes.ActSuccess ? "OK" : "FAIL")}"); - Console.WriteLine($" Load : {(loaded ? "OK" : "FAIL")}"); - Console.WriteLine($" Trade/Sell : {(sold ? "OK" : "FAIL")}"); - Console.WriteLine($" Full cycle : {(produceRes.ActSuccess && loaded && sold ? "PASS ✓" : "FAIL ✗")}"); - Console.WriteLine("══════════════════════════════════════"); + Console.WriteLine("══════════════════════════════════════════════"); + Console.WriteLine(" Price tech test complete."); + Console.WriteLine("══════════════════════════════════════════════"); } // ──────────────────────────────────────────────────────────────────── @@ -353,10 +346,8 @@ private static async Task ReadStreamAsync( } // ──────────────────────────────────────────────────────────────────── - // 寻路导航 + // 导航 // ──────────────────────────────────────────────────────────────────── - - // 导航到地图上最近的 targetType 类型格旁边(BFS + 重试) private static async Task NavigateToType( AvailableService.AvailableServiceClient client, SharedState state, MessageOfMap map, long teamId, long charId, @@ -369,7 +360,7 @@ private static async Task NavigateToType( var path = FindPathToType(map, cx / CellSize, cy / CellSize, targetType); if (path == null) return false; - if (path.Count <= 1) return true; // 已经到了 + if (path.Count <= 1) return true; bool ok = true; foreach (var cell in path.Skip(1)) @@ -382,18 +373,17 @@ private static async Task NavigateToType( return false; } - // 导航到 (targetRow, targetCol) 格旁边的可行走格(用于精确到达本队工厂) private static async Task NavigateToCell( AvailableService.AvailableServiceClient client, - SharedState state, MessageOfMap map, long teamId, long charId, - int targetRow, int targetCol, CancellationToken ct) + SharedState state, long teamId, long charId, + int row, int col, MessageOfMap map, CancellationToken ct) { for (int attempt = 0; attempt < 8 && !ct.IsCancellationRequested; attempt++) { if (!state.TryGetPos(out int cx, out int cy)) { await Task.Delay(100, ct); continue; } - var path = FindPathAdjacentTo(map, cx / CellSize, cy / CellSize, targetRow, targetCol); + var path = FindPathToCell(map, cx / CellSize, cy / CellSize, row, col); if (path == null) return false; if (path.Count <= 1) return true; @@ -408,7 +398,6 @@ private static async Task NavigateToCell( return false; } - // 移动到某个格的中心,带防卡死偏转 private static async Task MoveToCellAsync( AvailableService.AvailableServiceClient client, SharedState state, long teamId, long charId, @@ -431,7 +420,6 @@ private static async Task MoveToCellAsync( double angle = Math.Atan2(dy, dx); stall = dis >= lastDis - 20 ? stall + 1 : 0; - // 卡死检测:连续 4 帧未前进则左右偏转 if (stall >= 4) angle += (stall / 4) % 2 == 0 ? 0.35 : -0.35; client.Move(new MoveMsg @@ -448,10 +436,8 @@ private static async Task MoveToCellAsync( } // ──────────────────────────────────────────────────────────────────── - // BFS 寻路 + // BFS // ──────────────────────────────────────────────────────────────────── - - // Space 和 Bush 可行走;Factory/Market/Resource/ComputeCenter/Barrier 不可行走 private static bool IsPassable(PlaceType p) => p is PlaceType.Space or PlaceType.Bush; @@ -470,14 +456,10 @@ private static bool IsTraversable(MessageOfMap map, int r, int c, int clearance) return true; } - // 从起点 BFS,找到最近 targetType 旁边的可行走格,返回完整路径 private static List<(int r, int c)>? FindPathToType( MessageOfMap map, int sr, int sc, PlaceType targetType) - { - // clearance=1 优先(避免紧贴墙走),失败回退 clearance=0 - return FindPathToType(map, sr, sc, targetType, 1) + => FindPathToType(map, sr, sc, targetType, 1) ?? FindPathToType(map, sr, sc, targetType, 0); - } private static List<(int r, int c)>? FindPathToType( MessageOfMap map, int sr, int sc, PlaceType targetType, int clearance) @@ -509,8 +491,7 @@ private static bool IsTraversable(MessageOfMap map, int r, int c, int clearance) return best == null ? null : Reconstruct(best.Value, sr, sc, prevR, prevC); } - // 到指定格 (tr,tc) 相邻格的路径(用于精确导航到本队工厂) - private static List<(int r, int c)>? FindPathAdjacentTo( + private static List<(int r, int c)>? FindPathToCell( MessageOfMap map, int sr, int sc, int tr, int tc) { int h = map.Rows.Count; @@ -518,7 +499,6 @@ private static bool IsTraversable(MessageOfMap map, int r, int c, int clearance) int w = map.Rows[0].Cols.Count; var (dist, prevR, prevC) = BfsFrom(map, sr, sc, h, w, clearance: 0); - int[] dr = [-1, 1, 0, 0], dc = [0, 0, -1, 1]; (int r, int c)? best = null; int bestD = int.MaxValue; @@ -529,7 +509,6 @@ private static bool IsTraversable(MessageOfMap map, int r, int c, int clearance) if (dist[ar, ac] < 0 || dist[ar, ac] >= bestD) continue; bestD = dist[ar, ac]; best = (ar, ac); } - return best == null ? null : Reconstruct(best.Value, sr, sc, prevR, prevC); } @@ -545,9 +524,8 @@ private static (int[,] dist, int[,] prevR, int[,] prevC) BfsFrom( dist[sr, sc] = 0; prevR[sr, sc] = sr; prevC[sr, sc] = sc; var q = new Queue<(int, int)>(); - - // 如果起点本身不可通行(如站在资源上),也从邻近的可通行格开始扩展 int[] dr = [-1, 1, 0, 0], dcc = [0, 0, -1, 1]; + if (!IsTraversable(map, sr, sc, clearance)) { for (int k = 0; k < 4; k++) @@ -563,10 +541,7 @@ private static (int[,] dist, int[,] prevR, int[,] prevC) BfsFrom( } } } - else - { - q.Enqueue((sr, sc)); - } + else q.Enqueue((sr, sc)); while (q.Count > 0) { @@ -603,18 +578,67 @@ private static (int[,] dist, int[,] prevR, int[,] prevC) BfsFrom( } // ──────────────────────────────────────────────────────────────────── - // 通用工具 + // 攒钱辅助:回到工厂 → 生产 Food → 装货 → 到市场卖 → 循环直到 CP 达标 // ──────────────────────────────────────────────────────────────────── + private static async Task GrindCP( + AvailableService.AvailableServiceClient client, + SharedState state, long teamId, long charId, + MessageOfMap map, CancellationToken ct) + { + const int targetCP = 80; + for (int round = 0; round < 10 && !ct.IsCancellationRequested; round++) + { + if (state.ComputingPower >= targetCP) break; + + // 回工厂 + if (!state.TryGetFactoryPos(out int fx, out int fy)) break; + if (!await NavigateToCell(client, state, teamId, charId, fx / CellSize, fy / CellSize, map, ct)) + { Log("[GrindCP] Can't reach factory."); break; } + + // 等空闲 + 生产 + await WaitFor(refreshMs: 400, timeoutSec: 12, ct, + () => state.FactoryCanProduce, + desc: "CanProduce for grind"); + client.Produce(new ProduceGoodsMsg { TeamId = teamId, ProductType = Food, MaxProduceNum = 1 }); + + // 等生产完成 + await Task.Delay(2500, ct); + await WaitFor(refreshMs: 400, timeoutSec: 10, ct, + () => state.GetFactoryGoods(Food) >= 1, + desc: "grind produce complete"); + + if (state.GetFactoryGoods(Food) < 1) continue; + + // 装货 + client.Load(new LoadMsg { TeamId = teamId, PlayerId = charId, ProductType = Food, ProductAmount = 1 }); + await Task.Delay(300, ct); + + // 去市场卖 + if (!await NavigateToType(client, state, map, teamId, charId, PlaceType.Market, ct)) break; + client.Trade(new TradeMsg { TeamId = teamId, PlayerId = charId, ProductType = Food, ProductAmount = 1, IsBuy = false }); + await Task.Delay(500, ct); // 等流推送 CP + Log($"[GrindCP] Round {round + 1}: CP={state.ComputingPower}, Score={state.TeamScore}"); + } + } - private static async Task WaitFor(Func cond, int timeoutSec, CancellationToken ct) + // ──────────────────────────────────────────────────────────────────── + // 工具 + // ──────────────────────────────────────────────────────────────────── + /// + /// 等待 condition 为 true,每次检查前 sleep refreshMs 让流推送新帧。 + /// + private static async Task WaitFor( + int refreshMs, int timeoutSec, CancellationToken ct, + Func condition, string desc) { - var end = DateTime.UtcNow.AddSeconds(timeoutSec); - while (DateTime.UtcNow < end && !ct.IsCancellationRequested) + var deadline = DateTime.UtcNow.AddSeconds(timeoutSec); + while (DateTime.UtcNow < deadline && !ct.IsCancellationRequested) { - if (cond()) return true; - await Task.Delay(120, ct); + if (condition()) return true; + await Task.Delay(refreshMs, ct); } - return cond(); + Log($"[WaitFor] Timeout waiting for: {desc}"); + return false; } private static async Task TimeoutTask(Task task, int sec, CancellationToken ct) @@ -623,12 +647,6 @@ private static async Task TimeoutTask(Task task, int sec, CancellationToke return await Task.WhenAny(task, delay) == task; } - private static void Fail(CancellationTokenSource cts, string msg) - { - Console.WriteLine($"[FAIL] {msg}"); - cts.Cancel(); - } - private static void Log(string msg) => Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {msg}"); } diff --git a/logic/ClientTest2/ClientTest2.csproj b/logic/ClientTest2/ClientTest2.csproj index 52890d73..bd526a82 100644 --- a/logic/ClientTest2/ClientTest2.csproj +++ b/logic/ClientTest2/ClientTest2.csproj @@ -1,20 +1,20 @@ - - Exe - net8.0 - enable - enable - + + Exe + net8.0 + enable + enable + - - - - - + + + + + - - - + + + diff --git a/logic/ClientTest2/Program.cs b/logic/ClientTest2/Program.cs index f4412843..76a2b014 100644 --- a/logic/ClientTest2/Program.cs +++ b/logic/ClientTest2/Program.cs @@ -2,17 +2,14 @@ using Protobuf; // ============================================================================ -// 复杂多角色策略测试(固定分工 + view-range 战斗中断 + 独立推进召唤) +// 多算力中心占领 + 科技全面升级测试 // -// Robot (charId=1) : 占领最近 ComputeCenter → 切换到 Load/Sell 循环 -// Car (charId=2) : 飞向最近 Resource 持续 Harvest -// Drone (charId=3) : 持续锁定并攻击最近敌方 Factory -// -// 任意角色视野内出现敌方角色 → 中断当前任务追击至 atk_size(1000) 内攻击; -// 敌人离开视野/死亡后下一 tick 自动恢复主任务。 +// 1. 召唤 1 个 Robot +// 2. 不断占领算力中心(占完一个找下一个) +// 3. 20s 后尝试升级所有科技 // // 启动:dotnet run --project logic/ClientTest2 -- -// 推荐: --gameTimeInSecond 120 --teamCount 2 +// 推荐: --teamCount 2 // ============================================================================ namespace ClientTest2 @@ -22,814 +19,305 @@ public static class Program private const int CellSize = 1000; private const int CellCenter = 500; private const double ArrivalRadius = 300.0; - private const int CharCost = 50; // GameData.{Drone,Robot,AutoCar}Cost = 50 - private const int AtkSize = 1000; // GameData.*ATKsize = 1000 - private const long RobotId = 1; - private const long CarId = 2; - private const long DroneId = 3; - - // ──────────────────────────────────────────────────────────────────── - // SharedState:帧读取线程写、所有角色 task 读 - // ──────────────────────────────────────────────────────────────────── + private const long CharId = 1; + private sealed class SharedState { private readonly object _lk = new(); private readonly TaskCompletionSource _gameStartTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly TaskCompletionSource _charSeenTcs = + new(TaskCreationOptions.RunContinuationsAsynchronously); - public Task GameStartTask => _gameStartTcs.Task; - - // 团队经济 - private int _teamMaterial; - private long _factoryCp; - private bool _factoryCanRecruit = true; - private bool _factoryCanProduce = true; - private int _factoryHp; + private bool _hasPos; + private int _charX, _charY; + private long _cp; private int _facX = -1, _facY = -1; - private readonly Dictionary _facGoods = new(); - - // 我方角色:playerId → MyChar - private readonly Dictionary _myChars = new(); - - // 敌方角色(每帧重建) - private readonly List _enemies = new(); - - // 敌方工厂(每帧重建) - private readonly List _enemyFactories = new(); - - // 算力中心(每帧重建) - private readonly List _centers = new(); - - public sealed class MyChar - { - public CharacterType Type; - public int X, Y; - public int Hp; - public int CurrentLoad; - public CharacterState State; - public DateTime LastSeen; - } - - public sealed class EnemyChar - { - public long TeamId, PlayerId; - public int X, Y, Hp; - public CharacterType Type; - } - - public sealed class EnemyFactory - { - public long TeamId, FactoryId; - public int X, Y, Hp; - } + private readonly List _centers = new(); - public sealed class CCInfo + public sealed class CC { - public long CenterId; + public long Id, OwnerTeamId; public int X, Y; - public long OwnerTeamId; - public int OccupyProgress; } - // 活跃资源位置(帧中非 HARVESTED 的资源 cell),Car 寻路用 - private readonly HashSet<(int cellX, int cellY)> _activeResourceCells = new(); + public Task GameStartTask => _gameStartTcs.Task; + public Task CharSeenTask => _charSeenTcs.Task; public void ApplyFrame(MessageToClient frame, long teamId) { if (frame.GameState is GameState.GameStart or GameState.GameRunning) _gameStartTcs.TrySetResult(true); - if (frame.AllMessage != null) - { - int idx = (int)teamId - 1; - if ((uint)idx < (uint)frame.AllMessage.Teams.Count) - { - lock (_lk) { _teamMaterial = frame.AllMessage.Teams[idx].Material; } - } - } - lock (_lk) { - _enemies.Clear(); - _enemyFactories.Clear(); _centers.Clear(); - _activeResourceCells.Clear(); - } - - var now = DateTime.UtcNow; - foreach (var obj in frame.ObjMessage) - { - var fac = obj.FactoryMessage; - if (fac != null) + foreach (var obj in frame.ObjMessage) { - if (fac.TeamId == teamId) + var fac = obj.FactoryMessage; + if (fac != null && fac.TeamId == teamId) { - lock (_lk) - { - _factoryCp = fac.ComputingPower; - _factoryCanRecruit = fac.CanRecruit; - _factoryCanProduce = fac.CanProduce; - _factoryHp = fac.Hp; - _facX = fac.X; - _facY = fac.Y; - _facGoods.Clear(); - foreach (var gs in fac.ProductInventory) - _facGoods[gs.ProductType] = gs.Quantity; - } - } - else if (fac.TeamId is >= 1 and <= 4 && fac.Hp > 0) - { - lock (_lk) - { - _enemyFactories.Add(new EnemyFactory - { - TeamId = fac.TeamId, - FactoryId = fac.FactoryId, - X = fac.X, - Y = fac.Y, - Hp = fac.Hp - }); - } + _cp = fac.ComputingPower; + _facX = fac.X; + _facY = fac.Y; } - continue; - } - var cc = obj.ComputeCenterMessage; - if (cc != null) - { - lock (_lk) + var cc = obj.ComputeCenterMessage; + if (cc != null) { - _centers.Add(new CCInfo + _centers.Add(new CC { - CenterId = cc.CenterId, - X = cc.X, - Y = cc.Y, + Id = cc.CenterId, OwnerTeamId = cc.OwnerTeamId, - OccupyProgress = cc.OccupyProgress + X = cc.X, + Y = cc.Y }); } - continue; - } - - var ch = obj.CharacterMessage; - if (ch != null) - { - if (ch.TeamId == teamId) - { - lock (_lk) - { - if (!_myChars.TryGetValue(ch.PlayerId, out var mc)) - { - mc = new MyChar(); - _myChars[ch.PlayerId] = mc; - } - mc.Type = ch.CharacterType; - mc.X = ch.X; - mc.Y = ch.Y; - mc.Hp = ch.Hp; - mc.CurrentLoad = ch.CurrentLoad; - mc.State = ch.CharacterActiveState; - mc.LastSeen = now; - } - } - else - { - lock (_lk) - { - _enemies.Add(new EnemyChar - { - TeamId = ch.TeamId, - PlayerId = ch.PlayerId, - X = ch.X, - Y = ch.Y, - Hp = ch.Hp, - Type = ch.CharacterType - }); - } - } - continue; - } - var res = obj.ResourceMessage; - if (res != null) - { - if (res.ResourceState != Protobuf.ResourceState.Harvested - && res.RemainingAmount > 0) + var ch = obj.CharacterMessage; + if (ch != null && ch.TeamId == teamId && ch.PlayerId == CharId) { - lock (_lk) - { - _activeResourceCells.Add( - (res.X / CellSize, res.Y / CellSize)); - } + _hasPos = true; + _charX = ch.X; + _charY = ch.Y; + _charSeenTcs.TrySetResult(true); } } } } - // ── 读访问器 ──────────────────────────────────────────────── - public int Material { get { lock (_lk) return _teamMaterial; } } - public long FactoryCp { get { lock (_lk) return _factoryCp; } } - public bool FactoryCanRecruit { get { lock (_lk) return _factoryCanRecruit; } } - public bool FactoryCanProduce { get { lock (_lk) return _factoryCanProduce; } } - public int FactoryHp { get { lock (_lk) return _factoryHp; } } + public bool TryGetPos(out int x, out int y) + { lock (_lk) { x = _charX; y = _charY; return _hasPos; } } + + public long CP { get { lock (_lk) return _cp; } } public bool TryGetFactoryPos(out int x, out int y) { lock (_lk) { x = _facX; y = _facY; return _facX >= 0; } } - public int GetFactoryGoods(GoodsType type) - { lock (_lk) return _facGoods.GetValueOrDefault(type, 0); } - - public bool TryGetMyCharPos(long pid, out int x, out int y) - { - lock (_lk) - { - if (_myChars.TryGetValue(pid, out var mc)) - { x = mc.X; y = mc.Y; return true; } - x = 0; y = 0; return false; - } - } - - public bool TryGetMyChar(long pid, out MyChar mc) - { - lock (_lk) - { - if (_myChars.TryGetValue(pid, out var found)) - { mc = found; return true; } - mc = null!; return false; - } - } - - public bool IsMyCharAlive(long pid, double withinSec = 1.5) - { - lock (_lk) - { - if (!_myChars.TryGetValue(pid, out var mc)) return false; - return (DateTime.UtcNow - mc.LastSeen).TotalSeconds <= withinSec - && mc.State != CharacterState.Deceased; - } - } - - public List GetEnemies() { lock (_lk) return _enemies.ToList(); } - public List GetEnemyFactories() { lock (_lk) return _enemyFactories.ToList(); } - public List GetCenters() { lock (_lk) return _centers.ToList(); } - public HashSet<(int, int)> GetActiveResourceCells() { lock (_lk) return new HashSet<(int, int)>(_activeResourceCells); } + public List GetCenters() { lock (_lk) return _centers.ToList(); } } - // ──────────────────────────────────────────────────────────────────── - // Main - // ──────────────────────────────────────────────────────────────────── public static async Task Main(string[] args) { - if (args.Length < 2) - { - Console.WriteLine("Usage: ClientTest2 "); - return; - } - if (!long.TryParse(args[0], out long playerId) || - !long.TryParse(args[1], out long teamId)) - { - Console.WriteLine("Invalid arguments."); - return; - } + if (args.Length < 2) { Console.WriteLine("Usage: ClientTest2 "); return; } + if (!long.TryParse(args[0], out long pid) || !long.TryParse(args[1], out long tid)) return; var channel = new Channel("127.0.0.1:8888", ChannelCredentials.Insecure); await channel.ConnectAsync(DateTime.UtcNow.AddSeconds(5)); var client = new AvailableService.AvailableServiceClient(channel); var streamCall = client.RegisterFactory(new RegisterFactoryMsg - { - PlayerId = playerId, - TeamId = teamId, - SideFlag = (int)teamId - }); - + { PlayerId = pid, TeamId = tid, SideFlag = (int)tid }); var state = new SharedState(); using var cts = new CancellationTokenSource(); Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; - var streamTask = ReadStreamAsync(streamCall, state, teamId, cts.Token); - - try - { - await Run(client, state, cts, teamId); - } + var streamTask = ReadStreamAsync(streamCall, state, tid, cts.Token); + try { await Run(client, state, cts, tid); } catch (OperationCanceledException) { } - catch (Exception ex) - { - Console.WriteLine($"[EXCEPTION] {ex.Message}\n{ex.StackTrace}"); - } - finally - { - cts.Cancel(); - try { await streamTask; } catch { } - await channel.ShutdownAsync(); - } + catch (Exception ex) { Console.WriteLine($"[EX] {ex}"); } + finally { cts.Cancel(); try { await streamTask; } catch { } await channel.ShutdownAsync(); } } - // ──────────────────────────────────────────────────────────────────── - // 主流程编排 - // ──────────────────────────────────────────────────────────────────── private static async Task Run( - AvailableService.AvailableServiceClient client, - SharedState state, - CancellationTokenSource cts, - long teamId) + AvailableService.AvailableServiceClient client, SharedState state, + CancellationTokenSource cts, long teamId) { var ct = cts.Token; Log("Waiting for game start..."); - if (!await TimeoutTask(state.GameStartTask, 30, ct)) - { Log("[FAIL] Game start timeout."); return; } - + if (!await Timeout(state.GameStartTask, 30, ct)) { Log("[FAIL] Start timeout."); return; } + Log("[OK] Game started."); + + // [1] 召唤 Robot + Log("Creating Robot..."); + var cr = client.CreateCharacter(new CreateCharacterMsg + { TeamId = teamId, PlayerId = CharId, CharacterType = CharacterType.Robot }); + if (!cr.ActSuccess) { Log("[FAIL] CreateCharacter."); return; } + if (!await Timeout(state.CharSeenTask, 10, ct)) { Log("[FAIL] Char not seen."); return; } + Log($"[OK] Robot spawned. CP={state.CP}"); + + // [2] 获取地图 var map = client.GetMap(new NullRequest()); + var gameStartTime = DateTime.UtcNow; + var techTime = gameStartTime.AddSeconds(20); - // ── 召唤 Robot(CP 初值=100,足够立即召唤)────────────────── - if (!await SpawnAndAwaitVisible(client, state, teamId, RobotId, CharacterType.Robot, ct)) - { Log("[FAIL] Robot spawn failed."); return; } - - // 启动后台生产协调 - var produceTask = Task.Run(() => ProduceCoordinator(client, state, teamId, ct), ct); - - // 启动 Robot 任务 - var robotTask = Task.Run(() => RobotTask(client, state, map, teamId, ct), ct); - - // 异步等 Car CP 到位再召唤 - var carTask = Task.Run(async () => - { - if (await SpawnWhenAffordable(client, state, teamId, CarId, CharacterType.AutonomousCar, ct)) - await CarTask(client, state, map, teamId, ct); - }, ct); - - // 异步等 Drone CP 到位再召唤 - var droneTask = Task.Run(async () => - { - if (await SpawnWhenAffordable(client, state, teamId, DroneId, CharacterType.Drone, ct)) - await DroneTask(client, state, map, teamId, ct); - }, ct); - - // 等到 ct 取消(游戏结束 / Ctrl+C) - await Task.WhenAny( - Task.WhenAll(robotTask, carTask, droneTask, produceTask), - Task.Delay(Timeout.Infinite, ct)); - } - - // ──────────────────────────────────────────────────────────────────── - // 召唤 + 等待可见 - // ──────────────────────────────────────────────────────────────────── - private static async Task SpawnAndAwaitVisible( - AvailableService.AvailableServiceClient client, - SharedState state, long teamId, long charId, CharacterType type, - CancellationToken ct) - { - Log($"Creating {type} (charId={charId})..."); - for (int i = 0; i < 20 && !ct.IsCancellationRequested; i++) - { - if (state.FactoryCanRecruit) - { - var res = client.CreateCharacter(new CreateCharacterMsg - { - TeamId = teamId, - PlayerId = charId, - CharacterType = type - }); - if (res.ActSuccess) break; - } - await Task.Delay(200, ct); - } - - // 等待帧里看到这个 charId - var deadline = DateTime.UtcNow.AddSeconds(8); - while (DateTime.UtcNow < deadline && !ct.IsCancellationRequested) - { - if (state.TryGetMyCharPos(charId, out _, out _)) - { - Log($" {type} (charId={charId}) spawned."); - return true; - } - await Task.Delay(120, ct); - } - return false; - } + // [3] 持续占领 CC,直到 20s 倒计时结束 + Log("Starting CC occupation loop (20s)..."); + int occCount = 0; + var occupiedIds = new HashSet(); - // 等到 CP 足够 + CanRecruit 后召唤;轮询直到成功或取消 - private static async Task SpawnWhenAffordable( - AvailableService.AvailableServiceClient client, - SharedState state, long teamId, long charId, CharacterType type, - CancellationToken ct) - { - Log($"Waiting for CP >= {CharCost} to recruit {type}..."); - while (!ct.IsCancellationRequested) + while (DateTime.UtcNow < techTime && !ct.IsCancellationRequested) { - if (state.FactoryCp >= CharCost && state.FactoryCanRecruit) - { - if (await SpawnAndAwaitVisible(client, state, teamId, charId, type, ct)) - return true; - Log($" {type} spawn attempt failed, retry..."); - } - await Task.Delay(400, ct); - } - return false; - } + var centers = state.GetCenters(); - // ──────────────────────────────────────────────────────────────────── - // Robot:占领 CC → Load/Sell 循环 - // ──────────────────────────────────────────────────────────────────── - private static async Task RobotTask( - AvailableService.AvailableServiceClient client, - SharedState state, MessageOfMap map, long teamId, CancellationToken ct) - { - Log("[Robot] Phase 1: navigate to nearest unowned ComputeCenter."); - if (!await NavigateToType(client, state, map, teamId, RobotId, PlaceType.ComputeCenter, ct)) - { Log("[Robot][WARN] Failed to reach ComputeCenter; aborting Robot task."); return; } + // 找最近的未被我方占领的 CC + if (!state.TryGetPos(out int cx, out int cy)) + { await Task.Delay(150, ct); continue; } - // 占领 - Log("[Robot] Phase 2: occupying ComputeCenter."); - for (int i = 0; i < 25 && !ct.IsCancellationRequested; i++) - { - var or = client.Occupy(new OccupyMsg - { - TeamId = teamId, - PlayerId = RobotId, - TargetX = 0, - TargetY = 0, - TargetComputeCenterId = -1 - }); - if (or.ActSuccess) break; - await Task.Delay(200, ct); - } - // 等待我方占领进度完成 - var occDeadline = DateTime.UtcNow.AddSeconds(20); - bool occupied = false; - while (DateTime.UtcNow < occDeadline && !ct.IsCancellationRequested) - { - if (state.TryGetMyCharPos(RobotId, out int rx, out int ry)) + SharedState.CC? target = null; + long bestD = long.MaxValue; + foreach (var cc in centers) { - foreach (var cc in state.GetCenters()) - { - long dx = cc.X - rx, dy = cc.Y - ry; - if (dx * dx + dy * dy < (long)(CellSize * 2) * (CellSize * 2) - && cc.OwnerTeamId == teamId) - { occupied = true; break; } - } + if (cc.OwnerTeamId == teamId || occupiedIds.Contains(cc.Id)) continue; + long dx = cc.X - cx, dy = cc.Y - cy; + long d = dx * dx + dy * dy; + if (d < bestD) { bestD = d; target = cc; } } - if (occupied) break; - await Task.Delay(300, ct); - } - Log(occupied ? "[Robot] CC occupied." : "[Robot][WARN] CC occupy uncertain; continuing."); - - // 切到 Load/Sell 循环;先 EndAllAction 清掉 OCUPPYING 状态 - client.EndAllAction(new IDMsg { TeamId = teamId, PlayerId = RobotId }); - await Task.Delay(150, ct); - - const GoodsType Sell = GoodsType.Semiconductor; - int cycle = 0; - while (!ct.IsCancellationRequested) - { - if (await CombatPriority(client, state, teamId, RobotId, ct)) continue; - if (state.GetFactoryGoods(Sell) < 1) + if (target == null) { - await Task.Delay(400, ct); + Log("All visible CCs occupied or no CCs found. Idle..."); + await Task.Delay(1000, ct); continue; } - cycle++; - Log($"[Robot] Cycle #{cycle}: navigating to factory."); - if (!state.TryGetFactoryPos(out int fx, out int fy)) - { await Task.Delay(300, ct); continue; } - - if (!await NavigateToCellWithCombat(client, state, map, teamId, RobotId, - fx / CellSize, fy / CellSize, ct)) - { Log("[Robot] Navigate to factory failed, retry."); continue; } + // 导航到 CC + int tr = target.X / CellSize, tc = target.Y / CellSize; + Log($"Navigating to CC#{target.Id} at ({tr},{tc})..."); + if (!await MoveToCell(client, state, teamId, map, tr, tc, ct)) continue; - // Load - bool loaded = false; - for (int i = 0; i < 12 && !ct.IsCancellationRequested; i++) + // 占领 + Log($"Occupying CC#{target.Id}..."); + for (int i = 0; i < 30 && !ct.IsCancellationRequested; i++) { - var lr = client.Load(new LoadMsg - { - TeamId = teamId, - PlayerId = RobotId, - ProductType = Sell, - ProductAmount = 1 - }); - if (lr.ActSuccess) { loaded = true; break; } + var or = client.Occupy(new OccupyMsg + { TeamId = teamId, PlayerId = CharId, TargetX = 0, TargetY = 0, TargetComputeCenterId = -1 }); + if (or.ActSuccess) break; await Task.Delay(200, ct); } - if (!loaded) { Log("[Robot] Load failed, retry cycle."); await Task.Delay(300, ct); continue; } - Log($"[Robot] Loaded x1 {Sell}."); - // 去市场 - if (!await NavigateToTypeWithCombat(client, state, map, teamId, RobotId, PlaceType.Market, ct)) - { Log("[Robot] Navigate to market failed, retry."); continue; } - - // Trade - bool sold = false; - for (int i = 0; i < 12 && !ct.IsCancellationRequested; i++) + // 等占领完成 + bool gotIt = false; + var od = DateTime.UtcNow.AddSeconds(15); + while (DateTime.UtcNow < od && !ct.IsCancellationRequested) { - var tr = client.Trade(new TradeMsg + foreach (var cc in state.GetCenters()) { - TeamId = teamId, - PlayerId = RobotId, - ProductType = Sell, - ProductAmount = 1, - IsBuy = false - }); - if (tr.ActSuccess) { sold = true; break; } - await Task.Delay(200, ct); + long dx = cc.X - target.X, dy = cc.Y - target.Y; + if (dx * dx + dy * dy < 500 * 500 && cc.OwnerTeamId == teamId) + { gotIt = true; break; } + } + if (gotIt) break; + await Task.Delay(500, ct); } - Log(sold ? $"[Robot] Sold cycle #{cycle} OK." : "[Robot] Trade failed."); - } - } - - // ──────────────────────────────────────────────────────────────────── - // Car:去最近资源点持续 Harvest - // ──────────────────────────────────────────────────────────────────── - private static async Task CarTask( - AvailableService.AvailableServiceClient client, - SharedState state, MessageOfMap map, long teamId, CancellationToken ct) - { - Log("[Car] Phase 1: navigate to nearest Resource."); - if (!await NavigateToTypeWithCombat(client, state, map, teamId, CarId, PlaceType.Resource, ct)) - { Log("[Car][WARN] Failed to reach Resource."); return; } - - int harvestCount = 0; - while (!ct.IsCancellationRequested) - { - if (await CombatPriority(client, state, teamId, CarId, ct)) continue; - var hr = client.Harvest(new ResourceMsg + if (gotIt) { - TeamId = teamId, - PlayerId = CarId, - ResourceId = 0, - Amount = 0 - }); - if (hr.ActSuccess) - { - harvestCount++; - if (harvestCount % 5 == 1) - Log($"[Car] Harvest call OK (count={harvestCount}, material={state.Material})."); - await Task.Delay(2000, ct); + occupiedIds.Add(target.Id); + occCount++; + Log($"[OK] CC#{target.Id} occupied! (total={occCount}, CP={state.CP})"); } else - { - // 资源耗尽或不在资源旁 → 用帧中活跃资源位置重新寻路 - Log("[Car] Harvest failed, searching next active resource..."); - if (!await NavigateToNearestActiveResourceWithCombat( - client, state, map, teamId, CarId, ct)) - { Log("[Car] No reachable active resource, idle."); await Task.Delay(2000, ct); } - } + Log($"[WARN] CC#{target.Id} occupy timeout."); } - } - // ──────────────────────────────────────────────────────────────────── - // Drone:持续锁定攻击最近敌方 Factory - // ──────────────────────────────────────────────────────────────────── - private static async Task DroneTask( - AvailableService.AvailableServiceClient client, - SharedState state, MessageOfMap map, long teamId, CancellationToken ct) - { - Log("[Drone] Phase: hunting enemy factory."); - int hits = 0; - while (!ct.IsCancellationRequested) - { - if (await CombatPriority(client, state, teamId, DroneId, ct)) continue; - - // 选目标 - if (!state.TryGetMyCharPos(DroneId, out int dx, out int dy)) - { await Task.Delay(150, ct); continue; } - - var enemies = state.GetEnemyFactories(); - if (enemies.Count == 0) - { await Task.Delay(300, ct); continue; } - - SharedState.EnemyFactory? target = null; - double bestD = double.MaxValue; - foreach (var f in enemies) - { - if (f.Hp <= 0) continue; - double d = Dist(f.X, f.Y, dx, dy); - if (d < bestD) { bestD = d; target = f; } - } - if (target == null) - { await Task.Delay(300, ct); continue; } - - // 范围内:Attack - // BFS 导航到工厂相邻格的理论距离 = 1000,但 ArrivalRadius=300, - // 角色可能停在距工厂中心 700~1300 的点。一旦 >1000 就超出 atk range。 - // 所以用 bestD <= AtkSize+200 的宽松阈值发起攻击。 - if (bestD <= AtkSize + 200) - { - var ar = client.Attack(new AttackMsg - { - TeamId = teamId, - PlayerId = DroneId, - AttackRange = AtkSize, - AttackedPlayerId = -1, - AttackedTeamId = target.TeamId - }); - if (ar.ActSuccess) - { - hits++; - if (hits % 5 == 1) - Log($"[Drone] Hit factory T{target.TeamId} hp={target.Hp} d={bestD:F0} (hits={hits})."); - await Task.Delay(1050, ct); - } - else - { - // 可能距离仍然不够,朝工厂直移一步 - double a = Math.Atan2(target.Y - dy, target.X - dx); - client.Move(new MoveMsg { TeamId = teamId, PlayerId = DroneId, TimeInMilliseconds = 200, Angle = a }); - await Task.Delay(200, ct); - } - } - else if (bestD <= 3000) - { - // 2~3 格以内:直接朝工厂移动(精度 > BFS 到相邻格再超距循环) - double angle = Math.Atan2(target.Y - dy, target.X - dx); - client.Move(new MoveMsg { TeamId = teamId, PlayerId = DroneId, TimeInMilliseconds = 200, Angle = angle }); - await Task.Delay(150, ct); - } - else - { - // 远距离:BFS 导航到 target.cell 旁边 - int tr = target.X / CellSize, tc = target.Y / CellSize; - await NavigateToCellWithCombat(client, state, map, teamId, DroneId, tr, tc, ct); - } - } - } + // [4] 20s 到了,尝试升级所有科技 + var waitRemaining = techTime - DateTime.UtcNow; + if (waitRemaining > TimeSpan.Zero) await Task.Delay(waitRemaining, ct); - // ──────────────────────────────────────────────────────────────────── - // 战斗中断(任何角色都可调用) - // 返回 true 表示这个 tick 已处理完毕(调用方 continue 即可) - // ──────────────────────────────────────────────────────────────────── - private static async Task CombatPriority( - AvailableService.AvailableServiceClient client, - SharedState state, long teamId, long charId, CancellationToken ct) - { - if (!state.IsMyCharAlive(charId)) return false; - if (!state.TryGetMyCharPos(charId, out int sx, out int sy)) return false; - if (!state.TryGetMyChar(charId, out var me)) return false; + Log($"=== Tech upgrade phase ==="); + Log($"CP before upgrades: {state.CP}"); - int viewRange = me.Type switch + var allTechs = new (TechType type, string name, int cost)[] { - CharacterType.Drone => 7000, - CharacterType.Robot => 5000, - CharacterType.AutonomousCar => 5000, - _ => 5000 + (TechType.IncreaseHp, "INCREASE_HP", 30), + (TechType.IncreaseRobust, "INCREASE_ROBUST", 30), + (TechType.IncreaseAttackPower, "INCREASE_ATTACK_POWER", 60), + (TechType.IncreaseAttackSize, "INCREASE_ATTACK_SIZE", 60), + (TechType.IncreaseMoveSpeed, "INCREASE_MOVE_SPEED", 40), + (TechType.IncreaseCarryCapacity,"INCREASE_CARRY_CAPACITY",50), + (TechType.IncreaseEfficiency, "INCREASE_EFFICIENCY", 40), + (TechType.IncreaseProduction, "INCREASE_PRODUCTION", 60), + (TechType.IncreaseStorage, "INCREASE_STORAGE", 50), + (TechType.IncreasePrice, "INCREASE_PRICE", 80), + (TechType.DecreaseCost, "DECREASE_COST", 50), }; - SharedState.EnemyChar? nearest = null; - double bestD = double.MaxValue; - foreach (var e in state.GetEnemies()) - { - double d = Dist(e.X, e.Y, sx, sy); - if (d < bestD) { bestD = d; nearest = e; } - } - if (nearest == null || bestD > viewRange) return false; - - if (bestD <= AtkSize) - { - var ar = client.Attack(new AttackMsg - { - TeamId = teamId, - PlayerId = charId, - AttackRange = AtkSize, - AttackedPlayerId = nearest.PlayerId, - AttackedTeamId = nearest.TeamId - }); - if (ar.ActSuccess) - { - Log($"[Combat][char {charId}] hit T{nearest.TeamId}P{nearest.PlayerId} d={bestD:F0} hp={nearest.Hp}"); - await Task.Delay(1050, ct); - } - else - { - await Task.Delay(180, ct); - } - return true; - } - else - { - // 朝敌人移动 200ms - double angle = Math.Atan2(nearest.Y - sy, nearest.X - sx); - client.Move(new MoveMsg - { - TeamId = teamId, - PlayerId = charId, - TimeInMilliseconds = 200, - Angle = angle - }); - await Task.Delay(150, ct); - return true; - } - } + Console.WriteLine(); + Console.WriteLine("=============================================="); + Console.WriteLine(" Tech Upgrade Results"); + Console.WriteLine("=============================================="); - // 在导航过程中,每个 cell 之间允许战斗中断 - private static async Task NavigateToTypeWithCombat( - AvailableService.AvailableServiceClient client, - SharedState state, MessageOfMap map, long teamId, long charId, - PlaceType targetType, CancellationToken ct) - { - for (int attempt = 0; attempt < 6 && !ct.IsCancellationRequested; attempt++) + int totalOk = 0; + foreach (var (type, name, cost) in allTechs) { - if (await CombatPriority(client, state, teamId, charId, ct)) continue; - if (!state.TryGetMyCharPos(charId, out int cx, out int cy)) - { await Task.Delay(120, ct); continue; } + if (ct.IsCancellationRequested) break; - var path = FindPathToType(map, cx / CellSize, cy / CellSize, targetType); - if (path == null) return false; - if (path.Count <= 1) return true; + long cpBefore = state.CP; + var res = client.UplevelTech(new UplevelTechMsg + { TeamId = teamId, TechType = type }); + await Task.Delay(400, ct); + long cpAfter = state.CP; + long delta = cpAfter - cpBefore; - bool ok = true; - foreach (var cell in path.Skip(1)) - { - if (await CombatPriority(client, state, teamId, charId, ct)) { ok = false; break; } - if (!await MoveToCellAsync(client, state, teamId, charId, cell.r, cell.c, ct)) - { ok = false; break; } - } - if (ok) return true; + string status = res.ActSuccess ? "OK" : "FAIL"; + Console.WriteLine($" {name,-30} cost={cost,3} cp_before={cpBefore,4} cp_after={cpAfter,4} {status}"); + if (res.ActSuccess) totalOk++; } - return false; - } - // 同 NavigateToTypeWithCombat(Resource) 但只考虑活跃(未枯竭)资源 - private static async Task NavigateToNearestActiveResourceWithCombat( - AvailableService.AvailableServiceClient client, - SharedState state, MessageOfMap map, long teamId, long charId, - CancellationToken ct) - { - for (int attempt = 0; attempt < 6 && !ct.IsCancellationRequested; attempt++) + // [5] 尝试第 2 级 + Console.WriteLine(); + Console.WriteLine("--- Level 2 ---"); + await Task.Delay(2000, ct); + Log($"CP before L2: {state.CP}"); + + foreach (var (type, name, cost) in allTechs) { - if (await CombatPriority(client, state, teamId, charId, ct)) continue; - if (!state.TryGetMyCharPos(charId, out int cx, out int cy)) - { await Task.Delay(120, ct); continue; } + if (ct.IsCancellationRequested) break; - var actives = state.GetActiveResourceCells(); - if (actives.Count == 0) return false; - var path = FindPathToNearestActiveResource(map, cx / CellSize, cy / CellSize, actives); - if (path == null) return false; - if (path.Count <= 1) return true; + long cpBefore = state.CP; + var res = client.UplevelTech(new UplevelTechMsg + { TeamId = teamId, TechType = type }); + await Task.Delay(400, ct); + long cpAfter = state.CP; + long delta = cpAfter - cpBefore; - bool ok = true; - foreach (var cell in path.Skip(1)) - { - if (await CombatPriority(client, state, teamId, charId, ct)) { ok = false; break; } - if (!await MoveToCellAsync(client, state, teamId, charId, cell.r, cell.c, ct)) - { ok = false; break; } - } - if (ok) return true; + string status = res.ActSuccess ? "OK" : "FAIL"; + Console.WriteLine($" {name,-30} cost={cost,3} cp_before={cpBefore,4} cp_after={cpAfter,4} {status}"); + if (res.ActSuccess) totalOk++; } - return false; - } - - private static async Task NavigateToCellWithCombat( - AvailableService.AvailableServiceClient client, - SharedState state, MessageOfMap map, long teamId, long charId, - int tr, int tc, CancellationToken ct) - { - for (int attempt = 0; attempt < 6 && !ct.IsCancellationRequested; attempt++) - { - if (await CombatPriority(client, state, teamId, charId, ct)) continue; - if (!state.TryGetMyCharPos(charId, out int cx, out int cy)) - { await Task.Delay(120, ct); continue; } - var path = FindPathAdjacentTo(map, cx / CellSize, cy / CellSize, tr, tc); - if (path == null) return false; - if (path.Count <= 1) return true; + // [6] 尝试第 3 级(应全部失败,max=2) + Console.WriteLine(); + Console.WriteLine("--- Level 3 (should all FAIL) ---"); + await Task.Delay(2000, ct); - bool ok = true; - foreach (var cell in path.Skip(1)) - { - if (await CombatPriority(client, state, teamId, charId, ct)) { ok = false; break; } - if (!await MoveToCellAsync(client, state, teamId, charId, cell.r, cell.c, ct)) - { ok = false; break; } - } - if (ok) return true; + foreach (var (type, name, cost) in allTechs.Take(4)) + { + if (ct.IsCancellationRequested) break; + var res = client.UplevelTech(new UplevelTechMsg + { TeamId = teamId, TechType = type }); + await Task.Delay(200, ct); + Console.WriteLine($" {name,-30} {(res.ActSuccess ? "OK" : "FAIL (expected)")}"); } - return false; + + Console.WriteLine(); + Console.WriteLine("=============================================="); + Console.WriteLine($" Total successful upgrades: {totalOk}"); + Console.WriteLine("=============================================="); } - // 不带战斗的导航(Robot 占领之前用,避免战斗打断占领前的部署) - private static async Task NavigateToType( - AvailableService.AvailableServiceClient client, - SharedState state, MessageOfMap map, long teamId, long charId, - PlaceType targetType, CancellationToken ct) + // ========================================================================= + // 导航到指定 cell + // ========================================================================= + private static async Task MoveToCell( + AvailableService.AvailableServiceClient client, SharedState state, + long teamId, MessageOfMap map, int tr, int tc, CancellationToken ct) { - for (int attempt = 0; attempt < 6 && !ct.IsCancellationRequested; attempt++) + for (int attempt = 0; attempt < 8 && !ct.IsCancellationRequested; attempt++) { - if (!state.TryGetMyCharPos(charId, out int cx, out int cy)) + if (!state.TryGetPos(out int cx, out int cy)) { await Task.Delay(120, ct); continue; } - var path = FindPathToType(map, cx / CellSize, cy / CellSize, targetType); + var path = FindPathTo(map, cx / CellSize, cy / CellSize, tr, tc); if (path == null) return false; if (path.Count <= 1) return true; bool ok = true; foreach (var cell in path.Skip(1)) { - if (!await MoveToCellAsync(client, state, teamId, charId, cell.r, cell.c, ct)) + if (!await MoveToCellStep(client, state, teamId, cell.r, cell.c, ct)) { ok = false; break; } } if (ok) return true; @@ -837,68 +325,18 @@ private static async Task NavigateToType( return false; } - // ──────────────────────────────────────────────────────────────────── - // 后台生产协调:material 攒够就触发 Produce(Semiconductor) - // ──────────────────────────────────────────────────────────────────── - private static async Task ProduceCoordinator( - AvailableService.AvailableServiceClient client, - SharedState state, long teamId, CancellationToken ct) - { - const GoodsType Product = GoodsType.Semiconductor; - const int Cost = 10; // GameData.CostSemiconductor - int produced = 0; - - while (!ct.IsCancellationRequested) - { - if (state.Material >= Cost && state.FactoryCanProduce - && state.GetFactoryGoods(Product) < 5) // 留点容量给销售 - { - var pr = client.Produce(new ProduceGoodsMsg - { - TeamId = teamId, - ProductType = Product, - MaxProduceNum = 1 - }); - if (pr.ActSuccess) - { - produced++; - Log($"[Produce] Issued #{produced} (material={state.Material})."); - // 等到生产完成(CanProduce 重新为 true) - var deadline = DateTime.UtcNow.AddSeconds(15); - while (DateTime.UtcNow < deadline && !ct.IsCancellationRequested) - { - await Task.Delay(300, ct); - if (state.FactoryCanProduce) break; - } - } - else - { - await Task.Delay(500, ct); - } - } - else - { - await Task.Delay(500, ct); - } - } - } - - // ──────────────────────────────────────────────────────────────────── - // 移动到 cell 中心(带反卡死偏转) - // ──────────────────────────────────────────────────────────────────── - private static async Task MoveToCellAsync( - AvailableService.AvailableServiceClient client, - SharedState state, long teamId, long charId, int row, int col, CancellationToken ct) + private static async Task MoveToCellStep( + AvailableService.AvailableServiceClient client, SharedState state, + long teamId, int row, int col, CancellationToken ct) { - int tx = row * CellSize + CellCenter; - int ty = col * CellSize + CellCenter; - var deadline = DateTime.UtcNow.AddSeconds(10); + int tx = row * CellSize + CellCenter, ty = col * CellSize + CellCenter; + var dl = DateTime.UtcNow.AddSeconds(12); double lastDis = double.MaxValue; int stall = 0; - while (DateTime.UtcNow < deadline && !ct.IsCancellationRequested) + while (DateTime.UtcNow < dl && !ct.IsCancellationRequested) { - if (!state.TryGetMyCharPos(charId, out int cx, out int cy)) + if (!state.TryGetPos(out int cx, out int cy)) { await Task.Delay(60, ct); continue; } double dx = tx - cx, dy = ty - cy; @@ -910,23 +348,17 @@ private static async Task MoveToCellAsync( if (stall >= 4) angle += (stall / 4) % 2 == 0 ? 0.35 : -0.35; client.Move(new MoveMsg - { - TeamId = teamId, - PlayerId = charId, - TimeInMilliseconds = 200, - Angle = angle - }); + { TeamId = teamId, PlayerId = CharId, TimeInMilliseconds = 200, Angle = angle }); lastDis = dis; await Task.Delay(120, ct); } return false; } - // ──────────────────────────────────────────────────────────────────── - // BFS 寻路 - // ──────────────────────────────────────────────────────────────────── - private static bool IsPassable(PlaceType p) => - p is PlaceType.Space or PlaceType.Bush; + // ========================================================================= + // BFS + // ========================================================================= + private static bool IsPassable(PlaceType p) => p is PlaceType.Space or PlaceType.Bush; private static bool IsTraversable(MessageOfMap map, int r, int c, int clearance) { @@ -937,86 +369,17 @@ private static bool IsTraversable(MessageOfMap map, int r, int c, int clearance) for (int dc = -clearance; dc <= clearance; dc++) { int nr = r + dr, nc = c + dc; - if ((uint)nr < (uint)h && (uint)nc < (uint)w && - !IsPassable(map.Rows[nr].Cols[nc])) return false; + if ((uint)nr < (uint)h && (uint)nc < (uint)w && !IsPassable(map.Rows[nr].Cols[nc])) + return false; } return true; } - private static List<(int r, int c)>? FindPathToType( - MessageOfMap map, int sr, int sc, PlaceType targetType) - => FindPathToType(map, sr, sc, targetType, 1) ?? FindPathToType(map, sr, sc, targetType, 0); - - private static List<(int r, int c)>? FindPathToType( - MessageOfMap map, int sr, int sc, PlaceType targetType, int clearance) - { - int h = map.Rows.Count; - if (h == 0) return null; - int w = map.Rows[0].Cols.Count; - if ((uint)sr >= (uint)h || (uint)sc >= (uint)w) return null; - - var (dist, prevR, prevC) = BfsFrom(map, sr, sc, h, w, clearance); - - int[] dr = [-1, 1, 0, 0], dc = [0, 0, -1, 1]; - (int r, int c)? best = null; - int bestD = int.MaxValue; - for (int r = 0; r < h; r++) - for (int c = 0; c < w; c++) - { - if (map.Rows[r].Cols[c] != targetType) continue; - for (int k = 0; k < 4; k++) - { - int ar = r + dr[k], ac = c + dc[k]; - if ((uint)ar >= (uint)h || (uint)ac >= (uint)w) continue; - if (dist[ar, ac] < 0 || dist[ar, ac] >= bestD) continue; - bestD = dist[ar, ac]; best = (ar, ac); - } - } - return best == null ? null : Reconstruct(best.Value, sr, sc, prevR, prevC); - } - - // 同 FindPathToType(Resource) 但跳过不在 activeCells 中的资源格 - private static List<(int r, int c)>? FindPathToNearestActiveResource( - MessageOfMap map, int sr, int sc, HashSet<(int, int)> activeCells) - => FindPathToNearestActiveResource(map, sr, sc, activeCells, 0); - - private static List<(int r, int c)>? FindPathToNearestActiveResource( - MessageOfMap map, int sr, int sc, HashSet<(int, int)> activeCells, int clearance) - { - int h = map.Rows.Count; - if (h == 0) return null; - int w = map.Rows[0].Cols.Count; - if ((uint)sr >= (uint)h || (uint)sc >= (uint)w) return null; - - var (dist, prevR, prevC) = BfsFrom(map, sr, sc, h, w, clearance); - - int[] dr = [-1, 1, 0, 0], dc = [0, 0, -1, 1]; - (int r, int c)? best = null; - int bestD = int.MaxValue; - for (int r = 0; r < h; r++) - for (int c = 0; c < w; c++) - { - if (map.Rows[r].Cols[c] != PlaceType.Resource) continue; - if (!activeCells.Contains((r, c))) continue; // 跳过已枯竭的资源 - for (int k = 0; k < 4; k++) - { - int ar = r + dr[k], ac = c + dc[k]; - if ((uint)ar >= (uint)h || (uint)ac >= (uint)w) continue; - if (dist[ar, ac] < 0 || dist[ar, ac] >= bestD) continue; - bestD = dist[ar, ac]; best = (ar, ac); - } - } - return best == null ? null : Reconstruct(best.Value, sr, sc, prevR, prevC); - } - - private static List<(int r, int c)>? FindPathAdjacentTo( - MessageOfMap map, int sr, int sc, int tr, int tc) + private static List<(int r, int c)>? FindPathTo(MessageOfMap map, int sr, int sc, int tr, int tc) { - int h = map.Rows.Count; - if (h == 0) return null; + int h = map.Rows.Count; if (h == 0) return null; int w = map.Rows[0].Cols.Count; - - var (dist, prevR, prevC) = BfsFrom(map, sr, sc, h, w, clearance: 0); + var (dist, prevR, prevC) = Bfs(map, sr, sc, h, w, clearance: 0); int[] dr = [-1, 1, 0, 0], dc = [0, 0, -1, 1]; (int r, int c)? best = null; int bestD = int.MaxValue; @@ -1030,15 +393,15 @@ private static bool IsTraversable(MessageOfMap map, int r, int c, int clearance) return best == null ? null : Reconstruct(best.Value, sr, sc, prevR, prevC); } - private static (int[,] dist, int[,] prevR, int[,] prevC) BfsFrom( + private static (int[,] dist, int[,] prevR, int[,] prevC) Bfs( MessageOfMap map, int sr, int sc, int h, int w, int clearance) { - var dist = new int[h, w]; - var prevR = new int[h, w]; - var prevC = new int[h, w]; - for (int r = 0; r < h; r++) - for (int c = 0; c < w; c++) - { dist[r, c] = -1; prevR[r, c] = prevC[r, c] = -1; } + var dist = new int[h, w]; var prevR = new int[h, w]; var prevC = new int[h, w]; + for (int r = 0; r < h; r++) for (int c = 0; c < w; c++) + { + dist[r, c] = -1; + prevR[r, c] = prevC[r, c] = -1; + } dist[sr, sc] = 0; prevR[sr, sc] = sr; prevC[sr, sc] = sc; var q = new Queue<(int, int)>(); @@ -1051,12 +414,7 @@ private static (int[,] dist, int[,] prevR, int[,] prevC) BfsFrom( int nr = sr + dr[k], nc = sc + dcc[k]; if ((uint)nr >= (uint)h || (uint)nc >= (uint)w) continue; if (IsTraversable(map, nr, nc, clearance)) - { - dist[nr, nc] = 1; - prevR[nr, nc] = sr; - prevC[nr, nc] = sc; - q.Enqueue((nr, nc)); - } + { dist[nr, nc] = 1; prevR[nr, nc] = sr; prevC[nr, nc] = sc; q.Enqueue((nr, nc)); } } } else q.Enqueue((sr, sc)); @@ -1070,8 +428,7 @@ private static (int[,] dist, int[,] prevR, int[,] prevC) BfsFrom( if ((uint)nr >= (uint)h || (uint)nc >= (uint)w) continue; if (dist[nr, nc] >= 0) continue; if (!IsTraversable(map, nr, nc, clearance)) continue; - dist[nr, nc] = dist[r, c] + 1; - prevR[nr, nc] = r; prevC[nr, nc] = c; + dist[nr, nc] = dist[r, c] + 1; prevR[nr, nc] = r; prevC[nr, nc] = c; q.Enqueue((nr, nc)); } } @@ -1081,49 +438,33 @@ private static (int[,] dist, int[,] prevR, int[,] prevC) BfsFrom( private static List<(int r, int c)> Reconstruct( (int r, int c) end, int sr, int sc, int[,] prevR, int[,] prevC) { - var path = new List<(int, int)>(); - var cur = end; + var path = new List<(int, int)>(); var cur = end; while (!(cur.r == sr && cur.c == sc)) { - path.Add(cur); - int pr = prevR[cur.r, cur.c], pc = prevC[cur.r, cur.c]; - if (pr < 0) return []; - cur = (pr, pc); + path.Add(cur); int pr = prevR[cur.r, cur.c], pc = prevC[cur.r, cur.c]; + if (pr < 0) return []; cur = (pr, pc); } - path.Add((sr, sc)); - path.Reverse(); - return path; + path.Add((sr, sc)); path.Reverse(); return path; } - // ──────────────────────────────────────────────────────────────────── - // 流式帧读取 + 杂项 - // ──────────────────────────────────────────────────────────────────── + // ========================================================================= + // Misc + // ========================================================================= private static async Task ReadStreamAsync( - AsyncServerStreamingCall call, - SharedState state, long teamId, CancellationToken ct) + AsyncServerStreamingCall call, SharedState state, + long teamId, CancellationToken ct) { - try - { - while (await call.ResponseStream.MoveNext(ct)) - state.ApplyFrame(call.ResponseStream.Current, teamId); - } + try { while (await call.ResponseStream.MoveNext(ct)) state.ApplyFrame(call.ResponseStream.Current, teamId); } catch (RpcException) { } catch (OperationCanceledException) { } } - private static async Task TimeoutTask(Task task, int sec, CancellationToken ct) + private static async Task Timeout(Task task, int sec, CancellationToken ct) { var delay = Task.Delay(TimeSpan.FromSeconds(sec), ct); return await Task.WhenAny(task, delay) == task; } - private static double Dist(int x1, int y1, int x2, int y2) - { - double dx = x1 - x2, dy = y1 - y2; - return Math.Sqrt(dx * dx + dy * dy); - } - - private static void Log(string msg) => - Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {msg}"); + private static void Log(string msg) => Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] {msg}"); } } diff --git a/logic/GameClass/GameObj/Map/Map.cs b/logic/GameClass/GameObj/Map/Map.cs index 10bc3ab2..2c341b8f 100644 --- a/logic/GameClass/GameObj/Map/Map.cs +++ b/logic/GameClass/GameObj/Map/Map.cs @@ -272,7 +272,17 @@ public Map(MapStruct mapResource, ComputeCenterType Atype = ComputeCenterType.NU Add(new Space(GameData.GetCellCenterPos(i, j))); break; case PlaceType.MARKET: - Add(new Market(GameData.GetCellCenterPos(i, j), MarketType.MEDIUM_MARKET)); + { + double centerY = height / 2.0; + double centerX = width / 2.0; + double maxDist = Math.Sqrt(centerY * centerY + centerX * centerX); + double dist = Math.Sqrt((i - centerY) * (i - centerY) + (j - centerX) * (j - centerX)); + double norm = dist / maxDist; + MarketType mktType = norm < 1.0 / 3.0 ? MarketType.LARGE_MARKET + : norm < 2.0 / 3.0 ? MarketType.MEDIUM_MARKET + : MarketType.SMALL_MARKET; + Add(new Market(GameData.GetCellCenterPos(i, j), mktType)); + } break; case PlaceType.FACTORY: Add(new Factory(GameData.GetCellCenterPos(i, j))); diff --git a/logic/Preparation/Utility/Logger.cs b/logic/Preparation/Utility/Logger.cs index 91608021..65d92800 100644 --- a/logic/Preparation/Utility/Logger.cs +++ b/logic/Preparation/Utility/Logger.cs @@ -97,13 +97,20 @@ private void Log(LogLevel level, string msg, string file, string member) private class MultiFileLoggerProvider : ILoggerProvider { private readonly Dictionary _writers = new(); - private static readonly string AllLogPath = "logs/all.log"; - private static readonly StreamWriter AllLogWriter; + private const string AllLogPath = "logs/all.log"; + private readonly StreamWriter _allLogWriter; - static MultiFileLoggerProvider() + private static bool s_directoryCreated; + + public MultiFileLoggerProvider() { - Directory.CreateDirectory("logs"); - AllLogWriter = new StreamWriter(AllLogPath, append: false) { AutoFlush = true }; + if (!s_directoryCreated) + { + Directory.CreateDirectory("logs"); + s_directoryCreated = true; + } + var allLogStream = new FileStream(AllLogPath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); + _allLogWriter = new StreamWriter(allLogStream) { AutoFlush = true }; } public ILogger CreateLogger(string categoryName) @@ -111,16 +118,17 @@ public ILogger CreateLogger(string categoryName) if (!_writers.ContainsKey(categoryName)) { var file = $"logs/{categoryName}.log"; - _writers[categoryName] = new StreamWriter(file, append: false) { AutoFlush = true }; + var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.ReadWrite); + _writers[categoryName] = new StreamWriter(fileStream) { AutoFlush = true }; } - return new MultiFileLogger(_writers[categoryName], categoryName); + return new MultiFileLogger(_writers[categoryName], _allLogWriter, categoryName); } public void Dispose() { foreach (var writer in _writers.Values) writer.Dispose(); - AllLogWriter.Dispose(); + _allLogWriter.Dispose(); } /// @@ -129,11 +137,13 @@ public void Dispose() private class MultiFileLogger : ILogger { private readonly StreamWriter _writer; + private readonly StreamWriter _allWriter; private readonly string _categoryName; - public MultiFileLogger(StreamWriter writer, string categoryName) + public MultiFileLogger(StreamWriter writer, StreamWriter allWriter, string categoryName) { _writer = writer; + _allWriter = allWriter; _categoryName = categoryName; } @@ -159,9 +169,9 @@ public void Log(LogLevel logLevel, EventId eventId, { _writer.WriteLine(logLine); } - lock (AllLogWriter) + lock (_allWriter) { - AllLogWriter.WriteLine(logLine); + _allWriter.WriteLine(logLine); } } } @@ -218,6 +228,7 @@ public static Logger CreateLogger(string name) /// public static void SetLogLevel(LogLevel loglevel) { + _loggerFactory?.Dispose(); LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { builder.ClearProviders() diff --git a/logic/Server/ArgumentOptions.cs b/logic/Server/ArgumentOptions.cs index d30e47bf..0fb32ae6 100644 --- a/logic/Server/ArgumentOptions.cs +++ b/logic/Server/ArgumentOptions.cs @@ -18,8 +18,8 @@ public class ArgumentOptions [Option('p', "port", Required = true, HelpText = "Server listening port")] public ushort ServerPort { get; set; } = 8888; - [Option("teamCount", Required = false, HelpText = "The number of teams, 4 by defualt")] - public ushort TeamCount { get; set; } = 1; + [Option("teamCount", Required = false, HelpText = "The number of teams, 4 by default")] + public ushort TeamCount { get; set; } = 2; [Option("CharacterNum", Required = false, HelpText = "The max number of Character, 6 by default")] public ushort CharacterCount { get; set; } = 6; diff --git a/logic/Server/RpcServices.cs b/logic/Server/RpcServices.cs index e3316453..84a37d04 100644 --- a/logic/Server/RpcServices.cs +++ b/logic/Server/RpcServices.cs @@ -464,11 +464,31 @@ public override Task CreateCharacter(CreateCharacterMsg request, Server request.TeamId, request.PlayerId, Transformation.CharacterTypeFromProto(request.CharacterType)); - // if (boolRes.ActSuccess) teamMoneyPool.SubMoney(activateCost); GameServerLogging.logger.LogDebug($"END CreateCharacter:{boolRes.ActSuccess}"); return Task.FromResult(boolRes); } + public override Task CreateCharacterRID(CreateCharacterMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug($"TRY CreateCharacterRID: CharacterType {request.CharacterType} from Team {request.TeamId}"); + CreatCharacterRes res = new(); + if (request.TeamId <= 0 || request.TeamId > TeamCount || request.PlayerId <= 0 || !ValidPlayerID(request.PlayerId)) + { + res.ActSuccess = false; + GameServerLogging.logger.LogDebug($"END CreateCharacterRID: Invalid TeamId {request.TeamId} or PlayerId {request.PlayerId}"); + return Task.FromResult(res); + } + res.ActSuccess = + game.RecruitCharacterAtFactory( + request.TeamId, + request.PlayerId, + Transformation.CharacterTypeFromProto(request.CharacterType)); + if (res.ActSuccess) + res.PlayerId = request.PlayerId; + GameServerLogging.logger.LogDebug($"END CreateCharacterRID: {res.ActSuccess}, PlayerId={res.PlayerId}"); + return Task.FromResult(res); + } + public override Task Produce(ProduceGoodsMsg request, ServerCallContext context) { GameServerLogging.logger.LogDebug($"TRY Produce Goods: Team {request.TeamId} want to " + @@ -485,6 +505,20 @@ public override Task Produce(ProduceGoodsMsg request, ServerCallContext return Task.FromResult(boolRes); } + public override Task UplevelTech(UplevelTechMsg request, ServerCallContext context) + { + GameServerLogging.logger.LogDebug($"TRY UplevelTech: Team {request.TeamId}, Tech {request.TechType}"); + BoolRes boolRes = new() + { + ActSuccess = + game.UplevelTech( + request.TeamId, + (Preparation.Utility.TechType)(int)request.TechType) + }; + GameServerLogging.logger.LogDebug($"END UplevelTech: {boolRes.ActSuccess}"); + return Task.FromResult(boolRes); + } + public override Task EndAllAction(IDMsg request, ServerCallContext context) { GameServerLogging.logger.LogDebug(