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.
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.Prohlížeč se může zeptat, jestli stránka smí přistupovat k aplikacím na tomto počítači — povol to.
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
}
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.