Downloads containing trivia.mut

Downloads
Name Author Game Mode Rating
TSF with JJ2+ Only: TriviaFeatured Download sAlAmAnDeR Mutator 9.3 Download file

File preview

  1. #pragma name "Trivia"
  2.  
  3. //Globals
  4. bool enableDebugLogging = false;
  5. bool showScoresEachQuestion = false;
  6.  
  7. Config@ config;
  8. StateManager@ stateManager;
  9. TriviaGame@ game;
  10. PersistentScoreSet@ overallScores;
  11. array<ChatMessage@> chatMessages;
  12.  
  13. //Constants
  14. const uint8 ASCII_TAB = 9;
  15. const uint8 ASCII_SPACE = 32;
  16. const uint8 ASCII_LINE_FEED = 10;
  17. const uint8 ASCII_CARRIAGE_RETURN = 13;
  18. const uint8 ASCII_UNDERSCORE = 95;
  19. const uint8 ASCII_PIPE = 124;
  20. const uint8 ASCII_SECTION = 167;
  21.  
  22. const uint8 ASCII_CONVERT_CASE = 32;
  23.  
  24. const string QUESTION_FILE = "trivia.asdat";
  25. const string SCORES_FILE = "trivia-scores.asdat";
  26. const string CONFIG_FILE = "trivia-config.asdat";
  27. const string CHAT_PREFIX = chr(ASCII_SECTION) + "1";
  28. const uint16 MESSAGE_PRINT_WAIT = 5*70;
  29. const uint16 BETWEEN_QUESTION_WAIT = 8*70;
  30. const uint16 QUESTION_TIME_NO_HINTS = 20*70;
  31. const uint16 TIME_PER_HINT = 10*70;
  32. const uint16 MAX_DISPLAY_CHARS = 53;
  33.  
  34. //Utility Functions
  35. bool isUpperCase(uint8 code) { return (code >= 65 && code <= 90); }
  36. bool isLowerCase(uint8 code) { return (code >= 97 && code <= 122); }
  37. bool isNumeric(uint8 code) {return (code >= 48 && code <= 57);}
  38. bool isAlphabetic(uint8 code) {return  isUpperCase(code) || isLowerCase(code);}
  39. bool isAlphanumeric(uint8 code) {return isAlphabetic(code) || isNumeric(code);}
  40.  
  41. //Returns a one-character string corresponding to an ASCII value.
  42. //http://www.jazz2online.com/snippets/68/operations-on-ascii-values/
  43. string chr(uint8 value){
  44.   string str="\0";
  45.   str[0]=value;
  46.   return str;
  47. }
  48.  
  49. //Returns the ASCII value of the first character of a string.
  50. uint8 ord(string &in str){
  51.   if(str.isEmpty()) {return 0;}
  52.   return str[0];
  53. }
  54.  
  55. string toLower(string input) {
  56.         string toLower = input;
  57.         for(uint i = 0; i < input.length; i++) {
  58.                 if(isUpperCase(toLower[i]))
  59.                         toLower[i] += ASCII_CONVERT_CASE;
  60.         }
  61.         return toLower;
  62. }
  63.  
  64. string toUpper(string input) {
  65.         string toUpper = input;
  66.         for(uint i = 0; i < input.length; i++) {
  67.                 if(isLowerCase(toUpper[i]))
  68.                         toUpper[i] -= ASCII_CONVERT_CASE;
  69.         }
  70.         return toUpper;
  71. }
  72.  
  73. bool startsWith(string data, string prefix) {
  74.         return data.substr(0,prefix.length) == prefix;
  75. }
  76.  
  77. bool endsWith(string data, string suffix) {
  78.         return data.substr(data.length-suffix.length,suffix.length) == suffix;
  79. }
  80.  
  81. array<string> split(string data, string delimeter) {
  82.         uint8 del = ord(delimeter);
  83.         array<string> result;
  84.         string curSubstring;
  85.         for(uint i = 0; i < data.length; i++) {
  86.                 if(data[i] == del) {
  87.                         result.insertLast(curSubstring);
  88.                         curSubstring = "";
  89.                 } else {
  90.                         curSubstring += chr(data[i]);
  91.                 }
  92.         }
  93.         result.insertLast(curSubstring);
  94.         return result;
  95. }
  96.  
  97. string trim(string data) {
  98.         int i = 0, j = data.length-1;
  99.         while(data[i] == ASCII_SPACE || data[i] == ASCII_TAB) i++;
  100.         while(data[j] == ASCII_SPACE || data[j] == ASCII_TAB) j--;
  101.         return data.substr(i, j-i+1);
  102. }
  103.  
  104. bool isAdmin(jjPLAYER@ player) {
  105.         return player.isAdmin || player.clientID == 0;
  106. }
  107.  
  108. int getNumPipes(string message) {
  109.         int count = 0;
  110.         for(uint i = 0; i < message.length(); i++) {
  111.                 if(message[i] == ASCII_PIPE)
  112.                         count++;
  113.         }
  114.         count = count % 8;
  115.         if(count == 6) count = 0;
  116.         return count;
  117. }
  118.  
  119. string makePipes(uint count) {
  120.         string res = "";
  121.         for(uint i = 0; i < count; i++)
  122.                 res += chr(ASCII_PIPE);
  123.         return res;
  124. }
  125.  
  126. void sendChat(string chatMessage, bool split = true)  {
  127.         if(!split) {
  128.                 jjChat(CHAT_PREFIX + chatMessage);
  129.                 return;
  130.         }
  131.         const uint MAX_SUBSTRING_LENGTH = MAX_DISPLAY_CHARS - CHAT_PREFIX.length();
  132.         uint start = 0, end = MAX_SUBSTRING_LENGTH > chatMessage.length() ? chatMessage.length() : MAX_SUBSTRING_LENGTH;
  133.         string text = chatMessage.substr(start, end-start+1);
  134.        
  135.         if(chatMessage[end-1] != ASCII_SPACE && end < chatMessage.length() && chatMessage[end] != ASCII_SPACE) {
  136.                 int lastSpace = text.findLast(" ");
  137.                 if(lastSpace != -1) {
  138.                         end -= (text.length() - lastSpace);
  139.                         text = chatMessage.substr(start, end-start+1);
  140.                 }
  141.         }
  142.  
  143.         string toSend = CHAT_PREFIX + trim(text);
  144.         jjConsole(toSend,true);
  145.         while(end+1 < chatMessage.length()) {
  146.                 int numPipes = getNumPipes(toSend);
  147.                 uint len = MAX_SUBSTRING_LENGTH - numPipes;
  148.                 start = end+1;
  149.                 end = start + len > chatMessage.length() ? chatMessage.length() : start + len;
  150.                 text = chatMessage.substr(start, end-start+1);
  151.                 if(end > 0 && end < chatMessage.length() && chatMessage[end] != ASCII_SPACE && end+1 < chatMessage.length() && chatMessage[end+1] != ASCII_SPACE) {
  152.                         int lastSpace = text.findLast(" ");
  153.                         if(lastSpace != -1) {
  154.                                 end -= (text.length() - lastSpace);
  155.                                 text = chatMessage.substr(start, end-start+1);
  156.                         }
  157.                 }
  158.                
  159.                 toSend = CHAT_PREFIX + makePipes(numPipes) + trim(text);
  160.                 jjConsole(toSend,true);
  161.         }
  162. }
  163.  
  164. //Classes
  165. class Config {
  166.         uint8 numHints;         //Answers will have UP TO this many hints
  167.         uint numRounds;             //The game will have UP TO this many rounds
  168.         uint questionsPerRound;  //The game will have UP TO this many questions per round
  169.        
  170.         Config() {
  171.                 load();
  172.         }
  173.        
  174.         void setDefaults() {
  175.                 numHints = 3;
  176.                 numRounds = 5;
  177.                 questionsPerRound = 5;
  178.         }
  179.        
  180.         void save() {
  181.                 jjSTREAM file(CONFIG_FILE);
  182.                 file.clear();
  183.                 file.push(numHints);
  184.                 file.push(numRounds);
  185.                 file.push(questionsPerRound);
  186.                 file.save(CONFIG_FILE);
  187.         }
  188.        
  189.         void load() {
  190.                 jjSTREAM file(CONFIG_FILE);
  191.                 if(file.isEmpty()) {
  192.                         jjConsole("Config file not found. Using default settings.");
  193.                         setDefaults();
  194.                         save();
  195.                         return;
  196.                 }
  197.                 file.pop(numHints);
  198.                 file.pop(numRounds);
  199.                 file.pop(questionsPerRound);
  200.         }
  201. }
  202.  
  203. class Hint {
  204.         private string hint;
  205.         private uint8 numBlanks = 0;
  206.        
  207.         Hint(string hint) {
  208.                 this.hint = hint;
  209.                 for(uint i = 0; i < hint.length; i++) {
  210.                         if(hint[i] == ASCII_UNDERSCORE)
  211.                                 numBlanks++;
  212.                 }
  213.         }
  214.        
  215.         Hint(string hint, int numBlanks) {
  216.                 this.hint = hint;
  217.                 this.numBlanks = numBlanks;
  218.         }
  219.        
  220.         uint8 getNumBlanks() {
  221.                 return numBlanks;
  222.         }
  223.        
  224.         string toString() const {
  225.                 return hint;
  226.         }
  227. }
  228.  
  229. class Answer {
  230.         private string answer;
  231.         private uint quizzableLength = 0;
  232.         private bool valid = true;
  233.        
  234.         Answer(string answer) {
  235.                 this.answer = answer;
  236.                 if(answer.length() > 57) {
  237.                         jjConsole("ERROR: answer \"" + answer + "\" cannot be typed in JJ2!");
  238.                         valid = false;
  239.                         return;
  240.                 }
  241.                 for(uint i = 0; i < answer.length; i++) {
  242.                         if(answer[i] == ASCII_UNDERSCORE) {
  243.                                 jjConsole("ERROR: answer \"" + answer + "\" contains underscores!");
  244.                                 valid = false;
  245.                                 return;
  246.                         }
  247.                 }
  248.                
  249.                 for(uint i = 0; i < answer.length(); i++) {
  250.                         if(isAlphanumeric(answer[i]))
  251.                                 quizzableLength++;
  252.                 }
  253.         }
  254.        
  255.         string toString() const {
  256.                 return answer;
  257.         }
  258.        
  259.         uint getQuizzableLength() const {
  260.                 return quizzableLength;
  261.         }
  262.        
  263.         int getLength() const {
  264.                 return answer.length();
  265.         }
  266.        
  267.         bool isValid() const {
  268.                 return valid;
  269.         }
  270.        
  271.         int opCmp(Answer@ other) const {
  272.                 return this.getQuizzableLength() - other.getQuizzableLength();
  273.         }
  274. }
  275.  
  276. class Question {
  277.         private string question;
  278.         private array<Answer@> answers;
  279.         private array<Hint@> hints;
  280.  
  281.         Question(string question, array<Answer@> answers) {
  282.                 this.question = question;
  283.                 this.answers = answers;
  284.                
  285.                 //this.answers.sortDesc();
  286.                 Hint@ nextHint = generateHint();
  287.                 while(@nextHint != null) {
  288.                         hints.insertLast(nextHint);
  289.                         @nextHint = generateHint();
  290.                 }
  291.         }
  292.        
  293.         array<Answer@> getAnswers() {
  294.                 return answers;
  295.         }
  296.        
  297.         array<Hint@> getHints() {
  298.                 return hints;
  299.         }
  300.        
  301.         Answer@ checkAnswer(string answer) {
  302.                 for(uint i = 0; i < answers.length(); i++) {
  303.                         Answer@ checkAnswer = answers[i];
  304.                         if(toLower(answer) == toLower(checkAnswer.toString()))
  305.                                 return checkAnswer;
  306.                 }
  307.                 return null;
  308.         }
  309.        
  310.         bool isValid() {
  311.                 for(uint i = 0; i < answers.length; i++)
  312.                         if(!answers[i].isValid())
  313.                                 return false;
  314.                 return true;
  315.         }
  316.        
  317.         string toString() {
  318.                 return this.question;
  319.         }
  320.        
  321.         string toFullString() {
  322.                 string result = "Q:";
  323.                 result += this.question;
  324.                 result += "\nA:[";
  325.                 for(uint i = 0; i < answers.length; i++) {
  326.                         result += answers[i].toString();
  327.                         if(i != answers.length - 1)
  328.                                 result += ",";
  329.                 }
  330.                 result += "]";
  331.                 return result;
  332.         }
  333.        
  334.         private Hint@ generateHint() {
  335.                 if(answers.length() == 0 || config.numHints == 0) return null;
  336.                 Answer@ longestAnswer = @answers[0];
  337.                 string longestAnswerString = longestAnswer.toString();
  338.                 string hint = "";
  339.                 if(hints.length() == 0) {
  340.                         for(uint i = 0; i < longestAnswerString.length(); i++)
  341.                                 if(isAlphanumeric(longestAnswerString[i]))
  342.                                         hint += "_";
  343.                                 else
  344.                                         hint += chr(longestAnswerString[i]);
  345.                         return @Hint(hint);
  346.                 }
  347.                 Hint@ prevHint = hints[hints.length()-1];
  348.                 hint = prevHint.toString();
  349.                 uint numBlanksLeft = prevHint.getNumBlanks();
  350.                 uint numBlanksFilled = 0;
  351.                 uint blanksToFillIn = longestAnswer.getQuizzableLength() / config.numHints;
  352.                
  353.                 while(numBlanksLeft > 0 && numBlanksFilled < blanksToFillIn) {
  354.                         int randomIndex = jjRandom() % hint.length();
  355.                         if(hint[randomIndex] != ASCII_UNDERSCORE)
  356.                                 continue;
  357.                         hint[randomIndex] = longestAnswerString[randomIndex];
  358.                         numBlanksLeft--;
  359.                         numBlanksFilled++;
  360.                 }
  361.                 if(numBlanksLeft >= prevHint.getNumBlanks())
  362.                         return null;
  363.                
  364.                 return @Hint(hint);
  365.         }
  366. }
  367.  
  368. class Category {
  369.         private array<Question@> questions;
  370.         private string instructions;
  371.         private string label;
  372.        
  373.         Category(string label, string instructions, array<Question@> questions) {
  374.                 this.label = label;
  375.                 this.instructions = instructions;
  376.                 this.questions = questions;
  377.         }
  378.        
  379.         Category(Category@ other) {
  380.                 this.label = other.label;
  381.                 this.instructions = other.instructions;
  382.                 this.questions = other.questions;
  383.         }
  384.        
  385.         Question@ retrieveNextQuestion() {
  386.                 Question@ nextQuestion;
  387.                 if(questions.length() > 0) {
  388.                         @nextQuestion = @questions[questions.length()-1];
  389.                         questions.removeAt(questions.length()-1);
  390.                         return nextQuestion;
  391.                 }
  392.                 return null;
  393.         }
  394.        
  395.         array<Question@> getQuestions() {
  396.                 return questions;
  397.         }
  398.        
  399.         string toFullString() {
  400.                 string result = "[";
  401.                
  402.                 result += this.label;
  403.                 result += "]\n";
  404.                 if(this.instructions != "") {
  405.                         result += "I:";
  406.                         result += this.instructions;
  407.                         result += "\n";
  408.                 }
  409.                 result += "\n";
  410.                 for(uint i = 0; i < questions.length; i++) {
  411.                         result += questions[i].toFullString();
  412.                         result += "\n\n";
  413.                 }
  414.                 return result;
  415.         }
  416.        
  417.         string getInstructions() {
  418.                 return instructions;
  419.         }
  420.  
  421.         string getLabel() {
  422.                 return label;
  423.         }
  424.        
  425.         Category@ shuffle() {
  426.                 array<Question@> newQuestions = questions;
  427.                 //Fisher-Yates shuffle
  428.                 for(uint i = newQuestions.length() - 1; i > 0; i--) {
  429.                         int j = jjRandom() % (i+1);
  430.                         Question@ temp = @newQuestions[i];
  431.                         @newQuestions[i] = @newQuestions[j];
  432.                         @newQuestions[j] = @temp;
  433.                 }
  434.                 return @Category(label, instructions, newQuestions);
  435.         }
  436. }
  437.  
  438. class CategorySet {
  439.         private array<Category@> categories;
  440.        
  441.         CategorySet(array<Category@> categories) {
  442.                 this.categories = categories;
  443.         }
  444.        
  445.         array<Category@> getCategories() {
  446.                 return categories;
  447.         }
  448.        
  449.         CategorySet@ shuffle() {
  450.                 array<Category@> newCategories = categories;
  451.                 //Fisher-Yates shuffle
  452.                 for(uint i = newCategories.length() - 1; i > 0; i--) {
  453.                         int j = jjRandom() % (i+1);
  454.                         Category@ temp = @newCategories[i];
  455.                         @newCategories[i] = @newCategories[j];
  456.                         @newCategories[j] = @temp;
  457.                 }
  458.                 return @CategorySet(newCategories);
  459.         }
  460.        
  461.         Category@ retrieveNextCategory() {
  462.                 Category@ nextCategory;
  463.                 if(categories.length() > 0) {
  464.                         @nextCategory = @categories[categories.length()-1];
  465.                         categories.removeAt(categories.length()-1);
  466.                         return nextCategory;
  467.                 }
  468.                 return null;
  469.         }
  470.        
  471.         string toFullString() {
  472.                 string result = "";
  473.                 for(uint i = 0; i < categories.length; i++) {
  474.                         result += categories[i].toFullString();
  475.                         result += "\n";
  476.                 }
  477.                 return result;
  478.         }
  479. }
  480.  
  481. class PlainTextParser {
  482.         private jjSTREAM@ file;
  483.         private uint position;
  484.         private uint size;
  485.  
  486.         PlainTextParser(string filename) {
  487.                 @file = @jjSTREAM(filename);
  488.                 size = file.getSize();
  489.         }
  490.        
  491.         string getLine() {
  492.                 uint8 byte;
  493.                 string line = "";
  494.                 while(position++ < size) {
  495.                         file.pop(byte);
  496.                         if(byte == ASCII_CARRIAGE_RETURN)
  497.                                 continue;
  498.                         if(byte == ASCII_LINE_FEED)
  499.                                 break;
  500.                         line += chr(byte);
  501.                 }
  502.                 return line;
  503.         }
  504.        
  505.         bool hasNext() {
  506.                 return position < size;
  507.         }
  508.        
  509.         uint fileSize() {
  510.                 return size;
  511.         }
  512. }
  513.  
  514. class QuestionParser {
  515.         PlainTextParser@ parser;
  516.  
  517.         private int lineCount = 1;
  518.         private bool expectAnswer = false;
  519.         private array<Question@> curCategoryQuestions;
  520.         private array<Category@> categories;
  521.        
  522.         private string question = "";
  523.         private string instructions = "";
  524.         private string label = "";
  525.        
  526.         CategorySet@ parse(string filename) {
  527.                 @parser = PlainTextParser(filename);
  528.                 uint totalBytes = parser.fileSize();
  529.                 if(totalBytes == 0) {
  530.                         jjConsole("ERROR: " + filename + " appears to be empty or not exist!");
  531.                         return @CategorySet(array<Category@>());
  532.                 }
  533.                
  534.                 while(parser.hasNext()) {
  535.                         string line = parser.getLine();
  536.                         if(line.length() == 0 || startsWith(line,"%")) {lineCount++; continue;}
  537.                         if(startsWith(line,"[") && endsWith(line,"]") && !expectAnswer) { //New Category
  538.                                 if(curCategoryQuestions.length() > 0 || categories.length > 0) { //If this is not the first category, then the previous one is complete
  539.                                         categories.insertLast(@Category(label, instructions, curCategoryQuestions));
  540.                                         curCategoryQuestions.resize(0);
  541.                                         label = "";
  542.                                         instructions = "";
  543.                                 }
  544.                                 label = trim(line.substr(1, line.length() - 2));
  545.                         } else if(startsWith(line,"I:") && !expectAnswer) {
  546.                                 if(trim(line).length() == 2) {
  547.                                         jjConsole("LINE " + lineCount + ": You must provide instructions after an 'I:'");
  548.                                         return @CategorySet(array<Category@>());
  549.                                 }
  550.                                 instructions = trim(line.substr(2,line.length()-2));
  551.                         } else if(startsWith(line,"Q:") && !expectAnswer) {
  552.                                 if(trim(line).length() == 2) {
  553.                                         jjConsole("LINE " + lineCount + ": You must provide a question after a 'Q:'");
  554.                                         return @CategorySet(array<Category@>());
  555.                                 }
  556.                                 question = trim(line.substr(2,line.length()-2));
  557.                                 expectAnswer = true;
  558.                         } else if(startsWith(line,"A:") && expectAnswer) {
  559.                                 if(trim(line).length() == 2) {
  560.                                         jjConsole("LINE " + lineCount + ": You must provide an answer after a 'A:'");
  561.                                         return @CategorySet(array<Category@>());
  562.                                 }
  563.                                 string ans = line.substr(2,line.length()-2);
  564.                                 array<string> parsedAnswers = split(ans, ";");
  565.                                 array<Answer@> answers;
  566.                                 for(uint i = 0; i < parsedAnswers.length; i++)
  567.                                         if(parsedAnswers[i].length() > 0)
  568.                                                 answers.insertLast(@Answer(trim(parsedAnswers[i])));
  569.                                 Question@ newQuestion = @Question(question, answers);
  570.                                 if(newQuestion.isValid())
  571.                                         curCategoryQuestions.insertLast(newQuestion);
  572.                                 expectAnswer = false;
  573.                         } else if(startsWith(line,"[") && endsWith(line,"]") && expectAnswer) {
  574.                                 jjConsole("LINE " + lineCount + ": category label found but answer expected: " + line);
  575.                                 return @CategorySet(array<Category@>());
  576.                         } else if(startsWith(line,"I:") && expectAnswer) {
  577.                                 jjConsole("LINE " + lineCount + ": instructions found but answer expected: " + line);
  578.                                 return @CategorySet(array<Category@>());
  579.                         } else if(startsWith(line,"Q:") && expectAnswer) {
  580.                                 jjConsole("LINE " + lineCount + ": question found but answer expected: " + line);
  581.                                 return @CategorySet(array<Category@>());
  582.                         } else if(startsWith(line,"A:") && !expectAnswer) {
  583.                                 jjConsole("LINE " + lineCount + ": unexpected answer: " + line);
  584.                                 return @CategorySet(array<Category@>());
  585.                         } else {
  586.                                 jjConsole("LINE " + lineCount + ": unrecognized syntax: " + line);
  587.                                 return @CategorySet(array<Category@>());
  588.                         }
  589.                        
  590.                         lineCount++;
  591.                 }
  592.                 if(curCategoryQuestions.length() > 0 || categories.length > 0) { //EOF reached, so add the last category (if there were any)
  593.                         categories.insertLast(@Category(label, instructions, curCategoryQuestions));
  594.                 }
  595.                 array<Category@> results = categories;
  596.                
  597.                 lineCount = 1;
  598.                 expectAnswer = false;
  599.                 curCategoryQuestions.resize(0);
  600.                 categories.resize(0);
  601.        
  602.                 question = "";
  603.                 instructions = "";
  604.                 label = "";
  605.  
  606.                 return @CategorySet(results);
  607.         }
  608. }
  609.  
  610. class ConfigParser {
  611.        
  612.         void saveConfig() {
  613.        
  614.         }
  615.        
  616.         void loadConfig() {
  617.        
  618.         }
  619. }
  620.  
  621. class Score {
  622.         private int points;
  623.         private int gamesWon;
  624.        
  625.         Score() {
  626.                 points = 0;
  627.                 gamesWon = 0;
  628.         }
  629.        
  630.         Score(int points, int gamesWon) {
  631.                 this.points = points;
  632.                 this.gamesWon = gamesWon;
  633.         }
  634.        
  635.         int getPoints() const {
  636.                 return points;
  637.         }
  638.        
  639.         int getGamesWon() const {
  640.                 return gamesWon;
  641.         }
  642.        
  643.         int opCmp(Score@ other) const {
  644.                 int compare = this.points - other.points;
  645.                 return compare == 0 ? this.gamesWon - other.gamesWon : compare;
  646.         }
  647. }
  648.  
  649. class PlayerScore {
  650.         private string player;
  651.         private Score score;
  652.  
  653.         PlayerScore(string player, Score score) {
  654.                 this.player = player;
  655.                 this.score = score;
  656.         }
  657.  
  658.         string getPlayer() const {
  659.                 return player;
  660.         }
  661.  
  662.         Score getScore() const {
  663.                 return score;
  664.         }
  665.        
  666.         int opCmp(PlayerScore@ other) const {
  667.                 return score.opCmp(other.score);
  668.         }
  669. }
  670.  
  671. class ScoreSet {
  672.         private dictionary scores;
  673.        
  674.         private void addPlayer(string playerName) {
  675.                 Score@ handle = cast<Score>(scores[playerName]);
  676.                 if(handle is null)
  677.                         @scores[playerName] = Score();
  678.         }
  679.        
  680.         void addPoints(string playerName, int numPoints) {
  681.                 addPlayer(playerName);
  682.                 Score playerOldScore;
  683.                 scores.get(playerName, playerOldScore);
  684.                 scores[playerName] = Score(playerOldScore.getPoints() + numPoints, playerOldScore.getGamesWon());
  685.         }
  686.        
  687.         void addGamesWon(string playerName, int numGames) {
  688.                 addPlayer(playerName);
  689.                 Score playerOldScore;
  690.                 scores.get(playerName, playerOldScore);
  691.                 scores[playerName] = Score(playerOldScore.getPoints(), playerOldScore.getGamesWon() + numGames);
  692.         }
  693.        
  694.         Score@ getScore(string playerName) const {
  695.                 addPlayer(playerName);
  696.                 Score score;
  697.                 scores.get(playerName, score);
  698.                 return @score;
  699.         }
  700.        
  701.         uint numScores() const {
  702.                 return scores.getSize();
  703.         }
  704.        
  705.         void reset() {
  706.                 scores.deleteAll();
  707.         }
  708.        
  709.         array<PlayerScore@> sortedByScoreDesc() const {
  710.                 array<PlayerScore@> playerScores;
  711.                 array<string> playerNames = scores.getKeys();
  712.                 for(uint i = 0; i < playerNames.length(); i++) {
  713.                         Score score;
  714.                         scores.get(playerNames[i], score);
  715.                         playerScores.insertLast(@PlayerScore(playerNames[i], score));
  716.                 }
  717.                 playerScores.sortDesc();
  718.                 return playerScores;
  719.         }
  720.        
  721.         string getLeadingPlayer() const {
  722.                 array<PlayerScore@> leaders = sortedByScoreDesc();
  723.                 if(leaders.length() == 0)
  724.                         return "-Nobody-";
  725.                 else
  726.                         return leaders[0].getPlayer();
  727.         }
  728.        
  729.         array<PlayerScore@> getFirstPlaceTies() const {
  730.                 array<PlayerScore@> result;
  731.                 array<PlayerScore@> leaders = sortedByScoreDesc();
  732.                 if(leaders.length() > 0) {
  733.                         int topScore = leaders[0].getScore().getPoints();
  734.                         for(uint i = 0; i < leaders.length(); i++) {
  735.                                 if(leaders[i].getScore().getPoints() == topScore) {
  736.                                         result.insertLast(leaders[i]);
  737.                                 } else
  738.                                         break;
  739.                         }
  740.                 }
  741.                 return result;
  742.         }
  743. }
  744.  
  745. class PersistentScoreSet {
  746.         private ScoreSet scores;
  747.  
  748.         PersistentScoreSet() {
  749.                 loadScores();
  750.                 array<PlayerScore@> ps = sortedByScoreDesc();
  751.         }
  752.        
  753.         void addPoints(string playerName, int numPoints) {
  754.                 scores.addPoints(playerName, numPoints);
  755.                 saveScores();
  756.         }
  757.        
  758.         void addGamesWon(string player, int gamesWon) {
  759.                 scores.addGamesWon(player, gamesWon);
  760.                 saveScores();
  761.         }
  762.        
  763.         string getLeadingPlayer() const {
  764.                 return scores.getLeadingPlayer();
  765.         }
  766.  
  767.         array<PlayerScore@> sortedByScoreDesc() const {
  768.                 return scores.sortedByScoreDesc();
  769.         }
  770.        
  771.         Score getScore(string player) const {
  772.                 return scores.getScore(player);
  773.         }
  774.        
  775.         uint numScores() const {
  776.                 return scores.numScores();
  777.         }
  778.        
  779.         void saveScores() {
  780.                 jjSTREAM file(SCORES_FILE);
  781.                 file.clear();
  782.                 array<PlayerScore@> playerScores = scores.sortedByScoreDesc();
  783.                 for(uint i = 0; i < playerScores.length; i++) {
  784.                         file.push(playerScores[i].getPlayer());
  785.                         file.push(playerScores[i].getScore().getPoints());
  786.                         file.push(playerScores[i].getScore().getGamesWon());
  787.                 }
  788.                 file.save(SCORES_FILE);
  789.         }
  790.        
  791.         void loadScores() {
  792.                 jjSTREAM file(SCORES_FILE);
  793.                 while(!file.isEmpty()) {
  794.                         string playerName;
  795.                         int points;
  796.                         int gamesWon;
  797.                         file.pop(playerName);
  798.                         file.pop(points);
  799.                         file.pop(gamesWon);
  800.                         scores.addPoints(playerName, points);
  801.                         scores.addGamesWon(playerName, gamesWon);
  802.                 }
  803.         }
  804. }
  805.  
  806. class Timer {
  807.         int inter, startTime, end;
  808.         bool running;
  809.        
  810.         Timer() {
  811.                 this.running = false;
  812.         }
  813.        
  814.         Timer start(int interval) {
  815.                 if(interval < 0)
  816.                         return start();
  817.                 this.inter = interval;
  818.                 this.startTime = jjGameTicks;
  819.                 this.end = this.startTime + this.inter;
  820.                 this.running = true;
  821.                 return this;
  822.         }
  823.        
  824.         Timer start() {
  825.                 this.startTime = jjGameTicks;
  826.                 this.running = true;
  827.                 this.end = -1;
  828.                 this.inter = -1;
  829.                 return this;
  830.         }
  831.        
  832.         Timer stop() {
  833.                 this.running = false;
  834.                 return this;
  835.         }
  836.        
  837.         bool isFinished() const {
  838.                 if(!this.running) return false;
  839.                 if(this.end == -1) return false;
  840.                 return jjGameTicks >= this.end;
  841.         }
  842.        
  843.         uint elapsedTime() const {
  844.                 return jjGameTicks-this.startTime;
  845.         }
  846.        
  847.         int endTime() const {
  848.                 return end;
  849.         }
  850.        
  851.         int remainingTime() const {
  852.                 if(this.interval() == -1) return -1;
  853.                 return this.interval() - this.elapsedTime();
  854.         }
  855.        
  856.         int interval() const {
  857.                 return this.inter;
  858.         }
  859.        
  860.         Timer reset() {
  861.                 start(this.inter);
  862.                 return this;
  863.         }
  864.        
  865.         bool isStarted() const {
  866.                 return this.running;
  867.         }
  868. }
  869.  
  870. class MessagePrinter {
  871.         private Timer@ timer = @Timer();
  872.         private array<string> toPrint;
  873.         private uint32 interval;
  874.         private uint curMessage;
  875.        
  876.         MessagePrinter(array<string> toPrint, uint32 interval) {
  877.                 this.toPrint = toPrint;
  878.                 this.interval = interval;
  879.                 curMessage = 0;
  880.                 timer.start();
  881.         }
  882.        
  883.         void update() {
  884.                 if(interval == 0)
  885.                         while(!isDone())
  886.                                 print();
  887.                 if(curMessage == 0) print();
  888.                 if(!isDone() && timer.elapsedTime() > interval) print();
  889.         }
  890.        
  891.         private void print() {
  892.                 sendChat(toPrint[curMessage++]);
  893.                 timer.reset();
  894.         }
  895.  
  896.         bool isDone() {
  897.                 return curMessage >= toPrint.length();
  898.         }
  899.  
  900.         array<string> getStrings() {
  901.                 return toPrint;
  902.         }
  903. }
  904.  
  905. class TriviaGame {
  906.         private uint roundNum;
  907.         private uint questionNum;
  908.         private ScoreSet@ scores;
  909.         private CategorySet@ categorySet;
  910.         private Category@ curCategory;
  911.         private Question@ curQuestion;
  912.        
  913.         TriviaGame() {
  914.                 initialize();
  915.                 @scores = @ScoreSet();
  916.                 QuestionParser@ parser = @QuestionParser();
  917.                 @categorySet = @parser.parse(QUESTION_FILE);
  918.                 if(categorySet.getCategories().length() > 0) {
  919.                         @categorySet = @categorySet.shuffle();
  920.                         nextCategory();
  921.                 }
  922.         }
  923.        
  924.         private void initialize() {
  925.                 roundNum = 0;
  926.                 questionNum = 0;
  927.                 @scores = null;
  928.                 @categorySet = null;
  929.                 @curCategory = null;
  930.                 @curQuestion = null;
  931.         }
  932.        
  933.         uint getRoundNum() const {return roundNum;}
  934.        
  935.         uint getQuestionNum() const {return questionNum;}
  936.        
  937.         ScoreSet@ getScores() const {return scores;}
  938.        
  939.         Category@ getCurCategory() const {return curCategory;}
  940.        
  941.         Question@ getCurQuestion() const {return curQuestion;}
  942.        
  943.         bool hasCurCategory() const {return curCategory !is null;}
  944.        
  945.         bool hasCurQuestion() const {return curQuestion !is null;}
  946.        
  947.         bool isActive() const {return roundNum > 0;}
  948.        
  949.         Category@ nextCategory() {
  950.                 @curCategory = @categorySet.retrieveNextCategory();
  951.                 if(hasCurCategory()) {
  952.                         roundNum++;
  953.                         @curCategory = curCategory.shuffle();
  954.                 }
  955.                 return curCategory;
  956.         }
  957.        
  958.         Question@ nextQuestion() {
  959.                 @curQuestion = @curCategory.retrieveNextQuestion();
  960.                 if(hasCurQuestion())
  961.                         questionNum++;
  962.                 return curQuestion;
  963.         }
  964.        
  965.         void endQuestion() {
  966.                 @curQuestion = null;
  967.         }
  968.        
  969.         void endGame() {
  970.                 initialize();
  971.         }
  972. }
  973.  
  974. class ChatMessage {
  975.         private jjPLAYER@ player;
  976.         private string message;
  977.        
  978.         ChatMessage(jjPLAYER@ player, string message) {
  979.                 @this.player = @player;
  980.                 this.message = message;
  981.         }
  982.        
  983.         jjPLAYER@ getPlayer() {
  984.                 return player;
  985.         }
  986.  
  987.         string getMessage() {
  988.                 return message;
  989.         }
  990. }
  991.  
  992. class StateManager {
  993.         private State@ curState;
  994.        
  995.         void changeState(State@ newState) {
  996.                 if(newState !is null) {
  997.                         @curState = @newState;
  998.                         newState.onEnter();
  999.                 }
  1000.         }
  1001.        
  1002.         State@ getState() {
  1003.                 return curState;
  1004.         }
  1005. }
  1006.  
  1007. interface State {
  1008.         void onEnter();
  1009.         void doState(ChatMessage@ message);
  1010. }
  1011.  
  1012. class BaseState : State {
  1013.         void onEnter() {}
  1014.  
  1015.         void doState(ChatMessage@ message) {
  1016.                 if(message is null) return;
  1017.                 if(enableDebugLogging) jjDebug("BASE::doState");
  1018.                 string name = message.getPlayer().name;
  1019.                 if(toLower(message.getMessage()) == "@score") {
  1020.                         if(game !is null && game.isActive()) {
  1021.                                 sendChat(name + "'s current score is: " + game.getScores().getScore(name).getPoints());
  1022.                         }
  1023.                 } else if(toLower(message.getMessage()) == "@stop" && isAdmin(message.getPlayer())) {
  1024.                         stateManager.changeState(@StoppedState());
  1025.                 } else if(toLower(message.getMessage()) == "@top") {
  1026.                         array<PlayerScore@> leaders = overallScores.sortedByScoreDesc();
  1027.                         int numTop = leaders.length() < 3 ? leaders.length() : 3;
  1028.                         for(int i = 0; i < numTop; i++) {
  1029.                                 Score score = leaders[i].getScore();
  1030.                                 string player = leaders[i].getPlayer();
  1031.                                 sendChat((i+1) + ". " + player + ", points: " + score.getPoints() + ", wins:" + score.getGamesWon());
  1032.                         }
  1033.                 } else if(startsWith(toLower(message.getMessage()),"@whois ")) {
  1034.                         array<string> splitString = split(message.getMessage()," ");
  1035.                        
  1036.                         int index;
  1037.                         if(splitString.length > 1) {
  1038.                                 index = parseInt(splitString[1]) - 1;
  1039.                         }
  1040.                         array<PlayerScore@> leaders = overallScores.sortedByScoreDesc();
  1041.                         if(index < 0) return;
  1042.                         if(uint(index) >= leaders.length()) {
  1043.                                 sendChat("Nobody is " + (index+1) + " yet.");
  1044.                                 return;
  1045.                         }
  1046.                         Score score = leaders[index].getScore();
  1047.                         string player = leaders[index].getPlayer();
  1048.                         sendChat((index+1) + ". " + player + ", points: " + score.getPoints() + ", wins:" + score.getGamesWon());
  1049.                        
  1050.                 } else if(toLower(message.getMessage()) == "@rank") {
  1051.                         string player = message.getPlayer().name;
  1052.                         array<PlayerScore@> leaders = overallScores.sortedByScoreDesc();
  1053.                         uint num = 0;
  1054.                         bool found = false;
  1055.                         Score@ score = null;
  1056.                         for(; num < leaders.length(); num++) {
  1057.                                 if(leaders[num].getPlayer() == player) {
  1058.                                         @score = leaders[num].getScore();
  1059.                                         found = true;
  1060.                                         break;
  1061.                                 }
  1062.                         }
  1063.                         if(!found) {
  1064.                                 game.getScores().getScore(player);
  1065.                                 score = overallScores.getScore(player);
  1066.                         }
  1067.                         sendChat((num+1) + ". " + player + ", points: " + score.getPoints() + ", wins:" + score.getGamesWon());
  1068.                 } else if(startsWith(toLower(message.getMessage()),"@numrounds ") && isAdmin(message.getPlayer())) {
  1069.                         array<string> splitString = split(message.getMessage()," ");
  1070.                        
  1071.                         int num;
  1072.                         if(splitString.length > 1) {
  1073.                                 num = parseInt(splitString[1]);
  1074.                                 if(num <= 0) return;
  1075.                         }
  1076.                         config.numRounds = num;
  1077.                         config.save();
  1078.                         jjConsole("Number of rounds has been set to " + num, true);
  1079.                 } else if(startsWith(toLower(message.getMessage()),"@numqs ") && isAdmin(message.getPlayer())) {
  1080.                         array<string> splitString = split(message.getMessage()," ");
  1081.                        
  1082.                         int num;
  1083.                         if(splitString.length > 1) {
  1084.                                 num = parseInt(splitString[1]);
  1085.                                 if(num <= 0) return;
  1086.                         }
  1087.                         config.questionsPerRound = num;
  1088.                         config.save();
  1089.                         jjConsole("Questions per round has been set to " + num, true);
  1090.                 } else if(startsWith(toLower(message.getMessage()),"@numhints ") && isAdmin(message.getPlayer())) {
  1091.                         array<string> splitString = split(message.getMessage()," ");
  1092.                        
  1093.                         int num;
  1094.                         if(splitString.length > 1) {
  1095.                                 num = parseInt(splitString[1]);
  1096.                                 if(num < 0) return;
  1097.                         }
  1098.                         config.numHints = num;
  1099.                         config.save();
  1100.                         jjConsole("Hints per question has been set to " + num, true);
  1101.                 }
  1102.         }
  1103. }
  1104.  
  1105. class StoppedState : BaseState, State {
  1106.         private MessagePrinter@ printer;
  1107.        
  1108.         void doState(ChatMessage@ message)  {
  1109.                 BaseState::doState(message);
  1110.                 if(printer !is null) printer.update();
  1111.                 if(message is null) return;
  1112.                 if(enableDebugLogging) jjDebug("StoppedState::doState");
  1113.                 if(toLower(message.getMessage()) == "@start") {
  1114.                         @game = @TriviaGame();
  1115.                         if(!game.hasCurCategory()) {
  1116.                                 jjConsole("ERROR: Game cannot proceed. Check server log for details.", true);
  1117.                         } else {
  1118.                                 stateManager.changeState(@QuestionState());
  1119.                         }
  1120.                 }
  1121.         }
  1122.  
  1123.         void onEnter() {
  1124.                 array<string> list();
  1125.                 list.insertLast(CHAT_PREFIX + "Type @start to begin trivia.");
  1126.                 @printer = @MessagePrinter(list, MESSAGE_PRINT_WAIT);
  1127.         }
  1128. }
  1129.  
  1130. class EndGameState : BaseState, State {
  1131.         private Timer@ timer = @Timer();
  1132.  
  1133.         void doState(ChatMessage@ message)  {
  1134.                 BaseState::doState(message);
  1135.                 if(timer.elapsedTime() > MESSAGE_PRINT_WAIT)
  1136.                         stateManager.changeState(@StoppedState());
  1137.         }
  1138.  
  1139.         void onEnter() {
  1140.                 array<PlayerScore@> firstPlace = game.getScores().getFirstPlaceTies();
  1141.                 if(firstPlace.length() <= 0 || firstPlace[0].getScore().getPoints() == 0) {
  1142.                         sendChat("Nobody wins!");
  1143.                         return;
  1144.                 }
  1145.                 if(firstPlace.length() > 1) {
  1146.                         sendChat("Game ends in a " + firstPlace.length() + "-way tie");
  1147.                         for(uint i = 0; i < firstPlace.length(); i++) {
  1148.                                 PlayerScore@ winner = @firstPlace[i];
  1149.                                 overallScores.addGamesWon(winner.getPlayer(),1);
  1150.                                 sendChat("|||" + winner.getPlayer() + ": " + winner.getScore().getPoints());
  1151.                         }
  1152.                 } else {
  1153.                         PlayerScore@ winner = @firstPlace[0];
  1154.                         overallScores.addGamesWon(winner.getPlayer(),1);
  1155.                         sendChat("|||" + winner.getPlayer() + " wins the game with " + winner.getScore().getPoints() + " points!");
  1156.                 }
  1157.                 game.endGame();
  1158.                 timer.start();
  1159.         }
  1160. }
  1161.  
  1162. enum QuestionSubState {
  1163.         INIT,
  1164.         PRINTING_INSTRUCTIONS,
  1165.         WAITING_TO_START,
  1166.         PRINTING_QUESTION,
  1167.         WAITING_FOR_ANSWER,
  1168.         WAITING_TO_ASK
  1169. }
  1170.  
  1171. class QuestionState : BaseState, State {
  1172.         private MessagePrinter@ printer;
  1173.         private Timer@ timer = @Timer();
  1174.         private uint hintsGiven = 0;
  1175.         private QuestionSubState subState = QuestionSubState::INIT;
  1176.         private uint questionCount = 0;
  1177.         private uint totalQuestionTime;
  1178.        
  1179.         QuestionState() {
  1180.                 if(enableDebugLogging) jjDebug("QuestionState constructor called");
  1181.         }
  1182.        
  1183.         void onEnter()  {
  1184.                 if(enableDebugLogging) jjDebug("QuestionState::onEnter");
  1185.                 subState = QuestionSubState::PRINTING_INSTRUCTIONS;
  1186.                 array<string> list();
  1187.                 list.insertLast("ROUND " + game.getRoundNum() + " begins now.");
  1188.                 list.insertLast("The topic is: " + game.getCurCategory().getLabel());
  1189.                 if(game.getCurCategory().getInstructions() != "")
  1190.                         list.insertLast(game.getCurCategory().getInstructions());
  1191.                 @printer = @MessagePrinter(list, MESSAGE_PRINT_WAIT);
  1192.                 if(enableDebugLogging) jjDebug("QuestionState::onEnter - end of onEnter");
  1193.         }
  1194.  
  1195.         void doState(ChatMessage@ message) {
  1196.                 BaseState::doState(message);
  1197.                 if(message !is null && message.getMessage() == "@skipq" && isAdmin(message.getPlayer()) && game.hasCurQuestion()) {
  1198.                         askQuestion();
  1199.                 } else if(message !is null && message.getMessage() == "@skipc" && isAdmin(message.getPlayer()) && game.hasCurCategory()) {
  1200.                         game.nextCategory();
  1201.                         if(game.hasCurCategory())
  1202.                                 stateManager.changeState(@QuestionState());
  1203.                         else
  1204.                                 stateManager.changeState(@EndGameState());
  1205.                 } else if(subState == QuestionSubState::PRINTING_INSTRUCTIONS) {
  1206.                         printer.update();
  1207.                         if(printer.isDone()) {
  1208.                                 subState = QuestionSubState::WAITING_TO_START;
  1209.                                 timer.reset();
  1210.                         }
  1211.                 } else if(subState == QuestionSubState::WAITING_TO_START && timer.elapsedTime() > BETWEEN_QUESTION_WAIT) {
  1212.                         askQuestion();
  1213.                 } else if(subState == QuestionSubState::PRINTING_QUESTION) {
  1214.                         printer.update();
  1215.                         if(printer.isDone())
  1216.                                 subState = QuestionSubState::WAITING_FOR_ANSWER;
  1217.                 } else if(subState == QuestionSubState::WAITING_FOR_ANSWER) {
  1218.                         if(!checkCorrectAnswer(message)) {
  1219.                                 array<Hint@> availableHints = game.getCurQuestion().getHints();
  1220.                                 uint numAvailableHints = availableHints.length();
  1221.                                 uint maxHintsToShow = numAvailableHints < config.numHints ? numAvailableHints : config.numHints;
  1222.                                 if((config.numHints <= 0 && timer.elapsedTime() > QUESTION_TIME_NO_HINTS) ||
  1223.                                    (config.numHints > 0  && timer.elapsedTime() > TIME_PER_HINT * (maxHintsToShow + 1))) {
  1224.                                         sendChat("Nobody got it!");
  1225.                                         subState = QuestionSubState::WAITING_TO_ASK;
  1226.                                         timer.reset();
  1227.                                         game.endQuestion();
  1228.                                 } else if(config.numHints > 0 && timer.elapsedTime() > TIME_PER_HINT * (hintsGiven + 1)) {
  1229.                                         sendChat("HINT: " + availableHints[hintsGiven++].toString());
  1230.                                 }
  1231.                         }
  1232.                 } else if(subState == QuestionSubState::WAITING_TO_ASK && timer.elapsedTime() > BETWEEN_QUESTION_WAIT) {
  1233.                         askQuestion();
  1234.                 }
  1235.         }
  1236.        
  1237.         private bool checkCorrectAnswer(ChatMessage@ message)  {
  1238.                 if(subState != QuestionSubState::WAITING_FOR_ANSWER || message is null || !game.hasCurQuestion())
  1239.                         return false;
  1240.                 Answer@ matchingAnswer = game.getCurQuestion().checkAnswer(message.getMessage());
  1241.                 if(matchingAnswer !is null) {
  1242.                         awardPoints(message, matchingAnswer.toString());
  1243.                         return true;
  1244.                 }
  1245.                 return false;
  1246.         }
  1247.        
  1248.         void awardPoints(ChatMessage@ message, string response) {
  1249.                 string name = message.getPlayer().name;
  1250.                 sendChat(name + " is correct! The answer was:");
  1251.                 sendChat("|||" + response);
  1252.                 game.getScores().addPoints(name, 10);
  1253.                 overallScores.addPoints(name, 10);
  1254.                 if(showScoresEachQuestion)
  1255.                         sendChat("Their new score is: " + game.getScores().getScore(name).getPoints());
  1256.                 subState = QuestionSubState::WAITING_TO_ASK;
  1257.                 timer.reset();
  1258.                 game.endQuestion();
  1259.         }
  1260.        
  1261.         void askQuestion() {
  1262.                 if(enableDebugLogging) jjDebug("QuestionState::askQuestion");
  1263.                 bool nextRound = false;
  1264.                 if(questionCount < config.questionsPerRound) {
  1265.                         subState = QuestionSubState::WAITING_FOR_ANSWER;
  1266.                         game.nextQuestion();
  1267.                         if(!game.hasCurQuestion())
  1268.                                 nextRound = true;
  1269.                 } else
  1270.                         nextRound = true;
  1271.                 if(nextRound) {
  1272.                         onEndRound();
  1273.                         return;
  1274.                 }
  1275.                 questionCount++;
  1276.                 displayQuestion(game.getCurQuestion());
  1277.                 totalQuestionTime = TIME_PER_HINT * (game.getCurQuestion().getHints().length() + 1);
  1278.                 hintsGiven = 0;
  1279.                 timer.reset();
  1280.                 if(enableDebugLogging) jjDebug("QuestionState::askQuestion - end of askQuestion");
  1281.         }
  1282.        
  1283.         void displayQuestion(Question question) {
  1284.                 sendChat("|||Question " + game.getQuestionNum() + ":");
  1285.                 sendChat("|||" + question.toString());
  1286.         }
  1287.  
  1288.         void onEndRound() {
  1289.                 if(enableDebugLogging) jjDebug("QuestionState::onEndRound");
  1290.                 game.nextCategory();
  1291.                 if(!game.hasCurCategory() || game.getRoundNum() >= config.numRounds+1) {
  1292.                         stateManager.changeState(@EndGameState());
  1293.                         return;
  1294.                 }
  1295.                 stateManager.changeState(@QuestionState());
  1296.         }
  1297. }
  1298.  
  1299. //Hooks
  1300. void onLevelLoad() {
  1301.         if(jjIsServer) {
  1302.                 @config = @Config();
  1303.                 @overallScores = @PersistentScoreSet();
  1304.                 @stateManager = @StateManager();
  1305.                 stateManager.changeState(@StoppedState());
  1306.         }
  1307. }
  1308.  
  1309. void onChat(int clientID, string &in stringReceived, CHAT::Type chatType) {
  1310.         if(jjIsServer) {
  1311.                 jjPLAYER@ player = @null;
  1312.                 for(uint i = 0; i < 32; i++) {
  1313.                         if(jjPlayers[i].isActive && jjPlayers[i].clientID == clientID)
  1314.                                 @player = @jjPlayers[i];
  1315.                 }
  1316.                 chatMessages.insertLast(@ChatMessage(player, stringReceived));
  1317.         }
  1318. }
  1319.  
  1320. void onMain() {
  1321.         if(jjIsServer) {
  1322.                 ChatMessage@ curMessage = null;
  1323.                 if(chatMessages.length() > 0) {
  1324.                         @curMessage = @chatMessages[0];
  1325.                         chatMessages.removeAt(0);
  1326.                 }
  1327.                 stateManager.getState().doState(curMessage);
  1328.         }
  1329. }
  1330.  
  1331. bool onLocalChat(string &in stringReceived, CHAT::Type chatType) {
  1332.         if(toLower(stringReceived) == "@help") {
  1333.                 jjPrint("Trivia mutator help!");
  1334.                 jjPrint("@help  - displays this help text");
  1335.                 jjPrint("@start - begins the trivia game");
  1336.                 jjPrint("@score - shows your score in this game");
  1337.                 jjPrint("@rank  - shows your total points and wins");
  1338.                 jjPrint("@top   - shows the top 3 players");
  1339.                 jjPrint("@whois - shows the player with the given rank");
  1340.                 if(isAdmin(jjLocalPlayers[0])) {
  1341.                         jjPrint("----ADMIN ONLY COMMANDS----");
  1342.                         jjPrint("@stop - stops the game");
  1343.                         jjPrint("@skipq - skips the current question");
  1344.                         jjPrint("@skipc - skips the current category");
  1345.                         jjPrint("@numqs - sets the number of questions per round");
  1346.                         jjPrint("@numrounds - sets the number of rounds per game");
  1347.                         jjPrint("@numhints - sets the number of hints in a question");
  1348.                 }
  1349.         }
  1350.         return false;
  1351. }