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.
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.The browser may ask whether the page may access apps on this computer. Allow it.
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
}
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.