diff --git a/README.md b/README.md index 8acddf5..7e716c6 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,99 @@ Ed infine c'è il modulo che si occupa del far avanzare le partite e della risol 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 + []PlayerCmd{ + ChoosePlayerCmd{ + Target: "player-i", // veggente + Message: "Chi vuoi puntare?", + Options: allPlayers, + }, + ChoosePlayerCmd{ + 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 + []PlayerMsg{ + ChoosePlayerMsg{ + Target: "player-i", // veggente + Answer: "player-1", + }, + ChoosePlayerMsg{ + 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 + + ```go + ChoosePlayerCmd{ + Target: "player-1", + Message: "Chi vuoi bruciare al rogo?", + Options: allAlivePlayers, + }, + ChoosePlayerCmd{ + Target: "player-2", + Message: "Chi vuoi bruciare al rogo?", + Options: allAlivePlayers, + }, + ... + ChoosePlayerCmd{ + Target: "player-n", + Message: "Chi vuoi bruciare al rogo?", + Options: allAlivePlayers, + }, + ``` + + inoltre se è stato incluso il kamikaze bisogna anche aggiungere un messaggio per far agire il kamikaze durante il giorno + + ```go + OptionalCmd{ + ChoosePlayerCmd{ + 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". diff --git a/lupus/constants.go b/lupus/constants.go new file mode 100644 index 0000000..e65973d --- /dev/null +++ b/lupus/constants.go @@ -0,0 +1,80 @@ +package lupus + +var ( + AuraBianca = "bianca" + AuraNera = "nera" +) + +var ( + FazioneBuoni = "buoni" + FazioneCattivi = "cattivi" +) + +var ( + Contadino = Ruolo{ + Uid: "contadino", + Nome: "Contadino", + Fazione: FazioneBuoni, + Aura: AuraBianca, + } + Lupo = Ruolo{ + Uid: "lupo", + Nome: "Lupo", + Fazione: FazioneCattivi, + Aura: AuraNera, + } + Fattucchiera = Ruolo{ + Uid: "fattucchiera", + Nome: "Fattucchiera", + Fazione: FazioneCattivi, + Aura: AuraNera, + } + Indemoniato = Ruolo{ + Uid: "indemoniato", + Nome: "Indemoniato", + Fazione: FazioneCattivi, + Aura: AuraBianca, + } + Kamikaze = Ruolo{ + Uid: "kamikaze", + Nome: "Kamikaze", + Fazione: FazioneBuoni, + Aura: AuraBianca, + } + Guardia = Ruolo{ + Uid: "guardia", + Nome: "Guardia", + Fazione: FazioneBuoni, + Aura: AuraBianca, + } + Cacciatore = Ruolo{ + Uid: "cacciatore", + Nome: "Cacciatore", + Fazione: FazioneBuoni, + Aura: AuraBianca, + } + Medium = Ruolo{ + Uid: "medium", + Nome: "Medium", + Fazione: FazioneBuoni, + Aura: AuraBianca, + } + Veggente = Ruolo{ + Uid: "veggente", + Nome: "Veggente", + Fazione: FazioneBuoni, + Aura: AuraBianca, + } +) + +// Ruoli è una lista di ruoli comuni, l'ordine è puramente casuale +var RuoliMap = map[string]Ruolo{ + Contadino.Uid: Contadino, + Lupo.Uid: Lupo, + Fattucchiera.Uid: Fattucchiera, + Indemoniato.Uid: Indemoniato, + Guardia.Uid: Guardia, + Cacciatore.Uid: Cacciatore, + Medium.Uid: Medium, + Veggente.Uid: Veggente, +} diff --git a/lupus/lupus.go b/lupus/lupus.go deleted file mode 100644 index 7891379..0000000 --- a/lupus/lupus.go +++ /dev/null @@ -1,132 +0,0 @@ -package lupus - -// Ruleset si occupa di far avanzare la partita, per ora un'implementazione di questa interfaccia è meglio se non ha stato (TODO: Forse sarebbe meglio come struct di funzioni) -type Ruleset interface { - // Start viene chiamata all'inizio di una partita per inizializzare una partita partendo da alcune info di configurazione - Start(PartitaConfig) PartitaState - - // Update prende lo stato della partita e ne ritorna uno nuovo eventualmente utilizzando delle "UserResponse" fatte precedentemente all'utente (alla prima chiamata questa lista è vuota), successivamente c'è una corrispondenza tra lo slice di "[]UserRequest" ritornato ed il successivo slice "[]UserResponse" ricevuto. - Update(state PartitaState, responses []UserResponse) (PartitaState, []UserRequest) -} - -// PartitaConfig è un config minimale per creare il primo stato della partita -type PartitaConfig struct { - Players []string - RoleCounts map[Ruolo]int -} - -// PartitaState rappresenta lo stato della partita -type PartitaState struct { - // Players è una mappa da username a giocatore - Players []Player `json:"players"` - // Time indica la fase corrente del gioco (la parità indica notte/giorno e si inizia da "Notte 0") - Time uint `json:"time"` - // PhaseActions indica quali azioni sono state fatte in una certa fase - Actions []Action `json:"actions"` - // Won inizialmente è nil e diventa &"FazioneBuoni" o &"FazioneCattivi" quando una delle due fazioni viene dichiarata vincitrice - Won *string -} - -type Player struct { - Username string `json:"username"` - Ruolo Ruolo `json:"ruolo"` - Vivo bool `json:"vivo"` -} - -type Ruolo struct { - Uid string `json:"uid"` - Nome string `json:"nome"` - Fazione string `json:"fazione"` - Aura string `json:"aura"` -} - -type Action struct { - Uid string `json:"uid"` - Time uint `json:"time"` // Time indica in quale fase è stata compiuta l'azione - Player string `json:"player"` - TargetPlayer string `json:"targetPlayer"` -} - -var ( - AuraBianca = "bianca" - AuraNera = "nera" -) - -var ( - FazioneBuoni = "buoni" - FazioneCattivi = "cattivi" -) - -var ( - Contadino = Ruolo{ - Uid: "contadino", - Nome: "Contadino", - Fazione: FazioneBuoni, - Aura: AuraBianca, - } - Lupo = Ruolo{ - Uid: "lupo", - Nome: "Lupo", - Fazione: FazioneCattivi, - Aura: AuraNera, - } - Fattucchiera = Ruolo{ - Uid: "fattucchiera", - Nome: "Fattucchiera", - Fazione: FazioneCattivi, - Aura: AuraNera, - } - Indemoniato = Ruolo{ - Uid: "indemoniato", - Nome: "Indemoniato", - Fazione: FazioneCattivi, - Aura: AuraBianca, - } - Guardia = Ruolo{ - Uid: "guardia", - Nome: "Guardia", - Fazione: FazioneBuoni, - Aura: AuraBianca, - } - Cacciatore = Ruolo{ - Uid: "cacciatore", - Nome: "Cacciatore", - Fazione: FazioneBuoni, - Aura: AuraBianca, - } - Medium = Ruolo{ - Uid: "medium", - Nome: "Medium", - Fazione: FazioneBuoni, - Aura: AuraBianca, - } - Veggente = Ruolo{ - Uid: "veggente", - Nome: "Veggente", - Fazione: FazioneBuoni, - Aura: AuraBianca, - } -) - -// Ruoli è una lista di ruoli comuni, l'ordine è puramente casuale -var RuoliMap = map[string]Ruolo{ - Contadino.Uid: Contadino, - Lupo.Uid: Lupo, - Fattucchiera.Uid: Fattucchiera, - Indemoniato.Uid: Indemoniato, - Guardia.Uid: Guardia, - Cacciatore.Uid: Cacciatore, - Medium.Uid: Medium, - Veggente.Uid: Veggente, -} - -type UserRequest struct { - TargetPlayer string - - Request any // TODO: Work in progress -} -type UserResponse struct { - TargetPlayer string - - Response any // TODO: Work in progress -} diff --git a/lupus/model.go b/lupus/model.go new file mode 100644 index 0000000..c1d5e13 --- /dev/null +++ b/lupus/model.go @@ -0,0 +1,85 @@ +package lupus + +// PartitaConfig è un config minimale per creare il primo stato della partita +type PartitaConfig struct { + Players []string + RoleCounts map[Ruolo]int +} + +// PartitaState rappresenta lo stato della partita +type PartitaState struct { + // Players è una mappa da username a giocatore + Players []Player `json:"players"` + // Time indica la fase corrente del gioco (la parità indica notte/giorno e si inizia da "Notte 0") + Time uint `json:"time"` + // PhaseActions indica quali azioni sono state fatte in una certa fase + Actions []Action `json:"actions"` + // Won inizialmente è nil e diventa &"FazioneBuoni" o &"FazioneCattivi" quando una delle due fazioni viene dichiarata vincitrice + Won *string +} + +func (p PartitaState) IsNotte() bool { return p.Time%2 == 0 } +func (p PartitaState) IsGiorno() bool { return p.Time%2 == 1 } + +func (p PartitaState) NumeroGiorno() uint { return (p.Time + 1) / 2 } + +type Player struct { + Username string `json:"username"` + Ruolo Ruolo `json:"ruolo"` + Vivo bool `json:"vivo"` +} + +type Action struct { + Uid string `json:"uid"` + Time uint `json:"time"` // Time indica in quale fase è stata compiuta l'azione + Player string `json:"player"` + TargetPlayer string `json:"targetPlayer"` +} + +type Ruolo struct { + Uid string `json:"uid"` + Nome string `json:"nome"` + Fazione string `json:"fazione"` + Aura string `json:"aura"` +} + +func RuoloMainCmd(player Player, state PartitaState) (cmd PlayerCommand, noop bool) { + switch player.Ruolo { + case Veggente, Lupo, Cacciatore, Guardia, Kamikaze: + alivePlayers := []string{} + for _, p := range state.Players { + if p.Vivo { + alivePlayers = append(alivePlayers, p.Username) + } + } + + return PlayerCommand{ + TargetPlayer: player.Username, + Request: ChoosePlayerCmd{alivePlayers}, + }, false + case Medium: + deadPlayers := []string{} + for _, p := range state.Players { + if !p.Vivo { + deadPlayers = append(deadPlayers, p.Username) + } + } + + return PlayerCommand{ + TargetPlayer: player.Username, + Request: ChoosePlayerCmd{deadPlayers}, + }, false + case Fattucchiera: + allPlayers := []string{} + for _, p := range state.Players { + allPlayers = append(allPlayers, p.Username) + } + + return PlayerCommand{ + TargetPlayer: player.Username, + Request: ChoosePlayerCmd{allPlayers}, + }, false + default: + return PlayerCommand{}, true + } +} diff --git a/lupus/ruleset.go b/lupus/ruleset.go new file mode 100644 index 0000000..7588847 --- /dev/null +++ b/lupus/ruleset.go @@ -0,0 +1,43 @@ +package lupus + +// Ruleset si occupa di far avanzare la partita +type Ruleset struct { + // Start viene chiamata all'inizio di una partita per inizializzare una partita partendo da alcune info di configurazione + Start func(PartitaConfig) PartitaState + + // Update prende lo stato della partita e ne ritorna uno nuovo eventualmente utilizzando delle "UserResponse" fatte precedentemente all'utente (alla prima chiamata questa lista è vuota), successivamente c'è una corrispondenza tra lo slice di "[]UserRequest" ritornato ed il successivo slice "[]UserResponse" ricevuto. + Update func(state PartitaState, responses []PlayerMessage) (PartitaState, []PlayerCommand) +} + +type Cmd interface{ Cmd() } +type Msg interface{ Msg() } + +// PlayerCommand rappresenta un "comando" per un certo giocatore, nella fattispecie può dire di mostrare al giocatore un messaggio o anche un prompt che chiede qualcosa all'utente +type PlayerCommand struct { + TargetPlayer string + + Request Cmd // TODO: Work in progress +} + +// PlayerMessage è una risposta da parte del giocatore ad un comando +type PlayerMessage struct { + TargetPlayer string + + Response Msg // TODO: Work in progress +} + +// +// Cmd & Msg +// + +type ChoosePlayerCmd struct { + Players []string +} + +func (ChoosePlayerCmd) Cmd() {} + +type ChoosePlayerMsg struct { + Player string +} + +func (ChoosePlayerMsg) Msg() {} diff --git a/lupus/ruleset_1.go b/lupus/ruleset_1.go index aa35cb5..6cdc7e9 100644 --- a/lupus/ruleset_1.go +++ b/lupus/ruleset_1.go @@ -6,40 +6,63 @@ import ( "github.com/aziis98/lupus-lite/util" ) -type ruleset1 struct{} +var Ruleset1 = Ruleset{ + Start: func(config PartitaConfig) PartitaState { + state := PartitaState{ + Players: make([]Player, len(config.Players)), + Time: 0, + Actions: []Action{}, + } -var Ruleset1 ruleset1 + // Creo uno slice con il numero di ruoli in base al numero corrispondente per ruolo in "config.RoleCounts" + ruoli := make([]Ruolo, 0, len(config.Players)) + for ruolo, count := range config.RoleCounts { + ruoli = append(ruoli, util.RepeatedSlice(ruolo, count)...) + } -func (ruleset1) Start(config PartitaConfig) PartitaState { - state := PartitaState{ - Players: make([]Player, len(config.Players)), - Time: 0, - Actions: []Action{}, - } + // ed i rimanenti sono contadini + for len(ruoli) < len(config.Players) { // riempi il resto dei ruoli con contadini + ruoli = append(ruoli, Contadino) + } - ruoli := []Ruolo{} - for ruolo, count := range config.RoleCounts { - ruoli = append(ruoli, util.RepeatedSlice(ruolo, count)...) - } + // mischio la lista di ruoli + util.Shuffle(ruoli) - for len(ruoli) < len(config.Players) { // riempi il resto dei ruoli con contadini - ruoli = append(ruoli, Contadino) - } + for i, username := range config.Players { + log.Printf(`Al giocatore %q è stato assegnato il ruolo di %q`, username, ruoli[i].Nome) - util.Shuffle(ruoli) // mischia la lista di ruoli + state.Players[i] = Player{ + Username: username, + Ruolo: ruoli[i], + Vivo: true, + } + } - for _, username := range config.Players { - var ruolo Ruolo - ruolo, ruoli = ruoli[0], ruoli[1:] + return state + }, + Update: func(state PartitaState, responses []PlayerMessage) (PartitaState, []PlayerCommand) { + if state.Time == 0 { + ruoliAgenti := map[Ruolo]struct{}{ + Fattucchiera: {}, + Veggente: {}, + } - log.Printf(`Al giocatore %q è stato assegnato il ruolo di %q`, username, ruolo.Nome) + playerCommands := []PlayerCommand{} - state.Players = append(state.Players, Player{ - Username: username, - Ruolo: ruolo, - Vivo: true, - }) - } + for _, player := range state.Players { + if _, present := ruoliAgenti[player.Ruolo]; present { + if cmd, noop := RuoloMainCmd(player, state); !noop { + playerCommands = append(playerCommands, cmd) + } + } + } - return state + state.Time++ + return state, playerCommands + } else { + + } + + return state, []PlayerCommand{} + }, }