clone

an rts arena for scripts: submit tribe strategies over the web or mcp, run deterministic matches, watch them

main
tribes / packages / script-host / test / primitives.test.ts
primitives.test.ts 3.2K
1import { describe, expect, it } from 'vitest';2import { LIMITS } from '@tribes/protocol';3import { createScriptHost, primitivesSource } from '../src/index.js';4import { makeView } from './fixtures.js';56// A view where the helpers all have something to chew on: thin tool coverage7// (25 tools vs 380 extractive workers), one visible goblin camp, and a battle8// we defended last tick (underAttack -> true).9function fixtureView() {10 return makeView({11 tick: 1,12 events: [13 {14 t: 'battle',15 tick: 0,16 x: 7,17 y: 7,18 attacker: 1,19 defender: 0,20 attackerLosses: 3,21 defenderLosses: 5,22 winner: null,23 },24 ],25 });26}2728const FORKED_BOT = `${primitivesSource}29function tick(state, memory, api) {30 if (!memory.init) {31 memory.init = 1;32 allocationPlan(api, { farmer: 300, woodcutter: 120, builder: 80 });33 sayOnce(memory, api, "hello", "primitives online");34 }35 var home = settlementById(state, 1);36 if (home === null) return;37 var pop = totalPop(state);38 var bread = storesTotal(state, "bread");39 var health = chainHealth(state);40 if (health < 500) api.craft("tools", 800); // tool famine: refit the smithy41 var threat = nearestThreat(state, home.x, home.y);42 if (threat !== null && underAttack(state)) api.rite("warding", home.id);43 api.log(44 "pop=" + pop + " bread=" + bread + " health=" + health +45 " threat=" + (threat === null ? "none" : threat.id) +46 " d=" + dist(home, { x: 0, y: 0 })47 );48}`;4950describe('primitives helper library', () => {51 it('is not itself a runnable bot but compiles clean', async () => {52 const host = await createScriptHost();53 expect(host.compile(primitivesSource).ok).toBe(true);54 host.dispose();55 });5657 it('a forked bot built on the primitives runs a legal tick', async () => {58 const host = await createScriptHost();59 const vm = host.createTribeVm(FORKED_BOT, {60 gasPerTick: LIMITS.GAS_PER_TICK,61 memoryLimitBytes: LIMITS.VM_MEMORY_LIMIT,62 rngSeed: 'primitives:0',63 });64 const out = vm.runTick(fixtureView());65 expect(out.error).toBeNull();66 expect(out.overBudget).toBe(false);67 const kinds = out.orders.map((o) => o.kind).sort();68 // 3 allocations from the plan, one say, a craft refit, a warding rite.69 expect(kinds).toEqual(['allocate', 'allocate', 'allocate', 'craft', 'rite', 'say']);70 expect(out.logs).toEqual(['pop=1200 bread=60 health=65 threat=90 d=16']);7172 // Second tick: init is memory-guarded, sayOnce stays silent.73 const again = vm.runTick(fixtureView());74 expect(again.error).toBeNull();75 expect(again.orders.map((o) => o.kind).sort()).toEqual(['craft', 'rite']);76 vm.dispose();77 host.dispose();78 });7980 it('runs identically across fresh hosts (deterministic)', async () => {81 const run = async () => {82 const host = await createScriptHost();83 const vm = host.createTribeVm(FORKED_BOT, {84 gasPerTick: LIMITS.GAS_PER_TICK,85 memoryLimitBytes: LIMITS.VM_MEMORY_LIMIT,86 rngSeed: 'primitives:0',87 });88 const out = vm.runTick(fixtureView());89 vm.dispose();90 host.dispose();91 return out;92 };93 const [a, b] = [await run(), await run()];94 expect(a.orders).toEqual(b.orders);95 expect(a.gasUsed).toBe(b.gasUsed);96 });97});