Move notes to docs/how, docs/why, docs/lessons
how/ - how things work (terrain generation, command system) why/ - design philosophy (telnet-first, text worlds) lessons/ - things we learned the hard way (charset vs mtts) Removes notes/ — DAYDREAMING.txt became DREAMBOOK.md, charset-vs-mtts expanded into docs/lessons/ with the connect_maxwait fix documented.
This commit is contained in:
parent
0d0c142993
commit
25bb565091
8 changed files with 273 additions and 226 deletions
|
|
@ -14,9 +14,29 @@ Telnet MUD engine built on telnetlib3. Python 3.12+, managed with uv.
|
|||
- `src/mudlib/` - the engine (commands, world, combat, render, store)
|
||||
- `tests/` - pytest tests
|
||||
- `worlds/` - world definition files (yaml/toml, version controlled)
|
||||
- `notes/` - design docs, daydreaming
|
||||
- `docs/` - project knowledge (see below)
|
||||
- `DREAMBOOK.md` - the vision, philosophy, wild ideas. not a spec
|
||||
- `repos/` - symlinked reference repos (telnetlib3, miniboa). gitignored, not our code
|
||||
|
||||
## Docs
|
||||
|
||||
Three categories in `docs/`. Plain text, not markdown.
|
||||
|
||||
- `docs/how/` - how things work. write one when you build something non-obvious.
|
||||
terrain generation, command system, etc. aimed at someone reading the code
|
||||
who wants the "why did we do it this way" context.
|
||||
- `docs/why/` - design philosophy. telnet-first, text worlds, etc. the reasoning
|
||||
behind big decisions. doesn't change often.
|
||||
- `docs/lessons/` - things we learned the hard way. write one when you hit a
|
||||
real bug or gotcha that cost time. charset-vs-mtts, etc. include the fix so
|
||||
we don't repeat it.
|
||||
|
||||
Update docs when:
|
||||
- you build a new system (add a how/)
|
||||
- you make a design decision worth explaining (add a why/)
|
||||
- you debug something painful (add a lessons/)
|
||||
- existing docs become wrong (update them)
|
||||
|
||||
## Architecture
|
||||
|
||||
- telnetlib3 is a **dependency**, not vendored. contribute fixes upstream
|
||||
|
|
|
|||
58
docs/how/commands.txt
Normal file
58
docs/how/commands.txt
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
command system
|
||||
==============
|
||||
|
||||
commands are registered in a simple dict mapping names to async handlers.
|
||||
|
||||
async def my_command(player: Player, args: str) -> None:
|
||||
player.writer.write("hello\r\n")
|
||||
await player.writer.drain()
|
||||
|
||||
register("mycommand", my_command, aliases=["mc"])
|
||||
|
||||
dispatch parses input, looks up the handler, calls it:
|
||||
|
||||
await dispatch(player, "mycommand some args")
|
||||
|
||||
if the command isn't found, the player gets "Unknown command: ..."
|
||||
|
||||
movement
|
||||
--------
|
||||
|
||||
8 directions, each with short and long aliases:
|
||||
|
||||
n/north s/south e/east w/west
|
||||
ne/northeast nw/northwest se/southeast sw/southwest
|
||||
|
||||
movement checks passability before updating position. impassable terrain
|
||||
gives "You can't go that way."
|
||||
|
||||
nearby players see arrival/departure messages:
|
||||
|
||||
jared leaves east.
|
||||
jared arrives from the west.
|
||||
|
||||
"nearby" = within viewport range (10 tiles) of old or new position.
|
||||
|
||||
look
|
||||
----
|
||||
|
||||
look/l renders the viewport. player is always @ at center. other players
|
||||
show as *. output is ANSI-colored.
|
||||
|
||||
adding commands
|
||||
---------------
|
||||
|
||||
1. create src/mudlib/commands/yourcommand.py
|
||||
2. import register from mudlib.commands
|
||||
3. define async handler(player, args)
|
||||
4. call register() with name and aliases
|
||||
5. import the module in server.py so registration runs at startup
|
||||
|
||||
code
|
||||
----
|
||||
|
||||
src/mudlib/commands/__init__.py registry + dispatch
|
||||
src/mudlib/commands/movement.py direction commands
|
||||
src/mudlib/commands/look.py look/l
|
||||
src/mudlib/commands/quit.py quit/q
|
||||
src/mudlib/player.py Player dataclass + registry
|
||||
64
docs/how/terrain-generation.txt
Normal file
64
docs/how/terrain-generation.txt
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
terrain generation
|
||||
==================
|
||||
|
||||
the world is a 2D grid of tiles generated deterministically from a seed.
|
||||
same seed = same world, always.
|
||||
|
||||
how it works
|
||||
------------
|
||||
|
||||
1. generate a permutation table from the seed (shuffled 0-255)
|
||||
2. compute elevation at 1/4 resolution using layered Perlin noise (3 octaves)
|
||||
3. normalize elevation to [0, 1]
|
||||
4. bilinear interpolate up to full resolution
|
||||
5. derive terrain from elevation thresholds:
|
||||
> 0.75 mountain ^
|
||||
> 0.55 forest T
|
||||
> 0.25 grass .
|
||||
> 0.15 sand :
|
||||
<= 0.15 water ~
|
||||
6. trace rivers from random high-elevation points downhill to water
|
||||
|
||||
the 1/4 resolution trick is why generation takes ~1s instead of ~9s for a
|
||||
1000x1000 map. terrain features are large enough that interpolation doesn't
|
||||
lose visible detail.
|
||||
|
||||
geography emerges naturally from noise: mountain ranges form along ridges,
|
||||
forests cluster at foothills, sand appears at shorelines, oceans fill low
|
||||
basins. rivers connect highlands to water bodies.
|
||||
|
||||
world config lives in worlds/<name>/config.toml:
|
||||
|
||||
[world]
|
||||
name = "Earth"
|
||||
seed = 42
|
||||
width = 1000
|
||||
height = 1000
|
||||
|
||||
rendering
|
||||
---------
|
||||
|
||||
viewport is centered on the player (@). size comes from NAWS (terminal
|
||||
dimensions) — eventually. for now it's a fixed 21x11.
|
||||
|
||||
ANSI colors per terrain type:
|
||||
. grass green
|
||||
^ mountain dark gray
|
||||
~ water blue
|
||||
T forest green
|
||||
: sand yellow
|
||||
@ player bold white
|
||||
* entity bold red
|
||||
|
||||
passability
|
||||
-----------
|
||||
|
||||
mountains and water are impassable. forest, grass, sand are walkable.
|
||||
water will eventually have shallow/deep variants (wadeable vs boat-only).
|
||||
|
||||
code
|
||||
----
|
||||
|
||||
src/mudlib/world/terrain.py world generation + noise
|
||||
src/mudlib/render/ansi.py ANSI color mapping
|
||||
worlds/earth/config.toml earth config
|
||||
72
docs/lessons/charset-vs-mtts.txt
Normal file
72
docs/lessons/charset-vs-mtts.txt
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
charset negotiation vs mtts: why tintin++ says WILL then REJECTED
|
||||
=================================================================
|
||||
|
||||
the problem
|
||||
-----------
|
||||
|
||||
telnetlib3 defaults to connect_maxwait=4.0, waiting for CHARSET encoding
|
||||
negotiation to resolve. MUD clients like tintin++ reject CHARSET but advertise
|
||||
encoding support through MTTS instead. result: 4 seconds of dead air on every
|
||||
connection.
|
||||
|
||||
the fix (our side)
|
||||
------------------
|
||||
|
||||
pass connect_maxwait=0.5 to create_server(). the actual option negotiation
|
||||
finishes in ~500ms. CHARSET gets rejected almost immediately but telnetlib3
|
||||
doesn't have a fallback path — it just waits for the timeout. 0.5s is plenty.
|
||||
|
||||
server = await telnetlib3.create_server(
|
||||
host="127.0.0.1", port=PORT, shell=shell, connect_maxwait=0.5
|
||||
)
|
||||
|
||||
the upstream fix (telnetlib3)
|
||||
-----------------------------
|
||||
|
||||
when CHARSET is rejected, telnetlib3 could check MTTS bit 3 for UTF-8 support
|
||||
and resolve encoding immediately instead of running out the clock. this would
|
||||
be a good contribution. the data is already there:
|
||||
|
||||
MTTS 2825 → bit 3 set → client supports UTF-8
|
||||
|
||||
what happens on the wire
|
||||
------------------------
|
||||
|
||||
telnetlib3 sends DO CHARSET. tintin++ responds WILL CHARSET ("i know what that
|
||||
is") then immediately REJECTED ("but i don't want to use it"). this is normal
|
||||
for MUD clients — they prefer MTTS.
|
||||
|
||||
+0.000 server send DO CHARSET
|
||||
+0.100 client recv WILL CHARSET
|
||||
+0.100 server send SB CHARSET REQUEST UTF-8 ... US-ASCII
|
||||
+0.200 client recv SB CHARSET REJECTED
|
||||
|
||||
meanwhile, in the same negotiation, tintin++ already told us via MTTS:
|
||||
|
||||
ttype3: MTTS 2825
|
||||
bit 0 (1) = ANSI color
|
||||
bit 3 (8) = UTF-8
|
||||
bit 8 (256) = 256 colors
|
||||
bit 9 (512) = OSC color palette
|
||||
bit 11 (2048) = true color
|
||||
|
||||
two protocols, one answer. MTTS (via TTYPE round 3) is what the MUD ecosystem
|
||||
uses. RFC 2066 CHARSET is technically correct but practically ignored.
|
||||
|
||||
will echo and mud clients
|
||||
-------------------------
|
||||
|
||||
related discovery: telnetlib3 intentionally skips WILL ECHO for detected MUD
|
||||
clients. tintin++ is in the MUD_TERMINALS fingerprint list. MUD clients
|
||||
interpret WILL ECHO as "password mode" and mask input, so the server avoids
|
||||
sending it. the client handles its own local echo.
|
||||
|
||||
this means will_echo is always False for tintin++, and that's correct. don't
|
||||
try to fight it or wait for it.
|
||||
|
||||
see also
|
||||
--------
|
||||
|
||||
- MTTS spec: https://tintin.mudhalla.net/protocols/mtts/
|
||||
- RFC 2066: CHARSET option
|
||||
- telnetlib3 source: server.py _negotiate_echo(), fingerprinting.py _is_maybe_mud()
|
||||
32
docs/why/telnet-first.txt
Normal file
32
docs/why/telnet-first.txt
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
why telnet first
|
||||
================
|
||||
|
||||
from the dreambook: "we honor telnet. the protocol is the interface."
|
||||
|
||||
telnet is the native interface. a person with a terminal emulator gets the full
|
||||
experience. no browser, no client download. `telnet mud.example.com 6789` and
|
||||
you're in.
|
||||
|
||||
telnetlib3 gives us:
|
||||
- NAWS (terminal dimensions — adapt viewport to window size)
|
||||
- TTYPE/MTTS (client capability detection)
|
||||
- GMCP/MSDP (structured data for rich clients like Mudlet)
|
||||
- SGA (suppress go-ahead for modern line handling)
|
||||
- async server with reader/writer streams
|
||||
|
||||
the MUD ecosystem has decades of protocol extensions built on telnet. we get
|
||||
all of them for free by staying in the protocol.
|
||||
|
||||
a web client, when it comes, should just be a terminal emulator (xterm.js)
|
||||
pointing at the telnet port. same protocol, different transport.
|
||||
|
||||
design consequences
|
||||
-------------------
|
||||
|
||||
- all output is text + ANSI escape codes. no binary formats.
|
||||
- line endings are \r\n (telnet NVT standard)
|
||||
- input is line-based (readline). no raw keystroke handling (yet)
|
||||
- server-side echo is complicated — MUD clients handle their own echo
|
||||
(see docs/lessons/charset-vs-mtts.txt for the will_echo story)
|
||||
- NAWS tells us the terminal size. viewport adapts. small terminal = smaller
|
||||
map view. this is a feature, not a limitation.
|
||||
26
docs/why/text-worlds.txt
Normal file
26
docs/why/text-worlds.txt
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
why text
|
||||
========
|
||||
|
||||
from the dreambook, drawing on david jacobson's research at brandeis:
|
||||
|
||||
narrow bandwidth doesn't impoverish experience, it enriches imagination. when
|
||||
you read "a torch flickers against wet stone walls" your brain builds a
|
||||
cathedral. VR hands you someone else's cathedral.
|
||||
|
||||
text worlds are collaborative hallucinations between the author and the reader.
|
||||
|
||||
the MOO community proved this for decades. LambdaMOO, MediaMOO, PMC-MOO —
|
||||
people formed real relationships, built real communities, lived real social
|
||||
lives through nothing but text on a terminal.
|
||||
|
||||
presence isn't about polygons. presence is about engagement, attention, the
|
||||
feeling that you are *there* and that what happens *matters*.
|
||||
|
||||
what the research showed
|
||||
------------------------
|
||||
|
||||
- presence comes from engagement, not fidelity
|
||||
- identity is constructed through language (description, emotes, speech)
|
||||
- community forms through shared space and shared creation
|
||||
- the mask enables authenticity — removing physical cues lets people construct
|
||||
identity more deliberately. that's not deception, it's freedom.
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
i like to imagine blending concepts with a MUD, interactive fiction, text
|
||||
based adventures.
|
||||
|
||||
so in one part it's moving around like you are in a mud, room names, short
|
||||
description perhaps, long description perhaps, and a little ascii
|
||||
representation that perhaps renders on the left or right or above or beneath
|
||||
room description and available exits
|
||||
|
||||
|
||||
however, i love what trenton gave us. the ascii worlds that update on
|
||||
movement/look and have you centered in them
|
||||
|
||||
i love the math of the combat, you could understand damage with percentages as
|
||||
a function of your powerlevel, which was also your health. stamina emptying
|
||||
also had you passing out, iirc. tbh i dont entirely remember how it worked. im
|
||||
actually quite fuzzy on it atm
|
||||
|
||||
let me type some of the combat commands
|
||||
|
||||
punch right/left [target, not needed if u have an active target aka in combat]
|
||||
roundhouse [target]
|
||||
dodge right/left
|
||||
duck
|
||||
jump
|
||||
parry high/low
|
||||
|
||||
you might see:
|
||||
|
||||
jake arrives from the west
|
||||
jake pulls back his right fist
|
||||
|
||||
this informs you that you had better parry high or dodge left
|
||||
|
||||
parry high was roughly needed.. in a small window.. not RIGHT away, and not too
|
||||
late
|
||||
|
||||
all commands took time so you could just parry high .9 seconds oto early and
|
||||
then instnatly fire it agtain, yhou had to fail the parry first or something
|
||||
|
||||
we will honestly have to tweak all tthese timings
|
||||
|
||||
---
|
||||
|
||||
so i said we're on an ascii map, that was earth
|
||||
|
||||
you could travel around earth and find mobs to find
|
||||
|
||||
if your powerlevel was 100
|
||||
|
||||
and you fought a mob that had the same pl let us say, you could earn no mopre
|
||||
than 10 pl from then, 10%. but if you were 1000pl and killed a 100pl, you
|
||||
wouldnt gain 10%, the function doews account your actual power level too
|
||||
(diminishign returns i guess idk, means yhou gotta move on and fight more
|
||||
p[owerful mobs)
|
||||
|
||||
|
||||
what i really want to think about though isnt all these details from ONE dbz
|
||||
mud i played
|
||||
|
||||
but, the.. framework to suport:
|
||||
|
||||
ascii mobs, combat, that kinda stuff
|
||||
|
||||
tweaking it, expanding it, etc
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
charset negotiation vs mtts: why tintin++ says WILL then REJECTED
|
||||
=================================================================
|
||||
|
||||
the problem
|
||||
-----------
|
||||
|
||||
when connecting to a telnetlib3 server, there's a 4-second delay before the
|
||||
shell starts. the server is waiting for encoding to resolve. it never does.
|
||||
|
||||
what the logs show
|
||||
------------------
|
||||
|
||||
the full option negotiation between telnetlib3 and tintin++ takes about 500ms.
|
||||
everything resolves quickly - TTYPE, SGA, NAWS, NEW_ENVIRON. but CHARSET
|
||||
negotiation goes like this:
|
||||
|
||||
26.220 server sends DO CHARSET
|
||||
26.332 tintin responds WILL CHARSET ("i can do charset negotiation")
|
||||
26.332 server sends SB CHARSET REQUEST UTF-8 UTF-16 LATIN1 ... US-ASCII
|
||||
26.442 tintin responds SB CHARSET REJECTED
|
||||
|
||||
tintin++ said "i support charset negotiation" and then rejected a list of 16
|
||||
charsets including UTF-8 and US-ASCII. every charset the server knows about.
|
||||
|
||||
the server marks encoding as unresolved, waits for connect_maxwait (4.0s),
|
||||
gives up, falls back to US-ASCII, and finally starts the shell.
|
||||
|
||||
30.122 server "encoding failed after 4.00s: US-ASCII"
|
||||
30.122 server "negotiation complete after 4.00s."
|
||||
|
||||
so the actual work is done in 500ms. the remaining 3.5 seconds is dead air.
|
||||
|
||||
but tintin++ already told us the answer
|
||||
----------------------------------------
|
||||
|
||||
in the same connection, tintin++ sent this through NEW_ENVIRON:
|
||||
|
||||
CHARSET=ASCII
|
||||
CLIENT_NAME=TinTin++
|
||||
CLIENT_VERSION=2.02.61
|
||||
MTTS=2825
|
||||
TERMINAL_TYPE=tmux-256color
|
||||
|
||||
and through TTYPE cycling (3 rounds):
|
||||
|
||||
ttype1: TINTIN++
|
||||
ttype2: tmux-256color
|
||||
ttype3: MTTS 2825
|
||||
|
||||
MTTS is the MUD Terminal Type Standard, a bitmask the MUD community created
|
||||
for advertising client capabilities. 2825 in binary:
|
||||
|
||||
2825 = 2048 + 512 + 256 + 8 + 1
|
||||
|
||||
bit 0 (1) = ANSI color
|
||||
bit 3 (8) = UTF-8
|
||||
bit 8 (256) = 256 colors
|
||||
bit 9 (512) = OSC color palette
|
||||
bit 11 (2048) = true color
|
||||
|
||||
bit 3 is set. tintin++ explicitly advertises UTF-8 support through MTTS.
|
||||
|
||||
two standards, one winner
|
||||
-------------------------
|
||||
|
||||
RFC 2066 CHARSET (1997) is a proper telnet standard. it defines a full
|
||||
negotiation dance:
|
||||
|
||||
1. one side sends DO CHARSET, other responds WILL CHARSET
|
||||
2. requester sends SB CHARSET REQUEST <sep> <charset-1> <sep> <charset-2> ...
|
||||
3. responder either ACCEPTED <charset> or REJECTED
|
||||
|
||||
this is thorough but cumbersome. the MUD community needed something simpler.
|
||||
|
||||
MTTS piggybacks on the TTYPE mechanism that already existed. instead of adding
|
||||
a new option negotiation, it uses the third round of TTYPE cycling to send a
|
||||
bitfield encoding all client capabilities in a single integer. no extra
|
||||
round-trips, no subnegotiation, works with any server that already does TTYPE.
|
||||
|
||||
round 1: client name "TINTIN++"
|
||||
round 2: terminal type "tmux-256color"
|
||||
round 3: MTTS bitfield "MTTS 2825"
|
||||
|
||||
the MUD world settled on MTTS. clients implement it properly. RFC 2066 CHARSET
|
||||
gets a polite WILL ("sure i know what that option is") followed by REJECTED
|
||||
("but i don't actually want to use it"). tintin++ is not alone in this - most
|
||||
MUD clients behave the same way.
|
||||
|
||||
what telnetlib3 could do better
|
||||
-------------------------------
|
||||
|
||||
right now telnetlib3 waits for CHARSET to resolve encoding. when it gets
|
||||
REJECTED, it has no fallback path except running out the clock. but by the time
|
||||
CHARSET is rejected (26.442), the server already has:
|
||||
|
||||
- MTTS=2825 via NEW_ENVIRON (26.595) or will have it momentarily
|
||||
- MTTS 2825 via TTYPE round 3 (26.595)
|
||||
|
||||
if the server checked MTTS bit 3 after CHARSET rejection, it could resolve
|
||||
encoding to UTF-8 immediately and start the shell in under 600ms instead of
|
||||
waiting the full 4 seconds.
|
||||
|
||||
this would be a good upstream contribution to telnetlib3: when CHARSET is
|
||||
rejected, check MTTS flags for encoding hints before falling back to the
|
||||
timeout.
|
||||
|
||||
the full annotated session
|
||||
--------------------------
|
||||
|
||||
timestamps are seconds within the connection. total negotiation takes ~477ms.
|
||||
the 4-second mark is the connect_maxwait timeout.
|
||||
|
||||
+0.000 server Connection from <Peer 127.0.0.1 51910>
|
||||
|
||||
-- phase 1: TTYPE discovery --
|
||||
|
||||
+0.000 server send IAC DO TTYPE
|
||||
+0.101 tintin recv IAC WILL TTYPE
|
||||
+0.101 server send IAC SB TTYPE SEND IAC SE (ask for ttype1)
|
||||
|
||||
-- phase 2: advanced negotiation begins after WILL TTYPE --
|
||||
|
||||
+0.102 server send IAC WILL SGA
|
||||
+0.102 server send IAC WILL BINARY
|
||||
+0.102 server send IAC DO NAWS
|
||||
+0.102 server send IAC DO CHARSET
|
||||
|
||||
+0.212 tintin recv IAC SB TTYPE IS 'TINTIN++' (ttype1: client name)
|
||||
+0.212 server (recognizes MUD client, skips WILL ECHO)
|
||||
+0.213 server send IAC DO NEW_ENVIRON
|
||||
+0.213 server send IAC SB TTYPE SEND IAC SE (ask for ttype2)
|
||||
|
||||
+0.213 tintin recv IAC DO SGA -> local_option[SGA] = True
|
||||
+0.214 tintin recv IAC DONT BINARY -> local_option[BINARY] = False
|
||||
+0.214 tintin recv IAC WILL NAWS
|
||||
+0.214 tintin recv IAC SB NAWS (cols=89, rows=56) (terminal size!)
|
||||
+0.214 tintin recv IAC WILL CHARSET
|
||||
+0.214 server send IAC WILL CHARSET (reciprocating)
|
||||
+0.214 server send IAC SB CHARSET REQUEST UTF-8 ... US-ASCII IAC SE
|
||||
|
||||
-- charset rejected, but negotiation continues --
|
||||
|
||||
+0.323 tintin recv IAC WILL NEW_ENVIRON
|
||||
+0.323 server send IAC SB NEW_ENVIRON SEND ...
|
||||
+0.323 tintin recv IAC SB TTYPE IS 'tmux-256color' (ttype2: terminal)
|
||||
+0.324 server send IAC SB TTYPE SEND IAC SE (ask for ttype3)
|
||||
+0.324 tintin recv IAC SB CHARSET REJECTED *** rejected ***
|
||||
|
||||
+0.434 tintin recv NEW_ENVIRON IS: CHARSET=ASCII
|
||||
+0.477 tintin recv NEW_ENVIRON IS: CLIENT_NAME=TinTin++
|
||||
+0.477 tintin recv NEW_ENVIRON IS: CLIENT_VERSION=2.02.61
|
||||
+0.477 tintin recv NEW_ENVIRON IS: MTTS=2825 *** utf-8 is here ***
|
||||
+0.477 tintin recv NEW_ENVIRON IS: TERMINAL_TYPE=tmux-256color
|
||||
+0.477 tintin recv IAC SB TTYPE IS 'MTTS 2825' (ttype3: capability bits)
|
||||
|
||||
-- all options resolved, but encoding still "pending" --
|
||||
|
||||
+4.004 server "encoding failed after 4.00s: US-ASCII"
|
||||
+4.004 server "negotiation complete after 4.00s."
|
||||
|
||||
-- shell finally starts, 3.5 seconds of dead air later --
|
||||
Loading…
Reference in a new issue