Postav si vlastního bota

Bot API · protokol 1

Nechceš nic instalovat? Zkus Programuj přímo v prohlížeči — píšeš bota v editoru vedle hry.

TinyHold umí předat hru tvému programu. Hra v prohlížeči se připojí k botovi, který běží na tvém počítači, posílá mu pravidla a stav hry a přijímá od něj stejné rozkazy, jaké dáváš myší. Napsat ho můžeš v čem chceš — stačí, aby uměl WebSocket.

Jak začít

  1. Spusť bota jako WebSocket server na svém počítači, třeba na portu 7777.
  2. Otevři hru s adresou https://tinyhold.cz/hrat/?bot=ws://127.0.0.1:7777&botklic=muj-tajny-klic-1234. Klíč si vymysli (16–128 znaků, smí obsahovat A–Z, a–z, 0–9, _ a -) a bot ho při připojení zkontroluje.
  3. Zvol hru proti počítači. V rohu okna uvidíš „Bot: připojen" a bot začne dostávat stav.

Prohlížeč se může zeptat, jestli stránka smí přistupovat k aplikacím na tomto počítači — povol to.

Pravidla

Nejmenší bot

Node.js 18 nebo novější a balíček ws (npm install ws). Bot posílá nečinné dělníky těžit zlato a cvičí dalšího dělníka — zbytek je na tobě.

// 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
}

Stáhnout bot-minimal.mjs

Úplná specifikace

Specifikace protokolu (Markdown) — všechny zprávy, stav hry, rozkazy, chybové kódy a katalog pravidel. Hodí se ji vložit i do AI asistenta, který ti bota pomůže napsat.