diff --git a/game/game.go b/game/game.go index 18c1113..5f3f01d 100644 --- a/game/game.go +++ b/game/game.go @@ -454,6 +454,8 @@ type Game struct { ChatLanguageISO639_1 string `methods:"POST,PUT"` GameMasterEnabled bool `methods:"POST"` RequireGameMasterInvitation bool `methods:"POST,PUT"` + GracePeriodMinutes time.Duration `methods:"POST,PUT"` + GracePeriodsPerPlayer int `methods:"POST,PUT"` GameMasterInvitations GameMasterInvitations GameMaster auth.User @@ -542,6 +544,12 @@ func (g *Game) canMergeInto(o *Game, avoid *auth.User) bool { if g.NationAllocation != o.NationAllocation { return false } + if g.GracePeriodMinutes != o.GracePeriodMinutes { + return false + } + if g.GracePeriodsPerPlayer != o.GracePeriodsPerPlayer { + return false + } if g.Anonymous != o.Anonymous { return false } @@ -812,6 +820,8 @@ func merge(ctx context.Context, r Request, game *Game, user *auth.User) (*Game, Filter("DisableGroupChat=", game.DisableGroupChat). Filter("DisablePrivateChat=", game.DisablePrivateChat). Filter("NationAllocation=", game.NationAllocation). + Filter("GracePeriodMinutes=", game.GracePeriodMinutes). + Filter("GracePeriodsPerPlayer=", game.GracePeriodsPerPlayer). Filter("Anonymous=", game.Anonymous). Filter("SkipMuster=", game.SkipMuster). Filter("ChatLanguageISO639_1=", game.ChatLanguageISO639_1). diff --git a/game/member.go b/game/member.go index a248117..60c5af2 100644 --- a/game/member.go +++ b/game/member.go @@ -59,6 +59,7 @@ type Member struct { NewestPhaseState PhaseState UnreadMessages int Replaceable bool + GracePeriodsUsed int } type Members []Member @@ -90,6 +91,7 @@ func (m *Member) Anonymize(r Request) { m.NationPreferences = "" m.UnreadMessages = 0 m.NewestPhaseState = PhaseState{} + m.GracePeriodsUsed = 0 m.User.Email = "" m.User.FamilyName = "Doe" m.User.GivenName = "John" diff --git a/game/phase.go b/game/phase.go index 451f30f..6a56669 100644 --- a/game/phase.go +++ b/game/phase.go @@ -785,6 +785,23 @@ func (p *PhaseResolver) Act() error { return p.actNonMustered() } + // Load order map, which is necessary for grace period calculation. + orderMap, err := p.Phase.Orders(p.Context) + if err != nil { + log.Errorf(p.Context, "Unable to load orders for %v: %v; fix phase.Orders or hope datastore will get fixed", PP(p.Phase), err) + return err + } + log.Infof(p.Context, "Orders at resolve time: %v", PP(orderMap)) + + // Check if we should postpone due to grace periods. + providedGrace, err := p.provideGrace(orderMap) + if err != nil { + return err + } + if providedGrace { + return nil + } + // Clean up old phase states, and populate the nonEliminatedUserIds slice if necessary. phaseStateIDs := make([]*datastore.Key, len(p.PhaseStates)) @@ -823,13 +840,6 @@ func (p *PhaseResolver) Act() error { log.Infof(p.Context, "PhaseStates at resolve time: %v", PP(p.PhaseStates)) - orderMap, err := p.Phase.Orders(p.Context) - if err != nil { - log.Errorf(p.Context, "Unable to load orders for %v: %v; fix phase.Orders or hope datastore will get fixed", PP(p.Phase), err) - return err - } - log.Infof(p.Context, "Orders at resolve time: %v", PP(orderMap)) - s, err := p.Phase.State(p.Context, p.Variant, orderMap) if err != nil { log.Errorf(p.Context, "Unable to create godip State for %v: %v; fix godip!", PP(p.Phase), err) @@ -1271,6 +1281,83 @@ func (p *PhaseResolver) Act() error { return nil } +func (p *PhaseResolver) provideGrace(orderMap map[godip.Nation]map[godip.Province][]string) (bool, error) { + if p.Game.GracePeriodMinutes == 0 || p.Game.GracePeriodsPerPlayer == 0 || p.Phase.PhaseMeta.GraceUsed { + return false, nil + } + + graceNations := []string{} + for memberIdx, member := range p.Game.Members { + _, hadOrders := orderMap[member.Nation] + wasReady := false + for _, phaseState := range p.PhaseStates { + if phaseState.Nation == member.Nation { + wasReady = phaseState.ReadyToResolve + } + } + if !wasReady && !hadOrders && member.GracePeriodsUsed < p.Game.GracePeriodsPerPlayer { + p.Game.Members[memberIdx].GracePeriodsUsed++ + p.Phase.GraceUsed = true + graceNations = append(graceNations, string(member.Nation)) + } + } + + if p.Phase.GraceUsed { + // Postpone the phase. + now := time.Now() + p.Phase.DeadlineAt = now.Add(time.Minute * p.Game.GracePeriodMinutes) + p.Game.NewestPhaseMeta = []PhaseMeta{p.Phase.PhaseMeta} + + // Save everything. + phaseID, err := p.Phase.ID(p.Context) + if err != nil { + log.Errorf(p.Context, "p.Phase.ID(...): %v; wtf?", err) + return false, err + } + toSave := []interface{}{ + p.Game, p.Phase, + } + keys := []*datastore.Key{ + p.Game.ID, phaseID, + } + if _, err := datastore.PutMulti(p.Context, keys, toSave); err != nil { + log.Errorf(p.Context, "datastore.PutMulti(..., %+v, %+v): %v; hope datastore gets fixed", keys, toSave, err) + return false, err + } + + // Notify everyone. + allMembers := []string{} + for _, nat := range p.Variant.Nations { + allMembers = append(allMembers, string(nat)) + } + notificationBody := fmt.Sprintf( + "%v %v ready to resolve and gave no orders, since they still have grace periods phase resolution has been postponed %v (until %v).", + english.OxfordWordSeries(graceNations, "and"), + english.Plural(len(graceNations), "wasn't", "weren't"), + p.Phase.DeadlineAt.Sub(now).Round(time.Minute), + p.Phase.DeadlineAt.Format(time.RFC822), + ) + if err := AsyncSendMsgFunc.EnqueueIn( + p.Context, 0, + p.Phase.GameID, + DiplicitySender, + allMembers, + notificationBody, + p.Phase.Host, + ); err != nil { + log.Errorf(p.Context, "AsyncSendMsgFunc(..., %v, %v, %+v, %q, %q): %v; fix it?", p.Phase.GameID, DiplicitySender, p.Variant.Nations, notificationBody, p.Phase.Host, err) + return false, err + } + + if err := p.Phase.ScheduleResolution(p.Context); err != nil { + log.Errorf(p.Context, "Unable to schedule resolution for %v: %v; fix ScheduleResolution or hope datastore gets fixed", PP(p.Phase), err) + return false, err + } + return true, nil + } + return false, nil +} + func (p *PhaseResolver) actNonMustered() error { if p.Game.Mustered { return fmt.Errorf("Game %+v is mustered!", p.Game) @@ -1559,6 +1646,7 @@ type PhaseMeta struct { Year int Type godip.PhaseType Resolved bool + GraceUsed bool CreatedAt time.Time CreatedAgo time.Duration `datastore:"-" ticker:"true"` ResolvedAt time.Time diff --git a/go.mod b/go.mod index ceb9f04..ad8730f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.14 require ( github.com/aymerick/raymond v2.0.2+incompatible github.com/davecgh/go-spew v1.1.1 - github.com/dustin/go-humanize v1.0.0 // indirect + github.com/dustin/go-humanize v1.0.0 github.com/golang/protobuf v1.4.3 // indirect github.com/gorilla/feeds v1.1.1 github.com/gorilla/mux v1.8.0