Build your own bot

Bot API · protocol 1

Don't want to install anything? Try Code a bot right in the browser. You write the bot in an editor next to the game.

TinyHold can hand the game over to your program. The game in the browser connects to a bot running on your computer, sends it the rules and the game state, and accepts the same orders from it that you give with the mouse. You can write it in any language you like. It only needs to speak WebSocket.

Getting started

  1. Run your bot as a WebSocket server on your computer, for example on port 7777.
  2. Open the game with the address https://tinyhold.cz/hrat/?bot=ws://127.0.0.1:7777&botklic=my-secret-key-1234&lng=en. Make up the key yourself (16–128 characters, may contain A–Z, a–z, 0–9, _ and -) and the bot checks it when it connects.
  3. Choose a game against the computer. A badge in the corner of the window shows that the bot is connected (for now in Czech), and the bot starts receiving the state.

The browser may ask whether the page may access apps on this computer. Allow it.

Rules

The smallest bot

Node.js 18 or newer and the ws package (npm install ws). The bot sends idle Pawns to mine gold and trains another Pawn. The rest is up to you.

The API reference (bot-api.md) and the comments in the example bot are in Czech for now.

// Nejmenší bot pro TinyHold (Bot API, protokol 1 — https://tinyhold.cz/Boti).
//
// Spuštění:  npm install ws  &&  node bot-minimal.mjs
// Pak otevři adresu, kterou bot vypíše. Bot posílá nečinné dělníky těžit zlato
// a dokud je na to, cvičí dalšího dělníka. Zbytek je na tobě.
import { randomBytes } from 'node:crypto'
import { WebSocketServer } from 'ws'

const PORT = Number(process.env.PORT ?? 7777)
const KEY = process.env.BOTKLIC ?? randomBytes(12).toString('hex')

const server = new WebSocketServer({
  host: '127.0.0.1',
  port: PORT,
  // Doporučení specifikace (§2.1): kromě klíče ověřit i Origin — spojení smí navázat jen
  // stránka hry samotná (produkce nebo lokální vývojový server), ne libovolná cizí stránka
  // s otevřeným prohlížečem.
  verifyClient: info => info.origin === 'https://tinyhold.cz' || /^http:\/\/(localhost|127\.0\.0\.1):\d+$/.test(info.origin),
})
console.log(`Bot čeká na ws://127.0.0.1:${PORT}`)
console.log(`Otevři hru: https://tinyhold.cz/hrat/?bot=ws://127.0.0.1:${PORT}&botklic=${KEY}`)

server.on('connection', ws => {
  let trusted = false
  let game = null
  let request = 0
  ws.on('message', data => {
    let msg
    try {
      msg = JSON.parse(String(data))
    } catch {
      return // Ignoruj zprávy, které nejsou JSON
    }
    if (typeof msg !== 'object' || msg === null) return // Ignoruj zprávy, které nejsou objekt (null, čísla, řetězce)
    // První zpráva musí být hello se správným klíčem; jinak se zavře spojení.
    if (!trusted) {
      if (msg.type !== 'hello' || msg.key !== KEY) {
        ws.close()
        return
      }
      trusted = true
      console.log(`Připojeno: TinyHold ${msg.game?.version}`)
    } else if (msg.type === 'gameStarted') {
      // `map` = navigační mřížka (passable/buildable) a její revize; po znovupřipojení aktuální.
      game = { id: msg.gameId, slot: msg.you.slot, grid: msg.map }
      console.log(`Nová partie na mapě ${msg.mapId}, hraju za slot ${game.slot}`)
    } else if (msg.type === 'navigationGrid' && game != null && msg.gameId === game.id) {
      // Mapa se změnila (stavba, pokácený strom…). Mřížka platí pro snímek, který přijde hned za ní.
      game.grid = { ...game.grid, revision: msg.revision, passable: msg.passable, buildable: msg.buildable }
    } else if (msg.type === 'turn' && game != null && msg.turn % 5 === 0) {
      const commands = decide(msg.snapshot, game.slot)
      if (commands.length > 0) {
        ws.send(JSON.stringify({ type: 'commands', requestId: `r${++request}`, gameId: game.id, commands }))
      }
    } else if (msg.type === 'gameEnded') {
      console.log(`Konec partie: ${msg.result}, vítězný tým ${msg.winnerTeam}`)
      game = null
    }
  })
})

function decide(snapshot, slot) {
  const me = snapshot.players.find(p => p.slot === slot)
  const mine = snapshot.entities.filter(e => e.owner === slot)
  const castle = mine.find(e => e.type === 'castle' && e.built)
  if (me == null || castle == null) return []
  const commands = []
  // Nečinné dělníky k nejbližšímu dolu se zlatem.
  const mines = snapshot.entities.filter(e => e.type === 'goldmine' && e.amount > 0)
  const goldmine = nearest(mines, castle)
  const idle = mine.filter(e => e.type === 'pawn' && e.order.kind === 'idle')
  if (goldmine != null && idle.length > 0) {
    commands.push({ kind: 'gather', units: idle.map(e => e.id), target: goldmine.id })
  }
  // Dalšího dělníka, když je zlato, místo v populaci a hrad zrovna nic necvičí.
  if (me.gold >= 50 && me.pop < me.popCap && castle.production.queue.length === 0) {
    commands.push({ kind: 'train', building: castle.id, what: 'pawn' })
  }
  return commands
}

function nearest(list, from) {
  let best = null
  let bestDistance = Infinity
  for (const e of list) {
    const dx = e.pos.x - from.pos.x
    const dy = e.pos.y - from.pos.y
    const d = dx * dx + dy * dy
    if (d < bestDistance) { bestDistance = d; best = e }
  }
  return best
}

Download bot-minimal.mjs

Full specification

Protocol specification (Markdown): all messages, the game state, orders, error codes and the rules catalog. It is also handy to paste it into an AI assistant that helps you write the bot.