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.
| Call | What 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_statsraises aValueError, 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.
| Stat | 1 point | 25 points | 97 points |
|---|---|---|---|
| Armor: health, paid for in speed | |||
| health | 52 | 100 | 244 |
| move_speed | 39.8 | 33.8 | 15.8 |
| turn_speed | 59.6 | 50.0 | 21.2 |
| gun_turn_speed | 79.5 | 67.5 | 31.5 |
| Firepower: damage, paid for in waiting | |||
| damage | 1.0 | 3.4 | 10.6 |
| reload wait, in seconds | 0.20 | 0.92 | 3.08 |
| Sensors: how far you see, paid for in how wide | |||
| scan_range | 100 | 172 | 388 |
| scan_arc | 170 | 77 | 15 |
| Ballistics: how far and how fast your shots go | |||
| bullet_speed | 150 | 270 | 630 |
| bullet_range | 100 | 196 | 484 |
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.
| Call | What 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.
| Call | Gives 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
0stops 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_angleis where the gun is now pointing, from 0 to 360 degrees.0is 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.
| Call | Gives 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 Enemy | What it is |
|---|---|
| enemy.distance | How far away it is, in units. |
| enemy.direction | How 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.x | Its position across the arena. |
| enemy.y | Its position down the arena. |
| enemy.position | Both at once, as (x, y). |
| enemy.health | How much health it has left. |
| enemy.name | The name it set for itself. |
| enemy.id | Its 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.
| Call | What 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 Bullet | What it is |
|---|---|
| bullet.x | Where it is now, across the arena. |
| bullet.y | Where it is now, down the arena. |
| bullet.active | True while it is still flying. |
| bullet.id | Its number, which no other bullet gets. |
| tank.bullets | A 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.
| Property | What it is |
|---|---|
| Where you are | |
| tank.x | Across the arena. 0 is the middle, and it grows to the right. |
| tank.y | Down the arena. 0 is the middle, and it grows downwards. |
| tank.position | Both at once, as (x, y). You can write x, y = tank.position. |
| tank.angle | Which way the body points, 0 to 360 degrees. 0 is to the right, and it grows clockwise. |
| tank.gun_angle | Which way the gun points, on the same scale. |
| tank.velocity | How fast you are really moving, in units a second. Negative means backwards. |
| How you are | |
| tank.health | What is left of you. |
| tank.max_health | What you started with. |
| tank.alive | True while your health is above 0. |
| tank.reload_timer | Seconds until the gun is loaded. 0 means now. |
| tank.name | The name you set. |
| Your build | |
| tank.move_speed | Your driving number, set by armor. |
| tank.turn_speed | Your body turning number, set by armor. |
| tank.gun_turn_speed | Your gun turning number, set by armor. |
| tank.damage | What one of your shots takes off. |
| tank.scan_range | How far you can see. |
| tank.scan_arc | How wide your cone is, in degrees. |
| tank.bullet_speed | How fast your shots travel, in units a second. |
| tank.bullet_range | How far your shots get before they expire. |
| tank.armor_points | What you spent on armor. |
| tank.firepower_points | What you spent on firepower. |
| tank.sensors_points | What you spent on sensors. |
| tank.ballistics_points | What you spent on ballistics. |
| The match | |
| tank.enemies_remaining | How many other tanks are still alive. |
| tank.bullets | The bullets you have fired lately. |
| tank.team | Your side in Team Deathmatch, otherwise an empty string. |
| The arena | |
| tank.arena_width | How wide the arena you are actually fighting in is. |
| tank.arena_height | How 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.
| Property | What it is |
|---|---|
| tank.fuel | How much you have left. |
| tank.max_fuel | How much you started with. |
| tank.fuel_percentage | The same thing from 0 to 100, which is usually the easier one to test. |
| tank.has_fuel | False 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 this | Called when | Useful fields on the event |
|---|---|---|
| tank.on_hit_by_bullet | Something hits you. | damage, angle, shooter_id |
| tank.on_bullet_hit | One of your shots lands. | damage, target_name, target_id, bullet_id |
| tank.on_enemy_died | Any other tank is destroyed. | enemy_name, enemy_id |
| tank.on_died | You are destroyed. | killer_id |
| tank.on_new_round | A new round starts. | round |
| tank.on_round_end | A round ends and another is coming. | nothing you need to read |
| tank.on_battle_end | The whole match is over. | nothing you need to read |
| tank.on_stalemate_respawn | The 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_captured | King of the Hill only: you take the hill. | nothing to read |
| tank.on_hill_lost | King of the Hill only: you lose it. | nothing to read |
| tank.on_wave_started | Survival only: a wave arrives. | wave, enemies |
| tank.on_wave_complete | Survival only: the last of a wave dies. | wave, next_wave, score |
| tank.on_respawned | You 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 Event | What it is |
|---|---|
| event.type | Which event this is, as text, such as "hit_by_bullet". |
| event.damage | How much damage the hit did. |
| event.angle | The direction the bullet was traveling, in degrees. Whatever fired it is the other way, at (event.angle + 180) % 360. |
| event.shooter_id | The id of the tank that shot you. |
| event.target_id | The id of the tank you hit. |
| event.target_name | The name of the tank you hit. |
| event.bullet_id | Which of your bullets landed. |
| event.enemy_id | The id of the tank that died. |
| event.enemy_name | The name of the tank that died. |
| event.killer_id | The id of the tank that destroyed you, or -1 if it was a wall or nobody. |
| event.round | Which round is starting. |
| event.wave | Which wave this is about. |
| event.next_wave | The wave that is coming next. |
| event.enemies | How many arrived in this wave. |
| event.score | Your Survival score. |
| event.lives_remaining | Lives left after a Survival respawn. |
| event.x | Where you came back, across the arena. |
| event.y | Where 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 call | What it is |
|---|---|
| tank.team | Your side, "blue" or "red". |
| enemy.team | That 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.
| Property | What it is |
|---|---|
| tank.wave | Which wave you are on, counting from 1. |
| tank.wave_enemies_remaining | How many of this wave are still alive. |
| tank.between_waves | True during the pause before the next wave. |
| tank.wave_timer | How many seconds of that pause have gone by. |
| tank.lives | Lives left. At 0, the next death ends the run. |
| tank.score | Ten 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.
| Property | What it is |
|---|---|
| tank.in_hill | True while you are standing inside the circle. |
| tank.hill_score | Your score: the number of seconds you have held the hill so far. |
| tank.hill_owner | The id of the tank holding it, or -1 when nobody is. Two tanks inside the circle means nobody holds it. |
| tank.hill_radius | How big the circle is. The teacher picks this before the match. |
| tank.points_to_win | How 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
- 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. - 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. - Turning the body to look around. Scanning follows the gun. If you
want to see somewhere else, use
set_turret_power. - Forgetting that
scan()can beNone.enemy.distanceon an empty scan is anAttributeError. Writeif enemy:first, every time. - Treating
enemy.directionas a bearing. It is an offset from your gun. Zero does not mean north, it means you are on target. - Points that do not add up to 100.
set_statsraises aValueErrorand your tank never reaches the arena. Add the four numbers up yourself before you run it. - Calling
set_statsorset_nameafterrun. Nothing aftertank.run(brain)happens until the match is over. Setup goes above it, andrunis the last line. - Changing a variable outside the brain without
global. Python quietly makes a new local instead, so your tank forgets everything between ticks. - Keeping an
Enemyfrom an older tick. Itsdistanceanddirectionwere true when you scanned and are not true now. Scan again. - Firing without asking
can_fire(). Shots while the gun is reloading do nothing at all. - 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. - Typing in 600 and 450. That is the large arena. Read
tank.arena_widthandtank.arena_height, or letdistance_to_wall()do it for you.