📚Game documentation

This section will cover the information for the Game Environment.

🎲 Game Rules

Winning conditions The winner of a game is the last bot standing, or the bot that scores the most points at the end of 180 seconds (i.e. 1800 game 'ticks').

Health Points (HP) Each agent starts the game with 3 HP. You lose 1 HP each time you are within range of an exploding bomb. If you run out of HP, it's game over.

Game controls On each game 'tick' (every 100ms), you can either move up, left, down, right, place a bomb, or do nothing.

Scoring points Score points by destroying blocks, collecting treasure chests or hitting other players with bombs. Blocks can only be destroyed from the impact of exploding bombs.

Wooden blocks Takes one hit to destroy and gives +2 points to the Agent.

Ore blocks Takes three hits to destroy and gives +10 points to the Agent that makes the final hit.

Metal blocks Metal blocks are indestructible.

Treasure Chests Treasure chests spawn randomly around the map. Walk over a tile containing a treasure chest to collect +1 point.

Bombs Each Agent starts the game with 3 Ammo. Placing a bomb costs 1 Ammo. Bombs explode 3.5 seconds after they've been placed, and hits any player or blocks to the top, left, right or left-side of it with a 2-tile blast radius. Bombs can detonate early, if they are hit by another exploding bomb. Players are only impacted by the bomb if they are within range on the game tick that the bombs explode on. Hitting another player with a bomb will net you 25 Points.

Bomb ammo Ammo spawns randomly around the map. Walk over tiles containing ammo to pick them up. Ammo tokens that are not picked up will perish after 175 ticks (17.5s) and randomly respawn in a new location.

🏕️ Game Environment

Below is a simplified diagram of what happens at each 'tick' of the game (every 0.1s):

🏃‍♂️ Running the Game

Once all game dependencies properly installed outlined in the Setup guide. The game can be launched using command line:

python -m coderone.dungeon.main --help

Or alternatively:

coderone-dungeon --help

There are a number of command line options supported by the game driver. To get a full list of options run:

python -m coderone.dungeon.main

Alternatively, if the above doesn't work, you can try:

coderone-dungeon

Command Line Options Summary

The game runner recognises a number of command line options.

  • --headless - run the game without graphics. Tournament matches will be run in this mode.

  • --interactive - game is created with an extra player for the interactive user. This player can be controlled using the keyboard.

  • --watch - automatically reload the user's agent if the source code files changes. This allows for interactive development as code can be edited while the game is running.

  • --record <FILE> - record game action into a specified file for later review.

Interactive mode keys

  • Enter - to pause / un-pause the game

  • R - to restart the game with new random map

  • / / / - arrows to move the player

  • <SPACE> - to place the bomb

Game modes

There are 3 main modes to run the game:

  • Interactive mode - when a human player can participate in a match. This is a 'normal' game mode when a human user can play the game with the keyboard the game to explore it. This mode requires at least one bot.

  • Match - two (or more) AI agents play a game without a human participant.

  • 'Headless' match - the game is played by bots with no human participant and without graphics output. This is the mode used to run a tournament.

By default, game runs in a tournament mode, without user input, with graphics output. For example, to run a random match between two AI agent "agent1.py" and "agent2.py", run:

coderone-dungeon agent1 agent2   # coderone-dungeon agent1.py agent2.py also works

🤖 Agent

From here on we're referring to your bot as an agent (in keeping with AI research terminology).

Your job is to define a class Agent.

You'll need to complete the next_move method contained within your class which is called on each turn of the game and:

  • Is given information about the game environment (stored in game_state and player_state)

  • Must return an action (see '🕹️ Actions' section)

Below is what our most simple agent, the Random Agent, looks like. You are welcome to use it as a template for your agent.

import random

class Agent:
	def __init__(self, player_num, env):
		# your agent's initialization code goes here, if any
		pass

	def next_move(self, game_state, player_state):
		'''
		This method is called each time the player needs to choose an action
		'''

		actions = ['','u','d','l','r','p']
		action = random.choice(actions)

		return action

🗺️ Game Map

The map of the game is represented as below:

Each object in the game is represented by a string 'Tag':

0: Player 1 (Wizard)

1: Player 2 (Knight)

ib: Metal Block (i.e. Indestructible Block)

sb: Wooden Block (i.e. Soft Block)

ob: Ore Block

b: Bomb

a: Ammo

t: Treasure Chest

For more information on how to request information about the game environment, check out the '🎮 Game State' section.

⚠️ Note: if a player and bomb are on the same tile of the map, it will be represented as b only. To access the location of the player, use player_state.location.

🕹️ Actions

Actions are represented by a string value:

  • '': Do nothing

  • l: Move left

  • r: Move right

  • u: Move up

  • d: Move down

  • p: Place a bomb

On each game tick (i.e. every 100ms), the game will request your agent's action as one of the above values. If you do not provide an action for that tick, you agent will do nothing. You can take as long as you wish to produce an action, but the longer you take to make a move, the more the game will update around you.

🎮 Game State

All information on the current state of the game is given to your agent each turn in two objects: game_state and player_state

game_state (class object), has the following properties:

  • is_over (Boolean): whether the game has ended

  • tick_number (int): the number of turns that have passed. Each 'tick' corresponds to 100ms

  • size (tuple): the size of the game map, represented as (x,y) - (x = columns, y = rows)

  • bombs (list of tuples): list of the locations of all bombs currently on the map

  • ammo (list of tuples): list of the locations of all ammo currently on the map

  • treasure (list of tuples): list of the locations of all treasure currently on the map

  • all_blocks (list of tuples): list of the locations of all blocks (wooden, ore, metal) currently on the map

  • soft_blocks (list of tuples): list of the locations of all wooden blocks currently on the map

  • ore_blocks (list of tuples): list of the locations of all ore blocks currently on the map

  • indestructible_blocks (list of tuples): list of the locations of all indestructible metal blocks currently on the map

# Example usage
print(f"The size of the board is {game_state.size[0]+1} x {game_state.size[1]+1}")

> The size of the board is 12 x 10 
# Example usage
for bomb in game_state.bombs:
  print(f"Bomb at x:{bomb[0]} y:{bomb[1}")

> Bomb at x:0 y:1
> Bomb at x:4 y:3
> Bomb at x:2 y:9

game_state also provides the following methods:

entity_at(location): takes an input location as an (x,y) tuple and returns the tag of the entity/object at that location:

  • As an int (if the entity is a Player, i.e. 0 or 1)

  • None if empty

  • A string otherwise (e.g. sb, ob, a, etc).

# Example usage
entity = game_state.entity_at((x,y)) # entity will return 'sb', 'ob', etc.

is_in_bounds(location): takes an input location as an (x,y) tuple and returns True or False (Boolean) depending on whether that location is within the boundaries of the game map

# Example usage
is_in_bounds = is_in_bounds((x,y))

# returns TRUE if it is a legal location within the game map
# returns FALSE otherwise (i.e. it is beyond the scope of the game map)

is_occupied(location): takes an input location as an (x,y) tuple and returns True or False (Boolean) depending on whether that location is occupied (i.e. contains ammo, block, players etc.)

# Example usage
is_occupied = is_occupied((x,y))    

# returns TRUE if that location contains ammo, treasure, block, player, etc.

opponents(excluding_player_pid): takes an Agent ID as an integer and returns the current location of the opponent players as a list of tuples

# Example usage
list_of_opponents = opponents(0)       # If you are Player 1 (id = 0)

for opponent in list_of_opponents:
  print(f"Player at x:{opponent[0]} y:{opponent[1}")

> Player at x:0 y:3
> Player at x:5 y:9

player_state (class object), with the following properties:

  • id (int): your agent's player number (i.e. Player 1 = 0, Player 2 = 1, and so on)

  • ammo (int): the amount of ammo your player currently has

  • hp (int): the amount of HP your player currently has

  • location (tuple): your agent's location on the map, represented as (x,y)

  • reward (int): the total score of your agent

  • power (int): the blast-radius of the bombs placed by the player (default: 2)

# Example usage
print(f"I am currently at position {player_state.location}")

> I am currently at position (0,4)

⌛ Asynchronous Game State

The game will tick every 100ms and progress game_state regardless of whether your Agent has returned its action or not within that time (i.e. it will not wait for your Agent). If your Agent takes more than 100ms to return its move, its action will be picked up on the next tick. This can therefore lead to an asynchronous situation between what your Agent thinks the current game_state will be, versus what is actually happening.

To illustrate, here is what happens if your Agent returns its move within 100ms:

Game> start tick: 0
Game> finished tick: 0
Agent> Tick 0, my position is (8, 2)
Agent> I'm going to move: l
Game> start tick: 1
Game> finished tick: 1
Agent> Tick 1, my position is (7, 2)
Agent> I'm going to move: l
Game> start tick: 2
Game> finished tick: 2
Agent> Tick 2, my position is (6, 2)
Agent> I'm going to move: l

This is the behaviour you might expect.

Here is what can happen if your Agent takes more than 100ms to process its moves:

Game> start tick: 0
Game> finished tick: 0
Agent> Tick 0, my position is (7, 5)
Game> start tick: 1
Game> finished tick: 1
Game> start tick: 2
Game> finished tick: 2
Agent> I'm going to move: u
Agent> Tick 2, my position is (7, 5)
Game> start tick: 3
Game> finished tick: 3
Game> start tick: 4
Game> finished tick: 4
Agent> I'm going to move: d
Agent> Tick 4, my position is (7, 6)
Game> start tick: 5
Game> finished tick: 5
Game> start tick: 6
Game> finished tick: 6
Agent> I'm going to move: r
Agent> Tick 5, my position is (7, 5)
Agent> I'm going to move: r

Here, you can see the asynchrony between your Agent and the Game. The action u was issued after tick 2, and will therefore be picked up by the Game in tick 3. The game_state your Agent observes in tick 2 will therefore show you what the Game looks like without u having been executed yet. This is a limitation of how the game environment is set up to enable the game to progress without being stalled by any Agent in play.

⚙️ Configuration

On the first run the game will generate a default config file config.json and store in the OS-specific configuration directory.

The default config.json looks like this:

{
	"headless": false,
	"interactive": false,
	"start_paused": true,
	"wait_end": 5,
	"max_iterations": 3000,
	"tick_step": 0.10
}

Config notes

In your local development environment you have access to all config options, such as number of iterations the game runs (max_iterations) or game update time step (tick_step). However, these options are fixed in the tournament and can not be modified so please ensure your Agent works correctly with the default values.

Last updated