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/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/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/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.py b/tools/macros.py new file mode 100644 index 0000000..7ea7be6 --- /dev/null +++ b/tools/macros.py @@ -0,0 +1,383 @@ +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) + # 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' + 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) + # 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' + 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')