From 33097812d513861199df804944525a02c510e1f5 Mon Sep 17 00:00:00 2001 From: Kenny Young Date: Sat, 20 Oct 2018 20:08:15 -0700 Subject: [PATCH 1/4] Prerequisite Editing Allows prerequisites to be edited on the puzzle's Edit page. I've chosen to put them here rather than on their own page, which would have been a bit easier. This is the page they were edited on in the old site, and it was helpful to see the chosen prerequisites next to the min count to make sure that everything lined up. --- Data/DataModel/Prerequisites.cs | 10 +++ ServerCore/Pages/Puzzles/Edit.cshtml | 68 +++++++++++++++++++ ServerCore/Pages/Puzzles/Edit.cshtml.cs | 90 ++++++++++++++++++++++++- 3 files changed, 167 insertions(+), 1 deletion(-) diff --git a/Data/DataModel/Prerequisites.cs b/Data/DataModel/Prerequisites.cs index 3baf8f4b..7cb516e0 100644 --- a/Data/DataModel/Prerequisites.cs +++ b/Data/DataModel/Prerequisites.cs @@ -12,11 +12,21 @@ public class Prerequisites [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ID { get; set; } + /// + /// The puzzle ID that depends on others. + /// + public int PuzzleID { get; set; } + /// /// The puzzle that depends on others. /// 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/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..53829192 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.ToListAsync(); + PotentialPrerequisites = await potentialPrerequitesQ.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.ToListAsync(); + PotentialPrerequisitesOf = await potentialPrerequitesOfQ.ToListAsync(); + return Page(); } @@ -60,9 +89,68 @@ public async Task OnPostAsync() return RedirectToPage("./Index"); } + public async Task OnPostAddPrerequisiteAsync() + { + if (!PuzzleExists(NewPrerequisiteID) || PrerequisiteExists(Puzzle.ID, NewPrerequisiteID)) + { + return NotFound(); + } + + _context.Prerequisites.Add(new Prerequisites() { PuzzleID = Puzzle.ID, PrerequisiteID = NewPrerequisiteID }); + await _context.SaveChangesAsync(); + + return RedirectToPage(); + } + + public async Task OnPostAddPrerequisiteOfAsync() + { + if (!PuzzleExists(NewPrerequisiteOfID) || PrerequisiteExists(NewPrerequisiteOfID, Puzzle.ID)) + { + return NotFound(); + } + + _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); + } } } From f1d33c70a75aaeb29a1fc340b08db6540fd1e40c Mon Sep 17 00:00:00 2001 From: Kenny Young Date: Sat, 20 Oct 2018 21:41:29 -0700 Subject: [PATCH 2/4] Consolidate all state setters into PuzzleStateHelper In preparation for prerequisite-based unlocking, force all setters of unlock/solved state do do their set operations via PuzzleStateHelper, so that we can ultimately use that to perform the prerequisite unlock. I also removed the IsUnlocked and IsSolved bools from PuzzleStatePerTeam to discourage others from using these, since they don't perform well in queries. Finally, I fixed all compiler messages in files I touched - all just cosmetic stuff. --- Data/DataModel/PuzzleStatePerTeam.cs | 32 -------- .../ModelBases/PuzzleStatePerTeamPageModel.cs | 32 +++----- ServerCore/Pages/Events/Map.cshtml.cs | 74 +++++++++++-------- ServerCore/Pages/Submissions/Index.cshtml.cs | 7 +- ServerCore/Pages/Teams/Play.cshtml | 2 +- ServerCore/PuzzleStateHelper.cs | 74 ++++++++++++++++--- 6 files changed, 118 insertions(+), 103 deletions(-) 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/Submissions/Index.cshtml.cs b/ServerCore/Pages/Submissions/Index.cshtml.cs index c5749a54..a9f1915d 100644 --- a/ServerCore/Pages/Submissions/Index.cshtml.cs +++ b/ServerCore/Pages/Submissions/Index.cshtml.cs @@ -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..872b95a1 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,54 @@ 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; + } + + if (value != null) + { + // TODO: Unlock puzzles here when prerequisites are solved! + } + + await context.SaveChangesAsync(); + } + /// /// 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 +184,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 +206,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 +220,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) { From 47180ce02644d8abfc957d64a230c47942aeea0b Mon Sep 17 00:00:00 2001 From: Kenny Young Date: Sun, 21 Oct 2018 20:41:34 -0700 Subject: [PATCH 3/4] Unlock puzzles when enough prerequisites are solved! --- ServerCore/Pages/Submissions/Index.cshtml.cs | 2 +- ServerCore/PuzzleStateHelper.cs | 97 +++++++++++++++++++- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/ServerCore/Pages/Submissions/Index.cshtml.cs b/ServerCore/Pages/Submissions/Index.cshtml.cs index a9f1915d..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(); diff --git a/ServerCore/PuzzleStateHelper.cs b/ServerCore/PuzzleStateHelper.cs index 872b95a1..0d346884 100644 --- a/ServerCore/PuzzleStateHelper.cs +++ b/ServerCore/PuzzleStateHelper.cs @@ -168,12 +168,12 @@ public static async Task SetSolveStateAsync(PuzzleServerContext context, Event e states[i].SolvedTime = value; } + await context.SaveChangesAsync(); + if (value != null) { - // TODO: Unlock puzzles here when prerequisites are solved! + await UnlockIfPrequisitesMetAsync(context, eventObj, puzzle, team, value.Value); } - - await context.SaveChangesAsync(); } /// @@ -242,5 +242,96 @@ private static async Task> GetFullReadWriteQueryA // 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 UnlockIfPrequisitesMetAsync(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(); + } } } From 056670e42c9713b68370a6dac0d7d9e99b3f5527 Mon Sep 17 00:00:00 2001 From: Kenny Young Date: Wed, 24 Oct 2018 20:20:59 -0700 Subject: [PATCH 4/4] PR comments and a little sorting --- Data/DataModel/Prerequisites.cs | 4 ++-- ServerCore/Pages/Puzzles/Edit.cshtml.cs | 26 +++++++++++++++---------- ServerCore/PuzzleStateHelper.cs | 5 +++-- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Data/DataModel/Prerequisites.cs b/Data/DataModel/Prerequisites.cs index 7cb516e0..e86f8dfd 100644 --- a/Data/DataModel/Prerequisites.cs +++ b/Data/DataModel/Prerequisites.cs @@ -13,12 +13,12 @@ public class Prerequisites public int ID { get; set; } /// - /// The puzzle ID 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. + /// The puzzle that depends on others to be solved before it will be unlocked. /// public virtual Puzzle Puzzle { get; set; } diff --git a/ServerCore/Pages/Puzzles/Edit.cshtml.cs b/ServerCore/Pages/Puzzles/Edit.cshtml.cs index 53829192..683a25ce 100644 --- a/ServerCore/Pages/Puzzles/Edit.cshtml.cs +++ b/ServerCore/Pages/Puzzles/Edit.cshtml.cs @@ -49,14 +49,14 @@ public async Task OnGetAsync(int id) 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.ToListAsync(); - PotentialPrerequisites = await potentialPrerequitesQ.ToListAsync(); + 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.ToListAsync(); - PotentialPrerequisitesOf = await potentialPrerequitesOfQ.ToListAsync(); + CurrentPrerequisitesOf = await currentPrerequisitesOfQ.OrderBy(p => p.Name).ToListAsync(); + PotentialPrerequisitesOf = await potentialPrerequitesOfQ.OrderBy(p => p.Name).ToListAsync(); return Page(); } @@ -91,26 +91,32 @@ public async Task OnPostAsync() public async Task OnPostAddPrerequisiteAsync() { - if (!PuzzleExists(NewPrerequisiteID) || PrerequisiteExists(Puzzle.ID, NewPrerequisiteID)) + if (!PuzzleExists(NewPrerequisiteID)) { return NotFound(); } - _context.Prerequisites.Add(new Prerequisites() { PuzzleID = Puzzle.ID, PrerequisiteID = NewPrerequisiteID }); - await _context.SaveChangesAsync(); + 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) || PrerequisiteExists(NewPrerequisiteOfID, Puzzle.ID)) + if (!PuzzleExists(NewPrerequisiteOfID)) { return NotFound(); } - _context.Prerequisites.Add(new Prerequisites() { PuzzleID = NewPrerequisiteOfID, PrerequisiteID = Puzzle.ID }); - await _context.SaveChangesAsync(); + if (!PrerequisiteExists(NewPrerequisiteOfID, Puzzle.ID)) + { + _context.Prerequisites.Add(new Prerequisites() { PuzzleID = NewPrerequisiteOfID, PrerequisiteID = Puzzle.ID }); + await _context.SaveChangesAsync(); + } return RedirectToPage(); } diff --git a/ServerCore/PuzzleStateHelper.cs b/ServerCore/PuzzleStateHelper.cs index 0d346884..0b7b73d1 100644 --- a/ServerCore/PuzzleStateHelper.cs +++ b/ServerCore/PuzzleStateHelper.cs @@ -170,9 +170,10 @@ public static async Task SetSolveStateAsync(PuzzleServerContext context, Event e await context.SaveChangesAsync(); + // if this puzzle got solved, look for others to unlock if (value != null) { - await UnlockIfPrequisitesMetAsync(context, eventObj, puzzle, team, value.Value); + await UnlockAnyPuzzlesThatThisSolveUnlockedAsync(context, eventObj, puzzle, team, value.Value); } } @@ -252,7 +253,7 @@ private static async Task> GetFullReadWriteQueryA /// 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 UnlockIfPrequisitesMetAsync(PuzzleServerContext context, Event eventObj, Puzzle puzzleJustSolved, Team team, DateTime unlockTime) + 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);