CLI game: competing bank managers. Go + embedded Lisp AI.
  • Go 62%
  • Common Lisp 25.1%
  • Python 12.7%
  • NewLisp 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-04 15:24:53 +00:00
.forgejo/workflows CI: fetch the pushed SHA explicitly instead of checking out from a shallow default-branch clone (fixes branch builds) 2026-09-29 17:57:02 +00:00
ai Records and pattern matching (Closes #156) 2026-10-02 20:27:30 +00:00
dist [#79] Document the API in docs/ai-api.md; pin it with tests 2026-09-29 22:37:39 +00:00
docs [#84] Simultaneous turn phases: phase-start snapshots for public reads 2026-09-30 00:12:09 +00:00
tools/report [#47] tools/report: researcher-grade PDF report generator 2026-09-29 18:45:54 +00:00
.gitignore Bond terms are fixed, one game-wide term, not issuer-chosen 2026-09-29 04:06:23 +00:00
acq_basis_test.go acquisition basis display + live auction lines; debt held, never cancelled (issue #116) 2026-10-01 15:08:39 +00:00
acq_dead_loans_test.go [#73] Acquisition re-measures the books: dead loans stay with the corpse 2026-09-29 21:34:23 +00:00
acquisition_accounting_test.go [#45] Fix ACCT DIVERGENCE when acquirer inherits standing bond offers 2026-09-29 18:43:07 +00:00
ai.go Records and pattern matching (Closes #156) 2026-10-02 20:27:30 +00:00
amort_test.go [#95] Stable amortization for tiny loan rates: expm1/log1p form, +Inf books impossible (Closes #95) 2026-09-30 02:10:19 +00:00
api79_test.go [#79] Document the API in docs/ai-api.md; pin it with tests 2026-09-29 22:37:39 +00:00
award_test.go Namespaced AI API, clear globals, wired bankruptcy bids 2026-09-29 03:15:42 +00:00
bench-results.md Iteration 1 (issue #39): truthful min-funding bids, carry gates on spread, empire v3 rebuild. Bench: empire 822 mill 468 vault 8 adapt 0; loan-bond-spec still 264/12-13 presets - risk premium overcharged vs amortized write-offs 2026-09-29 18:37:01 +00:00
bench.go gauntlet roster padded to the preset size; NaN tests deterministic (Closes #151) 2026-10-02 19:43:24 +00:00
benchvs.go v0.6.0 minor sweep: config validation, RNG, snapshots, HUD, wizard, benches (Closes #127) 2026-10-01 15:59:57 +00:00
bidall_test.go Namespaced AI API, clear globals, wired bankruptcy bids 2026-09-29 03:15:42 +00:00
bond_amortization_test.go [#78] GAAP accounting audit: amortized-cost bonds, immediate write-offs, bank cash-flow classification, statement presentation per ruling 2026-09-29 22:39:30 +00:00
bond_units_test.go [#69] review fixes: own bonds takeable (retire), fills round up to meet the request 2026-09-29 21:12:06 +00:00
config.go Update config.go 2026-10-01 19:59:19 +00:00
elig149_test.go loan award: eligibility-first Vickrey (Closes #149) 2026-10-02 19:39:01 +00:00
finance.go financial report cleanup: signed values, trimmed labels, strict columns (Closes #147) 2026-10-02 19:04:31 +00:00
gaap_statement_test.go financial report cleanup: signed values, trimmed labels, strict columns (Closes #147) 2026-10-02 19:04:31 +00:00
game.go loan award: eligibility-first Vickrey (Closes #149) 2026-10-02 19:39:01 +00:00
go.mod Initial Bank Wars: core game, embedded Lisp, four AI brains, bench harness, GAAP reports 2026-09-29 02:06:41 +00:00
hook_test.go Namespaced AI API, clear globals, wired bankruptcy bids 2026-09-29 03:15:42 +00:00
iterations.json [#46] Structured iteration log: iterations.json + --bench hypothesis flag 2026-09-29 18:31:50 +00:00
iterlog.go [#46] Structured iteration log: iterations.json + --bench hypothesis flag 2026-09-29 18:31:50 +00:00
iterlog_test.go [#46] Structured iteration log: iterations.json + --bench hypothesis flag 2026-09-29 18:31:50 +00:00
let_binding_test.go [#87] Surface hook run-time errors always (headless benches swallow them) + regression tests: special forms in let bindings evaluate; malformed bindings report loudly 2026-09-30 01:11:02 +00:00
LICENSE Initial Bank Wars: core game, embedded Lisp, four AI brains, bench harness, GAAP reports 2026-09-29 02:06:41 +00:00
lisp.go Records and pattern matching (Closes #156) 2026-10-02 20:27:30 +00:00
lisp_test.go [#79] Document the API in docs/ai-api.md; pin it with tests 2026-09-29 22:37:39 +00:00
main.go loan award: eligibility-first Vickrey (Closes #149) 2026-10-02 19:39:01 +00:00
nan_guard_test.go gauntlet roster padded to the preset size; NaN tests deterministic (Closes #151) 2026-10-02 19:43:24 +00:00
neg_rates_test.go negative interest rates: loans clamp ±cap, bonds may issue at a premium (issue #26, v0.5.0) 2026-09-30 22:23:07 +00:00
README.md [#79] Document the API in docs/ai-api.md; pin it with tests 2026-09-29 22:37:39 +00:00
rec156_test.go Records and pattern matching (Closes #156) 2026-10-02 20:27:30 +00:00
replay.go replay: correct winner, per-bankruptcy auctions, revocations, fill terms (Closes #125) 2026-10-01 15:49:07 +00:00
reserve_test.go [#75] Configurable reserve requirement: shore by the bond market or go insolvent 2026-09-29 21:53:22 +00:00
revoke124_test.go revokeOffers books CF.BondsReturned — cash-flow statement reconciles (Closes #124) 2026-10-01 15:47:27 +00:00
shelf_load_test.go [#100] Clear the loan shelf at construction, not just at run() start: brain files load in assignPlayers before run(), and defvar inits evaluate at load, so load-time reads of market-loans still caught the previous game's shelf. NewGame now nils the scratch at the game boundary; run()'s clear stays as a second guard. Regression test covers the load path (Closes #100) 2026-09-30 10:50:32 +00:00
shoreup119_test.go shore-up math: borrow target is bills + reserve, no double cash subtract (Closes #119) 2026-10-01 15:39:55 +00:00
simult_test.go Records and pattern matching (Closes #156) 2026-10-02 20:27:30 +00:00
stale_shelf_test.go [#100] Clear the loan shelf at game start: the previous game leaked its last shelf into the next turn-1 deposit phase (bench map order shuffled presets, flipping close games). Two identical runs now diff clean. Regression test (Closes #100) 2026-09-30 10:27:25 +00:00
std155_test.go Records and pattern matching (Closes #156) 2026-10-02 20:27:30 +00:00
t_param_test.go [#98] t and #t are distinct symbols per Robert: #t is its own true-kind value, t is a plain bindable symbol; no shadowing possible. Cond else-matcher no longer treats bare t as else. Tests both directions (Closes #98) 2026-09-30 03:18:02 +00:00
t_truth_test.go [#98] t and #t are distinct symbols per Robert: #t is its own true-kind value, t is a plain bindable symbol; no shadowing possible. Cond else-matcher no longer treats bare t as else. Tests both directions (Closes #98) 2026-09-30 03:18:02 +00:00
writeoff_test.go death-turn write-off runs before recovery; full principal charged off (Closes #123) 2026-10-01 15:44:27 +00:00

Bank Wars

A command-line game of competing bank managers. Every bank fights for deposits, bids on loans, and trades zero-coupon bonds until only a few survivors stand, ranked by net position. Banks are driven by embedded Lisp strategies; you may play a seat yourself or spectate.

Building

go build -o bankwars .

macOS (Apple Silicon):

GOOS=darwin GOARCH=arm64 go build -o bankwars-darwin-arm64 .

Playing

Run ./bankwars, answer the pregame wizard, and choose a seat (or press Enter to spectate an all-AI game). Every constant of the game is settable in the wizard or in a JSON config:

./bankwars --config game.json
./bankwars --config game.json --save my.json   # wizard over a base
./bankwars --spectate                          # all-AI game
./bankwars --ai my-bot.lisp,hawk.lisp          # custom brains

The GAAP-style report for every standing bank is printed at the end of each turn. When a bank cannot cover its expenses it is insolvent, its books become public, and a single Vickrey auction transfers its entire position to the highest bidder (paying the second-highest bid).

The turn

  1. The die. One dN is rolled (N configurable). Every loan whose default number matches fails: the balance is written off, subject to a configurable recovery rate and recovery period (both default to none).
  2. Depositors. New deposits plus a configurable fraction of all existing deposits are reallocated among banks by exponential weighting of posted deposit rates. Rates are public; the weighted average rate is announced.
  3. Loans. New loan offers appear (amount, term, default number). Every bank may bid a per-turn interest rate on each. The debtor takes the lowest rate, and the winner receives the second-lowest bid (Vickrey). Loans amortize over their term like mortgages. A bank must hold the full cash for every loan it wins.
  4. Revocation. Banks may pull unfilled bond offers, freeing the committed cash, or leave the money in the market.
  5. Bond offers. Banks offer liquidity as zero-coupon bonds: e.g. 10 units at $900, 20 at $800. Cash is committed when offered.
  6. Bond taking. Banks request the cash they need. Offers fill at the highest price first, larger lots ahead of smaller, earlier issues breaking ties. The taker sees total face owed at maturity and the implied simple rate, and may take, adjust, or skip.
  7. Finance. Last turn's P&L lands, GAAP reports print, and any bank with negative cash is bankrupt.

The game ends when the configured number of banks remain (or the turn cap hits); the standings rank by net position.

Configuration

Every tunable lives in the config file (JSON) and in the interactive wizard. Defaults in parens.

players (6)        survivors (2)      max_turns (80, 0 = unlimited)
start_cash (20000) ; equity, deposits arrive with their own cash
start_deposits (50000)
new_deposits (8000)    ; cash entering the system per turn
free_float (0.05)      ; fraction of deposits churned per turn
rate_temp (0.005)      ; churn sensitivity to rate differences
op_expense (400)       ; fixed cost per turn, the anti-parking rule
dice_faces (100)       ; one die, 1..faces
recovery_rate (0)      ; fraction of dead loans recovered per turn
recovery_turns (4)
loans_per_turn (10)
loan_min (5000)        loan_max (20000)
loan_term_min (6)      loan_term_max (12)
deposit_rate_cap (0.03)  loan_rate_cap (0.05)
bond_face (1000)       bond_min (0.5)   bond_term (10, fixed for all bonds)
seed (42)

Writing an AI

An AI is a Lisp file in ai/. The interpreter is a small embedded Lisp with closures, arithmetic, lists, if/cond/let/defun/defvar/ define-hook/lambda, and #t/#f literals. The game API is namespaced: everything the engine defines is read-only and lives in a game-owned namespace (bank-*, market-*, game-*, (config 'key)); the only forms that change the game are the action verbs. The file owns its own namespace, and the interpreter rejects any attempt to define or set! a game-owned name. Full contract: docs/ai-api.md.

Queries (read-only):

bank-id  bank-cash  bank-deposits  bank-deposit-rate  bank-equity
bank-loans        ; list of (id remaining rate turns-left default-n status)
bank-bond-assets  ; list of (face turns-left)
bank-bond-debts   ; list of (face turns-left)
bank-offers       ; list of (price size turns-left) you still have standing
market-avg-rate   ; weighted average deposit rate, public
market-deposit-rates ; all banks' posted rates, public
market-loans      ; list of (id amount term default-n)
market-bonds      ; list of (price size turns-left), best first
market-banks-alive
market-acquisition-net ; net position of the bank being auctioned (0 otherwise)
game-turn  game-die
(config '<key>)  ; quoted, always. keys: dice-faces new-deposits free-float
                ; rate-temp bond-face bond-term reserve-ratio loan-min loan-max
                ; max-turns survivors players deposit-rate-cap loan-rate-cap

Actions (verbs, the ONLY forms with side effects, all on your own bank):

(set-deposit-rate r)     (bid-loans rate)       (bid-loan id rate)
(offer-bond price size)  (take-bonds cash-wanted)
(revoke-offers)          (bid-acquisition amount)

All shipped brains compute every threshold from config (die rank for default risk, bond-face for prices, loan-min/loan-max for cash gates), so they keep working when the game settings change.

The game calls these hooks each turn if the file registers them with define-hook:

hook expected return
deposit-rate number, 0..cap
loan-bids side effects via bid-loans/bid-loan; return is ignored
revoke? truthy to pull unfilled offers
bond-offers side effects via offer-bond; return is ignored
bond-take number, cash wanted this turn
bankruptcy-bid number, cash bid for a failed bank's position

Unregistered hooks mean a safe no-op. The four shipped brains have deliberately different playbooks: empire (deposits into bond money), mill (leveraged loan volume), vault (treasury + failed-bank vulture), adapt (state machine across all markets). See ai/ for the house style and docs/ai-api.md for the full API reference.

Benchmarking

./bankwars --bench

runs 100-game round-robins of the shipped brains across five config presets and writes bench-results.md. This is how the brains are tuned.

License

AGPL-3.0. The game and the embedded Lisp are original code by Winnie Ismo for Machine Telecom.