/// <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()); }
/// <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)); }
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()); }
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); }
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; }
/// <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(); }
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(); }
/// <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 }); }
/// <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(); } }