Code Tank Reference

Everything the codetank library gives your tank. Keep this tab open.

1. Starting a tank

Every tank file has the same four parts: connect, set up, write a brain, run.

import codetank

tank = codetank.connect()
tank.set_name("Rolling Thunder")
tank.set_stats(armor=25, firepower=25, sensors=25, ballistics=25)


def brain(tank):
    tank.set_tracks(60, 60)
    tank.set_turret_power(50)
    if tank.can_fire():
        tank.fire()


tank.run(brain)

That file runs. It drives in a circle and fires at nothing, so it is a starting point and not much else.

Two keys worth knowing in the editor. Ctrl-/ comments out whichever lines you have selected, and comments them back in if they are already out, which is how you try something without deleting what worked. F gives the arena the whole window for watching a battle.

CallWhat it does
codetank.connect()Gives you your tank. It has to be the first thing you do.
tank.set_name(name)The name above your tank in the arena. The first 16 characters are kept.
tank.set_stats(armor=25, firepower=25, sensors=25, ballistics=25)Spends your 100 points. See section 2.
tank.set_tank_color(r, g, b)Your color. Each of the three is 0.0 to 1.0, so (1.0, 0.5, 0.0) is orange.
tank.run(brain)Hands your brain to the game. It has to be the last line.

What the brain function is

Your brain is a function that takes one argument, the tank. The game calls it about 20 times a second, and it has to finish and return each time. One pass of the brain is one decision.

Do not loop and do not sleep inside the brain. A while True: loop never gives the tank back, and time.sleep() just wastes the tick. Set the tracks, set the turret, maybe fire, and return. You will be called again in 1/20 of a second.

To remember something from one tick to the next, keep it in a variable outside the brain and say global before you change it.

ticks = 0


def brain(tank):
    global ticks
    ticks += 1
    if ticks % 20 == 0:
        print("one second of battle")

print() goes to the output panel under the arena. If your brain raises an error, the game prints it once with the line number instead of spamming you twenty times a second.

2. The stat budget

You have 100 points for four stats. Every point you spend somewhere is a point you did not spend somewhere else.

tank.set_stats(armor=10, firepower=40, sensors=25, ballistics=25)
  • The four numbers must add up to exactly 100.
  • Each one has to be at least 1 and at most 97.
  • Break either rule and set_stats raises a ValueError, so your tank never starts.
  • Call it near the top of the file, before tank.run(brain).

What the points buy

The column headings are how many points you put in that stat.

Stat1 point25 points97 points
Armor: health, paid for in speed
health52100244
move_speed39.833.815.8
turn_speed59.650.021.2
gun_turn_speed79.567.531.5
Firepower: damage, paid for in waiting
damage1.03.410.6
reload wait, in seconds0.200.923.08
Sensors: how far you see, paid for in how wide
scan_range100172388
scan_arc1707715
Ballistics: how far and how fast your shots go
bullet_speed150270630
bullet_range100196484

Distances are in arena units. A tank is 30 units across, and the large arena is 600 by 450. Angles and arcs are in degrees.

Two of those numbers are not units per second. move_speed and turn_speed are build numbers. At full power the tank really travels about four times move_speed, so a balanced tank crosses the ground at about 135 units a second. If you want the real figure while you are driving, read tank.velocity.

Sensors trade range against width. The scan cone always covers the same area, so a long cone is a thin one. At 1 point you see 100 units through a 170 degree fan. At 97 you see 388 units through a 15 degree slot.

Reading your own build

You do not have to copy your numbers into your code by hand. The tank knows them.

def brain(tank):
    print("I see", tank.scan_range, "and shoot", tank.bullet_range)

3. Driving

A tank has two tracks and no steering wheel. You set the power of each one and the tank goes where that takes it.

tank.set_tracks(left, right)   # each one from -100 to 100

This is not a move command that finishes. It is a setting. The tracks stay where you put them until you set them again, so you do not have to call it every tick.

CallWhat the tank does
tank.set_tracks(100, 100)Straight ahead, as fast as the build goes.
tank.set_tracks(-100, -100)Straight backwards.
tank.set_tracks(100, -100)Spins in place, without going anywhere.
tank.set_tracks(-100, 100)Spins in place the other way.
tank.set_tracks(100, 50)Drives and curves. The slower track is the inside of the curve.
tank.set_tracks(0, 0)Stops.

Anything above 100 or below -100 is clamped, so set_tracks(250, 250) is just full ahead. More power on the right track than on the left makes tank.angle grow, which is clockwise on the screen. If your tank turns the wrong way, swap the two numbers.

Walls

The arena is a box. You cannot drive through the edge, and grinding along it wastes the whole fight.

CallGives you
tank.distance_to_wall()How far the wall is in the direction the body is pointing.
tank.distance_to_wall(angle)How far the wall is along any bearing in degrees.
tank.distance_to_wall(tank.gun_angle)How far the wall is the way the gun is looking.
def brain(tank):
    if tank.distance_to_wall() < 40:
        tank.set_tracks(-60, 60)      # turn away before you arrive
    else:
        tank.set_tracks(80, 80)

It already knows how big the arena is, so the same code works in the small, medium and large arenas. The ladder is always the large one, 600 by 450, and there is no Arena setting while you are climbing it: every rung is built on a distance, and what your build can see and reach does not shrink with the room. Survival and Versus are where the smaller arenas live.

4. The turret

The gun turns on its own. It does not care which way the body is pointing, and the body does not care which way the gun is pointing.

tank.set_turret_power(power)   # -100 to 100
  • Positive turns the gun clockwise, negative turns it counter clockwise, and 0 stops it.
  • Like the tracks, this is a setting. The gun keeps turning until you change it.
  • How fast it turns at full power is tank.gun_turn_speed, which armor slows down.
  • tank.gun_angle is where the gun is now pointing, from 0 to 360 degrees. 0 is to the right of the arena.
def brain(tank):
    tank.set_tracks(0, 0)             # the body can sit still
    tank.set_turret_power(60)         # while the gun sweeps round
    print("gun is on", round(tank.gun_angle))

The gun is also your eyes. Scanning looks down the gun, not down the body, and bullets leave along the gun. Turning the body changes nothing about what you can see or where a shot goes.

5. Scanning

You can only see inside a cone in front of the gun. Nothing outside that cone exists as far as your code is concerned.

CallGives you
tank.scan()The closest tank in the cone, as an Enemy, or None if the cone is empty.
tank.scan_all()A list of every tank in the cone, closest first. An empty list if there are none.

The cone is tank.scan_range long and tank.scan_arc degrees wide in total, so half of that arc sits each side of the gun. Dead tanks are not returned. Scanning costs nothing and takes no time, so you can do it every tick.

def brain(tank):
    tank.set_turret_power(60)

    enemy = tank.scan()
    if enemy:
        print(enemy.name, "is", round(enemy.distance), "units away")
        print("off my gun by", round(enemy.direction), "degrees")

What an Enemy carries

On an EnemyWhat it is
enemy.distanceHow far away it is, in units.
enemy.directionHow far off your gun it is, in degrees, from -180 to 180. Positive means clockwise from the gun, so positive turret power closes the gap. Zero means the gun is on it.
enemy.xIts position across the arena.
enemy.yIts position down the arena.
enemy.positionBoth at once, as (x, y).
enemy.healthHow much health it has left.
enemy.nameThe name it set for itself.
enemy.idIts number in the arena. Names repeat, ids do not.
enemy.team"blue" or "red" in Team Deathmatch, otherwise an empty string.
enemy.is_teammate()True if it is on your side. Always False outside Team Deathmatch.

enemy.direction is not a compass bearing. It is measured from your gun, so it changes when your gun turns even if the enemy stands still. The bearing of your gun is tank.gun_angle.

An Enemy is a snapshot taken when you called scan(). It does not update itself. Call scan() again next tick for fresh numbers.

More than one enemy

scan_all() hands you a list, and a list is something you can loop over or pick from.

def brain(tank):
    for enemy in tank.scan_all():
        print(enemy.name, round(enemy.distance), enemy.health)

6. Firing

The shot leaves along the gun, travels at tank.bullet_speed, and disappears after tank.bullet_range units whether it hit anything or not.

CallWhat it does
tank.can_fire()True when the gun is loaded. It is the same as asking whether tank.reload_timer has reached 0.
tank.fire()Shoots. Gives you a Bullet, or None if the gun was still reloading.
tank.is_aimed_at(enemy)True if that enemy is within 5 degrees of the gun.
tank.is_aimed_at(enemy, tolerance=2)The same question with your own number of degrees.
tank.is_in_range(enemy)True if enemy.distance is inside your bullet_range.
def brain(tank):
    enemy = tank.scan()
    if enemy and tank.is_aimed_at(enemy) and tank.can_fire():
        tank.fire()

Calling fire() while reloading is not an error. Nothing happens and you get None back. Firing does not cost fuel, and you can fire with the tracks stopped.

Both aim questions need a fresh Enemy. is_aimed_at works off enemy.direction and is_in_range works off enemy.distance, so an Enemy you kept from an earlier tick answers with old news.

Following a bullet

You can watch a shot you fired. You will not need this to shoot well, but it is there.

On a BulletWhat it is
bullet.xWhere it is now, across the arena.
bullet.yWhere it is now, down the arena.
bullet.activeTrue while it is still flying.
bullet.idIts number, which no other bullet gets.
tank.bulletsA list of the bullets you have fired lately.
def brain(tank):
    shot = tank.fire()
    if shot:
        print("shot", shot.id, "is away")

7. What your tank knows about itself

All of these are properties, not calls, so there are no parentheses after them. Reading one is instant.

PropertyWhat it is
Where you are
tank.xAcross the arena. 0 is the middle, and it grows to the right.
tank.yDown the arena. 0 is the middle, and it grows downwards.
tank.positionBoth at once, as (x, y). You can write x, y = tank.position.
tank.angleWhich way the body points, 0 to 360 degrees. 0 is to the right, and it grows clockwise.
tank.gun_angleWhich way the gun points, on the same scale.
tank.velocityHow fast you are really moving, in units a second. Negative means backwards.
How you are
tank.healthWhat is left of you.
tank.max_healthWhat you started with.
tank.aliveTrue while your health is above 0.
tank.reload_timerSeconds until the gun is loaded. 0 means now.
tank.nameThe name you set.
Your build
tank.move_speedYour driving number, set by armor.
tank.turn_speedYour body turning number, set by armor.
tank.gun_turn_speedYour gun turning number, set by armor.
tank.damageWhat one of your shots takes off.
tank.scan_rangeHow far you can see.
tank.scan_arcHow wide your cone is, in degrees.
tank.bullet_speedHow fast your shots travel, in units a second.
tank.bullet_rangeHow far your shots get before they expire.
tank.armor_pointsWhat you spent on armor.
tank.firepower_pointsWhat you spent on firepower.
tank.sensors_pointsWhat you spent on sensors.
tank.ballistics_pointsWhat you spent on ballistics.
The match
tank.enemies_remainingHow many other tanks are still alive.
tank.bulletsThe bullets you have fired lately.
tank.teamYour side in Team Deathmatch, otherwise an empty string.
The arena
tank.arena_widthHow wide the arena you are actually fighting in is.
tank.arena_heightHow tall it is.
def brain(tank):
    print(tank.health, "hp,", round(tank.fuel_percentage), "percent fuel")
    print(tank.enemies_remaining, "still out there")

Read the arena, do not assume it. The teacher picks small, medium or large, so use tank.arena_width and tank.arena_height rather than typing in 600 and 450. The module also has codetank.ARENA_WIDTH and friends, but those describe the large arena only, which makes them wrong two times out of three.

8. Fuel

Driving uses fuel. When it runs out the tracks stop for the rest of the round, and that is that.

PropertyWhat it is
tank.fuelHow much you have left.
tank.max_fuelHow much you started with.
tank.fuel_percentageThe same thing from 0 to 100, which is usually the easier one to test.
tank.has_fuelFalse once you are dry.

Things worth knowing:

  • Fuel pays for distance covered, not for time. Sitting still is free.
  • Turning the gun is free, firing is free, and spinning the body in place costs almost nothing because you do not go anywhere.
  • A heavy tank carries more fuel and burns it faster, so every build can drive about the same total distance. No build is secretly thriftier.
  • Out of fuel is not out of the fight: the gun still turns and still shoots.
  • Fuel is refilled for a new round, and in Survival it is refilled when a wave is cleared.
def brain(tank):
    if not tank.has_fuel:
        tank.set_turret_power(50)     # nothing else to do but look
        return

    tank.set_tracks(70, 70)

9. When something happens

Some things do not wait for your brain to ask. You can hand the game a function and it will call it when that thing happens.

def on_hit(event):
    print("ow,", event.damage, "damage from", round(event.angle))


tank.on_hit_by_bullet = on_hit

Write the function, then set it on the tank with no parentheses. You are handing over the function itself, not calling it. All of this goes before tank.run(brain). Your callback is given one argument, an Event.

Every hook there is

Set thisCalled whenUseful fields on the event
tank.on_hit_by_bulletSomething hits you.damage, angle, shooter_id
tank.on_bullet_hitOne of your shots lands.damage, target_name, target_id, bullet_id
tank.on_enemy_diedAny other tank is destroyed.enemy_name, enemy_id
tank.on_diedYou are destroyed.killer_id
tank.on_new_roundA new round starts.round
tank.on_round_endA round ends and another is coming.nothing you need to read
tank.on_battle_endThe whole match is over.nothing you need to read
tank.on_stalemate_respawnThe downloadable game moves everybody when nothing has happened for a long time, and calls this. Here nothing moves: two minutes with no hit is simply a draw, so this never happens in your browser.nothing you need to read
tank.on_hill_capturedKing of the Hill only: you take the hill.nothing to read
tank.on_hill_lostKing of the Hill only: you lose it.nothing to read
tank.on_wave_startedSurvival only: a wave arrives.wave, enemies
tank.on_wave_completeSurvival only: the last of a wave dies.wave, next_wave, score
tank.on_respawnedYou come back after being destroyed.x, y, lives_remaining

What an Event carries

Every event has all of these fields. The ones that do not apply are 0 or an empty string, so only read the ones the table above lists for that hook.

On an EventWhat it is
event.typeWhich event this is, as text, such as "hit_by_bullet".
event.damageHow much damage the hit did.
event.angleThe direction the bullet was traveling, in degrees. Whatever fired it is the other way, at (event.angle + 180) % 360.
event.shooter_idThe id of the tank that shot you.
event.target_idThe id of the tank you hit.
event.target_nameThe name of the tank you hit.
event.bullet_idWhich of your bullets landed.
event.enemy_idThe id of the tank that died.
event.enemy_nameThe name of the tank that died.
event.killer_idThe id of the tank that destroyed you, or -1 if it was a wall or nobody.
event.roundWhich round is starting.
event.waveWhich wave this is about.
event.next_waveThe wave that is coming next.
event.enemiesHow many arrived in this wave.
event.scoreYour Survival score.
event.lives_remainingLives left after a Survival respawn.
event.xWhere you came back, across the arena.
event.yWhere you came back, down the arena.

A new round puts positions, health and fuel back as they were. It does not touch your variables, so anything you were counting keeps its value into the next round. on_new_round is where you put it back yourself if you want that.

A callback is not your brain. It runs once, when the thing happens, and then it is over. It is the place to write down what you learned, not the place to drive. Change a variable there and let the brain act on it next tick, and remember global.

hits_taken = 0


def on_hit(event):
    global hits_taken
    hits_taken += 1


tank.on_hit_by_bullet = on_hit


def brain(tank):
    if hits_taken > 3:
        print("this is going badly")

10. Other battle types

Everything above works in every battle type. These are the extras. Reading one of them in a mode it does not belong to is not an error. You get a harmless default instead, such as 0, an empty string, or -1 where an id would go.

Team Deathmatch

The arena is split into a blue side and a red side.

Property or callWhat it is
tank.teamYour side, "blue" or "red".
enemy.teamThat tank's side.
enemy.is_teammate()True if it is on your side. It already knows which side you are on, so you do not pass it anything.

scan() and scan_all() return teammates too. They report every living tank in your cone, friend or not. In a team match that makes is_teammate() the first question to ask about anything you find.

def brain(tank):
    for other in tank.scan_all():
        if other.is_teammate():
            continue
        print("enemy:", other.name, round(other.distance))

Survival

Waves of tanks come at you, each wave harder than the last, and you have lives instead of one life.

PropertyWhat it is
tank.waveWhich wave you are on, counting from 1.
tank.wave_enemies_remainingHow many of this wave are still alive.
tank.between_wavesTrue during the pause before the next wave.
tank.wave_timerHow many seconds of that pause have gone by.
tank.livesLives left. At 0, the next death ends the run.
tank.scoreTen points a kill, multiplied by the wave it was killed on.

The three Survival hooks are on_wave_started, on_wave_complete and on_respawned. See section 9.

def brain(tank):
    if tank.between_waves:
        tank.set_tracks(0, 0)         # nothing to shoot at yet
        return

    print("wave", tank.wave, "with", tank.wave_enemies_remaining, "left")

King of the Hill

There is a circle in the middle of the arena and the match is won by standing in it. Being destroyed is a few seconds out, not the end, so the scoreboard decides it. The hill is always centered on (0, 0), and these five properties tell you about it.

PropertyWhat it is
tank.in_hillTrue while you are standing inside the circle.
tank.hill_scoreYour score: the number of seconds you have held the hill so far.
tank.hill_ownerThe id of the tank holding it, or -1 when nobody is. Two tanks inside the circle means nobody holds it.
tank.hill_radiusHow big the circle is. The teacher picks this before the match.
tank.points_to_winHow many seconds of holding it win the match.

Your own id is not something you can read, so compare tank.hill_owner against an enemy.id from a scan when you want to know whether the tank holding it is one you can see. tank.in_hill is the quicker question about yourself.

def brain(tank):
    if tank.in_hill:
        print("on the hill,", round(tank.hill_score), "of", tank.points_to_win)
    elif tank.hill_owner == -1:
        print("the hill is empty")
    else:
        print("someone else is holding the hill")

Two callbacks go with them, tank.on_hill_captured and tank.on_hill_lost. Both are called with an Event that has nothing on it worth reading: the fact that it happened is the whole message. on_respawned fires here too when you come back, carrying event.x and event.y.

You can still work out where the middle is yourself, which is useful for steering towards it rather than just asking whether you have arrived.

import math


def brain(tank):
    from_middle = math.hypot(tank.x, tank.y)
    print("I am", round(from_middle), "from the middle")

11. How a match is won

Destroying everybody is one way for a match to end. It is not the only one, and a match you won without firing a shot still counts.

Last one standing

In an ordinary battle the match ends the moment only one tank is still alive, and that tank wins. If the last two are destroyed at the same instant, nobody is left and it is a draw.

The clock

A battle is given 180 seconds, which is three minutes of arena time. If the clock runs out with more than one tank alive, the match is decided on health, and this is the part worth reading twice:

  • What is compared is health as a fraction of what you started with, not health in hit points.
  • The highest fraction wins.
  • If the two best are exactly level, the match is a draw.

Fractions rather than hit points, because builds do not start level. A heavy tank with 244 health sitting on 100 has 41 percent of itself left. A light tank with 52 health sitting on 30 has 58 percent, so the light one takes the match even though it has less than a third of the other one's hit points.

So you can win a match without destroying anything. If the clock runs out and you have been hit less than the other tank, as a fraction, the match is yours. A tank that never finds a target and never gets found can win this way, and so can one that stands in a corner and sprays. Both of those are results the arena is happy to hand out.

Your own fraction is two properties you already have.

def brain(tank):
    share = tank.health / tank.max_health
    print("I have", round(share * 100), "percent of myself left")

A round somebody ends

There is an End round button in the header while a battle is on. It stops the round where it is and decides it on exactly the rules above: the hill in King of the Hill, the health of each side in Team Deathmatch, and otherwise the fraction of health each tank has left. Level is still a draw.

It is there for a round that is clearly going nowhere with minutes of clock still to run. Nothing is skipped: the round is decided, everybody is told, and a best of three carries on to the next round. It is not the same as Reset, which throws the battle away and settles nothing. A round decided this way does not pass a ladder rung, for the same reason the clock does not: nothing was destroyed.

A draw by stalemate

If 120 seconds go by with no hit landing on anybody, and two or more tanks are still alive, the match is declared a draw there and then. Two tanks that cannot find each other, or cannot reach each other, do not get to sit there for ever.

The downloadable game handles this differently: it picks everybody up and puts them down somewhere else to get the fight going again, which is what tank.on_stalemate_respawn is for. The browser does not move anybody. It draws.

King of the Hill has no stalemate clock, because standing still on the hill and refusing to be drawn off it is the game rather than a problem.

Team Deathmatch

A side is finished when nobody on it is standing, and the other side wins. The win is credited to everyone on the winning side, including any of them that were destroyed getting there. It was a team that won it.

On the clock it goes to the side in better shape: every living tank's health is turned into a fraction of its own maximum, the fractions are added up for each side, and the bigger total wins. Adding fractions rather than hit points means a side of two is not beaten simply by a side of three being larger. Level totals are a draw.

King of the Hill

The match goes to the first tank to hold the hill for tank.points_to_win seconds. Holding it means being the only tank in the circle: with two of you inside, the clock stops for both and tank.hill_owner is -1.

Being destroyed here is a five second wait and then you are back, with full health and full fuel, placed clear of the hill. Nobody is ever knocked out of the match, so there is no last one standing to find and no clock to run out. It ends on the scoreboard.

Survival

Survival is not won, it is survived. Waves keep coming, each one harder, and there is no match clock at all. You start with 3 lives; the run ends when you are destroyed with none left, and the answer is how far you got: tank.wave and tank.score, which is ten points a kill multiplied by the wave it happened on.

Passing a rung on the ladder

Beating a ladder tank once does not pass it. It takes 2 wins against the same rung, and the page shows you where you are, as 1 of 2 wins. One win can turn on nothing more than where the two tanks happened to be put at the start, and that is not evidence that your tank is better.

Three more things about the counting:

  • A rung is passed by destroying it. The clock running out with you ahead on health is a win, and the page says so, but it passes no rung. A tank that only has to be ahead after three minutes can be beaten by the code the editor starts you with, which is why the ladder asks for the kill.
  • A draw counts for nobody. It is not a loss, but it does not move you up either.
  • Fuel on and fuel off are two separate ladders. Wins with the fuel switched off are kept apart and never added to the others, because a tank that can drive for ever is solving a different problem. Beating a rung one way leaves it still to beat the other way.

12. Common mistakes

  1. Looping inside the brain. A while True: in your brain hangs your tank and it is thrown out of the match. The brain must return. The game is the loop.
  2. Sleeping inside the brain. time.sleep() does not slow the game down, it only wastes your tick. Count ticks in a variable instead: 20 of them is a second.
  3. Turning the body to look around. Scanning follows the gun. If you want to see somewhere else, use set_turret_power.
  4. Forgetting that scan() can be None. enemy.distance on an empty scan is an AttributeError. Write if enemy: first, every time.
  5. Treating enemy.direction as a bearing. It is an offset from your gun. Zero does not mean north, it means you are on target.
  6. Points that do not add up to 100. set_stats raises a ValueError and your tank never reaches the arena. Add the four numbers up yourself before you run it.
  7. Calling set_stats or set_name after run. Nothing after tank.run(brain) happens until the match is over. Setup goes above it, and run is the last line.
  8. Changing a variable outside the brain without global. Python quietly makes a new local instead, so your tank forgets everything between ticks.
  9. Keeping an Enemy from an older tick. Its distance and direction were true when you scanned and are not true now. Scan again.
  10. Firing without asking can_fire(). Shots while the gun is reloading do nothing at all.
  11. Shooting at something further away than tank.bullet_range. The bullet expires in mid air. Seeing a tank and being able to reach it are two different questions.
  12. Typing in 600 and 450. That is the large arena. Read tank.arena_width and tank.arena_height, or let distance_to_wall() do it for you.