OnPostAsync(int puzzleId, int teamId)
// Update puzzle state if submission was correct
if (Submission.Response != null && Submission.Response.IsSolution)
{
- var statesQ = await PuzzleStateHelper.GetFullReadWriteQueryAsync(_context, this.Event, Submission.Puzzle, Submission.Team);
- PuzzleStatePerTeam puzzleState = await statesQ.FirstOrDefaultAsync();
- puzzleState.IsSolved = true;
- Submission.TimeSubmitted = (DateTime)puzzleState.SolvedTime;
+ await PuzzleStateHelper.SetSolveStateAsync(_context, Event, Submission.Puzzle, Submission.Team, Submission.TimeSubmitted);
}
_context.Submissions.Add(Submission);
@@ -65,7 +62,7 @@ public async Task OnGetAsync(int puzzleId, int teamId)
PuzzleId = puzzleId;
TeamId = teamId;
- Submission correctSubmission = this.Submissions?.Where((s) => s.Response != null && s.Response.IsSolution).FirstOrDefault();
+ Submission correctSubmission = Submissions?.Where((s) => s.Response != null && s.Response.IsSolution).FirstOrDefault();
if (correctSubmission != null)
{
AnswerToken = correctSubmission.SubmissionText;
diff --git a/ServerCore/Pages/Teams/Play.cshtml b/ServerCore/Pages/Teams/Play.cshtml
index 09b248d6..d799ad6a 100644
--- a/ServerCore/Pages/Teams/Play.cshtml
+++ b/ServerCore/Pages/Teams/Play.cshtml
@@ -63,7 +63,7 @@
}
- @if (item.State.IsSolved)
+ @if (item.State.SolvedTime != null)
{
Solved at @item.State.SolvedTime
}
diff --git a/ServerCore/PuzzleStateHelper.cs b/ServerCore/PuzzleStateHelper.cs
index 1b0eaee4..0b7b73d1 100644
--- a/ServerCore/PuzzleStateHelper.cs
+++ b/ServerCore/PuzzleStateHelper.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
@@ -41,10 +42,12 @@ public static IQueryable GetFullReadOnlyQuery(PuzzleServerCo
});
}
+#pragma warning disable IDE0031 // despite the compiler message, "teamstate?.UnlockedTime", etc does not compile here
+
if (puzzle != null)
{
- var teams = context.Teams.Where(t => t.Event == eventObj);
- var states = context.PuzzleStatePerTeam.Where(state => state.Puzzle == puzzle);
+ IQueryable teams = context.Teams.Where(t => t.Event == eventObj);
+ IQueryable states = context.PuzzleStatePerTeam.Where(state => state.Puzzle == puzzle);
return from t in teams
join state in states on t.ID equals state.TeamID into tmp
@@ -64,8 +67,8 @@ from teamstate in tmp.DefaultIfEmpty()
if (team != null)
{
- var puzzles = context.Puzzles.Where(p => p.Event == eventObj);
- var states = context.PuzzleStatePerTeam.Where(state => state.Team == team);
+ IQueryable puzzles = context.Puzzles.Where(p => p.Event == eventObj);
+ IQueryable states = context.PuzzleStatePerTeam.Where(state => state.Team == team);
return from p in puzzles
join state in states on p.ID equals state.PuzzleID into tmp
@@ -82,6 +85,7 @@ from teamstate in tmp.DefaultIfEmpty()
Notes = teamstate == null ? null : teamstate.Notes
};
}
+#pragma warning restore IDE0031
throw new NotImplementedException("Full event query is NYI and may never be needed; use the sparse one");
}
@@ -124,6 +128,55 @@ public static IQueryable GetSparseQuery(PuzzleServerContext
return context.PuzzleStatePerTeam.Where(state => state.Puzzle.Event == eventObj);
}
+ ///
+ /// 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.
+ ///
+ /// The puzzle DB context
+ /// The event we are querying from
+ /// The puzzle; if null, get all puzzles in the event.
+ /// The team; if null, get all the teams in the event.
+ /// The unlock time (null if relocking)
+ /// A task that can be awaited for the unlock/lock operation
+ public static async Task SetUnlockStateAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, DateTime? value)
+ {
+ IQueryable statesQ = await PuzzleStateHelper.GetFullReadWriteQueryAsync(context, eventObj, puzzle, team);
+ List states = await statesQ.ToListAsync();
+
+ for (int i = 0; i < states.Count; i++)
+ {
+ states[i].UnlockedTime = value;
+ }
+ await context.SaveChangesAsync();
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The puzzle DB context
+ /// The event we are querying from
+ /// The puzzle; if null, get all puzzles in the event.
+ /// The team; if null, get all the teams in the event.
+ /// The solve time (null if unsolving)
+ /// A task that can be awaited for the solve/unsolve operation
+ public static async Task SetSolveStateAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team, DateTime? value)
+ {
+ IQueryable statesQ = await PuzzleStateHelper.GetFullReadWriteQueryAsync(context, eventObj, puzzle, team);
+ List states = await statesQ.ToListAsync();
+
+ for (int i = 0; i < states.Count; i++)
+ {
+ states[i].SolvedTime = value;
+ }
+
+ await context.SaveChangesAsync();
+
+ // if this puzzle got solved, look for others to unlock
+ if (value != null)
+ {
+ await UnlockAnyPuzzlesThatThisSolveUnlockedAsync(context, eventObj, puzzle, team, value.Value);
+ }
+ }
+
///
/// 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.
///
@@ -132,7 +185,7 @@ public static IQueryable GetSparseQuery(PuzzleServerContext
/// The puzzle; if null, get all puzzles in the event.
/// The team; if null, get all the teams in the event.
/// A query of PuzzleStatePerTeam objects that can be sorted and instantiated, but you can't edit the results.
- public static async Task> GetFullReadWriteQueryAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team)
+ private static async Task> GetFullReadWriteQueryAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzle, Team team)
{
if (context == null)
{
@@ -154,9 +207,9 @@ public static async Task> GetFullReadWriteQueryAs
}
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();
+ IQueryable teamIdsQ = context.Teams.Where(p => p.Event == eventObj).Select(p => p.ID);
+ IQueryable puzzleStateTeamIdsQ = context.PuzzleStatePerTeam.Where(s => s.Puzzle == puzzle).Select(s => s.TeamID);
+ List teamIdsWithoutState = await teamIdsQ.Except(puzzleStateTeamIdsQ).ToListAsync();
if (teamIdsWithoutState.Count > 0)
{
@@ -168,9 +221,9 @@ public static async Task> GetFullReadWriteQueryAs
}
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();
+ IQueryable puzzleIdsQ = context.Puzzles.Where(p => p.Event == eventObj).Select(p => p.ID);
+ IQueryable puzzleStatePuzzleIdsQ = context.PuzzleStatePerTeam.Where(s => s.Team == team).Select(s => s.PuzzleID);
+ List puzzleIdsWithoutState = await puzzleIdsQ.Except(puzzleStatePuzzleIdsQ).ToListAsync();
if (puzzleIdsWithoutState.Count > 0)
{
@@ -190,5 +243,96 @@ public static async Task> GetFullReadWriteQueryAs
// now this query is no longer sparse because we just filled it all out!
return GetSparseQuery(context, eventObj, puzzle, team);
}
+
+ ///
+ /// Unlock any puzzles that need to be unlocked due to the recent solve of a prerequisite.
+ ///
+ /// The puzzle DB context
+ /// The event we are working in
+ /// The puzzle just solved; if null, all the puzzles in the event (which will make more sense once we add per author filtering)
+ /// The team that just solved; if null, all the teams in the event.
+ /// The time that the puzzle should be marked as unlocked.
+ ///
+ 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 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 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 teamsToUpdate = team == null ? await context.Teams.Where(t => t.Event == eventObj).ToListAsync() : new List() { 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 unlockedPuzzleIDsForTeamT = new HashSet();
+ HashSet solvedPuzzleIDsForTeamT = new HashSet();
+
+ 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();
+ }
}
}
|