/// <summary> /// Set the solve state of some puzzle state records. In the course of setting the state, 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> /// <param name="value">The solve time (null if unsolving)</param> /// <param name="author"></param> /// <returns> /// A task that can be awaited for the solve/unsolve operation /// </returns> public static async Task SetSolveStateAsync( PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, DateTime?value, PuzzleUser author = null) { IQueryable <PuzzleStatePerTeam> statesQ = PuzzleStateHelper .GetFullReadWriteQuery(context, eventObj, puzzle, team, author); List <PuzzleStatePerTeam> states = await statesQ.ToListAsync(); for (int i = 0; i < states.Count; i++) { // Only allow solved time to be modified if it is being marked as unsolved (set to null) or if it is being solved for the first time if (value == null || states[i].SolvedTime == null) { // Unlock puzzles when solving them if (value != null && states[i].UnlockedTime == null) { states[i].UnlockedTime = value; } states[i].SolvedTime = value; } } // Award hint coins if (value != null && puzzle != null && puzzle.HintCoinsForSolve != 0) { if (team != null) { team.HintCoinCount += puzzle.HintCoinsForSolve; } else { var allTeams = from Team curTeam in context.Teams where curTeam.Event == eventObj select curTeam; foreach (Team curTeam in allTeams) { curTeam.HintCoinCount += puzzle.HintCoinsForSolve; } } } await context.SaveChangesAsync(); // if this puzzle got solved, look for others to unlock if (puzzle != null && value != null) { await UnlockAnyPuzzlesThatThisSolveUnlockedAsync(context, eventObj, puzzle, team, value.Value); } }
public static IQueryable <Puzzle> PuzzlesCausingGlobalLockout( PuzzleServerContext context, Event eventObj, Team team) { DateTime now = DateTime.UtcNow; return(PuzzleStateHelper.GetSparseQuery(context, eventObj, null, team) .Where(state => state.SolvedTime == null && state.UnlockedTime != null && state.Puzzle.MinutesOfEventLockout != 0 && state.UnlockedTime.Value + TimeSpan.FromMinutes(state.Puzzle.MinutesOfEventLockout) > now) .Select((s) => s.Puzzle)); }
public static async Task CheckForTimedUnlocksAsync( PuzzleServerContext context, Event eventObj, Team team) { DateTime expiry; lock (TimedUnlockExpiryCache) { // throttle this by an expiry interval before we do anything even remotely expensive if (TimedUnlockExpiryCache.TryGetValue(team.ID, out expiry) && expiry >= DateTime.UtcNow) { return; } } DateTime now = DateTime.UtcNow; // do the unlocks in a loop. // The loop will catch cascading unlocks, e.g. if someone does not hit the site between 11:59 and 12:31, catch up to the 12:30 unlocks immediately. while (true) { var puzzlesToSolveByTime = await PuzzleStateHelper.GetSparseQuery(context, eventObj, null, team) .Where(state => state.SolvedTime == null && state.UnlockedTime != null && state.Puzzle.MinutesToAutomaticallySolve != null && state.UnlockedTime.Value + TimeSpan.FromMinutes(state.Puzzle.MinutesToAutomaticallySolve.Value) <= now) .Select((state) => new { Puzzle = state.Puzzle, UnlockedTime = state.UnlockedTime.Value }) .ToListAsync(); foreach (var state in puzzlesToSolveByTime) { // mark solve time as when the puzzle was supposed to complete, so cascading unlocks work properly. await PuzzleStateHelper.SetSolveStateAsync(context, eventObj, state.Puzzle, team, state.UnlockedTime + TimeSpan.FromMinutes(state.Puzzle.MinutesToAutomaticallySolve.Value)); } // get out of the loop if we did nothing if (puzzlesToSolveByTime.Count == 0) { break; } } lock (TimedUnlockExpiryCache) { // effectively, expiry = Math.Max(DateTime.UtcNow, LastGlobalExpiry) + ClosestExpirySpacing - if you could use Math.Max on DateTime expiry = DateTime.UtcNow; if (expiry < LastGlobalExpiry) { expiry = LastGlobalExpiry; } expiry += ClosestExpirySpacing; TimedUnlockExpiryCache[team.ID] = expiry; LastGlobalExpiry = expiry; } }
/// <summary> /// Set the unlock state of some puzzle state records. In the course of setting the state, 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> /// <param name="value">The unlock time (null if relocking)</param> /// <returns>A task that can be awaited for the unlock/lock operation</returns> public static async Task SetUnlockStateAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, DateTime?value, PuzzleUser author = null) { IQueryable <PuzzleStatePerTeam> statesQ = await PuzzleStateHelper.GetFullReadWriteQueryAsync(context, eventObj, puzzle, team, author); List <PuzzleStatePerTeam> states = await statesQ.ToListAsync(); for (int i = 0; i < states.Count; i++) { states[i].UnlockedTime = value; } await context.SaveChangesAsync(); }
/// <summary> /// Set the solve state of some puzzle state records. In the course of setting the state, 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> /// <param name="value">The solve time (null if unsolving)</param> /// <param name="author"></param> /// <returns> /// A task that can be awaited for the solve/unsolve operation /// </returns> public static async Task SetSolveStateAsync( PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, DateTime?value, PuzzleUser author = null) { IQueryable <PuzzleStatePerTeam> statesQ = await PuzzleStateHelper .GetFullReadWriteQueryAsync(context, eventObj, puzzle, team, author); List <PuzzleStatePerTeam> states = await statesQ.ToListAsync(); for (int i = 0; i < states.Count; i++) { states[i].SolvedTime = value; } // Award hint coins if (value != null && puzzle != null && puzzle.HintCoinsForSolve != 0) { if (team != null) { team.HintCoinCount += puzzle.HintCoinsForSolve; } else { var allTeams = from Team curTeam in context.Teams where curTeam.Event == eventObj select curTeam; foreach (Team curTeam in allTeams) { curTeam.HintCoinCount += puzzle.HintCoinsForSolve; } } } await context.SaveChangesAsync(); // if this puzzle got solved, look for others to unlock if (value != null) { await UnlockAnyPuzzlesThatThisSolveUnlockedAsync(context, eventObj, puzzle, team, value.Value); } }
/// <summary> /// Set the unlock state of some puzzle state records. In the course of setting the state, 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> /// <param name="value">The unlock time (null if relocking)</param> /// <returns>A task that can be awaited for the unlock/lock operation</returns> public static async Task SetUnlockStateAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, DateTime?value, PuzzleUser author = null) { IQueryable <PuzzleStatePerTeam> statesQ = PuzzleStateHelper.GetFullReadWriteQuery(context, eventObj, puzzle, team, author); List <PuzzleStatePerTeam> states = await statesQ.ToListAsync(); for (int i = 0; i < states.Count; i++) { // Only allow unlock time to be modified if we were relocking it (setting it to null) or unlocking it for the first time if (value == null || states[i].UnlockedTime == null) { states[i].UnlockedTime = value; } } await context.SaveChangesAsync(); }
public static async Task CheckForTimedUnlocksAsync( PuzzleServerContext context, Event eventObj, Team team) { DateTime expiry; lock (TimedUnlockExpiryCache) { // throttle this by an expiry interval before we do anything even remotely expensive if (TimedUnlockExpiryCache.TryGetValue(team.ID, out expiry) && expiry >= DateTime.UtcNow) { return; } } DateTime now = DateTime.UtcNow; var puzzlesToSolveByTime = await PuzzleStateHelper.GetSparseQuery(context, eventObj, null, team) .Where(state => state.SolvedTime == null && state.UnlockedTime != null && state.Puzzle.MinutesToAutomaticallySolve != null && state.UnlockedTime.Value + TimeSpan.FromMinutes(state.Puzzle.MinutesToAutomaticallySolve.Value) <= now) .Select(state => state.Puzzle) .ToListAsync(); foreach (Puzzle puzzle in puzzlesToSolveByTime) { await PuzzleStateHelper.SetSolveStateAsync(context, eventObj, puzzle, team, DateTime.UtcNow); } lock (TimedUnlockExpiryCache) { // effectively, expiry = Math.Max(DateTime.UtcNow, LastGlobalExpiry) + ClosestExpirySpacing - if you could use Math.Max on DateTime expiry = DateTime.UtcNow; if (expiry < LastGlobalExpiry) { expiry = LastGlobalExpiry; } expiry += ClosestExpirySpacing; TimedUnlockExpiryCache[team.ID] = expiry; LastGlobalExpiry = expiry; } }
/// <summary> /// Set the email only mode of some puzzle state records. In the course /// of setting the state, 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> /// <param name="value">The new email only state for the puzzle</param> /// <param name="author"></param> /// <returns> /// A task that can be awaited for the lockout operation /// </returns> public static async Task SetEmailOnlyModeAsync( PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, bool value, PuzzleUser author = null) { IQueryable <PuzzleStatePerTeam> statesQ = await PuzzleStateHelper .GetFullReadWriteQueryAsync(context, eventObj, puzzle, team, author); List <PuzzleStatePerTeam> states = await statesQ.ToListAsync(); for (int i = 0; i < states.Count; i++) { states[i].IsEmailOnlyMode = value; if (value == true) { states[i].WrongSubmissionCountBuffer += 50; } } await context.SaveChangesAsync(); }
/// <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(); }
/// <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(); }