diff --git a/README.md b/README.md index 7e716c6..9a0a453 100644 --- a/README.md +++ b/README.md @@ -81,13 +81,13 @@ Lo stato della partita è contenuto in `lupus.PartitaState` e viene aggiornato d 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{ + []Cmd{ + ChooseOptionCmd{ Target: "player-i", // veggente Message: "Chi vuoi puntare?", Options: allPlayers, }, - ChoosePlayerCmd{ + ChooseOptionCmd{ Target: "player-j", // fattucchiera Message: "Chi vuoi puntare?", Options: allPlayers, @@ -98,12 +98,12 @@ Lo stato della partita è contenuto in `lupus.PartitaState` e viene aggiornato d 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{ + []Msg{ + ChooseOptionMsg{ Target: "player-i", // veggente Answer: "player-1", }, - ChoosePlayerMsg{ + ChooseOptionMsg{ Target: "player-j", // fattucchiera Answer: "player-1", }, @@ -119,36 +119,37 @@ Lo stato della partita è contenuto in `lupus.PartitaState` e viene aggiornato d }, ``` - 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 + 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 - 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, - } + 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, + } + }, + }, } ``` @@ -157,3 +158,33 @@ Lo stato della partita è contenuto in `lupus.PartitaState` e viene aggiornato d ## 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, + // ... + }) +}) +// ... +``` diff --git a/lupus/model.go b/lupus/model.go index c1d5e13..9dbbf78 100644 --- a/lupus/model.go +++ b/lupus/model.go @@ -43,43 +43,43 @@ type Ruolo struct { Aura string `json:"aura"` } -func RuoloMainCmd(player Player, state PartitaState) (cmd PlayerCommand, noop bool) { +var defaultMessageMap = map[Ruolo]string{ + Veggente: "Chi vuoi veggiare?", + Lupo: "Chi vuoi uccidere?", + Cacciatore: "Chi vuoi cacciare?", + Guardia: "Chi vuoi guardiare?", + Medium: "Chi vuoi mediare?", + Fattucchiera: "Chi vuoi fattuccare?", + Kamikaze: "Su chi vuoi farti esplodere?", +} + +func RuoloMainCmd(player Player, state PartitaState) Cmd { + options := []string{} + switch player.Ruolo { case Veggente, Lupo, Cacciatore, Guardia, Kamikaze: - alivePlayers := []string{} for _, p := range state.Players { if p.Vivo { - alivePlayers = append(alivePlayers, p.Username) + options = append(options, 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) + options = append(options, 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) + options = append(options, p.Username) } - - return PlayerCommand{ - TargetPlayer: player.Username, - Request: ChoosePlayerCmd{allPlayers}, - }, false default: - return PlayerCommand{}, true + return nil } + + return NewChooseOptionCmd( + player.Username, + defaultMessageMap[player.Ruolo], + options, + ) } diff --git a/lupus/ruleset.go b/lupus/ruleset.go index 7588847..768926e 100644 --- a/lupus/ruleset.go +++ b/lupus/ruleset.go @@ -6,38 +6,69 @@ type Ruleset struct { 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) + Update func(state PartitaState, responses []Msg) (PartitaState, []Cmd) } -type Cmd interface{ Cmd() } -type Msg interface{ Msg() } +// Commands -// 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 +type Cmd interface { + TargetPlayer() string +} + +type baseCmd struct { + targetPlayer string +} + +func (c baseCmd) TargetPlayer() string { + return c.targetPlayer +} + +// Messages - Request Cmd // TODO: Work in progress +type Msg interface { + TargetPlayer() string } -// PlayerMessage è una risposta da parte del giocatore ad un comando -type PlayerMessage struct { - TargetPlayer string +type baseMsg struct { + targetPlayer string +} - Response Msg // TODO: Work in progress +func (m baseMsg) TargetPlayer() string { + return m.targetPlayer } +// -------------------- // +// Commands and Message // +// -------------------- // + // -// Cmd & Msg +// Choose Option Form // -type ChoosePlayerCmd struct { - Players []string +type ChooseOptionCmd struct { + baseCmd + Message string + Options []string } -func (ChoosePlayerCmd) Cmd() {} +func NewChooseOptionCmd(player, message string, options []string) Cmd { + return &ChooseOptionCmd{baseCmd{player}, message, options} +} -type ChoosePlayerMsg struct { - Player string +type ChooseOptionMsg struct { + baseMsg + Answer string } -func (ChoosePlayerMsg) Msg() {} +// +// Display Message Form +// + +type DisplayMessageCmd struct { + baseMsg + Message string +} + +func NewDisplayCmd(option, message string) Cmd { + return &DisplayMessageCmd{baseMsg{option}, message} +} diff --git a/lupus/ruleset_1.go b/lupus/ruleset_1.go index 6cdc7e9..b068b8e 100644 --- a/lupus/ruleset_1.go +++ b/lupus/ruleset_1.go @@ -1,11 +1,15 @@ package lupus import ( + "fmt" "log" + "math/rand" "github.com/aziis98/lupus-lite/util" ) +const MessageRogo = "Chi vuoi bruciare al rogo?" + var Ruleset1 = Ruleset{ Start: func(config PartitaConfig) PartitaState { state := PartitaState{ @@ -40,29 +44,70 @@ var Ruleset1 = Ruleset{ return state }, - Update: func(state PartitaState, responses []PlayerMessage) (PartitaState, []PlayerCommand) { + Update: func(state PartitaState, responses []Msg) (PartitaState, []Cmd) { + playerCommands := []Cmd{} + if state.Time == 0 { ruoliAgenti := map[Ruolo]struct{}{ Fattucchiera: {}, Veggente: {}, } - playerCommands := []PlayerCommand{} - for _, player := range state.Players { if _, present := ruoliAgenti[player.Ruolo]; present { - if cmd, noop := RuoloMainCmd(player, state); !noop { - playerCommands = append(playerCommands, cmd) - } + playerCommands = append(playerCommands, RuoloMainCmd(player, state)) + } + } + + state.Time++ + return state, playerCommands + } + + // di giorno i giocatori vivi ricevono un form per votare qualcuno da bruciare al rogo e possono scegliere solo tra i vivi + if state.IsGiorno() { + alivePlayers := []string{} + for _, player := range state.Players { + if player.Vivo { + alivePlayers = append(alivePlayers, player.Username) } } + for _, option := range alivePlayers { + playerCommands = append(playerCommands, NewChooseOptionCmd( + option, + MessageRogo, + alivePlayers, + )) + } + state.Time++ return state, playerCommands - } else { + } + + if state.IsNotte() { + i := rand.Intn(len(state.Players)) + + diedPlayer := state.Players[i] + state.Players[i].Vivo = false + + alivePlayers := []string{} + for _, player := range state.Players { + if player.Vivo { + alivePlayers = append(alivePlayers, player.Username) + } + } + + for _, playerId := range alivePlayers { + playerCommands = append(playerCommands, NewDisplayCmd( + playerId, + fmt.Sprintf("@%s è morto questa notte", diedPlayer.Username), + )) + } + state.Time++ + return state, playerCommands } - return state, []PlayerCommand{} + return state, []Cmd{} }, } diff --git a/util/util.go b/util/util.go index 4f0b113..2491d16 100644 --- a/util/util.go +++ b/util/util.go @@ -56,3 +56,30 @@ func Shuffle[T any](slice []T) { slice[i], slice[j] = slice[j], slice[i] }) } + +func Sample[T any](vs []T, count int) []T { + buffer := append([]T{}, vs...) + result := make([]T, count) + + for i := 0; i < count; i++ { + n := rand.Intn(len(buffer)) + result[i], buffer = buffer[n], append(buffer[:n], buffer[n+1:]...) + } + + return result +} + +func GroupBy[K comparable, V any](vs []V, keyFn func(V) K) map[K][]V { + r := map[K][]V{} + for _, v := range vs { + key := keyFn(v) + group, ok := r[key] + if !ok { + group = []V{} + } + + r[key] = append(group, v) + } + + return r +}