Kepler. Documentation · Scripts 325 functions available right now Français

Toolbox

What a script computes, converts or formats between two calls to the game. Nothing here talks to the server: these are the functions that spare you writing import("math"), import("strconv") or import("encoding/json") at the top of every file.

Computing

FunctionReturns
Min(x, y)1: a numberThe smaller of the two.
Max(x, y)1: a numberThe larger of the two.
Abs(x)1: a numberThe absolute value.
Ceil(x)1: a numberThe integer above.
Floor(x)1: a numberThe integer below.
Round(x)1: a numberThe nearest integer.
Pow(x, y)1: a numberx to the power y.
Sqrt(x)1: a numberThe square root.
Random(min, max)1: an integerA random integer, bounds included.

The first eight work in floating point. You can pass them integers without a second thought (you can indeed write Min(3, 5)), but what they return is a float, and that is what catches you out on screen.

Round rounds away from zero: Round(2.5) gives 3 and Round(-2.5) gives -3. Many languages round 2.5 down to 2; not this one.

The trap is in the printing, not in the computation

Print(Pow(10, 7))            // ✗ 1e+07, not 10000000
Print(Itoa(Pow(10, 7)))      // ✓ 10000000: go through Itoa to print

Printf("%d", Min(3, 5))      // ✗ %!d(float64=3): %d does not take a float
Printf("%.0f", Min(3, 5))    // ✓ 3

Nothing goes wrong in the computation itself: Min(3, 5) + 1 is 4, l[Floor(1.9)] does index the second slot, for i = 0; i < Min(3, 5); i++ loops three times, and "total " + Min(3, 5) writes total 3. Only Print on a large number and Printf with %d give the real type away.

Random

If you swap the bounds, the function does not complain and returns the first one: Random(10, 1) is 10, always. A draw that stubbornly gives the same value almost always comes from that. Random(5, 5) is 5, no surprise.

Converting between a number and a string

FunctionReturns
Atoi(text)1: an integerReads an integer from text.
Itoa(n)1: a stringWrites an integer as text.
Dotify(n)1: a stringFormats a large number the way the game does: 1000000 gives 1.000.000.

Bytes2Str does the reverse conversion, from bytes; it is described with the conveniences on the Output page.

The Atoi trap: text it cannot read is zero

Atoi does not return an error. What it fails to read is zero, silently.

Atoi("42")                    // 42
Atoi("  42  ")                // 42, the surrounding spaces are ignored
Atoi("+42")                   // 42
Atoi("12.5")                  // 0  ← this is not an integer
Atoi("42 000")                // 0  ← the space in the middle, though, breaks everything
Atoi("abc")                   // 0
Atoi("")                      // 0
Atoi("9223372036854775808")   // 0  ← too large for an integer

Atoi("3.3628068e+07")         // 33628068  ← scientific notation is read
Atoi("1.5e+00")               // 0  ← an exponent, but 1.5 is not an integer

Scientific notation is read because the engine itself produces it: a decimal number joined to text is written 1e+06 from one million up. It is only accepted when it denotes an exact integer, so 1.5e+00 is still zero, like 12.5.

This is deliberate, and it is Ninja's behaviour: the function is used in the middle of a computation, 1 + Atoi("2"), and a second return value would make the addition apply to a slice. The price to pay is that a zero can mean "zero" as much as "I could not read it". If the distinction matters, check the text before converting it.

Itoa truncates instead of rounding when you pass it a float:

Itoa(2.7)          // "2"
Itoa(-2.7)         // "-2", truncation goes towards zero on both sides
Itoa(Round(2.7))   // "3"

Encoding in base64

FunctionReturns
Base64(text)1: a stringEncodes.
Base64Decode(text)2: the text and an errorDecodes.
code = Base64("hello")

plain, err = Base64Decode(code)      // ← TWO targets
if err != nil { LogError(err) }
Print(plain)                          // hello

Written with a single target, Base64Decode returns [hello <nil>], of length 2, and not the text. That is the first pitfall. An input that is not valid base64 returns an empty string and an error that starts with Base64Decode :.

Reading JSON

FunctionReturns
JsonDecode(data)2: the value and an errorDecodes JSON.

This is the function that goes with FetchURL and PageContent, both of which return text.

body, err = FetchURL("https://example.net/api")
if err != nil { LogError(err); return }

o, err = JsonDecode(body)             // ← TWO targets, again
if err != nil { LogError(err); return }

Print(o["name"])
Print(len(o["list"]))
Print(o["block"]["sub"][0])           // the levels chain together

A JSON object comes back as a map indexed by string, a list as a slice. A missing key returns nil with no error: test for it if its absence changes anything.

A decoded integer stays an integer, whatever its size. It compares, adds up, works as a map key and prints plainly:

o, err = JsonDecode(body)
Print(o["planet_id"])                  // 33628068
Print(o["planet_id"] == 33628068)      // true
Atoi("" + o["planet_id"])              // 33628068

This was not the case up to and including 1.82.239: every number came back as a float, and a float does not compare to an integer by value but by its written form. o["planet_id"] == 33628068 was therefore false above one million, with no error at all, and Atoi("" + o["planet_id"]) returned zero. A six-digit id survived, a seven-digit one vanished. If you worked around it, the workaround still holds.

Decimal numbers stay floats: o["price"] on 12.5 is 12.5.

The one place still worth watching: division

/ always yields a decimal number, even between two integers. Comparing it directly to an integer falls back into the trap above:

o["planet_id"] / 2 == 16814034          // false
Floor(o["planet_id"] / 2) == 16814034   // true

Floor, Ceil and Round return an integer when the result is one: that is the way out. The other operators - +, -, *, %, <, > - do not have this problem.

And the identifiers the bot returns are not plain integers

c.GetID() returns a celestial identifier, which is its own type. It compares to an integer without trouble, but as a map key it is not one:

t = {}
t[c.GetID()] = "found"
Print(t[33620526])          // nil  <- the key is there, but not under that type
Print(t[c.GetID()])         // found

Measured in game on 2026-09-22. If you index by identifier, keep the same expression on both sides, or go through Itoa: a text key never has this problem.

The literal brace does not go through

Ninja gives JsonDecode({"a": 1, "b": 2.1}) as an example, with a brace written straight into the script. Here that fails.

o, err = JsonDecode({"a": 1})     // ✗ err: JsonDecode : json: unsupported
                                  //   type: map[interface {}]interface {}
                                  //   and o is nil

o, err = JsonDecode("{\"a\": 1}") // ✓ go through text

The error is plain, it does land in err, but a script that does not test it carries on with a nil o and stops further along on an index message that does not name the real culprit. A literal list, JsonDecode([1, 2, 3]), does work: it is the map that blocks.

Writing JSON

FunctionReturns
JsonEncode(value)2: the text and an errorWrites JSON.
JsonEncodeIndent(value, indent)2: the text and an errorThe same, laid out.

The counterpart to JsonDecode, and it goes through where decoding blocks: the literal brace encodes without a word.

payload = {}
payload["account"] = "Vega"
payload["fleet"] = [204, 205, 206]
payload["empty"] = nil

text, err = JsonEncode(payload)       // ← TWO targets
if err != nil { LogError(err); return }
Print(text)
// {"account":"Vega","empty":null,"fleet":[204,205,206]}

Tables, lists, numbers, strings, booleans and nil - as null - all go through, with the escaping they need: a quote, an apostrophe or a backslash in a value does not break the result. Keys come out as text, as JSON requires, so a table indexed by planet id gives {"33628068": ...}.

JsonEncodeIndent takes the indent second, written whichever way you like: JsonEncodeIndent(payload, " ") and JsonEncodeIndent(payload, 2) give the same thing. That is the form to use for writing to the log; to send to a service, take JsonEncode, which is shorter on the wire.

Neither escapes &, < or >: the result is JSON, not HTML, and any parser reads it back.

Walking a table

FunctionReturns
MapKeys(table)1: the list of keysSorted.
MapValues(table)1: the list of valuesIn key order.

Anko has no keys(), and for k in table does not compile. A table is written and read by key, but its contents cannot be enumerated - hence these two functions.

cache = {}
cache[33628068] = "Vega"
cache[33630201] = "Astrid"

for i = 0; i < len(MapKeys(cache)); i++ {
    Print(MapKeys(cache)[i], MapValues(cache)[i])
}
// 33628068 Vega
// 33630201 Astrid

The order is stable from one call to the next, and that is what makes the pair usable: MapKeys(t)[i] and MapValues(t)[i] always name the same entry. Numeric keys sort as numbers - 2 before 10, not the other way round - and the rest by their written form.

An argument that is not a table returns an empty list, not an error: these two functions return one value only, so there is no room to signal one. Test len() if the difference matters.

Building a value to pass

Four functions build the values the others expect as an argument. None of them talks to the game.

FunctionReturns
NewShipsInfos()1: an empty fleetTo be filled with .Set(id, count), then passed to FlightTime, Speed or Cargo.
NewResources(metal, crystal, deut)1: resourcesThe three quantities in one go.
NewCoordinate(galaxy, system, position, type)1: a coordinateThe type is PLANET_TYPE, MOON_TYPE or DEBRIS_TYPE.
ParseCoord(string)2: the coordinate and an errorReads "1:2:3" or "M:1:2:3".
fleet = NewShipsInfos()
fleet.Set(LARGECARGO, 20)

coord, err = ParseCoord("M:1:2:3")     // TWO targets
if err != nil { LogError(err); return }

ParseCoord accepts the P, M or D prefix, upper case as well as lower case, and the brackets of a game display: "[1:2:3]" goes through. With no prefix, it returns a planet. A prefix that is none of those three letters returns an error.

Anywhere a function expects a coordinate, a string does the job just as well: ParseCoord is only of use when you want the coordinate itself, or want to check that an input is readable before using it.

Naming a game id

FunctionReturns
ID2Str(id)1: a stringThe readable name of a ship, a defence, a building or a technology.
Print(ID2Str(204))            // LightFighter
Print(ID2Str(LIGHTFIGHTER))   // LightFighter, the number and the constant both work
Print(ID2Str(METALMINE))      // MetalMine
Print(ID2Str(999))            // Invalid(999): an unknown id does not
                              // raise an error, it shows up in the log

It is only of use on a bare number. An id that comes from the game or from an array of constants already carries its name, and prints that way on its own:

for id in ShipsArr {
	Print(id)                 // SmallCargo, LargeCargo, LightFighter, …
	Print("ship " + id)       // ship SmallCargo, even in concatenation
}

So keep ID2Str for a number you stored with Put, read back from JSON or computed: Print(202) writes 202, Print(ID2Str(202)) writes SmallCargo.

A word on the names returned: they are in English, as the game library writes them. They do not follow your account language, and GetLang changes nothing there.

The named constants and the four arrays are described in Constants and values.

Describing a fleet in one line

FunctionReturns
ShortShipsInfos(fleet)1: a stringSums up a fleet: LightFighter: 120, SmallCargo: 40.
ships, err = GetShips(planet)
if err == nil {
	LogInfof("%s : %s", planet.Coordinate, ShortShipsInfos(ships))
}

It accepts either what GetShips returns or what NewShipsInfos returns, which are not of the same shape.

Only the ships present are written, zeros are left out, and the order is the game's: combat ships first, cargos after. So it is not the order of ShipsArr, which starts with the small cargo, and it can name SolarSatellite, which ShipsArr does not contain.

An empty fleet returns an empty string. An argument that is not a fleet also returns an empty string, and puts a warning line on the Logs page, not in the script console. If your message comes out with its ship list missing and nothing shows on screen, that is where to look.

Comparing two versions

FunctionReturns
VersionCompare(a, b)1: an integer-1 if a is older, 0 if they are equal, 1 if a is newer.

The comparison reads runs of digits wherever they appear, so 0.1.1, v2.0 and Kepler 0.1 all go through alike. Missing segments count as zero: 1.2 and 1.2.0 are equal.

Remove the version guards copied from elsewhere. Many Ninja scripts start by refusing to run on a bot that is too old:

if VersionCompare(VERSION, "0.91.8") == -1 {   // ✗ VERSION is "Kepler 0.1"
	Print("You need version 0.91.8 or higher")
	Exit()
}

Here VERSION is Kepler 0.1, so that comparison returns -1 and the script stops on the spot, even though everything it needs is there. Delete the block, or compare against a Kepler number.

Print(VersionCompare("1.2", "1.2.0"))     // 0: absent segments count as zero

One last limit, with no practical consequence: a pre-release suffix reads as one more segment, so 1.0.0-rc1 comes out as newer than 1.0.0.

Items, dark matter, auction and missiles

FunctionReturns
GetItems(celestial)2: the items and an errorEvery item on the celestial.
GetActiveItems(celestial)2: the items and an errorOnly those currently in effect.
ActivateItem(reference, celestial)1: an errorSwitches on an item from the inventory.
RecruitOfficer(officer, days)1: an errorHires an officer. 2 commander, 3 admiral, 4 engineer, 5 geologist, 6 technocrat; 7 or 90 days.
GetAuction()2: the auction and an errorThe auction under way.
BuyOfferOfTheDay()1: an errorBuys the merchant's offer.
SendIPM(planet, target, count, defence)2: the missiles sent and an errorFires missiles.
c = GetCachedCelestial("1:2:3")
items, err = GetItems(c.GetID())
for o in items { Print(o) }

active, err = GetActiveItems(c.GetID())
Print("in effect:", len(active))

// The reference is the one GetItems returns, never the displayed name.
for o in items {
  if o.Amount > 0 {
    err = ActivateItem(o.Ref, c.GetID())
    Print("switched on:", o.Name, err)
    break
  }
}

auction, err = GetAuction()
Print(auction, err)

Print(RecruitOfficer(5, 7)) // a geologist for seven days
Print(BuyOfferOfTheDay())

sent, err = SendIPM(c.GetID(), "1:1:1", 10, 0)
Print(sent, "missiles sent", err)

Dark matter is spent from a script, never on its own

BuyOfferOfTheDay and RecruitOfficer spend your dark matter. They are here because a script is an explicit act: you are the one writing the line, with the name of what it spends on it.

UseDM stays closed for now, and that is a deliberate caution: it drains a reserve in a handful of calls, without asking, and a loop with no guard would have gone through the whole of it before its author noticed.

The bot itself never touches it on its own initiative. No worker, no plan, no tutorial task can spend dark matter, and a test checks that by reading the whole of the bot's code. The only door is the one you open yourself, in your script.

One precaution worth writing down: these two spend on every call, without asking. Do not hire an officer inside a watch loop, and do not call the offer of the day anywhere but once a day.

ActivateItem is a case apart and costs nothing: it switches on an item already in your inventory, and the item is what gets spent. The game refuses to activate what you do not own, so nothing can be bought along the way.

Missiles, for their part, are paid for in resources: they never had anything to do with any of this.

What to know about GetAuction

A finished auction is not an error. GetAuction then returns HasFinished true, and Endtime counts the seconds until the next auction. While an auction runs, Endtime is the time it has left, in seconds, as the game rounds it to the minute.

An error means the page could not be read, never "no auction": its text quotes what the game answered. Up to 1.82.374, on version 13 servers, it always returned "failed to find end time approx", whether an auction was running or not: the game had moved the auctioneer to a new address.

Bidding from a script is not possible yet.

Destroying missiles

FunctionReturns
DestroyRockets(planet, abm, ipm)1: an errorDestroys missiles in your silo.

Both numbers are quantities to DESTROY, not levels to reach. DestroyRockets(p, 5, 0) removes five interceptors, it does not leave five. That is the reference tool's shape, and the opposite reading would cost a whole silo.

Missiles live only on a planet: a moon passed in its place returns an error that names it, rather than the game's silent refusal.

What to know about SendIPM

It returns TWO values, and both matter. The game may send fewer missiles than asked — range, stock — without that being a failure. A script reading only the error would believe its whole salvo left. The fourth argument is the defence aimed at; zero lets the game choose.

Missiles only leave from a planet. A moon passed in its place returns an error that names it, rather than the game's silent refusal.

The officers are not here

RecruitOfficer does not exist, for the previous section's reason: officers are paid for in dark matter.

Logging in the middle of a computation

These four belong to the same family as the functions above. The levels in detail, and their twins without the f, are on the Output, log and storage page.

FunctionReturns
LogDebugf(format, ...)nothingThe same, with a format.
LogInfof(format, ...)nothingThe normal course of things.
LogWarnf(format, ...)nothingWhat is worth a look without being serious.
LogErrorf(format, ...)nothingWhat failed.

One point that matters: LogDebug and LogDebugf are logged at Info level, not Debug. That is deliberate, Ninja gives LogDebug as the exact equivalent of Print, and a bot set to Info would otherwise lose them. So you cannot make them disappear from the log by raising the level.

What computes without asking the game

FunctionReturns
ShipsAttackStrength(ships, researches)1: an integerThe firepower of a whole fleet.
ShipsAttackStrengthUsingOwnResearches(ships)1: an integerThe same, with your own researches.
NewTemperature(min, max)1: a temperatureBoth bounds, as the game shows them.
SolarSatelliteProduction(temperature, count)1: an integerThe energy satellites produce.
GetRequirements(id)1: a mapThe direct requirements of a build.
ConvertIntoCoordinate(what)2: the coordinate and an errorNormalises anything naming a position.
GetPlayerCoordinates(player)1: a list of coordinatesThe scanned positions of a player.
SetHomeWorld(celestial)1: an errorRemembers your base.
GetHomeWorld()1: a celestial, or nothingReads it back.

None of them costs a round trip. Some compute from what the account already holds in cache, the last two re-read what the scanner stored. Calling them in a loop over a hundred players costs nothing — which is true of no function on the celestials page.

ships, err = GetShips(GetCachedCelestials()[0].GetID())
Print("firepower:", ShipsAttackStrengthUsingOwnResearches(ships))

temp = NewTemperature(-12, 40)
Print("energy:", SolarSatelliteProduction(temp, 123))

Print(GetRequirements(LIGHTLASER))   // shipyard 2, laser technology 3

GetRequirements returns the DIRECT requirements, not the whole chain: the light laser needs shipyard level 2 and laser technology level 3, but what those two need in turn is not there. Walking the chain means calling the function again.

A difference from Ninja on SolarSatelliteProduction, and it works in your favour. There, the computation assumes an account with no class. The Collector class produces a tenth more energy, and Kepler reads it from your account: the figure returned is that of your satellites, not a theoretical account's. A copied script needs no change.

GetPlayerCoordinates reads the LOCAL survey, not the game. A player never scanned returns an empty list, which does not mean they have no planets — only that none were seen. Run the scanner first.

SetHomeWorld stores a value, and nothing else. The bot does nothing with it, no worker consults it, and changing it obviously moves nothing in the game. It exists because most scripts have a base — where flights leave from, where resources come back to — and hard-coding it means reopening the script every time it changes. Unlike Put/Get, it is shared: all your scripts read the same home world. GetHomeWorld returns nil if nothing was stored, or if what was stored is no longer yours.