pyscreeps-arena 0.3.6__py3-none-any.whl → 0.5.8.0__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 (43) hide show
  1. pyscreeps_arena/__init__.py +59 -2
  2. pyscreeps_arena/compiler.py +616 -73
  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/qmapker/__init__.py +1 -0
  17. pyscreeps_arena/ui/qmapker/qmapmarker.py +339 -0
  18. pyscreeps_arena/ui/qmapker/qvariable.py +303 -0
  19. pyscreeps_arena/ui/qmapker/test_compact_variable.py +61 -0
  20. pyscreeps_arena/ui/qmapker/test_qmapmarker.py +71 -0
  21. pyscreeps_arena/ui/qmapker/test_qvariable.py +49 -0
  22. pyscreeps_arena/ui/qmapker/to_code.py +100 -0
  23. pyscreeps_arena/ui/qmapv/__init__.py +3 -0
  24. pyscreeps_arena/ui/qmapv/qcinfo.py +567 -0
  25. pyscreeps_arena/ui/qmapv/qco.py +441 -0
  26. pyscreeps_arena/ui/qmapv/qmapv.py +728 -0
  27. pyscreeps_arena/ui/qmapv/test_array_drag.py +191 -0
  28. pyscreeps_arena/ui/qmapv/test_drag.py +107 -0
  29. pyscreeps_arena/ui/qmapv/test_qcinfo.py +169 -0
  30. pyscreeps_arena/ui/qmapv/test_qco_drag.py +7 -0
  31. pyscreeps_arena/ui/qmapv/test_qmapv.py +224 -0
  32. pyscreeps_arena/ui/qmapv/test_simple_array.py +303 -0
  33. pyscreeps_arena/ui/qrecipe/__init__.py +1 -0
  34. pyscreeps_arena/ui/qrecipe/model.py +434 -0
  35. pyscreeps_arena/ui/qrecipe/qrecipe.py +914 -0
  36. pyscreeps_arena/ui/rs_icon.py +43 -0
  37. {pyscreeps_arena-0.3.6.dist-info → pyscreeps_arena-0.5.8.0.dist-info}/METADATA +15 -3
  38. pyscreeps_arena-0.5.8.0.dist-info/RECORD +47 -0
  39. {pyscreeps_arena-0.3.6.dist-info → pyscreeps_arena-0.5.8.0.dist-info}/WHEEL +1 -1
  40. pyscreeps_arena-0.5.8.0.dist-info/entry_points.txt +4 -0
  41. pyscreeps_arena-0.3.6.dist-info/RECORD +0 -17
  42. pyscreeps_arena-0.3.6.dist-info/entry_points.txt +0 -2
  43. {pyscreeps_arena-0.3.6.dist-info → pyscreeps_arena-0.5.8.0.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,32 @@ 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
+ class MatchCaseError(Exception):
21
+ pass
22
+
23
+
24
+ def replace_src_prefix(file_list):
25
+ """
26
+ 将列表中以'./src.'开头的字符串替换为'./'
27
+
28
+ 参数:
29
+ file_list: 字符串列表
30
+
31
+ 返回:
32
+ 替换后的新列表
33
+ """
34
+ _ = []
35
+
36
+ for item in file_list:
37
+ if isinstance(item, str) and item.startswith('./src.'):
38
+ _new = item.replace('./src.', './', 1)
39
+ if _new in file_list:
40
+ continue
41
+ _.append(item)
42
+
43
+ return _
44
+
45
+
19
46
  # def InsertPragmaBefore(content:str) -> str:
20
47
  # """
21
48
  # 在content的开头插入__pragma__('noalias', 'undefined')等内容 |
@@ -43,7 +70,7 @@ class Compiler_Const:
43
70
 
44
71
  TOTAL_INSERT_AT_HEAD = """
45
72
  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';
73
+ 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
74
  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
75
 
49
76
  import {arenaInfo} from "game";
@@ -56,27 +83,40 @@ import {searchPath, CostMatrix} from "game/path-finder"
56
83
 
57
84
  TOTAL_APPEND_ATEND = """
58
85
  export var sch = Scheduler();
59
- var monitor = Monitor(2);
86
+ var monitor = Monitor(1);
60
87
  know.now = 0;
61
88
 
62
- _SchedulerMeta.__types__ = []; // 清空js首次构造时引入的数据
63
- _StageMachineMeta.__recursive__ = []; // 清空js首次构造时引入的数据
64
- __init_my_exists_creep_before_k__();
89
+ StageMachineLogicMeta.__types__ = []; // 清空js首次构造时引入的数据
90
+ __init_before_k__();
91
+ let knowCost = 0;
92
+ let monitorCost = 0;
93
+ let stepCost = 0;
94
+ let timeLine = 0;
65
95
  export var loop = function () {
66
- know.now = get.ticks ();
96
+ get.handle();
97
+ know.now = get.now;
98
+ timeLine = get.cpu_us();
67
99
  know.handle();
68
- if (know.now === 1) {
100
+ knowCost = get.cpu_us() - timeLine;
101
+
102
+ timeLine = get.cpu_us();
103
+ monitor.handle();
104
+ monitorCost = get.cpu_us() - timeLine;
105
+ for (const creep of know.creeps){
106
+ creep.handle();
107
+ }
108
+ if (know.now === 1) {
69
109
  std.show_welcome();
70
110
  init (know);
111
+
71
112
  }
72
-
73
- monitor.handle();
74
- for (const creep of know._creeps){
75
- creep.motion.handle();
76
- }
77
113
  step (know);
78
- sch.handle();
114
+ timeLine = get.cpu_us();
115
+ if (get._SCH_FLAG) sch.handle();
116
+ stepCost = get.cpu_us() - timeLine;
79
117
  std.show_usage ();
118
+ print("knowCost:", knowCost, "monitorCost:", monitorCost, "stepCost:", stepCost);
119
+ if (know.draw) know.draw();
80
120
  };
81
121
  """
82
122
 
@@ -114,42 +154,61 @@ export var loop = function () {
114
154
 
115
155
  ARENA_IMPORTS_GETTER = {
116
156
  const.ARENA_GREEN: lambda: f"""
117
- class BodyPart{{
118
- constructor(){{
157
+ const ARENA_COLOR_TYPE = "GREEN";
158
+ class GameAreaEffect{{
159
+ constructor(){{
160
+ }}
161
+ }};
162
+ class GameConstructionBoost{{
163
+ constructor(){{
119
164
  }}
120
165
  }};
121
- const ScoreCollector = StructureSpawn;
166
+ import {{ Portal as GamePortal}} from 'arena/season_{config.season}/{const.ARENA_GREEN}/{"advanced" if config.level in ["advance", "advanced"] else "basic"}/prototypes';
122
167
  """,
168
+ # import {Portal} from 'arena/season_1/portal_exploration/basic/prototypes';
169
+
123
170
  const.ARENA_BLUE: lambda: f"""
124
- const ScoreCollector = StructureSpawn;
125
- import {{ Flag, BodyPart}} from 'arena/season_{config.season}/capture_the_flag/basic';
171
+ const ARENA_COLOR_TYPE = "BLUE";
172
+ const GameScoreCollector = GameStructureSpawn;
173
+ class GameAreaEffect{{
174
+ constructor(){{
175
+ }}
176
+ }};
177
+ class GamePortal{{
178
+ constructor(){{
179
+ }}
180
+ }};
181
+ class GameConstructionBoost{{
182
+ constructor(){{
183
+ }}
184
+ }};
126
185
  """,
127
186
  const.ARENA_RED: lambda: f"""
128
- class BodyPart{{
129
- constructor(){{
187
+ const ARENA_COLOR_TYPE = "RED";
188
+ class GamePortal{{
189
+ constructor(){{
130
190
  }}
131
191
  }};
132
- import {{ RESOURCE_SCORE, ScoreCollector, AreaEffect, EFFECT_DAMAGE, EFFECT_FREEZE }} from 'arena/season_{config.season}/collect_and_control/basic';
192
+ 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';
193
+ 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
194
 
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
195
  """,
138
196
  const.ARENA_GRAY: lambda: f"""
139
- class BodyPart{{
140
- constructor(){{
197
+ const ARENA_COLOR_TYPE = "GRAY";
198
+ class GameAreaEffect{{
199
+ constructor(){{
200
+ }}
201
+ }};
202
+ class GamePortal{{
203
+ constructor(){{
204
+ }}
205
+ }};
206
+ class GameConstructionBoost{{
207
+ constructor(){{
141
208
  }}
142
209
  }};
143
- const ScoreCollector = StructureSpawn;
144
- import("game/prototypes")
145
- .then((module) => {{ const Flag = module.Flag; }})
146
- .catch((error) => {{ }});
147
210
  """,
148
211
  }
149
- ARENA_IMPORTS_NOT_BLUE = ""
150
- ARENA_IMPORTS_NOT_BLUE1 = """
151
- import { StructureTower } from 'game/prototypes'
152
- """
153
212
 
154
213
 
155
214
  class Compiler_Utils(Compiler_Const):
@@ -165,7 +224,7 @@ class Compiler_Utils(Compiler_Const):
165
224
  if not Compiler_Utils.last_output:
166
225
  Compiler_Utils.last_output = True
167
226
  print()
168
- core.warn('Compiler_Utils.auto_read', core.lformat(LOC_FILE_NOT_EXISTS, [fpath]), end='', head='\n', ln=config.language)
227
+ core.warn('Compiler_Utils.auto_read', core.lformat(LOC_FILE_NOT_EXISTS, ["", fpath]), end='', head='\n', ln=config.language)
169
228
  return ""
170
229
 
171
230
  try:
@@ -177,12 +236,16 @@ class Compiler_Utils(Compiler_Const):
177
236
  return f.read()
178
237
  except UnicodeDecodeError:
179
238
  # 如果使用检测到的编码读取失败,尝试使用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()
239
+ try:
240
+ with open(fpath, 'rb') as f: # 以二进制模式打开文件
241
+ raw_data = f.read() # 读取文件的原始数据
242
+ result = chardet.detect(raw_data) # 使用chardet检测编码
243
+ encoding = result['encoding'] # 获取检测到的编码
244
+ with open(fpath, 'r', encoding=encoding) as f: # 使用检测到的编码打开文件
245
+ return f.read()
246
+ except UnicodeDecodeError as e:
247
+ core.error('Compiler_Utils.auto_read', core.lformat(LOC_FILE_READ_FAILED, [fpath, "UnicodeError", e]), end='', head='\n', ln=config.language)
248
+ quit(-1)
186
249
 
187
250
  def copy_to(self) -> list:
188
251
  """
@@ -283,8 +346,102 @@ class Compiler_Utils(Compiler_Const):
283
346
 
284
347
  return '\n'.join(result) # 将处理后的所有代码行连接成一个字符串,并返回最终结果 | join all processed lines into a string and return
285
348
 
286
- def find_chain_import(self, fpath: str, search_dirs: list[str], project_path: str = None, records: dict[str, None] = None) -> list[str]:
349
+ def expand_folder_imports(self, fpath: str, project_path: str = None):
287
350
  """
351
+ 扩展文件夹导入语句:将 `from folder import *` 替换为 `from folder.module import *`
352
+ 仅在文件夹没有 __init__.py 时执行此操作
353
+
354
+ :param fpath: 要处理的文件路径
355
+ :param project_path: 项目根路径,用于解析相对导入,默认为 None(使用文件所在目录)
356
+ """
357
+ if not os.path.exists(fpath):
358
+ return
359
+
360
+ content = self.auto_read(fpath)
361
+ lines = content.split('\n')
362
+ new_lines = []
363
+ changed = False
364
+
365
+ for line in lines:
366
+ m = self.PY_IMPORT_PAT.match(line)
367
+ if not m:
368
+ new_lines.append(line)
369
+ continue
370
+
371
+ original_target = m.group(1)
372
+ target = original_target
373
+ target_path = project_path or os.path.dirname(fpath)
374
+
375
+ # 处理相对路径(向前定位)
376
+ if target.startswith('.'):
377
+ target_path = os.path.dirname(fpath)
378
+ count = 0
379
+ for c in target:
380
+ if c == '.':
381
+ count += 1
382
+ else:
383
+ break
384
+
385
+ # 向上移动目录
386
+ if count > 1:
387
+ for _ in range(count - 1):
388
+ target_path = os.path.dirname(target_path)
389
+
390
+ # 移除开头的点
391
+ target = target[count:]
392
+
393
+ # 如果 target 为空,跳过
394
+ if not target:
395
+ new_lines.append(line)
396
+ continue
397
+
398
+ # 向后定位,构建完整路径
399
+ temp_target = target
400
+ while (_idx := temp_target.find('.')) != -1:
401
+ part = temp_target[:_idx]
402
+ target_path = os.path.join(target_path, part)
403
+ temp_target = temp_target[_idx + 1:]
404
+
405
+ # 最终的文件夹路径
406
+ final_dir_path = os.path.join(target_path, temp_target) if temp_target else target_path
407
+
408
+ # 检查是否是文件夹且没有 __init__.py
409
+ if os.path.isdir(final_dir_path):
410
+ init_path = os.path.join(final_dir_path, '__init__.py')
411
+ if not os.path.exists(init_path):
412
+ # 找到所有 .py 文件(排除 __init__.py)| 如果包含子目录,产生一个警告
413
+ # try:
414
+ # py_files = [f for f in os.listdir(final_dir_path) if f.endswith('.py') and f != '__init__.py']
415
+ # except (FileNotFoundError, PermissionError):
416
+ py_files = []
417
+ for item in os.listdir(final_dir_path):
418
+ _path = os.path.join(final_dir_path, item)
419
+ if os.path.isfile(_path) and item.endswith('.py') and item != '__init__.py':
420
+ py_files.append(item)
421
+ elif os.path.isdir(_path):
422
+ rel = os.path.relpath(final_dir_path, project_path)
423
+ core.warn(f'Compiler.expand_folder_imports', core.lformat(LOC_DIR_UNDER_NONINIT_DIR, [item, rel]), end='', head='\n', ln=config.language)
424
+
425
+ # 为每个 .py 文件生成导入语句
426
+ if py_files:
427
+ for py_file in py_files:
428
+ module_name = py_file[:-3]
429
+ new_import = f"from {original_target}.{module_name} import *"
430
+ new_lines.append(new_import)
431
+ changed = True
432
+ continue
433
+
434
+ # 保留原行
435
+ new_lines.append(line)
436
+
437
+ # 如果文件有修改,写回
438
+ if changed:
439
+ new_content = '\n'.join(new_lines)
440
+ with open(fpath, 'w', encoding='utf-8') as f:
441
+ f.write(new_content)
442
+
443
+ def find_chain_import(self, fpath: str, search_dirs: list[str], project_path: str = None, records: dict[str, None] = None) -> list[str]:
444
+ r"""
288
445
  查找文件中的所有import语句,并返回所有import的文件路径 | find all import statements in a file and return the paths of all imported files
289
446
  PY_IMPORT_PAT: re.compile(r'\s+from\s+(.+)(?=\s+import)\s+import\s+\*')
290
447
  :param fpath: str 目标文件路径 | target file path
@@ -296,7 +453,7 @@ class Compiler_Utils(Compiler_Const):
296
453
  if records is None:
297
454
  records = {}
298
455
  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)
456
+ core.error('Compiler.find_chain_import', core.lformat(LOC_FILE_NOT_EXISTS, ["py", fpath]), head='\n', ln=config.language)
300
457
  imps = []
301
458
  content = self.auto_read(fpath)
302
459
  project_path = project_path or os.path.dirname(fpath)
@@ -342,14 +499,89 @@ class Compiler_Utils(Compiler_Const):
342
499
 
343
500
  return imps
344
501
 
502
+ def find_chain_import2(self, fpath: str, search_dirs: list[str], project_path: str = None, records: dict[str, None] = None) -> list[str]:
503
+ r"""
504
+ 查找文件中的所有import语句,并返回所有import的文件路径 | find all import statements in a file and return the paths of all imported files
505
+ PY_IMPORT_PAT: re.compile(r'\s+from\s+(.+)(?=\s+import)\s+import\s+\*')
506
+ :param fpath: str 目标文件路径 | target file path
507
+ :param search_dirs: list[str] 搜索目录 | search directories
508
+ :param project_path=None: str python项目中的概念,指根文件所在的目录。如果不指定,默认使用第一次调用时给定的fpath,并且稍后的递归会全部使用此路径 |
509
+ 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
510
+ :param records=None: dict[str, None] 记录已经查找过的文件路径,避免重复查找 | record the file paths that have been searched to avoid duplicate searches
511
+ """
512
+ if records is None:
513
+ records = {}
514
+ if not os.path.exists(fpath):
515
+ core.error('Compiler.find_chain_import', core.lformat(LOC_FILE_NOT_EXISTS, [fpath]), head='\n', ln=config.language)
516
+ imps = []
517
+ content = self.auto_read(fpath)
518
+ project_path = project_path or os.path.dirname(fpath)
519
+
520
+ # 添加根目录和 src 目录到 search_dirs
521
+ root_dir = os.path.dirname(project_path) # 根目录
522
+ src_dir = os.path.join(root_dir, 'src') # src 目录
523
+ if root_dir not in search_dirs:
524
+ search_dirs = [root_dir] + search_dirs
525
+ if src_dir not in search_dirs:
526
+ search_dirs = [src_dir] + search_dirs
527
+
528
+ for no, line in enumerate(content.split('\n')):
529
+ m = self.PY_IMPORT_PAT.match(line)
530
+ if m:
531
+ target = m.group(1)
532
+ target_path = project_path
533
+
534
+ ## 向前定位 | locate forward
535
+ if target.startswith('.'):
536
+ target_path = os.path.dirname(fpath) # 因为使用了相对路径,所以需要先定位到当前文件所在的目录 |
537
+ # because relative path is used, need to locate the directory where the current file is located first
538
+ count = 0
539
+ for c in target:
540
+ if c == '.':
541
+ count += 1
542
+ else:
543
+ break
544
+ if count > 1:
545
+ for _ in range(count - 1):
546
+ target_path = os.path.dirname(target_path)
547
+
548
+ ## 向后定位 | locate backward
549
+ while (_idx := target.find('.')) != -1:
550
+ first_name = target[:_idx]
551
+ target_path = os.path.join(target_path, first_name)
552
+ target = target[_idx + 1:]
553
+
554
+ ## 检查是否存在 | check if exists
555
+ this_path = os.path.join(target_path, target)
556
+ if os.path.isdir(this_path):
557
+ this_path = os.path.join(this_path, '__init__.py')
558
+ else:
559
+ this_path += '.py'
560
+
561
+ if not os.path.exists(this_path):
562
+ # 如果当前路径不存在,尝试在 search_dirs 中查找
563
+ for search_dir in search_dirs:
564
+ search_path = os.path.join(search_dir, target.replace('.', os.sep)) + ('.py' if not os.path.isdir(this_path) else os.sep + '__init__.py')
565
+ if os.path.exists(search_path):
566
+ this_path = search_path
567
+ break
568
+ else:
569
+ core.error('Compiler.find_chain_import', core.lformat(LOC_CHAIN_FILE_NOT_EXISTS, [fpath, no + 1, this_path]), head='\n', ln=config.language)
570
+ if this_path not in records:
571
+ records[this_path] = None
572
+ tmp = self.find_chain_import(this_path, search_dirs, project_path, records) + [this_path]
573
+ imps.extend(tmp)
574
+
575
+ return imps
576
+
345
577
  @staticmethod
346
- def relist_pyimports_to_jsimports(base_dir:str, pyimps:list[str]) -> list[str]:
578
+ def relist_pyimports_to_jsimports(base_dir: str, pyimps: list[str]) -> list[str]:
347
579
  """
348
580
  将python的imports路径列表转换为js的imports路径列表 | convert a list of python imports paths to a list of js imports paths
349
581
  """
350
582
  jsimps = []
351
583
  for pyimp in pyimps:
352
- rel_path_nodes:list[str] = os.path.relpath(pyimp, base_dir).replace('\\', '/').split('/')
584
+ rel_path_nodes: list[str] = os.path.relpath(pyimp, base_dir).replace('\\', '/').split('/')
353
585
  if rel_path_nodes[-1] == '__init__.py':
354
586
  rel_path_nodes.pop()
355
587
  else:
@@ -358,21 +590,276 @@ class Compiler_Utils(Compiler_Const):
358
590
  return jsimps
359
591
 
360
592
  # ---------- 自定义函数 ---------- #
593
+
361
594
  @staticmethod
362
- def stage_recursive_replace(content:str) -> str:
595
+ def stage_recursive_replace(content: str) -> str:
596
+ """
597
+ 移除 '@recursive' 装饰器行,并在文末添加对应的 _recursiveLogin 调用。
598
+
599
+ 对于类方法: _recursiveLogin("ClassName", "method_name")
600
+ 对于普通函数: _recursiveLogin("", "function_name")
363
601
  """
364
- 替换'@recursive'为'@recursive(<fname>)', 其中<fname>为被装饰器标记的函数名 |
365
- Replace '@recursive' with '@recursive(<fname>)', where <fname> is the name of the decorated function.
602
+ calls_to_add = []
603
+ deletions = []
604
+
605
+ # 1. 收集所有类定义的位置和缩进
606
+ class_pattern = re.compile(r'^(\s*)class\s+(\w+)', re.MULTILINE)
607
+ classes = [(m.start(), len(m.group(1)), m.group(2))
608
+ for m in class_pattern.finditer(content)]
366
609
 
367
- @\s*recursive\s+def\s+([^\s\(]+)
610
+ # 2. 查找所有 @recursive 装饰器
611
+ decorator_pattern = re.compile(r'^\s*@\s*recursive\s*$\n?', re.MULTILINE)
612
+
613
+ for dec_match in decorator_pattern.finditer(content):
614
+ dec_end = dec_match.end()
615
+
616
+ # 查找接下来的函数定义(跳过可能的空行)
617
+ after_decorator = content[dec_end:]
618
+ func_match = re.search(r'^(\s*)def\s+([^\s\(]+)', after_decorator, re.MULTILINE)
619
+
620
+ if not func_match:
621
+ continue
622
+
623
+ func_indent_len = len(func_match.group(1))
624
+ func_name = func_match.group(2)
625
+
626
+ # 3. 确定类名:查找装饰器前最近的、缩进小于函数缩进的类
627
+ class_name = ""
628
+ for cls_pos, cls_indent_len, cls_name in reversed(classes):
629
+ if cls_pos < dec_match.start() and func_indent_len > cls_indent_len:
630
+ class_name = cls_name
631
+ break
632
+
633
+ # 4. 记录删除位置和调用信息
634
+ deletions.append((dec_match.start(), dec_end))
635
+ calls_to_add.append(f'_recursiveLogin("{class_name}", "{func_name}")')
636
+
637
+ # 5. 应用删除(倒序避免位置偏移)
638
+ if not deletions:
639
+ return content
640
+
641
+ result = content
642
+ for start, end in sorted(deletions, key=lambda x: x[0], reverse=True):
643
+ result = result[:start] + result[end:]
644
+
645
+ # 6. 在文末添加调用
646
+ if calls_to_add:
647
+ result = '\n'.join(calls_to_add) + '\n' + result
648
+
649
+ return result
650
+
651
+ @staticmethod
652
+ def process_mate_code(code):
653
+ # 用于存储匹配到的信息
654
+ mate_assignments = []
655
+ # 匹配变量赋值为Mate()的正则表达式,允许变量定义中包含或不包含类型注解
656
+ assign_pattern = re.compile(r'(\w+)\s*(?:\:\s*\w*)?\s*=\s*Mate\s*\(')
657
+ # 匹配类定义的正则表达式
658
+ class_pattern = re.compile(r'class\s+(\w+)')
659
+ # 用于记录当前所在的类名
660
+ current_class = None
661
+ # 将代码按行分割
662
+ lines = code.split('\n')
663
+ # 遍历每一行
664
+ for i, line in enumerate(lines):
665
+ # 匹配类定义
666
+ class_match = class_pattern.match(line)
667
+ if class_match:
668
+ current_class = class_match.group(1)
669
+ # 匹配变量赋值为Mate()
670
+ assign_match = assign_pattern.search(line)
671
+ if assign_match:
672
+ # 检查group(1)前面同一行内是否有#,如果有则忽略
673
+ comment = re.search(r'#', line[:assign_match.start()])
674
+ if comment:
675
+ continue
676
+ variable_name = assign_match.group(1)
677
+ # 存储匹配到的信息
678
+ mate_assignments += [(variable_name, current_class)]
679
+
680
+ output_strings = []
681
+ for variable_name, class_name in mate_assignments:
682
+ output_string = f"# > insert Object.defineProperty ({class_name}, '{variable_name}', property.call ({class_name}, {class_name}.{variable_name}._MateGet_, {class_name}.{variable_name}._MateSet_));"
683
+ output_strings.append(output_string)
684
+
685
+ return code + '\n'.join(output_strings)
686
+
687
+ @staticmethod
688
+ def remove_long_docstring(content: str) -> str:
368
689
  """
369
- return re.sub(r'@\s*recursive(\s+def\s+)([^\s\(]+)', r'@recursive("\2")\1\2', content)
690
+ 移除长注释 | remove long docstring
691
+ """
692
+ code = re.sub(r'"""[^"]*"""', '', content)
693
+ code = re.sub(r"'''[^']*'''", '', code)
694
+ return code
695
+
696
+ @classmethod
697
+ def _collect_logical_line(cls, lines: List[str], start_idx: int) -> Tuple[str, int]:
698
+ """收集从start_idx开始的逻辑行(处理多行语句,直到遇到:结尾)"""
699
+ if start_idx >= len(lines):
700
+ return "", start_idx
701
+
702
+ parts = [lines[start_idx].rstrip()]
703
+ i = start_idx
704
+
705
+ # 持续收集直到找到以:结尾的行
706
+ while i < len(lines) and not parts[-1].endswith(':'):
707
+ i += 1
708
+ if i < len(lines):
709
+ parts.append(lines[i].rstrip())
710
+
711
+ return " ".join(parts), i
712
+
713
+ @classmethod
714
+ def _convert_block(cls, lines: List[str], match_counter: Optional[List[int]] = None) -> List[str]:
715
+ if match_counter is None:
716
+ match_counter = [0]
717
+
718
+ result = []
719
+ i = 0
720
+
721
+ while i < len(lines):
722
+ line = lines[i].rstrip()
723
+
724
+ # 检测match语句(支持多行)
725
+ if re.match(r'^\s*match\s+', line):
726
+ full_match, end_idx = cls._collect_logical_line(lines, i)
727
+
728
+ match_stmt = re.match(r'^(\s*)match\s+(.+?)\s*:', full_match)
729
+ if not match_stmt:
730
+ result.append(line)
731
+ i += 1
732
+ continue
733
+
734
+ indent = match_stmt.group(1)
735
+ subject = match_stmt.group(2).strip()
736
+ i = end_idx + 1 # 跳过match语句
737
+
738
+ # 生成临时变量
739
+ var_name = f"__MATCH_{match_counter[0]}__"
740
+ match_counter[0] += 1
741
+ result.append(f"{indent}{var_name} = {subject}")
742
+
743
+ # 解析case语句
744
+ cases: List[Tuple[str, List[str]]] = []
745
+ case_indent = None
746
+
747
+ while i < len(lines):
748
+ case_line = lines[i].rstrip()
749
+
750
+ # 缩进检查 - case必须比match缩进更多
751
+ if not case_line.startswith(indent + ' ') and case_line.strip():
752
+ if re.match(r'^\s*case\s+', case_line):
753
+ raise MatchCaseError(f"第 {i + 1} 行: case 缩进必须大于 match")
754
+ break
755
+
756
+ # 检测case语句(不再使用_collect_logical_line,而是单独处理每一行)
757
+ case_match = re.match(r'^(\s+)case\s+(.+?)\s*:', case_line)
758
+ if case_match:
759
+ curr_case_indent = case_match.group(1)
760
+ case_val = case_match.group(2).strip()
761
+
762
+ # 验证缩进 - 允许不同的case缩进(用于嵌套)
763
+ if len(curr_case_indent) <= len(indent):
764
+ raise MatchCaseError(f"第 {i + 1} 行: case 缩进必须大于 match")
765
+
766
+ # 不再强制要求所有case缩进一致,允许嵌套情况下的不同缩进
767
+ if case_indent is None:
768
+ case_indent = curr_case_indent
769
+
770
+ # 提取内联代码(如果有)
771
+ inline_code = ""
772
+ if ':' in case_line:
773
+ after_colon = case_line.split(':', 1)[1].strip()
774
+ if after_colon:
775
+ inline_code = after_colon
776
+
777
+ i += 1
778
+
779
+ # 收集case块
780
+ block_lines = []
781
+ if inline_code:
782
+ block_lines.append(f"{curr_case_indent} {inline_code}")
783
+
784
+ while i < len(lines):
785
+ block_line = lines[i].rstrip()
786
+ if not block_line.strip():
787
+ block_lines.append(block_line)
788
+ i += 1
789
+ continue
790
+
791
+ # 检查是否是下一个case或者缩进回到当前match级别
792
+ if re.match(r'^\s*case\s+', block_line):
793
+ # 检查这个case是否属于当前match还是父级match
794
+ next_case_indent = re.match(r'^\s*', block_line).group(0)
795
+ if len(next_case_indent) <= len(indent):
796
+ # 属于父级match,退出当前match的处理
797
+ break
798
+ # 仍然属于当前match,继续收集
799
+ if block_line.startswith(curr_case_indent + ' '):
800
+ block_lines.append(block_line)
801
+ i += 1
802
+ continue
803
+ else:
804
+ break
805
+
806
+ if block_line.startswith(indent) and not block_line.startswith(curr_case_indent):
807
+ break
808
+
809
+ if block_line.startswith(curr_case_indent + ' '):
810
+ block_lines.append(block_line)
811
+ i += 1
812
+ continue
813
+
814
+ break
370
815
 
816
+ cases.append((case_val, block_lines))
817
+ else:
818
+ break
819
+
820
+ # 验证case
821
+ seen = set()
822
+ for idx, (val, _) in enumerate(cases):
823
+ if val == '_':
824
+ if idx != len(cases) - 1:
825
+ raise MatchCaseError(f"第 {i + 1} 行附近: case _ 必须在最后")
826
+ else:
827
+ if val in seen:
828
+ raise MatchCaseError(f"第 {i + 1} 行附近: 重复的 case 值 '{val}'")
829
+ seen.add(val)
830
+
831
+ # 生成if/elif/else
832
+ for idx, (case_val, blk_lines) in enumerate(cases):
833
+ keyword = "else" if case_val == '_' else ("if" if idx == 0 else "elif")
834
+ if keyword == "else":
835
+ result.append(f"{indent}else:")
836
+ else:
837
+ result.append(f"{indent}{keyword} {var_name} == {case_val}:")
838
+
839
+ if blk_lines:
840
+ # 递归处理block_lines,以支持嵌套match
841
+ converted_blk_lines = cls._convert_block(blk_lines, match_counter)
842
+ result.extend(converted_blk_lines)
843
+
844
+ continue
845
+
846
+ result.append(line)
847
+ i += 1
848
+
849
+ return result
850
+
851
+ @classmethod
852
+ def convert_match_to_if(cls, code: str) -> str:
853
+ lines = code.split('\n')
854
+ converted_lines = cls._convert_block(lines, [0])
855
+ return '\n'.join(converted_lines)
371
856
 
372
857
 
373
858
  class CompilerBase(Compiler_Utils):
374
859
 
375
- def __init__(self, src_dir, build_dir):
860
+ def __init__(self):
861
+ src_dir = "src"
862
+ build_dir = "build"
376
863
  # check
377
864
  if not os.path.exists(src_dir):
378
865
  core.error('Compiler.__init__', core.lformat(LOC_FILE_NOT_EXISTS, ['src', src_dir]), head='\n', ln=config.language)
@@ -435,6 +922,7 @@ class Compiler(CompilerBase):
435
922
  # 将PYFILE_PRAGMA_INSERTS.replace("\t", "").replace(" ", "")插入到文件开头
436
923
  content = self.auto_read(fpath)
437
924
  content = self.PYFILE_PRAGMA_INSERTS.replace("\t", "").replace(" ", "") + content
925
+ # content = self.remove_long_docstring(content) # 移除长注释 | remove long docstring
438
926
 
439
927
  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
928
  f.write(content)
@@ -468,6 +956,8 @@ class Compiler(CompilerBase):
468
956
  if m and (not m.group(2) or m.group(2)[0] != '*'):
469
957
  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
958
 
959
+ self.expand_folder_imports(fpath, self.build_dir)
960
+
471
961
  # -------------------------------- EXPAND IMPORT * -------------------------------- #
472
962
  _imports = self.find_chain_import(self.target_py, [os.path.dirname(self.src_dir), self.src_dir])
473
963
  _js_imports = self.relist_pyimports_to_jsimports(self.build_dir, _imports)
@@ -506,6 +996,14 @@ class Compiler(CompilerBase):
506
996
  else:
507
997
  _pre_sort_[fname] = 65535
508
998
 
999
+ # ------------------------------------ 自定义:mate & match ------------------------------------ #
1000
+ for fpath in py_fpath:
1001
+ content = self.auto_read(fpath)
1002
+ content = self.process_mate_code(content) # 调用process_mate_code
1003
+ content = self.convert_match_to_if(content) # 调用convert_match_to_if
1004
+ with open(fpath, 'w', encoding='utf-8') as f:
1005
+ f.write(content)
1006
+
509
1007
  # ------------------------------------ DEFINE ------------------------------------ #
510
1008
  # 扫描所有# > define的内容,然后在.py中移除这些行,并记录下来
511
1009
  # | get all # > define in .py files, and record them
@@ -579,11 +1077,10 @@ class Compiler(CompilerBase):
579
1077
  with open(fpath, 'w', encoding='utf-8') as f:
580
1078
  f.write(new_content)
581
1079
 
582
- # ------------------------------------ 自定义 ------------------------------------ #
583
- # 调用stage_recursive_replace
1080
+ # ------------------------------------ 自定义:调用stage_recursive_replace ------------------------------------ #
584
1081
  for fpath in py_fpath:
585
1082
  content = self.auto_read(fpath)
586
- content = self.stage_recursive_replace(content)
1083
+ content = self.stage_recursive_replace(content) # 调用stage_recursive_replace
587
1084
  with open(fpath, 'w', encoding='utf-8') as f:
588
1085
  f.write(content)
589
1086
 
@@ -591,9 +1088,9 @@ class Compiler(CompilerBase):
591
1088
  return _imports, _js_imports, _pre_sort_, _pre_define_, _js_replace_
592
1089
 
593
1090
  def transcrypt_cmd(self):
594
- # 执行cmd命令: transcrypt -b -m -n -s -e 6 target | execute cmd: transcrypt -b -m -n -s -e 6 target
1091
+ # 执行cmd命令: python -m transcrypt -b -m -n -s -e 6 target | execute cmd: python -m transcrypt -b -m -n -s -e 6 target
595
1092
  # 并获取cmd得到的输出 | and get the output of the cmd
596
- cmd = 'transcrypt -b -m -n -s -e 6 %s' % self.target_py
1093
+ cmd = 'python -m transcrypt -b -m -n -s -e 6 %s' % self.target_py
597
1094
  core.lprint(WAIT, core.lformat(LOC_TRANSCRYPTING, [cmd]), end="", ln=config.language)
598
1095
  p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
599
1096
  stdout, stderr = p.communicate()
@@ -657,7 +1154,7 @@ class Compiler(CompilerBase):
657
1154
 
658
1155
  content = self.auto_read(self.target_js)
659
1156
  if modules is None: modules = []
660
- new_modules, new_content = [],""
1157
+ new_modules, new_content = [], ""
661
1158
  for line in content.split('\n'):
662
1159
  m = re.search(self.JS_IMPORT_PAT, line)
663
1160
  if not m:
@@ -703,7 +1200,7 @@ class Compiler(CompilerBase):
703
1200
  """
704
1201
  return re.sub(r'import[^\n]*\n', '', raw)
705
1202
 
706
- def generate_total_js(self, usr_modules, t_imps: list[str], f_sorts, f_replaces, g_replaces) -> str:
1203
+ def generate_total_js(self, usr_modules, t_imps: list[str], f_sorts, f_replaces, g_replaces, min_js_files=None) -> str:
707
1204
  """
708
1205
  生成总的main.js
709
1206
  按照如下顺序组合:
@@ -717,13 +1214,20 @@ class Compiler(CompilerBase):
717
1214
  :param f_sorts: dict{module_name: sort_priority}
718
1215
  :param f_replaces: dict{module_name: dict{old: new}}
719
1216
  :param g_replaces: dict{old: new}
1217
+ :param min_js_files: list[str] # .min.js文件路径列表
720
1218
  :return: str
721
1219
  """
722
1220
  arena_name = const.ARENA_NAMES.get(config.arena, const.ARENA_NAMES['green']) # like green -> spawn_and_swamp
723
1221
  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
1222
  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"
1223
+ total_js += f"const __AUTHOR__ = '{const.AUTHOR}';\nconst __AUTHOR_CN__ = '{const.BILIBILI_NAME}';"
1224
+
1225
+ # 添加.min.js文件的import语句
1226
+ if min_js_files:
1227
+ for min_js_path in min_js_files:
1228
+ min_js_filename = os.path.basename(min_js_path)
1229
+ total_js += f"\nimport \"./{min_js_filename}\";"
1230
+ total_js += "\n"
727
1231
 
728
1232
  core.lprint(WAIT, LOC_GENERATING_TOTAL_MAIN_JS, end="", ln=config.language)
729
1233
 
@@ -790,19 +1294,29 @@ class Compiler(CompilerBase):
790
1294
  def find_add_pure_js_files(self, sorts, modules):
791
1295
  """
792
1296
  找到所有的纯js文件,并添加到modules中
1297
+ 忽略.min.js文件,这些文件会被单独处理
793
1298
  :param sorts:
794
1299
  :param modules:
795
- :return:
1300
+ :return: list 返回所有.min.js文件的列表
796
1301
  """
1302
+ min_js_files = []
797
1303
  for root, dirs, files in os.walk(self.lib_dir):
798
1304
  for file in files:
799
1305
  if file.endswith('.js') and file not in modules:
800
- fpath = str(os.path.join(root, file))
801
- fname = file.replace('\\', '/')
802
- # copy file to target
803
- shutil.copy(fpath, os.path.join(self.target_dir, fname))
804
- sorts[fname] = self.__parse_js_file_sort(fpath)
805
- modules.append("./" + fname)
1306
+ # 如果是.min.js文件,不拷贝到target,而是记录到单独列表
1307
+ if file.endswith('.min.js'):
1308
+ fpath = str(os.path.join(root, file))
1309
+ min_js_files.append(fpath)
1310
+ else:
1311
+ # 普通js文件,按原逻辑处理
1312
+ fpath = str(os.path.join(root, file))
1313
+ fname = file.replace('\\', '/')
1314
+ # copy file to target
1315
+ shutil.copy(fpath, os.path.join(self.target_dir, fname))
1316
+ sorts[fname] = self.__parse_js_file_sort(fpath)
1317
+ modules.append("./" + fname)
1318
+
1319
+ return min_js_files
806
1320
 
807
1321
  def compile(self, paste=False):
808
1322
  """
@@ -813,8 +1327,7 @@ class Compiler(CompilerBase):
813
1327
  imps, jimps, sorts, defs, reps = self.pre_compile()
814
1328
  self.transcrypt_cmd()
815
1329
  imports, modules = self.analyze_rebuild_main_js(defs, jimps)
816
- 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)
1330
+ min_js_files = self.find_add_pure_js_files(sorts, modules)
818
1331
 
819
1332
  core.lprint(WAIT, LOC_EXPORTING_TOTAL_MAIN_JS, end="", ln=config.language)
820
1333
 
@@ -825,12 +1338,28 @@ class Compiler(CompilerBase):
825
1338
  if not mjs_path.endswith('js'):
826
1339
  mjs_path = os.path.join(mjs_path, 'main.mjs')
827
1340
 
1341
+ # 获取目标目录路径
1342
+ dir_path = os.path.dirname(mjs_path)
1343
+ build_dir_path = os.path.dirname(build_main_mjs)
1344
+
1345
+ # 复制.min.js文件到目标目录
1346
+ for min_js_path in min_js_files:
1347
+ min_js_filename = os.path.basename(min_js_path)
1348
+ # 复制到build目录
1349
+ shutil.copy(min_js_path, os.path.join(build_dir_path, min_js_filename))
1350
+ # 复制到最终导出目录
1351
+ shutil.copy(min_js_path, os.path.join(dir_path, min_js_filename))
1352
+
1353
+ # 生成total_js,传入.min.js文件列表
1354
+ total_js = imports + "\n" + self.generate_total_js(
1355
+ replace_src_prefix(modules), imps, sorts, self.FILE_STRONG_REPLACE, reps, min_js_files
1356
+ )
1357
+
828
1358
  # write main.mjs
829
1359
  with open(build_main_mjs, 'w', encoding='utf-8') as f:
830
1360
  f.write(total_js)
831
1361
 
832
1362
  # export main.mjs
833
- dir_path = os.path.dirname(mjs_path)
834
1363
  if not os.path.exists(dir_path):
835
1364
  core.error('Compiler.compile', core.lformat(LOC_EXPORT_DIR_PATH_NOT_EXISTS, [dir_path]), head='\n', ln=config.language)
836
1365
  with open(mjs_path, 'w', encoding='utf-8') as f:
@@ -889,6 +1418,20 @@ class Compiler(CompilerBase):
889
1418
 
890
1419
 
891
1420
  if __name__ == '__main__':
892
- compiler = Compiler('src', 'library', 'build')
893
- compiler.compile()
894
- compiler.clean()
1421
+ # compiler = Compiler('src', 'library', 'build')
1422
+ # compiler.compile()
1423
+ # compiler.clean()
1424
+ test = """
1425
+
1426
+ def patrolling(self, c: Creep):
1427
+ e = self.center.nearest(k.civilian.enemies, 5)
1428
+ if e:
1429
+ match c.test(e,
1430
+ int(c.hpPer <= 0.9) * 2,
1431
+ int(c.info.melee) * -3
1432
+ ):
1433
+ case True: c.move(e, SWAMP_MOTION)
1434
+ case False: c.move(self.bpos, SWAMP_MOTION)
1435
+
1436
+ """
1437
+ print(f"res=\n{Compiler.convert_match_to_if(test)}")