Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions game/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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).
Expand Down
2 changes: 2 additions & 0 deletions game/member.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type Member struct {
NewestPhaseState PhaseState
UnreadMessages int
Replaceable bool
GracePeriodsUsed int
}

type Members []Member
Expand Down Expand Up @@ -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"
Expand Down
102 changes: 95 additions & 7 deletions game/phase.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super keen on these error conditions returning false. The grace period has been used up and the deadline has been postponed, so I think this should return true, even though the messages weren't sent.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Go custom is that if the error is non-nil, then everything else should be ignored. The "false" is just because we need something to return, and false is the default boolean value.

Also - the error will cause the wrapping transaction to roll back, and thus no grace period will actually be used.

}

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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down