pyscreeps-arena 0.3.6__py3-none-any.whl → 0.5.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. pyscreeps_arena/__init__.py +59 -2
  2. pyscreeps_arena/compiler.py +568 -60
  3. pyscreeps_arena/core/config.py +1 -1
  4. pyscreeps_arena/core/const.py +6 -5
  5. pyscreeps_arena/localization.py +10 -0
  6. pyscreeps_arena/project.7z +0 -0
  7. pyscreeps_arena/ui/P2PY.py +108 -0
  8. pyscreeps_arena/ui/__init__.py +12 -0
  9. pyscreeps_arena/ui/creeplogic_edit.py +14 -0
  10. pyscreeps_arena/ui/map_render.py +705 -0
  11. pyscreeps_arena/ui/mapviewer.py +14 -0
  12. pyscreeps_arena/ui/project_ui.py +215 -0
  13. pyscreeps_arena/ui/qcreeplogic/__init__.py +3 -0
  14. pyscreeps_arena/ui/qcreeplogic/model.py +72 -0
  15. pyscreeps_arena/ui/qcreeplogic/qcreeplogic.py +770 -0
  16. pyscreeps_arena/ui/qmapv/__init__.py +3 -0
  17. pyscreeps_arena/ui/qmapv/qcinfo.py +567 -0
  18. pyscreeps_arena/ui/qmapv/qco.py +441 -0
  19. pyscreeps_arena/ui/qmapv/qmapv.py +728 -0
  20. pyscreeps_arena/ui/qmapv/test_array_drag.py +191 -0
  21. pyscreeps_arena/ui/qmapv/test_drag.py +107 -0
  22. pyscreeps_arena/ui/qmapv/test_qcinfo.py +169 -0
  23. pyscreeps_arena/ui/qmapv/test_qco_drag.py +7 -0
  24. pyscreeps_arena/ui/qmapv/test_qmapv.py +224 -0
  25. pyscreeps_arena/ui/qmapv/test_simple_array.py +303 -0
  26. pyscreeps_arena/ui/qrecipe/__init__.py +1 -0
  27. pyscreeps_arena/ui/qrecipe/model.py +434 -0
  28. pyscreeps_arena/ui/qrecipe/qrecipe.py +914 -0
  29. pyscreeps_arena/ui/rs_icon.py +43 -0
  30. {pyscreeps_arena-0.3.6.dist-info → pyscreeps_arena-0.5.7.1.dist-info}/METADATA +15 -3
  31. pyscreeps_arena-0.5.7.1.dist-info/RECORD +40 -0
  32. {pyscreeps_arena-0.3.6.dist-info → pyscreeps_arena-0.5.7.1.dist-info}/WHEEL +1 -1
  33. pyscreeps_arena-0.5.7.1.dist-info/entry_points.txt +4 -0
  34. pyscreeps_arena-0.3.6.dist-info/RECORD +0 -17
  35. pyscreeps_arena-0.3.6.dist-info/entry_points.txt +0 -2
  36. {pyscreeps_arena-0.3.6.dist-info → pyscreeps_arena-0.5.7.1.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,7 @@ import chardet
9
9
  import subprocess
10
10
  import pyperclip
11
11
  from colorama import Fore
12
+ from typing import List, Optional, Tuple, Union
12
13
 
13
14
  WAIT = Fore.YELLOW + ">>>" + Fore.RESET
14
15
  GREEN = Fore.GREEN + "{}" + Fore.RESET
@@ -16,6 +17,27 @@ python_version_info = sys.version_info
16
17
  python_version_info = f"{python_version_info.major}.{python_version_info.minor}.{python_version_info.micro}"
17
18
 
18
19
 
20
+ def replace_src_prefix(file_list):
21
+ """
22
+ 将列表中以'./src.'开头的字符串替换为'./'
23
+
24
+ 参数:
25
+ file_list: 字符串列表
26
+
27
+ 返回:
28
+ 替换后的新列表
29
+ """
30
+ _ = []
31
+
32
+ for item in file_list:
33
+ if isinstance(item, str) and item.startswith('./src.'):
34
+ _new = item.replace('./src.', './', 1)
35
+ if _new in file_list:
36
+ continue
37
+ _.append(item)
38
+
39
+ return _
40
+
19
41
  # def InsertPragmaBefore(content:str) -> str:
20
42
  # """
21
43
  # 在content的开头插入__pragma__('noalias', 'undefined')等内容 |
@@ -43,7 +65,7 @@ class Compiler_Const:
43
65
 
44
66
  TOTAL_INSERT_AT_HEAD = """
45
67
  import { createConstructionSite, findClosestByPath, findClosestByRange, findInRange, findPath, getCpuTime, getDirection, getHeapStatistics, getObjectById, getObjects, getObjectsByPrototype, getRange, getTerrainAt, getTicks,} from 'game/utils';
46
- import { ConstructionSite, Creep, GameObject, OwnedStructure, Resource, Source, Structure, StructureContainer, StructureExtension, StructureRampart, StructureRoad, StructureSpawn, StructureWall, StructureTower} from 'game/prototypes';
68
+ import { ConstructionSite as GameConstructionSite, Creep as GameCreep, GameObject as GameObjectProto, OwnedStructure, Resource as GameResource, Source as GameSource, Structure as GameStructure, StructureContainer as GameStructureContainer, StructureExtension as GameStructureExtension, StructureRampart as GameStructureRampart, StructureRoad as GameStructureRoad, StructureSpawn as GameStructureSpawn, StructureWall as GameStructureWall, StructureTower as GameStructureTower, Flag as GameFlag} from 'game/prototypes';
47
69
  import { ATTACK, ATTACK_POWER, BODYPART_COST, BODYPART_HITS, BOTTOM, BOTTOM_LEFT, BOTTOM_RIGHT, BUILD_POWER, CARRY, CARRY_CAPACITY, CONSTRUCTION_COST, CONSTRUCTION_COST_ROAD_SWAMP_RATIO, CONSTRUCTION_COST_ROAD_WALL_RATIO, CONTAINER_CAPACITY, CONTAINER_HITS, CREEP_SPAWN_TIME, DISMANTLE_COST, DISMANTLE_POWER, ERR_BUSY, ERR_FULL, ERR_INVALID_ARGS, ERR_INVALID_TARGET, ERR_NAME_EXISTS, ERR_NOT_ENOUGH_ENERGY, ERR_NOT_ENOUGH_EXTENSIONS, ERR_NOT_ENOUGH_RESOURCES, ERR_NOT_FOUND, ERR_NOT_IN_RANGE, ERR_NOT_OWNER, ERR_NO_BODYPART, ERR_NO_PATH, ERR_TIRED, EXTENSION_ENERGY_CAPACITY, EXTENSION_HITS, HARVEST_POWER, HEAL, HEAL_POWER, LEFT, MAX_CONSTRUCTION_SITES, MAX_CREEP_SIZE, MOVE, OBSTACLE_OBJECT_TYPES, OK, RAMPART_HITS, RAMPART_HITS_MAX, RANGED_ATTACK, RANGED_ATTACK_DISTANCE_RATE, RANGED_ATTACK_POWER, RANGED_HEAL_POWER, REPAIR_COST, REPAIR_POWER, RESOURCES_ALL, RESOURCE_DECAY, RESOURCE_ENERGY, RIGHT, ROAD_HITS, ROAD_WEAROUT, SOURCE_ENERGY_REGEN, SPAWN_ENERGY_CAPACITY, SPAWN_HITS, STRUCTURE_PROTOTYPES, TERRAIN_PLAIN, TERRAIN_SWAMP, TERRAIN_WALL, TOP, TOP_LEFT, TOP_RIGHT, TOUGH, TOWER_CAPACITY, TOWER_COOLDOWN, TOWER_ENERGY_COST, TOWER_FALLOFF, TOWER_FALLOFF_RANGE, TOWER_HITS, TOWER_OPTIMAL_RANGE, TOWER_POWER_ATTACK, TOWER_POWER_HEAL, TOWER_POWER_REPAIR, TOWER_RANGE, WALL_HITS, WALL_HITS_MAX, WORK} from 'game/constants';
48
70
 
49
71
  import {arenaInfo} from "game";
@@ -56,27 +78,40 @@ import {searchPath, CostMatrix} from "game/path-finder"
56
78
 
57
79
  TOTAL_APPEND_ATEND = """
58
80
  export var sch = Scheduler();
59
- var monitor = Monitor(2);
81
+ var monitor = Monitor(1);
60
82
  know.now = 0;
61
83
 
62
- _SchedulerMeta.__types__ = []; // 清空js首次构造时引入的数据
63
- _StageMachineMeta.__recursive__ = []; // 清空js首次构造时引入的数据
64
- __init_my_exists_creep_before_k__();
84
+ StageMachineLogicMeta.__types__ = []; // 清空js首次构造时引入的数据
85
+ __init_before_k__();
86
+ let knowCost = 0;
87
+ let monitorCost = 0;
88
+ let stepCost = 0;
89
+ let timeLine = 0;
65
90
  export var loop = function () {
66
- know.now = get.ticks ();
91
+ get.handle();
92
+ know.now = get.now;
93
+ timeLine = get.cpu_us();
67
94
  know.handle();
68
- if (know.now === 1) {
95
+ knowCost = get.cpu_us() - timeLine;
96
+
97
+ timeLine = get.cpu_us();
98
+ monitor.handle();
99
+ monitorCost = get.cpu_us() - timeLine;
100
+ for (const creep of know.creeps){
101
+ creep.handle();
102
+ }
103
+ if (know.now === 1) {
69
104
  std.show_welcome();
70
105
  init (know);
106
+
71
107
  }
72
-
73
- monitor.handle();
74
- for (const creep of know._creeps){
75
- creep.motion.handle();
76
- }
77
108
  step (know);
78
- sch.handle();
109
+ timeLine = get.cpu_us();
110
+ if (get._SCH_FLAG) sch.handle();
111
+ stepCost = get.cpu_us() - timeLine;
79
112
  std.show_usage ();
113
+ print("knowCost:", knowCost, "monitorCost:", monitorCost, "stepCost:", stepCost);
114
+ if (know.draw) know.draw();
80
115
  };
81
116
  """
82
117
 
@@ -114,42 +149,60 @@ export var loop = function () {
114
149
 
115
150
  ARENA_IMPORTS_GETTER = {
116
151
  const.ARENA_GREEN: lambda: f"""
117
- class BodyPart{{
118
- constructor(){{
152
+ const ARENA_COLOR_TYPE = "GREEN";
153
+ class GameAreaEffect{{
154
+ constructor(){{
119
155
  }}
120
156
  }};
121
- const ScoreCollector = StructureSpawn;
157
+ class GameConstructionBoost{{
158
+ constructor(){{
159
+ }}
160
+ }};
161
+ import {{ Portal as GamePortal}} from 'arena/season_{config.season}/{const.ARENA_GREEN}/{"advanced" if config.level in ["advance", "advanced"] else "basic"}/prototypes';
162
+
122
163
  """,
123
164
  const.ARENA_BLUE: lambda: f"""
124
- const ScoreCollector = StructureSpawn;
125
- import {{ Flag, BodyPart}} from 'arena/season_{config.season}/capture_the_flag/basic';
165
+ const ARENA_COLOR_TYPE = "BLUE";
166
+ const GameScoreCollector = GameStructureSpawn;
167
+ class GameAreaEffect{{
168
+ constructor(){{
169
+ }}
170
+ }};
171
+ class GamePortal{{
172
+ constructor(){{
173
+ }}
174
+ }};
175
+ class GameConstructionBoost{{
176
+ constructor(){{
177
+ }}
178
+ }};
126
179
  """,
127
180
  const.ARENA_RED: lambda: f"""
128
- class BodyPart{{
129
- constructor(){{
181
+ const ARENA_COLOR_TYPE = "RED";
182
+ class GamePortal{{
183
+ constructor(){{
130
184
  }}
131
185
  }};
132
- import {{ RESOURCE_SCORE, ScoreCollector, AreaEffect, EFFECT_DAMAGE, EFFECT_FREEZE }} from 'arena/season_{config.season}/collect_and_control/basic';
186
+ import {{ ConstructionBoost as GameConstructionBoost, AreaEffect as GameAreaEffect }} from 'arena/season_{config.season}/{const.ARENA_RED}/{"advanced" if config.level in ["advance", "advanced"] else "basic"}/prototypes';
187
+ import {{ EFFECT_CONSTRUCTION_BOOST, EFFECT_SLOWDOWN }} from 'arena/season_{config.season}/{const.ARENA_RED}/{"advanced" if config.level in ["advance", "advanced"] else "basic"}/constants';
133
188
 
134
- import ("arena/season_{config.season}/collect_and_control/basic")
135
- .then((module) => {{ const RESOURCE_SCORE_X = module.RESOURCE_SCORE_X; const RESOURCE_SCORE_Y = module.RESOURCE_SCORE_Y; const RESOURCE_SCORE_Z = module.RESOURCE_SCORE_Z; }})
136
- .catch((error) => {{ }});
137
189
  """,
138
190
  const.ARENA_GRAY: lambda: f"""
139
- class BodyPart{{
140
- constructor(){{
191
+ const ARENA_COLOR_TYPE = "GRAY";
192
+ class GameAreaEffect{{
193
+ constructor(){{
194
+ }}
195
+ }};
196
+ class GamePortal{{
197
+ constructor(){{
198
+ }}
199
+ }};
200
+ class GameConstructionBoost{{
201
+ constructor(){{
141
202
  }}
142
203
  }};
143
- const ScoreCollector = StructureSpawn;
144
- import("game/prototypes")
145
- .then((module) => {{ const Flag = module.Flag; }})
146
- .catch((error) => {{ }});
147
204
  """,
148
205
  }
149
- ARENA_IMPORTS_NOT_BLUE = ""
150
- ARENA_IMPORTS_NOT_BLUE1 = """
151
- import { StructureTower } from 'game/prototypes'
152
- """
153
206
 
154
207
 
155
208
  class Compiler_Utils(Compiler_Const):
@@ -165,7 +218,7 @@ class Compiler_Utils(Compiler_Const):
165
218
  if not Compiler_Utils.last_output:
166
219
  Compiler_Utils.last_output = True
167
220
  print()
168
- core.warn('Compiler_Utils.auto_read', core.lformat(LOC_FILE_NOT_EXISTS, [fpath]), end='', head='\n', ln=config.language)
221
+ core.warn('Compiler_Utils.auto_read', core.lformat(LOC_FILE_NOT_EXISTS, ["", fpath]), end='', head='\n', ln=config.language)
169
222
  return ""
170
223
 
171
224
  try:
@@ -177,12 +230,16 @@ class Compiler_Utils(Compiler_Const):
177
230
  return f.read()
178
231
  except UnicodeDecodeError:
179
232
  # 如果使用检测到的编码读取失败,尝试使用chardet检测编码
180
- with open(fpath, 'rb') as f: # 以二进制模式打开文件
181
- raw_data = f.read() # 读取文件的原始数据
182
- result = chardet.detect(raw_data) # 使用chardet检测编码
183
- encoding = result['encoding'] # 获取检测到的编码
184
- with open(fpath, 'r', encoding=encoding) as f: # 使用检测到的编码打开文件
185
- return f.read()
233
+ try:
234
+ with open(fpath, 'rb') as f: # 以二进制模式打开文件
235
+ raw_data = f.read() # 读取文件的原始数据
236
+ result = chardet.detect(raw_data) # 使用chardet检测编码
237
+ encoding = result['encoding'] # 获取检测到的编码
238
+ with open(fpath, 'r', encoding=encoding) as f: # 使用检测到的编码打开文件
239
+ return f.read()
240
+ except UnicodeDecodeError as e:
241
+ core.error('Compiler_Utils.auto_read', core.lformat(LOC_FILE_READ_FAILED, [fpath, "UnicodeError", e]), end='', head='\n', ln=config.language)
242
+ quit(-1)
186
243
 
187
244
  def copy_to(self) -> list:
188
245
  """
@@ -283,8 +340,104 @@ class Compiler_Utils(Compiler_Const):
283
340
 
284
341
  return '\n'.join(result) # 将处理后的所有代码行连接成一个字符串,并返回最终结果 | join all processed lines into a string and return
285
342
 
286
- def find_chain_import(self, fpath: str, search_dirs: list[str], project_path: str = None, records: dict[str, None] = None) -> list[str]:
343
+ def expand_folder_imports(self, fpath: str, project_path: str = None):
344
+ """
345
+ 扩展文件夹导入语句:将 `from folder import *` 替换为 `from folder.module import *`
346
+ 仅在文件夹没有 __init__.py 时执行此操作
347
+
348
+ :param fpath: 要处理的文件路径
349
+ :param project_path: 项目根路径,用于解析相对导入,默认为 None(使用文件所在目录)
287
350
  """
351
+ if not os.path.exists(fpath):
352
+ return
353
+
354
+ content = self.auto_read(fpath)
355
+ lines = content.split('\n')
356
+ new_lines = []
357
+ changed = False
358
+
359
+ for line in lines:
360
+ m = self.PY_IMPORT_PAT.match(line)
361
+ if not m:
362
+ new_lines.append(line)
363
+ continue
364
+
365
+ original_target = m.group(1)
366
+ target = original_target
367
+ target_path = project_path or os.path.dirname(fpath)
368
+
369
+ # 处理相对路径(向前定位)
370
+ if target.startswith('.'):
371
+ target_path = os.path.dirname(fpath)
372
+ count = 0
373
+ for c in target:
374
+ if c == '.':
375
+ count += 1
376
+ else:
377
+ break
378
+
379
+ # 向上移动目录
380
+ if count > 1:
381
+ for _ in range(count - 1):
382
+ target_path = os.path.dirname(target_path)
383
+
384
+ # 移除开头的点
385
+ target = target[count:]
386
+
387
+ # 如果 target 为空,跳过
388
+ if not target:
389
+ new_lines.append(line)
390
+ continue
391
+
392
+ # 向后定位,构建完整路径
393
+ temp_target = target
394
+ while (_idx := temp_target.find('.')) != -1:
395
+ part = temp_target[:_idx]
396
+ target_path = os.path.join(target_path, part)
397
+ temp_target = temp_target[_idx + 1:]
398
+
399
+ # 最终的文件夹路径
400
+ final_dir_path = os.path.join(target_path, temp_target) if temp_target else target_path
401
+
402
+ # 检查是否是文件夹且没有 __init__.py
403
+ if os.path.isdir(final_dir_path):
404
+ init_path = os.path.join(final_dir_path, '__init__.py')
405
+ if not os.path.exists(init_path):
406
+ # 找到所有 .py 文件(排除 __init__.py)| 如果包含子目录,产生一个警告
407
+ # try:
408
+ # py_files = [f for f in os.listdir(final_dir_path) if f.endswith('.py') and f != '__init__.py']
409
+ # except (FileNotFoundError, PermissionError):
410
+ py_files = []
411
+ for item in os.listdir(final_dir_path):
412
+ _path = os.path.join(final_dir_path, item)
413
+ if os.path.isfile(_path) and item.endswith('.py') and item != '__init__.py':
414
+ py_files.append(item)
415
+ elif os.path.isdir(_path):
416
+ rel = os.path.relpath(final_dir_path, project_path)
417
+ core.warn(f'Compiler.expand_folder_imports', core.lformat(LOC_DIR_UNDER_NONINIT_DIR, [item, rel]), end='', head='\n', ln=config.language )
418
+
419
+
420
+ # 为每个 .py 文件生成导入语句
421
+ if py_files:
422
+ for py_file in py_files:
423
+ module_name = py_file[:-3]
424
+ new_import = f"from {original_target}.{module_name} import *"
425
+ new_lines.append(new_import)
426
+ changed = True
427
+ continue
428
+
429
+ # 保留原行
430
+ new_lines.append(line)
431
+
432
+ # 如果文件有修改,写回
433
+ if changed:
434
+ new_content = '\n'.join(new_lines)
435
+ with open(fpath, 'w', encoding='utf-8') as f:
436
+ f.write(new_content)
437
+
438
+
439
+ def find_chain_import(self, fpath: str, search_dirs: list[str], project_path: str = None, records: dict[str, None] = None) -> list[str]:
440
+ r"""
288
441
  查找文件中的所有import语句,并返回所有import的文件路径 | find all import statements in a file and return the paths of all imported files
289
442
  PY_IMPORT_PAT: re.compile(r'\s+from\s+(.+)(?=\s+import)\s+import\s+\*')
290
443
  :param fpath: str 目标文件路径 | target file path
@@ -296,7 +449,7 @@ class Compiler_Utils(Compiler_Const):
296
449
  if records is None:
297
450
  records = {}
298
451
  if not os.path.exists(fpath):
299
- core.error('Compiler.find_chain_import', core.lformat(LOC_FILE_NOT_EXISTS, [fpath]), head='\n', ln=config.language)
452
+ core.error('Compiler.find_chain_import', core.lformat(LOC_FILE_NOT_EXISTS, ["py", fpath]), head='\n', ln=config.language)
300
453
  imps = []
301
454
  content = self.auto_read(fpath)
302
455
  project_path = project_path or os.path.dirname(fpath)
@@ -342,6 +495,81 @@ class Compiler_Utils(Compiler_Const):
342
495
 
343
496
  return imps
344
497
 
498
+ def find_chain_import2(self, fpath: str, search_dirs: list[str], project_path: str = None, records: dict[str, None] = None) -> list[str]:
499
+ r"""
500
+ 查找文件中的所有import语句,并返回所有import的文件路径 | find all import statements in a file and return the paths of all imported files
501
+ PY_IMPORT_PAT: re.compile(r'\s+from\s+(.+)(?=\s+import)\s+import\s+\*')
502
+ :param fpath: str 目标文件路径 | target file path
503
+ :param search_dirs: list[str] 搜索目录 | search directories
504
+ :param project_path=None: str python项目中的概念,指根文件所在的目录。如果不指定,默认使用第一次调用时给定的fpath,并且稍后的递归会全部使用此路径 |
505
+ concept in python-project, refers to the directory where the root file is located. If not specified, the fpath given at the first call is used by default, and all subsequent recursions will use this path
506
+ :param records=None: dict[str, None] 记录已经查找过的文件路径,避免重复查找 | record the file paths that have been searched to avoid duplicate searches
507
+ """
508
+ if records is None:
509
+ records = {}
510
+ if not os.path.exists(fpath):
511
+ core.error('Compiler.find_chain_import', core.lformat(LOC_FILE_NOT_EXISTS, [fpath]), head='\n', ln=config.language)
512
+ imps = []
513
+ content = self.auto_read(fpath)
514
+ project_path = project_path or os.path.dirname(fpath)
515
+
516
+ # 添加根目录和 src 目录到 search_dirs
517
+ root_dir = os.path.dirname(project_path) # 根目录
518
+ src_dir = os.path.join(root_dir, 'src') # src 目录
519
+ if root_dir not in search_dirs:
520
+ search_dirs = [root_dir] + search_dirs
521
+ if src_dir not in search_dirs:
522
+ search_dirs = [src_dir] + search_dirs
523
+
524
+ for no, line in enumerate(content.split('\n')):
525
+ m = self.PY_IMPORT_PAT.match(line)
526
+ if m:
527
+ target = m.group(1)
528
+ target_path = project_path
529
+
530
+ ## 向前定位 | locate forward
531
+ if target.startswith('.'):
532
+ target_path = os.path.dirname(fpath) # 因为使用了相对路径,所以需要先定位到当前文件所在的目录 |
533
+ # because relative path is used, need to locate the directory where the current file is located first
534
+ count = 0
535
+ for c in target:
536
+ if c == '.':
537
+ count += 1
538
+ else:
539
+ break
540
+ if count > 1:
541
+ for _ in range(count - 1):
542
+ target_path = os.path.dirname(target_path)
543
+
544
+ ## 向后定位 | locate backward
545
+ while (_idx := target.find('.')) != -1:
546
+ first_name = target[:_idx]
547
+ target_path = os.path.join(target_path, first_name)
548
+ target = target[_idx + 1:]
549
+
550
+ ## 检查是否存在 | check if exists
551
+ this_path = os.path.join(target_path, target)
552
+ if os.path.isdir(this_path):
553
+ this_path = os.path.join(this_path, '__init__.py')
554
+ else:
555
+ this_path += '.py'
556
+
557
+ if not os.path.exists(this_path):
558
+ # 如果当前路径不存在,尝试在 search_dirs 中查找
559
+ for search_dir in search_dirs:
560
+ search_path = os.path.join(search_dir, target.replace('.', os.sep)) + ('.py' if not os.path.isdir(this_path) else os.sep + '__init__.py')
561
+ if os.path.exists(search_path):
562
+ this_path = search_path
563
+ break
564
+ else:
565
+ core.error('Compiler.find_chain_import', core.lformat(LOC_CHAIN_FILE_NOT_EXISTS, [fpath, no + 1, this_path]), head='\n', ln=config.language)
566
+ if this_path not in records:
567
+ records[this_path] = None
568
+ tmp = self.find_chain_import(this_path, search_dirs, project_path, records) + [this_path]
569
+ imps.extend(tmp)
570
+
571
+ return imps
572
+
345
573
  @staticmethod
346
574
  def relist_pyimports_to_jsimports(base_dir:str, pyimps:list[str]) -> list[str]:
347
575
  """
@@ -358,21 +586,277 @@ class Compiler_Utils(Compiler_Const):
358
586
  return jsimps
359
587
 
360
588
  # ---------- 自定义函数 ---------- #
589
+
361
590
  @staticmethod
362
- def stage_recursive_replace(content:str) -> str:
591
+ def stage_recursive_replace(content: str) -> str:
363
592
  """
364
- 替换'@recursive'为'@recursive(<fname>)', 其中<fname>为被装饰器标记的函数名 |
365
- Replace '@recursive' with '@recursive(<fname>)', where <fname> is the name of the decorated function.
593
+ 移除 '@recursive' 装饰器行,并在文末添加对应的 _recursiveLogin 调用。
366
594
 
367
- @\s*recursive\s+def\s+([^\s\(]+)
595
+ 对于类方法: _recursiveLogin("ClassName", "method_name")
596
+ 对于普通函数: _recursiveLogin("", "function_name")
368
597
  """
369
- return re.sub(r'@\s*recursive(\s+def\s+)([^\s\(]+)', r'@recursive("\2")\1\2', content)
598
+ calls_to_add = []
599
+ deletions = []
370
600
 
601
+ # 1. 收集所有类定义的位置和缩进
602
+ class_pattern = re.compile(r'^(\s*)class\s+(\w+)', re.MULTILINE)
603
+ classes = [(m.start(), len(m.group(1)), m.group(2))
604
+ for m in class_pattern.finditer(content)]
605
+
606
+ # 2. 查找所有 @recursive 装饰器
607
+ decorator_pattern = re.compile(r'^\s*@\s*recursive\s*$\n?', re.MULTILINE)
608
+
609
+ for dec_match in decorator_pattern.finditer(content):
610
+ dec_end = dec_match.end()
611
+
612
+ # 查找接下来的函数定义(跳过可能的空行)
613
+ after_decorator = content[dec_end:]
614
+ func_match = re.search(r'^(\s*)def\s+([^\s\(]+)', after_decorator, re.MULTILINE)
615
+
616
+ if not func_match:
617
+ continue
618
+
619
+ func_indent_len = len(func_match.group(1))
620
+ func_name = func_match.group(2)
621
+
622
+ # 3. 确定类名:查找装饰器前最近的、缩进小于函数缩进的类
623
+ class_name = ""
624
+ for cls_pos, cls_indent_len, cls_name in reversed(classes):
625
+ if cls_pos < dec_match.start() and func_indent_len > cls_indent_len:
626
+ class_name = cls_name
627
+ break
628
+
629
+ # 4. 记录删除位置和调用信息
630
+ deletions.append((dec_match.start(), dec_end))
631
+ calls_to_add.append(f'_recursiveLogin("{class_name}", "{func_name}")')
632
+
633
+ # 5. 应用删除(倒序避免位置偏移)
634
+ if not deletions:
635
+ return content
636
+
637
+ result = content
638
+ for start, end in sorted(deletions, key=lambda x: x[0], reverse=True):
639
+ result = result[:start] + result[end:]
640
+
641
+ # 6. 在文末添加调用
642
+ if calls_to_add:
643
+ result = '\n'.join(calls_to_add) + '\n' + result
644
+
645
+ return result
646
+
647
+ @staticmethod
648
+ def process_mate_code(code):
649
+ # 用于存储匹配到的信息
650
+ mate_assignments = []
651
+ # 匹配变量赋值为Mate()的正则表达式,允许变量定义中包含或不包含类型注解
652
+ assign_pattern = re.compile(r'(\w+)\s*(?:\:\s*\w*)?\s*=\s*Mate\s*\(')
653
+ # 匹配类定义的正则表达式
654
+ class_pattern = re.compile(r'class\s+(\w+)')
655
+ # 用于记录当前所在的类名
656
+ current_class = None
657
+ # 将代码按行分割
658
+ lines = code.split('\n')
659
+ # 遍历每一行
660
+ for i, line in enumerate(lines):
661
+ # 匹配类定义
662
+ class_match = class_pattern.match(line)
663
+ if class_match:
664
+ current_class = class_match.group(1)
665
+ # 匹配变量赋值为Mate()
666
+ assign_match = assign_pattern.search(line)
667
+ if assign_match:
668
+ # 检查group(1)前面同一行内是否有#,如果有则忽略
669
+ comment = re.search(r'#', line[:assign_match.start()])
670
+ if comment:
671
+ continue
672
+ variable_name = assign_match.group(1)
673
+ # 存储匹配到的信息
674
+ mate_assignments += [(variable_name, current_class)]
675
+
676
+ output_strings = []
677
+ for variable_name, class_name in mate_assignments:
678
+ output_string = f"# > insert Object.defineProperty ({class_name}, '{variable_name}', property.call ({class_name}, {class_name}.{variable_name}._MateGet_, {class_name}.{variable_name}._MateSet_));"
679
+ output_strings.append(output_string)
680
+
681
+ return code + '\n'.join(output_strings)
682
+
683
+
684
+ @staticmethod
685
+ def remove_long_docstring(content:str) -> str:
686
+ """
687
+ 移除长注释 | remove long docstring
688
+ """
689
+ code = re.sub(r'"""[^"]*"""', '', content)
690
+ code = re.sub(r"'''[^']*'''", '', code)
691
+ return code
692
+
693
+ @classmethod
694
+ def _collect_logical_line(cls, lines: List[str], start_idx: int) -> Tuple[str, int]:
695
+ """收集从start_idx开始的逻辑行(处理多行语句,直到遇到:结尾)"""
696
+ if start_idx >= len(lines):
697
+ return "", start_idx
698
+
699
+ parts = [lines[start_idx].rstrip()]
700
+ i = start_idx
701
+
702
+ # 持续收集直到找到以:结尾的行
703
+ while i < len(lines) and not parts[-1].endswith(':'):
704
+ i += 1
705
+ if i < len(lines):
706
+ parts.append(lines[i].rstrip())
707
+
708
+ return " ".join(parts), i
709
+
710
+ @classmethod
711
+ def _convert_block(cls, lines: List[str], match_counter: Optional[List[int]] = None) -> List[str]:
712
+ if match_counter is None:
713
+ match_counter = [0]
714
+
715
+ result = []
716
+ i = 0
717
+
718
+ while i < len(lines):
719
+ line = lines[i].rstrip()
720
+
721
+ # 检测match语句(支持多行)
722
+ if re.match(r'^\s*match\s+', line):
723
+ full_match, end_idx = cls._collect_logical_line(lines, i)
724
+
725
+ match_stmt = re.match(r'^(\s*)match\s+(.+?)\s*:', full_match)
726
+ if not match_stmt:
727
+ result.append(line)
728
+ i += 1
729
+ continue
730
+
731
+ indent = match_stmt.group(1)
732
+ subject = match_stmt.group(2).strip()
733
+ i = end_idx + 1 # 跳过match语句
734
+
735
+ # 生成临时变量
736
+ var_name = f"__MATCH_{match_counter[0]}__"
737
+ match_counter[0] += 1
738
+ result.append(f"{indent}{var_name} = {subject}")
739
+
740
+ # 解析case语句
741
+ cases: List[Tuple[str, List[str]]] = []
742
+ case_indent = None
743
+
744
+ while i < len(lines):
745
+ case_line = lines[i].rstrip()
746
+
747
+ # 缩进检查 - case必须比match缩进更多
748
+ if not case_line.startswith(indent + ' ') and case_line.strip():
749
+ if re.match(r'^\s*case\s+', case_line):
750
+ raise MatchCaseError(f"第 {i + 1} 行: case 缩进必须大于 match")
751
+ break
752
+
753
+ # 检测case语句(不再使用_collect_logical_line,而是单独处理每一行)
754
+ case_match = re.match(r'^(\s+)case\s+(.+?)\s*:', case_line)
755
+ if case_match:
756
+ curr_case_indent = case_match.group(1)
757
+ case_val = case_match.group(2).strip()
758
+
759
+ # 验证缩进 - 允许不同的case缩进(用于嵌套)
760
+ if len(curr_case_indent) <= len(indent):
761
+ raise MatchCaseError(f"第 {i + 1} 行: case 缩进必须大于 match")
762
+
763
+ # 不再强制要求所有case缩进一致,允许嵌套情况下的不同缩进
764
+ if case_indent is None:
765
+ case_indent = curr_case_indent
766
+
767
+ # 提取内联代码(如果有)
768
+ inline_code = ""
769
+ if ':' in case_line:
770
+ after_colon = case_line.split(':', 1)[1].strip()
771
+ if after_colon:
772
+ inline_code = after_colon
773
+
774
+ i += 1
775
+
776
+ # 收集case块
777
+ block_lines = []
778
+ if inline_code:
779
+ block_lines.append(f"{curr_case_indent} {inline_code}")
780
+
781
+ while i < len(lines):
782
+ block_line = lines[i].rstrip()
783
+ if not block_line.strip():
784
+ block_lines.append(block_line)
785
+ i += 1
786
+ continue
787
+
788
+ # 检查是否是下一个case或者缩进回到当前match级别
789
+ if re.match(r'^\s*case\s+', block_line):
790
+ # 检查这个case是否属于当前match还是父级match
791
+ next_case_indent = re.match(r'^\s*', block_line).group(0)
792
+ if len(next_case_indent) <= len(indent):
793
+ # 属于父级match,退出当前match的处理
794
+ break
795
+ # 仍然属于当前match,继续收集
796
+ if block_line.startswith(curr_case_indent + ' '):
797
+ block_lines.append(block_line)
798
+ i += 1
799
+ continue
800
+ else:
801
+ break
802
+
803
+ if block_line.startswith(indent) and not block_line.startswith(curr_case_indent):
804
+ break
805
+
806
+ if block_line.startswith(curr_case_indent + ' '):
807
+ block_lines.append(block_line)
808
+ i += 1
809
+ continue
810
+
811
+ break
812
+
813
+ cases.append((case_val, block_lines))
814
+ else:
815
+ break
816
+
817
+ # 验证case
818
+ seen = set()
819
+ for idx, (val, _) in enumerate(cases):
820
+ if val == '_':
821
+ if idx != len(cases) - 1:
822
+ raise MatchCaseError(f"第 {i + 1} 行附近: case _ 必须在最后")
823
+ else:
824
+ if val in seen:
825
+ raise MatchCaseError(f"第 {i + 1} 行附近: 重复的 case 值 '{val}'")
826
+ seen.add(val)
827
+
828
+ # 生成if/elif/else
829
+ for idx, (case_val, blk_lines) in enumerate(cases):
830
+ keyword = "else" if case_val == '_' else ("if" if idx == 0 else "elif")
831
+ if keyword == "else":
832
+ result.append(f"{indent}else:")
833
+ else:
834
+ result.append(f"{indent}{keyword} {var_name} == {case_val}:")
835
+
836
+ if blk_lines:
837
+ # 递归处理block_lines,以支持嵌套match
838
+ converted_blk_lines = cls._convert_block(blk_lines, match_counter)
839
+ result.extend(converted_blk_lines)
840
+
841
+ continue
842
+
843
+ result.append(line)
844
+ i += 1
845
+
846
+ return result
847
+
848
+ @classmethod
849
+ def convert_match_to_if(cls, code: str) -> str:
850
+ lines = code.split('\n')
851
+ converted_lines = cls._convert_block(lines, [0])
852
+ return '\n'.join(converted_lines)
371
853
 
372
854
 
373
855
  class CompilerBase(Compiler_Utils):
374
856
 
375
- def __init__(self, src_dir, build_dir):
857
+ def __init__(self):
858
+ src_dir = "src"
859
+ build_dir = "build"
376
860
  # check
377
861
  if not os.path.exists(src_dir):
378
862
  core.error('Compiler.__init__', core.lformat(LOC_FILE_NOT_EXISTS, ['src', src_dir]), head='\n', ln=config.language)
@@ -435,6 +919,7 @@ class Compiler(CompilerBase):
435
919
  # 将PYFILE_PRAGMA_INSERTS.replace("\t", "").replace(" ", "")插入到文件开头
436
920
  content = self.auto_read(fpath)
437
921
  content = self.PYFILE_PRAGMA_INSERTS.replace("\t", "").replace(" ", "") + content
922
+ # content = self.remove_long_docstring(content) # 移除长注释 | remove long docstring
438
923
 
439
924
  with open(fpath, 'w', encoding='utf-8') as f: # 注意,这里修改的是build目录下的文件,不是源文件 | Note that the file under the build directory is modified here, not the source file
440
925
  f.write(content)
@@ -468,6 +953,8 @@ class Compiler(CompilerBase):
468
953
  if m and (not m.group(2) or m.group(2)[0] != '*'):
469
954
  core.error('Compiler.pre_compile', core.lformat(LOC_IMPORT_STAR2_ERROR, [m.group(1), m.group(2), m.group(1)]), head='\n', ln=config.language)
470
955
 
956
+ self.expand_folder_imports(fpath, self.build_dir)
957
+
471
958
  # -------------------------------- EXPAND IMPORT * -------------------------------- #
472
959
  _imports = self.find_chain_import(self.target_py, [os.path.dirname(self.src_dir), self.src_dir])
473
960
  _js_imports = self.relist_pyimports_to_jsimports(self.build_dir, _imports)
@@ -506,6 +993,14 @@ class Compiler(CompilerBase):
506
993
  else:
507
994
  _pre_sort_[fname] = 65535
508
995
 
996
+ # ------------------------------------ 自定义:mate & match ------------------------------------ #
997
+ for fpath in py_fpath:
998
+ content = self.auto_read(fpath)
999
+ content = self.process_mate_code(content) # 调用process_mate_code
1000
+ content = self.convert_match_to_if(content) # 调用convert_match_to_if
1001
+ with open(fpath, 'w', encoding='utf-8') as f:
1002
+ f.write(content)
1003
+
509
1004
  # ------------------------------------ DEFINE ------------------------------------ #
510
1005
  # 扫描所有# > define的内容,然后在.py中移除这些行,并记录下来
511
1006
  # | get all # > define in .py files, and record them
@@ -579,21 +1074,21 @@ class Compiler(CompilerBase):
579
1074
  with open(fpath, 'w', encoding='utf-8') as f:
580
1075
  f.write(new_content)
581
1076
 
582
- # ------------------------------------ 自定义 ------------------------------------ #
583
- # 调用stage_recursive_replace
1077
+ # ------------------------------------ 自定义:调用stage_recursive_replace ------------------------------------ #
584
1078
  for fpath in py_fpath:
585
1079
  content = self.auto_read(fpath)
586
- content = self.stage_recursive_replace(content)
1080
+ content = self.stage_recursive_replace(content) # 调用stage_recursive_replace
587
1081
  with open(fpath, 'w', encoding='utf-8') as f:
588
1082
  f.write(content)
589
1083
 
1084
+
590
1085
  core.lprint(GREEN.format('[2/6]'), LOC_DONE, " ", LOC_PREPROCESSING_FINISH, sep="", head="\r", ln=config.language)
591
1086
  return _imports, _js_imports, _pre_sort_, _pre_define_, _js_replace_
592
1087
 
593
1088
  def transcrypt_cmd(self):
594
- # 执行cmd命令: transcrypt -b -m -n -s -e 6 target | execute cmd: transcrypt -b -m -n -s -e 6 target
1089
+ # 执行cmd命令: python -m transcrypt -b -m -n -s -e 6 target | execute cmd: python -m transcrypt -b -m -n -s -e 6 target
595
1090
  # 并获取cmd得到的输出 | and get the output of the cmd
596
- cmd = 'transcrypt -b -m -n -s -e 6 %s' % self.target_py
1091
+ cmd = 'python -m transcrypt -b -m -n -s -e 6 %s' % self.target_py
597
1092
  core.lprint(WAIT, core.lformat(LOC_TRANSCRYPTING, [cmd]), end="", ln=config.language)
598
1093
  p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
599
1094
  stdout, stderr = p.communicate()
@@ -721,9 +1216,8 @@ class Compiler(CompilerBase):
721
1216
  """
722
1217
  arena_name = const.ARENA_NAMES.get(config.arena, const.ARENA_NAMES['green']) # like green -> spawn_and_swamp
723
1218
  self.TOTAL_INSERT_AT_HEAD += self.ARENA_IMPORTS_GETTER[arena_name]() # add arena imports
724
- if config.arena != "blue":
725
- self.TOTAL_INSERT_AT_HEAD += self.ARENA_IMPORTS_NOT_BLUE
726
1219
  total_js = f"const __VERSION__ = '{const.VERSION}';\nconst __PYTHON_VERSION__ = '{python_version_info}';" + self.TOTAL_INSERT_AT_HEAD + f"\nexport var LANGUAGE = '{config.language}';\n"
1220
+ total_js += f"const __AUTHOR__ = '{const.AUTHOR}';\nconst __AUTHOR_CN__ = '{const.BILIBILI_NAME}';"
727
1221
 
728
1222
  core.lprint(WAIT, LOC_GENERATING_TOTAL_MAIN_JS, end="", ln=config.language)
729
1223
 
@@ -814,7 +1308,7 @@ class Compiler(CompilerBase):
814
1308
  self.transcrypt_cmd()
815
1309
  imports, modules = self.analyze_rebuild_main_js(defs, jimps)
816
1310
  self.find_add_pure_js_files(sorts, modules)
817
- total_js = imports + "\n" + self.generate_total_js(modules, imps, sorts, self.FILE_STRONG_REPLACE, reps)
1311
+ total_js = imports + "\n" + self.generate_total_js(replace_src_prefix(modules), imps, sorts, self.FILE_STRONG_REPLACE, reps)
818
1312
 
819
1313
  core.lprint(WAIT, LOC_EXPORTING_TOTAL_MAIN_JS, end="", ln=config.language)
820
1314
 
@@ -889,6 +1383,20 @@ class Compiler(CompilerBase):
889
1383
 
890
1384
 
891
1385
  if __name__ == '__main__':
892
- compiler = Compiler('src', 'library', 'build')
893
- compiler.compile()
894
- compiler.clean()
1386
+ # compiler = Compiler('src', 'library', 'build')
1387
+ # compiler.compile()
1388
+ # compiler.clean()
1389
+ test = """
1390
+
1391
+ def patrolling(self, c: Creep):
1392
+ e = self.center.nearest(k.civilian.enemies, 5)
1393
+ if e:
1394
+ match c.test(e,
1395
+ int(c.hpPer <= 0.9) * 2,
1396
+ int(c.info.melee) * -3
1397
+ ):
1398
+ case True: c.move(e, SWAMP_MOTION)
1399
+ case False: c.move(self.bpos, SWAMP_MOTION)
1400
+
1401
+ """
1402
+ print(f"res=\n{Compiler.convert_match_to_if(test)}")