From 81b61dbf9b3a35323d3770850d1e05933595f04b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:37:26 +0000 Subject: [PATCH 1/5] Initial plan From e558d9f00f34e0881e01f54f0838b6f58c76324d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:00:33 +0000 Subject: [PATCH 2/5] Convert all JavaScript to Python Co-authored-by: jncraton <103612+jncraton@users.noreply.github.com> --- .github/workflows/build.yml | 5 - makefile | 2 +- tools/abbrevs.py | 36 +++ tools/ai.py | 67 ++++++ tools/buildConverter.py | 63 ++++++ tools/build_ai.py | 46 ++++ tools/build_launcher_package.py | 41 ++++ tools/macros.py | 377 ++++++++++++++++++++++++++++++++ tools/race.py | 283 ++++++++++++++++++++++++ 9 files changed, 914 insertions(+), 6 deletions(-) create mode 100644 tools/abbrevs.py create mode 100644 tools/ai.py create mode 100644 tools/buildConverter.py create mode 100755 tools/build_ai.py create mode 100755 tools/build_launcher_package.py create mode 100644 tools/macros.py create mode 100644 tools/race.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 589acd4..9fafade 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,11 +10,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Set up Node.js - uses: actions/setup-node@v2 - with: - node-version: 14 - - name: Install python3-tk run: sudo apt-get install --fix-missing python3-tk diff --git a/makefile b/makefile index bef32a5..62dda82 100644 --- a/makefile +++ b/makefile @@ -63,7 +63,7 @@ build/combined.pyai: build/terran.pyai build/zerg.pyai build/protoss.pyai build/%.pyai: src/% @echo Building $@ $< @cp tools/config_$(config).json tools/config.json - @node tools/build_ai $(subst src/,,$<) $@; + @python3 tools/build_ai.py $(subst src/,,$<) $@; @rm tools/config.json clean: diff --git a/tools/abbrevs.py b/tools/abbrevs.py new file mode 100644 index 0000000..014b45f --- /dev/null +++ b/tools/abbrevs.py @@ -0,0 +1,36 @@ +import json +import re +import os + +# Load abbreviations from JSON file +script_dir = os.path.dirname(os.path.abspath(__file__)) +with open(os.path.join(script_dir, 'abbrevs.json'), 'r') as f: + abbrevs = json.load(f) + +# Build abbreviation replacements +abbrevs_replacements = [] + +for key in abbrevs: + for short in abbrevs[key]: + abbrevs_replacements.append({ + 'short': short, + 'long': key, + }) + +def expand(abbrev): + """Expand a single abbreviation to its full form""" + for a in abbrevs_replacements: + abbrev = re.sub(r'^' + a['short'] + r'$', a['long'], abbrev, flags=re.IGNORECASE) + return abbrev + +def parse(content): + """Parse content and expand abbreviations in function arguments""" + def replace_abbrev(match): + prefix = match.group(1) + arg = match.group(2) + postfix = match.group(3) + arg = expand(arg) + return prefix + arg + postfix + + content = re.sub(r"([,\(] *)([A-Za-z ']*?)([,\)])", replace_abbrev, content) + return content diff --git a/tools/ai.py b/tools/ai.py new file mode 100644 index 0000000..0452787 --- /dev/null +++ b/tools/ai.py @@ -0,0 +1,67 @@ +import re +import sys +import os + +# Add tools directory to path for imports +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from race import Race + +class AI: + def __init__(self, race_name, config): + self.race_name = race_name + self.race = Race(race_name, config) + self.src = "" + self.config = config + + def build(self): + """Build the AI script for the race""" + # Default boilerplate + if self.race_name == 'terran': + self.src = 'TMCx(1342, 101, aiscript):\n' + elif self.race_name == 'protoss': + self.src = 'PMCx(1343, 101, aiscript):\n' + elif self.race_name == 'zerg': + self.src = 'ZMCx(1344, 101, aiscript):\n' + + self.src += self.race.load_contents('main') + + # Add wait(1) before non-special lines + def add_wait(match): + return 'wait(1)\n' + match.group(0) + '\n' + + self.src = re.sub(r'^(?!(TMCx|ZMCx|PMCx|\-\-|#|debug|random)).+$', + add_wait, self.src, flags=re.MULTILINE) + + # Add debug blocks for verbosity >= 10 + verbosity = self.config.get('verbosity', 0) + race_verbosity = self.config.get(self.race_name, {}).get('verbosity', 0) + + if verbosity >= 10 or race_verbosity >= 10: + debug_count = 0 + + def get_code(num): + """Generate a short code from a number""" + valid_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_' + + tens = num // len(valid_chars) + remainder = num - (tens * len(valid_chars)) + + tens -= 1 + + tens_char = valid_chars[tens] if tens >= 0 else '' + return tens_char + valid_chars[remainder] + + def add_debug(match): + nonlocal debug_count + debug_count += 1 + block_name = f'd10_{debug_count}' + code = get_code(debug_count) + + return (f'debug({block_name}, {code})\n' + f'--{block_name}--\n' + + match.group(0) + '\n') + + self.src = re.sub(r'^(?!(TMCx|ZMCx|PMCx|\-\-|#|debug|wait)).+$', + add_debug, self.src, flags=re.MULTILINE) + + return self.src diff --git a/tools/buildConverter.py b/tools/buildConverter.py new file mode 100644 index 0000000..0d21b70 --- /dev/null +++ b/tools/buildConverter.py @@ -0,0 +1,63 @@ +import json +import re +import os +import sys + +# Add tools directory to path for imports +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import abbrevs + +# Load units from JSON file +script_dir = os.path.dirname(os.path.abspath(__file__)) +with open(os.path.join(script_dir, 'units.json'), 'r') as f: + units = json.load(f) + +def is_unit(unit): + """Check if the given string is a unit""" + return any(u['unit'] == unit for u in units) + +def get_unit_info(unit): + """Get information for a specific unit""" + for u in units: + if u['unit'] == unit: + return u + return None + +def parse(content): + """Parse build orders and convert them to script commands""" + owned = {} + supply_from_units = 0 + + def replace_build(match): + nonlocal supply_from_units + supply = int(match.group(1)) + unit = match.group(2) + + unit = abbrevs.expand(unit) + + if unit not in owned: + owned[unit] = 0 + owned[unit] += 1 + + wait_for_worker = int(supply - supply_from_units) + + ret = f'build({wait_for_worker}, Peon, 80)\n' + ret += f'wait_buildstart({wait_for_worker}, Peon)\n' + + if is_unit(unit): + unit_info = get_unit_info(unit) + # Use int() to match JavaScript's parseInt() behavior which truncates decimals + supply_from_units += int(float(unit_info['supply'])) + + ret += f'train({owned[unit]}, {unit})\n' + elif unit == 'Expand' or unit == 'expand': + ret += 'expand(1, gen_expansions_expansion)\n' + else: + ret += f'build({owned[unit]}, {unit}, 80)\n' + ret += f'wait_buildstart({owned[unit]}, {unit})\n' + + return ret + + content = re.sub(r'^(\d+) (.*)$', replace_build, content, flags=re.MULTILINE) + + return content diff --git a/tools/build_ai.py b/tools/build_ai.py new file mode 100755 index 0000000..13f845a --- /dev/null +++ b/tools/build_ai.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +import sys +import os +import json +import subprocess +from datetime import datetime + +# Add tools directory to path for imports +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from ai import AI + +def build(input_race, output_file): + """Build the AI script for the given race""" + # Load config + script_dir = os.path.dirname(os.path.abspath(__file__)) + config_path = os.path.join(script_dir, 'config.json') + + with open(config_path, 'r') as f: + config = json.load(f) + + # Build the AI + ai = AI(input_race, config) + src = ai.build() + + # Get git commit hash + try: + result = subprocess.run(['git', 'rev-parse', 'HEAD'], + capture_output=True, text=True, check=True) + commit = result.stdout.strip()[:6] + except: + commit = '000000' + + # Replace placeholders + src = src.replace('{commit}', commit) + src = src.replace('{now}', datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + + # Write output + with open(output_file, 'w', encoding='utf-8') as f: + f.write(src) + +if __name__ == '__main__': + if len(sys.argv) >= 3: + build(sys.argv[1], sys.argv[2]) + else: + print(f'Usage: python3 {sys.argv[0]} race output') + sys.exit(1) diff --git a/tools/build_launcher_package.py b/tools/build_launcher_package.py new file mode 100755 index 0000000..25b1d98 --- /dev/null +++ b/tools/build_launcher_package.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import os +import json +import sys + +# Add tools directory to path for imports +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import build_ai + +races = ['zerg', 'terran', 'protoss'] + +for race in races: + builds_dir = os.path.join('src', race, 'builds') + + if not os.path.exists(builds_dir): + continue + + builds = [f for f in os.listdir(builds_dir) if f.endswith('.pyai')] + + for build in builds: + print(race, build) + + # Load default config + with open('tools/config_default.json', 'r') as f: + config = json.load(f) + + # Set the specific build + if race not in config: + config[race] = {} + config[race]['useBuild'] = build + + # Write temporary config + with open('tools/config.json', 'w') as f: + json.dump(config, f) + + # Build the AI + output_dir = os.path.join('dist', 'BWAILauncher_package', race) + os.makedirs(output_dir, exist_ok=True) + + output_file = os.path.join(output_dir, build.replace('.pyai', '.txt')) + build_ai.build(race, output_file) diff --git a/tools/macros.py b/tools/macros.py new file mode 100644 index 0000000..6dd34cc --- /dev/null +++ b/tools/macros.py @@ -0,0 +1,377 @@ +import json +import re +import os + +# Load config +script_dir = os.path.dirname(os.path.abspath(__file__)) + +block_counter = 0 + +def next_block_name(): + """Generate next unique block name""" + global block_counter + block_counter += 1 + return f'gen_macro_{block_counter}' + +def expand_enemy_owns(units, block): + """Expand enemy owns check for multiple units""" + out = "" + for unit in units: + out += f'enemyowns_jump({unit}, {block})\n' + return out + +def parse(content): + """Parse content and expand macros""" + global block_counter + + # Load config if it exists + config = {} + config_path = os.path.join(script_dir, 'config.json') + if os.path.exists(config_path): + with open(config_path, 'r') as f: + config = json.load(f) + + lines = re.split(r'[\r\n]+', content) + result = '' + indent_level = 0 + blocks = [] + + for line in lines: + if indent_level > 0: + for i in range(indent_level, 0, -1): + if ' ' in line: + line = line.replace(' ', '', 1) + else: + indent_level -= 1 + result += blocks.pop() + + if 'if ' in line: + def replace_if(match): + function_name = match.group(1) + params = match.group(2) + if function_name == 'owned': + return f'if_owned({params})' + elif function_name == 'townpoint': + return f'try_townpoint({params})' + else: + return f'{function_name}_jump({params})' + + line = re.sub(r'if (.*)\((.*)\)', replace_if, line) + + if ':' in line: + start_block = next_block_name() + end_block = next_block_name() + + if 'multirun_loop' in line: + line = line.replace('multirun_loop', 'multirun()') + blocks.append(f'wait(75)\ngoto({start_block})\n--{end_block}--\n') + elif 'multirun' in line: + line = line.replace('multirun', 'multirun()') + blocks.append(f'stop()\n--{end_block}--\n') + elif 'loop' in line: + line = line.replace('loop', 'goto()') + blocks.append(f'wait(75)\ngoto({start_block})\n--{end_block}--\n') + else: + blocks.append(f'--{end_block}--\n') + + indent_level += 1 + line = line.replace(':', '') + if re.search(r'[a-zA-Z0-9]\)', line): + line = re.sub(r'\)', f', {start_block})', line) + else: + line = re.sub(r'\)', f'{start_block})', line) + result += line + '\n' + result += f'goto({end_block})\n' + result += f'--{start_block}--\n' + else: + result += line + '\n' + + while blocks: + result += blocks.pop() + + # attack_async + def replace_attack_async(match): + start = next_block_name() + escape = next_block_name() + + return (f'multirun({start})\n' + f'goto({escape})\n' + f'--{start}--\n' + f'attack_do()\n' + f'attack_clear()\n' + f'stop()\n' + f'--{escape}--\n') + + result = re.sub(r'attack_async\(\)', replace_attack_async, result) + + # attack_simple + def replace_attack_simple(match): + return 'attack_do()\nattack_prepare()\nattack_clear()' + + result = re.sub(r'attack_simple\(\)', replace_attack_simple, result) + + # wait_resources + def replace_wait_resources(match): + minerals = match.group(1) + gas = match.group(2) + loop_start = next_block_name() + loop_escape = next_block_name() + + return (f'--{loop_start}--\n' + f'resources_jump({minerals},{gas},{loop_escape})\n' + f'wait(10)\n' + f'goto({loop_start})\n' + f'--{loop_escape}--\n') + + result = re.sub(r'wait_resources\((.*),(.*)\)', replace_wait_resources, result) + + # wait_until + def replace_wait_until(match): + time = match.group(1) + loop_start = next_block_name() + loop_escape = next_block_name() + + return (f'--{loop_start}--\n' + f'time_jump({time},{loop_escape})\n' + f'wait(300)\n' + f'goto({loop_start})\n' + f'--{loop_escape}--\n') + + result = re.sub(r'wait_until\((.*)\)', replace_wait_until, result) + + # wait_owned + def replace_wait_owned(match): + unit = match.group(1) + loop_start = next_block_name() + loop_escape = next_block_name() + + return (f'--{loop_start}--\n' + f'if_owned({unit},{loop_escape})\n' + f'wait(300)\n' + f'goto({loop_start})\n' + f'--{loop_escape}--\n') + + result = re.sub(r'wait_owned\((.*)\)', replace_wait_owned, result) + + # message + def replace_message(match): + message = match.group(1) + next_block = next_block_name() + + return (f'debug({next_block},{message})\n' + f'--{next_block}--\n') + + result = re.sub(r'message\((.*)\)', replace_message, result) + + # enemyownscloaked_jump + def replace_enemyownscloaked_jump(match): + block = match.group(1) + units = ['Zerg Lurker', 'Protoss Arbiter', 'Protoss Templar Archives', + 'Protoss Dark Templar', 'Terran Ghost', 'Terran Wraith'] + return expand_enemy_owns(units, block) + + result = re.sub(r'enemyownscloaked_jump\((.*)\)', replace_enemyownscloaked_jump, result) + + # rush_jump + def replace_rush_jump(match): + block = match.group(1) + too_late_for_buildings = next_block_name() + too_late_for_units = next_block_name() + + return (f'time_jump(2, {too_late_for_buildings})\n' # 2 is roughly 1:20 + + expand_enemy_owns(['Zerg Spawning Pool', 'Terran Barracks', 'Protoss Gateway'], block) + '\n' + f'--{too_late_for_buildings}--\n' + f'time_jump(4, {too_late_for_units})\n' # 4 is roughly 2:50 + + expand_enemy_owns(['Zerg Zergling', 'Terran Marine', 'Protoss Zealot'], block) + '\n' + f'--{too_late_for_units}--') + + result = re.sub(r'rush_jump\((.*)\)', replace_rush_jump, result) + + # enemyownsairtech_jump + def replace_enemyownsairtech_jump(match): + block = match.group(1) + units = ['Terran Starport', 'Protoss Stargate', 'Zerg Spire'] + return expand_enemy_owns(units, block) + + result = re.sub(r'enemyownsairtech_jump\((.*)\)', replace_enemyownsairtech_jump, result) + + # enemyownsair_jump + def replace_enemyownsair_jump(match): + block = match.group(1) + units = ['Terran Science Vessel', 'Terran Wraith', 'Terran Valkyrie', 'Terran Battlecruiser', + 'Zerg Mutalisk', 'Zerg Scourge', 'Zerg Guardian', 'Zerg Devourer', 'Zerg Queen', + 'Protoss Scout', 'Protoss Corsair', 'Protoss Carrier', 'Protoss Arbiter'] + return expand_enemy_owns(units, block) + + result = re.sub(r'enemyownsair_jump\((.*)\)', replace_enemyownsair_jump, result) + + # build_start + def replace_build_start(match): + args = match.group(1).split(',') + amount = args[0] + building = args[1] + priority = args[2] if len(args) > 2 else '80' + return (f'build({amount}, {building}, {priority})\n' + f'wait_buildstart({amount}, {building})') + + result = re.sub(r'build_start\((.*)\)', replace_build_start, result) + + # build_finish + def replace_build_finish(match): + args = match.group(1).split(',') + amount = args[0] + building = args[1] + priority = args[2] if len(args) > 2 else '80' + return (f'build({amount}, {building}, {priority})\n' + f'wait_buildstart({amount}, {building})\n' + f'wait_build({amount}, {building})') + + result = re.sub(r'build_finish\((.*)\)', replace_build_finish, result) + + # build_separately + def replace_build_separately(match): + args = match.group(1).split(',') + amount = int(args[0]) + building = args[1] + priority = args[2] if len(args) > 2 else '80' + ret = '' + + for i in range(1, amount + 1): + ret += (f'build({i}, {building}, {priority})\n' + f'wait_buildstart({i}, {building})\n' + f'wait_build({i}, {building})\n') + + return ret + + result = re.sub(r'build_separately\((.*)\)', replace_build_separately, result) + + # attack_train + def replace_attack_train(match): + args = match.group(1).split(',') + amount = args[0] + unit = args[1] + return (f'do_morph({amount}, {unit})\n' + f'attack_add({amount}, {unit})') + + result = re.sub(r'attack_train\((.*)\)', replace_attack_train, result) + + # defenseclear + def replace_defenseclear(match): + return ('defenseclear_gg()\n' + 'defenseclear_ga()\n' + 'defenseclear_ag()\n' + 'defenseclear_aa()\n') + + result = re.sub(r'defenseclear\(\)', replace_defenseclear, result) + + # defense_ground + def replace_defense_ground(match): + unit = match.group(1) + do_build = next_block_name() + skip_build = next_block_name() + + return (f'defenseuse_gg(1, {unit})\n' + f'defenseuse_ga(1, {unit})\n' + f'time_jump(6, {do_build})\n' + f'goto({skip_build})\n' + f'--{do_build}--\n' + f'defensebuild_gg(1, {unit})\n' + f'defensebuild_ga(1, {unit})\n' + f'--{skip_build}--\n') + + result = re.sub(r'defense_ground\((.*)\)', replace_defense_ground, result) + + # defense_air + def replace_defense_air(match): + unit = match.group(1) + do_build = next_block_name() + skip_build = next_block_name() + + return (f'defenseuse_ag(1, {unit})\n' + f'defenseuse_aa(1, {unit})\n' + f'time_jump(6, {do_build})\n' + f'goto({skip_build})\n' + f'--{do_build}--\n' + f'defensebuild_ag(1, {unit})\n' + f'defensebuild_aa(1, {unit})\n' + f'--{skip_build}--\n') + + result = re.sub(r'defense_air\((.*)\)', replace_defense_air, result) + + # defense_ground_train + def replace_defense_ground_train(match): + unit = match.group(1) + return (f'defenseuse_gg(1, {unit})\n' + f'defenseuse_ga(1, {unit})\n' + f'defensebuild_gg(1, {unit})\n' + f'defensebuild_ga(1, {unit})\n') + + result = re.sub(r'defense_ground_train\((.*)\)', replace_defense_ground_train, result) + + # defense_air_train + def replace_defense_air_train(match): + unit = match.group(1) + return (f'defenseuse_ag(1, {unit})\n' + f'defenseuse_aa(1, {unit})\n' + f'defensebuild_ag(1, {unit})\n' + f'defensebuild_aa(1, {unit})\n') + + result = re.sub(r'defense_air_train\((.*)\)', replace_defense_air_train, result) + + # (difficulty + def replace_difficulty(match): + difficulty = config.get('difficulty', 0) if config else 0 + return f'({difficulty}' + + result = re.sub(r'\(difficulty', replace_difficulty, result) + + # create_bonus_workers + def replace_create_bonus_workers(match): + bonus_workers = config.get('bonusWorkers', 0) if config else 0 + ret = '' + for i in range(bonus_workers): + ret += 'create_unit(Peon, 2000, 2000)\n' + return ret + + result = re.sub(r'create_bonus_workers\(\)', replace_create_bonus_workers, result) + + # attack_multiple + def replace_attack_multiple(match): + mul = int(match.group(1)) + params = match.group(2) + ret = '' + + unit_strs = params.split(',') + units = [] + + for unit_str in unit_strs: + unit_str = unit_str.strip() + parts = unit_str.split(' ') + quantity = parts[0] + name = ' '.join(parts[1:]) + units.append({'quantity': quantity, 'name': name}) + + more_units_prob = 256 // mul + done_block = next_block_name() + + for i in range(1, mul + 1): + more_units = next_block_name() + + for unit in units: + ret += f'train({int(unit["quantity"]) * i}, {unit["name"]})\n' + + ret += f'random_jump({more_units_prob},{more_units})\n' + + for unit in units: + ret += f'attack_add({int(unit["quantity"]) * i}, {unit["name"]})\n' + + ret += f'goto({done_block})\n' + ret += f'--{more_units}--\n' + + ret += f'--{done_block}--\n' + + return ret + + result = re.sub(r'attack_multiple\((.*?), (.*)\)', replace_attack_multiple, result) + + return result diff --git a/tools/race.py b/tools/race.py new file mode 100644 index 0000000..28bfc62 --- /dev/null +++ b/tools/race.py @@ -0,0 +1,283 @@ +import os +import re +import json +import sys + +# Add tools directory to path for imports +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import abbrevs +import macros +import buildConverter + +debug_count = 0 +block_count = 0 +nonce = 0 + +class Race: + def __init__(self, name, config): + self.name = name + self.config = config + + def get_full_path(self, filename): + """Get the full path for a given filename""" + filename = filename.replace('.pyai', '') + '.pyai' + + src_path = self.config.get('srcPath', 'src/') + + paths_to_check = [ + os.path.join(src_path, self.name, filename), + os.path.join(src_path, self.name, 'managers', filename), + os.path.join(src_path, 'managers', filename), + os.path.join(src_path, self.name, 'expansions', filename), + os.path.join(src_path, 'expansions', filename), + os.path.join(src_path, filename), + ] + + for path in paths_to_check: + if os.path.exists(path): + return path + + # Return the first guess if file doesn't exist + return paths_to_check[0] + + def load_contents(self, filename, skip_block_header=False): + """Load and parse template file contents""" + filename = self.get_full_path(filename) + return self.parse_template(filename, skip_block_header) + "\n" + + def get_file_block(self, filename): + """Generate a block name from a filename""" + src_path = self.config.get('srcPath', 'src/') + block = 'gen_' + filename.replace(src_path, '') + block = re.sub(r'[-_ /]', '_', block) + block = block.replace('.pyai', '') + block = block.replace(self.name, '') + block = re.sub(r'__', '_', block) + return block + + def parse_template(self, filename, skip_block_header=False): + """Parse a template file and return the processed content""" + global debug_count, block_count, nonce + + comment = f"\n#{filename}\n" + + if not skip_block_header: + file_block = self.get_file_block(filename) + else: + file_block = self.get_file_block(filename) + str(nonce) + nonce += 1 + + if 'header' in filename: + block = '' + else: + block = f'--{file_block}--\n' + + with open(filename, 'r', encoding='utf-8') as f: + content = f.read() + + # Replace repeat() + content = re.sub(r'repeat\(\)', f'wait(75)\ngoto({file_block})', content) + + # Replace include() + def replace_include(match): + inc_filename = match.group(1) + if inc_filename == 'freemoney' and self.config.get('difficulty', 0) == 0: + return "" + return self.load_contents(inc_filename, True) + + content = re.sub(r'include\((.*)\)', replace_include, content) + + # Replace include_block() + def replace_include_block(match): + inc_filename = match.group(1) + return self.load_contents(inc_filename) + "stop()\n" + + content = re.sub(r'include_block\((.*)\)', replace_include_block, content) + + # Replace expand() + def replace_expand(match): + num = match.group(1) + block = match.group(2) + if 'gen_expansions' in block: + return f'expand({num}{block})' + else: + return f'expand({num}gen_expansions_{block})' + + content = re.sub(r'expand\(([\d, ]+)(.*)\)', replace_expand, content) + + # Replace panic() + def replace_panic(match): + block = match.group(1) + return f'panic(gen_expansions_{block})' + + content = re.sub(r'panic\((.*)\)', replace_panic, content) + + # Replace multirun_file() + def replace_multirun_file(match): + global block_count + + relative_filename = match.group(1) + block_count += 1 + + # Note: Using "undefined" to match JavaScript bug where 'block' variable + # is used before being reassigned in the replacement function + done_block = "undefined_done_" + str(block_count) + multirun_block = self.get_file_block(self.get_full_path(relative_filename)) + + return (f"multirun({multirun_block})\n" + f"goto({done_block})\n" + + self.load_contents(relative_filename) + "\n" + f"stop()\n" + f"--{done_block}--") + + content = re.sub(r'multirun_file\((.*)\)', replace_multirun_file, content) + + # Replace choose_from_dir() + def replace_choose_from_dir(match): + dir_name = match.group(1) + return self.choose_from_dir(dir_name, file_block) + + content = re.sub(r'choose_from_dir\((.*)\)', replace_choose_from_dir, content) + + # Replace build_weight() + def replace_build_weight(match): + weight = float(match.group(1)) + skip_chance = int((1 - weight) * 255) + return f'random_jump({skip_chance}, gen_builds)' + + content = re.sub(r'build_weight\((.*)\)', replace_build_weight, content) + + # Replace style_weight() + def replace_style_weight(match): + weight = float(match.group(1)) + skip_chance = int((1 - weight) * 255) + return f'random_jump({skip_chance}, gen_lategame)' + + content = re.sub(r'style_weight\((.*)\)', replace_style_weight, content) + + # Replace use_build_vs() + def replace_use_build_vs(match): + races = match.group(1) + message = '' + + verbosity = self.config.get('verbosity', 0) + race_verbosity = self.config.get(self.name, {}).get('verbosity', 0) + + if verbosity >= 5 or race_verbosity >= 5: + build_name = re.sub(r'[\-\n ]', '', block) + build_name = build_name.replace('gen_builds_', '').replace('_', ' ') + message = self.debug(f'Using {build_name} build') + + return self.race_skip(races, 'gen_builds', file_block) + message + + content = re.sub(r'use_build_vs\((.*)\)', replace_use_build_vs, content) + + # Replace use_attack_vs() + def replace_use_attack_vs(match): + races = match.group(1) + message = '' + + verbosity = self.config.get('verbosity', 0) + race_verbosity = self.config.get(self.name, {}).get('verbosity', 0) + + if verbosity >= 5 or race_verbosity >= 5: + style_name = re.sub(r'[\-\n ]', '', block) + style_name = style_name.replace('gen_attack_', '').replace('_', ' ') + message = self.debug(f'Using {style_name} attack') + + return 'wait(50)\n' + self.race_skip(races, 'gen_attacks', file_block) + message + + content = re.sub(r'use_attack_vs\((.*)\)', replace_use_attack_vs, content) + + # Apply buildConverter, macros, and abbrevs + content = buildConverter.parse(content) + content = macros.parse(content) + content = abbrevs.parse(content) + + # Replace race-specific placeholders + if self.name == 'terran': + content = content.replace('Town Hall', "Terran Command Center") + content = content.replace('Peon', "Terran SCV") + content = content.replace('Gas', "Terran Refinery") + elif self.name == 'zerg': + content = content.replace('Town Hall', "Zerg Hatchery") + content = content.replace('Peon', "Zerg Drone") + content = content.replace('Gas', "Zerg Extractor") + elif self.name == 'protoss': + content = content.replace('Town Hall', "Protoss Nexus") + content = content.replace('Peon', "Protoss Probe") + content = content.replace('Gas', "Protoss Assimilator") + + return comment + block + content + + def choose_from_dir(self, dir_name, file_block): + """Generate code to randomly choose from files in a directory""" + ret = "" + ret += f'--gen_{dir_name}--\n' + ret += f'--gen_jump_loop{dir_name}--\n' + + files = [] + src_path = self.config.get('srcPath', 'src/') + dir_path = os.path.join(src_path, self.name, dir_name) + + try: + if os.path.exists(dir_path): + files = sorted([f for f in os.listdir(dir_path) if f.endswith('.pyai')]) + except Exception as e: + print(f'Missing directory {dir_path}') + + def get_build_contents(build): + build = build.replace('.pyai', '') + contents = self.load_contents(os.path.join(self.name, dir_name, build + '.pyai')) + return contents + + race_config = self.config.get(self.name, {}) + use_build = race_config.get('useBuild') + + if dir_name == 'builds' and use_build: + ret += get_build_contents(use_build) + else: + if files: + for file in files: + file_name = file.replace('.pyai', '').replace(' ', '_') + if file[0] == '_': + ret += f"goto(gen_{dir_name}_{file_name.replace('_', '', 1)})\n" + else: + ret += f"random_jump(2, gen_{dir_name}_{file_name})\n" + + ret += f'goto(gen_jump_loop{dir_name})\n' + + for file in files: + ret += get_build_contents(file) + ret += f'goto(gen_end_{dir_name})\n' + + ret += f'--gen_end_{dir_name}--\n' + + return ret + + def race_skip(self, races, skip_block, file_block): + """Generate race detection code""" + races = races.replace(' ', '') + race_list = races.split(',') + + valid_enemies = {} + for race in race_list: + valid_enemies[race.lower()[0]] = True + + complete = file_block + '_race_checked' + + result = (f'enemyowns_jump(command center, {complete if valid_enemies.get("t") else skip_block})\n' + f'enemyowns_jump(hatchery, {complete if valid_enemies.get("z") else skip_block})\n' + f'enemyowns_jump(nexus, {complete if valid_enemies.get("p") else skip_block})\n' + f'--{complete}--\n') + + return result + + def debug(self, message): + """Generate debug message code""" + global block_count + block_count += 1 + block_name = f'd_{block_count}' + + return (f'\ndebug({block_name}, {message})\n' + f'--{block_name}--\n') From 57dcb1c8625000028bca3094ec798315e2a763c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 23:06:08 +0000 Subject: [PATCH 3/5] Remove JavaScript files and update documentation Co-authored-by: jncraton <103612+jncraton@users.noreply.github.com> --- CONTRIBUTING.md | 3 +- tools/abbrevs.js | 32 --- tools/ai.js | 58 ------ tools/buildConverter.js | 53 ----- tools/build_ai.js | 23 --- tools/build_launcher_package.js | 17 -- tools/macros.js | 338 -------------------------------- tools/race.js | 245 ----------------------- 8 files changed, 1 insertion(+), 768 deletions(-) delete mode 100644 tools/abbrevs.js delete mode 100644 tools/ai.js delete mode 100644 tools/buildConverter.js delete mode 100644 tools/build_ai.js delete mode 100644 tools/build_launcher_package.js delete mode 100644 tools/macros.js delete mode 100644 tools/race.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0da7166..d6cee7c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,9 +10,8 @@ Linux is strongly recommended for development. Everything should work on other p ### Prerequisites: -- [node.js](http://nodejs.org/download/) - [GNU Make](https://www.gnu.org/software/make/) -- [Python](http://www.python.org/download/) 2.7 32 bit. This particular version is required to be able to interact with storm.dll using ctypes. +- [Python](http://www.python.org/download/) 2.7 32 bit for PyAI compiler (interacting with storm.dll) and Python 3.6+ for build scripts ### Build diff --git a/tools/abbrevs.js b/tools/abbrevs.js deleted file mode 100644 index 6bac859..0000000 --- a/tools/abbrevs.js +++ /dev/null @@ -1,32 +0,0 @@ -var abbrevs = require('./abbrevs.json'); -var abbrevsReplacements = []; - -Object.keys(abbrevs).forEach(function(key) { - for (var i = 0; i < abbrevs[key].length; i += 1) { - abbrevsReplacements.push({ - 'short': abbrevs[key][i], - 'long': key, - }); - } -}); - -var expand = function expand(abbrev) { - abbrevsReplacements.forEach(function(a) { - abbrev = abbrev.replace(RegExp('^' + a.short + '$', 'i'), a.long); - }); - - return abbrev; -} - -var parse = function parse(content) { - content = content.replace(/([,\(] *)([A-Za-z ']*?)([,\)])/g, function(original, prefix, arg, postfix) { - arg = expand(arg); - - return prefix + arg + postfix; - }); - - return content; -} - -exports.parse = parse; -exports.expand = expand; \ No newline at end of file diff --git a/tools/ai.js b/tools/ai.js deleted file mode 100644 index eb1c085..0000000 --- a/tools/ai.js +++ /dev/null @@ -1,58 +0,0 @@ -var Race = require('./race'); -var config = require('./config.json'); - -function AI (race_name) { - var race = new Race(race_name); - var src = ""; - - this.build = function() { - // Default boilerplate - switch (race_name) { - case 'terran': - src = 'TMCx(1342, 101, aiscript):\n'; - break; - case 'protoss': - src = 'PMCx(1343, 101, aiscript):\n'; - break; - case 'zerg': - src = 'ZMCx(1344, 101, aiscript):\n'; - break; - } - - src += race.loadContents('main'); - - src = src.replace(/^(?!(TMCx|ZMCx|PMCx|\-\-|#|debug|random)).+$/mg, function(original) { - return 'wait(1)\n' + - original + '\n'; - }); - - if (config.verbosity >= 10 || config[race_name].verbosity >= 10) { - debug_count = 0 - - src = src.replace(/^(?!(TMCx|ZMCx|PMCx|\-\-|#|debug|wait)).+$/mg, function(original) { - function getCode(num) { - var valid_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_' - - var tens = Math.floor(num / valid_chars.length) - var remainder = num - (tens * valid_chars.length) - - tens -= 1 - - return '' + (tens >= 0 ? valid_chars[tens] : '') + valid_chars[remainder] - } - - debug_count += 1; - var block_name = 'd10_' + debug_count; - return 'debug(' + block_name + ', ' + getCode(debug_count) + ')\n' + - '--' + block_name + '--\n' + - original + '\n'; - }); - } - - return src; - } - - return this; -} - -module.exports = AI; \ No newline at end of file diff --git a/tools/buildConverter.js b/tools/buildConverter.js deleted file mode 100644 index 7995134..0000000 --- a/tools/buildConverter.js +++ /dev/null @@ -1,53 +0,0 @@ -var units = require('./units.json'); -var abbrevs = require('./abbrevs.js'); - -function isUnit(unit) { - return units.some(function (u) { - return u.unit === unit; - }); -} - -function getUnitInfo(unit) { - return units.filter(function (u) { - return u.unit === unit; - })[0]; -} - -var parse = function parse(content) { - var owned = {}; - var supplyFromUnits = 0; - - content = content.replace(/^(\d+) (.*)$/mg, function(original, supply, unit) { - var ret = ""; - - unit = abbrevs.expand(unit); - - if(!owned[unit]) { - owned[unit] = 0; - } - owned[unit] += 1; - - var waitForWorker = supply - supplyFromUnits; - - ret += 'build(' + waitForWorker + ', Peon, 80)\n' + - 'wait_buildstart(' + waitForWorker + ', Peon)\n'; - - if (isUnit(unit)) { - supplyFromUnits += parseInt(getUnitInfo(unit).supply); - - ret += 'train(' + owned[unit] + ', ' + unit + ')\n'; - } else if (unit === 'Expand' || unit === 'expand') { - ret += 'expand(1, gen_expansions_expansion)\n'; - } else { - ret += 'build(' + owned[unit] + ', ' + unit + ', 80)\n' + - 'wait_buildstart(' + owned[unit] + ', ' + unit + ')\n'; - } - - return ret; - }); - - return content; -}; - -exports.parse = parse; - diff --git a/tools/build_ai.js b/tools/build_ai.js deleted file mode 100644 index de8f5c7..0000000 --- a/tools/build_ai.js +++ /dev/null @@ -1,23 +0,0 @@ -var fs = require('fs'); - -exports.build = function(input, output) { - var AI = require('./ai'); - var exec = require('child_process').exec; - - var src = AI(input).build(); - - exec('git rev-parse HEAD', function (error, commit) { - commit = commit.replace('\n', '').substring(0,6); - src = src.replace(/{commit}/g, commit); - fs.writeFileSync(output, src); - }); - - src = src.replace(/{now}/g, new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')); -} - -if(process.argv[3]) { - exports.build(process.argv[2], process.argv[3]) -} else { - console.log('Usage: node ' + process.argv[1] + ' race output'); -} - diff --git a/tools/build_launcher_package.js b/tools/build_launcher_package.js deleted file mode 100644 index 02c4cdd..0000000 --- a/tools/build_launcher_package.js +++ /dev/null @@ -1,17 +0,0 @@ -var fs = require('fs') -var exec = require('child_process').exec; -var build_ai = require('./build_ai') - -races = ['zerg','terran','protoss'] - -races.forEach(function (race) { - builds = fs.readdirSync('src/' + race + '/builds') - - builds.forEach(function (build) { - console.log(race, build) - config = JSON.parse(fs.readFileSync('tools/config_default.json')) - config[race].useBuild = build - fs.writeFileSync('tools/config.json', JSON.stringify(config)) - build_ai.build(race.replace('.pyai'), 'dist/BWAILauncher_package/' + race + '/' + build.replace('pyai', 'txt')) - }) -}) diff --git a/tools/macros.js b/tools/macros.js deleted file mode 100644 index 8c7ba3c..0000000 --- a/tools/macros.js +++ /dev/null @@ -1,338 +0,0 @@ -var config = require('./config.json'); - -var block_counter = 0; - -var parse = function parse(content) { - function expandEnemyOwns(units, block) { - var out = ""; - - for(var i = 0; i < units.length; i += 1) { - out += 'enemyowns_jump(' + units[i] + ', ' + block + ')\n'; - } - - return out; - } - - function nextBlockName() { - block_counter++; - return 'gen_macro_' + block_counter; - } - - var lines = content.split(/[\r\n]+/); - - content = ''; - - var indent_level = 0; - var blocks = []; - lines.forEach(function (line) { - if (indent_level > 0) { - for(var i = indent_level; i > 0; i--) { - if (line.search(' ') > -1) { - line = line.replace(' ', '') - } else { - indent_level--; - content += blocks.pop() - } - } - } - - if (line.search('if ') > -1) { - line = line.replace(/if (.*)\((.*)\)/, function (match, function_name, params) { - if (function_name == 'owned') { - return 'if_owned(' + params + ')' - } else if (function_name == 'townpoint') { - return 'try_townpoint(' + params + ')' - } else { - return function_name + '_jump(' + params + ')' - } - }) - } - - if (line.indexOf(':') > -1) { - var start_block = nextBlockName() - var end_block = nextBlockName() - - if (line.search('multirun_loop') > -1) { - line = line.replace('multirun_loop', 'multirun()') - blocks.push('wait(75)\ngoto(' + - start_block + - ')\n' + - '--' + end_block + '--\n') - } else if (line.search('multirun') > -1) { - line = line.replace('multirun', 'multirun()') - blocks.push('stop()\n' + - '--' + end_block + '--\n') - } else if (line.search('loop') > -1) { - line = line.replace('loop', 'goto()') - blocks.push('wait(75)\n' + - 'goto(' + start_block + ')\n' + - '--' + end_block + '--\n') - } else { - blocks.push('--' + end_block + '--\n') - } - - indent_level++; - line = line.replace(':', '') - if (line.search(/[a-zA-Z0-9]\)/) > -1) { - line = line.replace(')', ', ' + start_block + ')') - } else { - line = line.replace(')', start_block + ')') - } - content += line + '\n' - content += 'goto(' + end_block + ')\n' - content += '--' + start_block + '--\n' - } else { - content += line + '\n' - } - }) - - while (blocks.length > 0) { - content += blocks.pop() - } - - content = content.replace(/attack_async\(\)/g, function(original) { - var start = nextBlockName(); - var escape = nextBlockName(); - - return 'multirun(' + start + ')\n' + - 'goto(' + escape + ')\n' + - '--' + start + '--\n' + - 'attack_do()\n' + - 'attack_clear()\n' + - 'stop()\n' + - '--' + escape + '--\n'; - }); - - content = content.replace(/attack_simple\(\)/g, function(original) { - return 'attack_do()\n' + - 'attack_prepare()\n' + - 'attack_clear()' - }); - - content = content.replace(/wait_resources\((.*),(.*)\)/g, function(original, minerals, gas) { - var loop_start = nextBlockName(); - var loop_escape = nextBlockName(); - - return '--' + loop_start + '--\n' + - 'resources_jump(' + minerals + ',' + gas + ',' + loop_escape + ')\n' + - 'wait(10)\n' + - 'goto(' + loop_start + ')\n' + - '--' + loop_escape + '--\n'; - }); - - content = content.replace(/wait_until\((.*)\)/g, function(original, time) { - var loop_start = nextBlockName(); - var loop_escape = nextBlockName(); - - return '--' + loop_start + '--\n' + - 'time_jump(' + time + ',' + loop_escape + ')\n' + - 'wait(300)\n' + - 'goto(' + loop_start + ')\n' + - '--' + loop_escape + '--\n'; - }); - - content = content.replace(/wait_owned\((.*)\)/g, function(original, unit) { - var loop_start = nextBlockName(); - var loop_escape = nextBlockName(); - - return '--' + loop_start + '--\n' + - 'if_owned(' + unit + ',' + loop_escape + ')\n' + - 'wait(300)\n' + - 'goto(' + loop_start + ')\n' + - '--' + loop_escape + '--\n'; - }); - - content = content.replace(/message\((.*)\)/g, function(original, message) { - var next_block = nextBlockName(); - - return 'debug(' + next_block + ',' + message + ')\n' + - '--' + next_block + '--\n'; - }); - - content = content.replace(/enemyownscloaked_jump\((.*)\)/g, function(original, block) { - var units = ['Zerg Lurker', 'Protoss Arbiter', 'Protoss Templar Archives', 'Protoss Dark Templar', 'Terran Ghost', 'Terran Wraith']; - return expandEnemyOwns(units, block); - }); - - content = content.replace(/rush_jump\((.*)\)/g, function(original, block) { - tooLateForBuildings = nextBlockName() - tooLateForUnits = nextBlockName() - - return 'time_jump(2, ' + tooLateForBuildings + ')\n' + // 2 is roughly 1:20 - expandEnemyOwns(['Zerg Spawning Pool', 'Terran Barracks', 'Protoss Gateway'], block) + '\n' + - '--' + tooLateForBuildings + '--\n' + - 'time_jump(4, ' + tooLateForUnits + ')\n' + // 4 is roughly 2:50 - expandEnemyOwns(['Zerg Zergling', 'Terran Marine', 'Protoss Zealot'], block) + '\n' + - '--' + tooLateForUnits + '--' - }); - - content = content.replace(/enemyownsairtech_jump\((.*)\)/g, function(original, block) { - var units = ['Terran Starport', 'Protoss Stargate', 'Zerg Spire']; - return expandEnemyOwns(units, block); - }); - - content = content.replace(/enemyownsair_jump\((.*)\)/g, function(original, block) { - var units = ['Terran Science Vessel', 'Terran Wraith', 'Terran Valkyrie', 'Terran Battlecruiser', 'Zerg Mutalisk', 'Zerg Scourge', 'Zerg Guardian', 'Zerg Devourer', 'Zerg Queen', 'Protoss Scout', 'Protoss Corsair', 'Protoss Carrier', 'Protoss Arbiter']; - return expandEnemyOwns(units, block); - }); - - content = content.replace(/build_start\((.*)\)/g, function(original, args) { - args = args.split(','); - var amount = args[0]; - var building = args[1]; - var priority = args[2] || '80'; - return 'build(' + amount + ', ' + building + ', ' + priority + ')\n' + - 'wait_buildstart(' + amount + ', ' + building + ')'; - }); - - content = content.replace(/build_finish\((.*)\)/g, function(original, args) { - args = args.split(','); - var amount = args[0]; - var building = args[1]; - var priority = args[2] || '80'; - return 'build(' + amount + ', ' + building + ', ' + priority + ')\n' + - 'wait_buildstart(' + amount + ', ' + building + ')\n' + - 'wait_build(' + amount + ', ' + building + ')'; - }); - - content = content.replace(/build_separately\((.*)\)/g, function(original, args) { - args = args.split(','); - var amount = args[0]; - var building = args[1]; - var priority = args[2] || '80'; - var ret = ''; - - for (var i = 1; i <= amount; i++) { - ret += 'build(' + i + ', ' + building + ', ' + priority + ')\n' + - 'wait_buildstart(' + i + ', ' + building + ')\n' + - 'wait_build(' + i + ', ' + building + ')\n'; - } - - return ret - }); - - content = content.replace(/attack_train\((.*)\)/g, function(original, args) { - args = args.split(','); - var amount = args[0]; - var unit = args[1]; - return 'do_morph(' + amount + ', ' + unit + ')\n' + - 'attack_add(' + amount + ', ' + unit + ')'; - }); - - content = content.replace(/defenseclear\(()\)/g, function(original) { - return 'defenseclear_gg()\n' + - 'defenseclear_ga()\n' + - 'defenseclear_ag()\n' + - 'defenseclear_aa()\n'; - }); - - content = content.replace(/defense_ground\((.*)\)/g, function(original, unit) { - var do_build = nextBlockName() - var skip_build = nextBlockName() - - return 'defenseuse_gg(1, ' + unit + ')\n' + - 'defenseuse_ga(1, ' + unit + ')\n' + - 'time_jump(6, ' + do_build + ')\n' + - 'goto(' + skip_build + ')\n' + - '--' + do_build + '--\n' + - 'defensebuild_gg(1, ' + unit + ')\n' + - 'defensebuild_ga(1, ' + unit + ')\n' + - '--' + skip_build + '--\n' - }); - - content = content.replace(/defense_air\((.*)\)/g, function(original, unit) { - var do_build = nextBlockName() - var skip_build = nextBlockName() - - return 'defenseuse_ag(1, ' + unit + ')\n' + - 'defenseuse_aa(1, ' + unit + ')\n' + - 'time_jump(6, ' + do_build + ')\n' + - 'goto(' + skip_build + ')\n' + - '--' + do_build + '--\n' + - 'defensebuild_ag(1, ' + unit + ')\n' + - 'defensebuild_aa(1, ' + unit + ')\n' + - '--' + skip_build + '--\n' - }); - - content = content.replace(/defense_ground_train\((.*)\)/g, function(original, unit) { - var do_build = nextBlockName() - var skip_build = nextBlockName() - - return 'defenseuse_gg(1, ' + unit + ')\n' + - 'defenseuse_ga(1, ' + unit + ')\n' + - 'defensebuild_gg(1, ' + unit + ')\n' + - 'defensebuild_ga(1, ' + unit + ')\n'; - }); - - content = content.replace(/defense_air_train\((.*)\)/g, function(original, unit) { - var do_build = nextBlockName() - var skip_build = nextBlockName() - - return 'defenseuse_ag(1, ' + unit + ')\n' + - 'defenseuse_aa(1, ' + unit + ')\n' + - 'defensebuild_ag(1, ' + unit + ')\n' + - 'defensebuild_aa(1, ' + unit + ')\n' - }); - - content = content.replace(/\(difficulty/g, function(original) { - return '(' + config.difficulty - }); - - content = content.replace(/create_bonus_workers\(()\)/g, function(original) { - ret = '' - - for (var i = 0; i < config.bonusWorkers; i++) { - ret += 'create_unit(Peon, 2000, 2000)\n' - } - - return ret - }); - - content = content.replace(/attack_multiple\((.*?), (.*)\)/g, function(original, mul, params) { - var ret = '' - - units = params.split(',') - - units = units.map(function (unit) { - unit = unit.replace(/^ /g, '') - - unit = unit.split(' ') - - return { - quantity: unit.shift(), - name: unit.join(' ') - } - }) - - var more_units_prob = Math.floor(256/mul) - - var done_block = nextBlockName() - - for (var i = 1; i <= mul; i++) { - var more_units = nextBlockName(); - - units.forEach(function (unit) { - ret += 'train(' + unit.quantity * i + ', ' + unit.name + ')\n' - }) - - ret += 'random_jump(' + more_units_prob + ',' + more_units + ')\n' - - units.forEach(function (unit) { - ret += 'attack_add(' + unit.quantity * i + ', ' + unit.name + ')\n' - }) - - ret += 'goto(' + done_block + ')\n' - - ret += '--' + more_units + '--\n' - - } - - ret += '--' + done_block + '--\n' - - return ret - }); - - return content; -} - -exports.parse = parse; diff --git a/tools/race.js b/tools/race.js deleted file mode 100644 index 3fa4925..0000000 --- a/tools/race.js +++ /dev/null @@ -1,245 +0,0 @@ -var fs = require('fs'); - -var debug_count = 0; -var block_count = 0; -var nonce = 0 - -var config = require('./config.json'); - -var abbrevs = require('./abbrevs.js'); -var macros = require('./macros.js'); -var buildConverter = require('./buildConverter.js'); - -function Race(name) { - function getFullPath(filename) { - filename = filename.replace('.pyai', '') - filename += '.pyai' - - if(fs.existsSync(config.srcPath + name + '/' + filename)) { - filename = config.srcPath + name + '/' + filename; - } else if(fs.existsSync(config.srcPath + name + '/managers/' + filename)) { - filename = config.srcPath + name + '/managers/' + filename; - } else if(fs.existsSync(config.srcPath + 'managers/' + filename)) { - filename = config.srcPath + 'managers/' + filename; - } else if(fs.existsSync(config.srcPath + name + '/expansions/' + filename)) { - filename = config.srcPath + name + '/expansions/' + filename; - } else if(fs.existsSync(config.srcPath + 'expansions/' + filename)) { - filename = config.srcPath + 'expansions/' + filename; - } else { - filename = config.srcPath + filename; - } - - return filename; - } - - function loadContents(filename, skip_block_header) { - filename = getFullPath(filename) - - return parseTemplate(filename, skip_block_header) + "\n"; - } - - this.loadContents = loadContents; - - function parseTemplate(filename, skip_block_header) { - var comment = "\n#" + filename + '\n'; - var block; - var file_block; - - if(!skip_block_header) { - file_block = getFileBlock(filename); - } else { - file_block = getFileBlock(filename) + (nonce++); - } - - block = (filename.indexOf('header') > -1 ? '' : '--' + file_block + '--\n'); - - var content = fs.readFileSync(filename, 'utf-8'); - - content = content.replace(/repeat\(\)/g, 'wait(75)\ngoto(' + file_block + ')'); - - content = content.replace(/include\((.*)\)/g, function(command, filename) { - if (filename == 'freemoney' && config.difficulty == 0) { return "" } - return loadContents(filename, true); - }); - - content = content.replace(/include_block\((.*)\)/g, function(command, filename) { - return loadContents(filename) + "stop()\n"; - }); - - content = content.replace(/expand\(([\d, ]+)(.*)\)/g, function(command, num, block) { - if (block.indexOf('gen_expansions') > -1) { - return 'expand(' + num + block + ')' - } else { - return 'expand(' + num + 'gen_expansions_' + block + ')' - } - }); - - content = content.replace(/panic\((.*)\)/g, function(command, block) { - return 'panic(' + 'gen_expansions_' + block + ')' - }); - - content = content.replace(/multirun_file\((.*)\)/g, function(command, relative_filename) { - block_count += 1; - - var done_block = block + "_done_" + block_count - - var block = getFileBlock(getFullPath(relative_filename)) - - return "multirun("+ block + ")\n" + - "goto(" + done_block + ")\n" + - loadContents(relative_filename) + "\n" + - "stop()\n" + - "--" + done_block + "--"; - }); - - function chooseFromDir(dir) { - var ret = ""; - function append(text) { - ret += text + '\n'; - } - - append('--gen_' + dir + '--') - append('--gen_jump_loop' + dir + '--') - - var files = []; - - try { - var files = fs.readdirSync(config.srcPath + name + '/' + dir); - } catch (e) { - console.log('Missing directory ' + config.srcPath + name + '/' + dir); - } - - getBuildContents = function (build) { - build = build.replace('.pyai','') - contents = loadContents(name + '/' + dir + '/' + build + '.pyai'); - return contents - } - - if (dir == 'builds' && config[name].useBuild) { - append(getBuildContents(config[name].useBuild)); - } else { - if (files.length) { - for(var i = 0; i < files.length; i += 1) { - if(files[i][0] == '_') { - append("goto(gen_" + dir + "_" + files[i].replace('.pyai','').replace(/ /g,'_').replace(/^_/, '') + ")"); - } else { - append("random_jump(2, " + "gen_" + dir + "_" + files[i].replace('.pyai','').replace(/ /g,'_').replace(/^_/, '') + ")"); - } - } - - append('goto(gen_jump_loop' + dir + ')'); - - for(var i = 0; i < files.length; i += 1) { - append(getBuildContents(files[i])); - append('goto(' + 'gen_end_' + dir + ')'); - } - } - } - - append('--gen_end_' + dir + '--'); - - return ret; - } - - content = content.replace(/choose_from_dir\((.*)\)/g, function(command, dir) { - return chooseFromDir(dir); - }); - - function race_skip(races, skip_block) { - races = races.replace(/ /g, ''); - races = races.split(','); - - valid_enemies = {}; - - for(var i = 0; i < races.length; i +=1) { - valid_enemies[races[i].toLowerCase()[0]] = true; - } - - var complete = file_block + '_race_checked'; - - return('enemyowns_jump(command center, ' + (valid_enemies.t ? complete : skip_block) + ')\n' + - 'enemyowns_jump(hatchery, ' + (valid_enemies.z ? complete : skip_block) + ')\n' + - 'enemyowns_jump(nexus, ' + (valid_enemies.p ? complete : skip_block) + ')\n' + - '--' + complete + '--\n'); - } - - content = content.replace(/build_weight\((.*)\)/g, function(original, weight) { - var skip_chance = parseInt((1 - weight) * 255); - - return 'random_jump(' + skip_chance + ', gen_builds)'; - }); - - content = content.replace(/style_weight\((.*)\)/g, function(original, weight) { - var skip_chance = parseInt((1 - weight) * 255); - - return 'random_jump(' + skip_chance + ', gen_lategame)'; - }); - - content = content.replace(/use_build_vs\((.*)\)/g, function(original, races) { - var message = ''; - - if (config.verbosity >= 5) { - var buildName = block.replace(/[\-\n ]/g, '').replace('gen_builds_', '').replace(/_/g, ' '); - message = debug('Using ' + buildName + ' build'); - } - - return race_skip(races, 'gen_builds') + message; - }); - - content = content.replace(/use_attack_vs\((.*)\)/g, function(original, races) { - var message = ''; - - if (config.verbosity >= 5) { - var styleName = block.replace(/[\-\n ]/g, '').replace('gen_attack_', '').replace(/_/g, ' '); - message = debug('Using ' + styleName + ' attack'); - } - - return 'wait(50)\n' + - race_skip(races, 'gen_attacks') + message; - }); - - content = buildConverter.parse(content); - content = macros.parse(content); - content = abbrevs.parse(content); - - if (name === 'terran') { - content = content.replace(/Town Hall/g, "Terran Command Center"); - content = content.replace(/Peon/g, "Terran SCV"); - content = content.replace(/Gas/g, "Terran Refinery"); - } - - if (name === 'zerg') { - content = content.replace(/Town Hall/g, "Zerg Hatchery"); - content = content.replace(/Peon/g, "Zerg Drone"); - content = content.replace(/Gas/g, "Zerg Extractor"); - } - - if (name === 'protoss') { - content = content.replace(/Town Hall/g, "Protoss Nexus"); - content = content.replace(/Peon/g, "Protoss Probe"); - content = content.replace(/Gas/g, "Protoss Assimilator"); - } - - function debug(message) { - block_count += 1; - var block_name = 'd_' + block_count; - - return ('\ndebug(' + block_name + ', ' + message + ')\n' + - '--' + block_name + '--\n'); - } - - return comment + block + content; - } - function getFileBlock(filename) { - var block = 'gen_' + filename.replace(config.srcPath, ''); - block = block.replace(/[-_ \/]/g, '_'); - block = block.replace('.pyai', '') - block = block.replace(name, ''); - block = block.replace(/__/g, '_'); - return block; - } - - return this; -} - -module.exports = Race; \ No newline at end of file From dab778ac3091790b5f090570e7c17a75a05a1843 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 00:59:29 +0000 Subject: [PATCH 4/5] Co-authored-by: jncraton <103612+jncraton@users.noreply.github.com> --- tools/abbrevs.js | 32 +++++ tools/ai.js | 58 ++++++++ tools/build_ai.js | 23 ++++ tools/config.json | 18 +++ tools/macros.js | 338 ++++++++++++++++++++++++++++++++++++++++++++++ tools/race.js | 245 +++++++++++++++++++++++++++++++++ 6 files changed, 714 insertions(+) create mode 100644 tools/abbrevs.js create mode 100644 tools/ai.js create mode 100644 tools/build_ai.js create mode 100644 tools/config.json create mode 100644 tools/macros.js create mode 100644 tools/race.js diff --git a/tools/abbrevs.js b/tools/abbrevs.js new file mode 100644 index 0000000..6bac859 --- /dev/null +++ b/tools/abbrevs.js @@ -0,0 +1,32 @@ +var abbrevs = require('./abbrevs.json'); +var abbrevsReplacements = []; + +Object.keys(abbrevs).forEach(function(key) { + for (var i = 0; i < abbrevs[key].length; i += 1) { + abbrevsReplacements.push({ + 'short': abbrevs[key][i], + 'long': key, + }); + } +}); + +var expand = function expand(abbrev) { + abbrevsReplacements.forEach(function(a) { + abbrev = abbrev.replace(RegExp('^' + a.short + '$', 'i'), a.long); + }); + + return abbrev; +} + +var parse = function parse(content) { + content = content.replace(/([,\(] *)([A-Za-z ']*?)([,\)])/g, function(original, prefix, arg, postfix) { + arg = expand(arg); + + return prefix + arg + postfix; + }); + + return content; +} + +exports.parse = parse; +exports.expand = expand; \ No newline at end of file diff --git a/tools/ai.js b/tools/ai.js new file mode 100644 index 0000000..eb1c085 --- /dev/null +++ b/tools/ai.js @@ -0,0 +1,58 @@ +var Race = require('./race'); +var config = require('./config.json'); + +function AI (race_name) { + var race = new Race(race_name); + var src = ""; + + this.build = function() { + // Default boilerplate + switch (race_name) { + case 'terran': + src = 'TMCx(1342, 101, aiscript):\n'; + break; + case 'protoss': + src = 'PMCx(1343, 101, aiscript):\n'; + break; + case 'zerg': + src = 'ZMCx(1344, 101, aiscript):\n'; + break; + } + + src += race.loadContents('main'); + + src = src.replace(/^(?!(TMCx|ZMCx|PMCx|\-\-|#|debug|random)).+$/mg, function(original) { + return 'wait(1)\n' + + original + '\n'; + }); + + if (config.verbosity >= 10 || config[race_name].verbosity >= 10) { + debug_count = 0 + + src = src.replace(/^(?!(TMCx|ZMCx|PMCx|\-\-|#|debug|wait)).+$/mg, function(original) { + function getCode(num) { + var valid_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_' + + var tens = Math.floor(num / valid_chars.length) + var remainder = num - (tens * valid_chars.length) + + tens -= 1 + + return '' + (tens >= 0 ? valid_chars[tens] : '') + valid_chars[remainder] + } + + debug_count += 1; + var block_name = 'd10_' + debug_count; + return 'debug(' + block_name + ', ' + getCode(debug_count) + ')\n' + + '--' + block_name + '--\n' + + original + '\n'; + }); + } + + return src; + } + + return this; +} + +module.exports = AI; \ No newline at end of file diff --git a/tools/build_ai.js b/tools/build_ai.js new file mode 100644 index 0000000..de8f5c7 --- /dev/null +++ b/tools/build_ai.js @@ -0,0 +1,23 @@ +var fs = require('fs'); + +exports.build = function(input, output) { + var AI = require('./ai'); + var exec = require('child_process').exec; + + var src = AI(input).build(); + + exec('git rev-parse HEAD', function (error, commit) { + commit = commit.replace('\n', '').substring(0,6); + src = src.replace(/{commit}/g, commit); + fs.writeFileSync(output, src); + }); + + src = src.replace(/{now}/g, new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '')); +} + +if(process.argv[3]) { + exports.build(process.argv[2], process.argv[3]) +} else { + console.log('Usage: node ' + process.argv[1] + ' race output'); +} + diff --git a/tools/config.json b/tools/config.json new file mode 100644 index 0000000..76e06ea --- /dev/null +++ b/tools/config.json @@ -0,0 +1,18 @@ +{ + "srcPath": "src/", + "verbosity": 0, + "bonusWorkers": 0, + "difficulty": 0, + "terran": { + "verbosity": 0, + "useBuild": "" + }, + "zerg": { + "verbosity": 0, + "useBuild": "" + }, + "protoss": { + "verbosity": 0, + "useBuild": "" + } +} \ No newline at end of file diff --git a/tools/macros.js b/tools/macros.js new file mode 100644 index 0000000..8c7ba3c --- /dev/null +++ b/tools/macros.js @@ -0,0 +1,338 @@ +var config = require('./config.json'); + +var block_counter = 0; + +var parse = function parse(content) { + function expandEnemyOwns(units, block) { + var out = ""; + + for(var i = 0; i < units.length; i += 1) { + out += 'enemyowns_jump(' + units[i] + ', ' + block + ')\n'; + } + + return out; + } + + function nextBlockName() { + block_counter++; + return 'gen_macro_' + block_counter; + } + + var lines = content.split(/[\r\n]+/); + + content = ''; + + var indent_level = 0; + var blocks = []; + lines.forEach(function (line) { + if (indent_level > 0) { + for(var i = indent_level; i > 0; i--) { + if (line.search(' ') > -1) { + line = line.replace(' ', '') + } else { + indent_level--; + content += blocks.pop() + } + } + } + + if (line.search('if ') > -1) { + line = line.replace(/if (.*)\((.*)\)/, function (match, function_name, params) { + if (function_name == 'owned') { + return 'if_owned(' + params + ')' + } else if (function_name == 'townpoint') { + return 'try_townpoint(' + params + ')' + } else { + return function_name + '_jump(' + params + ')' + } + }) + } + + if (line.indexOf(':') > -1) { + var start_block = nextBlockName() + var end_block = nextBlockName() + + if (line.search('multirun_loop') > -1) { + line = line.replace('multirun_loop', 'multirun()') + blocks.push('wait(75)\ngoto(' + + start_block + + ')\n' + + '--' + end_block + '--\n') + } else if (line.search('multirun') > -1) { + line = line.replace('multirun', 'multirun()') + blocks.push('stop()\n' + + '--' + end_block + '--\n') + } else if (line.search('loop') > -1) { + line = line.replace('loop', 'goto()') + blocks.push('wait(75)\n' + + 'goto(' + start_block + ')\n' + + '--' + end_block + '--\n') + } else { + blocks.push('--' + end_block + '--\n') + } + + indent_level++; + line = line.replace(':', '') + if (line.search(/[a-zA-Z0-9]\)/) > -1) { + line = line.replace(')', ', ' + start_block + ')') + } else { + line = line.replace(')', start_block + ')') + } + content += line + '\n' + content += 'goto(' + end_block + ')\n' + content += '--' + start_block + '--\n' + } else { + content += line + '\n' + } + }) + + while (blocks.length > 0) { + content += blocks.pop() + } + + content = content.replace(/attack_async\(\)/g, function(original) { + var start = nextBlockName(); + var escape = nextBlockName(); + + return 'multirun(' + start + ')\n' + + 'goto(' + escape + ')\n' + + '--' + start + '--\n' + + 'attack_do()\n' + + 'attack_clear()\n' + + 'stop()\n' + + '--' + escape + '--\n'; + }); + + content = content.replace(/attack_simple\(\)/g, function(original) { + return 'attack_do()\n' + + 'attack_prepare()\n' + + 'attack_clear()' + }); + + content = content.replace(/wait_resources\((.*),(.*)\)/g, function(original, minerals, gas) { + var loop_start = nextBlockName(); + var loop_escape = nextBlockName(); + + return '--' + loop_start + '--\n' + + 'resources_jump(' + minerals + ',' + gas + ',' + loop_escape + ')\n' + + 'wait(10)\n' + + 'goto(' + loop_start + ')\n' + + '--' + loop_escape + '--\n'; + }); + + content = content.replace(/wait_until\((.*)\)/g, function(original, time) { + var loop_start = nextBlockName(); + var loop_escape = nextBlockName(); + + return '--' + loop_start + '--\n' + + 'time_jump(' + time + ',' + loop_escape + ')\n' + + 'wait(300)\n' + + 'goto(' + loop_start + ')\n' + + '--' + loop_escape + '--\n'; + }); + + content = content.replace(/wait_owned\((.*)\)/g, function(original, unit) { + var loop_start = nextBlockName(); + var loop_escape = nextBlockName(); + + return '--' + loop_start + '--\n' + + 'if_owned(' + unit + ',' + loop_escape + ')\n' + + 'wait(300)\n' + + 'goto(' + loop_start + ')\n' + + '--' + loop_escape + '--\n'; + }); + + content = content.replace(/message\((.*)\)/g, function(original, message) { + var next_block = nextBlockName(); + + return 'debug(' + next_block + ',' + message + ')\n' + + '--' + next_block + '--\n'; + }); + + content = content.replace(/enemyownscloaked_jump\((.*)\)/g, function(original, block) { + var units = ['Zerg Lurker', 'Protoss Arbiter', 'Protoss Templar Archives', 'Protoss Dark Templar', 'Terran Ghost', 'Terran Wraith']; + return expandEnemyOwns(units, block); + }); + + content = content.replace(/rush_jump\((.*)\)/g, function(original, block) { + tooLateForBuildings = nextBlockName() + tooLateForUnits = nextBlockName() + + return 'time_jump(2, ' + tooLateForBuildings + ')\n' + // 2 is roughly 1:20 + expandEnemyOwns(['Zerg Spawning Pool', 'Terran Barracks', 'Protoss Gateway'], block) + '\n' + + '--' + tooLateForBuildings + '--\n' + + 'time_jump(4, ' + tooLateForUnits + ')\n' + // 4 is roughly 2:50 + expandEnemyOwns(['Zerg Zergling', 'Terran Marine', 'Protoss Zealot'], block) + '\n' + + '--' + tooLateForUnits + '--' + }); + + content = content.replace(/enemyownsairtech_jump\((.*)\)/g, function(original, block) { + var units = ['Terran Starport', 'Protoss Stargate', 'Zerg Spire']; + return expandEnemyOwns(units, block); + }); + + content = content.replace(/enemyownsair_jump\((.*)\)/g, function(original, block) { + var units = ['Terran Science Vessel', 'Terran Wraith', 'Terran Valkyrie', 'Terran Battlecruiser', 'Zerg Mutalisk', 'Zerg Scourge', 'Zerg Guardian', 'Zerg Devourer', 'Zerg Queen', 'Protoss Scout', 'Protoss Corsair', 'Protoss Carrier', 'Protoss Arbiter']; + return expandEnemyOwns(units, block); + }); + + content = content.replace(/build_start\((.*)\)/g, function(original, args) { + args = args.split(','); + var amount = args[0]; + var building = args[1]; + var priority = args[2] || '80'; + return 'build(' + amount + ', ' + building + ', ' + priority + ')\n' + + 'wait_buildstart(' + amount + ', ' + building + ')'; + }); + + content = content.replace(/build_finish\((.*)\)/g, function(original, args) { + args = args.split(','); + var amount = args[0]; + var building = args[1]; + var priority = args[2] || '80'; + return 'build(' + amount + ', ' + building + ', ' + priority + ')\n' + + 'wait_buildstart(' + amount + ', ' + building + ')\n' + + 'wait_build(' + amount + ', ' + building + ')'; + }); + + content = content.replace(/build_separately\((.*)\)/g, function(original, args) { + args = args.split(','); + var amount = args[0]; + var building = args[1]; + var priority = args[2] || '80'; + var ret = ''; + + for (var i = 1; i <= amount; i++) { + ret += 'build(' + i + ', ' + building + ', ' + priority + ')\n' + + 'wait_buildstart(' + i + ', ' + building + ')\n' + + 'wait_build(' + i + ', ' + building + ')\n'; + } + + return ret + }); + + content = content.replace(/attack_train\((.*)\)/g, function(original, args) { + args = args.split(','); + var amount = args[0]; + var unit = args[1]; + return 'do_morph(' + amount + ', ' + unit + ')\n' + + 'attack_add(' + amount + ', ' + unit + ')'; + }); + + content = content.replace(/defenseclear\(()\)/g, function(original) { + return 'defenseclear_gg()\n' + + 'defenseclear_ga()\n' + + 'defenseclear_ag()\n' + + 'defenseclear_aa()\n'; + }); + + content = content.replace(/defense_ground\((.*)\)/g, function(original, unit) { + var do_build = nextBlockName() + var skip_build = nextBlockName() + + return 'defenseuse_gg(1, ' + unit + ')\n' + + 'defenseuse_ga(1, ' + unit + ')\n' + + 'time_jump(6, ' + do_build + ')\n' + + 'goto(' + skip_build + ')\n' + + '--' + do_build + '--\n' + + 'defensebuild_gg(1, ' + unit + ')\n' + + 'defensebuild_ga(1, ' + unit + ')\n' + + '--' + skip_build + '--\n' + }); + + content = content.replace(/defense_air\((.*)\)/g, function(original, unit) { + var do_build = nextBlockName() + var skip_build = nextBlockName() + + return 'defenseuse_ag(1, ' + unit + ')\n' + + 'defenseuse_aa(1, ' + unit + ')\n' + + 'time_jump(6, ' + do_build + ')\n' + + 'goto(' + skip_build + ')\n' + + '--' + do_build + '--\n' + + 'defensebuild_ag(1, ' + unit + ')\n' + + 'defensebuild_aa(1, ' + unit + ')\n' + + '--' + skip_build + '--\n' + }); + + content = content.replace(/defense_ground_train\((.*)\)/g, function(original, unit) { + var do_build = nextBlockName() + var skip_build = nextBlockName() + + return 'defenseuse_gg(1, ' + unit + ')\n' + + 'defenseuse_ga(1, ' + unit + ')\n' + + 'defensebuild_gg(1, ' + unit + ')\n' + + 'defensebuild_ga(1, ' + unit + ')\n'; + }); + + content = content.replace(/defense_air_train\((.*)\)/g, function(original, unit) { + var do_build = nextBlockName() + var skip_build = nextBlockName() + + return 'defenseuse_ag(1, ' + unit + ')\n' + + 'defenseuse_aa(1, ' + unit + ')\n' + + 'defensebuild_ag(1, ' + unit + ')\n' + + 'defensebuild_aa(1, ' + unit + ')\n' + }); + + content = content.replace(/\(difficulty/g, function(original) { + return '(' + config.difficulty + }); + + content = content.replace(/create_bonus_workers\(()\)/g, function(original) { + ret = '' + + for (var i = 0; i < config.bonusWorkers; i++) { + ret += 'create_unit(Peon, 2000, 2000)\n' + } + + return ret + }); + + content = content.replace(/attack_multiple\((.*?), (.*)\)/g, function(original, mul, params) { + var ret = '' + + units = params.split(',') + + units = units.map(function (unit) { + unit = unit.replace(/^ /g, '') + + unit = unit.split(' ') + + return { + quantity: unit.shift(), + name: unit.join(' ') + } + }) + + var more_units_prob = Math.floor(256/mul) + + var done_block = nextBlockName() + + for (var i = 1; i <= mul; i++) { + var more_units = nextBlockName(); + + units.forEach(function (unit) { + ret += 'train(' + unit.quantity * i + ', ' + unit.name + ')\n' + }) + + ret += 'random_jump(' + more_units_prob + ',' + more_units + ')\n' + + units.forEach(function (unit) { + ret += 'attack_add(' + unit.quantity * i + ', ' + unit.name + ')\n' + }) + + ret += 'goto(' + done_block + ')\n' + + ret += '--' + more_units + '--\n' + + } + + ret += '--' + done_block + '--\n' + + return ret + }); + + return content; +} + +exports.parse = parse; diff --git a/tools/race.js b/tools/race.js new file mode 100644 index 0000000..3fa4925 --- /dev/null +++ b/tools/race.js @@ -0,0 +1,245 @@ +var fs = require('fs'); + +var debug_count = 0; +var block_count = 0; +var nonce = 0 + +var config = require('./config.json'); + +var abbrevs = require('./abbrevs.js'); +var macros = require('./macros.js'); +var buildConverter = require('./buildConverter.js'); + +function Race(name) { + function getFullPath(filename) { + filename = filename.replace('.pyai', '') + filename += '.pyai' + + if(fs.existsSync(config.srcPath + name + '/' + filename)) { + filename = config.srcPath + name + '/' + filename; + } else if(fs.existsSync(config.srcPath + name + '/managers/' + filename)) { + filename = config.srcPath + name + '/managers/' + filename; + } else if(fs.existsSync(config.srcPath + 'managers/' + filename)) { + filename = config.srcPath + 'managers/' + filename; + } else if(fs.existsSync(config.srcPath + name + '/expansions/' + filename)) { + filename = config.srcPath + name + '/expansions/' + filename; + } else if(fs.existsSync(config.srcPath + 'expansions/' + filename)) { + filename = config.srcPath + 'expansions/' + filename; + } else { + filename = config.srcPath + filename; + } + + return filename; + } + + function loadContents(filename, skip_block_header) { + filename = getFullPath(filename) + + return parseTemplate(filename, skip_block_header) + "\n"; + } + + this.loadContents = loadContents; + + function parseTemplate(filename, skip_block_header) { + var comment = "\n#" + filename + '\n'; + var block; + var file_block; + + if(!skip_block_header) { + file_block = getFileBlock(filename); + } else { + file_block = getFileBlock(filename) + (nonce++); + } + + block = (filename.indexOf('header') > -1 ? '' : '--' + file_block + '--\n'); + + var content = fs.readFileSync(filename, 'utf-8'); + + content = content.replace(/repeat\(\)/g, 'wait(75)\ngoto(' + file_block + ')'); + + content = content.replace(/include\((.*)\)/g, function(command, filename) { + if (filename == 'freemoney' && config.difficulty == 0) { return "" } + return loadContents(filename, true); + }); + + content = content.replace(/include_block\((.*)\)/g, function(command, filename) { + return loadContents(filename) + "stop()\n"; + }); + + content = content.replace(/expand\(([\d, ]+)(.*)\)/g, function(command, num, block) { + if (block.indexOf('gen_expansions') > -1) { + return 'expand(' + num + block + ')' + } else { + return 'expand(' + num + 'gen_expansions_' + block + ')' + } + }); + + content = content.replace(/panic\((.*)\)/g, function(command, block) { + return 'panic(' + 'gen_expansions_' + block + ')' + }); + + content = content.replace(/multirun_file\((.*)\)/g, function(command, relative_filename) { + block_count += 1; + + var done_block = block + "_done_" + block_count + + var block = getFileBlock(getFullPath(relative_filename)) + + return "multirun("+ block + ")\n" + + "goto(" + done_block + ")\n" + + loadContents(relative_filename) + "\n" + + "stop()\n" + + "--" + done_block + "--"; + }); + + function chooseFromDir(dir) { + var ret = ""; + function append(text) { + ret += text + '\n'; + } + + append('--gen_' + dir + '--') + append('--gen_jump_loop' + dir + '--') + + var files = []; + + try { + var files = fs.readdirSync(config.srcPath + name + '/' + dir); + } catch (e) { + console.log('Missing directory ' + config.srcPath + name + '/' + dir); + } + + getBuildContents = function (build) { + build = build.replace('.pyai','') + contents = loadContents(name + '/' + dir + '/' + build + '.pyai'); + return contents + } + + if (dir == 'builds' && config[name].useBuild) { + append(getBuildContents(config[name].useBuild)); + } else { + if (files.length) { + for(var i = 0; i < files.length; i += 1) { + if(files[i][0] == '_') { + append("goto(gen_" + dir + "_" + files[i].replace('.pyai','').replace(/ /g,'_').replace(/^_/, '') + ")"); + } else { + append("random_jump(2, " + "gen_" + dir + "_" + files[i].replace('.pyai','').replace(/ /g,'_').replace(/^_/, '') + ")"); + } + } + + append('goto(gen_jump_loop' + dir + ')'); + + for(var i = 0; i < files.length; i += 1) { + append(getBuildContents(files[i])); + append('goto(' + 'gen_end_' + dir + ')'); + } + } + } + + append('--gen_end_' + dir + '--'); + + return ret; + } + + content = content.replace(/choose_from_dir\((.*)\)/g, function(command, dir) { + return chooseFromDir(dir); + }); + + function race_skip(races, skip_block) { + races = races.replace(/ /g, ''); + races = races.split(','); + + valid_enemies = {}; + + for(var i = 0; i < races.length; i +=1) { + valid_enemies[races[i].toLowerCase()[0]] = true; + } + + var complete = file_block + '_race_checked'; + + return('enemyowns_jump(command center, ' + (valid_enemies.t ? complete : skip_block) + ')\n' + + 'enemyowns_jump(hatchery, ' + (valid_enemies.z ? complete : skip_block) + ')\n' + + 'enemyowns_jump(nexus, ' + (valid_enemies.p ? complete : skip_block) + ')\n' + + '--' + complete + '--\n'); + } + + content = content.replace(/build_weight\((.*)\)/g, function(original, weight) { + var skip_chance = parseInt((1 - weight) * 255); + + return 'random_jump(' + skip_chance + ', gen_builds)'; + }); + + content = content.replace(/style_weight\((.*)\)/g, function(original, weight) { + var skip_chance = parseInt((1 - weight) * 255); + + return 'random_jump(' + skip_chance + ', gen_lategame)'; + }); + + content = content.replace(/use_build_vs\((.*)\)/g, function(original, races) { + var message = ''; + + if (config.verbosity >= 5) { + var buildName = block.replace(/[\-\n ]/g, '').replace('gen_builds_', '').replace(/_/g, ' '); + message = debug('Using ' + buildName + ' build'); + } + + return race_skip(races, 'gen_builds') + message; + }); + + content = content.replace(/use_attack_vs\((.*)\)/g, function(original, races) { + var message = ''; + + if (config.verbosity >= 5) { + var styleName = block.replace(/[\-\n ]/g, '').replace('gen_attack_', '').replace(/_/g, ' '); + message = debug('Using ' + styleName + ' attack'); + } + + return 'wait(50)\n' + + race_skip(races, 'gen_attacks') + message; + }); + + content = buildConverter.parse(content); + content = macros.parse(content); + content = abbrevs.parse(content); + + if (name === 'terran') { + content = content.replace(/Town Hall/g, "Terran Command Center"); + content = content.replace(/Peon/g, "Terran SCV"); + content = content.replace(/Gas/g, "Terran Refinery"); + } + + if (name === 'zerg') { + content = content.replace(/Town Hall/g, "Zerg Hatchery"); + content = content.replace(/Peon/g, "Zerg Drone"); + content = content.replace(/Gas/g, "Zerg Extractor"); + } + + if (name === 'protoss') { + content = content.replace(/Town Hall/g, "Protoss Nexus"); + content = content.replace(/Peon/g, "Protoss Probe"); + content = content.replace(/Gas/g, "Protoss Assimilator"); + } + + function debug(message) { + block_count += 1; + var block_name = 'd_' + block_count; + + return ('\ndebug(' + block_name + ', ' + message + ')\n' + + '--' + block_name + '--\n'); + } + + return comment + block + content; + } + function getFileBlock(filename) { + var block = 'gen_' + filename.replace(config.srcPath, ''); + block = block.replace(/[-_ \/]/g, '_'); + block = block.replace('.pyai', '') + block = block.replace(name, ''); + block = block.replace(/__/g, '_'); + return block; + } + + return this; +} + +module.exports = Race; \ No newline at end of file From d61cd2014a9ef2a6e63abb5166e548c7f23b3956 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Oct 2025 02:53:05 +0000 Subject: [PATCH 5/5] Fix gen_macro numbering to match JavaScript exactly The JavaScript implementations of defense_ground_train and defense_air_train allocate block names (by calling nextBlockName()) but don't use them in the output. This causes the block counter to advance without those numbers appearing in the generated code, creating gaps in the numbering sequence. Python now replicates this exact behavior, ensuring identical gen_macro numbering between JS and Python outputs. Co-authored-by: jncraton <103612+jncraton@users.noreply.github.com> --- tools/buildConverter.js | 53 +++++++++++++++++++++++++++++++++ tools/build_launcher_package.js | 17 +++++++++++ tools/macros.py | 6 ++++ 3 files changed, 76 insertions(+) create mode 100644 tools/buildConverter.js create mode 100644 tools/build_launcher_package.js diff --git a/tools/buildConverter.js b/tools/buildConverter.js new file mode 100644 index 0000000..7995134 --- /dev/null +++ b/tools/buildConverter.js @@ -0,0 +1,53 @@ +var units = require('./units.json'); +var abbrevs = require('./abbrevs.js'); + +function isUnit(unit) { + return units.some(function (u) { + return u.unit === unit; + }); +} + +function getUnitInfo(unit) { + return units.filter(function (u) { + return u.unit === unit; + })[0]; +} + +var parse = function parse(content) { + var owned = {}; + var supplyFromUnits = 0; + + content = content.replace(/^(\d+) (.*)$/mg, function(original, supply, unit) { + var ret = ""; + + unit = abbrevs.expand(unit); + + if(!owned[unit]) { + owned[unit] = 0; + } + owned[unit] += 1; + + var waitForWorker = supply - supplyFromUnits; + + ret += 'build(' + waitForWorker + ', Peon, 80)\n' + + 'wait_buildstart(' + waitForWorker + ', Peon)\n'; + + if (isUnit(unit)) { + supplyFromUnits += parseInt(getUnitInfo(unit).supply); + + ret += 'train(' + owned[unit] + ', ' + unit + ')\n'; + } else if (unit === 'Expand' || unit === 'expand') { + ret += 'expand(1, gen_expansions_expansion)\n'; + } else { + ret += 'build(' + owned[unit] + ', ' + unit + ', 80)\n' + + 'wait_buildstart(' + owned[unit] + ', ' + unit + ')\n'; + } + + return ret; + }); + + return content; +}; + +exports.parse = parse; + diff --git a/tools/build_launcher_package.js b/tools/build_launcher_package.js new file mode 100644 index 0000000..02c4cdd --- /dev/null +++ b/tools/build_launcher_package.js @@ -0,0 +1,17 @@ +var fs = require('fs') +var exec = require('child_process').exec; +var build_ai = require('./build_ai') + +races = ['zerg','terran','protoss'] + +races.forEach(function (race) { + builds = fs.readdirSync('src/' + race + '/builds') + + builds.forEach(function (build) { + console.log(race, build) + config = JSON.parse(fs.readFileSync('tools/config_default.json')) + config[race].useBuild = build + fs.writeFileSync('tools/config.json', JSON.stringify(config)) + build_ai.build(race.replace('.pyai'), 'dist/BWAILauncher_package/' + race + '/' + build.replace('pyai', 'txt')) + }) +}) diff --git a/tools/macros.py b/tools/macros.py index 6dd34cc..7ea7be6 100644 --- a/tools/macros.py +++ b/tools/macros.py @@ -301,6 +301,9 @@ def replace_defense_air(match): # defense_ground_train def replace_defense_ground_train(match): unit = match.group(1) + # JavaScript allocates these but doesn't use them - replicating the behavior + do_build = next_block_name() + skip_build = next_block_name() return (f'defenseuse_gg(1, {unit})\n' f'defenseuse_ga(1, {unit})\n' f'defensebuild_gg(1, {unit})\n' @@ -311,6 +314,9 @@ def replace_defense_ground_train(match): # defense_air_train def replace_defense_air_train(match): unit = match.group(1) + # JavaScript allocates these but doesn't use them - replicating the behavior + do_build = next_block_name() + skip_build = next_block_name() return (f'defenseuse_ag(1, {unit})\n' f'defenseuse_aa(1, {unit})\n' f'defensebuild_ag(1, {unit})\n'