Let's get you up and running!

Welcome to the exciting world of the BotBattle! In this tutorial, we will build a Hexidor bot together, one strategy at a time. We start with a bot that can barely move and end with a racer that sidesteps walls, drops its own walls on the rival, and guards its clock. Along the way you will learn the core concepts of both the game and the HexiScript programming language. So, buckle up and let's embark on this exhilarating adventure together!
To be able to develop your very own bot that plays Hexidor, we've created an online editor that you can use to create bots and play games all from your browser. You can edit the code of both players on the field, step through their execution and log important information to debug your code. Additionally, the code you write will automatically be saved with your account if you're logged in.
So go ahead and open the web editor using the button below in another tab or window, so you can experiment with the code fragments in this tutorial while you're reading through it!
Hexidor is a two-player race, inspired by the board game Quoridor, played on a hexagonal field of 61 tiles. Two characters start at the centers of opposite sides of the board, and the first one to step onto the side across from their start wins.
The game is turn-based: on your turn, you either move to an adjacent tile or place a wall. You can leapfrog in a straight line over your opponent's tile when the square right behind them is free. Each player gets 16 zigzag walls, and a wall always keeps at least one route to the goal open, so you can slow your rival down, but never completely trap them.
The part that makes bots interesting: every player has a 60 second turn clock (300,000 ticks) that is a budget for the entire game. It only counts down while it is your turn, and it never refills. Every line of HexiScript code you execute burns that clock, so thinking is expensive and your code must spend its thinking time wisely. If your clock reaches zero, you are eliminated and your opponent wins.
The complete rules, the coordinate system and all game functions are described in the game docs:
HexiScript is a small, purpose-built programming language for BotBattle, the documentation for which can be found here. It looks a bit like Python married to Lua: variables, if/else if/else, for and while loops, functions and arrays, all with integer arithmetic. A program executes from top to bottom, and every statement costs game ticks, so slow code makes a slow bot.
Before we start, you need to know the two HexiScript quirks that catch everybody off guard:
x = a + b is valid, but x = a + b + c is a syntax error. Every computation must be broken into single operations using intermediate variables.if (arrayLen(a) > 0) is a syntax error. Call the function first, store the result in a variable, then test the variable:a = [1, 2, 3]
n = arrayLen(a)
while (n > 0) do
x = pop(a)
n = n - 1
end whileAlso, like most programming languages, HexiScript is case sensitive, and it has no break or continue: to exit a loop early you set a flag variable and make the while condition depend on it. The return statement of a function is often the cleaner escape hatch. The complete documentation of HexiScript can be found by clicking the button below:
Let's write a bot that walks forward. HexiScript runs your file from top to bottom, so the convention is to define a main function and call it at the end. Our bot must never stop: the game keeps ticking your processor until you act or the clock dies, so main runs an endless loop.
There is one rule of the game we have to respect from the very first line: you may only act (move or place a wall) during your own turn. The function isMyTurn() tells us when that is, and calling moveInDirectionRel() outside your turn eliminates your bot. So the skeleton of every Hexidor bot is the same:
function main() do
while (1 == 1) do
mine = isMyTurn()
if (mine == 1) then
-- this is where our turn logic will grow
moveInDirectionRel(0)
end if
wait(100)
end while
end function
main()
The wait(100) pauses execution for 100 ticks before the loop checks again, so a bot that is not your turn idles cheaply.moveInDirectionRel(0) takes one argument: a direction relative to the way your bot is facing. There are six directions on a hexagonal field, numbered clockwise: 0 is straight ahead, 1 is to the front right, 2 to the back right, 3 directly behind, 4 to the back left and 5 to the front left. Your facing is updated automatically: it always points the way you moved last.
wait() in the idle loop is what makes waiting cheap, so smart bots precompute while the other player is thinking.Run this in the web editor and watch: our bot walks straight ahead, once per turn. After a few moves, the field edge is right in front of it, and then things go badly.
The game provides a family of "probe" functions that answer questions without acting, and thus cannot get you eliminated. The one we need right now is canMoveRel(relDir): it returns 1 if moving in that relative direction is legal (the tile exists, nobody stands there, no wall blocks the edge) and 0 otherwise. Checking costs a cheap 20 ticks, a move costs 1000.
Let's combine this into a helper we will reuse for the rest of the tutorial: a function that tries a direction and reports whether it worked.
-- Try to move in a relative direction. Returns 1 when the move was made.
function tryDirRel(rel) do
ok = canMoveRel(rel)
if (ok == 1) then
moveInDirectionRel(rel)
return 1
end if
return 0
end function
One more game function and we can stop stumbling around: getStartInfo(), which returns two absolute directions: the side of the board you started on and your goal side: the direction in which the winning tiles lie. Absolute directions are the six hex directions in board coordinates (0 up, 1 up right, 2 down right, 3 down, 4 down left, 5 up left), and directionMeToRel(dir) converts any absolute direction into a direction relative to your facing.
Here is our second bot: march at the goal, but never illegally:
-- Bot 2: march towards the goal side, but never attempt an illegal move.
function tryDirRel(rel) do
ok = canMoveRel(rel)
if (ok == 1) then
moveInDirectionRel(rel)
return 1
end if
return 0
end function
function main() do
-- Which way do I have to go to reach my goal side?
_s, goal = getStartInfo()
while (1 == 1) do
mine = isMyTurn()
if (mine == 1) then
-- goal is absolute; my facing changes every move
g = directionMeToRel(goal)
moved = tryDirRel(g)
end if
wait(100)
end while
end function
main()
A small detail: getStartInfo() returns two values and we only care about the second one. The underscore _s is just a throwaway variable name.
Play this against the default bot in the editor. It races straight towards its goal, but the moment canMoveRel answers 0 (a wall, the board edge or your opponent stands in the way), the bot stands still and silently wastes its whole 60 second clock, and loses.
Standing still is losing: the clock is the one opponent that never blinks. Our third bot sidesteps when the road ahead is blocked: first try towards the goal, then the neighbouring directions, and turning around only as a last resort. We also wrap this logic in a function of its own, runOrSidestep(), so the final bot can reuse it.
There is a small catch in the directions: they are numbered 0 to 5 on a circle, so "the direction before 0" is 5, and "after 5" is 0 again. HexiScript has no modulo-friendly wrap for negative numbers (and the % operator needs a division first), so we normalize with a simple if: if a number leaves the 0..5 range, subtract 6.
Note that we walk the fallback ladder through all six directions, worst options last. Any shorter list can leave pockets where every preferred direction is blocked (wall here, opponent there) and our bot would silently stall and burn its clock: with six levels, a bot can only be stopped when the game says every move is illegal, which simply cannot happen on a legal board.
-- Bot 3: never stand still: head towards the goal, sidestep when blocked.
function tryDirRel(rel) do
ok = canMoveRel(rel)
if (ok == 1) then
moveInDirectionRel(rel)
return 1
end if
return 0
end function
-- Head towards the goal side; sidestep when blocked, and turn around as
-- a last resort. Returns 1 when a move was made.
function runOrSidestep(goal) do
-- which way is home? (goal is absolute, our facing changes every move)
g = directionMeToRel(goal)
-- the two neighbours of the goal direction (relative, wrap around 6)
left = g + 5
if (left > 5) then
left = left - 6
end if
right = g + 1
if (right > 5) then
right = right - 6
end if
back = g + 3
if (back > 5) then
back = back - 6
end if
diagLeft = g + 4
if (diagLeft > 5) then
diagLeft = diagLeft - 6
end if
diagRight = g + 2
if (diagRight > 5) then
diagRight = diagRight - 6
end if
-- forward first; then the sidesteps; then turn around; and the two
-- remaining diagonals as a true last resort, so we always move
moved = tryDirRel(g)
if (moved == 0) then
moved = tryDirRel(left)
end if
if (moved == 0) then
moved = tryDirRel(right)
end if
if (moved == 0) then
moved = tryDirRel(back)
end if
if (moved == 0) then
moved = tryDirRel(diagLeft)
end if
if (moved == 0) then
moved = tryDirRel(diagRight)
end if
return moved
end function
function main() do
_s, goal = getStartInfo()
while (1 == 1) do
mine = isMyTurn()
if (mine == 1) then
moved = runOrSidestep(goal)
end if
wait(100)
end while
end function
main()
This bot is already respectable. Two of them racing each other on an open board finish in about 14 turns, it dodges walls instead of walking into them, and it can even squeeze past the opponent, because canMoveRel counts a leapfrog over them as a legal move and the game performs it automatically. Its weakness: it takes every detour, no matter how long. When a wall forces it sideways, it never asks whether the detour costs one step or ten: it just follows its preference order and hopes for the best.
Moving is only half of Hexidor. Each player has 16 zigzag walls, and placing one is as easy as moving: placeWall(q, r, side, orientation) takes the absolute coordinates of a tile, one of its six edges (side), and an orientation 0 or 1 saying which way the zigzag bends. And like moves, there is a free probe: canPlaceWall(q, r, side, orientation) returns 1 only when everything checks out: the wall fully fits on the board, does not overlap existing walls, you still have walls left, and your rival can still reach their goal.
A good default target for a wall is the edge directly in front of your rival's nose: the tile they stand on, in the direction they face.getPlayerInfo(id) gives us exactly that: alive, absolute q, absolute r, and facing. Here is the wall drop as a helper:
-- Place a wall across the edge the rival faces. canPlaceWall checks
-- everything for us: on the board, no overlap, walls left, and the rival
-- can still reach their goal side. Returns 1 when a wall was placed.
function tryWallOnRival(me) do
myWalls = getWalls(me)
if (myWalls < 2) then
return 0
end if
rival = 1 - me
alive, rq, rr, rfacing = getPlayerInfo(rival)
ok = canPlaceWall(rq, rr, rfacing, 0)
if (ok == 1) then
placeWall(rq, rr, rfacing, 0)
return 1
end if
ok = canPlaceWall(rq, rr, rfacing, 1)
if (ok == 1) then
placeWall(rq, rr, rfacing, 1)
return 1
end if
return 0
end function
When should we drop a wall? Placing one costs your entire turn, so walling is only worth it when the rival is ahead of you. To compare who is ahead, you need the straight-line distance of both players to their own goal: your own distance comes free from getStepsToGoal(), and the rival's distance you compute from their position: the goal columns are q = -4 (for player 0) and q = 4 (for player 1), so the rival runs towards -myGoal, and their distance is the difference between their q and that column, made positive with abs().
Combining the racing from bot 3 with the wall drop gives our final bot's decision logic:
function main() do
me = getPlayerId()
rival = 1 - me
_s, goal = getStartInfo()
-- my goal column: -4 for player 0, 4 for player 1
-- (the rival runs towards the other one)
myGoal = 4
if (me == 0) then
myGoal = -4
end if
lastWallTurn = -10
while (1 == 1) do
mine = isMyTurn()
if (mine == 1) then
-- how far is the RIVAL from the RIVAL's goal?
alive, rq, rr, rfacing = getPlayerInfo(rival)
rdiff = rq + myGoal
rdist = abs(rdiff)
-- ...versus my own straight-line distance
steps = getStepsToGoal()
placed = 0
turn = getTurnNumber()
cool = turn - lastWallTurn
losing = steps - rdist
if (losing > 0) then
-- rival is ahead: slow them down, but a wall costs a full
-- turn of racing: at most one wall every 4 turns
if (cool > 3) then
placed = tryWallOnRival(me)
if (placed == 1) then
lastWallTurn = turn
end if
end if
end if
if (placed == 0) then
moved = runOrSidestep(goal)
end if
end if
wait(100)
end while
end function
This is a solid, complete bot: it never crashes, never moves illegally, races, sidesteps walls, leapfrogs, and spends its walls to punish a rival who pulls ahead. It is not unbeatable though: it wins roughly half its games against equally clever rivals, and earlier, shorter versions of its fallback ladder once lost whole games on time when walls and the rival pinched them into a pocket with no preferred direction left. Closing those gaps is exactly what advanced bot programming is about, and it is where you take it from here.
Before we assemble everything, one number to remember: your entire game budget is 300,000 ticks, and a typical game lasts 15 to 30 of your turns. That leaves roughly 10,000 to 20,000 ticks per turn for thinking, and it only counts down while you are the one to move. The costs you have met so far fit comfortably: isMyTurn() is 1 tick, probes like canMoveRel() and canPlaceWall() cost 10 to 20 ticks, and the moves themselves (1,000 ticks) fire once per turn. A full game of our final bot typically spends barely a tenth of its clock: thinking is cheap as long as you do not spam calls from your loop.
Where bots get eliminated is at the other end: a bot that computes everything every turn, or that never calls wait() and mills about in tight loops while the opponent is thinking, bleeds its budget dry.getTimeLeft(id) reads any player's remaining budget in ticks: check it on your own turn, and when it gets small, stop deliberating and just move. There is no time bonus for clever code: the winner is whoever steps on the goal column first, and a simple bot with a healthy clock beats a sophisticated one that timed out.
Here is the finished product: bot 4, a racer with a sidestep ladder and a wall drop held in reserve for when the rival pulls ahead. Every piece is one you have already seen: the move ladder from bot 3, the wall helper, and the race decision on top. In head-to-head testing we measured it winning about half its games against bot 3 (wasting a turn on a wall is only worth it if the wall actually hurts) and going four wins, six losses against the default example bot from the web editor: it never crashes and never moves illegally, but it does sometimes lose the race against smarter wall play.
Copy it into the web editor, play it against the default bot on both sides of the board, and see how many walls it actually uses before it crosses the line.
-- Bot 4 (FINAL tutorial bot): race, and slow down the rival with walls
-- when they pull ahead.
function tryDirRel(rel) do
ok = canMoveRel(rel)
if (ok == 1) then
moveInDirectionRel(rel)
return 1
end if
return 0
end function
-- Head towards the goal side; sidestep when blocked, and turn around as
-- a last resort. Returns 1 when a move was made.
function runOrSidestep(goal) do
g = directionMeToRel(goal)
left = g + 5
if (left > 5) then
left = left - 6
end if
right = g + 1
if (right > 5) then
right = right - 6
end if
back = g + 3
if (back > 5) then
back = back - 6
end if
diagLeft = g + 4
if (diagLeft > 5) then
diagLeft = diagLeft - 6
end if
diagRight = g + 2
if (diagRight > 5) then
diagRight = diagRight - 6
end if
-- forward first; then the sidesteps; then turn around; and the two
-- remaining diagonals as a true last resort, so we always move
moved = tryDirRel(g)
if (moved == 0) then
moved = tryDirRel(left)
end if
if (moved == 0) then
moved = tryDirRel(right)
end if
if (moved == 0) then
moved = tryDirRel(back)
end if
if (moved == 0) then
moved = tryDirRel(diagLeft)
end if
if (moved == 0) then
moved = tryDirRel(diagRight)
end if
return moved
end function
-- Place a wall across the edge the rival faces. canPlaceWall checks
-- everything for us: on the board, no overlap, walls left, and the rival
-- can still reach their goal side. Returns 1 when a wall was placed.
function tryWallOnRival(me) do
myWalls = getWalls(me)
if (myWalls < 2) then
return 0
end if
rival = 1 - me
alive, rq, rr, rfacing = getPlayerInfo(rival)
ok = canPlaceWall(rq, rr, rfacing, 0)
if (ok == 1) then
placeWall(rq, rr, rfacing, 0)
return 1
end if
ok = canPlaceWall(rq, rr, rfacing, 1)
if (ok == 1) then
placeWall(rq, rr, rfacing, 1)
return 1
end if
return 0
end function
function main() do
me = getPlayerId()
rival = 1 - me
_s, goal = getStartInfo()
-- the rival's goal column is my starting column: -4 for player 1, 4 for player 0
myGoal = 4
if (me == 0) then
myGoal = -4
end if
lastWallTurn = -10
while (1 == 1) do
mine = isMyTurn()
if (mine == 1) then
-- how far is the RIVAL from the RIVAL's goal? (they run to -myGoal)
alive, rq, rr, rfacing = getPlayerInfo(rival)
rdiff = rq + myGoal
rdist = abs(rdiff)
-- ...versus my own straight-line distance
steps = getStepsToGoal()
placed = 0
turn = getTurnNumber()
cool = turn - lastWallTurn
losing = steps - rdist
if (losing > 0) then
-- rival is ahead: slow them down, but a wall costs a full
-- turn of racing: at most one wall every 4 turns
if (cool > 3) then
placed = tryWallOnRival(me)
if (placed == 1) then
lastWallTurn = turn
end if
end if
end if
if (placed == 0) then
moved = runOrSidestep(goal)
end if
end if
wait(100)
end while
end function
main()
You now have a bot that survives, races, walls, and budgets its thinking time. From here, the improvements are yours to invent. A few directions to chew on:
We hope this tutorial has given you a good starting point for creating your own bots in the BotBattle. Practice makes perfect, so keep experimenting and refining your strategies. Go ahead and start coding in the web editor, and once you're ready, submit your bot and watch it battle!
More information about the game can be found in the gameplay description. More information about the language is available in the editor, or in the HexiScript documentation.