You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

191 lines
7.6 KiB
Markdown

# Lupus Lite
## Usage
```bash
2 years ago
# Development Mode: also starts "npm run dev" inside "_frontend/"
$ MODE=dev go run .
# Production Mode
2 years ago
$ cd _frontend
$ npm run build
$ go build
```
## Architettura
Moduli principali (cose che vengono eseguite e fanno cose di _business logic_)
- [`/main.go`](./main.go)
Entry point principale del server, qui vengono inizializzate concretamente tutte le dipendenze dei vari moduli (come database, router http e servizio di autenticazione).
Per ora usiamo un semplice database ed un semplice servizio di autenticazione per le sessioni di accesso che tengono tutto in memoria (più avanti passeremo a SQLite, tanto basterà semplicemente scrivere un'altra implementazione per `database.Database`)
- [`/routes`](./routes)
Questo modulo dipende **molto** dal router HTTP in quanto contiene tutte le route del server. Oltre questo dipende solo dal modulo `/handlers` e non sa nulla di `/database` e `/auth`.
- [`/handlers`](./handlers)
Questo modulo permette di testare tutta l'applicazione in modo isolato dal resto in quanto non sa nulla dei router HTTP e dipende solo da `/auth` e `/database` (ed anche da `/events`, `/model` e `/util` ma questi sono moduli "puri") che possono essere facilmente _mocked_.
Moduli secondari (o boh "terminali" nel senso che dipendono solo da cose esterne a questo progetto)
- [`/events`](./events)
Questo modulo fornisce una struttura dati di "EventBus" che permette di mandare e ricevere eventi all'interno dell'applicazione.
- [`/model`](./model)
Questo modulo contiene i modelli di tutte le strutture usate nel database ed alcuni metodi di servizio come `User.PublicUser()` che converte un `User` in `PublicUser` che rappresenta la versione "sicura" senza i campi segreti (come l'hash della password) dell'utente.
- [`/database`](./database)
L'interfaccia principale è `Database` e contiene tutte le operazioni possibili da fare sul database. Per ora c'è solo un'implementazione in memoria data da `*memDB`
- [`/auth`](./auth)
L'interfaccia principale è `AuthService` e contiene alcuni metodi per autenticare e registrare gli utenti e creare token di sessione. Per ora l'unica implementazione è `*memAuth` e dipende da un'istanza di `database.Database` e tiene i token di sessione in memoria.
- [`/util`](./util)
Questo modulo contiene alcune funzioni di utility e per ora anche varie funzioni di validazione dei form che arrivano dalla frontend, come validazione di username e password per la registrazione degli utenti.
Ed infine c'è il modulo che si occupa del far avanzare le partite e della risoluzione automatica.
- [`/lupus`](./lupus)
Questo modulo non dipende da nulla, l'idea è che conterrà le strutture per gestire lo stato delle partite e gli algoritmi per la risoluzione automatica di quest'ultime.
Al massimo potrebbe contenere alcune informazioni su come serializzare lo stato delle partite però per ora tutte le strutture qui dentro sono annotate per essere serializzate a JSON ed anche quando passeremo ad SQLite forse converrà fare sempre così.
TODO: Al massimo potrebbe dipendere da `/model` ma forse non serve.
## Architettura Lupus (1)
Lo stato della partita è contenuto in `lupus.PartitaState` e viene aggiornato da una struct di tipo `lupus.Ruleset`. Il lifecycle di una partita è
- `Ruleset.Start(PartitaConfig) PartitaState`
Questa funzione prende un config e genera il primo stato della partita.
- `Ruleset.Update(PartitaState, []PlayerMsg) (PartitaState, []PlayerCmd)`
Dopo la chiamata di `Ruleset.Start(...)` viene chiamata questa funzione senza nessun messaggio da processare in input.
Questa funzione viene chiamata quando arriva con tutti i risultati dei comandi inviati dalla funzione precedente (o almeno per quei comandi che ritornano un risultato).
**Esempio.**
Supponiamo ad esempio che sia la prima notte a questo punto, intanto aggiorniamo, gli unici ruoli che agiscono sono il _veggente_ e la _fattucchiera_ quindi come nuovo stato ritorniamo lo stesso stato solo con solo cambiato `state.Time = 1` e come lista di comandi ritorniamo i form per agire con le liste di tutti i giocatori (per ora sono tutti vivi quindi è la stessa lista).
```go
2 years ago
[]Cmd{
ChooseOptionCmd{
Target: "player-i", // veggente
Message: "Chi vuoi puntare?",
Options: allPlayers,
},
2 years ago
ChooseOptionCmd{
Target: "player-j", // fattucchiera
Message: "Chi vuoi puntare?",
Options: allPlayers,
},
}
```
A questo punto il codice relativo alla partita va in pausa ed una volta che entrambi i giocatori hanno compilato i propri form la risposta sarà (ad esempio diciamo che sia la _fattucchiera_ che il _veggente_ hanno puntato `player-1` che è un _contadino_)
```go
2 years ago
[]Msg{
ChooseOptionMsg{
Target: "player-i", // veggente
Answer: "player-1",
},
2 years ago
ChooseOptionMsg{
Target: "player-j", // fattucchiera
Answer: "player-1",
},
}
```
a questo punto lo stato avrà `state.Time = 1` quindi sappiamo che è giorno e usiamo i messaggi precedenti per risolvere la partita facendo prima cambiare l'aura alla fattucchiera al giocatore puntato e poi facciamo agire il veggente quindi ci basterà ritornare qualcosa del tipo
```go
DisplayMsg{
Target: "player-i", // veggente
Message: "@player-1 è stato visto nero"
},
```
2 years ago
ora però è giorno ed i giocatori dovranno anche votare per il rogo quindi dobbiamo anche inviare una lista di messaggio di scelta per il voto come segue. Inoltre se è stato incluso il kamikaze bisogna anche aggiungere un messaggio per far agire il kamikaze durante il giorno
```go
2 years ago
OrthogonalCmd{
Cases: [][]Cmd{
[]Cmd{
ChooseOptionCmd{
Target: "player-1",
Message: "Chi vuoi bruciare al rogo?",
Options: allAlivePlayers,
},
ChooseOptionCmd{
Target: "player-2",
Message: "Chi vuoi bruciare al rogo?",
Options: allAlivePlayers,
},
...
ChooseOptionCmd{
Target: "player-n",
Message: "Chi vuoi bruciare al rogo?",
Options: allAlivePlayers,
},
},
[]Cmd{
ChooseOptionCmd{
Target: "player-?", // kamikaze
Message: "Su chi vuoi farti esplodere?",
Options: allAlivePlayers,
}
},
},
}
```
e l'idea è che un comando di tipo `OptionalCmd` semplicemente wrappa un'altro `Cmd` e lo rende "opzionale" ovvero fa sì che non sia strettamente necessario per processare gli altri comandi.
## Architettura Lupus (2)
TODO: Boh forse si può fare proprio qualcosa ad "eventi".
2 years ago
```go
// ...
partita.On("player_action", func (e Event) {
sourcePlayer := e.Custom("source_player")
targetPlayer := e.Custom("target_player")
partita.FaseActionsCount++
partita.Actions = append(partita.Actions, &Action{
Time: partita.Time,
// ...
})
if partita.FaseActionsCount == partita.FaseActingPlayerCount {
partita.Dispatch("end_fase")
}
})
partita.On("kamikaze_explode", func (e Event) {
sourcePlayer := e.Custom("source_player")
targetPlayer := e.Custom("target_player")
partita.Actions = append(partita.Actions, &Action{
Time: partita.Time,
// ...
})
})
// ...
```