Getting Started

Let's get you up and running!

Tournaments:BotBattle 2026-2027(Active)

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!

The Boring Stuff

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!

  Open Web Editor

About the Game

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:

  Open Hexidor Docs

About HexiScript

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:

 Two operands per expression. 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.
 No function calls inside conditions or expressions. 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 while

Also, 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:

  Open HexiScript Docs

Our First Move

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.

  You can keep computing during the opponent's turn (nobody stops you), but you can only act on your own turn. The 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.

  An illegal move (into a wall, off the board, onto your opponent) eliminates your bot instantly. HexiScript has no exceptions to catch: the only defense is to check legality before acting.
Look Before You Leap

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.

Never Stand Still

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.

Walls: Slowing Down Your Rival

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
  A cautionary tale from testing this bot: when we first wrote it, it dropped walls whenever the rival was even one step ahead and kept no reserve. That bot lost more games than it won in our tests: about three wins in ten games against the default bot. Adding just one rule, spending at most a wall every fourth turn, brought it back to roughly even. Every wall is a turn you do not move, and a wall placed too early barely slows anybody down: walls are a tempo weapon, not a reflex.

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.

Watch the Clock

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.

  You can keep computing during the opponent's turn. That is where expensive thinking belongs: probe the board and prepare while the rival's clock runs, then act instantly when your own turn starts.
The Complete Bot

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()
Outro

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:

  • Think while the rival moves. Our bot only wakes up on its own turn. The idle branch of the main loop is free thinking time: observe the board there, and act within the first ticks of your turn so your own clock pays as little as possible.
  • Pick better walls. We wall the tile the rival faces, but the strongest wall is the one that costs them the most extra steps. To find it, you need to answer "how far is the rival's goal from here?" for many tiles at once, which means keeping a model of the board and searching it: the natural next step is a shortest-path search (a breadth-first flood) over the 61 tiles.
  • Use the leapfrog offensively. Our bot only leapfrogs when it happens to be walking into the rival. Deliberately stepping next to them, so the tile behind them is the road you want, turns their body into your booster.
  • Play the mirror. Run the final bot against a copy of itself: who wins when both bots make identical decisions? (Hint: it is the one that moves first, and figuring out why is real tournament strategy.)

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.