func ScheduleUnresolvedPhases(c common.SkinnyContext) (err error) { unresolved := Phases{} if err = c.DB().Query().Where(kol.Equals{"Resolved", false}).All(&unresolved); err != nil { return } for index, _ := range unresolved { (&unresolved[index]).Schedule(c) } return }
func (self *Phase) emailTo(c common.SkinnyContext, game *Game, member *Member, user *user.User) (err error) { to := fmt.Sprintf("%v <%v>", member.Nation, user.Email) unsubTag := &common.UnsubscribeTag{ T: common.UnsubscribePhaseEmail, U: user.Id, } unsubTag.H = unsubTag.Hash(c.Secret()) encodedUnsubTag, err := unsubTag.Encode() if err != nil { return } contextLink, err := user.I("To see this in context: http://%v/games/%v", user.DiplicityHost, self.GameId) if err != nil { return } unsubLink, err := user.I("To unsubscribe: http://%v/unsubscribe/%v", user.DiplicityHost, encodedUnsubTag) if err != nil { return } text, err := user.I("A new phase has been created") if err != nil { return } subject, err := game.Describe(c, user) if err != nil { return } body := fmt.Sprintf(common.EmailTemplate, text, contextLink, unsubLink) go c.SendMail("diplicity", c.ReceiveAddress(), subject, body, []string{to}) return }
func (self *Phase) autoResolve(c common.SkinnyContext) (err error) { c.Infof("Auto resolving %v/%v due to timeout", self.GameId, self.Id) if err = c.Transact(func(c common.SkinnyContext) (err error) { if err = c.DB().Get(self); err != nil { err = fmt.Errorf("While trying to load %+v: %v", self, err) return } if self.Resolved { c.Infof("%+v was already resolved", self) return } game := &Game{Id: self.GameId} if err = c.DB().Get(game); err != nil { err = fmt.Errorf("While trying to load %+v's game: %v", self, err) return } return game.resolve(c, self) }); err != nil { return } return }
func (self *Game) start(c common.SkinnyContext) (err error) { if self.State != common.GameStateCreated { err = fmt.Errorf("%+v is already started", self) return } self.State = common.GameStateStarted self.Closed = true if err = c.DB().Set(self); err != nil { return } var startState *state.State if self.Variant == common.ClassicalString { if startState, err = classical.Start(); err != nil { return } } else { err = fmt.Errorf("Unknown variant %v", self.Variant) return } startPhase := startState.Phase() epoch, err := epoch.Get(c.DB()) if err != nil { return } phase := &Phase{ GameId: self.Id, Ordinal: 0, Orders: map[dip.Nation]map[dip.Province][]string{}, Resolutions: map[dip.Province]string{}, Season: startPhase.Season(), Year: startPhase.Year(), Type: startPhase.Type(), Deadline: epoch + (time.Minute * time.Duration(self.Deadlines[startPhase.Type()])), } phase.Units, phase.SupplyCenters, phase.Dislodgeds, phase.Dislodgers, phase.Bounces, _ = startState.Dump() if err = c.DB().Set(phase); err != nil { return } if err = self.allocate(c.DB(), phase); err != nil { return } if err = phase.Schedule(c); err != nil { return } phase.SendStartedEmails(c, self) return }
func (self *Game) Describe(c common.SkinnyContext, trans common.Translator) (result string, err error) { switch self.State { case common.GameStateCreated: return trans.I(string(common.BeforeGamePhaseType)) case common.GameStateStarted: var phase *Phase if _, phase, err = self.Phase(c.DB(), 0); err != nil { return } season := "" if season, err = trans.I(string(phase.Season)); err != nil { return } typ := "" if typ, err = trans.I(string(phase.Type)); err != nil { return } return trans.I("game_phase_description", season, phase.Year, typ) case common.GameStateEnded: return trans.I(string(common.AfterGamePhaseType)) } err = fmt.Errorf("Unknown game state for %+v", self) return }
func Start(c common.SkinnyContext) (err error) { startedAt, err := Get(c.DB()) if err != nil { return } c.Infof("Started at epoch %v", startedAt) startedTime := time.Now() var currently time.Duration go func() { for { time.Sleep(time.Minute) currently = time.Now().Sub(startedTime) + startedAt atomic.StoreInt64(&deltaPoint, int64(time.Now().UnixNano())) if err = Set(c.DB(), currently); err != nil { panic(err) } c.Debugf("Epoch %v", currently) } }() return }
func (self *Message) Send(c common.SkinnyContext, game *Game, sender *Member) (err error) { c.Debugf("Sending %#v from %#v in %#v", self.Body, sender.Nation, game.Id.String()) // make sure the sender is correct self.SenderId = sender.Id senderUser := &user.User{Id: sender.UserId} if err = c.DB().Get(senderUser); err != nil { return } // make sure the sender is one of the recipients self.RecipientIds[sender.Id.String()] = true // The sender but nobody else saw it... self.SeenBy = map[string]bool{ sender.Id.String(): true, } // See what phase type the game is in var phaseType dip.PhaseType switch game.State { case common.GameStateCreated: phaseType = common.BeforeGamePhaseType case common.GameStateStarted: var phase *Phase if _, phase, err = game.Phase(c.DB(), 0); err != nil { return } phaseType = phase.Type case common.GameStateEnded: phaseType = common.AfterGamePhaseType default: err = fmt.Errorf("Unknown game state for %+v", game) return } // Find what chats are allowed during this phase type allowedFlags := game.ChatFlags[phaseType] // See if the recipient count is allowed recipients := len(self.RecipientIds) if recipients == 2 { if (allowedFlags & common.ChatPrivate) == 0 { err = IllegalMessageError{ Description: fmt.Sprintf("%+v does not allow %+v during %+v", game, self, phaseType), Phrase: "This kind of message is not allowed at this stage of the game", } return } } else if recipients == len(common.VariantMap[game.Variant].Nations) { if (allowedFlags & common.ChatConference) == 0 { err = IllegalMessageError{ Description: fmt.Sprintf("%+v does not allow %+v during %+v", game, self, phaseType), Phrase: "This kind of message is not allowed at this stage of the game", } return } } else if recipients > 2 { if (allowedFlags & common.ChatGroup) == 0 { err = IllegalMessageError{ Description: fmt.Sprintf("%+v does not allow %+v during %+v", game, self, phaseType), Phrase: "This kind of message is not allowed at this stage of the game", } return } } else { err = fmt.Errorf("%+v doesn't have any recipients", self) return } members, err := game.Members(c.DB()) if err != nil { return } if err = c.DB().Set(self); err != nil { return } recipNations := sort.StringSlice{} for memberId, _ := range self.RecipientIds { for _, member := range members { if memberId == member.Id.String() { if member.Nation != "" { recipNations = append(recipNations, string(member.Nation)) } } } } sort.Sort(recipNations) recipName := strings.Join(recipNations, ", ") subKey := fmt.Sprintf("/games/%v/messages", game.Id) for memberId, _ := range self.RecipientIds { for _, member := range members { if memberId == member.Id.String() && self.SenderId.String() != memberId { user := &user.User{Id: member.UserId} if err = c.DB().Get(user); err == nil { if !user.MessageEmailDisabled { if !c.IsSubscribing(user.Email, subKey) { memberCopy := member gameDescription := "" if gameDescription, err = game.Describe(c, user); err == nil { go self.EmailTo(c, game, sender, senderUser, &memberCopy, user, gameDescription, recipName) } else { c.Errorf("Trying to describe %+v to %+v: %v", game, user, err) } } else { c.Infof("Not sending to %#v, already subscribing to %#v", user.Id.String(), subKey) } } else { c.Infof("Not sending to %#v, message email disabled", user.Id.String()) } } else { c.Errorf("Trying to load user %#v: %v", member.UserId.String(), err) } } } } return }
func IncomingMail(c common.SkinnyContext, msg *enmime.MIMEBody) (err error) { text := gmail.DecodeText(msg.Text, msg.GetHeader("Content-Type")) c.Debugf("Incoming mail to %#v\n%v", msg.GetHeader("To"), text) if match := gmail.AddrReg.FindString(msg.GetHeader("To")); match != "" { lines := []string{} mailUser := strings.Split(c.SendAddress(), "@")[0] for _, line := range strings.Split(text, "\n") { if !strings.Contains(line, mailUser) && strings.Index(line, ">") != 0 { lines = append(lines, line) } } for len(lines) > 0 && strings.TrimSpace(lines[0]) == "" { lines = lines[1:] } for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" { lines = lines[:len(lines)-1] } if len(lines) > 0 { if match2 := emailPlusReg.FindStringSubmatch(match); match2 != nil { var tag *MailTag if tag, err = DecodeMailTag(c.Secret(), match2[1]); err == nil { sender := &Member{Id: tag.R} if err = c.DB().Get(sender); err != nil { return } parent := &Message{Id: tag.M} if err = c.DB().Get(parent); err != nil { return } game := &Game{Id: parent.GameId} if err = c.DB().Get(game); err != nil { return } message := &Message{ Body: strings.TrimSpace(strings.Join(lines, "\n")), GameId: game.Id, RecipientIds: parent.RecipientIds, } c.Infof("Mail resulted in %+v from %+v", message, sender.Nation) return message.Send(c, game, sender) } } } } return nil }
func (self *Message) EmailTo(c common.SkinnyContext, game *Game, sender *Member, senderUser *user.User, recip *Member, recipUser *user.User, subject, recipName string) { mailTag := &MailTag{ M: self.Id, R: recip.Id, } mailTag.H = mailTag.Hash(c.Secret()) encodedMailTag, err := mailTag.Encode() if err != nil { c.Errorf("Failed to encode %+v: %v", mailTag, err) return } unsubTag := &common.UnsubscribeTag{ T: common.UnsubscribeMessageEmail, U: recipUser.Id, } unsubTag.H = unsubTag.Hash(c.Secret()) encodedUnsubTag, err := unsubTag.Encode() if err != nil { c.Errorf("Failed to encode %+v: %v", unsubTag, err) return } parts := strings.Split(c.ReceiveAddress(), "@") if len(parts) != 2 { if c.Env() == common.Development { parts = []string{"user", "host.tld"} } else { c.Errorf("Failed parsing %#v as an email address", c.ReceiveAddress()) return } } senderName := sender.ShortName(game, senderUser) replyTo := fmt.Sprintf("%v+%v@%v", parts[0], encodedMailTag, parts[1]) to := fmt.Sprintf("%v <%v>", recipName, recipUser.Email) memberIds := []string{} for memberId, _ := range self.RecipientIds { memberIds = append(memberIds, memberId) } sort.Sort(sort.StringSlice(memberIds)) contextLink, err := recipUser.I("To see this message in context: http://%v/games/%v/messages/%v", recipUser.DiplicityHost, self.GameId, self.ChannelId()) if err != nil { c.Errorf("Failed translating context link: %v", err) return } unsubLink, err := recipUser.I("To unsubscribe: http://%v/unsubscribe/%v", recipUser.DiplicityHost, encodedUnsubTag) if err != nil { c.Errorf("Failed translating unsubscribe link: %v", err) return } body := fmt.Sprintf(common.EmailTemplate, self.Body, contextLink, unsubLink) c.SendMail(senderName, replyTo, subject, body, []string{to}) }
func (self *Game) resolve(c common.SkinnyContext, phase *Phase) (err error) { // Check that we are in a phase where we CAN resolve if self.State != common.GameStateStarted { err = fmt.Errorf("%+v is not started", self) return } // Load our members members, err := self.Members(c.DB()) if err != nil { return } // Load the godip state for the phase state, err := phase.State() if err != nil { return } // Load "now" epoch, err := epoch.Get(c.DB()) if err != nil { return } // Just to limit runaway resolution to 100 phases. for i := 0; i < 100; i++ { // Resolve the phase! if err = state.Next(); err != nil { return } // Load the new godip phase from the state nextDipPhase := state.Phase() // Create a diplicity phase for the new phase nextPhase := &Phase{ GameId: self.Id, Ordinal: phase.Ordinal + 1, Orders: map[dip.Nation]map[dip.Province][]string{}, Resolutions: map[dip.Province]string{}, Season: nextDipPhase.Season(), Year: nextDipPhase.Year(), Type: nextDipPhase.Type(), Deadline: epoch + (time.Minute * time.Duration(self.Deadlines[nextDipPhase.Type()])), } // Set the new phase positions var resolutions map[dip.Province]error nextPhase.Units, nextPhase.SupplyCenters, nextPhase.Dislodgeds, nextPhase.Dislodgers, nextPhase.Bounces, resolutions = state.Dump() // Store the results of the previous godip phase in the previous diplicity phase for _, nationOrders := range phase.Orders { for prov, _ := range nationOrders { if res, found := resolutions[prov]; found && res != nil { phase.Resolutions[prov] = res.Error() } else { phase.Resolutions[prov] = "OK" } } } // Commit everyone that doesn't have any orders to give waitFor := []*Member{} active := []*Member{} nonSurrendering := []*Member{} for index, _ := range members { opts := dip.Options{} if opts, err = nextPhase.Options(members[index].Nation); err != nil { return } if err = self.endPhaseConsequences(c, phase, &members[index], opts, &waitFor, &active, &nonSurrendering); err != nil { return } } // Mark the old phase as resolved, and save it phase.Resolved = true if err = c.DB().Set(phase); err != nil { return } // If we have a solo victor, end and return if winner := nextDipPhase.Winner(state); winner != nil { var winnerMember *Member for _, member := range members { if member.Nation == *winner { winnerMember = &member break } } if winnerMember == nil { err = fmt.Errorf("None of %+v has nation %#v??", members, *winner) return } if err = self.end(c, nextPhase, members, winnerMember, common.SoloVictory(*winner)); err != nil { return } return } // End the game now if nobody is active anymore if len(active) == 0 { if err = self.end(c, nextPhase, members, nil, common.ZeroActiveMembers); err != nil { return } return } // End the game now if only one player isn't surrendering if len(nonSurrendering) == 1 { if err = self.end(c, nextPhase, members, nonSurrendering[0], common.SoloVictory(nonSurrendering[0].Nation)); err != nil { return } return } // Store the next phase if err = c.DB().Set(nextPhase); err != nil { return } // If there is anyone we need to wait for, schedule an auto resolve and return here. if len(waitFor) > 0 { if err = nextPhase.Schedule(c); err != nil { return } nextPhase.SendScheduleEmails(c, self) return } phase = nextPhase } return }
func (self *Game) end(c common.SkinnyContext, phase *Phase, members Members, winner *Member, reason common.EndReason) (err error) { self.EndReason = reason self.State = common.GameStateEnded if err = c.DB().Set(self); err != nil { return } phase.Resolved = true if err = c.DB().Set(phase); err != nil { return } if self.Ranking && winner != nil { pot := 0.0 spend := 0.0 for index, _ := range members { if !members[index].Id.Equals(winner.Id) { user := &user.User{Id: members[index].UserId} if err = c.DB().Get(user); err != nil { return } spend = user.Ranking * RankingBlind pot += spend user.Ranking -= spend if err = c.DB().Set(user); err != nil { return } } } winnerUser := &user.User{Id: winner.UserId} if err = c.DB().Get(winnerUser); err != nil { return } winnerUser.Ranking += pot if err = c.DB().Set(winnerUser); err != nil { return } } return }
func (self *Game) endPhaseConsequences(c common.SkinnyContext, phase *Phase, member *Member, opts dip.Options, waitFor, active, nonSurrendering *[]*Member) (err error) { surrender := false if !member.Committed { alreadyHitReliability := false if (self.NonCommitConsequences & common.ReliabilityHit) == common.ReliabilityHit { if err = member.ReliabilityDelta(c.DB(), -1); err != nil { return } c.Infof("Increased MISSED deadlines for %#v by one because %+v and %+v", string(member.UserId), self, phase) alreadyHitReliability = true } if (self.NonCommitConsequences & common.NoWait) == common.NoWait { c.Infof("Setting %#v to NoWait because of %+v and %+v", string(member.UserId), self, phase) member.NoWait = true } if (self.NonCommitConsequences & common.Surrender) == common.Surrender { c.Infof("Setting %#v to Surrender because of %+v and %+v", string(member.UserId), self, phase) surrender = true } if len(phase.Orders[member.Nation]) == 0 { if !alreadyHitReliability && (self.NMRConsequences&common.ReliabilityHit) == common.ReliabilityHit { if err = member.ReliabilityDelta(c.DB(), -1); err != nil { return } c.Infof("Increased MISSED deadlines for %#v by one because %+v and %+v", string(member.UserId), self, phase) } if (self.NMRConsequences & common.NoWait) == common.NoWait { c.Infof("Setting %#v to NoWait because of %+v and %+v", string(member.UserId), self, phase) member.NoWait = true } if (self.NMRConsequences & common.Surrender) == common.Surrender { c.Infof("Setting %#v to Surrender because of %+v and %+v", string(member.UserId), self, phase) surrender = true } } } else { if (self.NonCommitConsequences & common.ReliabilityHit) == common.ReliabilityHit { if err = member.ReliabilityDelta(c.DB(), 1); err != nil { return } c.Infof("Increased HELD deadlines for %#v by one because %+v and %+v", string(member.UserId), self, phase) } } if !surrender { *nonSurrendering = append(*nonSurrendering, member) } member.Options = opts if member.NoWait { member.Committed = false member.NoOrders = false } else { *active = append(*active, member) if len(opts) == 0 { member.Committed = true member.NoOrders = true } else { *waitFor = append(*waitFor, member) member.Committed = false member.NoOrders = false } } if err = c.DB().Set(member); err != nil { return } return }
func (self *Phase) Schedule(c common.SkinnyContext) error { if !self.Resolved { ep, err := epoch.Get(c.DB()) if err != nil { return err } timeout := self.Deadline - ep c.BetweenTransactions(func(c common.SkinnyContext) { if timeout > 0 { time.AfterFunc(timeout, func() { if err := self.autoResolve(c); err != nil { c.Errorf("Failed resolving %+v after %v: %v", self, timeout, err) } }) c.Debugf("Scheduled resolution of %v/%v in %v at %v", self.GameId, self.Id, timeout, time.Now().Add(timeout)) } else { c.Debugf("Resolving %v/%v immediately, it is %v overdue", self.GameId, self.Id, -timeout) if err := self.autoResolve(c); err != nil { c.Errorf("Failed resolving %+v immediately: %v", self, err) } } }) } return nil }
func (self *Phase) SendStartedEmails(c common.SkinnyContext, game *Game) (err error) { members, err := game.Members(c.DB()) if err != nil { return } for _, member := range members { user := &user.User{Id: member.UserId} if err = c.DB().Get(user); err != nil { return } if !user.PhaseEmailDisabled { subKey := fmt.Sprintf("/games/%v", game.Id) if !c.IsSubscribing(user.Email, subKey, common.SubscriptionTimeout) { if err = self.emailTo(c, game, &member, user); err != nil { c.Errorf("Failed sending to %#v: %v", user.Id.String(), err) return } } else { c.Infof("Not sending to %#v, already subscribing to %#v", user.Email, subKey) } } else { c.Infof("Not sending to %#v, phase email disabled", user.Email) } } return }
func (self *Phase) SendScheduleEmails(c common.SkinnyContext, game *Game) { members, err := game.Members(c.DB()) for _, member := range members { user := &user.User{Id: member.UserId} if err = c.DB().Get(user); err != nil { return } to := fmt.Sprintf("%v <%v>", member.Nation, user.Email) if !user.PhaseEmailDisabled && !c.IsSubscribing(user.Email, fmt.Sprintf("/games/%v", game.Id)) { unsubTag := &common.UnsubscribeTag{ T: common.UnsubscribePhaseEmail, U: user.Id, } unsubTag.H = unsubTag.Hash(c.Secret()) encodedUnsubTag, err := unsubTag.Encode() if err != nil { c.Errorf("Failed to encode %+v: %v", unsubTag, err) return } contextLink, err := user.I("To see this in context: http://%v/games/%v", user.DiplicityHost, self.GameId) if err != nil { c.Errorf("Failed translating context link: %v", err) return } unsubLink, err := user.I("To unsubscribe: http://%v/unsubscribe/%v", user.DiplicityHost, encodedUnsubTag) if err != nil { c.Errorf("Failed translating unsubscribe link: %v", err) return } text, err := user.I("A new phase has been created") if err != nil { c.Errorf("Failed translating: %v", err) return } subject, err := game.Describe(c, user) if err != nil { c.Errorf("Failed describing: %v", err) return } body := fmt.Sprintf(common.EmailTemplate, text, contextLink, unsubLink) c.SendMail("diplicity", c.ReceiveAddress(), subject, body, []string{to}) } } }