-
Notifications
You must be signed in to change notification settings - Fork 0
Example 08 build a calculator
Josh Dionne edited this page Aug 25, 2026
·
1 revision
Generated from the thetowersdk repository. Edits made here are overwritten on the next push — change the source file instead.
/**
* Example 8 — Build a calculator without writing any calculator.
*
* A builder carries its own defaults, a description of every input, a pure
* `compute`, and notes explaining anything it could not do. That is enough to
* render a whole tool generically: this file draws a form and a result table for
* a calculator it never names.
*
* Swap the id on the command line and the same twenty lines render a different
* calculator. That is the point — the UI is yours, the plumbing is not.
*
* Run it:
* npx tsx examples/08-build-a-calculator.ts
* npx tsx examples/08-build-a-calculator.ts uw.stones
*/
import { CALCULATOR_BUILDERS, findCalculatorBuilder } from 'thetowersdk/builders'
import { formatDuration, formatLargeNumber } from 'thetowersdk/formatting'
const requestedId = process.argv[2] ?? 'lab.research'
const builder = findCalculatorBuilder(requestedId)
if (!builder) {
console.error(`No calculator "${requestedId}". Available:`)
for (const option of CALCULATOR_BUILDERS) console.error(` ${option.id.padEnd(20)} ${option.title}`)
process.exit(1)
}
console.log(`${builder.title} — ${builder.summary}\n`)
/*
* Step 1 — render the form.
*
* Nothing below knows which calculator this is. `fields` says what each input is
* called, what kind of control it needs, its range, and its options; `defaults`
* says what to show before the user touches anything.
*/
const defaults = builder.defaults as Record<string, unknown>
console.log('Inputs:')
for (const field of builder.fields) {
const bits: string[] = [field.kind]
if (field.unit) bits.push(field.unit)
if (field.min !== undefined || field.max !== undefined) bits.push(`${field.min ?? '-'}..${field.max ?? '-'}`)
if (field.options) bits.push(`${field.options.length} options`)
/*
* A `number-list` default is a whole row, so it is summarised rather than printed —
* a real UI would render one control per entry here.
*/
const shown = Array.isArray(defaults[field.key])
? `[${(defaults[field.key] as unknown[]).length} values]`
: String(defaults[field.key])
console.log(` ${field.label.padEnd(22)} ${shown.padEnd(16)} (${bits.join(', ')})`)
if (field.help) console.log(` ${' '.repeat(22)} ${field.help}`)
}
console.log()
/*
* Step 2 — compute.
*
* `compute` takes a partial, so you can hand it exactly what the user has typed
* so far. Missing fields fall back to the defaults; out-of-range ones are
* clamped. It does not throw, so no try/catch is needed around a keystroke.
*/
const result = builder.compute({} as never) as unknown as Record<string, unknown>
console.log('Result:')
for (const [key, value] of Object.entries(result)) {
if (key === 'notes') continue
// Rows render as a count; a UI would draw the table.
if (Array.isArray(value)) {
console.log(` ${key.padEnd(22)} ${value.length} row(s)`)
continue
}
if (typeof value === 'number') {
/*
* Pick the formatter by what the number MEANS, not by its type.
*
* `formatLargeNumber` rounds to whole numbers below its first suffix — right for coins,
* because nobody holds 0.5 of one, but it turns a 0.5 ratio into "1". A fraction has to
* go through a percentage instead.
*/
const shown = /hours?$/i.test(key)
? formatDuration(value * 3600)
: /ratio|reduction|share|fraction/i.test(key)
? `${(value * 100).toFixed(1)}%`
: /percent$/i.test(key)
? `${value.toFixed(1)}%`
: formatLargeNumber(value)
console.log(` ${key.padEnd(22)} ${shown}`)
continue
}
console.log(` ${key.padEnd(22)} ${String(value)}`)
}
/*
* Step 3 — show what it could not do.
*
* Every builder returns `notes`. Surfacing them is the difference between a tool
* that says "nothing to buy, your target is below your current level" and one
* that shows a confident zero.
*/
const notes = result.notes as string[]
if (notes.length > 0) {
console.log('\nNotes:')
for (const note of notes) console.log(` - ${note}`)
}
console.log(`\nOther calculators: ${CALCULATOR_BUILDERS.map(b => b.id).join(', ')}`)Generated from TheTowerSDK — do not edit here. Docs and live demos: https://tmrxjd.github.io/TheTowerSDK/
- Quick Start
- Entry Points
- How It Fits Together
- Getting a Save File
- Reading a Save
- Reading The Community Wiki
- Formulas
- Builders
- Charts
- Effective Paths
- Examples
- Templates
- Building a Bot On This
- Google Sheets
- Desktop and Mobile
- Optional Add-ons
- Using It With An AI Agent
- Using Your Own Artwork
- Names And Acronyms
- Patch Notes
- Accuracy
- Versioning
- Where the Docs Live
- Contributing
- Credits
- License
Guides
Examples
- 01-browse-game-data.ts
- 02-read-a-save-file.ts
- 03-plan-upgrades-from-a-save.ts
- 04-generate-a-chart.ts
- 05-generate-a-cost-table.ts
- 06-read-the-community-wiki.ts
- 07-format-like-the-game.ts
- 08-build-a-calculator.ts
- 09-build-a-bot.ts
- 10-read-a-sheet.ts
- 11-build-a-knowledge-base.ts
- 12-show-module-and-card-art.ts
Templates