// Go through list of puzzles and compute the max number of hints addded to any particular puzzle. // Used to write out the CSV file with enough commas. private int computeMaxHints() { int max = 0; foreach (var kpr in puzzles) { PuzzleInfo pi = kpr.Value; int n = pi.responses.Count; if (n > max) { max = n; } } return(max); }
/// <summary> /// This is used for debugging purposes - be sure NOT to write out the decrypted /// data file in the production version of the app! The directory is the same as where the /// data file was loaded, and the name is hardcoded. /// </summary> /// <param name="encrypted"></param> public void writeCsvFile(String basePath, Boolean encrypted) { const string ENCRYPTED_PROPERTY = "encrypted"; String fileName = "data-out-FREETEXT.csv"; if (encrypted) { fileName = "data-out-ENCRYPTED.csv"; } else { Trace.WriteLine("WARNING: Writing UNENCRYPTED data to file!"); } String pathName = basePath + @"\" + fileName; using (TextWriter tw = new StreamWriter(pathName)) { // Write header properties tw.Write("POD,Version:1.0"); foreach (KeyValuePair <String, String> kvp in properties) { String k = kvp.Key; if (!k.Equals("version") && !k.Equals("pod")) { if (!k.Equals(ENCRYPTED_PROPERTY)) // we selectively add it later { String s = (kvp.Value.Length == 0) ? "" : ":" + kvp.Value; appendCell(tw, k + s); } } } int nProps = properties.Count; // Add the "encrypted" property if we need to... if (encrypted) { appendCell(tw, ENCRYPTED_PROPERTY); nProps++; } int maxHints = computeMaxHints(); int maxCols = maxHints + 3; // 3 for ID, Answer and Name if (maxCols < nProps) { maxCols = nProps; } else { writeCommas(tw, maxCols - nProps); } tw.WriteLine(""); // Write Table headers tw.Write("Id,Name,Answer"); for (int i = 0; i < maxHints; i++) { tw.Write(",Hint" + (i + 1)); } int colsWritten = maxHints + 3; // 3 for ID, Name and Answer if (colsWritten < maxCols) { writeCommas(tw, maxCols - colsWritten); } tw.WriteLine(""); // Write Puzzle rows foreach (String id in puzzleIDs) { PuzzleInfo pi = puzzles[id]; // Write ID and Name tw.Write(id); appendCell(tw, pi.puzzleName); // Write responses... (Answer comes first foreach (var pr in pi.responses) { String s = pr.pattern + (pr.response.Length == 0 ? "" : ":" + compressAliases(pr.response)); if (encrypted) { s = "[" + endecrypt(id, s, true) + "]"; // true == encrypt } appendCell(tw, s); } colsWritten = 2 + pi.responses.Count; // 2 for ID and Name if (colsWritten < maxCols) { writeCommas(tw, maxCols - colsWritten); } tw.WriteLine(""); } tw.Flush(); tw.Close(); } }
/// <summary> /// Adds all puzzles in the specified spreadsheet. Individual puzzles may not /// be added for a variety of reasons including: /// - Duplicate Puzzle ID (only first one gets added) /// - Empty answer field. /// - Invalid REGEX field in answer or hints. /// Errors are reported as Trace output. /// TODO: Have a systematic way to report skipped puzzles. /// </summary> /// <param name="sr"></param> private void addPuzzles(SimpleSpreadsheetReader sr) { // We expect that the first row contains the signature ("POD") followed by "version:1.0" followed by // additional properties (which we ignore for now) const int HEADER_ROWS = 2; const int MIN_COLS = 3; const String FILE_SIGNATURE = "POD"; const String PROP_ENCRYPTED = "encrypted"; int sheet = 0; bool encrypted = false; // whether answers and hints are encrypted or not. int numRows = sr.getNumRows(sheet); int numCols = sr.getNumCols(sheet); if (numRows < HEADER_ROWS || numCols < MIN_COLS) { ErrorReport.logError("Puzzle data spreadsheet is too small!"); throw new ArgumentException(); } String[] propertyRow = sr.getRowCells(0, 0, numCols - 1, sheet); String[] header = sr.getRowCells(1, 0, numCols - 1, sheet); // We expect the first property cell to be POD (all caps) if (!Utils.stripEndBlanks(propertyRow[0]).Equals(FILE_SIGNATURE)) { ErrorReport.logError("Puzzle data spreadsheet has an invalid/missing signature."); throw new ArgumentException(); } // Read rest of properties readProperties(propertyRow); // Check if answer keys are encrypted. if (properties.ContainsKey(PROP_ENCRYPTED)) { encrypted = true; } this.sourceIsEncrypted = encrypted; // We expect the first header cell to be "id" String headerId = Utils.stripEndBlanks(header[0]); if (!headerId.Equals(SPREADSHEET_LABEL_ID, StringComparison.CurrentCultureIgnoreCase)) { ErrorReport.logError("Puzzle data spreadsheet does not have the ID field."); throw new ArgumentException(); } int startRow = HEADER_ROWS; // First row of puzzle data int startCol = 0; // First col of puzzle data //int puzzleCount = numRows - HEADER_ROWS; // could be zero; it's valid to have 0 puzzles. for (int i = startRow; i < numRows; i++) { String[] sRow = sr.getRowCells(i, startCol, numCols - 1, sheet); const String REGEX_ID = @"^[0-9][0-9][0-9]$"; // For now, IDs must be 3-digit numbers. String id = Utils.stripBlanks(sRow[0]); if (!Regex.IsMatch(id, REGEX_ID)) { Trace.WriteLine(String.Format("Skipping row {0}: invalid ID", i)); continue; } // We got the ID, if needed decrypt remaining fields after Name if (encrypted) { decryptCells(id, sRow, 2, numCols - 1); // 2 == skip Id and Name. False == descrypt } // Now let's get the remaining columns. First, get the first two: Name and Answer. String name = Utils.stripEndBlanks(sRow[1]); String answer = Utils.stripEndBlanks(sRow[2]); // Neither should be blank. if (name.Equals("") || answer.Equals("")) { Trace.WriteLine(String.Format("Skipping row {0}: blank Name or Answer", i)); continue; } PuzzleResponse pAnswerResponse = buildPuzzleResponse(answer, PuzzleResponse.ResponseType.Correct); if (pAnswerResponse == null) { Trace.WriteLine(String.Format("Skipping row {0}: Invalid Answer", i)); continue; } PuzzleInfo pi = new PuzzleInfo(id, name); pi.addResponse(pAnswerResponse); // Add hints, if any... for (int j = 3; j < sRow.Length; j++) { String field = Utils.stripEndBlanks(sRow[j]); if (field.Length > 0) { PuzzleResponse pr = buildPuzzleResponse(field, PuzzleResponse.ResponseType.Incorrect); if (pr == null) { Trace.WriteLine(String.Format("Ignoring hint {0} on row {1}: Invalid hint content", j, i)); continue; } pi.addResponse(pr); } } try { puzzles.Add(id, pi); puzzleIDs.Add(id); Trace.WriteLine(String.Format("Adding row {0}: Puzzle ID {1}, Answer {2}", i, id, answer)); } catch (ArgumentException) { Trace.WriteLine(String.Format("Ignoring row {0}: Duplicate ID {1}", i, id)); } } }
/// <summary> /// Constructs a response for the user-generated solution. puzzleId /// MUST reference a valid puzzle else a KeyNotFound exception is /// thrown (call tryGetName first if not sure). /// </summary> /// <param name="puzzleId"></param> /// <param name="solution"></param> /// <returns></returns> public PuzzleResponse checkSolution(string puzzleId, string solution) { // Lookup puzzle... solution = normalizeSolution(solution); PuzzleInfo pi = puzzles[puzzleId]; PuzzleResponse pr = null; // Check blacklist state (<0 means we're ok) int delay = pi.blacklister.submitDelay; // Have we solved this before? Boolean alreadySolved = pi.puzzleSolved; // If we are not already solved, and we are blacklisted, we return a special "try later" message. if (!alreadySolved && delay > 0) { String sResponse; // Blacklisted! Boolean permanentlyBlacklisted = delay == Blacklister.BLACKLIST_FOREVER_TIME; if (permanentlyBlacklisted) { sResponse = PERMANENT_BLACKLISTED_RESPONSE; } else { String sDelay = delay + " seconds"; if (delay > 60) { int minutes = delay / 60; int seconds = delay % 60; sDelay = minutes + " minute" + ((minutes == 1) ? "" : "s"); if (seconds > 0) { sDelay += " and " + seconds + " seconds"; } } sResponse = String.Format(BLACKLISTED_RESPONSE, sDelay, pi.puzzleId); } pr = new PuzzleResponse(solution, PuzzleResponse.ResponseType.AskLater, sResponse); return(pr); // ***************** EARLY RETURN ******************* } pr = pi.matchResponse(solution); if (pr == null) { pr = new PuzzleResponse(solution, PuzzleResponse.ResponseType.NotFound, GENERIC_INCORRECT_RESPONSE); } pi.blacklister.registerSubmission(); // If already solved, but solution is not correct, we put a special message. if (!alreadySolved) { pi.puzzleSolved = (pr.type == PuzzleResponse.ResponseType.Correct); } else if (pr.type != PuzzleResponse.ResponseType.Correct) { // Puzzle has been solved before but there is a new, incorrect submission. We give a helpful message to the user. pr = new PuzzleResponse(solution, pr.type, INCORRECT_BUT_PUZZLE_ANSWERED_BEFORE); } return(pr); }