- .gitea/workflows/ci.yml: ruff + matrix pytest on 3.10/3.11/3.12 on push and PR to main; concurrency cancels superseded runs - .gitea/workflows/release.yml: gate (ruff + pytest) + build sdist/wheel on tag 'v*', uploads via actions/upload-artifact@v4 - README.md: add ci and version badges right under H1 - src/viandante/cli.py: drop unused prompt_toolkit.patch_stdout import - tests/test_deadsouls.py: add missing RoomBlock import (F821) - tests/test_logger.py: drop unused local 'raw' in unicode test - tests/test_mapper.py: rename unused locals to _a/_b/_c (F841) to preserve intent (B has 's' back-edge; C is orphan)
266 lines
12 KiB
Markdown
266 lines
12 KiB
Markdown
# viandante
|
||
|
||
[](http://192.168.0.183:8085/truetype/viandante/actions?workflow=ci)
|
||
[](http://192.168.0.183:8085/truetype/viandante/releases/tag/v0.2.0)
|
||
|
||
`viandante` ospita un **client MUD CLI** pensato per collegarsi a un MUD
|
||
LP‑MUD basato sul driver **FluffOS** (per esempio **Dead Souls**, libreria
|
||
3.8.6). Il client parla telnet con estensioni standard: `ECHO`, `NAWS`,
|
||
`TTYPE`, e rifiuta (`WONT`) tutte le altre (`GMCP`, `MSDP`, `MCCP`, `MXP`
|
||
per ora). Tiene traccia delle stanze visitate e le renderizza come **mappa
|
||
ASCII** on-demand.
|
||
|
||
|
||
|
||
---
|
||
|
||
> **viandante** — *Italian for "wanderer"*.
|
||
> A telnet client + ASCII mapper for LP-MUDs (FluffOS, Dead Souls),
|
||
> with GMCP vitals/room decoder and append-only session logging.
|
||
|
||
```
|
||
[ Town ] -- n -- [ Bank ]
|
||
| |
|
||
s e
|
||
| |
|
||
[ Gate ] ---------- [ Inn ]
|
||
```
|
||
|
||
> *Mapping the world, one step at a time.*
|
||
|
||
## Changelog
|
||
|
||
Le novità di `viandante` rispetto alla release 0.1 (bootstrap del client, prima del rename a `viandante`).
|
||
|
||
### v0.2.0 — GMCP, mapper BFS, EventLogger, rename
|
||
|
||
**Aggiunto**
|
||
|
||
- **GMCP decoder** (`src/viandante/gmcp.py`): validazione del module name via regex `^[A-Za-z][A-Za-z0-9_.]*$`, decoder per `Char.Vitals` con sinonimi (`hp`/`health`/`life`, `sp`/`mana`/`magic`, `mv`/`move`/`stam`), `Room.Info` con vnum del server come id stabile (`vnum{int(num)}`).
|
||
- **Mapper multi-source BFS** (`src/viandante/mapper.py`): coordinate calcolate solo al render (lazy layout), phase 1 BFS dal `current`, phase 2 orphan placement iterato (gestisce `n→s`), phase 3 stack nell'angolo in basso a destra per stanze isolate. Snapshot di `coords.items()` prima di mutare per evitare collisioni.
|
||
- **EventLogger** (`src/viandante/logger.py`): due sink opt‑in paralleli — `--log-json` JSONL append‑only (un evento per riga) e `--log-md` Markdown streamed con heading `## HH:MM:SS.mmm — kind`. Lock difensivo + idempotenza di `close()` + redazione password in tre punti (`line_sent` / `local_cmd` / `login`).
|
||
- **Comandi slash locali** in `cli._handle_local_command`: `/map` `/where` `/look` `/vitals` `/stats` `/password PWD` `/clear` `/help` `/quit`. Le `/...` non riconosciute vengono inoltrate al MUD; `/password PWD` logga `local_cmd` + `line_sent` + `login` tutti esplicitamente redatti.
|
||
- **Auto‑login regex** ancorate a riga (`_LOGIN_RE` / `_PASSWORD_RE`) con varianti `>` / `*` / `:` / `?` e invio one‑shot del nome; suggerimento `/password PWD` se la password è assente.
|
||
- **Status bar** `prompt_toolkit` con `FormattedText` rinfrescato 2×/s (`refresh_interval=0.5`) che mostra stanza corrente, GMCP on/off, hp/sp/mv.
|
||
- **Documentazione** completa in `docs/` (8 file: architecture, modules, protocol, usage, events, development, gitea + README index) con analisi architetturale, data flow, schema eventi del logger, flag CLI, workflow dev e flusso Gitea con warning sicurezza token.
|
||
|
||
**Modificato**
|
||
|
||
- **Hardening asyncio** in `cli._run`: `asyncio.wait(FIRST_COMPLETED)` + `conn.close()` *prima* del drain delle pending task. Sblocco via EOF invece che `CancelledError`.
|
||
- **Strip delle credenziali** in `.git/config` dopo ogni push: URL remoto resta pulito, il token viaggia solo inline nella URL del singolo `git push`.
|
||
|
||
**Rinominato**
|
||
|
||
- **`progetto2` → `viandante`** (Italian for *wanderer*) ovunque: package Python (`src/progetto2/` → `src/viandante/`), pyproject `name` + `description` + `keywords`, repo Gitea (`truetype/progetto2` → `truetype/viandante`), documentazione. L'entry‑point script resta `mudclient` (un solo nome da ricordare).
|
||
|
||
**Tests**
|
||
|
||
- **66 test** (precedenti 46 + 20 nuovi per `EventLogger`).
|
||
- Nuovo `tests/test_logger.py` per roundtrip JSONL + rendering MD, redazione password, idempotenza di `close()`, robustness su byte ESC non validi.
|
||
|
||
### v0.1.0
|
||
|
||
- Bootstrap iniziale: client telnet minimale (IAC state machine + GMCP marker), mapper ASCII naive, supporto Dead Souls via `localhost:6666`. Package e repo chiamati `progetto2`.
|
||
|
||
## Requisiti
|
||
|
||
- Python ≥ 3.10
|
||
- Una libreria Python: [`prompt_toolkit`](https://pypi.org/project/prompt-toolkit/) ≥ 3.0
|
||
|
||
## Setup
|
||
|
||
```bash
|
||
cd viandante
|
||
python3 -m venv .venv
|
||
source .venv/bin/activate
|
||
pip install -e ".[dev]"
|
||
```
|
||
|
||
Verrà installato il comando `mudclient`.
|
||
|
||
## Uso
|
||
|
||
```bash
|
||
# Default: si collega a localhost:6666 con un finestra iniziale 120×40
|
||
mudclient
|
||
|
||
# Espliciti
|
||
mudclient --host localhost --port 6666 --cols 120 --rows 40
|
||
|
||
# Auto-login: passa il nome; il client lo invia quando il server chiede
|
||
# "What name do you wish?". Se serve la password, il client chiede
|
||
# /password PWD oppure la puoi passare con --password (o env MUDCLIENT_PASSWORD).
|
||
mudclient --user zork --password hunter2
|
||
|
||
# Log di sessione: ogni evento (testo, GMCP, stanze, comandi locali, linee inviate)
|
||
# viene serializzato in append‑only, flushed dopo ogni write — adatto a `tail -f`.
|
||
mudclient --log-json ~/mud-session.jsonl --log-md ~/mud-session.md
|
||
```
|
||
|
||
> I file `--log-json` / `--log-md` vengono **truncati** all'apertura:
|
||
> ogni nuova sessione ricomincia da zero. Per ruotare manualmente
|
||
> prima della sessione:
|
||
> ```bash
|
||
> mv ~/mud-session.jsonl{,.prev} && mudclient --log-json ~/mud-session.jsonl
|
||
> ```
|
||
|
||
Schema eventi (un oggetto JSON per riga):
|
||
|
||
| `kind` | Scopo | Note |
|
||
| --- | --- | --- |
|
||
| `session_start` | apertura sessione | `host`, `port`, `user` |
|
||
| `session_end` | chiusura normale | `reason` |
|
||
| `disconnect` | chiusura socket | `reason` |
|
||
| `text` | chunk ANSI ricevuto dal server | `raw` preservato |
|
||
| `room_enter` | mapper ha registrato una stanza | `id`, `name`, `exits` |
|
||
| `gmcp` | frame GMCP accettato | `module`, `payload` |
|
||
| `vitals` | snapshot hp/sp/mv | solo i campi presenti |
|
||
| `line_sent` | riga inoltrata al MUD | password redatte (`***`) |
|
||
| `local_cmd` | comando `/...` intercettato | password/segreti redatti |
|
||
| `login` | prompt/risposta auto-login | `value` redatto di default; passare `redact=False` per i nomi account |
|
||
|
||
Una volta connesso:
|
||
|
||
| Tasto | Effetto |
|
||
| --- | --- |
|
||
| Testo qualsiasi | inviato al MUD terminato da `\r\n` |
|
||
| `/map` | disegna la mappa ASCII delle stanze visitate (multi-source BFS) |
|
||
| `/where` | mostra la stanza corrente e le uscite |
|
||
| `/look` | invia `look` al MUD |
|
||
| `/vitals` | mostra hp/sp/mv (via GMCP `Char.Vitals`) |
|
||
| `/stats` | locali scoperti, GMCP stato, vitali |
|
||
| `/password PWD` | invia una password al MUD |
|
||
| `/clear` | pulisce lo schermo |
|
||
| `/help` (`/?`) | mostra l'aiuto |
|
||
| `/quit` (`/q`, `Ctrl‑D`) | esce |
|
||
|
||
Il prompt è sempre del tipo:
|
||
|
||
```
|
||
[NomeStanza]> _
|
||
```
|
||
|
||
con il nome della stanza corrente in cyan brillante. In basso c'è una **status bar** che mostra:
|
||
|
||
```
|
||
[stanza: Outer Courtyard] [GMCP: on] HP 100/100 SP 50/50 MV 80/80
|
||
```
|
||
|
||
Quando il server invia `Char.Vitals` via GMCP, hp/sp/mv compaiono automaticamente.
|
||
|
||
## Layout dei sorgenti
|
||
|
||
```
|
||
viandante/
|
||
├── pyproject.toml # metadata + dipendenze + entry point `mudclient`
|
||
├── README.md # questo file
|
||
├── .gitignore
|
||
├── docs/ # documentazione completa (vedi sotto)
|
||
│ ├── README.md # indice della cartella docs
|
||
│ ├── architecture.md # analisi architetturale (data flow + boundary)
|
||
│ ├── modules.md # descrizione modulo‑per‑modulo
|
||
│ ├── protocol.md # telnet, GMCP, ANSI, euristiche Dead Souls
|
||
│ ├── usage.md # flag CLI, esempi, auto‑login, comandi slash
|
||
│ ├── events.md # schema eventi del logger
|
||
│ ├── development.md # setup dev, pytest, ruff, come estendere
|
||
│ └── gitea.md # flusso di pubblicazione sul Gitea + sicurezza token
|
||
├── src/viandante/ # package
|
||
│ ├── __init__.py
|
||
│ ├── __main__.py # supporta `python -m viandante`
|
||
│ ├── cli.py # entry point + UI a riga di comando
|
||
│ ├── telnet.py # state machine IAC + asyncio
|
||
│ ├── deadsouls.py # parser di stanze Dead Souls / LP‑MUD
|
||
│ ├── gmcp.py # parser GMCP (Char.Vitals / Room.Info)
|
||
│ ├── mapper.py # grafo delle stanze + render ASCII
|
||
│ ├── logger.py # EventLogger (JSONL + Markdown sinks)
|
||
│ └── ansi.py # stripper ANSI per il parser
|
||
└── tests/ # unit test (pytest)
|
||
├── test_telnet.py
|
||
├── test_ansi.py
|
||
├── test_deadsouls.py
|
||
├── test_mapper.py
|
||
└── test_logger.py
|
||
```
|
||
|
||
## 📚 Documentazione completa
|
||
|
||
La documentazione vive nella cartella [`docs/`](./docs/README.md) e
|
||
copre:
|
||
|
||
- **[`docs/architecture.md`](./docs/architecture.md)** — analisi
|
||
architetturale completa: dipendenze tra moduli, data flow di un
|
||
frame dal server, boundary concerns, refactor raccomandati.
|
||
- **[`docs/modules.md`](./docs/modules.md)** — cosa fa ogni modulo,
|
||
API pubblica, responsabilità, tolleranze.
|
||
- **[`docs/protocol.md`](./docs/protocol.md)** — IAC state machine,
|
||
GMCP wire format, euristiche Dead Souls, ANSI handling.
|
||
- **[`docs/usage.md`](./docs/usage.md)** — riferimento flag CLI, esempi
|
||
pratici, flusso auto‑login, comandi slash, rotazione log.
|
||
- **[`docs/events.md`](./docs/events.md)** — schema completo degli
|
||
eventi del logger (JSONL + Markdown), regole di redazione, esempi
|
||
`jq`.
|
||
- **[`docs/development.md`](./docs/development.md)** — workflow
|
||
sviluppatore: setup, test, lint, come aggiungere moduli, debugging.
|
||
- **[`docs/gitea.md`](./docs/gitea.md)** — flusso di pubblicazione sul
|
||
server Gitea `192.168.0.183:8085` (utente `truetype`) **+ regole di
|
||
sicurezza del token**.
|
||
|
||
## Note sul protocollo
|
||
|
||
Il client negozia:
|
||
|
||
- `ECHO`: invia `WILL ECHO` in risposta a `DO ECHO`. Il server non eseguirà
|
||
più echo remoto, mentre l'eco locale è gestita da `prompt_toolkit`.
|
||
- `NAWS`: se attivo, invia la dimensione della finestra iniziale.
|
||
- `TTYPE`: risponde con `XTERM-256COLOR` quando il server lo richiede.
|
||
- `GMCP`: viene **offerto proattivamente** (`WILL` al connect). Dead Souls
|
||
lo accetta e invia pacchetti `Char.Vitals`, `Room.Info`, `Char.Login`,
|
||
ecc. Quando una stanza arriva via `Room.Info`, si usa il `num`
|
||
(vnum) del server come id stabile della stanza.
|
||
|
||
Le righe di stato del parser considerano `IAC GA` (`249`) e `IAC EOR`
|
||
(`239`) come delimitatori di "record/prompt", così da poter chiudere un
|
||
blocco di stanza anche quando il prompt testuale non è immediatamente
|
||
riconoscibile.
|
||
|
||
La rilevazione automatica della geometria del mondo si basa sul **movimento
|
||
inviato dall'utente** + **descrizione della stanza** osservata subito
|
||
dopo: quando digiti `n`, il client informa il mapper; quando la risposta
|
||
del MUD arriva, `Mapper.visit()` collega automaticamente
|
||
`corrente → nuova` lungo la direzione digitata.
|
||
|
||
Il render della mappa è un **multi-source BFS** dal current room, con
|
||
fallback di **posizionamento orfano** per le stanze visitate in sequenza
|
||
indietro (es. `n→s`) e stack nell'angolo in basso a destra per stanze
|
||
completamente isolate.
|
||
|
||
## Limitazioni note (v0.2)
|
||
|
||
- **MSDP / MCCP / MCP / MXP** non sono ancora abilitati: una sessione che
|
||
li richiede ottiene `WONT` dal nostro lato. Dead Souls usa GMCP, che è
|
||
ben coperto; altri MUD basati su MSDP possono essere aggiunti seguendo
|
||
lo schema di `gmcp.py`.
|
||
- La detection delle stanze **senza GMCP** resta **euristica** sul testo.
|
||
- **Login automatico**: il client riconosce il prompt `What name do you
|
||
wish?` / `Password:` e invia automaticamente solo la prima volta.
|
||
Per la password senza `--password` viene mostrato un suggerimento
|
||
`usa /password <pwd>` (niente prompt annidato, troppo fragile).
|
||
- Gli `u`/`d` (verticali) sono rappresentati nella mappa ma non
|
||
influenzano le coordinate 2D (offset `(0,0)`).
|
||
- La status bar si aggiorna ad ogni rilettura della riga del prompt;
|
||
per refresh continui in idle si può aggiungere `refresh_interval`
|
||
al `PromptSession` come estensione futura.
|
||
|
||
## Sviluppo
|
||
|
||
```bash
|
||
pip install -e ".[dev]"
|
||
pytest # gira tutta la suite
|
||
ruff check src tests # lint
|
||
```
|
||
|
||
## Licenza
|
||
|
||
MIT per il codice del client. Il server **Dead Souls** non è GPL — assicurati
|
||
di rispettarne la licenza prima di redistribuirlo.
|