Correlation One's Terminal Game AI Competition Introduction




writeup-Copy2

Welcome everyone to a tutorial on Terminal, a programming competition by Correlation One. It's a competition, where your goal, as a programmer, is to create an artificial intelligence to compete against other players in the same environment.

You can play in the global competition as well as in the .

Terminal is Tower Defence type of game. You have 6 types of units to deploy according to game rules, but also have limited resources to do so. Game arena is diamond-shaped, where you always ocuppy bottom part of playfield. You are playing against one other bot (bot of the other player) that occupies top part. You can deploy units on your part of arena only.

There are 2 groups of units - information ones (offensive, can be created on the edge of arena only, costs Bits) and firewall ones (defensive, can be created anywhere, costs Cores). Each of these consists of 3 types of units.

Information units include Ping (fast, weak), EMP (slow, powerful) and Scrambler (slow, strong, affects only enemy information units).

Firewall units include Filter (cheap, but can't disrupt enemy units, mostly for protecting more expensive units), Destructor (expensive, but can disrupt enemy units) and Encryptor (strengthens your information units).

You are given some additional resources every turn, but game also rewards in other ways, like successful attack). You can find all game rules on the following webpage.

First, you can try to play by hand against couple of bosses. That's an interesting way to get acquainted with game and it's mechanics/rules. Go here, on the left-hand side choose "By hand." On the righ-hand side choose any boss, probably starting with the very first one.

But we are not going to talk about playing by hand, right? We are here to write our bots and take over the universe... ekhm, I mean win.

But before we start, the are couple of requirements we have to met first:

  • Game website requires Google Chrome to work properly - make sure you're using most recent version.
  • Python 3.6.0 or later
  • Java 10 or later
  • Windows PowerShell v5 or later for Windows users (or just a console/terminal for Linux and Mac users). If you're user of Windows 7, you can download PowerShell for free and install.

Java might be a bit problematic and requires couple of things to think about. Java 10 already reached end of support, we have to download and install Java 11. Unlike Java 8 and some others, Oracle doesn't allow to download just JRE (Java Runtime Environment) anymore and we have to download JDK (Java Development Kit). JDK is a bit bigger in size, but also unlike JRE, doesn't add own path to system PATH variable during installation. For windows users - check installation dir and copy full path of bin folder (for example C:\Program Files\Java\jdk-[YOUR VERSION]\bin\) and add it to system PATH variable.

If you have Java 8 installed as well (some tools and applications still require it), make sure to add it above/before path of Java 8.

To add something to your path on Windows 10, press your windows key, type "env," choose "edit the system environment variables," then click on the "environment variables" button. Under your user variables, find Path, then click to edit, then add, and add in the path above to your latest version of Java.

PowerShell has to be configured as well. Open PowerShell with administrator privileges and run:

Set-ExecutionPolicy Bypass

Now we're almost all set. Last part of the puzzle is to download Starter Kit. Extract downloaded repository, run PowerShell or console/terminal, change current working directory to the root of Starter Kit and run command:

python scripts/run_match.py python-algo python-algo

That should run a game between two starter bots and hopefully prove that you did everything correctly.

You should see folder called python-algo inside Starter Kit folder. Create a new one and call it tutorial-bot. Copy the below files and folders from python-algo into tutorial-bot folder:

  • gamelib
  • algo.json
  • run.ps1
  • run.sh

Create file called algo_strategy.py next to the newly copied files and folders. Now we are all set to start working on our bot. Open algo_strategy.py in your IDE/Editor of your choice, it's finally time to write some code.

Let's create shell of our bot. We have to import game library and create our own class inheriting from gamelib.AlgoCore, then add the on_game_start() method and save config object passed in by game engine and add on_turn() method. At the end we have to create the object of our class and call it.

import gamelib


# Our bot
class TutorialBot(gamelib.AlgoCore):

    # Called once - on game start, before first turn, init stage
    def on_game_start(self, config):
        self.config = config

    # Called every turn
    def on_turn(self, turn_state):
        pass

# Run  our bot
if __name__ == "__main__":
    algo = TutorialBot()
    algo.start()

First we have to have some knowledge of units we can use, let's get their objects from config.

        # Get unit types
        global FILTER, ENCRYPTOR, DESTRUCTOR, PING, EMP, SCRAMBLER
        FILTER, ENCRYPTOR, DESTRUCTOR, PING, EMP, SCRAMBLER = [config["unitInformation"][index]["shorthand"] for index in range(6)]

Next, fill on_turn() method with some code. We have to acquire game state for current turn first. GameState object expects two things from us - game config object and turn state (passed by game engine into on_turn() method). Returned game_state is our center of operations, because almost all of them are being done by invoking it's methods.

        # Acquire game state for current turn
        game_state = gamelib.GameState(self.config, turn_state)

Next we want to disable warnings. Warnings are going to be printed for example when we call a method that checks IF we can create some unit and we can't for any reason. I'm not sure why, and why not when we call a method THAT ACTUALLY CREATES unit. At the moment there's also a bug - it prints error even if unit can be successfuly created.

        # Warnings have a bug, always prints out "Could not spawn xx at location",
        # also always prinsts additional reason like "Not enough resources" when checkng if we can make an action
        # We don't want that
        # Has to be set every turn, as we have to create new object of GameState every turn
        game_state.enable_warnings = False

Next we are going to call two our two methods - one that builds defenses and the second one for attacks.

        # Build defenses and attack
        self.defense(game_state)
        self.attack(game_state)

And at the end submit turn.

        # Submit turn
        game_state.submit_turn()

Our new methods.

    # Builds defenses
    def defense(self, game_state):
        pass

    # Attacks opponent
    def attack(self, game_state):
        pass

Full code up to this point.

import gamelib


# Our bot
class TutorialBot(gamelib.AlgoCore):

    # Called once - on game start, before first turn, init stage
    def on_game_start(self, config):
        self.config = config

        # Get unit types
        global FILTER, ENCRYPTOR, DESTRUCTOR, PING, EMP, SCRAMBLER, UNIT_TO_ID
        FILTER, ENCRYPTOR, DESTRUCTOR, PING, EMP, SCRAMBLER = [config["unitInformation"][index]["shorthand"] for index in range(6)]

    # Called every turn
    def on_turn(self, turn_state):

        # Acquire game state for current turn
        game_state = gamelib.GameState(self.config, turn_state)

        # Warnings have a bug, always prints out "Could not spawn xx at location",
        # also always prinsts additional reason like "Not enough resources" when checkng if we can make an action
        # We don't want that
        # Has to be set every turn, as we have to create new object of GameState every turn
        game_state.enable_warnings = False

        # Build defenses and attack
        self.defense(game_state)
        self.attack(game_state)

        # Submit turn
        game_state.submit_turn()

    # Builds defenses
    def defense(self, game_state):
        pass

    # Attacks opponent
    def attack(self, game_state):
        pass

# Run  our bot
if __name__ == "__main__":
    algo = TutorialBot()
    algo.start()

You should be able to run that code now. Run below command in PowerShell (in context of our Starter Kit folder) and check. Remember that command, as you are going to be using it a lot.

python scripts/run_match.py tutorial-bot python-algo

Let's start adding some defenses. We are going to add Filters on both edges of playfield first, then some Destructors to help in early stages of the game, then fill gaps with more Filters leaving small openning in a middle (allowing our ofessive units to pass through). When fully built, it should look like this:

python tutorials

Small circle symbolizes Filter and double circle is a Destructor.

First we have to create a method that helps us create new defensive units. We want to be able to pass a list of locations and unit type to be created. Because a lot of units occupy the same row, let's also allow that list to be just a list of x coordinates accompanied by additonal parameter - row (y) number. We'll need game_state as well. Then our new method should create units for us.

    # Builds deffesive units on the playfield
    def build_defenses(self, location_list, firewall_unit, game_state, row=None):

Now we have to iterate over our list of locations.

        # Iterate list of locations and for every location...
        for location in location_list:

At that point, as described above, single location can be either a list like [2, 15] (which is ready to use coordinate) or just an int of x coordinate, but then additional parameter named row (y) has to be set as well. In second case lets combine x and row/y into coordinate.

            # ...check if it's a list [x, y], or a number x (in case of number rebuild coordinate using additional row argument)
            if not type(location) == list:
                location = [location, row]

Next we have to check if we can create unit at given location. To do that we can use can_spawn() method that does couple of checks for us, for example if we can afford unit, if location we want to create it on is not occupied, if position is in bounds of the game arena, etc.

            # can_spawn() checks if units are affordable, location is unoccupied, position is in bounds of play area, etc
            # If we can spawn unit on given location...
            if game_state.can_spawn(firewall_unit, location):

If unit can be created, let's create it and log that information. Game library doesn't handle updating available resources when we attempt to spawn a unit. We have to handle for that by ourselves. One method to do that is just by decrease available resource counter by amount we used to create unit. Of course during next round we're going to have updated amount of resources, it's just a case of current round. Why decreasing is important? It's important for can_spawn() which otherwise will always be returning that unit is affordable if we had enough resources at start of a turn. We could create many units and use up all resources, and method won't let us know about that, because it will see still same amount of resources.

                # ...spawn unit of given type at given location, and log action
                game_state.attempt_spawn(firewall_unit, location)
                gamelib.debug_write(f'Spawning {firewall_unit} at {location}')

                # Starter kit does not descrease amount of resource on unit spawn by unit cost of that resource
                # We have to update it manually, co can_spawn() method will check agains updated amount in next loop iteration
                # 0 - our index, we are always at index 0, opponent at index 1
                game_state._player_resources[0]['cores'] -= game_state.type_cost(firewall_unit)

If we can't create unit it might mean couple of things, for example we can't afford it. In case of that we want to return and do not try to spawn any new units during this turn (to avoid creating cheaper ones when more expensive are already not affordable, we want to create units in order). There's only one exception - we don't want to break our loop in case of location being occupied. That will allow us to execute the same code every turn and rebuild our defenses. If location is occupied - ignore that and continue.

            # can_spawn() is going to return false if location is occupied as well
            # We want to break a loop if we can't spawn unit, but only for other reasons that occupied location
            # That way we can invoke the same action every turn and easly rebuild missing units
            elif not game_state.contains_stationary_unit(location):
                return False

Last thing we need here is to return True if we didn't encounter any problems, like used up all resources. Full code of the method:

    # Builds defesive units on the playfield
    def build_defenses(self, location_list, firewall_unit, game_state, row=None):

        # Iterate list of locations and for every location...
        for location in location_list:

            # ...check if it's a list [x, y], or a number x (in case of number rebuild coordinate using additional row argument)
            if not type(location) == list:
                location = [location, row]

            # can_spawn() checks if units are affordable, location is unoccupied, position is in bounds of play area, etc
            # If we can spawn unit on given location...
            if game_state.can_spawn(firewall_unit, location):

                # ...spawn unit of given type at given location, and log action
                game_state.attempt_spawn(firewall_unit, location)
                gamelib.debug_write(f'Spawning {firewall_unit} at {location}')

                # Starter kit does not descrease amount of resource on unit spawn by unit cost of that resource
                # We have to update it manually, co can_spawn() method will check agains updated amount in next loop iteration
                # 0 - our index, we are always at index 0, opponent at index 1
                game_state._player_resources[0]['cores'] -= game_state.type_cost(firewall_unit)

            # can_spawn() is going to return false if location is occupied as well
            # We want to break a loop if we can't spawn unit, but only for other reasons that occupied location
            # That way we can invoke the same action every turn and easly rebuild missing units
            elif not game_state.contains_stationary_unit(location):
                return False

        return True

Now, we can finally spawn some defenses. We're going to add code into defense() method. Let's add Filters on both sides (that's an example of situation when we are gloing to pass full coordinates to our newly created above method).

        # Side Filters
        filters = [[0, 13], [27, 13], [1, 12], [26, 12]]
        if not self.build_defenses(filters, FILTER, game_state):
            return

Then let's add some more expensive Destructors and fill gaps with more Filters.

        # Line of defense
        row = 11
        destructors = [2, 25, 6, 21, 11, 16]
        if not self.build_defenses(destructors, DESTRUCTOR, game_state, row=row):
            return
        filters = [3, 24, 4, 23, 5, 22, 7, 20, 8, 19, 9, 18, 10, 17, 12, 15]
        if not self.build_defenses(filters, FILTER, game_state, row=row):
            return

We used list of x coordinates here and passing additional row parameter to our method for greater visibility of our code.

Full code of the method:

    # Builds defenses
    def defense(self, game_state):

        # Side Filters
        filters = [[0, 13], [27, 13], [1, 12], [26, 12]]
        if not self.build_defenses(filters, FILTER, game_state):
            return

        # Line of defense
        row = 11
        destructors = [2, 25, 6, 21, 11, 16]
        if not self.build_defenses(destructors, DESTRUCTOR, game_state, row=row):
            return
        filters = [3, 24, 4, 23, 5, 22, 7, 20, 8, 19, 9, 18, 10, 17, 12, 15]
        if not self.build_defenses(filters, FILTER, game_state, row=row):
            return

What we are trying to achive here is to create units in that exact order, and we are trying to create as many of them as we can afford. That means that this code is going to be ran every turn, adding new units as long as there are still some to build. The other purpose is to rebuild units destroyed by opponent.

Full code up to this point:

import gamelib

class TutorialBot(gamelib.AlgoCore):
    def on_game_start(self, config):
        self.config = config
        global FILTER, ENCRYPTOR, DESTRUCTOR, PING, EMP, SCRAMBLER, UNIT_TO_ID
        FILTER, ENCRYPTOR, DESTRUCTOR, PING, EMP, SCRAMBLER = [config['unitInformation'][idx]["shorthand"] for idx in range(6)]

    def on_turn(self, turn_state):
        game_state = gamelib.GameState(self.config, turn_state)
        game_state.enable_warnings = False

        self.defense(game_state)
        self.attack(game_state)

        game_state.submit_turn()

    def build_defenses(self, location_list, firewall_unit, game_state, row=None):
        for location in location_list:
            if not type(location) == list:
                location = [location, row]

            if game_state.can_spawn(firewall_unit, location):
                game_state.attempt_spawn(firewall_unit, location)
                gamelib.debug_write(f"{firewall_unit} deployed at {location}")
                game_state._player_resources[0]['cores'] -= game_state.type_cost(firewall_unit)

            elif not game_state.contains_stationary_unit(location):
                return False

        return True


    def defense(self, game_state):
        filters = [[0, 13], [27, 13], [1, 12], [26, 12]]
        if not self.build_defenses(filters, FILTER, game_state):
            return

        row = 11
        destructors = [2, 25, 6, 21, 11, 16]
        if not self.build_defenses(destructors, DESTRUCTOR, game_state, row=row):
            return

        filters = [3, 24, 4, 23, 5, 22, 7, 20, 8, 19, 9, 18, 10, 17, 12, 15]
        if not self.build_defenses(filters, FILTER, game_state, row=row):
            return


    def attack(self, game_state):
        pass


if __name__ == "__main__":
    algo = TutorialBot()
    algo.start()

Now, we can either run this python scripts/run_match.py tutorial-algo python-algo, or we could zip the tutorial-algo directory, then upload via the my algos button on the nav bar of terminal.c1games.com

Things are working as intended so far. Our next step is to work on attacking.





  • Correlation One's Terminal Game AI Competition Introduction