Repository navigation
Expand file tree
/
Copy pathwealth.py
More file actions
317 lines (289 loc) · 12.6 KB
/
Copy pathwealth.py
File metadata and controls
317 lines (289 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
"""Track your money: the BANK ACCOUNT report now and every few hours: ;wealth
;wealth (autostarts) ask BANK ACCOUNT after login and every 3 hours,
log every branch, and keep listening for tellers
;wealth now ask again now — typed at the running script, or from cold
for one report and out
BANK ACCOUNT works from anywhere: a local runs to the Estate Holders'
Council and comes back with a slip listing every branch (captured
2026-09-12, "You flag down a local you know works with the Estate
Holders' Council ..."); the option is free on a Premium account and an
urchin-runner (SimuCoins) service otherwise, so a silent answer is
reported as such and not retried until the next interval — except a
BANK ACCOUNT the game refused for roundtime ("...wait 1 seconds."),
asked again once the seconds have passed (#268). A character with no
account gets the slip with zero totals and "You have no open bank
accounts!" (captured 2026-09-26): that ends the report with nothing on
deposit, said so, and WEALTH and the summary follow as for any other —
it read as a silent answer and drew the Premium advice until #330.
The first ask waits START_DELAY (45 s) past the start, clear of the
autostarted ;sheet's login INV LIST, whose roundtime refused a BANK
ACCOUNT sent in the same second (#330). Each branch
line becomes a `bank` wealth row in ~/.revenant/history.db (the branch
in the `bank` column, the report's own copper total as the amount; its
Totals block is skipped, the branches add up to it); WEALTH follows
(INFO's Wealth and Debt blocks without the character sheet), its
carried coin and provincial debt logged as `carried` and `debt` rows
(the shape ;sheet writes), and the two are echoed as a summary per
currency as a list: its net, then each bank, carrying and owing, each
amount in its largest coin. A teller's balance
line ("Your current balance is ...", "As expected, there are ...")
heard in passing is logged the same way. The beholder Wealth view
reads the table, newest row per item. Stop with: ;stop wealth;
REVENANT_NO_WEALTH=1 or the Settings dialog turns the autostart off.
Balance grammar per lich's common-money (amounts like "1 platinum,
3 gold, 5 silver and 2 copper" convert at 10000/1000/100/10/1).
"""
import os
import re
import sqlite3
import time
from datetime import datetime, timezone
from pathlib import Path
from client.game import probe
from client.game.history import database_path as history_database_path
from client.game.money import parse_wealth, phrase, short, to_copper
INTERVAL = 3 * 3600 # seconds between BANK ACCOUNT asks
# Seconds after the start before the first ask: past the login noise and
# ;sheet's INV LIST (sent about 20 s in, 3-4 s of roundtime), which
# refused a BANK ACCOUNT sent in the same second (#330, 2026-09-26).
START_DELAY = 45
REPORT_WAIT = 20 # seconds a report gets to arrive before it counts as none
REPORT_SETTLE = 3 # seconds of silence after a branch line that end a report
PURSE_SECONDS = 3 # WEALTH's answer, opening window
PURSE_TAIL = 1.5 # ... and the tail past its (nonexistent) roundtime
NO_REPORT = (
"bank account gave no report — the ACCOUNT option is free on a Premium "
"account and an urchin-runner service otherwise"
)
clock = time.monotonic # tests replace it
_BALANCE = re.compile(
r"(?:current balance is|As expected, there are) "
r"(?P<amounts>.+?) (?P<currency>Kronars?|Lirums?|Dokoras?)\b"
)
# A branch line of the BANK ACCOUNT report: name, the copper total,
# a bar, the denominations, the currency last. The Totals block's
# lines end in a denomination, not a currency, so they never match.
_DEPOSIT = re.compile(
r"^\s*(?P<bank>[A-Za-z][A-Za-z' \-]*?):\s+(?P<copper>\d+)\s*\|\s.*?"
r"\b(?P<currency>Kronars|Lirums|Dokoras)\s*$"
)
_REPORT_END = re.compile(r"You have \d+ open bank accounts?")
# The report of a character with no account: zero totals, then this
# (captured 2026-09-26, #330) — a report of nothing, not a silence.
_NO_ACCOUNTS = re.compile(r"You have no open bank accounts")
NO_ACCOUNTS = "no bank account open — nothing on deposit"
# BANK ACCOUNT sent inside a roundtime did not run (#268, 2026-09-21):
# "...wait 1 seconds." — asked again once the seconds have passed.
_WAIT = re.compile(r"^\.\.\.wait (\d+) seconds?\.")
WAIT_PAD = 0.5
def parse_balance(line):
"""(currency, copper) from a teller's balance line, or None."""
match = _BALANCE.search(line)
if not match:
return None
total = to_copper(match.group("amounts"))
currency = match.group("currency")
if not currency.endswith("s"):
currency += "s"
return currency, total
def parse_deposit(line):
"""(bank, currency, copper) from a BANK ACCOUNT branch line, or None."""
match = _DEPOSIT.match(line)
if not match:
return None
return (
match.group("bank").strip(),
match.group("currency"),
int(match.group("copper")),
)
def database_path() -> Path:
return history_database_path()
SCHEMA = """
CREATE TABLE IF NOT EXISTS wealth (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
logged_at TEXT NOT NULL,
character_name TEXT NOT NULL,
kind TEXT NOT NULL,
currency TEXT NOT NULL,
copper INTEGER NOT NULL,
bank TEXT
)
"""
def ensure_schema(connection):
"""The table, and the `bank` column on a table from before it."""
connection.execute(SCHEMA)
columns = {row[1] for row in connection.execute("PRAGMA table_info(wealth)")}
if "bank" not in columns:
connection.execute("ALTER TABLE wealth ADD COLUMN bank TEXT")
connection.commit()
def record(connection, character, currency, copper, bank=None, logged_at=None):
"""One `bank` row; a report's branches share a logged_at."""
ensure_schema(connection)
connection.execute(
"INSERT INTO wealth (logged_at, character_name, kind, currency, copper, bank)"
" VALUES (?, ?, ?, ?, ?, ?)",
(
logged_at or datetime.now(timezone.utc).isoformat(),
character,
"bank",
currency,
copper,
bank,
),
)
connection.commit()
def record_held(connection, character, wealth, logged_at=None):
"""WEALTH's carried and debt as `carried`/`debt` rows, one per
currency, the shape ;sheet writes; {(kind, currency): copper}."""
ensure_schema(connection)
stamp = logged_at or datetime.now(timezone.utc).isoformat()
held = {}
for kind in ("carried", "debt"):
for currency, copper in wealth.get(kind, {}).items():
connection.execute(
"INSERT INTO wealth (logged_at, character_name, kind, currency, copper)"
" VALUES (?, ?, ?, ?, ?)",
(stamp, character, kind, currency, copper),
)
held[(kind, currency)] = copper
connection.commit()
return held
def latest_carried_and_debt(connection, character):
"""{(kind, currency): copper} — the newest carried and debt rows
logged, per currency (;sheet's from INFO or this script's from WEALTH)."""
ensure_schema(connection)
latest = {}
for kind, currency, copper in connection.execute(
"SELECT kind, currency, copper FROM wealth"
" WHERE character_name = ? AND kind IN ('carried', 'debt')"
" ORDER BY logged_at, seq",
(character,),
):
latest[(kind, currency)] = copper
return latest
def summary(report, held):
"""The money as a list (the operator, 2026-10-12: "people like
lists"): a heading per currency with its net, then a line per bank
branch, largest first, and the coins carried and owed when there are
any; each amount money.short's. No column padding: the main window's
font is proportional, where padded columns drift.
report is {(bank, currency): copper}; held is latest_carried_and_debt's."""
blocks = []
currencies = sorted({c for _, c in report} | {c for _, c in held})
for currency in currencies:
branches = {b: v for (b, c), v in report.items() if c == currency}
carried = held.get(("carried", currency), 0)
owed = held.get(("debt", currency), 0)
if not (sum(branches.values()) or carried or owed):
continue # a currency the character has nothing in
rows = sorted(branches.items(), key=lambda kv: -kv[1])
rows += [
(label, v) for label, v in (("carrying", carried), ("owing", owed)) if v
]
net = sum(branches.values()) + carried - owed
blocks.append((f"{currency}: {short(net)} net", rows))
lines = []
for heading, rows in blocks:
lines.append(heading)
lines.extend(f" {label} {short(copper)}" for label, copper in rows)
return lines
def main(s):
character = (
(s.state.name if s.state else None)
or os.environ.get("REVENANT_CHARACTER")
or "unknown"
)
once = "now" in [str(arg).lower() for arg in (s.args or [])]
path = database_path()
path.parent.mkdir(parents=True, exist_ok=True)
connection = sqlite3.connect(path)
if not once:
s.echo(
"tracking money — BANK ACCOUNT now and every 3 hours, tellers "
"overheard; ;wealth now asks again, ;stop wealth stops"
)
next_ask = clock() + (0 if once else START_DELAY)
asked_at = None # when the last BANK ACCOUNT went out, until answered
report, report_stamp, last_branch_at = {}, None, None
def finish():
"""The report is in: WEALTH for what is carried and owed right now
(logged like ;sheet's), then the summary of both."""
nonlocal asked_at, report, report_stamp, last_branch_at
held = record_held(
connection,
character,
parse_wealth(probe.ask(s, "wealth", PURSE_SECONDS, PURSE_TAIL)),
)
if not held: # WEALTH went unanswered: the newest logged figures
held = latest_carried_and_debt(connection, character)
if lines := summary(report, held):
# One block, one prefix: the list reads as a list in the window.
s.echo("\n".join(["wealth:", *lines]))
asked_at, report, report_stamp, last_branch_at = None, {}, None, None
try:
while True:
request = s.command(timeout=0)
if request is not None and "now" in request.lower():
next_ask = clock()
if clock() >= next_ask:
s.put("bank account")
asked_at, next_ask = clock(), clock() + INTERVAL
report, report_stamp = {}, None
line = s.get(timeout=1)
if line is None:
if (
report
and last_branch_at
and clock() - last_branch_at > REPORT_SETTLE
):
finish()
if once:
return
elif asked_at and not report and clock() - asked_at > REPORT_WAIT:
s.echo(f"wealth: {NO_REPORT}")
asked_at = None
if once:
return
continue
held_back = _WAIT.match(line.strip())
if held_back and asked_at is not None and not report:
# BANK ACCOUNT went out inside a roundtime and did not
# run (#268, 2026-09-21): ask again once it has passed.
next_ask = clock() + int(held_back.group(1)) + WAIT_PAD
asked_at = None
continue
balance = parse_balance(line)
if balance is not None:
currency, copper = balance
record(connection, character, currency, copper)
s.echo(f"bank balance noted: {phrase(copper, currency)}")
continue
if "on deposit" in line:
report_stamp = datetime.now(timezone.utc).isoformat()
continue
deposit = parse_deposit(line)
if deposit is not None:
bank, currency, copper = deposit
record(
connection,
character,
currency,
copper,
bank=bank,
logged_at=report_stamp,
)
report[(bank, currency)] = copper
last_branch_at = clock()
continue
if report and _REPORT_END.search(line):
finish()
if once:
return
continue
if asked_at is not None and not report and _NO_ACCOUNTS.search(line):
s.echo(f"wealth: {NO_ACCOUNTS}")
finish()
if once:
return
finally:
connection.close()