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
191 lines
7.6 KiB
Markdown
# Lupus Lite
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
# Development Mode: also starts "npm run dev" inside "_frontend/"
|
|
$ MODE=dev go run .
|
|
|
|
# Production Mode
|
|
$ 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
|
|
[]Cmd{
|
|
ChooseOptionCmd{
|
|
Target: "player-i", // veggente
|
|
Message: "Chi vuoi puntare?",
|
|
Options: allPlayers,
|
|
},
|
|
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
|
|
[]Msg{
|
|
ChooseOptionMsg{
|
|
Target: "player-i", // veggente
|
|
Answer: "player-1",
|
|
},
|
|
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"
|
|
},
|
|
```
|
|
|
|
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
|
|
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".
|
|
|
|
```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,
|
|
// ...
|
|
})
|
|
})
|
|
// ...
|
|
```
|