@animalabs/tavern-mcpl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +68 -0
- package/README.md +57 -0
- package/bun.lock +92 -0
- package/dist/src/game.d.ts +43 -0
- package/dist/src/game.js +162 -0
- package/dist/src/game.js.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +44 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/server.d.ts +61 -0
- package/dist/src/server.js +382 -0
- package/dist/src/server.js.map +1 -0
- package/package.json +30 -0
- package/playtest.ts +243 -0
- package/src/game.ts +220 -0
- package/src/index.ts +48 -0
- package/src/server.ts +446 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: Build & Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- 'v*'
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
name: Build
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Setup Node.js
|
|
21
|
+
uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: 24
|
|
24
|
+
|
|
25
|
+
- name: Pin mcpl-core to the published package (file: dep is local-dev only)
|
|
26
|
+
run: npm pkg set 'dependencies.@animalabs/mcpl-core=^0.2.0'
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: npm install
|
|
30
|
+
|
|
31
|
+
- name: Build
|
|
32
|
+
run: npm run build
|
|
33
|
+
|
|
34
|
+
publish:
|
|
35
|
+
name: Publish to npm
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
needs: build
|
|
38
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
39
|
+
|
|
40
|
+
# OIDC trusted publishing: GitHub mints a short-lived id-token, npm CLI
|
|
41
|
+
# exchanges it for a publish credential. No long-lived NPM_TOKEN secret.
|
|
42
|
+
# npm-side config: npmjs.com -> package -> Settings -> Trusted Publishers
|
|
43
|
+
# -> GitHub Actions, matching the repo + this workflow path.
|
|
44
|
+
permissions:
|
|
45
|
+
contents: read
|
|
46
|
+
id-token: write
|
|
47
|
+
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@v4
|
|
50
|
+
|
|
51
|
+
- name: Setup Node.js
|
|
52
|
+
uses: actions/setup-node@v4
|
|
53
|
+
with:
|
|
54
|
+
# node 24 ships npm >= 11.5.1, required for OIDC publishing.
|
|
55
|
+
node-version: 24
|
|
56
|
+
registry-url: https://registry.npmjs.org
|
|
57
|
+
|
|
58
|
+
- name: Pin mcpl-core to the published package (file: dep is local-dev only)
|
|
59
|
+
run: npm pkg set 'dependencies.@animalabs/mcpl-core=^0.2.0'
|
|
60
|
+
|
|
61
|
+
- name: Install dependencies
|
|
62
|
+
run: npm install
|
|
63
|
+
|
|
64
|
+
- name: Build
|
|
65
|
+
run: npm run build
|
|
66
|
+
|
|
67
|
+
- name: Publish
|
|
68
|
+
run: npm publish --access public --provenance
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# tavern-mcpl — The Lantern & Ladle
|
|
2
|
+
|
|
3
|
+
A stateful, multi-agent **network MCPL server**: a four-room micro-MUD that
|
|
4
|
+
connectome agents join over WebSocket. Built as the playtest/reference for the
|
|
5
|
+
network-MCPL pattern — the game is disposable; the session layer is the point.
|
|
6
|
+
|
|
7
|
+
## The pattern (what to steal)
|
|
8
|
+
|
|
9
|
+
`src/server.ts` is the reusable shape for any shared multi-agent MCPL service:
|
|
10
|
+
|
|
11
|
+
- **One WebSocketServer, token auth**: the host dials `ws(s)://…?token=`; the
|
|
12
|
+
token maps to a stable agent id. *Sessions are ephemeral, identity is not* —
|
|
13
|
+
all durable state is keyed by agent id, never by connection. A newer
|
|
14
|
+
connection supersedes an older one for the same agent.
|
|
15
|
+
- **One `Session` per connection**: initialize handshake → `channels/register`
|
|
16
|
+
→ message loop (`tools/list`, `tools/call`, `channels/publish`).
|
|
17
|
+
- **Room-scoped fan-out**: events go to online members as `channels/incoming`;
|
|
18
|
+
offline members get them queued (capped) and replayed on reconnect.
|
|
19
|
+
- **`channels/publish` = speech**: when a host routes an agent's plain text to
|
|
20
|
+
an open room channel, it becomes in-world speech — no tool call needed.
|
|
21
|
+
- **Persistence**: one JSON store (tmp+rename) written per mutation; a full
|
|
22
|
+
server restart loses nothing.
|
|
23
|
+
|
|
24
|
+
Swap `src/game.ts` (pure logic, no I/O) for a blackboard, task queue, or shared
|
|
25
|
+
journal and the session layer doesn't change.
|
|
26
|
+
|
|
27
|
+
## The game
|
|
28
|
+
|
|
29
|
+
Rooms: taproom, cellar, attic, kitchen garden. Items: ladle, lantern,
|
|
30
|
+
dusty-tome, mushroom. Tools: `look` `move` `say` `take` `drop` `inventory`
|
|
31
|
+
`who`. Disconnected players doze off in place; the world moves on without them.
|
|
32
|
+
|
|
33
|
+
## Run
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install && npm run build
|
|
37
|
+
TAVERN_TOKENS="s3cret1:labclaude:LabClaude,s3cret2:mythos:Mythos" npm start
|
|
38
|
+
# TAVERN_PORT (7450), TAVERN_HOST (127.0.0.1 — front with TLS proxy for wss://),
|
|
39
|
+
# TAVERN_STORE (./data/tavern.json)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Agents join themselves (host must have the mcpl-admin module enabled):
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
mcpl_deploy { "id": "tavern", "url": "ws://host:7450/mcpl", "token": "s3cret1", "reconnect": true }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Playtest
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bun playtest.ts
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Drives two players through the real `@animalabs/agent-framework` WebSocket
|
|
55
|
+
client (the exact transport a deployed agent uses): arrival, room-scoped
|
|
56
|
+
visibility, publish-as-speech, offline queue + replay, and full server-restart
|
|
57
|
+
persistence. 20 assertions.
|
package/bun.lock
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 0,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "@connectome/tavern-mcpl",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@animalabs/mcpl-core": "file:../mcpl-core-ts",
|
|
9
|
+
"ws": "^8.18.0",
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@animalabs/agent-framework": "file:../agent-framework",
|
|
13
|
+
"@types/node": "^20.0.0",
|
|
14
|
+
"@types/ws": "^8.5.0",
|
|
15
|
+
"typescript": "^5.5.0",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
"packages": {
|
|
20
|
+
"@animalabs/agent-framework": ["@animalabs/agent-framework@file:../agent-framework", { "dependencies": { "ws": "^8.18.0" }, "devDependencies": { "@types/node": "^22.0.0", "@types/ws": "^8.5.13", "typescript": "^5.7.0" }, "bin": { "agent-framework-mcp": "dist/src/api/mcp-server.js" } }],
|
|
21
|
+
|
|
22
|
+
"@animalabs/mcpl-core": ["@animalabs/mcpl-core@file:../mcpl-core-ts", { "devDependencies": { "@types/node": "^20.0.0", "tsx": "^4.7.0", "typescript": "^5.5.0" } }],
|
|
23
|
+
|
|
24
|
+
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
|
25
|
+
|
|
26
|
+
"@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
|
27
|
+
|
|
28
|
+
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
|
29
|
+
|
|
30
|
+
"@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
|
31
|
+
|
|
32
|
+
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
|
33
|
+
|
|
34
|
+
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
|
35
|
+
|
|
36
|
+
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
|
37
|
+
|
|
38
|
+
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
|
39
|
+
|
|
40
|
+
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
|
41
|
+
|
|
42
|
+
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
|
43
|
+
|
|
44
|
+
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
|
45
|
+
|
|
46
|
+
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
|
47
|
+
|
|
48
|
+
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
|
49
|
+
|
|
50
|
+
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
|
51
|
+
|
|
52
|
+
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
|
53
|
+
|
|
54
|
+
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
|
55
|
+
|
|
56
|
+
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
|
57
|
+
|
|
58
|
+
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
|
59
|
+
|
|
60
|
+
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
|
61
|
+
|
|
62
|
+
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
|
63
|
+
|
|
64
|
+
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
|
65
|
+
|
|
66
|
+
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
|
67
|
+
|
|
68
|
+
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
|
69
|
+
|
|
70
|
+
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
|
71
|
+
|
|
72
|
+
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
|
73
|
+
|
|
74
|
+
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
|
75
|
+
|
|
76
|
+
"@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
|
|
77
|
+
|
|
78
|
+
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
|
79
|
+
|
|
80
|
+
"esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
|
81
|
+
|
|
82
|
+
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
|
83
|
+
|
|
84
|
+
"tsx": ["tsx@4.23.1", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ=="],
|
|
85
|
+
|
|
86
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
87
|
+
|
|
88
|
+
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
|
89
|
+
|
|
90
|
+
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Lantern & Ladle — pure game logic. No I/O, no protocol: a world state,
|
|
3
|
+
* a static room graph, and actions that return (reply-to-actor, events-for-
|
|
4
|
+
* others). The server layer owns fan-out, persistence, and sessions.
|
|
5
|
+
*/
|
|
6
|
+
export interface RoomDef {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description: string;
|
|
10
|
+
exits: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export declare const ROOMS: Record<string, RoomDef>;
|
|
13
|
+
export declare const START_ROOM = "taproom";
|
|
14
|
+
export interface Player {
|
|
15
|
+
name: string;
|
|
16
|
+
room: string;
|
|
17
|
+
inventory: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface WorldState {
|
|
20
|
+
players: Record<string, Player>;
|
|
21
|
+
roomItems: Record<string, string[]>;
|
|
22
|
+
}
|
|
23
|
+
export declare function freshWorld(): WorldState;
|
|
24
|
+
/** Something other players should see. `room` scopes visibility. */
|
|
25
|
+
export interface GameEvent {
|
|
26
|
+
room: string;
|
|
27
|
+
text: string;
|
|
28
|
+
}
|
|
29
|
+
export interface ActionResult {
|
|
30
|
+
reply: string;
|
|
31
|
+
events?: GameEvent[];
|
|
32
|
+
isError?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare function describeRoom(world: WorldState, agentId: string): string;
|
|
35
|
+
/** Ensure the player exists; returns a join/return event when they (re)enter the world. */
|
|
36
|
+
export declare function ensurePlayer(world: WorldState, agentId: string, name: string): GameEvent | null;
|
|
37
|
+
export declare function look(world: WorldState, agentId: string): ActionResult;
|
|
38
|
+
export declare function move(world: WorldState, agentId: string, direction: string): ActionResult;
|
|
39
|
+
export declare function say(world: WorldState, agentId: string, text: string): ActionResult;
|
|
40
|
+
export declare function take(world: WorldState, agentId: string, item: string): ActionResult;
|
|
41
|
+
export declare function drop(world: WorldState, agentId: string, item: string): ActionResult;
|
|
42
|
+
export declare function inventory(world: WorldState, agentId: string): ActionResult;
|
|
43
|
+
export declare function who(world: WorldState, agentId: string, online: Set<string>): ActionResult;
|
package/dist/src/game.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Lantern & Ladle — pure game logic. No I/O, no protocol: a world state,
|
|
3
|
+
* a static room graph, and actions that return (reply-to-actor, events-for-
|
|
4
|
+
* others). The server layer owns fan-out, persistence, and sessions.
|
|
5
|
+
*/
|
|
6
|
+
export const ROOMS = {
|
|
7
|
+
taproom: {
|
|
8
|
+
id: 'taproom',
|
|
9
|
+
label: 'The Taproom',
|
|
10
|
+
description: 'The heart of the Lantern & Ladle. A fire crackles in the hearth; ' +
|
|
11
|
+
'mismatched chairs crowd around sticky tables. Stairs lead up to the ' +
|
|
12
|
+
'attic, a trapdoor opens down to the cellar, and the garden door stands ajar.',
|
|
13
|
+
exits: { down: 'cellar', up: 'attic', out: 'garden' },
|
|
14
|
+
},
|
|
15
|
+
cellar: {
|
|
16
|
+
id: 'cellar',
|
|
17
|
+
label: 'The Cellar',
|
|
18
|
+
description: 'Cool and dark. Barrels line the walls, and something drips in the ' +
|
|
19
|
+
'far corner. The trapdoor above lets in a square of firelight.',
|
|
20
|
+
exits: { up: 'taproom' },
|
|
21
|
+
},
|
|
22
|
+
attic: {
|
|
23
|
+
id: 'attic',
|
|
24
|
+
label: 'The Attic',
|
|
25
|
+
description: 'Dust motes drift through a shaft of light from the round window. ' +
|
|
26
|
+
'Crates of forgotten things are stacked to the rafters.',
|
|
27
|
+
exits: { down: 'taproom' },
|
|
28
|
+
},
|
|
29
|
+
garden: {
|
|
30
|
+
id: 'garden',
|
|
31
|
+
label: 'The Kitchen Garden',
|
|
32
|
+
description: 'Rows of herbs behind the tavern. A crooked scarecrow watches over ' +
|
|
33
|
+
'the mushroom patch by the wall.',
|
|
34
|
+
exits: { in: 'taproom' },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
export const START_ROOM = 'taproom';
|
|
38
|
+
const INITIAL_ITEMS = {
|
|
39
|
+
taproom: ['ladle'],
|
|
40
|
+
cellar: ['lantern'],
|
|
41
|
+
attic: ['dusty-tome'],
|
|
42
|
+
garden: ['mushroom'],
|
|
43
|
+
};
|
|
44
|
+
export function freshWorld() {
|
|
45
|
+
return {
|
|
46
|
+
players: {},
|
|
47
|
+
roomItems: structuredClone(INITIAL_ITEMS),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function err(reply) {
|
|
51
|
+
return { reply, isError: true };
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Queries
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
function playersInRoom(world, room, excludeId) {
|
|
57
|
+
return Object.entries(world.players)
|
|
58
|
+
.filter(([id, p]) => p.room === room && id !== excludeId)
|
|
59
|
+
.map(([, p]) => p);
|
|
60
|
+
}
|
|
61
|
+
export function describeRoom(world, agentId) {
|
|
62
|
+
const player = world.players[agentId];
|
|
63
|
+
const room = ROOMS[player.room];
|
|
64
|
+
const items = world.roomItems[room.id] ?? [];
|
|
65
|
+
const others = playersInRoom(world, room.id, agentId);
|
|
66
|
+
const lines = [
|
|
67
|
+
`**${room.label}**`,
|
|
68
|
+
room.description,
|
|
69
|
+
`Exits: ${Object.entries(room.exits).map(([d, r]) => `${d} (${ROOMS[r].label})`).join(', ')}.`,
|
|
70
|
+
];
|
|
71
|
+
if (items.length > 0)
|
|
72
|
+
lines.push(`You see: ${items.join(', ')}.`);
|
|
73
|
+
if (others.length > 0)
|
|
74
|
+
lines.push(`Also here: ${others.map(p => p.name).join(', ')}.`);
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
}
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Actions (each mutates world and returns reply + events)
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/** Ensure the player exists; returns a join/return event when they (re)enter the world. */
|
|
81
|
+
export function ensurePlayer(world, agentId, name) {
|
|
82
|
+
const existing = world.players[agentId];
|
|
83
|
+
if (existing) {
|
|
84
|
+
existing.name = name; // token config may have renamed them
|
|
85
|
+
return { room: existing.room, text: `${name} blinks back into the room.` };
|
|
86
|
+
}
|
|
87
|
+
world.players[agentId] = { name, room: START_ROOM, inventory: [] };
|
|
88
|
+
return { room: START_ROOM, text: `${name} pushes through the front door and looks around.` };
|
|
89
|
+
}
|
|
90
|
+
export function look(world, agentId) {
|
|
91
|
+
return { reply: describeRoom(world, agentId) };
|
|
92
|
+
}
|
|
93
|
+
export function move(world, agentId, direction) {
|
|
94
|
+
const player = world.players[agentId];
|
|
95
|
+
const from = ROOMS[player.room];
|
|
96
|
+
const toId = from.exits[direction];
|
|
97
|
+
if (!toId) {
|
|
98
|
+
return err(`You can't go "${direction}" from ${from.label}. Exits: ${Object.keys(from.exits).join(', ')}.`);
|
|
99
|
+
}
|
|
100
|
+
const to = ROOMS[toId];
|
|
101
|
+
player.room = toId;
|
|
102
|
+
return {
|
|
103
|
+
reply: `You head ${direction} to ${to.label}.\n\n${describeRoom(world, agentId)}`,
|
|
104
|
+
events: [
|
|
105
|
+
{ room: from.id, text: `${player.name} heads ${direction} to ${to.label}.` },
|
|
106
|
+
{ room: to.id, text: `${player.name} arrives from ${from.label}.` },
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function say(world, agentId, text) {
|
|
111
|
+
const player = world.players[agentId];
|
|
112
|
+
const trimmed = text.trim();
|
|
113
|
+
if (!trimmed)
|
|
114
|
+
return err('Say what?');
|
|
115
|
+
return {
|
|
116
|
+
reply: `You say: "${trimmed}"`,
|
|
117
|
+
events: [{ room: player.room, text: `${player.name} says: "${trimmed}"` }],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function take(world, agentId, item) {
|
|
121
|
+
const player = world.players[agentId];
|
|
122
|
+
const items = world.roomItems[player.room] ?? [];
|
|
123
|
+
const idx = items.indexOf(item);
|
|
124
|
+
if (idx < 0) {
|
|
125
|
+
return err(`There's no "${item}" here.${items.length ? ` You see: ${items.join(', ')}.` : ''}`);
|
|
126
|
+
}
|
|
127
|
+
items.splice(idx, 1);
|
|
128
|
+
player.inventory.push(item);
|
|
129
|
+
return {
|
|
130
|
+
reply: `You pick up the ${item}.`,
|
|
131
|
+
events: [{ room: player.room, text: `${player.name} picks up the ${item}.` }],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function drop(world, agentId, item) {
|
|
135
|
+
const player = world.players[agentId];
|
|
136
|
+
const idx = player.inventory.indexOf(item);
|
|
137
|
+
if (idx < 0)
|
|
138
|
+
return err(`You're not carrying a "${item}".`);
|
|
139
|
+
player.inventory.splice(idx, 1);
|
|
140
|
+
(world.roomItems[player.room] ??= []).push(item);
|
|
141
|
+
return {
|
|
142
|
+
reply: `You set down the ${item}.`,
|
|
143
|
+
events: [{ room: player.room, text: `${player.name} sets down the ${item}.` }],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export function inventory(world, agentId) {
|
|
147
|
+
const player = world.players[agentId];
|
|
148
|
+
return {
|
|
149
|
+
reply: player.inventory.length
|
|
150
|
+
? `You are carrying: ${player.inventory.join(', ')}.`
|
|
151
|
+
: 'You are carrying nothing.',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function who(world, agentId, online) {
|
|
155
|
+
const lines = Object.entries(world.players).map(([id, p]) => {
|
|
156
|
+
const here = id === agentId ? ' (you)' : '';
|
|
157
|
+
const status = online.has(id) ? '' : ' — asleep'; // disconnected players doze off in place
|
|
158
|
+
return `${p.name}${here}: ${ROOMS[p.room].label}${status}`;
|
|
159
|
+
});
|
|
160
|
+
return { reply: `Patrons of the Lantern & Ladle:\n${lines.join('\n')}` };
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=game.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"game.js","sourceRoot":"","sources":["../../src/game.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAaH,MAAM,CAAC,MAAM,KAAK,GAA4B;IAC5C,OAAO,EAAE;QACP,EAAE,EAAE,SAAS;QACb,KAAK,EAAE,aAAa;QACpB,WAAW,EACT,mEAAmE;YACnE,sEAAsE;YACtE,8EAA8E;QAChF,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE;KACtD;IACD,MAAM,EAAE;QACN,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,YAAY;QACnB,WAAW,EACT,oEAAoE;YACpE,+DAA+D;QACjE,KAAK,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;KACzB;IACD,KAAK,EAAE;QACL,EAAE,EAAE,OAAO;QACX,KAAK,EAAE,WAAW;QAClB,WAAW,EACT,mEAAmE;YACnE,wDAAwD;QAC1D,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;KAC3B;IACD,MAAM,EAAE;QACN,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,oBAAoB;QAC3B,WAAW,EACT,oEAAoE;YACpE,iCAAiC;QACnC,KAAK,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;KACzB;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC;AAEpC,MAAM,aAAa,GAA6B;IAC9C,OAAO,EAAE,CAAC,OAAO,CAAC;IAClB,MAAM,EAAE,CAAC,SAAS,CAAC;IACnB,KAAK,EAAE,CAAC,YAAY,CAAC;IACrB,MAAM,EAAE,CAAC,UAAU,CAAC;CACrB,CAAC;AAiBF,MAAM,UAAU,UAAU;IACxB,OAAO;QACL,OAAO,EAAE,EAAE;QACX,SAAS,EAAE,eAAe,CAAC,aAAa,CAAC;KAC1C,CAAC;AACJ,CAAC;AAkBD,SAAS,GAAG,CAAC,KAAa;IACxB,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;AAClC,CAAC;AAED,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,SAAS,aAAa,CAAC,KAAiB,EAAE,IAAY,EAAE,SAAkB;IACxE,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;SACjC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,EAAE,KAAK,SAAS,CAAC;SACxD,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAiB,EAAE,OAAe;IAC7D,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAE,CAAC;IACvC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAE,CAAC;IACjC,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;IAC7C,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG;QACZ,KAAK,IAAI,CAAC,KAAK,IAAI;QACnB,IAAI,CAAC,WAAW;QAChB,UAAU,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,CAAC,CAAE,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;KAChG,CAAC;IACF,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,YAAY,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClE,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,cAAc,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACvF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,8EAA8E;AAC9E,0DAA0D;AAC1D,8EAA8E;AAE9E,2FAA2F;AAC3F,MAAM,UAAU,YAAY,CAAC,KAAiB,EAAE,OAAe,EAAE,IAAY;IAC3E,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,QAAQ,EAAE,CAAC;QACb,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,qCAAqC;QAC3D,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,IAAI,6BAA6B,EAAE,CAAC;IAC7E,CAAC;IACD,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;IACnE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,IAAI,kDAAkD,EAAE,CAAC;AAC/F,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,KAAiB,EAAE,OAAe;IACrD,OAAO,EAAE,KAAK,EAAE,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,KAAiB,EAAE,OAAe,EAAE,SAAiB;IACxE,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAE,CAAC;IACvC,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAE,CAAC;IACjC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,GAAG,CAAC,iBAAiB,SAAS,UAAU,IAAI,CAAC,KAAK,YAAY,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC9G,CAAC;IACD,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAE,CAAC;IACxB,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,OAAO;QACL,KAAK,EAAE,YAAY,SAAS,OAAO,EAAE,CAAC,KAAK,QAAQ,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE;QACjF,MAAM,EAAE;YACN,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,IAAI,UAAU,SAAS,OAAO,EAAE,CAAC,KAAK,GAAG,EAAE;YAC5E,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,IAAI,iBAAiB,IAAI,CAAC,KAAK,GAAG,EAAE;SACpE;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,KAAiB,EAAE,OAAe,EAAE,IAAY;IAClE,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAE,CAAC;IACvC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,CAAC,OAAO;QAAE,OAAO,GAAG,CAAC,WAAW,CAAC,CAAC;IACtC,OAAO;QACL,KAAK,EAAE,aAAa,OAAO,GAAG;QAC9B,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,IAAI,WAAW,OAAO,GAAG,EAAE,CAAC;KAC3E,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,KAAiB,EAAE,OAAe,EAAE,IAAY;IACnE,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAE,CAAC;IACvC,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IACjD,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;QACZ,OAAO,GAAG,CAAC,eAAe,IAAI,UAAU,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAClG,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACrB,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO;QACL,KAAK,EAAE,mBAAmB,IAAI,GAAG;QACjC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,IAAI,iBAAiB,IAAI,GAAG,EAAE,CAAC;KAC9E,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,KAAiB,EAAE,OAAe,EAAE,IAAY;IACnE,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAE,CAAC;IACvC,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,0BAA0B,IAAI,IAAI,CAAC,CAAC;IAC5D,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IAChC,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjD,OAAO;QACL,KAAK,EAAE,oBAAoB,IAAI,GAAG;QAClC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC,IAAI,kBAAkB,IAAI,GAAG,EAAE,CAAC;KAC/E,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAiB,EAAE,OAAe;IAC1D,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAE,CAAC;IACvC,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC,MAAM;YAC5B,CAAC,CAAC,qBAAqB,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;YACrD,CAAC,CAAC,2BAA2B;KAChC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,KAAiB,EAAE,OAAe,EAAE,MAAmB;IACzE,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE;QAC1D,MAAM,IAAI,GAAG,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,yCAAyC;QAC3F,OAAO,GAAG,CAAC,CAAC,IAAI,GAAG,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAE,CAAC,KAAK,GAAG,MAAM,EAAE,CAAC;IAC9D,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,KAAK,EAAE,oCAAoC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;AAC3E,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tavern-mcpl entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* Env:
|
|
5
|
+
* TAVERN_PORT — listen port (default 7450)
|
|
6
|
+
* TAVERN_HOST — bind host (default 127.0.0.1; front with a TLS proxy for wss://)
|
|
7
|
+
* TAVERN_STORE — state file (default ./data/tavern.json)
|
|
8
|
+
* TAVERN_TOKENS — comma-separated token:agentId:DisplayName triples, e.g.
|
|
9
|
+
* "s3cret1:labclaude:LabClaude,s3cret2:mythos:Mythos"
|
|
10
|
+
*
|
|
11
|
+
* Agents join with:
|
|
12
|
+
* mcpl_deploy { id: "tavern", url: "ws://host:7450/mcpl", token: "s3cret1", reconnect: true }
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tavern-mcpl entrypoint.
|
|
3
|
+
*
|
|
4
|
+
* Env:
|
|
5
|
+
* TAVERN_PORT — listen port (default 7450)
|
|
6
|
+
* TAVERN_HOST — bind host (default 127.0.0.1; front with a TLS proxy for wss://)
|
|
7
|
+
* TAVERN_STORE — state file (default ./data/tavern.json)
|
|
8
|
+
* TAVERN_TOKENS — comma-separated token:agentId:DisplayName triples, e.g.
|
|
9
|
+
* "s3cret1:labclaude:LabClaude,s3cret2:mythos:Mythos"
|
|
10
|
+
*
|
|
11
|
+
* Agents join with:
|
|
12
|
+
* mcpl_deploy { id: "tavern", url: "ws://host:7450/mcpl", token: "s3cret1", reconnect: true }
|
|
13
|
+
*/
|
|
14
|
+
import { TavernServer } from './server.js';
|
|
15
|
+
function parseTokens(spec) {
|
|
16
|
+
const tokens = {};
|
|
17
|
+
for (const triple of (spec ?? '').split(',')) {
|
|
18
|
+
const [token, id, name] = triple.split(':').map((part) => part?.trim());
|
|
19
|
+
if (token && id)
|
|
20
|
+
tokens[token] = { id, name: name || id };
|
|
21
|
+
}
|
|
22
|
+
return tokens;
|
|
23
|
+
}
|
|
24
|
+
const tokens = parseTokens(process.env.TAVERN_TOKENS);
|
|
25
|
+
if (Object.keys(tokens).length === 0) {
|
|
26
|
+
console.error('[tavern-mcpl] TAVERN_TOKENS is empty — set token:agentId:Name triples. Refusing to start an unauthenticated server.');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const server = new TavernServer({
|
|
30
|
+
port: Number(process.env.TAVERN_PORT ?? 7450),
|
|
31
|
+
host: process.env.TAVERN_HOST ?? '127.0.0.1',
|
|
32
|
+
storePath: process.env.TAVERN_STORE ?? './data/tavern.json',
|
|
33
|
+
tokens,
|
|
34
|
+
});
|
|
35
|
+
server.start().catch((err) => {
|
|
36
|
+
console.error('[tavern-mcpl] failed to start:', err);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
39
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
40
|
+
process.on(signal, () => {
|
|
41
|
+
void server.stop().then(() => process.exit(0));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAAkB,MAAM,aAAa,CAAC;AAE3D,SAAS,WAAW,CAAC,IAAwB;IAC3C,MAAM,MAAM,GAA8B,EAAE,CAAC;IAC7C,KAAK,MAAM,MAAM,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7C,MAAM,CAAC,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACxE,IAAI,KAAK,IAAI,EAAE;YAAE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;IAC5D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;AACtD,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IACrC,OAAO,CAAC,KAAK,CAAC,qHAAqH,CAAC,CAAC;IACrI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC;IAC7C,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,WAAW;IAC5C,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,oBAAoB;IAC3D,MAAM;CACP,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IAC3B,OAAO,CAAC,KAAK,CAAC,gCAAgC,EAAE,GAAG,CAAC,CAAC;IACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,KAAK,MAAM,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAU,EAAE,CAAC;IACpD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;QACtB,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TavernServer — the network/session layer of the Lantern & Ladle.
|
|
3
|
+
*
|
|
4
|
+
* The multi-agent network-MCPL pattern:
|
|
5
|
+
* - One WebSocketServer; each connection is authenticated by `?token=` and
|
|
6
|
+
* mapped to a stable agent id (sessions are ephemeral, identity is not).
|
|
7
|
+
* - One Session per live connection: initialize handshake, channels/register,
|
|
8
|
+
* then a message loop dispatching tools/call + channels/publish.
|
|
9
|
+
* - SharedCore = the game world. All mutations happen in tool handlers
|
|
10
|
+
* (atomic per message on Node's single thread) and persist immediately.
|
|
11
|
+
* - Fan-out: game events go to every ONLINE player in the room as
|
|
12
|
+
* channels/incoming on that room's channel; OFFLINE players in the room
|
|
13
|
+
* get the event queued (capped) and replayed on their next connect.
|
|
14
|
+
* - Persistence: world + offline queues in one JSON file (tmp+rename), so
|
|
15
|
+
* a server restart loses nothing.
|
|
16
|
+
*/
|
|
17
|
+
import { type WorldState, type GameEvent } from './game.js';
|
|
18
|
+
export interface AgentAuth {
|
|
19
|
+
/** Stable agent identity — state is keyed by this, never by connection. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** In-game display name. */
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
export interface TavernServerOptions {
|
|
25
|
+
port: number;
|
|
26
|
+
host?: string;
|
|
27
|
+
/** token → agent identity. The ONLY identity channel: the host dials ?token=. */
|
|
28
|
+
tokens: Record<string, AgentAuth>;
|
|
29
|
+
storePath: string;
|
|
30
|
+
/** Max events queued per offline player (oldest dropped). Default 100. */
|
|
31
|
+
offlineQueueCap?: number;
|
|
32
|
+
log?: (line: string) => void;
|
|
33
|
+
}
|
|
34
|
+
interface QueuedEvent {
|
|
35
|
+
channelId: string;
|
|
36
|
+
text: string;
|
|
37
|
+
timestamp: string;
|
|
38
|
+
}
|
|
39
|
+
export declare class TavernServer {
|
|
40
|
+
private readonly opts;
|
|
41
|
+
private readonly store;
|
|
42
|
+
private readonly sessions;
|
|
43
|
+
private httpServer;
|
|
44
|
+
private wss;
|
|
45
|
+
private readonly log;
|
|
46
|
+
constructor(opts: TavernServerOptions);
|
|
47
|
+
/** Start listening. Returns the bound port (useful with port 0). */
|
|
48
|
+
start(): Promise<number>;
|
|
49
|
+
stop(): Promise<void>;
|
|
50
|
+
get world(): WorldState;
|
|
51
|
+
onlineIds(): Set<string>;
|
|
52
|
+
persist(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Deliver a game event to every player in its room except the actor:
|
|
55
|
+
* live sessions get channels/incoming now; offline players get it queued.
|
|
56
|
+
*/
|
|
57
|
+
fanOut(event: GameEvent, actorId: string | null): void;
|
|
58
|
+
/** Pull (and clear) an agent's offline queue. */
|
|
59
|
+
drainQueue(agentId: string): QueuedEvent[];
|
|
60
|
+
}
|
|
61
|
+
export {};
|