- Go 62%
- Common Lisp 25.1%
- Python 12.7%
- NewLisp 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
build / build (push) Successful in 26s
Reviewed-on: #165 |
||
| .forgejo/workflows | ||
| ai | ||
| dist | ||
| docs | ||
| tools/report | ||
| .gitignore | ||
| acq_basis_test.go | ||
| acq_dead_loans_test.go | ||
| acquisition_accounting_test.go | ||
| ai.go | ||
| amort_test.go | ||
| api79_test.go | ||
| award_test.go | ||
| bench-results.md | ||
| bench.go | ||
| benchvs.go | ||
| bidall_test.go | ||
| bond_amortization_test.go | ||
| bond_units_test.go | ||
| config.go | ||
| elig149_test.go | ||
| finance.go | ||
| gaap_statement_test.go | ||
| game.go | ||
| go.mod | ||
| hook_test.go | ||
| iterations.json | ||
| iterlog.go | ||
| iterlog_test.go | ||
| let_binding_test.go | ||
| LICENSE | ||
| lisp.go | ||
| lisp_test.go | ||
| main.go | ||
| nan_guard_test.go | ||
| neg_rates_test.go | ||
| README.md | ||
| rec156_test.go | ||
| replay.go | ||
| reserve_test.go | ||
| revoke124_test.go | ||
| shelf_load_test.go | ||
| shoreup119_test.go | ||
| simult_test.go | ||
| stale_shelf_test.go | ||
| std155_test.go | ||
| t_param_test.go | ||
| t_truth_test.go | ||
| writeoff_test.go | ||
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
- 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).
- 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.
- 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.
- Revocation. Banks may pull unfilled bond offers, freeing the committed cash, or leave the money in the market.
- Bond offers. Banks offer liquidity as zero-coupon bonds: e.g. 10 units at $900, 20 at $800. Cash is committed when offered.
- 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.
- 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.