diff --git a/Data/DataModel/Prerequisites.cs b/Data/DataModel/Prerequisites.cs index 3baf8f4b..e86f8dfd 100644 --- a/Data/DataModel/Prerequisites.cs +++ b/Data/DataModel/Prerequisites.cs @@ -13,10 +13,20 @@ public class Prerequisites public int ID { get; set; } /// - /// The puzzle that depends on others. + /// The puzzle ID that depends on others to be solved before it will be unlocked. + /// + public int PuzzleID { get; set; } + + /// + /// The puzzle that depends on others to be solved before it will be unlocked. /// public virtual Puzzle Puzzle { get; set; } + /// + /// A potential prerequisite ID of the puzzle named in Puzzle, which may need to be solved before Puzzle will be unlocked. + /// + public int PrerequisiteID { get; set; } + /// /// A potential prerequisite of the puzzle named in Puzzle, which may need to be solved before Puzzle will be unlocked. /// diff --git a/Data/DataModel/PuzzleStatePerTeam.cs b/Data/DataModel/PuzzleStatePerTeam.cs index b93c09e8..b6c63bbe 100644 --- a/Data/DataModel/PuzzleStatePerTeam.cs +++ b/Data/DataModel/PuzzleStatePerTeam.cs @@ -13,38 +13,6 @@ public class PuzzleStatePerTeam public int TeamID { get; set; } public virtual Team Team { get; set; } - /// - /// Whether or not the puzzle has been unlocked - /// - [NotMapped] - public bool IsUnlocked - { - get { return UnlockedTime != null; } - set - { - if (IsUnlocked != value) - { - UnlockedTime = value ? (DateTime?)DateTime.UtcNow : null; - } - } - } - - /// - /// Whether or not the puzzle has been solved - /// - [NotMapped] - public bool IsSolved - { - get { return SolvedTime != null; } - set - { - if (IsSolved != value) - { - SolvedTime = value ? (DateTime?)DateTime.UtcNow : null; - } - } - } - /// /// Whether or not the puzzle has been unlocked by this team, and if so when /// diff --git a/ServerCore/ModelBases/PuzzleStatePerTeamPageModel.cs b/ServerCore/ModelBases/PuzzleStatePerTeamPageModel.cs index 646fc083..b92945e6 100644 --- a/ServerCore/ModelBases/PuzzleStatePerTeamPageModel.cs +++ b/ServerCore/ModelBases/PuzzleStatePerTeamPageModel.cs @@ -15,7 +15,7 @@ public abstract class PuzzleStatePerTeamPageModel : EventSpecificPageModel public PuzzleStatePerTeamPageModel(PuzzleServerContext context) { - this.Context = context; + Context = context; } public IList PuzzleStatePerTeam { get; set; } @@ -24,10 +24,10 @@ public PuzzleStatePerTeamPageModel(PuzzleServerContext context) public async Task InitializeModelAsync(Puzzle puzzle, Team team, SortOrder? sort) { - IQueryable statesQ = PuzzleStateHelper.GetFullReadOnlyQuery(this.Context, this.Event, puzzle, team); - this.Sort = sort; + IQueryable statesQ = PuzzleStateHelper.GetFullReadOnlyQuery(Context, Event, puzzle, team); + Sort = sort; - switch(sort ?? this.DefaultSort) + switch(sort ?? DefaultSort) { case SortOrder.PuzzleAscending: statesQ = statesQ.OrderBy(state => state.Puzzle.Name); @@ -64,7 +64,7 @@ public async Task InitializeModelAsync(Puzzle puzzle, Team team, SortOrder? sort { SortOrder result = ascendingSort; - if (result == (this.Sort ?? DefaultSort)) + if (result == (Sort ?? DefaultSort)) { result = descendingSort; } @@ -77,28 +77,14 @@ public async Task InitializeModelAsync(Puzzle puzzle, Team team, SortOrder? sort return result; } - public async Task SetUnlockStateAsync(Puzzle puzzle, Team team, bool value) + public Task SetUnlockStateAsync(Puzzle puzzle, Team team, bool value) { - var statesQ = await PuzzleStateHelper.GetFullReadWriteQueryAsync(this.Context, this.Event, puzzle, team); - var states = await statesQ.ToListAsync(); - - for (int i = 0; i < states.Count; i++) - { - states[i].IsUnlocked = value; - } - await Context.SaveChangesAsync(); + return PuzzleStateHelper.SetUnlockStateAsync(Context, Event, puzzle, team, value ? (DateTime?)DateTime.UtcNow : null); } - public async Task SetSolveStateAsync(Puzzle puzzle, Team team, bool value) + public Task SetSolveStateAsync(Puzzle puzzle, Team team, bool value) { - var statesQ = await PuzzleStateHelper.GetFullReadWriteQueryAsync(this.Context, this.Event, puzzle, team); - var states = await statesQ.ToListAsync(); - - for (int i = 0; i < states.Count; i++) - { - states[i].IsSolved = value; - } - await Context.SaveChangesAsync(); + return PuzzleStateHelper.SetSolveStateAsync(Context, Event, puzzle, team, value ? (DateTime?)DateTime.UtcNow : null); } public enum SortOrder diff --git a/ServerCore/Pages/Events/Map.cshtml.cs b/ServerCore/Pages/Events/Map.cshtml.cs index faeb9624..400b8da7 100644 --- a/ServerCore/Pages/Events/Map.cshtml.cs +++ b/ServerCore/Pages/Events/Map.cshtml.cs @@ -27,20 +27,20 @@ public async Task OnGetAsync() { // get the puzzles and teams // TODO: Filter puzzles if an author; no need to filter teams. Revisit when authors exist. - var puzzles = await _context.Puzzles.Where(p => p.Event == this.Event).Select(p => new PuzzleStats() { Puzzle = p }).ToListAsync(); - var teams = await _context.Teams.Where(t => t.Event == this.Event).Select(t => new TeamStats() { Team = t }).ToListAsync(); + List puzzles = await _context.Puzzles.Where(p => p.Event == Event).Select(p => new PuzzleStats() { Puzzle = p }).ToListAsync(); + List teams = await _context.Teams.Where(t => t.Event == Event).Select(t => new TeamStats() { Team = t }).ToListAsync(); // build an ID-based lookup for puzzles and teams - var puzzleLookup = new Dictionary(); + Dictionary puzzleLookup = new Dictionary(); puzzles.ForEach(p => puzzleLookup[p.Puzzle.ID] = p); - var teamLookup = new Dictionary(); + Dictionary teamLookup = new Dictionary(); teams.ForEach(t => teamLookup[t.Team.ID] = t); // tabulate solve counts and team scores - var states = await PuzzleStateHelper.GetSparseQuery(_context, this.Event, null, null).ToListAsync(); - var stateList = new List(states.Count); - foreach (var state in states) + List states = await PuzzleStateHelper.GetSparseQuery(_context, Event, null, null).ToListAsync(); + List stateList = new List(states.Count); + foreach (PuzzleStatePerTeam state in states) { // TODO: Is it more performant to prefilter the states if an author, or is this sufficient? if (!puzzleLookup.TryGetValue(state.PuzzleID, out PuzzleStats puzzle) || !teamLookup.TryGetValue(state.TeamID, out TeamStats team)) @@ -50,7 +50,7 @@ public async Task OnGetAsync() stateList.Add(new StateStats() { Puzzle = puzzle, Team = team, UnlockedTime = state.UnlockedTime, SolvedTime = state.SolvedTime }); - if (state.IsSolved) + if (state.SolvedTime != null) { puzzle.SolveCount++; team.SolveCount++; @@ -86,53 +86,65 @@ public async Task OnGetAsync() var stateMap = new StateStats[puzzles.Count, teams.Count]; stateList.ForEach(state => stateMap[state.Puzzle.SortOrder, state.Team.SortOrder] = state); - this.Puzzles = puzzles; - this.Teams = teams; - this.StateMap = stateMap; + Puzzles = puzzles; + Teams = teams; + StateMap = stateMap; } public class PuzzleStats { - public Puzzle Puzzle; - public int SolveCount; - public int SortOrder; + public Puzzle Puzzle { get; set; } + public int SolveCount { get; set; } + public int SortOrder { get; set; } } public class TeamStats { - public Team Team; - public int SolveCount; - public int Score; - public int SortOrder; - public int? Rank; - public DateTime FinalMetaSolveTime = DateTime.MaxValue; + public Team Team { get; set; } + public int SolveCount { get; set; } + public int Score { get; set; } + public int SortOrder { get; set; } + public int? Rank { get; set; } + public DateTime FinalMetaSolveTime { get; set; } = DateTime.MaxValue; } public class StateStats { - public static StateStats Default = new StateStats(); + public static StateStats Default { get; } = new StateStats(); - public PuzzleStats Puzzle; - public TeamStats Team; - public DateTime? UnlockedTime; - public DateTime? SolvedTime; + public PuzzleStats Puzzle { get; set; } + public TeamStats Team { get; set; } + public DateTime? UnlockedTime { get; set; } + public DateTime? SolvedTime { get; set; } - public string DisplayText => this.SolvedTime != null ? "C" : this.UnlockedTime != null ? "U" : "L"; + public string DisplayText + { + get + { + return SolvedTime != null ? "C" : UnlockedTime != null ? "U" : "L"; + } + } - public int DisplayHue => this.SolvedTime != null ? 120 : this.UnlockedTime != null ? 60 : 0; + public int DisplayHue + { + get + { + return SolvedTime != null ? 120 : UnlockedTime != null ? 60 : 0; + } + } public int DisplayLightness { get { - if (this.SolvedTime != null) + if (SolvedTime != null) { - int minutes = (int)((DateTime.UtcNow - this.SolvedTime.Value).TotalMinutes); + int minutes = (int)((DateTime.UtcNow - SolvedTime.Value).TotalMinutes); return 75 - (Math.Min(minutes, 236) >> 2); } - else if (this.UnlockedTime != null) + else if (UnlockedTime != null) { - int minutes = (int)((DateTime.UtcNow - this.UnlockedTime.Value).TotalMinutes); + int minutes = (int)((DateTime.UtcNow - UnlockedTime.Value).TotalMinutes); return 75 - (Math.Min(minutes, 236) >> 2); } else diff --git a/ServerCore/Pages/Puzzles/Edit.cshtml b/ServerCore/Pages/Puzzles/Edit.cshtml index 3ef87427..03ec83d2 100644 --- a/ServerCore/Pages/Puzzles/Edit.cshtml +++ b/ServerCore/Pages/Puzzles/Edit.cshtml @@ -9,6 +9,7 @@

Puzzle


+

Properties

@@ -76,6 +77,73 @@
+
+

Prerequisites

+

These puzzles help to unlock me:

+
+ + + + + + + + + @foreach (var pre in Model.CurrentPrerequisites) + { + + + + + } + + + + + +
PrerequisiteAction
+ @(pre.Name) + + Remove +
+ + + +
+
+
+

I help to unlock these puzzles:

+
+ + + + + + + + + @foreach (var p in Model.CurrentPrerequisitesOf) + { + + + + + } + + + + + +
PuzzleAction
+ @(p.Name) + + Remove +
+ + + +
+
diff --git a/ServerCore/Pages/Puzzles/Edit.cshtml.cs b/ServerCore/Pages/Puzzles/Edit.cshtml.cs index b220a8e1..683a25ce 100644 --- a/ServerCore/Pages/Puzzles/Edit.cshtml.cs +++ b/ServerCore/Pages/Puzzles/Edit.cshtml.cs @@ -1,8 +1,10 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using ServerCore.DataModel; using ServerCore.ModelBases; @@ -21,6 +23,20 @@ public EditModel(PuzzleServerContext context) [BindProperty] public Puzzle Puzzle { get; set; } + [BindProperty] + public int NewPrerequisiteID { get; set; } + + [BindProperty] + public int NewPrerequisiteOfID { get; set; } + + public List PotentialPrerequisites { get; set; } + + public List CurrentPrerequisites { get; set; } + + public List PotentialPrerequisitesOf { get; set; } + + public List CurrentPrerequisitesOf { get; set; } + public async Task OnGetAsync(int id) { Puzzle = await _context.Puzzles.Where(m => m.ID == id).FirstOrDefaultAsync(); @@ -29,6 +45,19 @@ public async Task OnGetAsync(int id) { return NotFound(); } + + IQueryable currentPrerequisitesQ = _context.Prerequisites.Where(m => m.Puzzle == Puzzle).Select(m => m.Prerequisite); + IQueryable potentialPrerequitesQ = _context.Puzzles.Where(m => m.Event == Event && m != Puzzle).Except(currentPrerequisitesQ); + + CurrentPrerequisites = await currentPrerequisitesQ.OrderBy(p => p.Name).ToListAsync(); + PotentialPrerequisites = await potentialPrerequitesQ.OrderBy(p => p.Name).ToListAsync(); + + IQueryable currentPrerequisitesOfQ = _context.Prerequisites.Where(m => m.Prerequisite == Puzzle).Select(m => m.Puzzle); + IQueryable potentialPrerequitesOfQ = _context.Puzzles.Where(m => m.Event == Event && m != Puzzle).Except(currentPrerequisitesOfQ); + + CurrentPrerequisitesOf = await currentPrerequisitesOfQ.OrderBy(p => p.Name).ToListAsync(); + PotentialPrerequisitesOf = await potentialPrerequitesOfQ.OrderBy(p => p.Name).ToListAsync(); + return Page(); } @@ -60,9 +89,74 @@ public async Task OnPostAsync() return RedirectToPage("./Index"); } + public async Task OnPostAddPrerequisiteAsync() + { + if (!PuzzleExists(NewPrerequisiteID)) + { + return NotFound(); + } + + if (!PrerequisiteExists(Puzzle.ID, NewPrerequisiteID)) + { + _context.Prerequisites.Add(new Prerequisites() { PuzzleID = Puzzle.ID, PrerequisiteID = NewPrerequisiteID }); + await _context.SaveChangesAsync(); + } + + return RedirectToPage(); + } + + public async Task OnPostAddPrerequisiteOfAsync() + { + if (!PuzzleExists(NewPrerequisiteOfID)) + { + return NotFound(); + } + + if (!PrerequisiteExists(NewPrerequisiteOfID, Puzzle.ID)) + { + _context.Prerequisites.Add(new Prerequisites() { PuzzleID = NewPrerequisiteOfID, PrerequisiteID = Puzzle.ID }); + await _context.SaveChangesAsync(); + } + + return RedirectToPage(); + } + + public async Task OnGetRemovePrerequisiteAsync(int id, int prerequisite) + { + Prerequisites toRemove = await _context.Prerequisites.Where(m => m.PuzzleID == id && m.PrerequisiteID == prerequisite).FirstOrDefaultAsync(); + + if (toRemove != null) + { + _context.Prerequisites.Remove(toRemove); + await _context.SaveChangesAsync(); + } + + // redirect without the prerequisite info to keep the URL clean + return RedirectToPage(new { id }); + } + + public async Task OnGetRemovePrerequisiteOfAsync(int id, int prerequisiteOf) + { + Prerequisites toRemove = await _context.Prerequisites.Where(m => m.PuzzleID == prerequisiteOf && m.PrerequisiteID == id).FirstOrDefaultAsync(); + + if (toRemove != null) + { + _context.Prerequisites.Remove(toRemove); + await _context.SaveChangesAsync(); + } + + // redirect without the prerequisite info to keep the URL clean + return RedirectToPage(new { id }); + } + private bool PuzzleExists(int id) { return _context.Puzzles.Any(e => e.ID == id); } + + private bool PrerequisiteExists(int puzzleId, int prerequisiteId) + { + return _context.Prerequisites.Any(pr => pr.Puzzle.ID == puzzleId && pr.Prerequisite.ID == prerequisiteId); + } } } diff --git a/ServerCore/Pages/Submissions/Index.cshtml.cs b/ServerCore/Pages/Submissions/Index.cshtml.cs index c5749a54..ce6142d3 100644 --- a/ServerCore/Pages/Submissions/Index.cshtml.cs +++ b/ServerCore/Pages/Submissions/Index.cshtml.cs @@ -37,7 +37,7 @@ public async Task OnPostAsync(int puzzleId, int teamId) } // Create submission and add it to list - Submission.TimeSubmitted = DateTime.Now; + Submission.TimeSubmitted = DateTime.UtcNow; Submission.Puzzle = await _context.Puzzles.SingleOrDefaultAsync(p => p.ID == puzzleId); Submission.Team = await _context.Teams.Where((t) => t.ID == teamId).FirstOrDefaultAsync(); @@ -47,10 +47,7 @@ public async Task 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(); + } } }