├── .gitignore ├── .pylintrc ├── LICENSE ├── README.md ├── __init__.py ├── actions ├── __init__.py ├── anti_cheese │ ├── __init__.py │ ├── proxy_defense.py │ └── worker_rush_defense.py ├── macro │ ├── __init__.py │ ├── build │ │ ├── __init__.py │ │ ├── cavern_construction.py │ │ ├── creep_spread.py │ │ ├── evochamber_construction.py │ │ ├── expansion.py │ │ ├── extractor_construction.py │ │ ├── hydraden_construction.py │ │ ├── pit_construction.py │ │ ├── pool_construction.py │ │ ├── spine_construction.py │ │ ├── spire_construction.py │ │ ├── spore_construction.py │ │ ├── transformation_to_hive.py │ │ └── transformation_to_lair.py │ ├── buildings_positions.py │ ├── train │ │ ├── __init__.py │ │ ├── drone_creation.py │ │ ├── hydra_creation.py │ │ ├── mutalisk_creation.py │ │ ├── overlord_creation.py │ │ ├── overseer_creation.py │ │ ├── queen_creation.py │ │ ├── ultralisk_creation.py │ │ └── zergling_creation.py │ └── upgrades │ │ ├── __init__.py │ │ ├── cavern_upgrades.py │ │ ├── evochamber_upgrades.py │ │ ├── hydraden_upgrades.py │ │ └── spawning_pool_upgrades.py └── micro │ ├── __init__.py │ ├── army_value_tables.py │ ├── buildings_cancellation.py │ ├── micro_helpers.py │ ├── micro_main.py │ ├── specific_units_behaviors.py │ ├── unit │ ├── __init__.py │ ├── changeling_control.py │ ├── creep_tumor.py │ ├── drone_control.py │ ├── hydralisk_control.py │ ├── overlord_control.py │ ├── overseer_control.py │ ├── queen_control.py │ └── zergling_control.py │ └── worker_distribution.py ├── data_containers ├── __init__.py ├── data_container.py ├── our_possessions.py ├── our_structures.py ├── our_units.py ├── quantity_data.py ├── special_cases.py └── ungrouped_data.py ├── global_helpers.py ├── ladderbots.json ├── main.py └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | run_test.py 4 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 21 | # number of processors available to use. 22 | jobs=1 23 | 24 | # Control the amount of potential inferred values when inferring a single 25 | # object. This can help the performance when dealing with large functions or 26 | # complex, nested conditions. 27 | limit-inference-results=100 28 | 29 | # List of plugins (as comma separated values of python modules names) to load, 30 | # usually to register additional checkers. 31 | load-plugins= 32 | 33 | # Pickle collected data for later comparisons. 34 | persistent=yes 35 | 36 | # Specify a configuration file. 37 | #rcfile= 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=bad-continuation, 64 | no-name-in-module, 65 | no-member, 66 | print-statement, 67 | parameter-unpacking, 68 | unpacking-in-except, 69 | old-raise-syntax, 70 | backtick, 71 | long-suffix, 72 | old-ne-operator, 73 | old-octal-literal, 74 | import-star-module-level, 75 | non-ascii-bytes-literal, 76 | raw-checker-failed, 77 | bad-inline-option, 78 | locally-disabled, 79 | locally-enabled, 80 | file-ignored, 81 | suppressed-message, 82 | useless-suppression, 83 | deprecated-pragma, 84 | use-symbolic-message-instead, 85 | super-init-not-called, 86 | apply-builtin, 87 | basestring-builtin, 88 | buffer-builtin, 89 | cmp-builtin, 90 | coerce-builtin, 91 | execfile-builtin, 92 | file-builtin, 93 | long-builtin, 94 | raw_input-builtin, 95 | reduce-builtin, 96 | standarderror-builtin, 97 | unicode-builtin, 98 | xrange-builtin, 99 | coerce-method, 100 | delslice-method, 101 | getslice-method, 102 | setslice-method, 103 | no-absolute-import, 104 | old-division, 105 | dict-iter-method, 106 | dict-view-method, 107 | next-method-called, 108 | metaclass-assignment, 109 | indexing-exception, 110 | raising-string, 111 | reload-builtin, 112 | oct-method, 113 | hex-method, 114 | nonzero-method, 115 | cmp-method, 116 | input-builtin, 117 | round-builtin, 118 | intern-builtin, 119 | unichr-builtin, 120 | map-builtin-not-iterating, 121 | zip-builtin-not-iterating, 122 | range-builtin-not-iterating, 123 | filter-builtin-not-iterating, 124 | using-cmp-argument, 125 | eq-without-hash, 126 | div-method, 127 | idiv-method, 128 | rdiv-method, 129 | exception-message-attribute, 130 | invalid-str-codec, 131 | sys-max-int, 132 | bad-python3-import, 133 | deprecated-string-function, 134 | deprecated-str-translate-call, 135 | deprecated-itertools-function, 136 | deprecated-types-field, 137 | next-method-defined, 138 | dict-items-not-iterating, 139 | dict-keys-not-iterating, 140 | dict-values-not-iterating, 141 | deprecated-operator-function, 142 | deprecated-urllib-function, 143 | xreadlines-attribute, 144 | deprecated-sys-function, 145 | exception-escape, 146 | comprehension-escape 147 | 148 | # Enable the message, report, category or checker with the given id(s). You can 149 | # either give multiple identifier separated by comma (,) or put this option 150 | # multiple time (only on the command line, not in the configuration file where 151 | # it should appear only once). See also the "--disable" option for examples. 152 | enable=c-extension-no-member 153 | 154 | 155 | [REPORTS] 156 | 157 | # Python expression which should return a note less than 10 (10 is the highest 158 | # note). You have access to the variables errors warning, statement which 159 | # respectively contain the number of errors / warnings messages and the total 160 | # number of statements analyzed. This is used by the global evaluation report 161 | # (RP0004). 162 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 163 | 164 | # Template used to display messages. This is a python new-style format string 165 | # used to format the message information. See doc for all details. 166 | msg-template={abspath}:{line:5d},{column:2d}: 167 | 168 | # Set the output format. Available formats are text, parseable, colorized, json 169 | # and msvs (visual studio). You can also give a reporter class, e.g. 170 | # mypackage.mymodule.MyReporterClass. 171 | output-format=colorized 172 | 173 | # Tells whether to display a full report or only the messages. 174 | reports=no 175 | 176 | # Activate the evaluation score. 177 | score=yes 178 | 179 | 180 | [REFACTORING] 181 | 182 | # Maximum number of nested blocks for function / method body 183 | max-nested-blocks=5 184 | 185 | # Complete name of functions that never returns. When checking for 186 | # inconsistent-return-statements if a never returning function is called then 187 | # it will be considered as an explicit return statement and no message will be 188 | # printed. 189 | never-returning-functions=sys.exit 190 | 191 | 192 | [BASIC] 193 | 194 | # Naming style matching correct argument names. 195 | argument-naming-style=snake_case 196 | 197 | # Regular expression matching correct argument names. Overrides argument- 198 | # naming-style. 199 | #argument-rgx= 200 | 201 | # Naming style matching correct attribute names. 202 | attr-naming-style=snake_case 203 | 204 | # Regular expression matching correct attribute names. Overrides attr-naming- 205 | # style. 206 | #attr-rgx= 207 | 208 | # Bad variable names which should always be refused, separated by a comma. 209 | bad-names=foo, 210 | bar, 211 | baz, 212 | toto, 213 | tutu, 214 | tata 215 | 216 | # Naming style matching correct class attribute names. 217 | class-attribute-naming-style=any 218 | 219 | # Regular expression matching correct class attribute names. Overrides class- 220 | # attribute-naming-style. 221 | #class-attribute-rgx= 222 | 223 | # Naming style matching correct class names. 224 | class-naming-style=PascalCase 225 | 226 | # Regular expression matching correct class names. Overrides class-naming- 227 | # style. 228 | #class-rgx= 229 | 230 | # Naming style matching correct constant names. 231 | const-naming-style=UPPER_CASE 232 | 233 | # Regular expression matching correct constant names. Overrides const-naming- 234 | # style. 235 | #const-rgx= 236 | 237 | # Minimum line length for functions/classes that require docstrings, shorter 238 | # ones are exempt. 239 | docstring-min-length=-1 240 | 241 | # Naming style matching correct function names. 242 | function-naming-style=snake_case 243 | 244 | # Regular expression matching correct function names. Overrides function- 245 | # naming-style. 246 | #function-rgx= 247 | 248 | # Good variable names which should always be accepted, separated by a comma. 249 | good-names=i, 250 | j, 251 | k, 252 | ex, 253 | Run, 254 | _ 255 | 256 | # Include a hint for the correct naming format with invalid-name. 257 | include-naming-hint=no 258 | 259 | # Naming style matching correct inline iteration names. 260 | inlinevar-naming-style=any 261 | 262 | # Regular expression matching correct inline iteration names. Overrides 263 | # inlinevar-naming-style. 264 | #inlinevar-rgx= 265 | 266 | # Naming style matching correct method names. 267 | method-naming-style=snake_case 268 | 269 | # Regular expression matching correct method names. Overrides method-naming- 270 | # style. 271 | #method-rgx= 272 | 273 | # Naming style matching correct module names. 274 | module-naming-style=snake_case 275 | 276 | # Regular expression matching correct module names. Overrides module-naming- 277 | # style. 278 | #module-rgx= 279 | 280 | # Colon-delimited sets of names that determine each other's naming style when 281 | # the name regexes allow several styles. 282 | name-group= 283 | 284 | # Regular expression which should only match function or class names that do 285 | # not require a docstring. 286 | no-docstring-rgx=^_ 287 | 288 | # List of decorators that produce properties, such as abc.abstractproperty. Add 289 | # to this list to register other decorators that produce valid properties. 290 | # These decorators are taken in consideration only for invalid-name. 291 | property-classes=abc.abstractproperty 292 | 293 | # Naming style matching correct variable names. 294 | variable-naming-style=snake_case 295 | 296 | # Regular expression matching correct variable names. Overrides variable- 297 | # naming-style. 298 | #variable-rgx= 299 | 300 | 301 | [FORMAT] 302 | 303 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 304 | expected-line-ending-format= 305 | 306 | # Regexp for a line that is allowed to be longer than the limit. 307 | ignore-long-lines=^\s*(# )??$ 308 | 309 | # Number of spaces of indent required inside a hanging or continued line. 310 | indent-after-paren=4 311 | 312 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 313 | # tab). 314 | indent-string=' ' 315 | 316 | # Maximum number of characters on a single line. 317 | max-line-length=120 318 | 319 | # Maximum number of lines in a module. 320 | max-module-lines=1000 321 | 322 | # List of optional constructs for which whitespace checking is disabled. `dict- 323 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 324 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 325 | # `empty-line` allows space-only lines. 326 | no-space-check=trailing-comma, 327 | dict-separator 328 | 329 | # Allow the body of a class to be on the same line as the declaration if body 330 | # contains single statement. 331 | single-line-class-stmt=no 332 | 333 | # Allow the body of an if to be on the same line as the test if there is no 334 | # else. 335 | single-line-if-stmt=no 336 | 337 | 338 | [LOGGING] 339 | 340 | # Logging modules to check that the string format arguments are in logging 341 | # function parameter format. 342 | logging-modules=logging 343 | 344 | 345 | [MISCELLANEOUS] 346 | 347 | # List of note tags to take in consideration, separated by a comma. 348 | notes=FIXME, 349 | XXX, 350 | TODO 351 | 352 | 353 | [SIMILARITIES] 354 | 355 | # Ignore comments when computing similarities. 356 | ignore-comments=yes 357 | 358 | # Ignore docstrings when computing similarities. 359 | ignore-docstrings=yes 360 | 361 | # Ignore imports when computing similarities. 362 | ignore-imports=no 363 | 364 | # Minimum lines number of a similarity. 365 | min-similarity-lines=4 366 | 367 | 368 | [SPELLING] 369 | 370 | # Limits count of emitted suggestions for spelling mistakes. 371 | max-spelling-suggestions=4 372 | 373 | # Spelling dictionary name. Available dictionaries: none. To make it working 374 | # install python-enchant package.. 375 | spelling-dict= 376 | 377 | # List of comma separated words that should not be checked. 378 | spelling-ignore-words= 379 | 380 | # A path to a file that contains private dictionary; one word per line. 381 | spelling-private-dict-file= 382 | 383 | # Tells whether to store unknown words to indicated private dictionary in 384 | # --spelling-private-dict-file option instead of raising a message. 385 | spelling-store-unknown-words=no 386 | 387 | 388 | [TYPECHECK] 389 | 390 | # List of decorators that produce context managers, such as 391 | # contextlib.contextmanager. Add to this list to register other decorators that 392 | # produce valid context managers. 393 | contextmanager-decorators=contextlib.contextmanager 394 | 395 | # List of members which are set dynamically and missed by pylint inference 396 | # system, and so shouldn't trigger E1101 when accessed. Python regular 397 | # expressions are accepted. 398 | generated-members= 399 | 400 | # Tells whether missing members accessed in mixin class should be ignored. A 401 | # mixin class is detected if its name ends with "mixin" (case insensitive). 402 | ignore-mixin-members=yes 403 | 404 | # Tells whether to warn about missing members when the owner of the attribute 405 | # is inferred to be None. 406 | ignore-none=yes 407 | 408 | # This flag controls whether pylint should warn about no-member and similar 409 | # checks whenever an opaque object is returned when inferring. The inference 410 | # can return multiple potential results while evaluating a Python object, but 411 | # some branches might not be evaluated, which results in partial inference. In 412 | # that case, it might be useful to still emit no-member and other checks for 413 | # the rest of the inferred objects. 414 | ignore-on-opaque-inference=yes 415 | 416 | # List of class names for which member attributes should not be checked (useful 417 | # for classes with dynamically set attributes). This supports the use of 418 | # qualified names. 419 | ignored-classes=optparse.Values,thread._local,_thread._local 420 | 421 | # List of module names for which member attributes should not be checked 422 | # (useful for modules/projects where namespaces are manipulated during runtime 423 | # and thus existing member attributes cannot be deduced by static analysis. It 424 | # supports qualified module names, as well as Unix pattern matching. 425 | ignored-modules= 426 | 427 | # Show a hint with possible names when a member name was not found. The aspect 428 | # of finding the hint is based on edit distance. 429 | missing-member-hint=yes 430 | 431 | # The minimum edit distance a name should have in order to be considered a 432 | # similar match for a missing member name. 433 | missing-member-hint-distance=1 434 | 435 | # The total number of similar names that should be taken in consideration when 436 | # showing a hint for a missing member. 437 | missing-member-max-choices=1 438 | 439 | 440 | [VARIABLES] 441 | 442 | # List of additional names supposed to be defined in builtins. Remember that 443 | # you should avoid to define new builtins when possible. 444 | additional-builtins= 445 | 446 | # Tells whether unused global variables should be treated as a violation. 447 | allow-global-unused-variables=yes 448 | 449 | # List of strings which can identify a callback function by name. A callback 450 | # name must start or end with one of those strings. 451 | callbacks=cb_, 452 | _cb 453 | 454 | # A regular expression matching the name of dummy variables (i.e. expected to 455 | # not be used). 456 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 457 | 458 | # Argument names that match this expression will be ignored. Default to name 459 | # with leading underscore. 460 | ignored-argument-names=_.*|^ignored_|^unused_ 461 | 462 | # Tells whether we should check for unused import in __init__ files. 463 | init-import=no 464 | 465 | # List of qualified module names which can have objects that can redefine 466 | # builtins. 467 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 468 | 469 | 470 | [CLASSES] 471 | 472 | # List of method names used to declare (i.e. assign) instance attributes. 473 | defining-attr-methods=__init__, 474 | __new__, 475 | setUp 476 | 477 | # List of member names, which should be excluded from the protected access 478 | # warning. 479 | exclude-protected=_asdict, 480 | _fields, 481 | _replace, 482 | _source, 483 | _make 484 | 485 | # List of valid names for the first argument in a class method. 486 | valid-classmethod-first-arg=cls 487 | 488 | # List of valid names for the first argument in a metaclass class method. 489 | valid-metaclass-classmethod-first-arg=cls 490 | 491 | 492 | [DESIGN] 493 | 494 | # Maximum number of arguments for function / method. 495 | max-args=5 496 | 497 | # Maximum number of attributes for a class (see R0902). 498 | max-attributes=7 499 | 500 | # Maximum number of boolean expressions in an if statement. 501 | max-bool-expr=5 502 | 503 | # Maximum number of branch for function / method body. 504 | max-branches=12 505 | 506 | # Maximum number of locals for function / method body. 507 | max-locals=15 508 | 509 | # Maximum number of parents for a class (see R0901). 510 | max-parents=7 511 | 512 | # Maximum number of public methods for a class (see R0904). 513 | max-public-methods=20 514 | 515 | # Maximum number of return / yield for function / method body. 516 | max-returns=6 517 | 518 | # Maximum number of statements in function / method body. 519 | max-statements=50 520 | 521 | # Minimum number of public methods for a class (see R0903). 522 | min-public-methods=2 523 | 524 | 525 | [IMPORTS] 526 | 527 | # Allow wildcard imports from modules that define __all__. 528 | allow-wildcard-with-all=no 529 | 530 | # Analyse import fallback blocks. This can be used to support both Python 2 and 531 | # 3 compatible code, which means that the block might have code that exists 532 | # only in one or another interpreter, leading to false positives when analysed. 533 | analyse-fallback-blocks=no 534 | 535 | # Deprecated modules which should not be used, separated by a comma. 536 | deprecated-modules=optparse,tkinter.tix 537 | 538 | # Create a graph of external dependencies in the given file (report RP0402 must 539 | # not be disabled). 540 | ext-import-graph= 541 | 542 | # Create a graph of every (i.e. internal and external) dependencies in the 543 | # given file (report RP0402 must not be disabled). 544 | import-graph= 545 | 546 | # Create a graph of internal dependencies in the given file (report RP0402 must 547 | # not be disabled). 548 | int-import-graph= 549 | 550 | # Force import order to recognize a module as part of the standard 551 | # compatibility libraries. 552 | known-standard-library= 553 | 554 | # Force import order to recognize a module as part of a third party library. 555 | known-third-party=enchant 556 | 557 | 558 | [EXCEPTIONS] 559 | 560 | # Exceptions that will emit a warning when being caught. Defaults to 561 | # "Exception". 562 | overgeneral-exceptions=Exception 563 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mateus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **JackBot** 2 | 3 | Its a bot does an early aggression, then tries to make a transition to hydralisks then ultralisks (derived from 4 | CreepyBot) 5 | 6 | **Achievements** 7 | 8 | - Season 6 - top 11 (sc2ai ladder) 9 | 10 | **Requirements:** 11 | 12 | - Python 3.7 + 13 | 14 | - The most recent Sc2 version 15 | 16 | - The most recent s2clientprotocol 17 | 18 | - The most recent Python-sc2 library (pip install sc2 or pip3 install --upgrade git+https://github.com/Dentosal/python-sc2@develop -- force-reinstall) 19 | 20 | - Numpy newest version -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """Initialize the bot""" 2 | import argparse 3 | import asyncio 4 | import logging 5 | import aiohttp 6 | import sc2 7 | from sc2.portconfig import Portconfig 8 | from sc2.client import Client 9 | 10 | 11 | def run_ladder_game(bot): 12 | """Connect to the ladder server and run the game""" 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument("--GamePort", type=int, nargs="?", help="Game port") 15 | parser.add_argument("--StartPort", type=int, nargs="?", help="Start port") 16 | parser.add_argument("--LadderServer", type=str, nargs="?", help="Ladder server") 17 | parser.add_argument("--ComputerOpponent", type=str, nargs="?", help="Computer opponent") 18 | parser.add_argument("--ComputerRace", type=str, nargs="?", help="Computer race") 19 | parser.add_argument("--ComputerDifficulty", type=str, nargs="?", help="Computer difficulty") 20 | args, _ = parser.parse_known_args() 21 | if args.LadderServer is None: 22 | host = "127.0.0.1" 23 | else: 24 | host = args.LadderServer 25 | host_port = args.GamePort 26 | lan_port = args.StartPort 27 | ports = [lan_port + p for p in range(1, 6)] 28 | portconfig = Portconfig() 29 | portconfig.shared = ports[0] 30 | portconfig.server = [ports[1], ports[2]] 31 | portconfig.players = [[ports[3], ports[4]]] 32 | game = join_ladder_game(host=host, port=host_port, players=[bot], realtime=False, portconfig=portconfig) 33 | result = asyncio.get_event_loop().run_until_complete(game) 34 | print(result) 35 | 36 | 37 | async def join_ladder_game( 38 | host, port, players, realtime, portconfig, save_replay_as=None, step_time_limit=None, game_time_limit=None 39 | ): 40 | """Logic to join the ladder""" 41 | ws_url = "ws://{}:{}/sc2api".format(host, port) 42 | ws_connection = await aiohttp.ClientSession().ws_connect(ws_url, timeout=120) 43 | client = Client(ws_connection) 44 | try: 45 | result = await sc2.main._play_game(players[0], client, realtime, portconfig, step_time_limit, game_time_limit) 46 | if save_replay_as: 47 | await client.save_replay(save_replay_as) 48 | await client.leave() 49 | await client.quit() 50 | except ConnectionError: 51 | logging.error("Connection was closed before the game ended") 52 | return None 53 | finally: 54 | await ws_connection.close() 55 | return result 56 | -------------------------------------------------------------------------------- /actions/__init__.py: -------------------------------------------------------------------------------- 1 | """ Group all unit commands""" 2 | from .anti_cheese import get_cheese_defense_commands 3 | from .micro import get_army_and_building_commands 4 | from .micro.unit import get_macro_units_commands 5 | 6 | 7 | def get_unit_commands(cmd): 8 | """ Getter for all unit commands""" 9 | return get_macro_units_commands(cmd) + get_army_and_building_commands(cmd) + get_cheese_defense_commands(cmd) 10 | -------------------------------------------------------------------------------- /actions/anti_cheese/__init__.py: -------------------------------------------------------------------------------- 1 | """ Group all classes from anti_cheese""" 2 | from . import proxy_defense, worker_rush_defense 3 | 4 | 5 | def get_cheese_defense_commands(cmd): 6 | """ Getter for all commands from anti_cheese""" 7 | return proxy_defense.ProxyDefense(cmd), worker_rush_defense.WorkerRushDefense(cmd) 8 | -------------------------------------------------------------------------------- /actions/anti_cheese/proxy_defense.py: -------------------------------------------------------------------------------- 1 | """Everything related to handling proxies are here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class ProxyDefense: 6 | """Needs improvements on the quantity, also on the follow up(its overly defensive)""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | self.proxy_buildings = None 11 | self.worker_types = {UnitTypeId.PROBE, UnitTypeId.DRONE, UnitTypeId.SCV} 12 | self.atk_b = {UnitTypeId.SPINECRAWLER, UnitTypeId.PHOTONCANNON, UnitTypeId.BUNKER, UnitTypeId.PLANETARYFORTRESS} 13 | self.enemy_basic_production = {UnitTypeId.BARRACKS, UnitTypeId.GATEWAY} 14 | 15 | async def should_handle(self): 16 | """Requirements to run handle(can be improved, hard-coding the trigger distance is way to exploitable)""" 17 | if not self.main.iteration % 10: 18 | return False 19 | if self.main.townhalls: 20 | self.proxy_buildings = self.main.enemy_structures.exclude_type(self.enemy_basic_production).closer_than( 21 | 50, self.main.furthest_townhall_to_center 22 | ) 23 | return ( 24 | self.proxy_buildings 25 | and self.main.time <= 270 26 | and self.main.drone_amount >= 15 27 | and not self.main.ground_enemies 28 | ) 29 | 30 | async def handle(self): 31 | """Send workers aggressively to handle the near proxy / cannon rush, need to learn how to get the max 32 | surface area possible when attacking the buildings""" 33 | drone_force = self.main.drones.filter(lambda x: x.is_collecting and not x.is_attacking) 34 | if drone_force: 35 | for enemy_worker in self.main.enemies.of_type(self.worker_types).filter( 36 | lambda unit: any(unit.distance_to(our_building) <= 50 for our_building in self.main.structures) 37 | ): 38 | if not self.is_being_attacked(enemy_worker): 39 | self.main.add_action(drone_force.closest_to(enemy_worker).attack(enemy_worker)) 40 | shooter_buildings = self.proxy_buildings.of_type(self.atk_b) 41 | support_buildings = self.proxy_buildings - shooter_buildings # like pylons 42 | if shooter_buildings: 43 | drone_force = self.main.drones.filter(lambda x: x.order_target not in [y.tag for y in shooter_buildings]) 44 | self.pull_drones(shooter_buildings, drone_force) 45 | if support_buildings: 46 | self.pull_drones(support_buildings, drone_force) 47 | 48 | def is_being_attacked(self, unit): 49 | """ 50 | Calculates how often our units are attacking the given enemy unit 51 | 52 | Parameters 53 | ---------- 54 | unit: Enemy unit that is being targeted 55 | 56 | Returns 57 | ------- 58 | An integer value that corresponds to how many of our units are attacking the given target 59 | """ 60 | 61 | return len( 62 | ["" for attacker in self.main.units.filter(lambda x: x.is_attacking) if attacker.order_target == unit.tag] 63 | ) 64 | 65 | def pull_drones(self, selected_building_targets, available_drone_force): 66 | """Pull 3 drones to destroy the proxy building""" 67 | if available_drone_force: 68 | for target in selected_building_targets: 69 | if self.is_being_attacked(target) < 3: 70 | self.main.add_action(available_drone_force.closest_to(target).attack(target)) 71 | -------------------------------------------------------------------------------- /actions/anti_cheese/worker_rush_defense.py: -------------------------------------------------------------------------------- 1 | """Everything related to defending a worker rush goes here""" 2 | import heapq 3 | from sc2.constants import UnitTypeId 4 | from actions.micro.micro_helpers import MicroHelpers 5 | 6 | 7 | class WorkerRushDefense(MicroHelpers): 8 | """Ok for now, but probably can be expanded to handle more than just worker rushes""" 9 | 10 | def __init__(self, main): 11 | self.main = main 12 | self.base = self.close_enemy_workers = self.defense_force = self.defender_tags = self.defense_force_size = None 13 | self.worker_types = {UnitTypeId.PROBE, UnitTypeId.DRONE, UnitTypeId.SCV} 14 | 15 | async def should_handle(self): 16 | """Requirements to run handle""" 17 | self.base = self.main.hatcheries.ready 18 | if not self.base: 19 | return False 20 | self.close_enemy_workers = self.main.enemies.closer_than(8, self.base.first).of_type(self.worker_types) 21 | return self.close_enemy_workers or self.defender_tags 22 | 23 | async def handle(self): 24 | """It destroys every worker rush without losing more than 2 workers""" 25 | self.defense_force_size = int(len(self.close_enemy_workers) * 1.25) 26 | if self.defender_tags: 27 | if self.close_enemy_workers: 28 | self.refill_defense_force() 29 | for drone in self.defense_force: 30 | if not self.save_low_hp_drone(drone): 31 | if drone.weapon_cooldown <= 13.4: # Wanted cd value * 22.4 32 | self.attack_close_target(drone, self.close_enemy_workers) 33 | elif not self.move_to_next_target(drone, self.close_enemy_workers): 34 | self.move_low_hp(drone, self.close_enemy_workers) 35 | else: 36 | self.clear_defense_force() 37 | elif self.close_enemy_workers: 38 | self.defender_tags = self.select_defense_force(self.defense_force_size) 39 | 40 | def clear_defense_force(self): 41 | """If there is more workers on the defenders force than the ideal put it back to mining""" 42 | if self.defense_force: 43 | selected_mineral_field = self.main.state.mineral_field.closest_to(self.base.first) 44 | for drone in self.defense_force: 45 | self.main.add_action(drone.gather(selected_mineral_field)) 46 | self.defender_tags = [] 47 | self.defense_force = None 48 | 49 | def select_defense_force(self, count): 50 | """ 51 | Select all drones needed on the defenders force 52 | Parameters 53 | ---------- 54 | count: The needed amount of drones to fill the defense force 55 | 56 | Returns 57 | ------- 58 | A list with all drones that are part of the defense force, ordered by health(highest one prioritized) 59 | """ 60 | return [unit.tag for unit in heapq.nlargest(count, self.main.drones.collecting, key=lambda dr: dr.health)] 61 | 62 | def refill_defense_force(self): 63 | """If there are less workers on the defenders force than the ideal refill it""" 64 | self.defense_force = self.main.drones.filter(lambda worker: worker.tag in self.defender_tags and worker.health) 65 | defender_deficit = min(self.main.drone_amount - 1, self.defense_force_size) - len(self.defense_force) 66 | if defender_deficit > 0: 67 | self.defender_tags += self.select_defense_force(defender_deficit) 68 | 69 | def save_low_hp_drone(self, drone): 70 | """ 71 | Remove drones with less 6 hp(one worker hit) from the defending force 72 | Parameters 73 | ---------- 74 | drone: A drone from the defenders force 75 | 76 | Returns 77 | ------- 78 | True if the drone got removed from the force, False if the drone doesn't need to be removed 79 | """ 80 | if drone.health <= 6: 81 | if not drone.is_collecting: 82 | self.main.add_action(drone.gather(self.main.state.mineral_field.closest_to(self.base.first.position))) 83 | else: 84 | self.defender_tags.remove(drone.tag) 85 | return True 86 | return False 87 | -------------------------------------------------------------------------------- /actions/macro/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matuiss2/JackBot/1b45ce782df666dd21996011a04c92211c0e6368/actions/macro/__init__.py -------------------------------------------------------------------------------- /actions/macro/build/__init__.py: -------------------------------------------------------------------------------- 1 | """ Group all classes from macro/build""" 2 | from . import ( 3 | cavern_construction, 4 | evochamber_construction, 5 | expansion, 6 | extractor_construction, 7 | transformation_to_hive, 8 | hydraden_construction, 9 | transformation_to_lair, 10 | pit_construction, 11 | pool_construction, 12 | spine_construction, 13 | spire_construction, 14 | spore_construction, 15 | ) 16 | 17 | 18 | def get_build_commands(cmd): 19 | """ Getter for all commands from macro/build""" 20 | return ( 21 | pool_construction.PoolConstruction(cmd), 22 | expansion.Expansion(cmd), 23 | extractor_construction.ExtractorConstruction(cmd), 24 | evochamber_construction.EvochamberConstruction(cmd), 25 | cavern_construction.CavernConstruction(cmd), 26 | pit_construction.PitConstruction(cmd), 27 | transformation_to_hive.TransformationToHive(cmd), 28 | transformation_to_lair.TransformationToLair(cmd), 29 | spine_construction.SpineConstruction(cmd), 30 | spore_construction.SporeConstruction(cmd), 31 | spire_construction.SpireConstruction(cmd), 32 | hydraden_construction.HydradenConstruction(cmd), 33 | ) 34 | -------------------------------------------------------------------------------- /actions/macro/build/cavern_construction.py: -------------------------------------------------------------------------------- 1 | """Everything related to building logic for the ultralisk cavern goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class CavernConstruction: 6 | """Ok for now""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Builds the ultralisk cavern""" 13 | return self.main.can_build_unique(UnitTypeId.ULTRALISKCAVERN, self.main.caverns, self.main.hives) 14 | 15 | async def handle(self): 16 | """Build the cavern on the decided placement""" 17 | await self.main.place_building(UnitTypeId.ULTRALISKCAVERN) 18 | -------------------------------------------------------------------------------- /actions/macro/build/creep_spread.py: -------------------------------------------------------------------------------- 1 | """Everything related to calculate the creep spreading goes here""" 2 | import math 3 | from sc2.constants import AbilityId 4 | from sc2.data import ActionResult 5 | from sc2.position import Point2 6 | 7 | 8 | class CreepSpread: 9 | """It spreads creeps, finds 'optimal' locations for it(have trouble with ramps, many improvements can be made)""" 10 | 11 | def __init__(self): 12 | self.used_tumors = [] 13 | self.ordered_placements = self.unit_ability = None 14 | 15 | async def place_tumor(self, unit): 16 | """ Find a nice placement for the tumor and build it if possible, avoid expansion locations 17 | Makes creep to the enemy base, needs a better value function for the spreading, it gets stuck on ramps""" 18 | # Make sure unit can make tumor and what ability it is 19 | available_unit_abilities = await self.get_available_abilities(unit) 20 | if AbilityId.BUILD_CREEPTUMOR_QUEEN in available_unit_abilities: 21 | self.unit_ability = AbilityId.BUILD_CREEPTUMOR_QUEEN 22 | elif AbilityId.BUILD_CREEPTUMOR_TUMOR in available_unit_abilities: 23 | self.unit_ability = AbilityId.BUILD_CREEPTUMOR_TUMOR 24 | else: 25 | return None 26 | all_angles = 30 27 | spread_distance = 8 28 | location = unit.position 29 | # Define 30 positions around the unit 30 | positions = [ 31 | Point2( 32 | ( 33 | location.x + spread_distance * math.cos(math.pi * alpha * 2 / all_angles), 34 | location.y + spread_distance * math.sin(math.pi * alpha * 2 / all_angles), 35 | ) 36 | ) 37 | for alpha in range(all_angles) 38 | ] 39 | # check the availability of all this positions 40 | positions_vacancy = await self._client.query_building_placement( 41 | self._game_data.abilities[AbilityId.ZERGBUILD_CREEPTUMOR.value], positions 42 | ) 43 | # filter the successful ones on a list 44 | valid_placements = [p2 for idx, p2 in enumerate(positions) if positions_vacancy[idx] == ActionResult.Success] 45 | final_destiny = self.enemy_start_locations[0] 46 | if valid_placements: 47 | if self.tumors: 48 | self.ordered_placements = sorted( 49 | valid_placements, 50 | key=lambda pos: pos.distance_to_closest(self.tumors) - pos.distance_to_point2(final_destiny), 51 | reverse=True, 52 | ) 53 | else: 54 | self.ordered_placements = sorted( 55 | valid_placements, key=lambda pos: pos.distance_to_point2(final_destiny) 56 | ) 57 | self.avoid_blocking_expansions(unit) 58 | if self.unit_ability == AbilityId.BUILD_CREEPTUMOR_TUMOR: 59 | self.used_tumors.append(unit.tag) 60 | 61 | def avoid_blocking_expansions(self, unit): 62 | """ This is very expensive to the cpu, need optimization, keeps creep outside expansion locations""" 63 | for creep_location in self.ordered_placements: 64 | if all(creep_location.distance_to_point2(el) > 8.5 for el in self.expansion_locations): 65 | if self.unit_ability == AbilityId.BUILD_CREEPTUMOR_QUEEN or not self.tumors: 66 | self.add_action(unit(self.unit_ability, creep_location)) 67 | break 68 | if creep_location.distance_to_closest(self.tumors) >= 4: 69 | self.add_action(unit(self.unit_ability, creep_location)) 70 | break 71 | -------------------------------------------------------------------------------- /actions/macro/build/evochamber_construction.py: -------------------------------------------------------------------------------- 1 | """Everything related to building logic for the evolution chamber goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class EvochamberConstruction: 6 | """Can maybe be improved""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Requirements for building the evolution chambers, maybe its to early can probably be improved""" 13 | if ( 14 | self.main.building_requirements(UnitTypeId.EVOLUTIONCHAMBER, self.main.settled_pool, one_at_time=True) 15 | and len(self.main.evochambers) < 2 16 | ): 17 | 18 | return self.main.base_amount >= 3 if not self.main.settled_evochamber else self.main.ready_base_amount >= 3 19 | 20 | async def handle(self): 21 | """Build the evochamber""" 22 | await self.main.place_building(UnitTypeId.EVOLUTIONCHAMBER) 23 | -------------------------------------------------------------------------------- /actions/macro/build/expansion.py: -------------------------------------------------------------------------------- 1 | """Everything related to the expansion logic goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class Expansion: 6 | """Can be improved""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | self.drones = None 11 | 12 | async def should_handle(self): 13 | """Fourth base sometimes are not build at the expected time maybe reduce the lock for it, 14 | also maybe the 7th or more hatchery can be postponed for when extra mining patches or production are needed """ 15 | self.drones = self.main.drones.gathering 16 | if self.expansion_prerequisites: 17 | if self.expand_to_avoid_mineral_overflow: 18 | return True 19 | if not self.main.hatcheries_in_queue: # This is a mess and surely can be simplified 20 | base_amount = self.main.base_amount # added to save lines 21 | if base_amount <= 5: 22 | return self.main.zergling_amount >= 22 or self.main.time >= 285 if base_amount == 2 else True 23 | return self.main.caverns 24 | return False 25 | return False 26 | 27 | async def handle(self): 28 | """Expands to the nearest expansion location using the nearest drone to it""" 29 | for expansion in self.main.ordered_expansions: 30 | if await self.main.can_place(UnitTypeId.HATCHERY, expansion): 31 | if not self.main.ground_enemies.closer_than(15, expansion): 32 | self.main.add_action(self.drones.closest_to(expansion).build(UnitTypeId.HATCHERY, expansion)) 33 | break 34 | 35 | @property 36 | def expansion_prerequisites(self): 37 | """ Check if its safe to expand and if we have the necessary minerals 38 | if its not don't even run the remaining expansion logic""" 39 | return ( 40 | self.main.townhalls 41 | and self.main.can_afford(UnitTypeId.HATCHERY) 42 | and not self.main.close_enemies_to_base 43 | and (not self.main.close_enemy_production or self.main.time > 690) 44 | and self.drones 45 | ) 46 | 47 | @property 48 | def expand_to_avoid_mineral_overflow(self): 49 | """ When overflowing with minerals run this condition check""" 50 | return ( 51 | self.main.minerals >= 900 52 | and self.main.hatcheries_in_queue < 2 53 | and self.main.base_amount + self.main.hatcheries_in_queue < len(self.main.expansion_locations) 54 | and self.main.base_amount > 4 55 | ) 56 | -------------------------------------------------------------------------------- /actions/macro/build/extractor_construction.py: -------------------------------------------------------------------------------- 1 | """Everything related to building logic for the extractors goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class ExtractorConstruction: 6 | """Can probably be improved, 7 | but I think the problem is more on the vespene collection than the extractor building""" 8 | 9 | def __init__(self, main): 10 | self.main = main 11 | 12 | async def should_handle(self): 13 | """Couldn't find another way to build the geysers its heavily based on Burny's approach, 14 | still trying to find the optimal number""" 15 | nonempty_geysers_amount = len(self.main.extractors.filter(lambda vg: vg.vespene_contents > 0)) 16 | if ( 17 | self.main.vespene > self.main.minerals 18 | or not self.main.building_requirements(UnitTypeId.EXTRACTOR, self.main.ready_bases, one_at_time=True) 19 | or nonempty_geysers_amount >= 10 20 | ): 21 | return False 22 | if not self.main.hives and nonempty_geysers_amount >= 6: 23 | return False 24 | if ( 25 | not self.main.extractors 26 | and self.main.pools 27 | or nonempty_geysers_amount < 3 <= self.main.ready_base_amount 28 | or self.main.ready_base_amount > 3 29 | ): 30 | return True 31 | 32 | async def handle(self): 33 | """Just finish the action of building the extractor""" 34 | for geyser in self.main.state.vespene_geyser.closer_than(10, self.main.ready_bases.random): 35 | selected_drone = self.main.select_build_worker(geyser.position) 36 | if selected_drone: 37 | self.main.add_action(selected_drone.build(UnitTypeId.EXTRACTOR, geyser)) 38 | -------------------------------------------------------------------------------- /actions/macro/build/hydraden_construction.py: -------------------------------------------------------------------------------- 1 | """Everything related to building logic for the hydralisk den goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class HydradenConstruction: 6 | """Ok for now""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Requirement to build the hydraden""" 13 | return ( 14 | self.main.can_build_unique( 15 | UnitTypeId.HYDRALISKDEN, self.main.hydradens, (self.main.lairs and self.main.pools) 16 | ) 17 | and not self.main.close_enemy_production 18 | and not self.main.floated_buildings_bm 19 | and self.main.base_amount >= 3 20 | ) 21 | 22 | async def handle(self): 23 | """Build the hydraden""" 24 | await self.main.place_building(UnitTypeId.HYDRALISKDEN) 25 | -------------------------------------------------------------------------------- /actions/macro/build/pit_construction.py: -------------------------------------------------------------------------------- 1 | """Everything related to building logic for the infestation pits goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class PitConstruction: 6 | """Can be improved so its more situational and less greedy""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Requirement to build the infestation pit, sometimes it creates a big gap on the bot, 13 | maybe we should raise the lock""" 14 | return self.main.base_amount > 4 and self.main.can_build_unique(UnitTypeId.INFESTATIONPIT, self.main.pits) 15 | 16 | async def handle(self): 17 | """Places the pit""" 18 | await self.main.place_building(UnitTypeId.INFESTATIONPIT) 19 | -------------------------------------------------------------------------------- /actions/macro/build/pool_construction.py: -------------------------------------------------------------------------------- 1 | """Everything related to building logic for the pools goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class PoolConstruction: 6 | """Ok for now""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Requirement for building the spawning pool""" 13 | return self.main.can_build_unique(UnitTypeId.SPAWNINGPOOL, self.main.pools) and ( 14 | self.main.base_amount >= 2 or self.main.close_enemy_production or self.main.time > 145 15 | ) 16 | 17 | async def handle(self): 18 | """Places the pool""" 19 | await self.main.place_building(UnitTypeId.SPAWNINGPOOL) 20 | -------------------------------------------------------------------------------- /actions/macro/build/spine_construction.py: -------------------------------------------------------------------------------- 1 | """Everything related to building logic for the spines goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class SpineConstruction: 6 | """New placement untested""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | self.placement_position = None 11 | 12 | async def should_handle(self): 13 | """Requirements to build the spines""" 14 | if ( 15 | len(self.main.spines) < 4 16 | and self.main.already_pending(UnitTypeId.SPINECRAWLER) < 2 17 | and self.main.building_requirements(UnitTypeId.SPINECRAWLER, self.main.settled_pool) 18 | and self.main.townhalls 19 | ): 20 | self.placement_position = self.main.furthest_townhall_to_center.position.towards( 21 | self.main.main_base_ramp.depot_in_middle, 14 22 | ) 23 | return (self.main.close_enemy_production or self.main.close_enemies_to_base) and not any( 24 | enemy.distance_to(self.placement_position) < 13 for enemy in self.main.enemies 25 | ) 26 | 27 | async def handle(self): 28 | """Build the spines on the first base near the ramp in case there is a proxy""" 29 | await self.main.build(UnitTypeId.SPINECRAWLER, near=self.placement_position) 30 | -------------------------------------------------------------------------------- /actions/macro/build/spire_construction.py: -------------------------------------------------------------------------------- 1 | """Everything related to building logic for the spires goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class SpireConstruction: 6 | """Untested""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Build the spire if only floating buildings left""" 13 | return ( 14 | self.main.can_build_unique(UnitTypeId.SPIRE, self.main.spires) 15 | and self.main.floated_buildings_bm 16 | and self.main.upgraded_base 17 | ) 18 | 19 | async def handle(self): 20 | """Places the spire""" 21 | await self.main.place_building(UnitTypeId.SPIRE) 22 | -------------------------------------------------------------------------------- /actions/macro/build/spore_construction.py: -------------------------------------------------------------------------------- 1 | """Everything related to building logic for the spores goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class SporeConstruction: 6 | """Ok for now""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Requirements build the spores""" 13 | spore_building_trigger = ( 14 | self.main.flying_enemies 15 | and not (len(self.main.spores) > self.main.ready_base_amount or self.main.close_enemies_to_base) 16 | and (au for au in self.main.flying_enemies if au.can_attack_ground) 17 | ) 18 | if self.main.ready_bases: 19 | return ( 20 | (spore_building_trigger or self.main.time >= 420) 21 | and not self.main.already_pending(UnitTypeId.SPORECRAWLER) 22 | and self.main.building_requirements(UnitTypeId.SPORECRAWLER, self.main.settled_pool) 23 | ) 24 | 25 | async def handle(self): 26 | """Build the spore right on the middle of the base""" 27 | for base in self.main.ready_bases: 28 | spore_position = self.main.state.resources.closer_than(10, base).center.towards(base, 1) 29 | selected_drone = self.main.select_build_worker(spore_position) 30 | if ( 31 | not self.main.ground_enemies.closer_than(20, spore_position) 32 | and selected_drone 33 | and not self.main.spores.closer_than(15, spore_position) 34 | ): 35 | self.main.add_action(selected_drone.build(UnitTypeId.SPORECRAWLER, spore_position)) 36 | -------------------------------------------------------------------------------- /actions/macro/build/transformation_to_hive.py: -------------------------------------------------------------------------------- 1 | """Everything related to building logic for the hives goes here""" 2 | from sc2.constants import AbilityId, UnitTypeId 3 | 4 | 5 | class TransformationToHive: 6 | """Maybe can be improved""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | self.idle_lairs = None 11 | 12 | async def should_handle(self): 13 | """Requirement to build the hive, maybe its too greedy maybe we should raise the lock for it""" 14 | self.idle_lairs = self.main.lairs.ready.idle 15 | return self.idle_lairs and self.main.can_build_unique( 16 | UnitTypeId.HIVE, self.main.caverns, self.main.pits.ready 17 | ) 18 | 19 | async def handle(self): 20 | """Finishes the action of making the hive""" 21 | self.main.add_action(self.idle_lairs.first(AbilityId.UPGRADETOHIVE_HIVE)) 22 | -------------------------------------------------------------------------------- /actions/macro/build/transformation_to_lair.py: -------------------------------------------------------------------------------- 1 | """Everything related to building logic for the lairs goes here""" 2 | from sc2.constants import AbilityId, UnitTypeId 3 | 4 | 5 | class TransformationToLair: 6 | """Maybe can be improved, probably its a bit greedy it leaves a gap where the bot is vulnerable""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Requirements to build the lair""" 13 | return ( 14 | not self.main.upgraded_base 15 | and ( 16 | self.main.base_amount >= 3 17 | or (self.main.close_enemy_production and len(self.main.settled_evochamber) >= 2) 18 | ) 19 | and self.main.can_build_unique(UnitTypeId.LAIR, self.main.caverns, self.main.hatcheries.ready.idle) 20 | ) 21 | 22 | async def handle(self): 23 | """Finishes the action of making the lair choosing the safest available base""" 24 | self.main.add_action(self.main.furthest_townhall_to_center(AbilityId.UPGRADETOLAIR_LAIR)) 25 | -------------------------------------------------------------------------------- /actions/macro/buildings_positions.py: -------------------------------------------------------------------------------- 1 | """Everything related to building positioning goes here""" 2 | from sc2.constants import UnitTypeId 3 | from sc2.data import ActionResult 4 | from sc2.position import Point2 5 | 6 | 7 | class BuildingsPositions: 8 | """Ok for now""" 9 | 10 | def __init__(self): 11 | self.building_positions, self.viable_points = [], [] 12 | 13 | async def final_triage_for_viable_locations(self): 14 | """See if its possible to build an evochamber or an engineering bay at the position - checking both is needed 15 | because it runs at the main base that has creep already(evochamber can be placed) but it also runs on new 16 | bases as well and on this ones the creep doesn't exist at the position yet(engineering bay can be placed)""" 17 | e_bay_creation_ability = self._game_data.units[UnitTypeId.ENGINEERINGBAY.value].creation_ability 18 | e_bay_mask = await self._client.query_building_placement(e_bay_creation_ability, self.viable_points) 19 | evo_creation_ability = self._game_data.units[UnitTypeId.EVOLUTIONCHAMBER.value].creation_ability 20 | evo_mask = await self._client.query_building_placement(evo_creation_ability, self.viable_points) 21 | for point in [ 22 | point 23 | for i, point in enumerate(self.viable_points) 24 | if any(result == ActionResult.Success for result in [e_bay_mask[i], evo_mask[i]]) 25 | ]: 26 | if self.building_positions: 27 | if all( 28 | max(abs(already_found.x - point.x), abs(already_found.y - point.y)) >= 3 29 | for already_found in self.building_positions 30 | ): 31 | self.building_positions.append(point) 32 | else: 33 | self.building_positions.append(point) 34 | 35 | async def get_production_position(self): 36 | """Find the safest position looping through all possible ones""" 37 | for building_position in self.building_positions: 38 | if await self.can_place(UnitTypeId.EVOLUTIONCHAMBER, building_position): 39 | return building_position 40 | return None 41 | 42 | def initial_triage_for_viable_locations(self, townhall_center): 43 | """ Find all positions behind the mineral line""" 44 | surroundings = range(-11, 12) 45 | townhall_mineral_fields = self.state.mineral_field.closer_than(10, townhall_center) 46 | if townhall_mineral_fields: 47 | self.viable_points = [ 48 | point 49 | for point in ( 50 | Point2((x + townhall_center.x, y + townhall_center.y)) 51 | for x in surroundings 52 | for y in surroundings 53 | if 121 >= x * x + y * y >= 81 54 | ) 55 | if abs(point.distance_to(townhall_mineral_fields.closest_to(point)) - 3) < 0.5 56 | ] 57 | 58 | async def prepare_building_positions(self, center): 59 | """Check all possible positions behind the mineral line when a hatchery is built""" 60 | self.initial_triage_for_viable_locations(center) 61 | await self.final_triage_for_viable_locations() 62 | -------------------------------------------------------------------------------- /actions/macro/train/__init__.py: -------------------------------------------------------------------------------- 1 | """ Group all classes from macro/train""" 2 | from . import ( 3 | drone_creation, 4 | hydra_creation, 5 | mutalisk_creation, 6 | overlord_creation, 7 | overseer_creation, 8 | queen_creation, 9 | ultralisk_creation, 10 | zergling_creation, 11 | ) 12 | 13 | 14 | def get_train_commands(cmd): 15 | """ Getter for all commands from macro/train""" 16 | return ( 17 | drone_creation.DroneCreation(cmd), 18 | hydra_creation.HydraliskCreation(cmd), 19 | mutalisk_creation.MutaliskCreation(cmd), 20 | overlord_creation.OverlordCreation(cmd), 21 | overseer_creation.OverseerCreation(cmd), 22 | queen_creation.QueenCreation(cmd), 23 | ultralisk_creation.UltraliskCreation(cmd), 24 | zergling_creation.ZerglingCreation(cmd), 25 | ) 26 | -------------------------------------------------------------------------------- /actions/macro/train/drone_creation.py: -------------------------------------------------------------------------------- 1 | """Everything related to training drones goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class DroneCreation: 6 | """Needs improvements, its very greedy sometimes""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | self.workers_total = None 11 | 12 | async def should_handle(self): 13 | """Should this action be handled, needs more smart limitations, its very greedy sometimes""" 14 | self.workers_total = self.main.drone_amount 15 | extractors = self.main.extractors # to save lines 16 | if self.general_drone_requirements: 17 | if self.drone_building_on_beginning: 18 | return True 19 | workers_optimal_amount = min(sum(mp.ideal_harvesters * 1.15 for mp in self.main.townhalls | extractors), 91) 20 | if self.workers_total + self.main.drones_in_queue < workers_optimal_amount: 21 | return self.main.zergling_amount >= 18 or (self.main.time >= 840 and self.main.drones_in_queue < 3) 22 | return False 23 | 24 | async def handle(self): 25 | """Execute the action of training drones""" 26 | self.main.add_action(self.main.larvae.random.train(UnitTypeId.DRONE)) 27 | 28 | @property 29 | def general_drone_requirements(self): 30 | """Constant requirements for building drones""" 31 | return ( 32 | not self.main.close_enemies_to_base 33 | and self.main.can_train(UnitTypeId.DRONE) 34 | and not self.main.counter_attack_vs_flying 35 | ) 36 | 37 | @property 38 | def drone_building_on_beginning(self): 39 | """Requirements for building drones on the early game""" 40 | if self.workers_total == 12 and not self.main.drones_in_queue: 41 | return True 42 | if self.workers_total in (13, 14, 15) and self.main.overlord_amount + self.main.ovs_in_queue > 1: 43 | return True 44 | -------------------------------------------------------------------------------- /actions/macro/train/hydra_creation.py: -------------------------------------------------------------------------------- 1 | """Everything related to training hydralisks goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class HydraliskCreation: 6 | """Ok for now""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Requirements to train the hydralisks""" 13 | if not self.main.can_train(UnitTypeId.HYDRALISK, self.main.settled_hydraden): 14 | return False 15 | if self.main.settled_cavern: 16 | return self.main.ultra_amount * 4 > self.main.hydra_amount or ( 17 | self.main.armor_three_lock and self.main.hydra_amount < 4 18 | ) 19 | return not self.main.floated_buildings_bm 20 | 21 | async def handle(self): 22 | """Execute the action of training hydras""" 23 | self.main.add_action(self.main.larvae.random.train(UnitTypeId.HYDRALISK)) 24 | -------------------------------------------------------------------------------- /actions/macro/train/mutalisk_creation.py: -------------------------------------------------------------------------------- 1 | """Everything related to training mutalisks goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class MutaliskCreation: 6 | """Untested""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Requirements to train mutalisks, maybe some locks are needed""" 13 | return self.main.can_train(UnitTypeId.MUTALISK, self.main.spires.ready) 14 | 15 | async def handle(self): 16 | """Execute the action of training mutas""" 17 | self.main.add_action(self.main.larvae.random.train(UnitTypeId.MUTALISK)) 18 | -------------------------------------------------------------------------------- /actions/macro/train/overlord_creation.py: -------------------------------------------------------------------------------- 1 | """Everything related to training overlords goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class OverlordCreation: 6 | """Should be improved""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """We still get supply blocked sometimes, can be improved a lot still""" 13 | if self.main.supply_cap < 200 and self.main.supply_left < 8 + self.main.supply_used // 7: 14 | if self.main.can_train(UnitTypeId.OVERLORD): 15 | if self.block_overlords_on_beginning: 16 | return self.main.close_enemy_production 17 | if (self.main.base_amount in (1, 2) and self.main.ovs_in_queue) or self.main.ovs_in_queue >= 3: 18 | return False 19 | return True 20 | return False 21 | return False 22 | 23 | async def handle(self): 24 | """Execute the action of training overlords""" 25 | self.main.add_action(self.main.larvae.random.train(UnitTypeId.OVERLORD)) 26 | 27 | @property 28 | def block_overlords_on_beginning(self): 29 | """ Few locks for overlords on the early game, could be replaced for a hardcoded build order list""" 30 | return ( 31 | len(self.main.drones.ready) == 14 32 | or (self.main.overlord_amount == 2 and self.main.base_amount == 1) 33 | or (self.main.base_amount == 2 and not self.main.pools) 34 | ) 35 | -------------------------------------------------------------------------------- /actions/macro/train/overseer_creation.py: -------------------------------------------------------------------------------- 1 | """Everything related to training overseers goes here""" 2 | from sc2.constants import AbilityId, UnitTypeId 3 | 4 | 5 | class OverseerCreation: 6 | """Should be expanded a little, it needs at least one more to run alongside the offensive army""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | self.random_ov = None 11 | 12 | async def should_handle(self): 13 | """Requirements to morph overseers""" 14 | overseers = self.main.overseers | self.main.units(UnitTypeId.OVERLORDCOCOON) # to save lines 15 | if self.main.overlords: 16 | self.random_ov = self.main.overlords.random 17 | return ( 18 | self.main.building_requirements(UnitTypeId.OVERSEER, self.main.upgraded_base, one_at_time=True) 19 | and len(self.main.overseers) < self.main.ready_base_amount 20 | and (not overseers or self.random_ov.distance_to(overseers.closest_to(self.random_ov)) > 10) 21 | ) 22 | 23 | async def handle(self): 24 | """Morph the overseer""" 25 | self.main.add_action(self.random_ov(AbilityId.MORPH_OVERSEER)) 26 | -------------------------------------------------------------------------------- /actions/macro/train/queen_creation.py: -------------------------------------------------------------------------------- 1 | """Everything related to training queens goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class QueenCreation: 6 | """It possibly can get better but it seems good enough for now""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | self.hatcheries_and_hives = None 11 | 12 | async def should_handle(self): 13 | """Requirement for training the queens""" 14 | self.hatcheries_and_hives = self.main.townhalls.exclude_type(UnitTypeId.LAIR).idle.ready 15 | return ( 16 | not self.main.close_enemies_to_base 17 | and self.hatcheries_and_hives 18 | and len(self.main.queens) <= self.main.ready_base_amount 19 | and not self.main.already_pending(UnitTypeId.QUEEN) 20 | and self.main.can_train(UnitTypeId.QUEEN, self.main.settled_pool, larva=False) 21 | ) 22 | 23 | async def handle(self): 24 | """Execute the action of training queens""" 25 | self.main.add_action(self.hatcheries_and_hives.random.train(UnitTypeId.QUEEN)) 26 | -------------------------------------------------------------------------------- /actions/macro/train/ultralisk_creation.py: -------------------------------------------------------------------------------- 1 | """Everything related to training ultralisks goes here""" 2 | from sc2.constants import UnitTypeId, UpgradeId 3 | 4 | 5 | class UltraliskCreation: 6 | """Good for now but it might need to be changed vs particular enemy units compositions""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Requirement for training ultralisks""" 13 | if not self.main.can_train(UnitTypeId.ULTRALISK, self.main.settled_cavern): 14 | return False 15 | if self.main.second_tier_armor and not self.main.already_pending_upgrade(UpgradeId.ZERGGROUNDARMORSLEVEL3): 16 | self.main.armor_three_lock = True 17 | return False 18 | self.main.armor_three_lock = False 19 | return True 20 | 21 | async def handle(self): 22 | """Execute the action of training ultralisks""" 23 | self.main.add_action(self.main.larvae.random.train(UnitTypeId.ULTRALISK)) 24 | -------------------------------------------------------------------------------- /actions/macro/train/zergling_creation.py: -------------------------------------------------------------------------------- 1 | """Everything related to training zergling goes here""" 2 | from sc2.constants import UnitTypeId, UpgradeId 3 | 4 | 5 | class ZerglingCreation: 6 | """Ok for now""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Requirements to train zerglings, good enough for now but ratio values can probably be improved""" 13 | if self.block_production_for_upgrade or not self.main.can_train(UnitTypeId.ZERGLING, self.main.settled_pool): 14 | return False 15 | if self.train_to_avoid_mineral_overflow: 16 | return True 17 | if self.block_production_for_better_units: 18 | return False 19 | if self.main.floated_buildings_bm: 20 | return not (self.main.supply_used > 150 or len(self.main.mutalisks) * 10 <= self.main.zergling_amount) 21 | return True 22 | 23 | async def handle(self): 24 | """Execute the action of training zerglings""" 25 | self.main.add_action(self.main.larvae.random.train(UnitTypeId.ZERGLING)) 26 | 27 | @property 28 | def block_production_for_upgrade(self): 29 | """ Don't make zerglings if the zergling speed isn't done yet after 2:25""" 30 | return ( 31 | not self.main.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) 32 | and self.main.time > 145 33 | and not self.main.close_enemy_production 34 | ) 35 | 36 | @property 37 | def block_production_for_better_units(self): 38 | """ Block zerglings if there are better units to be made""" 39 | return (self.main.settled_hydraden and self.main.hydra_amount * 3 <= self.main.zergling_amount) or ( 40 | self.main.settled_cavern and self.main.ultra_amount * 8.5 <= self.main.zergling_amount 41 | ) 42 | 43 | @property 44 | def train_to_avoid_mineral_overflow(self): 45 | """ When overflowing with minerals and have a low amount of bases build zerglings""" 46 | return self.main.minerals >= 600 and self.main.ready_base_amount <= 5 47 | -------------------------------------------------------------------------------- /actions/macro/upgrades/__init__.py: -------------------------------------------------------------------------------- 1 | """ Group all classes from macro/upgrades""" 2 | from . import cavern_upgrades, evochamber_upgrades, hydraden_upgrades, spawning_pool_upgrades 3 | 4 | 5 | def get_upgrade_commands(cmd): 6 | """ Getter for all commands from macro/upgrades""" 7 | return ( 8 | cavern_upgrades.CavernUpgrades(cmd), 9 | evochamber_upgrades.EvochamberUpgrades(cmd), 10 | hydraden_upgrades.HydradenUpgrades(cmd), 11 | spawning_pool_upgrades.SpawningPoolUpgrades(cmd), 12 | ) 13 | -------------------------------------------------------------------------------- /actions/macro/upgrades/cavern_upgrades.py: -------------------------------------------------------------------------------- 1 | """Upgrading ultras special armor and speed""" 2 | from sc2.constants import AbilityId, UpgradeId 3 | 4 | 5 | class CavernUpgrades: 6 | """Ok for now""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | self.selected_research = self.available_cavern = None 11 | 12 | async def should_handle(self): 13 | """Requirements to upgrade stuff from caverns""" 14 | self.available_cavern = self.main.caverns.idle 15 | if self.main.can_upgrade( 16 | UpgradeId.CHITINOUSPLATING, AbilityId.RESEARCH_CHITINOUSPLATING, self.available_cavern 17 | ): 18 | self.selected_research = AbilityId.RESEARCH_CHITINOUSPLATING 19 | return True 20 | if self.main.can_upgrade( 21 | UpgradeId.ANABOLICSYNTHESIS, AbilityId.RESEARCH_ANABOLICSYNTHESIS, self.available_cavern 22 | ): 23 | self.selected_research = AbilityId.RESEARCH_ANABOLICSYNTHESIS 24 | return True 25 | 26 | async def handle(self): 27 | """Execute the action of upgrading ultra armor and speed""" 28 | self.main.add_action(self.available_cavern.first(self.selected_research)) 29 | -------------------------------------------------------------------------------- /actions/macro/upgrades/evochamber_upgrades.py: -------------------------------------------------------------------------------- 1 | """Upgrades made by evolution chambers""" 2 | from sc2.constants import AbilityId 3 | 4 | 5 | class EvochamberUpgrades: 6 | """Ok for now""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | self.upgrades_added = False 11 | self.upgrades_list = [ 12 | AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL1, 13 | AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL2, 14 | AbilityId.RESEARCH_ZERGMELEEWEAPONSLEVEL3, 15 | AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL1, 16 | AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL2, 17 | AbilityId.RESEARCH_ZERGGROUNDARMORLEVEL3, 18 | ] 19 | self.ranged_upgrades = { 20 | AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL1, 21 | AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL2, 22 | AbilityId.RESEARCH_ZERGMISSILEWEAPONSLEVEL3, 23 | } 24 | 25 | async def should_handle(self): 26 | """Requirements to upgrade stuff from evochambers""" 27 | return self.main.settled_evochamber 28 | 29 | async def handle(self): 30 | """Execute the action of upgrading armor, melee and ranged attacks""" 31 | if self.main.hydradens and not self.upgrades_added: 32 | self.upgrades_added = True 33 | self.upgrades_list.extend(self.ranged_upgrades) 34 | selected_evo = self.main.settled_evochamber.prefer_idle[0] 35 | available_abilities = await self.main.get_available_abilities(selected_evo) 36 | for upgrade in self.upgrades_list: 37 | if self.main.can_afford(upgrade) and upgrade in available_abilities: 38 | self.main.add_action(selected_evo(upgrade)) 39 | -------------------------------------------------------------------------------- /actions/macro/upgrades/hydraden_upgrades.py: -------------------------------------------------------------------------------- 1 | """Upgrading hydras speed and range""" 2 | from sc2.constants import AbilityId, UpgradeId 3 | 4 | 5 | class HydradenUpgrades: 6 | """Ok for now""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | self.selected_research = self.available_hydraden = None 11 | 12 | async def should_handle(self): 13 | """Requirements to upgrade stuff from hydradens""" 14 | self.available_hydraden = self.main.settled_hydraden.idle 15 | if self.main.floated_buildings_bm: 16 | return False 17 | if self.main.can_upgrade( 18 | UpgradeId.EVOLVEGROOVEDSPINES, AbilityId.RESEARCH_GROOVEDSPINES, self.available_hydraden 19 | ): 20 | self.selected_research = AbilityId.RESEARCH_GROOVEDSPINES 21 | return True 22 | if ( 23 | self.main.can_upgrade( 24 | UpgradeId.EVOLVEMUSCULARAUGMENTS, AbilityId.RESEARCH_MUSCULARAUGMENTS, self.available_hydraden 25 | ) 26 | and self.main.hydra_range 27 | ): 28 | self.selected_research = AbilityId.RESEARCH_MUSCULARAUGMENTS 29 | return True 30 | 31 | async def handle(self): 32 | """Execute the action of upgrading hydras speed and range""" 33 | self.main.add_action(self.available_hydraden.first(self.selected_research)) 34 | -------------------------------------------------------------------------------- /actions/macro/upgrades/spawning_pool_upgrades.py: -------------------------------------------------------------------------------- 1 | """Upgrading zerglings atk-speed and speed""" 2 | from sc2.constants import AbilityId, UpgradeId 3 | 4 | 5 | class SpawningPoolUpgrades: 6 | """Ok for now""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | self.selected_research = self.available_pool = None 11 | 12 | async def should_handle(self): 13 | """Requirements to upgrade stuff from pools""" 14 | self.available_pool = self.main.settled_pool.idle 15 | if self.main.can_upgrade( 16 | UpgradeId.ZERGLINGMOVEMENTSPEED, AbilityId.RESEARCH_ZERGLINGMETABOLICBOOST, self.available_pool 17 | ): 18 | self.selected_research = AbilityId.RESEARCH_ZERGLINGMETABOLICBOOST 19 | return True 20 | if ( 21 | self.main.can_upgrade( 22 | UpgradeId.ZERGLINGATTACKSPEED, AbilityId.RESEARCH_ZERGLINGADRENALGLANDS, self.available_pool 23 | ) 24 | and self.main.hives 25 | ): 26 | self.selected_research = AbilityId.RESEARCH_ZERGLINGADRENALGLANDS 27 | return True 28 | 29 | async def handle(self): 30 | """Execute the action of upgrading zergling atk-speed and speed""" 31 | self.main.add_action(self.available_pool.first(self.selected_research)) 32 | -------------------------------------------------------------------------------- /actions/micro/__init__.py: -------------------------------------------------------------------------------- 1 | """ Group all classes from unit""" 2 | from . import buildings_cancellation, micro_main, worker_distribution 3 | 4 | 5 | def get_army_and_building_commands(command): 6 | """ Getter for all commands from unit""" 7 | return ( 8 | buildings_cancellation.BuildingsCancellation(command), 9 | micro_main.ArmyControl(command), 10 | worker_distribution.WorkerDistribution(command), 11 | ) 12 | -------------------------------------------------------------------------------- /actions/micro/army_value_tables.py: -------------------------------------------------------------------------------- 1 | """Everything related to army value tables go here""" 2 | import numpy as np 3 | from sc2.constants import UnitTypeId 4 | 5 | 6 | def calculate_army_value(table, targets): 7 | """ 8 | Returns the sum of all targets unit values, if the id is unknown, add it as value 1 9 | Parameters 10 | ---------- 11 | table: Value table based on our unit type and their race 12 | targets: Close enemy threats 13 | 14 | Returns 15 | ------- 16 | The final enemy army value 17 | """ 18 | enemy_army_value = 0 19 | for enemy in targets: 20 | table.setdefault(enemy.type_id, 1) 21 | enemy_army_value += table[enemy.type_id] 22 | return enemy_army_value 23 | 24 | 25 | class ArmyValues: 26 | """Separate the enemy army values by unit and race 27 | (can be improved by tuning and considering upgrades, position, distance...might be a little to hard)""" 28 | 29 | big_counter = 2.5 30 | counter = 1.75 31 | enemy_advantage = 1.2 32 | even = 1 33 | countering = 0.5 34 | countering_a_lot = 0.2 35 | worker = 0.01 36 | 37 | def combatants_value(self, unit_position, zvalue, hvalue, uvalue): 38 | """ 39 | Calculate value for our army that is in battle 40 | Parameters 41 | ---------- 42 | unit_position: The position of the unit in question 43 | zvalue: Chosen zergling value 44 | hvalue: Chosen hydras value 45 | uvalue: Chosen ultras value 46 | 47 | Returns 48 | ------- 49 | The sum of all values(quantity * chosen values) 50 | """ 51 | return np.sum( 52 | np.array( 53 | [ 54 | len(self.main.zerglings.closer_than(13, unit_position)), 55 | len(self.main.hydras.closer_than(13, unit_position)), 56 | len(self.main.ultralisks.closer_than(13, unit_position)), 57 | ] 58 | ) 59 | * np.array([zvalue, hvalue, uvalue]) 60 | ) 61 | 62 | def enemy_protoss_value(self, unit, target_group): 63 | """ 64 | Calculates the right enemy value based on our unit type vs protoss 65 | Parameters 66 | ---------- 67 | unit: Our units 68 | target_group: Our targets 69 | 70 | Returns 71 | ------- 72 | The sum of all values(quantity * chosen values) based on our unit type 73 | """ 74 | if unit.type_id == UnitTypeId.ZERGLING: 75 | return self.protoss_value_for_zerglings(target_group) 76 | if unit.type_id == UnitTypeId.HYDRALISK: 77 | return self.protoss_value_for_hydralisks(target_group) 78 | return self.protoss_value_for_ultralisks(target_group) 79 | 80 | def enemy_terran_value(self, unit, target_group): 81 | """ 82 | Calculates the right enemy value based on our unit type vs terran 83 | Parameters 84 | ---------- 85 | unit: Our units 86 | target_group: Our targets 87 | 88 | Returns 89 | ------- 90 | The sum of all values(quantity * chosen values) based on our unit type 91 | """ 92 | if unit.type_id == UnitTypeId.ZERGLING: 93 | return self.terran_value_for_zerglings(target_group) 94 | if unit.type_id == UnitTypeId.HYDRALISK: 95 | return self.terran_value_for_hydralisks(target_group) 96 | return self.terran_value_for_ultralisks(target_group) 97 | 98 | def enemy_zerg_value(self, unit, target_group): 99 | """ 100 | Calculates the right enemy value based on our unit type vs zerg 101 | Parameters 102 | ---------- 103 | unit: Our units 104 | target_group: Our targets 105 | 106 | Returns 107 | ------- 108 | The sum of all values(quantity * chosen values) based on our unit type 109 | """ 110 | if unit.type_id == UnitTypeId.ZERGLING: 111 | return self.zerg_value_for_zerglings(target_group) 112 | if unit.type_id == UnitTypeId.HYDRALISK: 113 | return self.zerg_value_for_hydralisk(target_group) 114 | return self.zerg_value_for_ultralisks(target_group) 115 | 116 | def gathering_force_value(self, zvalue, hvalue, uvalue): 117 | """ 118 | Calculate value for our army that is gathering on the rally point 119 | Parameters 120 | ---------- 121 | zvalue: Chosen zergling value 122 | hvalue: Chosen hydras value 123 | uvalue: Chosen ultras value 124 | 125 | Returns 126 | ------- 127 | The sum of all values(quantity * chosen values) 128 | """ 129 | return np.sum( 130 | np.array([len(self.main.zerglings.ready), len(self.main.hydras.ready), len(self.main.ultralisks.ready)]) 131 | * np.array([zvalue, hvalue, uvalue]) 132 | ) 133 | 134 | def protoss_value_for_hydralisks(self, combined_enemies): 135 | """ 136 | Calculate the enemy army value for hydralisks vs protoss 137 | Parameters 138 | ---------- 139 | combined_enemies: All enemies in range 140 | 141 | Returns 142 | ------- 143 | The final enemy army value for hydralisks vs protoss after the calculations 144 | """ 145 | hydralisks_vs_protoss_table = { 146 | UnitTypeId.PHOENIX: self.countering, 147 | UnitTypeId.ORACLE: self.countering, 148 | UnitTypeId.COLOSSUS: self.counter, 149 | UnitTypeId.ADEPT: self.even, 150 | UnitTypeId.ARCHON: self.enemy_advantage, 151 | UnitTypeId.STALKER: self.even, 152 | UnitTypeId.DARKTEMPLAR: self.countering, 153 | UnitTypeId.PHOTONCANNON: self.counter, 154 | UnitTypeId.ZEALOT: self.countering, 155 | UnitTypeId.SENTRY: self.countering_a_lot, 156 | UnitTypeId.PROBE: self.worker, 157 | UnitTypeId.HIGHTEMPLAR: self.countering, 158 | UnitTypeId.CARRIER: self.big_counter, 159 | UnitTypeId.DISRUPTOR: self.enemy_advantage, 160 | UnitTypeId.IMMORTAL: self.counter, 161 | UnitTypeId.TEMPEST: self.even, 162 | UnitTypeId.VOIDRAY: self.countering, 163 | UnitTypeId.MOTHERSHIP: self.even, 164 | } 165 | return calculate_army_value(hydralisks_vs_protoss_table, combined_enemies) 166 | 167 | def protoss_value_for_ultralisks(self, combined_enemies): 168 | """ 169 | Calculate the enemy army value for ultralisks vs protoss 170 | Parameters 171 | ---------- 172 | combined_enemies: All enemies in range 173 | 174 | Returns 175 | ------- 176 | The final enemy army value for ultralisks vs protoss after the calculations 177 | """ 178 | ultralisks_vs_protoss_table = { 179 | UnitTypeId.COLOSSUS: self.countering, 180 | UnitTypeId.ADEPT: self.countering, 181 | UnitTypeId.ARCHON: self.enemy_advantage, 182 | UnitTypeId.STALKER: self.countering, 183 | UnitTypeId.DARKTEMPLAR: self.countering, 184 | UnitTypeId.PHOTONCANNON: self.even, 185 | UnitTypeId.ZEALOT: self.even, 186 | UnitTypeId.SENTRY: self.countering, 187 | UnitTypeId.PROBE: self.worker, 188 | UnitTypeId.HIGHTEMPLAR: self.countering_a_lot, 189 | UnitTypeId.DISRUPTOR: self.even, 190 | UnitTypeId.IMMORTAL: self.big_counter, 191 | } 192 | return calculate_army_value(ultralisks_vs_protoss_table, combined_enemies) 193 | 194 | def protoss_value_for_zerglings(self, combined_enemies): 195 | """ 196 | Calculate the enemy army value for zerglings vs protoss 197 | Parameters 198 | ---------- 199 | combined_enemies: All enemies in range 200 | 201 | Returns 202 | ------- 203 | The final enemy army value for zerglings vs protoss after the calculations 204 | """ 205 | zerglings_vs_protoss_table = { 206 | UnitTypeId.COLOSSUS: self.big_counter, 207 | UnitTypeId.ADEPT: self.enemy_advantage, 208 | UnitTypeId.ARCHON: self.counter, 209 | UnitTypeId.STALKER: self.countering, 210 | UnitTypeId.DARKTEMPLAR: self.even, 211 | UnitTypeId.PHOTONCANNON: self.counter, 212 | UnitTypeId.ZEALOT: self.enemy_advantage, 213 | UnitTypeId.SENTRY: self.countering, 214 | UnitTypeId.PROBE: self.worker, 215 | UnitTypeId.HIGHTEMPLAR: self.countering, 216 | UnitTypeId.DISRUPTOR: self.counter, 217 | UnitTypeId.IMMORTAL: self.enemy_advantage, 218 | } 219 | return calculate_army_value(zerglings_vs_protoss_table, combined_enemies) 220 | 221 | def terran_value_for_hydralisks(self, combined_enemies): 222 | """ 223 | Calculate the enemy army value for hydralisks vs terran 224 | Parameters 225 | ---------- 226 | combined_enemies: All enemies in range 227 | 228 | Returns 229 | ------- 230 | The final enemy army value for hydralisks vs terran after the calculations 231 | """ 232 | hydralisks_vs_terran_table = { 233 | UnitTypeId.BUNKER: self.even, 234 | UnitTypeId.HELLION: self.countering, 235 | UnitTypeId.HELLIONTANK: self.enemy_advantage, 236 | UnitTypeId.CYCLONE: self.even, 237 | UnitTypeId.GHOST: self.even, 238 | UnitTypeId.MARAUDER: self.counter, 239 | UnitTypeId.MARINE: self.countering, 240 | UnitTypeId.REAPER: self.countering, 241 | UnitTypeId.SCV: self.worker, 242 | UnitTypeId.SIEGETANKSIEGED: self.big_counter, 243 | UnitTypeId.SIEGETANK: self.enemy_advantage, 244 | UnitTypeId.THOR: self.even, 245 | UnitTypeId.VIKINGASSAULT: self.countering, 246 | UnitTypeId.BANSHEE: self.countering, 247 | UnitTypeId.BATTLECRUISER: self.even, 248 | UnitTypeId.LIBERATOR: self.counter, 249 | UnitTypeId.MEDIVAC: self.countering_a_lot, 250 | UnitTypeId.VIKINGFIGHTER: self.countering_a_lot, 251 | } 252 | return calculate_army_value(hydralisks_vs_terran_table, combined_enemies) 253 | 254 | def terran_value_for_ultralisks(self, combined_enemies): 255 | """ 256 | Calculate the enemy army value for ultralisks vs terran 257 | Parameters 258 | ---------- 259 | combined_enemies: All enemies in range 260 | 261 | Returns 262 | ------- 263 | The final enemy army value for ultralisks vs terran after the calculations 264 | """ 265 | ultralisks_vs_terran_table = { 266 | UnitTypeId.BUNKER: self.countering, 267 | UnitTypeId.HELLION: self.countering_a_lot, 268 | UnitTypeId.HELLIONTANK: self.countering, 269 | UnitTypeId.CYCLONE: self.countering, 270 | UnitTypeId.GHOST: self.counter, 271 | UnitTypeId.MARAUDER: self.enemy_advantage, 272 | UnitTypeId.MARINE: self.countering_a_lot, 273 | UnitTypeId.REAPER: self.countering_a_lot, 274 | UnitTypeId.SCV: self.worker, 275 | UnitTypeId.SIEGETANKSIEGED: self.counter, 276 | UnitTypeId.SIEGETANK: self.countering, 277 | UnitTypeId.THOR: self.counter, 278 | UnitTypeId.VIKINGASSAULT: self.countering, 279 | } 280 | return calculate_army_value(ultralisks_vs_terran_table, combined_enemies) 281 | 282 | def terran_value_for_zerglings(self, combined_enemies): 283 | """ 284 | Calculate the enemy army value for zerglings vs terran 285 | Parameters 286 | ---------- 287 | combined_enemies: All enemies in range 288 | 289 | Returns 290 | ------- 291 | The final enemy army value for zerglings vs terran after the calculations 292 | """ 293 | zerglings_vs_terran_table = { 294 | UnitTypeId.BUNKER: self.counter, 295 | UnitTypeId.HELLION: self.counter, 296 | UnitTypeId.HELLIONTANK: self.big_counter, 297 | UnitTypeId.CYCLONE: self.countering, 298 | UnitTypeId.GHOST: self.countering, 299 | UnitTypeId.MARAUDER: self.countering, 300 | UnitTypeId.MARINE: self.even, 301 | UnitTypeId.REAPER: self.countering, 302 | UnitTypeId.SCV: self.worker, 303 | UnitTypeId.SIEGETANKSIEGED: self.big_counter, 304 | UnitTypeId.SIEGETANK: self.enemy_advantage, 305 | UnitTypeId.THOR: self.countering, 306 | UnitTypeId.VIKINGASSAULT: self.countering, 307 | } 308 | return calculate_army_value(zerglings_vs_terran_table, combined_enemies) 309 | 310 | def zerg_value_for_hydralisk(self, combined_enemies): 311 | """ 312 | Calculate the enemy army value for hydralisks vs zerg 313 | Parameters 314 | ---------- 315 | combined_enemies: All enemies in range 316 | 317 | Returns 318 | ------- 319 | The final enemy army value for hydralisks vs zerg after the calculations 320 | """ 321 | hydralisks_vs_zerg_table = { 322 | UnitTypeId.LARVA: 0, 323 | UnitTypeId.QUEEN: self.even, 324 | UnitTypeId.ZERGLING: self.even, 325 | UnitTypeId.BANELING: self.counter, 326 | UnitTypeId.ROACH: self.even, 327 | UnitTypeId.RAVAGER: self.even, 328 | UnitTypeId.HYDRALISK: self.even, 329 | UnitTypeId.LURKERMP: self.countering, 330 | UnitTypeId.DRONE: self.worker, 331 | UnitTypeId.LURKERMPBURROWED: self.big_counter, 332 | UnitTypeId.INFESTOR: self.countering, 333 | UnitTypeId.INFESTEDTERRAN: self.countering, 334 | UnitTypeId.INFESTEDTERRANSEGG: self.countering_a_lot, 335 | UnitTypeId.SWARMHOSTMP: self.countering_a_lot, 336 | UnitTypeId.LOCUSTMP: self.even, 337 | UnitTypeId.ULTRALISK: self.big_counter, 338 | UnitTypeId.SPINECRAWLER: self.even, 339 | UnitTypeId.LOCUSTMPFLYING: self.countering, 340 | UnitTypeId.OVERLORD: 0, 341 | UnitTypeId.OVERSEER: 0, 342 | UnitTypeId.MUTALISK: self.countering, 343 | UnitTypeId.CORRUPTOR: 0, 344 | UnitTypeId.VIPER: self.countering, 345 | UnitTypeId.BROODLORD: self.even, 346 | UnitTypeId.BROODLING: self.countering, 347 | } 348 | return calculate_army_value(hydralisks_vs_zerg_table, combined_enemies) 349 | 350 | def zerg_value_for_ultralisks(self, combined_enemies): 351 | """ 352 | Calculate the enemy army value for ultralisks vs zerg 353 | Parameters 354 | ---------- 355 | combined_enemies: All enemies in range 356 | 357 | Returns 358 | ------- 359 | The final enemy army value for ultralisks vs zerg after the calculations 360 | """ 361 | ultralisks_vs_zerg_table = { 362 | UnitTypeId.LARVA: 0, 363 | UnitTypeId.QUEEN: self.countering, 364 | UnitTypeId.ZERGLING: self.countering_a_lot, 365 | UnitTypeId.BANELING: self.countering_a_lot, 366 | UnitTypeId.ROACH: self.countering, 367 | UnitTypeId.RAVAGER: self.countering, 368 | UnitTypeId.HYDRALISK: self.countering, 369 | UnitTypeId.LURKERMP: self.countering, 370 | UnitTypeId.DRONE: self.worker, 371 | UnitTypeId.LURKERMPBURROWED: self.counter, 372 | UnitTypeId.INFESTOR: self.countering, 373 | UnitTypeId.INFESTEDTERRAN: self.countering, 374 | UnitTypeId.INFESTEDTERRANSEGG: self.countering_a_lot, 375 | UnitTypeId.SWARMHOSTMP: self.countering_a_lot, 376 | UnitTypeId.LOCUSTMP: self.counter, 377 | UnitTypeId.ULTRALISK: self.even, 378 | UnitTypeId.SPINECRAWLER: self.even, 379 | UnitTypeId.BROODLING: self.countering, 380 | } 381 | return calculate_army_value(ultralisks_vs_zerg_table, combined_enemies) 382 | 383 | def zerg_value_for_zerglings(self, combined_enemies): 384 | """ 385 | Calculate the enemy army value for zerglings vs zerg 386 | Parameters 387 | ---------- 388 | combined_enemies: All enemies in range 389 | 390 | Returns 391 | ------- 392 | The final enemy army value for zerglings vs zerg after the calculations 393 | """ 394 | zerglings_vs_zerg_table = { 395 | UnitTypeId.LARVA: 0, 396 | UnitTypeId.QUEEN: self.even, 397 | UnitTypeId.ZERGLING: self.even, 398 | UnitTypeId.BANELING: self.enemy_advantage, 399 | UnitTypeId.ROACH: self.even, 400 | UnitTypeId.RAVAGER: self.even, 401 | UnitTypeId.HYDRALISK: self.even, 402 | UnitTypeId.LURKERMP: self.even, 403 | UnitTypeId.DRONE: self.worker, 404 | UnitTypeId.LURKERMPBURROWED: self.big_counter, 405 | UnitTypeId.INFESTOR: self.countering, 406 | UnitTypeId.INFESTEDTERRAN: self.even, 407 | UnitTypeId.INFESTEDTERRANSEGG: self.countering_a_lot, 408 | UnitTypeId.SWARMHOSTMP: self.countering, 409 | UnitTypeId.LOCUSTMP: self.counter, 410 | UnitTypeId.ULTRALISK: self.big_counter, 411 | UnitTypeId.SPINECRAWLER: self.counter, 412 | UnitTypeId.BROODLING: self.even, 413 | } 414 | return calculate_army_value(zerglings_vs_zerg_table, combined_enemies) 415 | -------------------------------------------------------------------------------- /actions/micro/buildings_cancellation.py: -------------------------------------------------------------------------------- 1 | """Everything related to cancelling buildings goes here""" 2 | from sc2.constants import AbilityId, UnitTypeId 3 | 4 | 5 | class BuildingsCancellation: 6 | """Ok for now""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Requirements for cancelling the building""" 13 | return self.main.time < 300 if self.main.close_enemy_production else self.main.structures.not_ready 14 | 15 | async def handle(self): 16 | """Cancel the threatened building adapted from Burny's bot""" 17 | for building in self.main.structures.not_ready.exclude_type(self.main.tumors): 18 | build_progress = building.build_progress 19 | relative_health = building.health_percentage 20 | if (relative_health < build_progress - 0.5 or relative_health < 0.05 and build_progress > 0.1) or ( 21 | building.type_id == UnitTypeId.HATCHERY and self.main.close_enemy_production 22 | ): 23 | self.main.add_action((building(AbilityId.CANCEL))) 24 | -------------------------------------------------------------------------------- /actions/micro/micro_helpers.py: -------------------------------------------------------------------------------- 1 | """Every helper for controlling units go here""" 2 | from sc2 import Race 3 | from sc2.constants import UnitTypeId, EffectId 4 | from sc2.position import Point2 5 | 6 | 7 | class MicroHelpers: 8 | """Group all helpers, for unit control and targeting here""" 9 | 10 | def attack_close_target(self, unit, enemies): 11 | """ 12 | It targets lowest hp units on its range, if there is any attack the closest 13 | Parameters 14 | ---------- 15 | unit: Unit from the attacking force 16 | enemies: All enemy targets 17 | 18 | Returns 19 | ------- 20 | True and the action(attack low hp enemy or any in range) if it meets the conditions 21 | """ 22 | close_targets = enemies.subgroup(target for target in enemies if unit.target_in_range(target)) 23 | if close_targets: 24 | self.main.add_action(unit.attack(self.find_closest_lowest_hp(unit, close_targets))) 25 | return True 26 | closest_target = enemies.closest_to(unit) 27 | if closest_target: 28 | self.main.add_action(unit.attack(closest_target)) 29 | return True 30 | return None 31 | 32 | def attack_start_location(self, unit): 33 | """ 34 | It tell to attack the starting location 35 | Parameters 36 | ---------- 37 | unit: Unit from the attacking force 38 | 39 | Returns 40 | ------- 41 | True and the action(attack the starting enemy location) if it meets the conditions 42 | """ 43 | if self.main.enemy_start_locations and not self.main.enemy_structures: 44 | self.main.add_action(unit.attack(self.main.enemy_start_locations[0])) 45 | return True 46 | return False 47 | 48 | @staticmethod 49 | def find_closest_lowest_hp(unit, enemies): 50 | """Find the closest within the lowest hp enemies""" 51 | return enemies.filter(lambda x: x.health == min(enemy.health for enemy in enemies)).closest_to(unit) 52 | 53 | def avoid_effects(self, unit): 54 | """Dodge any effects""" 55 | if not self.main.state.effects or unit.type_id == UnitTypeId.ULTRALISK: 56 | return False 57 | effects_radius = { 58 | EffectId.PSISTORMPERSISTENT: 1.5, 59 | EffectId.THERMALLANCESFORWARD: 0.3, 60 | EffectId.NUKEPERSISTENT: 8, 61 | EffectId.BLINDINGCLOUDCP: 2, 62 | EffectId.RAVAGERCORROSIVEBILECP: 0.5, 63 | EffectId.LURKERMP: 0.3, 64 | } # Exchange it for '.radius' when the data gets implemented 65 | ignored_effects = ( 66 | EffectId.SCANNERSWEEP, 67 | EffectId.GUARDIANSHIELDPERSISTENT, 68 | EffectId.LIBERATORTARGETMORPHDELAYPERSISTENT, 69 | EffectId.LIBERATORTARGETMORPHPERSISTENT, 70 | ) # Placeholder(must find better way to handle some of these) 71 | for effect in self.main.state.effects: 72 | if effect.id in ignored_effects: 73 | continue 74 | danger_zone = effects_radius[effect.id] + unit.radius + 0.4 75 | if unit.position.distance_to_closest(effect.positions) > danger_zone: 76 | break 77 | perimeter_of_effect = Point2.center(effect.positions).furthest(list(unit.position.neighbors8)) 78 | self.main.add_action(unit.move(perimeter_of_effect.towards(unit.position, -danger_zone))) 79 | return True 80 | return False 81 | 82 | def avoid_disruptor_shots(self, unit): 83 | """ 84 | If the enemy has disruptor's, run a dodging code. Exclude ultralisks 85 | Parameters 86 | ---------- 87 | unit: Unit from the attacking force 88 | 89 | Returns 90 | ------- 91 | True and the action(dodge the shot) if it meets the conditions 92 | """ 93 | if unit.type_id == UnitTypeId.ULTRALISK: 94 | return False 95 | for disruptor_ball in self.main.enemies.filter( 96 | lambda enemy: enemy.type_id == UnitTypeId.DISRUPTORPHASED and enemy.distance_to(unit) < 5 97 | ): 98 | self.main.add_action(unit.move(self.find_retreat_point(disruptor_ball, unit))) 99 | return True 100 | return None 101 | 102 | @staticmethod 103 | def find_pursuit_point(target, unit) -> Point2: 104 | """ 105 | Find a point towards the enemy unit 106 | Parameters 107 | ---------- 108 | unit: Unit from the attacking force 109 | target: All enemy targets 110 | 111 | Returns 112 | ------- 113 | A point that is close to the target 114 | """ 115 | difference = unit.position - target.position 116 | return Point2((unit.position.x + (difference.x / 2) * -1, unit.position.y + (difference.y / 2) * -1)) 117 | 118 | @staticmethod 119 | def find_retreat_point(target, unit) -> Point2: 120 | """ 121 | Find a point away from the enemy unit 122 | Parameters 123 | ---------- 124 | unit: Unit from the attacking force 125 | target: All enemy targets 126 | 127 | Returns 128 | ------- 129 | A point that is far from the target 130 | """ 131 | difference = unit.position - target.position 132 | return Point2((unit.position.x + (difference.x / 2), unit.position.y + (difference.y / 2))) 133 | 134 | async def handling_walls_and_attacking(self, unit, target): 135 | """ 136 | It micros normally if no wall, if there is one attack it 137 | (can be improved, it does whats expected but its a regression overall when there is no walls) 138 | Parameters 139 | ---------- 140 | unit: Unit from the attacking force 141 | target: All enemy targets 142 | 143 | Returns 144 | ------- 145 | True and the action(attack closest or overall micro logic) if it meets the conditions 146 | """ 147 | if await self.main._client.query_pathing(unit, target.closest_to(unit).position): 148 | if unit.type_id == UnitTypeId.ZERGLING: 149 | return self.microing_zerglings(unit, target) 150 | self.main.add_action(unit.attack(target.closest_to(unit.position))) 151 | return True 152 | if self.main.enemies.not_flying: 153 | self.main.add_action(unit.attack(self.main.enemies.not_flying.closest_to(unit.position))) 154 | return True 155 | 156 | def hit_and_run(self, target, unit, attack_trigger, run_trigger): 157 | """ 158 | Attack when the unit can, run while it can't. We outrun the enemy. 159 | Parameters 160 | ---------- 161 | target: All enemy targets 162 | unit: Unit from the attacking force 163 | attack_trigger: Weapon cooldown trigger value for attacking 164 | run_trigger: Weapon cooldown trigger value for running away 165 | 166 | Returns 167 | ------- 168 | True always, along side the actions to attack or to run 169 | """ 170 | # Only do this when our range > enemy range, our move speed > enemy move speed, and enemy is targeting us. 171 | our_range = unit.ground_range 172 | partial_enemy_range = target.ground_range 173 | if not partial_enemy_range: # If target is melee it returns None so to avoid crashes we convert it to integer 174 | partial_enemy_range = 0 175 | enemy_range = partial_enemy_range + target.radius 176 | # Our unit should stay just outside enemy range, and inside our range. 177 | if enemy_range: 178 | minimum_distance = enemy_range + unit.radius + 0.01 179 | else: 180 | minimum_distance = our_range - unit.radius 181 | if minimum_distance > our_range: # Check to make sure this range isn't negative. 182 | minimum_distance = our_range - unit.radius - 0.01 183 | # If our unit is in that range, and our attack is at least halfway off cooldown, attack. 184 | if minimum_distance <= unit.distance_to(target) <= our_range and unit.weapon_cooldown <= attack_trigger: 185 | self.main.add_action(unit.attack(target)) 186 | return True 187 | # If our unit is too close, or our weapon is on more than a quarter cooldown, run away. 188 | if unit.distance_to(target) < minimum_distance or unit.weapon_cooldown > run_trigger: 189 | self.main.add_action(unit.move(self.find_retreat_point(target, unit))) 190 | return True 191 | self.main.add_action(unit.move(self.find_pursuit_point(target, unit))) # If our unit is too far, run towards. 192 | return True 193 | 194 | def move_low_hp(self, unit, enemies): 195 | """Move to enemy with lowest HP""" 196 | self.main.add_action(unit.move(self.find_closest_lowest_hp(unit, enemies))) 197 | 198 | def move_to_next_target(self, unit, enemies): 199 | """ 200 | It helps on the targeting and positioning on the attack 201 | Parameters 202 | ---------- 203 | unit: Unit from the attacking force 204 | enemies: All enemy targets 205 | 206 | Returns 207 | ------- 208 | True and the action(move to the closest low hp enemy) if it meets the conditions 209 | """ 210 | targets_in_melee_range = enemies.closer_than(1, unit) 211 | if targets_in_melee_range: 212 | self.move_low_hp(unit, targets_in_melee_range) 213 | return True 214 | return None 215 | 216 | def move_to_rallying_point(self, targets, unit): 217 | """Set the point where the units should gather""" 218 | if self.main.ready_bases: 219 | enemy_main_base = self.main.enemy_start_locations[0] 220 | rally_point = self.main.ready_bases.closest_to(enemy_main_base).position.towards(enemy_main_base, 10) 221 | if unit.position.distance_to_point2(rally_point) > 5: 222 | self.main.add_action(unit.move(rally_point)) 223 | elif targets: 224 | self.main.add_action(unit.attack(targets.closest_to(unit.position))) 225 | 226 | def retreat_unit(self, unit, target): 227 | """ 228 | Tell the unit to retreat when overwhelmed 229 | Parameters 230 | ---------- 231 | unit: Unit from the attacking force 232 | target: Enemy target 233 | 234 | Returns 235 | ------- 236 | True and the action(retreat to rally point) if it meets the conditions, False always when suffering air harass 237 | or if we being attacked(base trade logic) 238 | """ 239 | if self.main.townhalls.closer_than(15, unit) or self.main.counter_attack_vs_flying: 240 | return False 241 | if self.main.enemy_race == Race.Zerg: 242 | enemy_value = self.enemy_zerg_value(unit, target) 243 | elif self.main.enemy_race == Race.Terran: 244 | enemy_value = self.enemy_terran_value(unit, target) 245 | else: 246 | enemy_value = self.enemy_protoss_value(unit, target) 247 | if ( 248 | self.main.townhalls 249 | and not self.main.close_enemies_to_base 250 | and not self.main.structures.closer_than(7, unit.position) 251 | and enemy_value >= self.combatants_value(unit.position, 1, 5, 13) 252 | ): 253 | self.move_to_rallying_point(target, unit) 254 | self.retreat_units.add(unit.tag) 255 | return True 256 | return False 257 | 258 | def stutter_step(self, target, unit): 259 | """ 260 | Attack when the unit can, run while it can't(We don't outrun the enemy) 261 | Parameters 262 | ---------- 263 | unit: Unit from the attacking force 264 | target: Enemy target 265 | 266 | Returns 267 | ------- 268 | True always and the action(attack or retreat, its different from hit and run) if it meets the conditions 269 | """ 270 | if not unit.weapon_cooldown: 271 | self.main.add_action(unit.attack(target)) 272 | return True 273 | self.main.add_action(unit.move(self.find_retreat_point(target, unit))) 274 | return True 275 | 276 | @staticmethod 277 | def threats_on_trigger_range(targets, unit, trigger_range): 278 | """ 279 | Identify threats based on given range 280 | Parameters 281 | ---------- 282 | targets: All enemy targets 283 | unit: Unit from the attacking force 284 | trigger_range: The range from the given unit as center to check for threats within the circle 285 | 286 | Returns 287 | ------- 288 | A generator with all threats within the range 289 | """ 290 | for enemy in targets.filter(lambda target: target.distance_to(unit) < trigger_range): 291 | yield enemy 292 | -------------------------------------------------------------------------------- /actions/micro/micro_main.py: -------------------------------------------------------------------------------- 1 | """Everything related to controlling army units goes here""" 2 | from sc2.constants import UnitTypeId 3 | from actions.micro.army_value_tables import ArmyValues 4 | from actions.micro.unit.zergling_control import ZerglingControl 5 | from actions.micro.specific_units_behaviors import SpecificUnitsBehaviors 6 | 7 | 8 | class ArmyControl(ZerglingControl, SpecificUnitsBehaviors, ArmyValues): 9 | """Can be improved performance wise also few bugs on some of it's elements""" 10 | 11 | def __init__(self, main): 12 | self.main = main 13 | self.retreat_units = set() 14 | self.baneling_sacrifices = {} 15 | self.targets = self.atk_force = self.hydra_targets = None 16 | self.army_types = {UnitTypeId.ZERGLING, UnitTypeId.HYDRALISK, UnitTypeId.MUTALISK, UnitTypeId.ULTRALISK} 17 | 18 | async def should_handle(self): 19 | """Requirements to run handle""" 20 | return self.main.units.of_type(self.army_types) 21 | 22 | async def handle(self): 23 | """Run the logic for all unit types, it can be improved a lot but is already much better than a-move""" 24 | self.set_unit_groups() 25 | await self.do_or_die_prompt() 26 | for attacking_unit in self.atk_force: 27 | if self.avoid_effects(attacking_unit): 28 | continue 29 | if self.avoid_disruptor_shots(attacking_unit): 30 | continue 31 | if self.anti_proxy_prompt: 32 | if self.attack_enemy_proxy_units(attacking_unit): 33 | continue 34 | self.move_to_rallying_point(self.targets, attacking_unit) 35 | continue 36 | if self.anti_terran_bm(attacking_unit): 37 | continue 38 | if attacking_unit.tag in self.retreat_units: 39 | self.has_retreated(attacking_unit) 40 | continue 41 | if self.specific_hydra_behavior(self.hydra_targets, attacking_unit): 42 | continue 43 | if await self.specific_zergling_behavior(self.targets, attacking_unit): 44 | continue 45 | if self.target_buildings(attacking_unit): 46 | continue 47 | if not self.main.close_enemies_to_base: 48 | self.delegate_idle_unit(attacking_unit) 49 | continue 50 | if self.keep_attacking(attacking_unit): 51 | continue 52 | self.move_to_rallying_point(self.targets, attacking_unit) 53 | 54 | @property 55 | def anti_proxy_prompt(self): 56 | """Requirements for the anti-proxy logic""" 57 | return ( 58 | self.main.close_enemy_production 59 | and self.main.spines 60 | and (self.main.time <= 480 or self.main.zergling_amount <= 14) 61 | ) 62 | 63 | def anti_terran_bm(self, unit): 64 | """Logic for countering the floating buildings bm""" 65 | if self.main.flying_enemy_structures and unit.can_attack_air: 66 | self.main.add_action(unit.attack(self.main.flying_enemy_structures.closest_to(unit.position))) 67 | return True 68 | return False 69 | 70 | def attack_closest_building(self, unit): 71 | """Attack the closest enemy building""" 72 | enemy_building = self.main.enemy_structures.not_flying 73 | if enemy_building: 74 | self.main.add_action(unit.attack(enemy_building.closest_to(self.main.furthest_townhall_to_center))) 75 | 76 | def attack_enemy_proxy_units(self, unit): 77 | """Requirements to attack the proxy army if it gets too close to the ramp""" 78 | return ( 79 | self.targets 80 | and unit.type_id == UnitTypeId.ZERGLING 81 | and self.targets.closer_than(5, unit) 82 | and self.microing_zerglings(unit, self.targets) 83 | ) 84 | 85 | async def do_or_die_prompt(self): 86 | """Just something to stop it going idle, attack with everything if nothing else can be done, 87 | or rebuild the main if we can, probably won't make much difference since its very different""" 88 | if not self.main.ready_bases: 89 | if self.main.minerals < 300: 90 | for unit in self.atk_force | self.main.drones: 91 | self.main.add_action(unit.attack(self.main.enemy_start_locations[0])) 92 | else: 93 | await self.main.expand_now() 94 | 95 | def has_retreated(self, unit): 96 | """Identify if the unit has retreated(a little bugged it doesn't always clean it)""" 97 | if self.main.townhalls.closer_than(15, unit): 98 | self.retreat_units.remove(unit.tag) 99 | 100 | def delegate_idle_unit(self, unit): 101 | """ 102 | Control the idle units, by gathering then or telling then to attack 103 | Parameters 104 | ---------- 105 | unit: Unit from the attacking force 106 | 107 | Returns 108 | ------- 109 | True and the action(attack starting enemy location or retreat) if it meets the conditions 110 | """ 111 | if ( 112 | self.main.townhalls 113 | and not self.main.counter_attack_vs_flying 114 | and self.gathering_force_value(1, 2, 4) < 42 115 | and self.retreat_units 116 | ): 117 | self.move_to_rallying_point(self.targets, unit) 118 | return True 119 | if not self.main.close_enemy_production or self.main.time >= 480: 120 | if self.main.townhalls: 121 | self.attack_closest_building(unit) 122 | return self.attack_start_location(unit) 123 | return False 124 | 125 | def keep_attacking(self, unit): 126 | """ 127 | It keeps the attack going if it meets the requirements no matter what 128 | Parameters 129 | ---------- 130 | unit: Unit from the attacking force 131 | 132 | Returns 133 | ------- 134 | True and the action(just attack the closest structure or closest enemy) if it meets the conditions 135 | """ 136 | if not self.retreat_units or self.main.close_enemies_to_base: 137 | if self.main.enemy_structures: 138 | self.main.add_action(unit.attack(self.main.enemy_structures.closest_to(unit.position))) 139 | return True 140 | if self.targets: 141 | self.main.add_action(unit.attack(self.targets.closest_to(unit.position))) 142 | return True 143 | return False 144 | return False 145 | 146 | def set_unit_groups(self): 147 | """Set the targets and atk_force, separating then by type""" 148 | if self.main.enemies: 149 | excluded_units = { 150 | UnitTypeId.ADEPTPHASESHIFT, 151 | UnitTypeId.DISRUPTORPHASED, 152 | UnitTypeId.EGG, 153 | UnitTypeId.LARVA, 154 | UnitTypeId.INFESTEDTERRANSEGG, 155 | UnitTypeId.INFESTEDTERRAN, 156 | } 157 | filtered_enemies = self.main.enemies.not_structure.exclude_type(excluded_units) 158 | enemy_base_on_construction = self.main.enemy_structures.filter( 159 | lambda base: base.type_id in {UnitTypeId.NEXUS, UnitTypeId.COMMANDCENTER, UnitTypeId.HATCHERY} 160 | and 0 < base.build_progress < 1 161 | ) 162 | static_defence = self.main.enemy_structures.of_type( 163 | {UnitTypeId.SPINECRAWLER, UnitTypeId.PHOTONCANNON, UnitTypeId.BUNKER, UnitTypeId.PLANETARYFORTRESS} 164 | ) 165 | self.targets = static_defence | filtered_enemies.not_flying | enemy_base_on_construction 166 | self.hydra_targets = ( 167 | static_defence | filtered_enemies.filter(lambda unit: not unit.is_snapshot) | enemy_base_on_construction 168 | ) 169 | self.atk_force = self.main.units.of_type(self.army_types) 170 | if self.main.floated_buildings_bm and self.main.supply_used >= 199: 171 | self.atk_force = self.atk_force | self.main.queens 172 | 173 | def target_buildings(self, unit): 174 | """ 175 | Target close buildings if no other target is available 176 | Parameters 177 | ---------- 178 | unit: Unit from the attacking force 179 | 180 | Returns 181 | ------- 182 | True and the action(just attack closer structures) if it meets the conditions 183 | """ 184 | if self.main.enemy_structures.closer_than(30, unit.position): 185 | self.main.add_action(unit.attack(self.main.enemy_structures.closest_to(unit.position))) 186 | return True 187 | return False 188 | -------------------------------------------------------------------------------- /actions/micro/specific_units_behaviors.py: -------------------------------------------------------------------------------- 1 | """Everything related to units behavior changers goes here""" 2 | from sc2.constants import UnitTypeId 3 | from actions.micro.micro_helpers import MicroHelpers 4 | from actions.micro.unit.hydralisk_control import HydraControl 5 | 6 | 7 | class SpecificUnitsBehaviors(HydraControl, MicroHelpers): 8 | """Ok for now""" 9 | 10 | def specific_hydra_behavior(self, hydra_targets, unit): 11 | """ 12 | Group everything related to hydras behavior on attack 13 | Parameters 14 | ---------- 15 | hydra_targets: Targets that hydras can reach(almost everything) 16 | unit: One hydra from the attacking force 17 | 18 | Returns 19 | ------- 20 | Actions(micro or retreat) if conditions are met False if not 21 | """ 22 | if hydra_targets and unit.type_id == UnitTypeId.HYDRALISK: 23 | close_hydra_targets = hydra_targets.closer_than(15, unit.position) 24 | if close_hydra_targets: 25 | if self.retreat_unit(unit, close_hydra_targets): 26 | return True 27 | if self.microing_hydras(hydra_targets, unit): 28 | return True 29 | return False 30 | 31 | async def specific_zergling_behavior(self, targets, unit): 32 | """ 33 | Group everything related to zergling behavior on attack 34 | Parameters 35 | ---------- 36 | targets: Targets that zerglings can reach(ground units mostly) 37 | unit: One zergling from the attacking force 38 | 39 | Returns 40 | ------- 41 | Actions(micro or retreat) if conditions are met False if not 42 | """ 43 | if targets: 44 | close_targets = targets.closer_than(15, unit.position) 45 | if close_targets: 46 | if self.retreat_unit(unit, close_targets): 47 | return True 48 | if await self.handling_walls_and_attacking(unit, close_targets): 49 | return True 50 | return False 51 | -------------------------------------------------------------------------------- /actions/micro/unit/__init__.py: -------------------------------------------------------------------------------- 1 | """ Group all classes from micro/unit""" 2 | from . import changeling_control, creep_tumor, drone_control, overlord_control, overseer_control, queen_control 3 | 4 | 5 | def get_macro_units_commands(cmd): 6 | """ Getter for all commands from micro/unit""" 7 | return ( 8 | changeling_control.ChangelingControl(cmd), 9 | creep_tumor.CreepTumor(cmd), 10 | drone_control.DroneControl(cmd), 11 | overlord_control.OverlordControl(cmd), 12 | overseer_control.OverseerControl(cmd), 13 | queen_control.QueenControl(cmd), 14 | ) 15 | -------------------------------------------------------------------------------- /actions/micro/unit/changeling_control.py: -------------------------------------------------------------------------------- 1 | """Everything related to scouting with changelings goes here""" 2 | 3 | 4 | class ChangelingControl: 5 | """Ok for now""" 6 | 7 | def __init__(self, main): 8 | self.main = main 9 | 10 | async def should_handle(self): 11 | """Sends the scouting drone changelings when not playing against proxies""" 12 | return not self.main.close_enemy_production 13 | 14 | async def handle(self): 15 | """It sends all changelings to scout the bases periodically""" 16 | for changeling in self.main.changelings.idle: 17 | for point in self.main.ordered_expansions[self.main.ready_base_amount :]: 18 | self.main.add_action(changeling.move(point, queue=True)) 19 | -------------------------------------------------------------------------------- /actions/micro/unit/creep_tumor.py: -------------------------------------------------------------------------------- 1 | """Everything related to placing creep tumors using tumors goes here""" 2 | 3 | 4 | class CreepTumor: 5 | """Ok for now""" 6 | 7 | def __init__(self, main): 8 | self.main = main 9 | self.tumors = None 10 | 11 | async def should_handle(self): 12 | """Requirements to run handle""" 13 | self.tumors = self.main.tumors.tags_not_in(self.main.used_tumors) 14 | return self.tumors 15 | 16 | async def handle(self): 17 | """Place the tumor""" 18 | for tumor in self.tumors: 19 | await self.main.place_tumor(tumor) 20 | -------------------------------------------------------------------------------- /actions/micro/unit/drone_control.py: -------------------------------------------------------------------------------- 1 | """Everything related to scouting with drones goes here""" 2 | 3 | 4 | class DroneControl: 5 | """Ok for now, maybe can be replaced later for zerglings""" 6 | 7 | def __init__(self, main): 8 | self.main = main 9 | self.scout = False 10 | 11 | async def should_handle(self): 12 | """Sends the scouting drone on the beginning""" 13 | return not self.scout 14 | 15 | async def handle(self): 16 | """It sends a drone to scout the map, for bases or proxies""" 17 | selected_drone = self.main.drones.random 18 | self.scout = True 19 | for point in self.main.ordered_expansions[1:6]: 20 | self.main.add_action(selected_drone.move(point, queue=True)) 21 | for point in self.main.enemy_start_locations: 22 | self.main.add_action(selected_drone.move(point, queue=True)) 23 | -------------------------------------------------------------------------------- /actions/micro/unit/hydralisk_control.py: -------------------------------------------------------------------------------- 1 | """Everything related to controlling hydralisks""" 2 | import math 3 | from sc2.constants import BuffId 4 | from actions.micro.micro_helpers import MicroHelpers 5 | 6 | 7 | class HydraControl(MicroHelpers): 8 | """Some mistakes mostly due to values I believe, can be improved""" 9 | 10 | def check_hydra_modifiers(self, unit): 11 | """ 12 | Modifiers for hydras 13 | Parameters 14 | ---------- 15 | unit: One hydra from the attacking force 16 | 17 | Returns 18 | ------- 19 | The speed and range of the hydras after the modifiers 20 | """ 21 | our_move_speed = unit.movement_speed 22 | our_range = unit.ground_range + unit.radius 23 | if self.main.hydra_range: # If we've researched grooved spines, hydras gets 1 more range. 24 | our_range += 1 25 | if self.main.hydra_speed: # If we've researched muscular augments, our move speed is 25% more. 26 | our_move_speed *= 1.25 27 | if self.main.has_creep(unit): 28 | our_move_speed *= 1.30 # If we're on creep, it's 30% more. 29 | if unit.has_buff(BuffId.SLOW): 30 | our_move_speed *= 0.5 # If we've been hit with concussive shells, our move speed is halved. 31 | if unit.has_buff(BuffId.FUNGALGROWTH): 32 | our_move_speed *= 0.25 # If we've been hit with fungal growth, our move speed is a quarter. 33 | # movement_speed returns the speed on normal speed not fastest so x 1.4 is necessary 34 | return our_move_speed * 1.4, our_range 35 | 36 | def microing_hydras(self, targets, unit): 37 | """ 38 | Control the hydras 39 | Parameters 40 | ---------- 41 | targets: The enemy hydra targets, it groups almost every enemy unit 42 | unit: One hydra from the attacking force 43 | 44 | Returns 45 | ------- 46 | Choose which action is better for which situation(hit and run, stutter_step or attacking the closest target) 47 | """ 48 | our_move_speed, our_range = self.check_hydra_modifiers(unit) 49 | closest_threat = None 50 | closest_threat_distance = math.inf 51 | for threat in self.threats_on_trigger_range(targets, unit, 14): 52 | if threat.distance_to(unit) < closest_threat_distance: 53 | closest_threat = threat 54 | closest_threat_distance = threat.distance_to(unit) 55 | if closest_threat: 56 | enemy_range = closest_threat.ground_range 57 | if not enemy_range: 58 | enemy_range = 0 59 | if our_range > enemy_range + closest_threat.radius and our_move_speed > closest_threat.movement_speed * 1.4: 60 | return self.hit_and_run(closest_threat, unit, 6.45, 3.35) 61 | return self.stutter_step(closest_threat, unit) 62 | return self.attack_close_target(unit, targets) # If there isn't a close enemy that does damage 63 | -------------------------------------------------------------------------------- /actions/micro/unit/overlord_control.py: -------------------------------------------------------------------------------- 1 | """Everything related to controlling overlords goes here""" 2 | 3 | 4 | class OverlordControl: 5 | """Can be expanded further to spread vision better on the map and be more dynamic(run away from enemies f.e)""" 6 | 7 | def __init__(self, main): 8 | self.main = main 9 | self.first_ov_scout = self.second_ov_scout = self.third_ov_scout = False 10 | self.selected_ov = self.scout_position = self.ready_overlords = None 11 | 12 | async def should_handle(self): 13 | """Requirements to move the overlords""" 14 | self.ready_overlords = self.main.overlords.ready 15 | return self.main.overlords.ready and any( 16 | not flag for flag in (self.first_ov_scout, self.second_ov_scout, self.third_ov_scout) 17 | ) 18 | 19 | async def handle(self): 20 | """Send the ovs to the center, our natural and near our natural(to check proxies and incoming attacks)""" 21 | map_center = self.main.game_info.map_center 22 | natural = self.main.ordered_expansions[1] 23 | if not self.first_ov_scout: 24 | self.first_ov_scout = True 25 | self.selected_ov = self.ready_overlords.first 26 | self.scout_position = natural 27 | elif not self.second_ov_scout and self.main.ready_overlord_amount == 2: 28 | self.second_ov_scout = True 29 | self.selected_ov = self.ready_overlords.closest_to(self.main.furthest_townhall_to_center) 30 | self.scout_position = natural.towards(map_center, 18) 31 | elif self.second_ov_scout and not self.third_ov_scout and self.main.ready_overlord_amount == 3: 32 | self.third_ov_scout = True 33 | self.selected_ov = self.ready_overlords.closest_to(self.main.townhalls.first) 34 | self.scout_position = map_center 35 | self.main.add_action(self.selected_ov.move(self.scout_position)) 36 | -------------------------------------------------------------------------------- /actions/micro/unit/overseer_control.py: -------------------------------------------------------------------------------- 1 | """Everything related to controlling overseers goes here""" 2 | from sc2.constants import AbilityId 3 | 4 | 5 | class OverseerControl: 6 | """Can be improved a lot, make one follow the army and improve existing logic as well""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | self.overseers = None 11 | 12 | async def should_handle(self): 13 | """Requirements to move the overlords""" 14 | self.overseers = self.main.overseers 15 | return self.overseers and self.main.ready_bases 16 | 17 | async def handle(self): 18 | """It sends the overseers at the closest bases and creates changelings can be improved a lot""" 19 | for overseer in (ovs for ovs in self.overseers if ovs.distance_to(self.main.ready_bases.closest_to(ovs)) > 5): 20 | for base in (th for th in self.main.ready_bases if th.distance_to(self.overseers.closest_to(th)) > 5): 21 | self.main.add_action(overseer.move(base)) 22 | for overseer in self.overseers.filter(lambda ov: ov.energy >= 50): 23 | self.main.add_action(overseer(AbilityId.SPAWNCHANGELING_SPAWNCHANGELING)) 24 | -------------------------------------------------------------------------------- /actions/micro/unit/queen_control.py: -------------------------------------------------------------------------------- 1 | """Everything related to queen abilities and distribution goes here""" 2 | from sc2.constants import AbilityId, BuffId 3 | 4 | 5 | class QueenControl: 6 | """Can be improved(Defense not utility), cancel other orders so it can defend better""" 7 | 8 | def __init__(self, main): 9 | self.main = main 10 | 11 | async def should_handle(self): 12 | """Requirement to run the queen distribution and actions""" 13 | return ( 14 | self.main.queens 15 | and self.main.townhalls 16 | and not (self.main.floated_buildings_bm and self.main.supply_used >= 199) 17 | ) 18 | 19 | async def handle(self): 20 | """Assign a queen to each base to make constant injections and the extras for creep spread 21 | Injection and creep spread are ok, can be expanded so it accepts transfusion and micro""" 22 | await self.handle_queen_abilities() 23 | self.handle_queen_distribution() 24 | 25 | async def handle_queen_abilities(self): 26 | """Logic for queen abilities""" 27 | for queen in self.main.queens.filter(lambda qu: qu.is_idle and qu.energy >= 25): 28 | selected_base = self.main.townhalls.closest_to(queen.position) 29 | if not selected_base.has_buff(BuffId.QUEENSPAWNLARVATIMER): 30 | self.main.add_action(queen(AbilityId.EFFECT_INJECTLARVA, selected_base)) 31 | continue 32 | await self.main.place_tumor(queen) 33 | 34 | def handle_queen_distribution(self): 35 | """Logic for distributing and attacking for queens - adding transfusion would be good""" 36 | for base in self.main.ready_bases.idle: 37 | if not self.main.queens.closer_than(5, base): 38 | for queen in self.main.queens: 39 | if self.main.enemies.not_structure.closer_than(10, queen.position): 40 | self.main.add_action(queen.attack(self.main.enemies.not_structure.closest_to(queen.position))) 41 | continue 42 | if not self.main.townhalls.closer_than(5, queen): 43 | self.main.add_action(queen.move(base.position)) 44 | -------------------------------------------------------------------------------- /actions/micro/unit/zergling_control.py: -------------------------------------------------------------------------------- 1 | """Everything related to controlling zerglings""" 2 | from sc2.constants import UnitTypeId 3 | from actions.micro.micro_helpers import MicroHelpers 4 | 5 | 6 | class ZerglingControl(MicroHelpers): 7 | """Can be improved in many ways""" 8 | 9 | def dodging_banelings(self, unit, targets): 10 | """ 11 | If the enemy has banelings, run baneling dodging code. It can be improved, 12 | its a little bugged and just avoid the baneling not pop it 13 | Parameters 14 | ---------- 15 | unit: One zergling from the attacking force 16 | targets: The enemy zergling targets, it groups almost every enemy unit on the ground 17 | 18 | Returns 19 | ------- 20 | Chosen action when a baneling is near(attack, move away or pop the baneling) 21 | """ 22 | self.handling_anti_banelings_group() 23 | if self.main.enemies.of_type(UnitTypeId.BANELING): 24 | for baneling in self.baneling_group(unit, targets): 25 | # Check for close banelings and if we've triggered any banelings 26 | if baneling.distance_to(unit) < 4 and self.baneling_sacrifices: 27 | # If we've triggered this specific baneling 28 | if baneling in self.baneling_sacrifices.values(): 29 | # And this zergling is targeting it, attack it 30 | if unit in self.baneling_sacrifices and baneling == self.baneling_sacrifices[unit]: 31 | self.main.add_action(unit.attack(baneling)) 32 | return True 33 | # Otherwise, run from it. 34 | self.main.add_action(unit.move(self.find_retreat_point(baneling, unit))) 35 | return True 36 | # If this baneling is not targeted yet, trigger it. 37 | return self.triggering_banelings(unit, baneling) 38 | return self.triggering_banelings(unit, baneling) 39 | return False 40 | 41 | def baneling_group(self, unit, targets): 42 | """ 43 | Put the banelings on one group 44 | Parameters 45 | ---------- 46 | unit: One zergling from the attacking force 47 | targets: The enemy zergling targets, it groups almost every enemy unit on the ground 48 | 49 | Returns 50 | ------- 51 | A group that includes every close baneling 52 | """ 53 | threats = self.threats_on_trigger_range(targets, unit, 5) 54 | for threat in threats: 55 | if threat.type_id == UnitTypeId.BANELING: 56 | yield threat 57 | 58 | def triggering_banelings(self, unit, baneling): 59 | """ 60 | If we haven't triggered any banelings, trigger it. 61 | Parameters 62 | ---------- 63 | unit: One zergling from the attacking force 64 | baneling: A baneling from the baneling_group 65 | 66 | Returns 67 | ------- 68 | Action to pop the enemy baneling 69 | """ 70 | self.baneling_sacrifices[unit] = baneling 71 | self.main.add_action(unit.attack(baneling)) 72 | return True 73 | 74 | def handling_anti_banelings_group(self): 75 | """If the sacrificial zergling dies before the missions is over remove him from the group(needs to be fixed)""" 76 | for zergling in list(self.baneling_sacrifices): 77 | if zergling not in self.main.units() or self.baneling_sacrifices[zergling] not in self.main.enemies: 78 | del self.baneling_sacrifices[zergling] 79 | 80 | def microing_zerglings(self, unit, targets): 81 | """ 82 | Target low hp units smartly, and surrounds when attack cd is down 83 | Parameters 84 | ---------- 85 | unit: One zergling from the attacking force 86 | targets: The enemy zergling targets, it groups almost every enemy unit on the ground 87 | 88 | Returns 89 | ------- 90 | The chosen action depending on the zergling situation 91 | """ 92 | if self.dodging_banelings(unit, targets): 93 | return True 94 | if self.check_zergling_modifiers(unit, targets): 95 | return True 96 | if self.move_to_next_target(unit, targets): 97 | return True 98 | self.main.add_action(unit.attack(targets.closest_to(unit.position))) 99 | return True 100 | 101 | def check_zergling_modifiers(self, unit, targets): 102 | """ 103 | Modifiers for zerglings 104 | Parameters 105 | ---------- 106 | unit: One zergling from the attacking force 107 | targets: The enemy zergling targets, it groups almost every enemy unit on the ground 108 | Returns 109 | ------- 110 | The chosen action depending on the modifiers 111 | """ 112 | if unit.weapon_cooldown <= 8.85 or (unit.weapon_cooldown <= 6.35 and self.main.zergling_atk_spd): 113 | return self.attack_close_target(unit, targets) 114 | return self.move_to_next_target(unit, targets) 115 | -------------------------------------------------------------------------------- /actions/micro/worker_distribution.py: -------------------------------------------------------------------------------- 1 | """Everything related to distributing drones to the right resource goes here""" 2 | from itertools import repeat 3 | from sc2.constants import UnitTypeId, UpgradeId 4 | 5 | 6 | class WorkerDistribution: 7 | """Some things can be improved(mostly about the ratio mineral-vespene)""" 8 | 9 | def __init__(self, main): 10 | self.main = main 11 | self.close_mineral_fields = self.geyser_tags = self.workers_to_distribute = self.mineral_tags = None 12 | 13 | async def should_handle(self): 14 | """Requirements to run handle""" 15 | return not self.main.iteration % 4 16 | 17 | async def handle(self): 18 | """Groups the resulting actions from all functions below""" 19 | self.distribute_idle_workers() 20 | self.switch_to_vespene() 21 | self.switch_to_minerals() 22 | self.distribute_to_bases() 23 | 24 | def calculate_distribution(self): 25 | """Calculate the ideal distribution for workers""" 26 | unsaturated_bases = self.main.ready_bases.filter(lambda base: base.ideal_harvesters > 0) 27 | self.close_mineral_fields = self.main.state.mineral_field.filter( 28 | lambda field: any(field.distance_to(base) <= 8 for base in unsaturated_bases) 29 | ) 30 | self.mineral_tags = {mf.tag for mf in self.main.state.mineral_field} 31 | self.geyser_tags = {ref.tag for ref in self.main.extractors} 32 | self.workers_to_distribute = self.main.drones.idle 33 | bases_deficit = [] 34 | for mining_place in unsaturated_bases | self.main.extractors.ready: 35 | difference = mining_place.surplus_harvesters 36 | if difference > 0: 37 | self.fill_workers_to_distribute(mining_place, difference) 38 | elif difference < 0: 39 | bases_deficit.append([mining_place, difference]) 40 | return bases_deficit, self.workers_to_distribute 41 | 42 | def distribute_idle_workers(self): 43 | """If the worker is idle send to the closest mineral""" 44 | if self.close_mineral_fields: 45 | for drone in self.main.drones.idle: 46 | self.main.add_action(drone.gather(self.close_mineral_fields.closest_to(drone))) 47 | 48 | def distribute_to_bases(self): 49 | """Distribute workers so it saturates the bases""" 50 | bases_deficit, workers_to_distribute = self.calculate_distribution() 51 | bases_deficit = [x for x in bases_deficit if x[0].type_id != UnitTypeId.EXTRACTOR] 52 | if bases_deficit and workers_to_distribute: 53 | mineral_fields_deficit = self.calculate_mineral_fields_deficit(bases_deficit) 54 | extractors_deficit = [x for x in bases_deficit if x[0].type_id == UnitTypeId.EXTRACTOR] 55 | for worker in workers_to_distribute: 56 | self.distribute_to_mineral_field(mineral_fields_deficit, worker, bases_deficit) 57 | self.distribute_to_extractor(extractors_deficit, worker) 58 | 59 | def distribute_to_extractor(self, extractors_deficit, worker): 60 | """Check vespene actual saturation and when the requirement are filled saturate the geyser""" 61 | if self.main.extractors.ready and extractors_deficit and self.check_gas_requirements: 62 | self.main.add_action(worker.gather(extractors_deficit[0][0])) 63 | extractors_deficit[0][1] += 1 64 | if not extractors_deficit[0][1]: 65 | del extractors_deficit[0] 66 | 67 | def distribute_to_mineral_field(self, mineral_fields_deficit, worker, bases_deficit): 68 | """Check base actual saturation and then saturate it""" 69 | if bases_deficit and mineral_fields_deficit: 70 | if len(mineral_fields_deficit) >= 2: 71 | del mineral_fields_deficit[0] 72 | self.main.add_action(worker.gather(mineral_fields_deficit[0])) 73 | bases_deficit[0][1] += 1 74 | if not bases_deficit[0][1]: 75 | del bases_deficit[0] 76 | 77 | def fill_workers_to_distribute(self, mining_place, iterations): 78 | """Select the workers and their quantity to distribute""" 79 | for _ in repeat(None, iterations): 80 | selected_tag = self.geyser_tags if mining_place.name == "Extractor" else self.mineral_tags 81 | moving_drones = self.main.drones.filter( 82 | lambda x: x.order_target in selected_tag and x not in self.workers_to_distribute 83 | ) 84 | self.workers_to_distribute.append(moving_drones.closest_to(mining_place)) 85 | 86 | def switch_to_vespene(self): 87 | """Performs the action of sending drones to geysers""" 88 | if self.check_gas_requirements: 89 | drones_gathering_amount = len(self.main.drones.gathering) 90 | for extractor in self.main.extractors: 91 | required_drones = extractor.ideal_harvesters - extractor.assigned_harvesters 92 | if 0 < required_drones < drones_gathering_amount: 93 | for drone in self.main.drones.gathering.sorted_by_distance_to(extractor).take(required_drones): 94 | self.main.add_action(drone.gather(extractor)) 95 | 96 | def calculate_mineral_fields_deficit(self, bases_deficit): 97 | """Calculate how many workers are left to saturate the base""" 98 | return sorted( 99 | [mf for mf in self.close_mineral_fields.closer_than(8, bases_deficit[0][0])], 100 | key=lambda mineral_field: ( 101 | mineral_field.tag not in {worker.order_target for worker in self.main.drones.collecting}, 102 | mineral_field.mineral_contents, 103 | ), 104 | ) 105 | 106 | @property 107 | def check_gas_requirements(self): 108 | """One of the requirements for gas collecting""" 109 | return ( 110 | self.gas_requirements_for_speedlings 111 | or self.main.vespene * 1.5 < self.main.minerals 112 | and self.main.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) in (0, 1) 113 | ) 114 | 115 | @property 116 | def gas_requirements_for_speedlings(self): 117 | """Gas collecting on the beginning so it research zergling speed fast""" 118 | return ( 119 | len(self.main.extractors.ready) == 1 120 | and not self.main.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) 121 | and self.main.vespene < 100 122 | ) 123 | 124 | def switch_to_minerals(self): 125 | """ Transfer drones from vespene to minerals when vespene count is way to high 126 | or early when zergling speed is being upgraded""" 127 | if ( 128 | self.main.vespene > self.main.minerals * 4 129 | and self.main.minerals >= 75 130 | or self.main.already_pending_upgrade(UpgradeId.ZERGLINGMOVEMENTSPEED) not in (0, 1) 131 | ): 132 | if self.close_mineral_fields: 133 | for drone in self.main.drones.gathering.filter(lambda x: x.order_target in self.geyser_tags): 134 | self.main.add_action(drone.gather(self.close_mineral_fields.closest_to(drone))) 135 | -------------------------------------------------------------------------------- /data_containers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matuiss2/JackBot/1b45ce782df666dd21996011a04c92211c0e6368/data_containers/__init__.py -------------------------------------------------------------------------------- /data_containers/data_container.py: -------------------------------------------------------------------------------- 1 | """All global variables and triggers are grouped here""" 2 | from data_containers.special_cases import SituationalData 3 | from data_containers.our_possessions import OurPossessionsData 4 | from data_containers.ungrouped_data import OtherData 5 | 6 | 7 | class MainDataContainer(SituationalData, OurPossessionsData, OtherData): 8 | """This is the main data container for all data the bot requires""" 9 | 10 | def __init__(self): 11 | SituationalData.__init__(self) 12 | OurPossessionsData.__init__(self) 13 | OtherData.__init__(self) 14 | self.close_enemy_production = self.floated_buildings_bm = None 15 | 16 | def enemy_special_cases(self): 17 | """Pretty much makes SituationalData be updated all iterations""" 18 | self.prepare_enemy_data() 19 | self.close_enemy_production = self.check_for_proxy_buildings() 20 | self.floated_buildings_bm = self.check_for_floated_buildings() 21 | 22 | def prepare_data(self): 23 | """Prepares the data every iteration""" 24 | self.counter_attack_vs_flying = self.close_enemies_to_base = False 25 | self.initialize_our_stuff() 26 | self.initialize_enemies() 27 | self.prepare_bases_data() 28 | self.enemy_special_cases() 29 | -------------------------------------------------------------------------------- /data_containers/our_possessions.py: -------------------------------------------------------------------------------- 1 | """All data that are on our possession are here""" 2 | from sc2.constants import UnitTypeId 3 | from data_containers.our_structures import OurBuildingsData 4 | from data_containers.our_units import OurUnitsData 5 | from data_containers.quantity_data import OurQuantityData 6 | 7 | 8 | class OurPossessionsData(OurBuildingsData, OurUnitsData, OurQuantityData): 9 | """This is the data container for all our units and buildings""" 10 | 11 | def __init__(self): 12 | OurBuildingsData.__init__(self) 13 | OurUnitsData.__init__(self) 14 | self.structures = self.hatcheries = self.lairs = self.hives = self.ready_bases = self.upgraded_base = None 15 | 16 | def initialize_our_amounts(self): 17 | """Initialize the amount of everything(repeated) on our possession""" 18 | self.buildings_amounts() 19 | self.unit_amounts() 20 | self.completed_asset_amounts() 21 | self.incomplete_asset_amounts() 22 | 23 | def initialize_bases(self): 24 | """Initialize our bases""" 25 | self.hatcheries = self.units(UnitTypeId.HATCHERY) 26 | self.lairs = self.units(UnitTypeId.LAIR) 27 | self.hives = self.units(UnitTypeId.HIVE) 28 | self.upgraded_base = self.lairs or self.hives 29 | self.ready_bases = self.townhalls.ready 30 | 31 | def initialize_buildings(self): 32 | """Initialize all our buildings""" 33 | self.structures = self.units.structure 34 | self.hatchery_buildings() 35 | self.lair_buildings() 36 | self.hive_buildings() 37 | self.finished_buildings() 38 | 39 | def initialize_our_stuff(self): 40 | """Initializes our stuff""" 41 | self.initialize_units() 42 | self.initialize_buildings() 43 | self.initialize_bases() 44 | self.initialize_our_amounts() 45 | 46 | def initialize_units(self): 47 | """Initialize our units""" 48 | self.hatchery_units() 49 | self.lair_units() 50 | self.hive_units() 51 | -------------------------------------------------------------------------------- /data_containers/our_structures.py: -------------------------------------------------------------------------------- 1 | """All data that are related to structures on our possession are here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class OurBuildingsData: 6 | """This is the data container for all our buildings""" 7 | 8 | def __init__(self): 9 | self.evochambers = self.pools = self.spines = self.tumors = self.extractors = self.spores = self.spires = None 10 | self.hydradens = self.pits = self.caverns = self.settled_evochamber = self.settled_pool = None 11 | self.settled_cavern = self.settled_hydraden = None 12 | self.creep_types = {UnitTypeId.CREEPTUMORQUEEN, UnitTypeId.CREEPTUMOR, UnitTypeId.CREEPTUMORBURROWED} 13 | 14 | def hatchery_buildings(self): 15 | """Initialize all our buildings(hatchery tech)""" 16 | self.evochambers = self.units(UnitTypeId.EVOLUTIONCHAMBER) 17 | self.pools = self.units(UnitTypeId.SPAWNINGPOOL) 18 | self.spines = self.units(UnitTypeId.SPINECRAWLER) 19 | self.tumors = self.units.of_type(self.creep_types) 20 | self.extractors = self.units(UnitTypeId.EXTRACTOR) 21 | self.spores = self.units(UnitTypeId.SPORECRAWLER) 22 | self.spires = self.units(UnitTypeId.SPIRE) 23 | 24 | def hive_buildings(self): 25 | """Initialize all our buildings (hive tech)""" 26 | self.caverns = self.units(UnitTypeId.ULTRALISKCAVERN) 27 | 28 | def lair_buildings(self): 29 | """Initialize all our buildings(lair tech)""" 30 | self.hydradens = self.units(UnitTypeId.HYDRALISKDEN) 31 | self.pits = self.units(UnitTypeId.INFESTATIONPIT) 32 | 33 | def finished_buildings(self): 34 | self.settled_evochamber = self.evochambers.ready 35 | self.settled_pool = self.pools.ready 36 | self.settled_cavern = self.caverns.ready 37 | self.settled_hydraden = self.hydradens.ready 38 | -------------------------------------------------------------------------------- /data_containers/our_units.py: -------------------------------------------------------------------------------- 1 | """All data that are related to units on our possession are here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class OurUnitsData: 6 | """This is the data container for all our units""" 7 | 8 | def __init__(self): 9 | self.overlords = self.drones = self.queens = self.zerglings = self.larvae = self.overseers = None 10 | self.mutalisks = self.hydras = self.ultralisks = self.changelings = None 11 | self.changeling_types = { 12 | UnitTypeId.CHANGELING, 13 | UnitTypeId.CHANGELINGZEALOT, 14 | UnitTypeId.CHANGELINGMARINESHIELD, 15 | UnitTypeId.CHANGELINGMARINE, 16 | UnitTypeId.CHANGELINGZERGLINGWINGS, 17 | UnitTypeId.CHANGELINGZERGLING, 18 | } 19 | 20 | def hatchery_units(self): 21 | """Initialize all our buildings(hatchery tech)""" 22 | self.overlords = self.units(UnitTypeId.OVERLORD) 23 | self.drones = self.units(UnitTypeId.DRONE) 24 | self.changelings = self.units.of_type(self.changeling_types) 25 | self.queens = self.units(UnitTypeId.QUEEN) 26 | self.zerglings = self.units(UnitTypeId.ZERGLING) 27 | self.larvae = self.units(UnitTypeId.LARVA) 28 | 29 | def hive_units(self): 30 | """Initialize all our buildings (hive tech)""" 31 | self.ultralisks = self.units(UnitTypeId.ULTRALISK) 32 | 33 | def lair_units(self): 34 | """Initialize all our buildings(lair tech)""" 35 | self.overseers = self.units(UnitTypeId.OVERSEER) 36 | self.mutalisks = self.units(UnitTypeId.MUTALISK) 37 | self.hydras = self.units(UnitTypeId.HYDRALISK) 38 | -------------------------------------------------------------------------------- /data_containers/quantity_data.py: -------------------------------------------------------------------------------- 1 | """All data that are related to quantity of the stuff on our possession are here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class OurQuantityData: 6 | """This is the data container for all our stuff amounts""" 7 | 8 | def __init__(self): 9 | self.hydra_amount = self.zergling_amount = self.overlord_amount = self.ovs_in_queue = self.drone_amount = None 10 | self.ready_overlord_amount = self.ready_base_amount = self.hatcheries_in_queue = self.base_amount = None 11 | self.drones_in_queue = self.ultra_amount = None 12 | 13 | def buildings_amounts(self): 14 | """Defines the amount of buildings on our possession separating by type""" 15 | self.base_amount = len(self.townhalls) 16 | 17 | def completed_asset_amounts(self): 18 | """Defines the amount of units and buildings that are finished on our possession separating by type""" 19 | self.ready_overlord_amount = len(self.overlords.ready) 20 | self.ready_base_amount = len(self.ready_bases) 21 | 22 | def incomplete_asset_amounts(self): 23 | """Defines the amount of units and buildings that are in progress on our possession separating by type""" 24 | self.hatcheries_in_queue = self.already_pending(UnitTypeId.HATCHERY) 25 | self.ovs_in_queue = self.already_pending(UnitTypeId.OVERLORD) 26 | self.drones_in_queue = self.already_pending(UnitTypeId.DRONE) 27 | 28 | def unit_amounts(self): 29 | """Defines the amount of units on our possession separating by type""" 30 | self.hydra_amount = len(self.hydras) 31 | self.ultra_amount = len(self.ultralisks) 32 | self.zergling_amount = len(self.zerglings) 33 | self.drone_amount = self.supply_workers 34 | self.overlord_amount = len(self.overlords) 35 | -------------------------------------------------------------------------------- /data_containers/special_cases.py: -------------------------------------------------------------------------------- 1 | """All situational data are here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class SituationalData: 6 | """This is the data container for all situational stuff""" 7 | 8 | def __init__(self): 9 | self.close_enemies_to_base = self.counter_attack_vs_flying = False 10 | self.basic_production_types = {UnitTypeId.BARRACKS, UnitTypeId.GATEWAY, UnitTypeId.HATCHERY} 11 | 12 | def check_for_floated_buildings(self): 13 | """Check if some terran wants to be funny with lifting up""" 14 | return ( 15 | self.flying_enemy_structures 16 | and len(self.enemy_structures) == len(self.flying_enemy_structures) 17 | and self.time > 300 18 | ) 19 | 20 | def check_for_proxy_buildings(self): 21 | """Check if there are any proxy buildings""" 22 | return bool( 23 | self.enemy_structures.filter( 24 | lambda building: building.type_id in self.basic_production_types 25 | and building.distance_to(self.start_location) < 75 26 | ) 27 | ) 28 | 29 | def prepare_enemy_data(self): 30 | """Prepare data related to enemy units""" 31 | if self.enemies: 32 | excluded_from_flying = { 33 | UnitTypeId.OVERLORD, 34 | UnitTypeId.OVERSEER, 35 | UnitTypeId.RAVEN, 36 | UnitTypeId.OBSERVER, 37 | UnitTypeId.WARPPRISM, 38 | UnitTypeId.MEDIVAC, 39 | UnitTypeId.VIPER, 40 | UnitTypeId.CORRUPTOR, 41 | } 42 | for hatch in self.townhalls: 43 | close_enemy = self.ground_enemies.closer_than(20, hatch.position) 44 | close_enemy_flying = self.flying_enemies.filter( 45 | lambda unit: unit.type_id not in excluded_from_flying and unit.distance_to(hatch.position) < 30 46 | ) 47 | if close_enemy and not self.close_enemies_to_base: 48 | self.close_enemies_to_base = True 49 | if close_enemy_flying and not self.counter_attack_vs_flying: 50 | self.counter_attack_vs_flying = True 51 | -------------------------------------------------------------------------------- /data_containers/ungrouped_data.py: -------------------------------------------------------------------------------- 1 | """All data that couldn't be grouped(yet) are here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class OtherData: 6 | """This is the data container for all not grouped stuff""" 7 | 8 | def __init__(self): 9 | self.enemies = self.flying_enemies = self.ground_enemies = self.enemy_structures = None 10 | self.furthest_townhall_to_center = self.flying_enemy_structures = None 11 | self.worker_types = {UnitTypeId.DRONE, UnitTypeId.SCV, UnitTypeId.PROBE} 12 | 13 | def initialize_enemies(self): 14 | """Initialize everything related to enemies""" 15 | self.enemies = self.known_enemy_units 16 | self.flying_enemies = self.enemies.flying 17 | self.ground_enemies = self.enemies.not_flying.not_structure.exclude_type(self.worker_types) 18 | self.enemy_structures = self.known_enemy_structures.exclude_type(UnitTypeId.AUTOTURRET) 19 | self.flying_enemy_structures = self.enemy_structures.flying 20 | 21 | def prepare_bases_data(self): 22 | """Global variable for the furthest townhall to center""" 23 | if self.townhalls: 24 | self.furthest_townhall_to_center = self.townhalls.furthest_to(self.game_info.map_center) 25 | -------------------------------------------------------------------------------- /global_helpers.py: -------------------------------------------------------------------------------- 1 | """Every helper for the bot goes here""" 2 | from sc2.constants import UnitTypeId 3 | 4 | 5 | class Globals: 6 | """Global wrappers""" 7 | 8 | def building_requirements(self, unit_type, requirement=True, one_at_time=False): 9 | """ 10 | Global requirements for building every structure 11 | Parameters 12 | ---------- 13 | unit_type: The only mandatory parameter, the unit type id to be built 14 | requirement: The basic requirement for the unit to be built 15 | one_at_time: If True, don't build it if there is another unit of the same type pending already 16 | 17 | Returns 18 | ------- 19 | True if requirements gets met 20 | """ 21 | if one_at_time and self.already_pending(unit_type): 22 | return False 23 | return requirement and self.can_afford(unit_type) 24 | 25 | def can_build_unique(self, unit_type, building, requirement=True): 26 | """ 27 | Global requirements for building unique buildings 28 | Parameters 29 | ---------- 30 | unit_type: The unit type id to be built 31 | building: This units list, to check if its empty 32 | requirement: The basic requirement for the unit to be built 33 | 34 | Returns 35 | ------- 36 | True if requirements gets met 37 | """ 38 | return ( 39 | self.can_afford(unit_type) 40 | and not building 41 | and self.building_requirements(unit_type, requirement, one_at_time=True) 42 | ) 43 | 44 | def can_train(self, unit_type, requirement=True, larva=True): 45 | """ 46 | Global requirements for creating an unit, locks production so hive and ultra cavern gets built 47 | Parameters 48 | ---------- 49 | unit_type: The unit type id to be trained 50 | requirement: The basic requirement for the unit to be trained 51 | larva: If False the unit doesn't need larva (only queen) 52 | 53 | Returns 54 | ------- 55 | True if requirements gets met 56 | """ 57 | if self.hives and not self.caverns: 58 | return False 59 | if self.pits.ready and not self.hives and not self.already_pending(UnitTypeId.HIVE): 60 | return False 61 | return (not larva or self.larvae) and self.can_afford(unit_type) and requirement 62 | 63 | def can_upgrade(self, upgrade, research, host_building): 64 | """ 65 | Global requirements for upgrades 66 | Parameters 67 | ---------- 68 | upgrade: The upgrade id to be researched 69 | research: The ability id to make the building research 70 | host_building: The building that is hosting the research 71 | 72 | Returns 73 | ------- 74 | True if requirements gets met 75 | """ 76 | return not self.already_pending_upgrade(upgrade) and self.can_afford(research) and host_building 77 | 78 | async def place_building(self, building): 79 | """Build it behind the mineral line if there is space""" 80 | position = await self.get_production_position() 81 | if not position: 82 | return None 83 | if any(enemy.distance_to(position) < 10 for enemy in self.enemies) and not self.close_enemy_production: 84 | return None 85 | selected_drone = self.select_build_worker(position) 86 | if selected_drone: 87 | self.add_action(selected_drone.build(building, position)) 88 | -------------------------------------------------------------------------------- /ladderbots.json: -------------------------------------------------------------------------------- 1 | {"Bots": {"JackBot": {"Race": "Zerg", "Type": "Python", "RootPath": "./", "FileName": "run.py", "Debug": true}}} 2 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """SC2 zerg bot by Matuiss with help of: 2 | Thommath(made the initial creep spread code), 3 | Tweakimp(made the initial building positioning, anti-drone-rush, worker distribution, among other helps), 4 | Burny(this bot is derived from his CreepyBot, it's already very different but give credit where it's due), 5 | Helfull(the idea and implementation of this bots structure came from him, also made the initial effect dodging code), 6 | Niknoc(made the initial hydra micro code) , 7 | Turing's Ego(helped with the code cleaning)""" 8 | import sc2 9 | from sc2.constants import UnitTypeId, UpgradeId 10 | from sc2.position import Point2 11 | from actions import get_unit_commands 12 | from actions.macro.build import get_build_commands 13 | from actions.macro.build.creep_spread import CreepSpread 14 | from actions.macro.train import get_train_commands 15 | from actions.macro.buildings_positions import BuildingsPositions 16 | from actions.macro.upgrades import get_upgrade_commands 17 | from data_containers.data_container import MainDataContainer 18 | from global_helpers import Globals 19 | 20 | 21 | class JackBot(sc2.BotAI, MainDataContainer, CreepSpread, BuildingsPositions, Globals): 22 | """It makes periodic attacks with zerglings early, it goes hydras mid-game and ultras end-game""" 23 | 24 | def __init__(self): 25 | CreepSpread.__init__(self) 26 | MainDataContainer.__init__(self) 27 | BuildingsPositions.__init__(self) 28 | self.hydra_range = self.hydra_speed = self.zergling_atk_spd = self.second_tier_armor = None 29 | self.iteration = self.add_action = None 30 | self.armor_three_lock = False 31 | self.unit_commands = get_unit_commands(self) 32 | self.train_commands = get_train_commands(self) 33 | self.build_commands = get_build_commands(self) 34 | self.upgrade_commands = get_upgrade_commands(self) 35 | self.ordered_expansions = [] 36 | 37 | def on_end(self, game_result): 38 | """Prints the game result on the console on the end of each game""" 39 | print(game_result.name) 40 | 41 | async def on_building_construction_complete(self, unit): 42 | """Prepares all the building placements near a new expansion""" 43 | if unit.type_id == UnitTypeId.HATCHERY: 44 | await self.prepare_building_positions(unit.position) 45 | 46 | async def on_upgrade_complete(self, upgrade): 47 | """Optimization, it changes the flag to True for the selected finished upgrade 48 | to try to avoid the very slow already_pending_upgrade calls (it calls this flags instead)""" 49 | if upgrade == UpgradeId.EVOLVEGROOVEDSPINES: 50 | self.hydra_range = True 51 | elif upgrade == UpgradeId.EVOLVEMUSCULARAUGMENTS: 52 | self.hydra_speed = True 53 | elif upgrade == UpgradeId.ZERGLINGATTACKSPEED: 54 | self.zergling_atk_spd = True 55 | elif upgrade == UpgradeId.ZERGGROUNDARMORSLEVEL2: 56 | self.second_tier_armor = True 57 | 58 | async def on_step(self, iteration): 59 | """Group all other functions in this bot, its the main""" 60 | self.iteration = iteration 61 | self.prepare_data() 62 | self.set_game_step() 63 | actions = [] 64 | self.add_action = actions.append 65 | if not iteration: 66 | await self.prepare_building_positions(self.townhalls.first.position) 67 | await self.prepare_expansion_locations() 68 | self.split_workers_on_beginning() 69 | if self.minerals >= 50: 70 | await self.run_commands(self.train_commands) 71 | await self.run_commands(self.unit_commands) 72 | await self.run_commands(self.build_commands) 73 | if not iteration % 10 and self.minerals >= 100 and self.vespene >= 100: 74 | await self.run_commands(self.upgrade_commands) 75 | await self.do_actions(actions) 76 | 77 | async def prepare_expansion_locations(self): 78 | """Prepare all expansion locations and put it in order based on pathing distance""" 79 | start = self.start_location 80 | waypoints = [ 81 | (await self._client.query_pathing(start, point), point) 82 | for point in list(self.expansion_locations) 83 | if await self._client.query_pathing(start, point) 84 | ] # remove all None values for pathing 85 | # p1 is the expansion location - p0 is the pathing distance to the starting base 86 | self.ordered_expansions = [start] + [Point2((p[1])) for p in sorted(waypoints, key=lambda p: p[0])] 87 | 88 | @staticmethod 89 | async def run_commands(commands): 90 | """Group all requirements and execution for a class logic""" 91 | for command in commands: 92 | if await command.should_handle(): 93 | await command.handle() 94 | 95 | def set_game_step(self): 96 | """It sets the interval of frames that it will take to make the actions, depending of the game situation""" 97 | if self.ground_enemies: 98 | if len(self.ground_enemies) >= 15: 99 | self._client.game_step = 1 100 | else: 101 | self._client.game_step = 2 102 | else: 103 | self._client.game_step = 3 104 | 105 | def split_workers_on_beginning(self): 106 | """Split the workers on the beginning """ 107 | for drone in self.drones: 108 | self.add_action(drone.gather(self.state.mineral_field.closest_to(drone))) 109 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | """Run the ladder or local game""" 2 | import random 3 | import sys 4 | from sc2 import Race, Difficulty, AIBuild, run_game, maps 5 | from sc2.player import Bot, Computer 6 | from __init__ import run_ladder_game 7 | from main import JackBot 8 | 9 | if __name__ == "__main__": 10 | if "--LadderServer" in sys.argv: 11 | # Ladder game started by LadderManager 12 | print("Starting ladder game...") 13 | run_ladder_game(Bot(Race.Zerg, JackBot())) 14 | else: 15 | # Local game 16 | while True: 17 | MAP = random.choice(["BlueshiftLE", "KairosJunctionLE", "ParaSiteLE", "PortAleksanderLE"]) 18 | BUILD = random.choice([AIBuild.Macro, AIBuild.Rush, AIBuild.Timing, AIBuild.Power, AIBuild.Air]) 19 | DIFFICULTY = random.choice([Difficulty.CheatInsane, Difficulty.CheatVision, Difficulty.CheatMoney]) 20 | RACE = random.choice([Race.Protoss, Race.Zerg, Race.Terran]) 21 | """ FINISHED_SETS = { 22 | BUILD == AIBuild.Air and DIFFICULTY == Difficulty.CheatVision and RACE == Race.Protoss, 23 | BUILD == AIBuild.Air and DIFFICULTY == Difficulty.CheatMoney and RACE == Race.Terran, 24 | BUILD == AIBuild.Air and RACE == Race.Zerg, 25 | BUILD == AIBuild.Macro and RACE == Race.Terran, 26 | BUILD == AIBuild.Macro and DIFFICULTY == Difficulty.CheatInsane and RACE == Race.Zerg, 27 | BUILD == AIBuild.Power and DIFFICULTY == Difficulty.CheatVision, 28 | BUILD == AIBuild.Power and DIFFICULTY == Difficulty.CheatMoney and RACE == Race.Zerg, 29 | BUILD == AIBuild.Power and DIFFICULTY == Difficulty.CheatInsane and RACE == Race.Protoss, 30 | BUILD == AIBuild.Power and DIFFICULTY == Difficulty.CheatInsane and RACE == Race.Terran, 31 | BUILD == AIBuild.Rush and RACE == Race.Terran, 32 | BUILD == AIBuild.Rush and DIFFICULTY == Difficulty.CheatMoney and RACE == Race.Protoss, 33 | BUILD == AIBuild.Rush and DIFFICULTY == Difficulty.CheatMoney and RACE == Race.Zerg, 34 | BUILD == AIBuild.Rush and DIFFICULTY == Difficulty.CheatInsane and RACE == Race.Zerg, 35 | BUILD == AIBuild.Timing and DIFFICULTY == Difficulty.CheatVision and RACE == Race.Terran, 36 | BUILD == AIBuild.Timing and DIFFICULTY == Difficulty.CheatVision and RACE == Race.Zerg, 37 | BUILD == AIBuild.Timing and DIFFICULTY == Difficulty.CheatMoney and RACE == Race.Protoss, 38 | BUILD == AIBuild.Timing and DIFFICULTY == Difficulty.CheatInsane and RACE == Race.Protoss, 39 | BUILD == AIBuild.Timing and DIFFICULTY == Difficulty.CheatInsane and RACE == Race.Zerg, 40 | } 41 | if any(FINISHED_SETS): 42 | print(f"{DIFFICULTY.name} {RACE.name} {BUILD.name} already done") 43 | continue """ 44 | break 45 | print(f"{DIFFICULTY.name} {RACE.name} {BUILD.name}") 46 | BOT = Bot(Race.Zerg, JackBot()) 47 | BUILTIN_BOT = Computer(RACE, DIFFICULTY, BUILD) 48 | run_game(maps.get(MAP), [BOT, BUILTIN_BOT], realtime=False) 49 | --------------------------------------------------------------------------------