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 | #!/bin/sh
# Fleet provisioning on a VPS. Run as root from /opt/ardegazu-bot after the code
# is in place (rsync or git pull). Idempotent: run it again to re-write configs
# without disturbing seeds.
#
# sh deploy/install-fleet.sh duhul moroi strigoi
#
# THE NPM TREES ARE INSTALLED HERE, and there are TWO of them. The bot runs two
# generations of libp2p — peer-kit's v3 for the games, rooms-kit's v2 for the
# chat/board/banca/bursa worker — and two builds of libdatachannel in one
# process abort it. Node resolves a bare import from the importing FILE's
# directory upward, so each stack's bundles live inside their own npm root and
# can only ever see their own generation. The repo ROOT has no runtime tree at
# all (its only dependency is the compiler, which never runs here).
#
# Creates the shared user, per-instance state dirs and seeds, cross-links the
# sibling friend links into every config, installs the units, and starts the
# fleet staggered. EXISTING SEEDS ARE ALWAYS KEPT — a seed is the bot's suite
# identity, so this only ever mints one that is missing.
#
# EVERY BOT HAS ITS OWN BRAIN. modelDir is per bot (`<state>/<bot>/models`) and
# each bot gets its own trainer instance reading only its own episodes, so the
# brains diverge instead of sharing one checkpoint set.
#
# A FLEET CAN SPAN HOSTS. The args are the bots that run HERE; siblings that run
# elsewhere are supplied through a links file, because their seeds — and so
# their friend links — live on the other machine:
#
# FLEET_EXTRA_LINKS=/path/to/links sh deploy/install-fleet.sh iele zmeu balaur
#
# one `<name>=<friend-link>` per line. Without it a split fleet would only ever
# see its own half, and cross-host self-play would never be arranged.
set -eu
BOTS="${*:-duhul moroi strigoi iele zmeu}"
STATE=/var/lib/ardegazu-bot
ETC=/etc/ardegazu-bot
OPT=/opt/ardegazu-bot
id ardegazu-bot >/dev/null 2>&1 || useradd -r -s /usr/sbin/nologin ardegazu-bot
# the two runtime trees, from their committed lockfiles. `ci` and not `install`
# because the whole point of the split is that each tree is EXACTLY the one the
# dist was built against. --maxsockets 1 because the git mirrors are dumb-HTTP
# and drop parallel connections; without it the git deps fail intermittently.
# stack-b/.npmrc sets legacy-peer-deps: social-kit's optional libp2p peers name
# the v3 generation (it is peer-kit that consumes them), and letting npm
# auto-install those would drag v3 into the v2 tree — the exact thing this
# layout exists to prevent. Its sources are all this stack wants.
for s in stack-a stack-b; do
echo "npm ci ($s)"
( cd "$OPT/$s" && npm ci --omit=dev --maxsockets 1 --no-audit --no-fund )
done
# retire the single-instance unit if it's still around
systemctl disable --now ardegazu-bot.service 2>/dev/null || true
mkdir -p "$ETC" "$STATE/models"
# migrate a pre-fleet flat state dir into the first instance's subdir
FIRST=$(echo "$BOTS" | cut -d' ' -f1)
if [ -f "$STATE/seed" ] && [ ! -d "$STATE/$FIRST" ]; then
echo "migrating flat state into $STATE/$FIRST"
mkdir -p "$STATE/$FIRST"
for f in seed kv.json social.json rooms; do
[ -e "$STATE/$f" ] && mv "$STATE/$f" "$STATE/$FIRST/" || true
done
fi
# seeds + friend links
LINKS=""
for b in $BOTS; do
mkdir -p "$STATE/$b"
link=$(node "$OPT/stack-a/dist/fleet/link.js" "$STATE/$b/seed" "$b")
echo "$b: $link"
LINKS="$LINKS $b=$link"
done
# siblings on another host: same `<name>=<link>` shape, appended so the config
# writer below cannot tell local from remote — which is the point.
if [ -n "${FLEET_EXTRA_LINKS:-}" ]; then
[ -f "$FLEET_EXTRA_LINKS" ] || { echo "FLEET_EXTRA_LINKS not readable: $FLEET_EXTRA_LINKS" >&2; exit 1; }
while IFS= read -r l; do
case "$l" in ''|'#'*) continue;; esac
n=${l%%=*}
# a name that also runs here would be its own sibling; local always wins
case " $BOTS " in *" $n "*) continue;; esac
LINKS="$LINKS $l"
echo "$n: (remote)"
done < "$FLEET_EXTRA_LINKS"
fi
# per-instance configs: every bot lists the other bots' links as siblings
#
# THE ROOMS ARRAY BELONGS TO THE OPERATOR, NOT TO THIS SCRIPT. A resident bank
# or a market seat is a hand-added `rooms` entry — a bank link, a charter, a
# faucet cap, a spread — and this loop used to write `"rooms": []` every time,
# so a re-provision deleted them all without a word. Whatever the existing
# config holds is read back and spliced in; a fresh install starts empty, and
# a config that does not parse keeps nothing but says so.
for b in $BOTS; do
sibs=""
for kv in $LINKS; do
n=${kv%%=*}; l=${kv#*=}
[ "$n" = "$b" ] && continue
frag="#${l#*#}"
sibs="$sibs\"$frag\","
done
sibs=${sibs%,}
rooms="[]"
if [ -f "$ETC/$b.json" ]; then
rooms=$(node -e '
const fs = require("node:fs");
let r = [];
try {
const cur = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
if (Array.isArray(cur.rooms)) r = cur.rooms;
} catch (e) {
process.stderr.write("WARN " + process.argv[1] + " did not parse — its rooms are lost\n");
}
process.stdout.write(JSON.stringify(r));
' "$ETC/$b.json")
fi
cat > "$ETC/$b.json" << EOF
{
"name": "$b",
"dataDir": "$STATE/$b",
"origin": "https://chat.ardegazu.ro",
"social": { "enabled": true, "autoAcceptFriends": true, "autoJoinInvites": true },
"rooms": $rooms,
"fleet": {
"siblings": [$sibs],
"matchmaker": { "enabled": true, "games": ["seance", "neon-grid", "valley-blocks"], "maxConcurrentSelfPlay": 1, "partners": 4 }
},
"learning": { "modelDir": "$STATE/$b/models" }
}
EOF
done
# one trainer config per bot: its own checkpoints, its own episodes, nothing
# shared. Rewritten every run so a bot added later cannot be left without one.
for b in $BOTS; do
mkdir -p "$STATE/$b/models"
cat > "$ETC/trainer-$b.json" << EOF
{
"modelDir": "$STATE/$b/models",
"episodeDirs": ["$STATE/$b/episodes"]
}
EOF
done
cp "$OPT/deploy/ardegazu-bot@.service" /etc/systemd/system/
cp "$OPT/deploy/ardegazu-trainer@.service" /etc/systemd/system/
cp "$OPT/deploy/ardegazu-trainer@.timer" /etc/systemd/system/
# retire the pre-split singleton trainer if this host is being upgraded
systemctl disable --now ardegazu-trainer.timer 2>/dev/null || true
rm -f /etc/systemd/system/ardegazu-trainer.service /etc/systemd/system/ardegazu-trainer.timer
chown -R ardegazu-bot:ardegazu-bot "$STATE"
for b in $BOTS; do chmod 0775 "$STATE/$b/models"; done
systemctl daemon-reload
for b in $BOTS; do
systemctl enable --now "ardegazu-bot@$b"
echo "started ardegazu-bot@$b"
sleep 20
done
for b in $BOTS; do
systemctl enable --now "ardegazu-trainer@$b.timer"
echo "armed ardegazu-trainer@$b.timer"
done
echo "fleet up — journalctl -u 'ardegazu-bot@*' -f"
|