The concept of PBR is very simple. You program your bot(s) to defeat all other bots on a 2-dimensional grid field. It is a turn-based war simulation game, where each bot decides the action per each turn, and, the simulation continues until there's only one the last bot left in the game.
When the game starts, it places all participating bots in random locations on the map, and, per each game turn, it executes the code of each bots and runs the simulation in a sequential order. Each bot needs to determines which action it wants to perform for each turn:
- Move: move up/down/left/right
- Hold: hold in defensive mode
- Attack: attack another bot
- Clone: spawn a new clone of itself on the map
The system's main control flow looks like this:
- Before turn #1:
- Decided initial execution order of bots
- Place all bots in random locations
- For each turn:
- Execute each bot codes sequentially
- Simulate bot commands sequentially
- Assess and remove dead bots
- Repeat turns until only one bot is left
The game starts every hour. To participate in the next round, submit your
You write the bot script in the Tengo language. Your code should export a function that takes one argument self
:
pbr := import("pbr")
export func(self) {
/* ... */
return pbr.move(1, 0)
}
Your code can import Tengo standard library (except for os
module), can use Tengo builtin functions (except for print
and printf
), and, also should import pbr module.
Exported function should return a bot command using one of pbr
module functions: pbr.move(), pbr.hold(), pbr.attack(), or, pbr.clone(). If the function does not return anything (or return undefined
), the system will treat it as move(0, 0)
(no move no hold).
In addition to all Tengo runtime types, the following types are also supported.
Position is a vector of 2 int values, typically representing a coordinate in the map.
.x
or[0]
: X component (int).y
or[1]
: Y component (int)
Position has the following operators defined:
(Position) + (Position) => (Position)
: (c1.x + c2.x, c1.y + c2.y)(Position) - (Position) => (Position)
: (c1.x - c2.x, c1.y - c2.y)(Position) * (int) => (Position)
: (c1.x * n, c1.y * n)(Position) / (int) => (Position)
: (c1.x / n, c1.y / n)(Position) * (float) => (Position)
: (c1.x * f, c1.y * f)(Position) / (float) => (Position)
: (c1.x / f, c1.y / f)(Position) == (Position) => (bool)
: c1.x == c2.x && c1.y == c2.y(Position) != (Position) => (bool)
: c1.x != c2.x || c1.y != c2.y
Rect represents an axis-aligned bounding box using 2 Positions: min
and max
.
.min
: minimum X, Y (inclusive) (Position).max
: maximum X, Y (inclusive) (Position).dimension
: length of the bounds (Position).contains(Position)
: tests if the bounds contains a given Position (returns bool)
Rect has the following operators defined:
(Rect) == (Rect) => (bool)
: b1.min == b2.min && b1.max == b2.max(Rect) != (Rect) => (bool)
: b1.min != b2.min || b1.max != b2.max
pbr := import("pbr")
pbr.hold() => (bot command)
Hold command tells the system the bot will hold at the current position. When on hold, the bot will take zero damage when it's attacked by other bots during the turn.
pbr.attack(dx int, dy int) => (bot command)
pbr.attack(d Position) => (bot command)
Attack command tells the system the bot will attack another bot that's located at (self.pos.x + dx, self.pos.y + dy)
. If the attack is successful (there exists another bot on that position and it's not on hold), the bot will gain some HP (damage multiplied by a attack bonus rate). Attacked bot will lose HP by the damage.
Actual damage amount is computed using the attacking bot's base damage (self.damage
) and the distance weight as follows:
(damage) = (attacker.damage) * (distance weight)
The distance weight (the second term) is basically exponential decay on the distance from the attacker to the target bot.
Distances:
-2 | -1 | 0 | +1 | +2 | |
---|---|---|---|---|---|
-2 | 4 | 3 | 2 | 3 | 4 |
-1 | 3 | 2 | 1 | 2 | 3 |
0 | 2 | 1 | B | 1 | 2 |
+1 | 3 | 2 | 1 | 2 | 3 |
+2 | 4 | 3 | 2 | 3 | 4 |
Distance Weights:
dist | weight |
---|---|
1 | 1/1 |
2 | 1/4 |
3 | 1/16 |
4 | 1/64 |
5 | 1/256 |
pbr.move(dx int, dy int) => (bot command)
pbr.move(d Position) => (bot command)
Move command tells the system that the bot wants to another position. Per each turn, a bot can move to one of the following positions:
-2 | -1 | 0 | +1 | +2 | |
---|---|---|---|---|---|
-2 | |||||
-1 | O | ||||
0 | O | B | O | ||
+1 | O | ||||
+2 |
Move command will fail if there's another bot in the target position, and, the bot will remain in the current position.
pbr.clone(dx int, dy int) => (bot command)
pbr.clone(d Position) => (bot command)
Clone command tells the systeme to create another copy of the current bot and spawn it in one of the following positions:
-2 | -1 | 0 | +1 | +2 | |
---|---|---|---|---|---|
-2 | |||||
-1 | O | ||||
0 | O | B | O | ||
+1 | O | ||||
+2 |
Like move commands, clone command will fail if there's another bot in the target position.
When cloning, the current bot and the new bot will split HP and the damage equally. For example, cloning a bot that has {hp: 120, damage: 10}
will make 2 bots that have {hp: 60, damage: 5}
respectively. All other stats will remain the same, and, the inventory will be shared across all clones of the same bot. Each clone will have different .index
(clone index) value.
pbr.pos(x int, y int) => Position
Creates a new Position value.
pbr.rect(min Position, max Position) => Rect
pbr.rect(min_x int, min_y int, max_x int, max_y int) => Rect
Creates a new Rect value.
pbr.abs(c Position) => Position
pbs.abs(x int, y int) => Position
Returns another Position value that has the absolute values of the given Position.
pbr.sign(c Position) => Position
pbs.sign(x int, y int) => Position
Returns another Position value that has the sign values (1
if positive, -1
if negative, or 0
if zero) of the given Position.
pbr.contains(b Rect, c Position) => bool
pbr.field_bounds => Rect
Is an immutable Rect value that represents the game field. You can use this value to test if the coordinate is within the game field boundary:
if pbr.field_bounds.contains(c) { /* ... */ }
pbr.dist1 => [Position]
Is an immutable array of Positions with the following values: {x: 1, y: 0}, {x: 0, y: 1}, {x: -1, y: 0}, {x: 0, y: -1}
. One use case for this value is to pick the next move delta randomly:
rand := import("rand")
return pbr.move(pbr.dist1[rand.intn(len(pbr.dist1))])
Each bots (clones) do
.name
: name of the bot.index
: clone index (starting from 0).damage
: base damage.age
: how many turns it had survived.hp
: health points.kill
: how many bots it had killed.pos
: the current position of the bot{x: X, y: Y}
Per each turn, the bot script's exported function is invoked with self
value, an immutable map that has the following elements.
....
....
....
....
....
....
....
....
....
....
-2 | -1 | 0 | +1 | +2 | |
---|---|---|---|---|---|
-2 | O | ||||
-1 | O | O | O | ||
0 | O | O | B | O | O |
+1 | O | O | O | ||
+2 | O |
....
-2 | -1 | 0 | +1 | +2 | |
---|---|---|---|---|---|
-2 | |||||
-1 | O | ||||
0 | O | B | O | ||
+1 | O | ||||
+2 |
Each bot is given an inventory, which it can use as a data storage that persists across turns. Bot can use it however it wants, but, items in the inventory have different weights, and, when the total weight exceeds 100.0, the bot cannot perform any other actions but HOLD.
Technically, the inventory is a mutable map, and, you can put different types of values (items) in it. The weights of different types are:
By spending some points, the bot can check its surroundings.
Costs:
The repo must have a pbr.tengo
file.
The bot script must export a function of the following signature:
func(self) {}
self.pos
: the current position of the botself.pos.x
: X coordinate in[0, 999]
self.pos.y
: Y coordinate in[0, 999]
self.inv
: a map that represents the inventory of the bot- Inventory is empty when the bot is spawned initially, but, you can add or remove items in it.
- You can use the inventory as a persistent data storage for the bot.
self.inv
: a map that represents the inventory of the botself.identity
:self.age
:self.points
:self.kills
:self.hits
:
Here's an
Int
: 1.0Float
: 1.0String
: 0.5 x len(v)Bool
: 0.25Char
: 0.5Bytes
: 0.25 x len(v)Time
: 1.0Array
: (sum of weight of elements) + 1.0Map
: (sum of weight of values) + (sum of weight of string keys) + 1.0
{
identity: "d5/mybot1@19a2b2d9410c01baccc3b15197fe3db62f84d2ac",
points: 193,
kills: 2,
pos: {
x: 234,
y: 94
}
}
Basically, you need to create a GitHub repo that contains the program for your bot, and, submit it to participate in the the game. The only required file in the repo is pbr.tengo
, and, you can put any other files (e.g. README.md
file for some explanation) in the same repo. The system will simply read and compile pbr.tengo
script file and spawn your bot in the battle ground.
You program your bot using the Tengo language.
pbr := import("pbr")
attackable := func() {
if pbr.detect(-1, 0) {
return {dx: -1, dy: 0}
}
}
export func(pos, inv) {
if enemy := pbr.detect(-1, 0); enemy {
}
return pbr.move(-1, 0)
}
Your script is supposed to export a function that takes 2 arguments and return an action to perform for the turn.
"pbr" module has the following functions:
bot := detect(dx, dy)
- Arg
dx
: relative X coordinate (e.g.0
,+2
,-1
) - Arg
dy
: relative Y coordinate (e.g.0
,+2
,-1
) - Return
- Bot Stats if there is another bot in that position
undefined
if there is no bots
pbr := import("pbr")
export func() {
return pbr.move(dx, dy)
}
- Arg
dx
: relative X coordinate (e.g.0
,+2
,-1
) - Arg
dy
: relative Y coordinate (e.g.0
,+2
,-1
)
In PBR, the following builtin functions and standard modules are disabled:
print
builtin functionprintf
builtin functionos
module
If bot's program code is too lengthy, it may cost the bot more points per each turn. There will be extra computational costs for additional 1000 bytes compiled .....
After each turn is concluded, the system will publish various results of the turn in a file in this repo. The reports will contains:
- Leaderboards
- Top points
- Top kills
- Top survivers
- Event Logs