예제 #1
0
        /// <summary>
        ///     Determines if the team should be locked out for their most
        ///     recent incorrect submission and returns the expiry time for the
        ///     lockout.
        /// </summary>
        /// <param name="ev"></param>
        /// <param name="submissions">
        ///     Expects submissions in chronological order
        /// </param>
        /// <param name="puzzleState"></param>
        /// <returns>
        ///     If the team should be locked out, returns the time when a team
        ///     can enter submissions again.
        ///     Null if the team should not be locked out.
        /// </returns>
        private static DateTime?ComputeLockoutExpiryTime(
            Event ev,
            IList <Submission> submissions,
            PuzzleStatePerTeam puzzleState)
        {
            int consecutiveWrongSubmissions = 0;

            /**
             * Count the number of submissions in the past N minutes where N is
             * the LockoutIncorrectGuessPeriod set for the event. If that count
             * exceeds the LockoutIncorrectGuessLimit set for the event, then
             * the team should be locked out of that puzzle.
             */
            DateTime incorrectGuessStartTime = DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(ev.LockoutIncorrectGuessPeriod));

            foreach (Submission s in submissions)
            {
                // if the guess is before the incorrect window, ignore it
                if (s.TimeSubmitted < incorrectGuessStartTime)
                {
                    continue;
                }

                if (s.Response == null)
                {
                    ++consecutiveWrongSubmissions;
                }
                else
                {
                    // Do not increment on partials, decrement instead! This is a tweak to improve the lockout story for DC puzzles.
                    // But don't overdecrement, lest we let people build a gigantic bank of free guesses.
                    consecutiveWrongSubmissions = Math.Max(0, consecutiveWrongSubmissions - 1);
                }
            }

            if (consecutiveWrongSubmissions <= ev.LockoutIncorrectGuessLimit)
            {
                return(null);
            }

            /**
             * The lockout duration is determined by the difference between the
             * count of wrong submissions in the lockout period and the lockout
             * limit. That difference is multiplied by the event's
             * LockoutDurationMultiplier to determine the lockout time in
             * minutes.
             */

            return(DateTime.UtcNow.AddMinutes(
                       (consecutiveWrongSubmissions -
                        ev.LockoutIncorrectGuessLimit) *
                       ev.LockoutDurationMultiplier));
        }
        /// <summary>
        /// Create missing PuzzleStatePerTeam rows from when they were lazily created
        /// </summary>
        public async Task <IActionResult> OnPostCreateMissingPSPTAsync()
        {
            List <int> allEvents = await(from ev in _context.Events
                                         select ev.ID).ToListAsync();

            var allPspts = await(from pspt in _context.PuzzleStatePerTeam
                                 select new { pspt.PuzzleID, pspt.TeamID }).ToDictionaryAsync(pspt => (((ulong)pspt.PuzzleID) << 32) | (uint)pspt.TeamID);

            List <PuzzleStatePerTeam> newPspts = new List <PuzzleStatePerTeam>();

            foreach (int ev in allEvents)
            {
                List <int> allPuzzles = await(from puzzle in _context.Puzzles
                                              where puzzle.EventID == ev
                                              select puzzle.ID).ToListAsync();
                List <int> allTeams = await(from team in _context.Teams
                                            where team.EventID == ev
                                            select team.ID).ToListAsync();

                foreach (int puzzle in allPuzzles)
                {
                    foreach (int team in allTeams)
                    {
                        ulong key = (((ulong)puzzle) << 32) | (uint)team;
                        if (!allPspts.ContainsKey(key))
                        {
                            PuzzleStatePerTeam newPspt = new PuzzleStatePerTeam()
                            {
                                PuzzleID = puzzle,
                                TeamID   = team,
                            };

                            newPspts.Add(newPspt);
                        }
                    }
                }
            }
            if (newPspts.Count > 0)
            {
                _context.PuzzleStatePerTeam.AddRange(newPspts.ToArray());
                await _context.SaveChangesAsync();

                Status = $"Added {newPspts.Count} missing PuzzleStatePerTeam rows";
            }
            else
            {
                Status = "No new PuzzleStatePerTeam rows needed";
            }

            return(Page());
        }
예제 #3
0
        /// <summary>
        ///     Determines if the team should be locked out for their most
        ///     recent incorrect submission and returns the expiry time for the
        ///     lockout.
        /// </summary>
        /// <param name="ev"></param>
        /// <param name="submissions">
        ///     Expects submissions in chronological order
        /// </param>
        /// <param name="puzzleState"></param>
        /// <returns>
        ///     If the team should be locked out, returns the time when a team
        ///     can enter submissions again.
        ///     Null if the team should not be locked out.
        /// </returns>
        private static DateTime?ComputeLockoutExpiryTime(
            Event ev,
            IList <Submission> submissions,
            PuzzleStatePerTeam puzzleState)
        {
            int consecutiveWrongSubmissions = 0;

            /**
             * Count the number of submissions in the past N minutes where N is
             * the LockoutIncorrectGuessPeriod set for the event. If that count
             * exceeds the LockoutIncorrectGuessLimit set for the event, then
             * the team should be locked out of that puzzle.
             */

            foreach (Submission s in submissions.Reverse())
            {
                // Do not increment on partials
                if (s.Response != null)
                {
                    continue;
                }

                if (s.TimeSubmitted.AddMinutes(ev.LockoutIncorrectGuessPeriod)
                    .CompareTo(DateTime.UtcNow) < 0)
                {
                    break;
                }

                ++consecutiveWrongSubmissions;
            }

            if (consecutiveWrongSubmissions <= ev.LockoutIncorrectGuessLimit)
            {
                return(null);
            }

            /**
             * The lockout duration is determined by the difference between the
             * count of wrong submissions in the lockout period and the lockout
             * limit. That difference is multiplied by the event's
             * LockoutDurationMultiplier to determine the lockout time in
             * minutes.
             */

            return(DateTime.UtcNow.AddMinutes(
                       (consecutiveWrongSubmissions -
                        ev.LockoutIncorrectGuessLimit) *
                       ev.LockoutDurationMultiplier));
        }
예제 #4
0
        public async Task <IActionResult> OnGet(int puzzleId)
        {
            PrereqPuzzleId = puzzleId;

            Puzzle puzzle = await(from puzz in _context.Puzzles
                                  where puzz.ID == puzzleId
                                  select puzz).FirstOrDefaultAsync();

            if (puzzle == null)
            {
                return(NotFound());
            }

            // Restrict this page to whistle stop non-puzzles
            if (puzzle.MinutesOfEventLockout == 0 || puzzle.IsPuzzle)
            {
                return(NotFound());
            }

            Team team = await UserEventHelper.GetTeamForPlayer(_context, Event, LoggedInUser);

            var puzzleStateQuery     = PuzzleStateHelper.GetFullReadOnlyQuery(_context, Event, null, team);
            PuzzleStatePerTeam state = await(from pspt in puzzleStateQuery
                                             where pspt.PuzzleID == puzzle.ID
                                             select pspt).FirstOrDefaultAsync();

            if (state == null)
            {
                return(NotFound());
            }

            // Only move forward if the puzzle is open and unsolved
            if (state.UnlockedTime == null || state.SolvedTime != null)
            {
                return(NotFound());
            }

            CurrentPuzzle = puzzle;

            // Treat all locked puzzles where this is a prerequisite as puzzles that can be unlocked
            PuzzleOptions = await(from pspt in puzzleStateQuery
                                  join prereq in _context.Prerequisites on pspt.PuzzleID equals prereq.PuzzleID
                                  where prereq.PrerequisiteID == puzzle.ID &&
                                  pspt.SolvedTime == null && pspt.UnlockedTime == null
                                  select prereq.Puzzle).ToListAsync();

            return(Page());
        }
예제 #5
0
        private static bool IsPuzzleSubmissionLimitReached(
            Event ev,
            IList <Submission> submissions,
            PuzzleStatePerTeam puzzleState)
        {
            uint wrongSubmissions = 0;

            foreach (Submission s in submissions)
            {
                if (s.Response == null)
                {
                    wrongSubmissions++;
                }
            }

            if (wrongSubmissions < puzzleState.WrongSubmissionCountBuffer)
            {
                return(false);
            }

            return(wrongSubmissions - puzzleState.WrongSubmissionCountBuffer >=
                   ev.MaxSubmissionCount);
        }
예제 #6
0
        public async Task <IActionResult> OnPostUnlock(int puzzleId, int unlockId)
        {
            Puzzle puzzle = await(from puzz in _context.Puzzles
                                  where puzz.ID == puzzleId
                                  select puzz).FirstOrDefaultAsync();

            if (puzzle == null)
            {
                return(NotFound());
            }

            // Restrict this page to whistle stop non-puzzles
            if (puzzle.MinutesOfEventLockout == 0 || puzzle.IsPuzzle)
            {
                return(NotFound());
            }

            Team team = await UserEventHelper.GetTeamForPlayer(_context, Event, LoggedInUser);

            using (var transaction = _context.Database.BeginTransaction())
            {
                var puzzleStateQuery           = PuzzleStateHelper.GetFullReadOnlyQuery(_context, Event, null, team);
                PuzzleStatePerTeam prereqState = await(from pspt in puzzleStateQuery
                                                       where pspt.PuzzleID == puzzle.ID
                                                       select pspt).FirstOrDefaultAsync();
                if (prereqState == null)
                {
                    return(NotFound());
                }

                // Only move forward if the prereq is open and unsolved
                if (prereqState.UnlockedTime == null || prereqState.SolvedTime != null)
                {
                    return(NotFound("Your team has already chosen and can't choose again"));
                }

                PuzzleStatePerTeam unlockState = await(from pspt in puzzleStateQuery
                                                       where pspt.PuzzleID == unlockId
                                                       select pspt).FirstOrDefaultAsync();
                if (unlockState == null)
                {
                    return(NotFound());
                }

                // The chosen puzzle must be locked (and unsolved)
                if (unlockState.UnlockedTime != null || unlockState.SolvedTime != null)
                {
                    return(NotFound("You've already chosen this puzzle"));
                }

                // Ensure the puzzle is actually one of the unlock options
                Prerequisites prereq = await(from pre in _context.Prerequisites
                                             where pre.PrerequisiteID == puzzleId && pre.PuzzleID == unlockId
                                             select pre).FirstOrDefaultAsync();
                if (prereq == null)
                {
                    return(NotFound());
                }

                await PuzzleStateHelper.SetSolveStateAsync(_context, Event, puzzle, team, DateTime.UtcNow);

                await PuzzleStateHelper.SetUnlockStateAsync(_context, Event, unlockState.Puzzle, team, DateTime.UtcNow);

                transaction.Commit();
            }

            Puzzle puzzleToUnlock = await(from p in _context.Puzzles
                                          where p.ID == unlockId
                                          select p).FirstOrDefaultAsync();

            string puzzleUrl;

            if (puzzleToUnlock.CustomURL != null)
            {
                puzzleUrl = PuzzleHelper.GetFormattedUrl(puzzleToUnlock, Event.ID);
            }
            else
            {
                puzzleUrl = puzzleToUnlock.PuzzleFile.UrlString;
            }

            return(Redirect(puzzleUrl));
        }
 public PuzzleWithState(Puzzle puzzle, PuzzleStatePerTeam state)
 {
     this.Puzzle = puzzle;
     this.State  = state;
 }
예제 #8
0
        /// <summary>
        /// Get a writable query of puzzle state. In the course of constructing the query, it will instantiate any state records that are missing on the server.
        /// </summary>
        /// <param name="context">The puzzle DB context</param>
        /// <param name="eventObj">The event we are querying from</param>
        /// <param name="puzzle">The puzzle; if null, get all puzzles in the event.</param>
        /// <param name="team">The team; if null, get all the teams in the event.</param>
        /// <returns>A query of PuzzleStatePerTeam objects that can be sorted and instantiated, but you can't edit the results.</returns>
        public static async Task <IQueryable <PuzzleStatePerTeam> > GetFullReadWriteQueryAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team)
        {
            if (context == null)
            {
                throw new ArgumentNullException("Context required.");
            }

            if (eventObj == null)
            {
                throw new ArgumentNullException("Event required.");
            }

            if (puzzle != null && team != null)
            {
                PuzzleStatePerTeam state = await context.PuzzleStatePerTeam.Where(s => s.Puzzle == puzzle && s.Team == team).FirstOrDefaultAsync();

                if (state == null)
                {
                    context.PuzzleStatePerTeam.Add(new DataModel.PuzzleStatePerTeam()
                    {
                        Puzzle = puzzle, Team = team
                    });
                }
            }
            else if (puzzle != null)
            {
                var teamIdsQ            = context.Teams.Where(p => p.Event == eventObj).Select(p => p.ID);
                var puzzleStateTeamIdsQ = context.PuzzleStatePerTeam.Where(s => s.Puzzle == puzzle).Select(s => s.TeamID);
                var teamIdsWithoutState = await teamIdsQ.Except(puzzleStateTeamIdsQ).ToListAsync();

                if (teamIdsWithoutState.Count > 0)
                {
                    for (int i = 0; i < teamIdsWithoutState.Count; i++)
                    {
                        context.PuzzleStatePerTeam.Add(new DataModel.PuzzleStatePerTeam()
                        {
                            Puzzle = puzzle, TeamID = teamIdsWithoutState[i]
                        });
                    }
                }
            }
            else if (team != null)
            {
                var puzzleIdsQ            = context.Puzzles.Where(p => p.Event == eventObj).Select(p => p.ID);
                var puzzleStatePuzzleIdsQ = context.PuzzleStatePerTeam.Where(s => s.Team == team).Select(s => s.PuzzleID);
                var puzzleIdsWithoutState = await puzzleIdsQ.Except(puzzleStatePuzzleIdsQ).ToListAsync();

                if (puzzleIdsWithoutState.Count > 0)
                {
                    for (int i = 0; i < puzzleIdsWithoutState.Count; i++)
                    {
                        context.PuzzleStatePerTeam.Add(new DataModel.PuzzleStatePerTeam()
                        {
                            Team = team, PuzzleID = puzzleIdsWithoutState[i]
                        });
                    }
                }
            }
            else if (puzzle == null && team == null)
            {
                throw new NotImplementedException("Full event query is NYI and may never be needed");
            }

            await context.SaveChangesAsync(); // query below will not return these unless we save

            // now this query is no longer sparse because we just filled it all out!
            return(GetSparseQuery(context, eventObj, puzzle, team));
        }
        /// <summary>
        /// Unlock any puzzles that need to be unlocked due to the recent solve of a prerequisite.
        /// </summary>
        /// <param name="context">The puzzle DB context</param>
        /// <param name="eventObj">The event we are working in</param>
        /// <param name="puzzleJustSolved">The puzzle just solved; if null, all the puzzles in the event (which will make more sense once we add per author filtering)</param>
        /// <param name="team">The team that just solved; if null, all the teams in the event.</param>
        /// <param name="unlockTime">The time that the puzzle should be marked as unlocked.</param>
        /// <returns></returns>
        private static async Task UnlockAnyPuzzlesThatThisSolveUnlockedAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzleJustSolved, Team team, DateTime unlockTime)
        {
            // a simple query for all puzzle IDs in the event - will be used at least once below
            IQueryable <int> allPuzzleIDsQ = context.Puzzles.Where(p => p.Event == eventObj).Select(p => p.ID);

            // if we solved a group of puzzles, every puzzle needs an update.
            // if we solved a single puzzle, only update the puzzles that have that one as a prerequisite.
            IQueryable <int> needsUpdatePuzzleIDsQ =
                puzzleJustSolved == null ?
                allPuzzleIDsQ :
                context.Prerequisites.Where(pre => pre.Prerequisite == puzzleJustSolved).Select(pre => pre.PuzzleID).Distinct();

            // get the prerequisites for all puzzles that need an update
            // information we get per puzzle: { id, min count, prerequisite IDs }
            var prerequisiteDataForNeedsUpdatePuzzles = await context.Prerequisites
                                                        .Where(pre => needsUpdatePuzzleIDsQ.Contains(pre.PuzzleID))
                                                        .GroupBy(pre => pre.Puzzle)
                                                        .Select(g => new {
                PuzzleID = g.Key.ID,
                g.Key.MinPrerequisiteCount,
                PrerequisiteIDs = g.Select(pre => pre.PrerequisiteID)
            })
                                                        .ToListAsync();

            // Are we updating one team or all teams?
            List <Team> teamsToUpdate = team == null ? await context.Teams.Where(t => t.Event == eventObj).ToListAsync() : new List <Team>()
            {
                team
            };

            // Update teams one at a time
            foreach (Team t in teamsToUpdate)
            {
                // Collect the IDs of all solved/unlocked puzzles for this team
                // sparse lookup is fine since if the state is missing it isn't unlocked or solved!
                var puzzleStateForTeamT = await PuzzleStateHelper.GetSparseQuery(context, eventObj, null, t)
                                          .Select(state => new { state.PuzzleID, state.UnlockedTime, state.SolvedTime })
                                          .ToListAsync();

                // Make a hash set out of them for easy lookup in case we have several prerequisites to chase
                HashSet <int> unlockedPuzzleIDsForTeamT = new HashSet <int>();
                HashSet <int> solvedPuzzleIDsForTeamT   = new HashSet <int>();

                foreach (var puzzleState in puzzleStateForTeamT)
                {
                    if (puzzleState.UnlockedTime != null)
                    {
                        unlockedPuzzleIDsForTeamT.Add(puzzleState.PuzzleID);
                    }

                    if (puzzleState.SolvedTime != null)
                    {
                        solvedPuzzleIDsForTeamT.Add(puzzleState.PuzzleID);
                    }
                }

                // now loop through all puzzles and count up who needs to be unlocked
                foreach (var puzzleToUpdate in prerequisiteDataForNeedsUpdatePuzzles)
                {
                    // already unlocked? skip
                    if (unlockedPuzzleIDsForTeamT.Contains(puzzleToUpdate.PuzzleID))
                    {
                        continue;
                    }

                    // Enough puzzles unlocked by count? Let's unlock it
                    if (puzzleToUpdate.PrerequisiteIDs.Where(id => solvedPuzzleIDsForTeamT.Contains(id)).Count() >= puzzleToUpdate.MinPrerequisiteCount)
                    {
                        PuzzleStatePerTeam state = await context.PuzzleStatePerTeam.Where(s => s.PuzzleID == puzzleToUpdate.PuzzleID && s.Team == t).FirstOrDefaultAsync();

                        if (state == null)
                        {
                            context.PuzzleStatePerTeam.Add(new DataModel.PuzzleStatePerTeam()
                            {
                                PuzzleID = puzzleToUpdate.PuzzleID, Team = t, UnlockedTime = unlockTime
                            });
                        }
                        else
                        {
                            state.UnlockedTime = unlockTime;
                        }
                    }
                }
            }

            // after looping through all teams, send one update with all changes made
            await context.SaveChangesAsync();
        }
예제 #10
0
        public async Task <IActionResult> OnPostImportAsync()
        {
            // the BindProperty only binds the event ID, let's get the rest
            if (await _context.Events.Where((e) => e.ID == ImportEventID).FirstOrDefaultAsync() == null)
            {
                return(NotFound());
            }

            // verify that we're an admin of the import event. current event administratorship is already validated.
            if (!await _context.EventAdmins.Where(ea => ea.Event.ID == ImportEventID && ea.Admin == LoggedInUser).AnyAsync())
            {
                return(Forbid());
            }

            var sourceEventAuthors = await _context.EventAuthors.Where((a) => a.Event.ID == ImportEventID).ToListAsync();

            var sourcePuzzles = await _context.Puzzles.Where((p) => p.Event.ID == ImportEventID).ToListAsync();

            // TODO: replace this with a checkbox and sufficient danger warnings about duplicate titles
            bool deletePuzzleIfPresent = true;

            using (var transaction = _context.Database.BeginTransaction(System.Data.IsolationLevel.Serializable))
            {
                // Step 1: Make sure all authors exist
                foreach (var sourceEventAuthor in sourceEventAuthors)
                {
                    var destEventAuthor = await _context.EventAuthors.Where((e) => e.Event == Event && e.Author == sourceEventAuthor.Author).FirstOrDefaultAsync();

                    if (destEventAuthor == null)
                    {
                        destEventAuthor       = new EventAuthors(sourceEventAuthor);
                        destEventAuthor.Event = Event;
                        _context.EventAuthors.Add(destEventAuthor);
                    }
                }

                // Step 2: Make sure all puzzles exist
                Dictionary <int, Puzzle> puzzleCloneMap = new Dictionary <int, Puzzle>();

                foreach (var sourcePuzzle in sourcePuzzles)
                {
                    // delete the puzzle if it exists
                    if (deletePuzzleIfPresent)
                    {
                        foreach (Puzzle p in _context.Puzzles.Where((p) => p.Event == Event && p.Name == sourcePuzzle.Name))
                        {
                            await PuzzleHelper.DeletePuzzleAsync(_context, p);
                        }
                    }

                    var destPuzzle = new Puzzle(sourcePuzzle);
                    destPuzzle.Event = Event;

                    puzzleCloneMap[sourcePuzzle.ID] = destPuzzle;
                    _context.Puzzles.Add(destPuzzle);
                }

                // Step 3: Save so that all our new objects have valid IDs
                await _context.SaveChangesAsync();

                // Step 4: Ancillary tables referring to puzzles
                foreach (var sourcePuzzle in sourcePuzzles)
                {
                    // PuzzleAuthors
                    foreach (PuzzleAuthors sourcePuzzleAuthor in _context.PuzzleAuthors.Where((p) => p.Puzzle == sourcePuzzle))
                    {
                        var destPuzzleAuthor = new PuzzleAuthors(sourcePuzzleAuthor);
                        destPuzzleAuthor.Puzzle = puzzleCloneMap[sourcePuzzleAuthor.Puzzle.ID];
                        _context.PuzzleAuthors.Add(destPuzzleAuthor);
                    }

                    // Responses
                    foreach (Response sourceResponse in _context.Responses.Where((r) => r.Puzzle == sourcePuzzle))
                    {
                        var destResponse = new Response(sourceResponse);
                        destResponse.Puzzle = puzzleCloneMap[sourceResponse.Puzzle.ID];
                        _context.Responses.Add(destResponse);
                    }

                    // Prerequisites
                    foreach (Prerequisites sourcePrerequisite in _context.Prerequisites.Where((r) => r.Puzzle == sourcePuzzle))
                    {
                        var destPrerequisite = new Prerequisites(sourcePrerequisite);
                        destPrerequisite.Puzzle       = puzzleCloneMap[sourcePrerequisite.Puzzle.ID];
                        destPrerequisite.Prerequisite = puzzleCloneMap[sourcePrerequisite.Prerequisite.ID];
                        _context.Prerequisites.Add(destPrerequisite);
                    }

                    // Hints
                    foreach (Hint sourceHint in _context.Hints.Where((h) => h.Puzzle == sourcePuzzle))
                    {
                        var destHint = new Hint(sourceHint);
                        destHint.Puzzle = puzzleCloneMap[sourceHint.Puzzle.ID];
                        _context.Hints.Add(destHint);

                        foreach (Team team in _context.Teams.Where(t => t.Event == Event))
                        {
                            _context.HintStatePerTeam.Add(new HintStatePerTeam()
                            {
                                Hint = destHint, TeamID = team.ID
                            });
                        }
                    }

                    // PuzzleStatePerTeam
                    foreach (Team team in _context.Teams.Where(t => t.Event == Event))
                    {
                        int  newPuzzleId           = puzzleCloneMap[sourcePuzzle.ID].ID;
                        bool hasPuzzleStatePerTeam = await(from pspt in _context.PuzzleStatePerTeam
                                                           where pspt.PuzzleID == newPuzzleId &&
                                                           pspt.TeamID == team.ID
                                                           select pspt).AnyAsync();
                        if (!hasPuzzleStatePerTeam)
                        {
                            PuzzleStatePerTeam newPspt = new PuzzleStatePerTeam()
                            {
                                TeamID = team.ID, PuzzleID = newPuzzleId
                            };
                            _context.PuzzleStatePerTeam.Add(newPspt);
                        }
                    }

                    // ContentFiles
                    foreach (ContentFile contentFile in _context.ContentFiles.Where((c) => c.Puzzle == sourcePuzzle))
                    {
                        ContentFile newFile = new ContentFile(contentFile);
                        newFile.Event  = Event;
                        newFile.Puzzle = puzzleCloneMap[contentFile.Puzzle.ID];
                        newFile.Url    = await FileManager.CloneBlobAsync(contentFile.ShortName, Event.ID, contentFile.Url);

                        _context.ContentFiles.Add(newFile);
                    }

                    // Pieces
                    foreach (Piece piece in _context.Pieces.Where((p) => p.Puzzle == sourcePuzzle))
                    {
                        Piece newPiece = new Piece(piece);
                        newPiece.Puzzle   = puzzleCloneMap[piece.PuzzleID];
                        newPiece.PuzzleID = puzzleCloneMap[piece.PuzzleID].ID; // unsure why I need this line for pieces but not others <shrug/>
                        _context.Pieces.Add(newPiece);
                    }
                }

                // Step 5: Final save and commit
                await _context.SaveChangesAsync();

                transaction.Commit();
            }

            return(RedirectToPage("./Details"));
        }
        /// <summary>
        /// Unlock any puzzles that need to be unlocked due to the recent solve of a prerequisite.
        /// </summary>
        /// <param name="context">The puzzle DB context</param>
        /// <param name="eventObj">The event we are working in</param>
        /// <param name="puzzleJustSolved">The puzzle just solved</param>
        /// <param name="team">The team that just solved; if null, all the teams in the event.</param>
        /// <param name="unlockTime">The time that the puzzle should be marked as unlocked.</param>
        /// <returns></returns>
        private static async Task UnlockAnyPuzzlesThatThisSolveUnlockedAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzleJustSolved, Team team, DateTime unlockTime)
        {
            // a simple query for all puzzle IDs in the event - will be used at least once below
            IQueryable <int> allPuzzleIDsQ = context.Puzzles.Where(p => p.Event == eventObj).Select(p => p.ID);


            // get the prerequisites for all puzzles that need an update
            // information we get per puzzle: { id, min count, number of solved prereqs including this one }
            var prerequisiteDataForNeedsUpdatePuzzles = (from possibleUnlock in context.Prerequisites
                                                         join unlockedBy in context.Prerequisites on possibleUnlock.PuzzleID equals unlockedBy.PuzzleID
                                                         join pspt in context.PuzzleStatePerTeam on unlockedBy.PrerequisiteID equals pspt.PuzzleID
                                                         join puz in context.Puzzles on unlockedBy.PrerequisiteID equals puz.ID
                                                         where possibleUnlock.Prerequisite == puzzleJustSolved && (team == null || pspt.TeamID == team.ID) && pspt.SolvedTime != null
                                                         group puz by new { unlockedBy.PuzzleID, unlockedBy.Puzzle.MinPrerequisiteCount, pspt.TeamID } into g
                                                         select new
            {
                PuzzleID = g.Key.PuzzleID,
                TeamID = g.Key.TeamID,
                g.Key.MinPrerequisiteCount,
                TotalPrerequisiteCount = g.Sum(p => (p.PrerequisiteWeight ?? 1))
            }).ToList();

            // Are we updating one team or all teams?
            List <Team> teamsToUpdate = team == null ? await context.Teams.Where(t => t.Event == eventObj).ToListAsync() : new List <Team>()
            {
                team
            };

            // Update teams one at a time
            foreach (Team t in teamsToUpdate)
            {
                // Collect the IDs of all solved/unlocked puzzles for this team
                // sparse lookup is fine since if the state is missing it isn't unlocked or solved!
                var puzzleStateForTeamT = await PuzzleStateHelper.GetSparseQuery(context, eventObj, null, t)
                                          .Select(state => new { state.PuzzleID, state.UnlockedTime, state.SolvedTime })
                                          .ToListAsync();

                // Make a hash set out of them for easy lookup in case we have several prerequisites to chase
                HashSet <int> unlockedPuzzleIDsForTeamT = new HashSet <int>();
                HashSet <int> solvedPuzzleIDsForTeamT   = new HashSet <int>();

                foreach (var puzzleState in puzzleStateForTeamT)
                {
                    if (puzzleState.UnlockedTime != null)
                    {
                        unlockedPuzzleIDsForTeamT.Add(puzzleState.PuzzleID);
                    }

                    if (puzzleState.SolvedTime != null)
                    {
                        solvedPuzzleIDsForTeamT.Add(puzzleState.PuzzleID);
                    }
                }

                // now loop through all puzzles and count up who needs to be unlocked
                foreach (var puzzleToUpdate in prerequisiteDataForNeedsUpdatePuzzles)
                {
                    // already unlocked? skip
                    if (unlockedPuzzleIDsForTeamT.Contains(puzzleToUpdate.PuzzleID))
                    {
                        continue;
                    }

                    // Enough puzzles unlocked by count? Let's unlock it
                    if (puzzleToUpdate.TeamID == t.ID && puzzleToUpdate.TotalPrerequisiteCount >= puzzleToUpdate.MinPrerequisiteCount)
                    {
                        PuzzleStatePerTeam state = await context.PuzzleStatePerTeam.Where(s => s.PuzzleID == puzzleToUpdate.PuzzleID && s.Team == t).FirstAsync();

                        state.UnlockedTime = unlockTime;
                    }
                }
            }

            // after looping through all teams, send one update with all changes made
            await context.SaveChangesAsync();
        }
예제 #12
0
        /// <summary>
        /// Evaulates player submissions then either saves them to the database or returns an error to the caller
        /// </summary>
        public static async Task <SubmissionResponse> EvaluateSubmission(PuzzleServerContext context, PuzzleUser loggedInUser, Event thisEvent, int puzzleId, string submissionText, bool allowFreeformSharing)
        {
            //Query data needed to process submission
            Team team = await UserEventHelper.GetTeamForPlayer(context, thisEvent, loggedInUser);

            Puzzle puzzle = await context.Puzzles.Where(
                (p) => p.ID == puzzleId).FirstOrDefaultAsync();

            PuzzleStatePerTeam puzzleState = await(PuzzleStateHelper
                                                   .GetFullReadOnlyQuery(
                                                       context,
                                                       thisEvent,
                                                       puzzle,
                                                       team))
                                             .FirstAsync();

            List <Puzzle> puzzlesCausingGlobalLockout = await PuzzleStateHelper.PuzzlesCausingGlobalLockout(context, thisEvent, team).ToListAsync();

            // Return early for cases when there's obviously nothing we can do with the submission
            // The submission text is empty
            if (String.IsNullOrWhiteSpace(submissionText))
            {
                return(new SubmissionResponse()
                {
                    ResponseCode = SubmissionResponseCode.EmptySubmission
                });
            }

            // The puzzle is locked
            if (puzzle == null || puzzleState.UnlockedTime == null)
            {
                return(new SubmissionResponse()
                {
                    ResponseCode = SubmissionResponseCode.PuzzleLocked
                });
            }

            // The user or team isn't known
            if (loggedInUser == null || team == null)
            {
                return(new SubmissionResponse()
                {
                    ResponseCode = SubmissionResponseCode.Unauthorized
                });
            }

            // The event hasn't started yet
            if (DateTime.UtcNow < thisEvent.EventBegin)
            {
                return(new SubmissionResponse()
                {
                    ResponseCode = SubmissionResponseCode.Unauthorized
                });
            }

            // The team is locked out
            if (puzzleState.IsTeamLockedOut || puzzleState.IsEmailOnlyMode)
            {
                return(new SubmissionResponse()
                {
                    ResponseCode = SubmissionResponseCode.TeamLockedOut
                });
            }

            // The puzzle has already been solved
            if (puzzleState.SolvedTime != null)
            {
                return(new SubmissionResponse()
                {
                    ResponseCode = SubmissionResponseCode.AlreadySolved
                });
            }

            // The team is under a global lockout
            if (puzzlesCausingGlobalLockout.Count != 0 && !puzzlesCausingGlobalLockout.Contains(puzzle))
            {
                return(new SubmissionResponse()
                {
                    ResponseCode = SubmissionResponseCode.TeamLockedOut
                });
            }

            List <SubmissionView> submissionViews = await(from sub in context.Submissions
                                                          join user in context.PuzzleUsers on sub.Submitter equals user
                                                          join r in context.Responses on sub.Response equals r into responses
                                                          from response in responses.DefaultIfEmpty()
                                                          where sub.Team == team &&
                                                          sub.Puzzle == puzzle
                                                          orderby sub.TimeSubmitted
                                                          select new SubmissionView()
            {
                Submission      = sub,
                Response        = response,
                SubmitterName   = user.Name,
                FreeformReponse = sub.FreeformResponse,
                IsFreeform      = puzzle.IsFreeform
            }).ToListAsync();

            List <Submission> submissions = new List <Submission>(submissionViews.Count);

            foreach (SubmissionView submissionView in submissionViews)
            {
                submissions.Add(submissionView.Submission);
            }

            // The submission is a duplicate
            bool duplicateSubmission = (from sub in submissions
                                        where sub.SubmissionText == Response.FormatSubmission(submissionText)
                                        select sub).Any();

            if (duplicateSubmission)
            {
                return(new SubmissionResponse()
                {
                    ResponseCode = SubmissionResponseCode.DuplicateSubmission
                });
            }

            // Create submission and add it to list
            Submission submission = new Submission
            {
                TimeSubmitted        = DateTime.UtcNow,
                Puzzle               = puzzleState.Puzzle,
                Team                 = puzzleState.Team,
                Submitter            = loggedInUser,
                AllowFreeformSharing = allowFreeformSharing
            };

            string submissionTextToCheck = Response.FormatSubmission(submissionText);

            if (puzzle.IsFreeform)
            {
                submission.UnformattedSubmissionText = submissionText;
            }
            else
            {
                submission.SubmissionText = submissionText;
            }

            submission.Response = await context.Responses.Where(
                r => r.Puzzle.ID == puzzleId &&
                submissionTextToCheck == r.SubmittedText)
                                  .FirstOrDefaultAsync();

            submissions.Add(submission);

            // Update puzzle state if submission was correct
            if (submission.Response != null && submission.Response.IsSolution)
            {
                await PuzzleStateHelper.SetSolveStateAsync(context,
                                                           thisEvent,
                                                           submission.Puzzle,
                                                           submission.Team,
                                                           submission.TimeSubmitted);
            }
            else if (!puzzle.IsFreeform && submission.Response == null && thisEvent.IsAnswerSubmissionActive)
            {
                // We also determine if the puzzle should be set to email-only mode.
                if (IsPuzzleSubmissionLimitReached(
                        thisEvent,
                        submissions,
                        puzzleState))
                {
                    await PuzzleStateHelper.SetEmailOnlyModeAsync(context,
                                                                  thisEvent,
                                                                  submission.Puzzle,
                                                                  submission.Team,
                                                                  true);

                    var authors = await context.PuzzleAuthors.Where((pa) => pa.Puzzle == submission.Puzzle).Select((pa) => pa.Author.Email).ToListAsync();

                    MailHelper.Singleton.SendPlaintextBcc(authors,
                                                          $"{thisEvent.Name}: Team {submission.Team.Name} is in email mode for {submission.Puzzle.Name}",
                                                          "");
                }
                else
                {
                    // If the submission was incorrect and not a partial solution,
                    // we will do the lockout computations now.
                    DateTime?lockoutExpiryTime = ComputeLockoutExpiryTime(
                        thisEvent,
                        submissions,
                        puzzleState);

                    if (lockoutExpiryTime != null)
                    {
                        await PuzzleStateHelper.SetLockoutExpiryTimeAsync(context,
                                                                          thisEvent,
                                                                          submission.Puzzle,
                                                                          submission.Team,
                                                                          lockoutExpiryTime);
                    }
                }
            }

            context.Submissions.Add(submission);
            await context.SaveChangesAsync();

            // Send back responses for cases where the database has been updated
            // Correct response
            if (submission.Response != null && submission.Response.IsSolution)
            {
                return(new SubmissionResponse()
                {
                    ResponseCode = SubmissionResponseCode.Correct, CompleteResponse = submission.Response
                });
            }

            // Freeform response
            if (puzzle.IsFreeform)
            {
                return(new SubmissionResponse()
                {
                    ResponseCode = SubmissionResponseCode.Freeform, FreeformResponse = submission.FreeformResponse
                });
            }

            // Partial response
            if (submission.Response != null && !submission.Response.IsSolution)
            {
                return(new SubmissionResponse()
                {
                    ResponseCode = SubmissionResponseCode.Partial, CompleteResponse = submission.Response
                });
            }

            // Default to incorrect
            return(new SubmissionResponse()
            {
                ResponseCode = SubmissionResponseCode.Incorrect
            });
        }
예제 #13
0
        /// <summary>
        /// Returns whether the user is authorized to view the file
        /// </summary>
        /// <param name="eventId">The current event</param>
        /// <param name="puzzle">The puzzle the file belongs to</param>
        /// <param name="content">The file</param>
        private async Task <bool> IsAuthorized(int eventId, Puzzle puzzle, ContentFile content)
        {
            Event currentEvent = await(from ev in context.Events
                                       where ev.ID == eventId
                                       select ev).SingleAsync();
            PuzzleUser user = await PuzzleUser.GetPuzzleUserForCurrentUser(context, User, userManager);

            // Admins can see all files
            if (await user.IsAdminForEvent(context, currentEvent))
            {
                return(true);
            }

            // Authors can see all files attached to their puzzles
            if (await UserEventHelper.IsAuthorOfPuzzle(context, puzzle, user))
            {
                return(true);
            }

            Team team = await UserEventHelper.GetTeamForPlayer(context, currentEvent, user);

            if (team == null)
            {
                return(false);
            }

            // Once answers are available, so are all other files
            if (currentEvent.AreAnswersAvailableNow)
            {
                return(true);
            }

            PuzzleStatePerTeam puzzleState = await PuzzleStateHelper.GetFullReadOnlyQuery(context, currentEvent, puzzle, team).SingleAsync();

            switch (content.FileType)
            {
            case ContentFileType.Answer:
                // The positive case is already handled above by checking AreAnswersAvailableNow
                return(false);

            case ContentFileType.Puzzle:
            case ContentFileType.PuzzleMaterial:
                if (puzzleState.UnlockedTime != null)
                {
                    return(true);
                }
                else
                {
                    return(false);
                }

            case ContentFileType.SolveToken:
                if (puzzleState.SolvedTime != null)
                {
                    return(true);
                }
                else
                {
                    return(false);
                }

            default:
                throw new NotImplementedException();
            }
        }