Downloads containing trivia.mut

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

File preview

#pragma name "Trivia"

//Globals 
bool enableDebugLogging = false;
bool showScoresEachQuestion = false;

Config@ config;
StateManager@ stateManager;
TriviaGame@ game;
PersistentScoreSet@ overallScores;
array<ChatMessage@> chatMessages;

//Constants
const uint8 ASCII_TAB = 9;
const uint8 ASCII_SPACE = 32;
const uint8 ASCII_LINE_FEED = 10;
const uint8 ASCII_CARRIAGE_RETURN = 13;
const uint8 ASCII_UNDERSCORE = 95;
const uint8 ASCII_PIPE = 124;
const uint8 ASCII_SECTION = 167;

const uint8 ASCII_CONVERT_CASE = 32;

const string QUESTION_FILE = "trivia.asdat";
const string SCORES_FILE = "trivia-scores.asdat";
const string CONFIG_FILE = "trivia-config.asdat";
const string CHAT_PREFIX = chr(ASCII_SECTION) + "1";
const uint16 MESSAGE_PRINT_WAIT = 5*70;
const uint16 BETWEEN_QUESTION_WAIT = 8*70;
const uint16 QUESTION_TIME_NO_HINTS = 20*70;
const uint16 TIME_PER_HINT = 10*70;
const uint16 MAX_DISPLAY_CHARS = 53;

//Utility Functions
bool isUpperCase(uint8 code) { return (code >= 65 && code <= 90); }
bool isLowerCase(uint8 code) { return (code >= 97 && code <= 122); }
bool isNumeric(uint8 code) {return (code >= 48 && code <= 57);}
bool isAlphabetic(uint8 code) {return  isUpperCase(code) || isLowerCase(code);}
bool isAlphanumeric(uint8 code) {return isAlphabetic(code) || isNumeric(code);}

//Returns a one-character string corresponding to an ASCII value. 
//http://www.jazz2online.com/snippets/68/operations-on-ascii-values/
string chr(uint8 value){ 
  string str="\0";
  str[0]=value;
  return str;
}

//Returns the ASCII value of the first character of a string.
uint8 ord(string &in str){ 
  if(str.isEmpty()) {return 0;}
  return str[0];
}

string toLower(string input) {
	string toLower = input;
	for(uint i = 0; i < input.length; i++) {
		if(isUpperCase(toLower[i]))
			toLower[i] += ASCII_CONVERT_CASE;
	}
	return toLower;
}

string toUpper(string input) {
	string toUpper = input;
	for(uint i = 0; i < input.length; i++) {
		if(isLowerCase(toUpper[i]))
			toUpper[i] -= ASCII_CONVERT_CASE;
	}
	return toUpper;
}

bool startsWith(string data, string prefix) {
	return data.substr(0,prefix.length) == prefix;
}

bool endsWith(string data, string suffix) {
	return data.substr(data.length-suffix.length,suffix.length) == suffix;
}

array<string> split(string data, string delimeter) {
	uint8 del = ord(delimeter);
	array<string> result;
	string curSubstring;
	for(uint i = 0; i < data.length; i++) {
		if(data[i] == del) {
			result.insertLast(curSubstring);
			curSubstring = "";
		} else {
			curSubstring += chr(data[i]);
		}
	}
	result.insertLast(curSubstring);
	return result;
}

string trim(string data) {
	int i = 0, j = data.length-1;
	while(data[i] == ASCII_SPACE || data[i] == ASCII_TAB) i++;
	while(data[j] == ASCII_SPACE || data[j] == ASCII_TAB) j--;
	return data.substr(i, j-i+1);
}

bool isAdmin(jjPLAYER@ player) {
	return player.isAdmin || player.clientID == 0;
}

int getNumPipes(string message) {
	int count = 0;
	for(uint i = 0; i < message.length(); i++) {
		if(message[i] == ASCII_PIPE)
			count++;
	}
	count = count % 8;
	if(count == 6) count = 0;
	return count;
}

string makePipes(uint count) {
	string res = "";
	for(uint i = 0; i < count; i++)
		res += chr(ASCII_PIPE);
	return res;
}

void sendChat(string chatMessage, bool split = true)  {
	if(!split) {
		jjChat(CHAT_PREFIX + chatMessage);
		return;
	}
	const uint MAX_SUBSTRING_LENGTH = MAX_DISPLAY_CHARS - CHAT_PREFIX.length();
	uint start = 0, end = MAX_SUBSTRING_LENGTH > chatMessage.length() ? chatMessage.length() : MAX_SUBSTRING_LENGTH;
	string text = chatMessage.substr(start, end-start+1);
	
	if(chatMessage[end-1] != ASCII_SPACE && end < chatMessage.length() && chatMessage[end] != ASCII_SPACE) {
		int lastSpace = text.findLast(" ");
		if(lastSpace != -1) {
			end -= (text.length() - lastSpace);
			text = chatMessage.substr(start, end-start+1);
		}
	}

	string toSend = CHAT_PREFIX + trim(text);
	jjConsole(toSend,true);
	while(end+1 < chatMessage.length()) {
		int numPipes = getNumPipes(toSend);
		uint len = MAX_SUBSTRING_LENGTH - numPipes;
		start = end+1;
		end = start + len > chatMessage.length() ? chatMessage.length() : start + len;
		text = chatMessage.substr(start, end-start+1);
		if(end > 0 && end < chatMessage.length() && chatMessage[end] != ASCII_SPACE && end+1 < chatMessage.length() && chatMessage[end+1] != ASCII_SPACE) {
			int lastSpace = text.findLast(" ");
			if(lastSpace != -1) {
				end -= (text.length() - lastSpace);
				text = chatMessage.substr(start, end-start+1);
			}
		}
		
		toSend = CHAT_PREFIX + makePipes(numPipes) + trim(text);
		jjConsole(toSend,true);
	}
}

//Classes
class Config {
	uint8 numHints;         //Answers will have UP TO this many hints
	uint numRounds;		    //The game will have UP TO this many rounds
	uint questionsPerRound;  //The game will have UP TO this many questions per round
	
	Config() {
		load();
	}
	
	void setDefaults() {
		numHints = 3;
		numRounds = 5;
		questionsPerRound = 5;
	}
	
	void save() {
		jjSTREAM file(CONFIG_FILE);
		file.clear();
		file.push(numHints);
		file.push(numRounds);
		file.push(questionsPerRound);
		file.save(CONFIG_FILE);
	}
	
	void load() {
		jjSTREAM file(CONFIG_FILE);
		if(file.isEmpty()) {
			jjConsole("Config file not found. Using default settings.");
			setDefaults();
			save();
			return;
		}
		file.pop(numHints);
		file.pop(numRounds);
		file.pop(questionsPerRound);
	}
}

class Hint {
	private string hint;
	private uint8 numBlanks = 0;
	
	Hint(string hint) {
		this.hint = hint;
		for(uint i = 0; i < hint.length; i++) {
			if(hint[i] == ASCII_UNDERSCORE)
				numBlanks++;
		}
	}
	
	Hint(string hint, int numBlanks) {
		this.hint = hint;
		this.numBlanks = numBlanks;
	}
	
	uint8 getNumBlanks() {
		return numBlanks;
	}
	
	string toString() const {
		return hint;
	}
}

class Answer {
	private string answer;
	private uint quizzableLength = 0;
	private bool valid = true;
	
	Answer(string answer) {
		this.answer = answer;
		if(answer.length() > 57) {
			jjConsole("ERROR: answer \"" + answer + "\" cannot be typed in JJ2!");
			valid = false;
			return;
		}
		for(uint i = 0; i < answer.length; i++) {
			if(answer[i] == ASCII_UNDERSCORE) {
				jjConsole("ERROR: answer \"" + answer + "\" contains underscores!");
				valid = false;
				return;
			}
		}
		
		for(uint i = 0; i < answer.length(); i++) {
			if(isAlphanumeric(answer[i]))
				quizzableLength++;
		}
	}
	
	string toString() const {
		return answer;
	}
	
	uint getQuizzableLength() const {
		return quizzableLength;
	}
	
	int getLength() const {
		return answer.length();
	}
	
	bool isValid() const {
		return valid;
	}
	
	int opCmp(Answer@ other) const {
		return this.getQuizzableLength() - other.getQuizzableLength();
	}
}

class Question {
	private string question;
	private array<Answer@> answers;
	private array<Hint@> hints;

	Question(string question, array<Answer@> answers) {
		this.question = question;
		this.answers = answers;
		
		//this.answers.sortDesc();
		Hint@ nextHint = generateHint();
		while(@nextHint != null) {
			hints.insertLast(nextHint);
			@nextHint = generateHint();
		}
	}
	
	array<Answer@> getAnswers() {
		return answers;
	}
	
	array<Hint@> getHints() {
		return hints;
	}
	
	Answer@ checkAnswer(string answer) {
		for(uint i = 0; i < answers.length(); i++) {
			Answer@ checkAnswer = answers[i];
			if(toLower(answer) == toLower(checkAnswer.toString()))
				return checkAnswer;
		}
		return null;
	}
	
	bool isValid() {
		for(uint i = 0; i < answers.length; i++) 
			if(!answers[i].isValid()) 
				return false;
		return true;
	}
	
	string toString() {
		return this.question;
	}
	
	string toFullString() {
		string result = "Q:";
		result += this.question;
		result += "\nA:[";
		for(uint i = 0; i < answers.length; i++) {
			result += answers[i].toString();
			if(i != answers.length - 1)
				result += ",";
		}
		result += "]";
		return result;
	}
	
	private Hint@ generateHint() {
		if(answers.length() == 0 || config.numHints == 0) return null;
		Answer@ longestAnswer = @answers[0];
		string longestAnswerString = longestAnswer.toString();
		string hint = "";
		if(hints.length() == 0) {
			for(uint i = 0; i < longestAnswerString.length(); i++)
				if(isAlphanumeric(longestAnswerString[i]))
					hint += "_";
				else
					hint += chr(longestAnswerString[i]);
			return @Hint(hint);
		}
		Hint@ prevHint = hints[hints.length()-1];
		hint = prevHint.toString();
		uint numBlanksLeft = prevHint.getNumBlanks();
		uint numBlanksFilled = 0;
		uint blanksToFillIn = longestAnswer.getQuizzableLength() / config.numHints;
		
		while(numBlanksLeft > 0 && numBlanksFilled < blanksToFillIn) {
			int randomIndex = jjRandom() % hint.length();
			if(hint[randomIndex] != ASCII_UNDERSCORE)
				continue;
			hint[randomIndex] = longestAnswerString[randomIndex];
			numBlanksLeft--;
			numBlanksFilled++;
		}
		if(numBlanksLeft >= prevHint.getNumBlanks())
			return null;
		
		return @Hint(hint);
	}
}

class Category {
	private array<Question@> questions;
	private string instructions;
	private string label;
	
	Category(string label, string instructions, array<Question@> questions) {
		this.label = label;
		this.instructions = instructions;
		this.questions = questions;
	}
	
	Category(Category@ other) {
		this.label = other.label;
		this.instructions = other.instructions;
		this.questions = other.questions;
	}
	
	Question@ retrieveNextQuestion() {
		Question@ nextQuestion;
		if(questions.length() > 0) {
			@nextQuestion = @questions[questions.length()-1];
			questions.removeAt(questions.length()-1);
			return nextQuestion;
		}
		return null;
	}
	
	array<Question@> getQuestions() {
		return questions;
	}
	
	string toFullString() {
		string result = "[";
		
		result += this.label;
		result += "]\n";
		if(this.instructions != "") {
			result += "I:";
			result += this.instructions;
			result += "\n";
		}
		result += "\n";
		for(uint i = 0; i < questions.length; i++) {
			result += questions[i].toFullString();
			result += "\n\n";
		}
		return result;
	}
	
	string getInstructions() {
		return instructions;
	}

	string getLabel() {
		return label;
	}
	
	Category@ shuffle() {
		array<Question@> newQuestions = questions;
		//Fisher-Yates shuffle
		for(uint i = newQuestions.length() - 1; i > 0; i--) {
			int j = jjRandom() % (i+1);
			Question@ temp = @newQuestions[i];
			@newQuestions[i] = @newQuestions[j];
			@newQuestions[j] = @temp;
		}
		return @Category(label, instructions, newQuestions);
	}
}

class CategorySet {
	private array<Category@> categories;
	
	CategorySet(array<Category@> categories) {
		this.categories = categories;
	}
	
	array<Category@> getCategories() {
		return categories;
	}
	
	CategorySet@ shuffle() {
		array<Category@> newCategories = categories;
		//Fisher-Yates shuffle
		for(uint i = newCategories.length() - 1; i > 0; i--) {
			int j = jjRandom() % (i+1);
			Category@ temp = @newCategories[i];
			@newCategories[i] = @newCategories[j];
			@newCategories[j] = @temp;
		}
		return @CategorySet(newCategories);
	}
	
	Category@ retrieveNextCategory() {
		Category@ nextCategory;
		if(categories.length() > 0) {
			@nextCategory = @categories[categories.length()-1];
			categories.removeAt(categories.length()-1);
			return nextCategory;
		}
		return null;
	}
	
	string toFullString() {
		string result = "";
		for(uint i = 0; i < categories.length; i++) {
			result += categories[i].toFullString();
			result += "\n";
		}
		return result;
	}
}

class PlainTextParser {
	private jjSTREAM@ file;
	private uint position;
	private uint size;

	PlainTextParser(string filename) {
		@file = @jjSTREAM(filename);
		size = file.getSize(); 
	}
	
	string getLine() {
		uint8 byte;
		string line = "";
		while(position++ < size) {
			file.pop(byte);
			if(byte == ASCII_CARRIAGE_RETURN)
				continue;
			if(byte == ASCII_LINE_FEED)
				break;
			line += chr(byte);
		}
		return line;
	}
	
	bool hasNext() {
		return position < size;
	}
	
	uint fileSize() {
		return size;
	}
}

class QuestionParser {
	PlainTextParser@ parser;

	private int lineCount = 1;
	private bool expectAnswer = false;
	private array<Question@> curCategoryQuestions;
	private array<Category@> categories;
	
	private string question = "";
	private string instructions = "";
	private string label = "";
	
	CategorySet@ parse(string filename) {
		@parser = PlainTextParser(filename);
		uint totalBytes = parser.fileSize();
		if(totalBytes == 0) {
			jjConsole("ERROR: " + filename + " appears to be empty or not exist!");
			return @CategorySet(array<Category@>());
		}
		
		while(parser.hasNext()) {
			string line = parser.getLine();
			if(line.length() == 0 || startsWith(line,"%")) {lineCount++; continue;}
			if(startsWith(line,"[") && endsWith(line,"]") && !expectAnswer) { //New Category
				if(curCategoryQuestions.length() > 0 || categories.length > 0) { //If this is not the first category, then the previous one is complete
					categories.insertLast(@Category(label, instructions, curCategoryQuestions));
					curCategoryQuestions.resize(0);
					label = "";
					instructions = "";
				}
				label = trim(line.substr(1, line.length() - 2));
			} else if(startsWith(line,"I:") && !expectAnswer) {
				if(trim(line).length() == 2) {
					jjConsole("LINE " + lineCount + ": You must provide instructions after an 'I:'");
					return @CategorySet(array<Category@>());
				}
				instructions = trim(line.substr(2,line.length()-2));
			} else if(startsWith(line,"Q:") && !expectAnswer) {
				if(trim(line).length() == 2) {
					jjConsole("LINE " + lineCount + ": You must provide a question after a 'Q:'");
					return @CategorySet(array<Category@>());
				}
				question = trim(line.substr(2,line.length()-2));
				expectAnswer = true;
			} else if(startsWith(line,"A:") && expectAnswer) {
				if(trim(line).length() == 2) {
					jjConsole("LINE " + lineCount + ": You must provide an answer after a 'A:'");
					return @CategorySet(array<Category@>());
				}
				string ans = line.substr(2,line.length()-2);
				array<string> parsedAnswers = split(ans, ";");
				array<Answer@> answers;
				for(uint i = 0; i < parsedAnswers.length; i++)
					if(parsedAnswers[i].length() > 0)
						answers.insertLast(@Answer(trim(parsedAnswers[i])));
				Question@ newQuestion = @Question(question, answers);
				if(newQuestion.isValid())
					curCategoryQuestions.insertLast(newQuestion);
				expectAnswer = false;
			} else if(startsWith(line,"[") && endsWith(line,"]") && expectAnswer) {
				jjConsole("LINE " + lineCount + ": category label found but answer expected: " + line);
				return @CategorySet(array<Category@>());
			} else if(startsWith(line,"I:") && expectAnswer) {
				jjConsole("LINE " + lineCount + ": instructions found but answer expected: " + line);
				return @CategorySet(array<Category@>());
			} else if(startsWith(line,"Q:") && expectAnswer) {
				jjConsole("LINE " + lineCount + ": question found but answer expected: " + line);
				return @CategorySet(array<Category@>());
			} else if(startsWith(line,"A:") && !expectAnswer) {
				jjConsole("LINE " + lineCount + ": unexpected answer: " + line);
				return @CategorySet(array<Category@>());
			} else {
				jjConsole("LINE " + lineCount + ": unrecognized syntax: " + line);
				return @CategorySet(array<Category@>());
			}
			
			lineCount++;
		}
		if(curCategoryQuestions.length() > 0 || categories.length > 0) { //EOF reached, so add the last category (if there were any)
			categories.insertLast(@Category(label, instructions, curCategoryQuestions));
		}
		array<Category@> results = categories;
		
		lineCount = 1;
		expectAnswer = false;
		curCategoryQuestions.resize(0);
		categories.resize(0);
	
		question = "";
		instructions = "";
		label = "";

		return @CategorySet(results);
	}
}

class ConfigParser {
	
	void saveConfig() {
	
	}
	
	void loadConfig() {
	
	}
}

class Score {
	private int points;
	private int gamesWon;
	
	Score() {
		points = 0;
		gamesWon = 0;
	}
	
	Score(int points, int gamesWon) {
		this.points = points;
		this.gamesWon = gamesWon;
	}
	
	int getPoints() const {
		return points;
	}
	
	int getGamesWon() const {
		return gamesWon;
	}
	
	int opCmp(Score@ other) const {
		int compare = this.points - other.points;
		return compare == 0 ? this.gamesWon - other.gamesWon : compare;
	}
}

class PlayerScore {
	private string player;
	private Score score;

	PlayerScore(string player, Score score) {
		this.player = player;
		this.score = score;
	}

	string getPlayer() const {
		return player;
	}

	Score getScore() const {
		return score;
	}
	
	int opCmp(PlayerScore@ other) const {
		return score.opCmp(other.score);
	}
}

class ScoreSet {
	private dictionary scores;
	
	private void addPlayer(string playerName) {
		Score@ handle = cast<Score>(scores[playerName]);
		if(handle is null)
			@scores[playerName] = Score();
	}
	
	void addPoints(string playerName, int numPoints) {
		addPlayer(playerName);
		Score playerOldScore;
		scores.get(playerName, playerOldScore);
		scores[playerName] = Score(playerOldScore.getPoints() + numPoints, playerOldScore.getGamesWon());
	}
	
	void addGamesWon(string playerName, int numGames) {
		addPlayer(playerName);
		Score playerOldScore;
		scores.get(playerName, playerOldScore);
		scores[playerName] = Score(playerOldScore.getPoints(), playerOldScore.getGamesWon() + numGames);
	}
	
	Score@ getScore(string playerName) const {
		addPlayer(playerName);
		Score score;
		scores.get(playerName, score);
		return @score;
	}
	
	uint numScores() const {
		return scores.getSize();
	}
	
	void reset() {
		scores.deleteAll();
	}
	
	array<PlayerScore@> sortedByScoreDesc() const {
		array<PlayerScore@> playerScores;
		array<string> playerNames = scores.getKeys();
		for(uint i = 0; i < playerNames.length(); i++) {
			Score score;
			scores.get(playerNames[i], score);
			playerScores.insertLast(@PlayerScore(playerNames[i], score));
		}
		playerScores.sortDesc();
		return playerScores;
	}
	
	string getLeadingPlayer() const {
		array<PlayerScore@> leaders = sortedByScoreDesc();
		if(leaders.length() == 0)
			return "-Nobody-";
		else
			return leaders[0].getPlayer();
	}
	
	array<PlayerScore@> getFirstPlaceTies() const {
		array<PlayerScore@> result;
		array<PlayerScore@> leaders = sortedByScoreDesc();
		if(leaders.length() > 0) {
			int topScore = leaders[0].getScore().getPoints();
			for(uint i = 0; i < leaders.length(); i++) {
				if(leaders[i].getScore().getPoints() == topScore) {
					result.insertLast(leaders[i]);
				} else
					break;
			}
		}
		return result;
	}
}

class PersistentScoreSet {
	private ScoreSet scores;

	PersistentScoreSet() {
		loadScores();
		array<PlayerScore@> ps = sortedByScoreDesc();
	}
	
	void addPoints(string playerName, int numPoints) {
		scores.addPoints(playerName, numPoints);
		saveScores();
	}
	
	void addGamesWon(string player, int gamesWon) {
		scores.addGamesWon(player, gamesWon);
		saveScores();
	}
	
	string getLeadingPlayer() const {
		return scores.getLeadingPlayer();
	}

	array<PlayerScore@> sortedByScoreDesc() const {
		return scores.sortedByScoreDesc();
	}
	
	Score getScore(string player) const {
		return scores.getScore(player);
	}
	
	uint numScores() const {
		return scores.numScores();
	}
	
	void saveScores() {
		jjSTREAM file(SCORES_FILE);
		file.clear();
		array<PlayerScore@> playerScores = scores.sortedByScoreDesc();
		for(uint i = 0; i < playerScores.length; i++) {
			file.push(playerScores[i].getPlayer());
			file.push(playerScores[i].getScore().getPoints());
			file.push(playerScores[i].getScore().getGamesWon());
		}
		file.save(SCORES_FILE);
	}
	
	void loadScores() {
		jjSTREAM file(SCORES_FILE);
		while(!file.isEmpty()) {
			string playerName;
			int points;
			int gamesWon;
			file.pop(playerName);
			file.pop(points);
			file.pop(gamesWon);
			scores.addPoints(playerName, points);
			scores.addGamesWon(playerName, gamesWon);
		}
	}
}

class Timer {
	int inter, startTime, end;
	bool running;
	
	Timer() {
		this.running = false;
	}
	
	Timer start(int interval) {
		if(interval < 0)
			return start();
		this.inter = interval;
		this.startTime = jjGameTicks;
		this.end = this.startTime + this.inter;
		this.running = true;
		return this;
	}
	
	Timer start() {
		this.startTime = jjGameTicks;
		this.running = true;
		this.end = -1;
		this.inter = -1;
		return this;
	}
	
	Timer stop() {
		this.running = false;
		return this;
	}
	
	bool isFinished() const {
		if(!this.running) return false;
		if(this.end == -1) return false;
		return jjGameTicks >= this.end;
	}
	
	uint elapsedTime() const {
		return jjGameTicks-this.startTime;
	}
	
	int endTime() const {
		return end;
	}
	
	int remainingTime() const {
		if(this.interval() == -1) return -1;
		return this.interval() - this.elapsedTime();
	}
	
	int interval() const {
		return this.inter;
	}
	
	Timer reset() {
		start(this.inter);
		return this;
	}
	
	bool isStarted() const {
		return this.running;
	}
}

class MessagePrinter {
	private Timer@ timer = @Timer();
	private array<string> toPrint;
	private uint32 interval;
	private uint curMessage;
	
	MessagePrinter(array<string> toPrint, uint32 interval) {
		this.toPrint = toPrint;
		this.interval = interval;
		curMessage = 0;
		timer.start();
	}
	
	void update() {
		if(interval == 0)
			while(!isDone())
				print();
		if(curMessage == 0) print();
		if(!isDone() && timer.elapsedTime() > interval) print();
	}
	
	private void print() {
		sendChat(toPrint[curMessage++]);
		timer.reset();
	}

	bool isDone() {
		return curMessage >= toPrint.length();
	}

	array<string> getStrings() {
		return toPrint;
	}
}

class TriviaGame {
	private uint roundNum;
	private uint questionNum;
	private ScoreSet@ scores;
	private CategorySet@ categorySet;
	private Category@ curCategory;
	private Question@ curQuestion;
	
	TriviaGame() {
		initialize();
		@scores = @ScoreSet();
		QuestionParser@ parser = @QuestionParser();
		@categorySet = @parser.parse(QUESTION_FILE);
		if(categorySet.getCategories().length() > 0) {
			@categorySet = @categorySet.shuffle();
			nextCategory();
		}
	}
	
	private void initialize() {
		roundNum = 0;
		questionNum = 0;
		@scores = null;
		@categorySet = null;
		@curCategory = null;
		@curQuestion = null;
	}
	
	uint getRoundNum() const {return roundNum;}
	
	uint getQuestionNum() const {return questionNum;}
	
	ScoreSet@ getScores() const {return scores;}
	
	Category@ getCurCategory() const {return curCategory;}
	
	Question@ getCurQuestion() const {return curQuestion;}
	
	bool hasCurCategory() const {return curCategory !is null;}
	
	bool hasCurQuestion() const {return curQuestion !is null;}
	
	bool isActive() const {return roundNum > 0;}
	
	Category@ nextCategory() {
		@curCategory = @categorySet.retrieveNextCategory();
		if(hasCurCategory()) {
			roundNum++;
			@curCategory = curCategory.shuffle();
		}
		return curCategory;
	}
	
	Question@ nextQuestion() {
		@curQuestion = @curCategory.retrieveNextQuestion();
		if(hasCurQuestion())
			questionNum++;
		return curQuestion;
	}
	
	void endQuestion() {
		@curQuestion = null;
	}
	
	void endGame() {
		initialize();
	}
}

class ChatMessage {
	private jjPLAYER@ player;
	private string message;
	
	ChatMessage(jjPLAYER@ player, string message) {
		@this.player = @player;
		this.message = message;
	}
	
	jjPLAYER@ getPlayer() {
		return player;
	}

	string getMessage() {
		return message;
	}
}

class StateManager {
	private State@ curState;
	
	void changeState(State@ newState) {
		if(newState !is null) {
			@curState = @newState;
			newState.onEnter();
		}
	}
	
	State@ getState() {
		return curState;
	}
}

interface State {
	void onEnter();
	void doState(ChatMessage@ message);
}

class BaseState : State {
	void onEnter() {}

	void doState(ChatMessage@ message) {
		if(message is null) return;
		if(enableDebugLogging) jjDebug("BASE::doState");
		string name = message.getPlayer().name;
		if(toLower(message.getMessage()) == "@score") {
			if(game !is null && game.isActive()) {
				sendChat(name + "'s current score is: " + game.getScores().getScore(name).getPoints());
			}
		} else if(toLower(message.getMessage()) == "@stop" && isAdmin(message.getPlayer())) {
			stateManager.changeState(@StoppedState());
		} else if(toLower(message.getMessage()) == "@top") {
			array<PlayerScore@> leaders = overallScores.sortedByScoreDesc();
			int numTop = leaders.length() < 3 ? leaders.length() : 3;
			for(int i = 0; i < numTop; i++) {
				Score score = leaders[i].getScore();
				string player = leaders[i].getPlayer();
				sendChat((i+1) + ". " + player + ", points: " + score.getPoints() + ", wins:" + score.getGamesWon());
			}
		} else if(startsWith(toLower(message.getMessage()),"@whois ")) {
			array<string> splitString = split(message.getMessage()," ");
			
			int index;
			if(splitString.length > 1) {
				index = parseInt(splitString[1]) - 1;
			}
			array<PlayerScore@> leaders = overallScores.sortedByScoreDesc();
			if(index < 0) return;
			if(uint(index) >= leaders.length()) {
				sendChat("Nobody is " + (index+1) + " yet.");
				return;
			}
			Score score = leaders[index].getScore();
			string player = leaders[index].getPlayer();
			sendChat((index+1) + ". " + player + ", points: " + score.getPoints() + ", wins:" + score.getGamesWon());
			
		} else if(toLower(message.getMessage()) == "@rank") {
			string player = message.getPlayer().name;
			array<PlayerScore@> leaders = overallScores.sortedByScoreDesc();
			uint num = 0;
			bool found = false;
			Score@ score = null;
			for(; num < leaders.length(); num++) {
				if(leaders[num].getPlayer() == player) {
					@score = leaders[num].getScore();
					found = true;
					break;
				}
			}
			if(!found) {
				game.getScores().getScore(player);
				score = overallScores.getScore(player);
			}
			sendChat((num+1) + ". " + player + ", points: " + score.getPoints() + ", wins:" + score.getGamesWon());
		} else if(startsWith(toLower(message.getMessage()),"@numrounds ") && isAdmin(message.getPlayer())) {
			array<string> splitString = split(message.getMessage()," ");
			
			int num;
			if(splitString.length > 1) {
				num = parseInt(splitString[1]);
				if(num <= 0) return;
			}
			config.numRounds = num;
			config.save();
			jjConsole("Number of rounds has been set to " + num, true);
		} else if(startsWith(toLower(message.getMessage()),"@numqs ") && isAdmin(message.getPlayer())) {
			array<string> splitString = split(message.getMessage()," ");
			
			int num;
			if(splitString.length > 1) {
				num = parseInt(splitString[1]);
				if(num <= 0) return;
			}
			config.questionsPerRound = num;
			config.save();
			jjConsole("Questions per round has been set to " + num, true);
		} else if(startsWith(toLower(message.getMessage()),"@numhints ") && isAdmin(message.getPlayer())) {
			array<string> splitString = split(message.getMessage()," ");
			
			int num;
			if(splitString.length > 1) {
				num = parseInt(splitString[1]);
				if(num < 0) return;
			}
			config.numHints = num;
			config.save();
			jjConsole("Hints per question has been set to " + num, true);
		}
	}
}

class StoppedState : BaseState, State {
	private MessagePrinter@ printer;
	
	void doState(ChatMessage@ message)  {
		BaseState::doState(message);
		if(printer !is null) printer.update();
		if(message is null) return;
		if(enableDebugLogging) jjDebug("StoppedState::doState");
		if(toLower(message.getMessage()) == "@start") {
			@game = @TriviaGame();
			if(!game.hasCurCategory()) {
				jjConsole("ERROR: Game cannot proceed. Check server log for details.", true);
			} else {
				stateManager.changeState(@QuestionState());
			}
		}
	}

	void onEnter() {
		array<string> list();
		list.insertLast(CHAT_PREFIX + "Type @start to begin trivia.");
		@printer = @MessagePrinter(list, MESSAGE_PRINT_WAIT);
	}
}

class EndGameState : BaseState, State {
	private Timer@ timer = @Timer();

	void doState(ChatMessage@ message)  {
		BaseState::doState(message);
		if(timer.elapsedTime() > MESSAGE_PRINT_WAIT)
			stateManager.changeState(@StoppedState());
	}

	void onEnter() {
		array<PlayerScore@> firstPlace = game.getScores().getFirstPlaceTies();
		if(firstPlace.length() <= 0 || firstPlace[0].getScore().getPoints() == 0) {
			sendChat("Nobody wins!");
			return;
		}
		if(firstPlace.length() > 1) {
			sendChat("Game ends in a " + firstPlace.length() + "-way tie");
			for(uint i = 0; i < firstPlace.length(); i++) {
				PlayerScore@ winner = @firstPlace[i];
				overallScores.addGamesWon(winner.getPlayer(),1);
				sendChat("|||" + winner.getPlayer() + ": " + winner.getScore().getPoints());
			}
		} else {
			PlayerScore@ winner = @firstPlace[0];
			overallScores.addGamesWon(winner.getPlayer(),1);
			sendChat("|||" + winner.getPlayer() + " wins the game with " + winner.getScore().getPoints() + " points!");
		}
		game.endGame();
		timer.start();
	}
}

enum QuestionSubState {
	INIT,
	PRINTING_INSTRUCTIONS,
	WAITING_TO_START,
	PRINTING_QUESTION,
	WAITING_FOR_ANSWER,
	WAITING_TO_ASK
}

class QuestionState : BaseState, State {
	private MessagePrinter@ printer;
	private Timer@ timer = @Timer();
	private uint hintsGiven = 0;
	private QuestionSubState subState = QuestionSubState::INIT;
	private uint questionCount = 0;
	private uint totalQuestionTime;
	
	QuestionState() {
		if(enableDebugLogging) jjDebug("QuestionState constructor called");
	}
	
	void onEnter()  {
		if(enableDebugLogging) jjDebug("QuestionState::onEnter");
		subState = QuestionSubState::PRINTING_INSTRUCTIONS;
		array<string> list();
		list.insertLast("ROUND " + game.getRoundNum() + " begins now.");
		list.insertLast("The topic is: " + game.getCurCategory().getLabel());
		if(game.getCurCategory().getInstructions() != "")
			list.insertLast(game.getCurCategory().getInstructions());
		@printer = @MessagePrinter(list, MESSAGE_PRINT_WAIT);
		if(enableDebugLogging) jjDebug("QuestionState::onEnter - end of onEnter");
	}

	void doState(ChatMessage@ message) {
		BaseState::doState(message);
		if(message !is null && message.getMessage() == "@skipq" && isAdmin(message.getPlayer()) && game.hasCurQuestion()) {
			askQuestion();
		} else if(message !is null && message.getMessage() == "@skipc" && isAdmin(message.getPlayer()) && game.hasCurCategory()) {
			game.nextCategory();
			if(game.hasCurCategory())
				stateManager.changeState(@QuestionState());
			else
				stateManager.changeState(@EndGameState());
		} else if(subState == QuestionSubState::PRINTING_INSTRUCTIONS) {
			printer.update();
			if(printer.isDone()) {
				subState = QuestionSubState::WAITING_TO_START;
				timer.reset();
			}
		} else if(subState == QuestionSubState::WAITING_TO_START && timer.elapsedTime() > BETWEEN_QUESTION_WAIT) {
			askQuestion();
		} else if(subState == QuestionSubState::PRINTING_QUESTION) {
			printer.update();
			if(printer.isDone())
				subState = QuestionSubState::WAITING_FOR_ANSWER;
		} else if(subState == QuestionSubState::WAITING_FOR_ANSWER) {
			if(!checkCorrectAnswer(message)) {
				array<Hint@> availableHints = game.getCurQuestion().getHints();
				uint numAvailableHints = availableHints.length();
				uint maxHintsToShow = numAvailableHints < config.numHints ? numAvailableHints : config.numHints;
				if((config.numHints <= 0 && timer.elapsedTime() > QUESTION_TIME_NO_HINTS) ||
				   (config.numHints > 0  && timer.elapsedTime() > TIME_PER_HINT * (maxHintsToShow + 1))) {
					sendChat("Nobody got it!");
					subState = QuestionSubState::WAITING_TO_ASK;
					timer.reset();
					game.endQuestion();
				} else if(config.numHints > 0 && timer.elapsedTime() > TIME_PER_HINT * (hintsGiven + 1)) {
					sendChat("HINT: " + availableHints[hintsGiven++].toString());
				}
			}
		} else if(subState == QuestionSubState::WAITING_TO_ASK && timer.elapsedTime() > BETWEEN_QUESTION_WAIT) {
			askQuestion();
		}
	}
	
	private bool checkCorrectAnswer(ChatMessage@ message)  {
		if(subState != QuestionSubState::WAITING_FOR_ANSWER || message is null || !game.hasCurQuestion()) 
			return false;
		Answer@ matchingAnswer = game.getCurQuestion().checkAnswer(message.getMessage());
		if(matchingAnswer !is null) {
			awardPoints(message, matchingAnswer.toString());
			return true;
		}
		return false;
	}
	
	void awardPoints(ChatMessage@ message, string response) {
		string name = message.getPlayer().name;
		sendChat(name + " is correct! The answer was:");
		sendChat("|||" + response);
		game.getScores().addPoints(name, 10);
		overallScores.addPoints(name, 10);
		if(showScoresEachQuestion)
			sendChat("Their new score is: " + game.getScores().getScore(name).getPoints());
		subState = QuestionSubState::WAITING_TO_ASK;
		timer.reset();
		game.endQuestion();
	}
	
	void askQuestion() {
		if(enableDebugLogging) jjDebug("QuestionState::askQuestion");
		bool nextRound = false;
		if(questionCount < config.questionsPerRound) {
			subState = QuestionSubState::WAITING_FOR_ANSWER;
			game.nextQuestion();
			if(!game.hasCurQuestion())
				nextRound = true;
		} else 
			nextRound = true;
		if(nextRound) {
			onEndRound();
			return;
		}
		questionCount++;
		displayQuestion(game.getCurQuestion());
		totalQuestionTime = TIME_PER_HINT * (game.getCurQuestion().getHints().length() + 1);
		hintsGiven = 0;
		timer.reset();
		if(enableDebugLogging) jjDebug("QuestionState::askQuestion - end of askQuestion");
	}
	
	void displayQuestion(Question question) {
		sendChat("|||Question " + game.getQuestionNum() + ":");
		sendChat("|||" + question.toString());
	}

	void onEndRound() {
		if(enableDebugLogging) jjDebug("QuestionState::onEndRound");
		game.nextCategory();
		if(!game.hasCurCategory() || game.getRoundNum() >= config.numRounds+1) {
			stateManager.changeState(@EndGameState());
			return;
		}
		stateManager.changeState(@QuestionState());
	}
}

//Hooks
void onLevelLoad() {
	if(jjIsServer) {
		@config = @Config();
		@overallScores = @PersistentScoreSet();
		@stateManager = @StateManager();
		stateManager.changeState(@StoppedState());
	}
}

void onChat(int clientID, string &in stringReceived, CHAT::Type chatType) {
	if(jjIsServer) {
		jjPLAYER@ player = @null;
		for(uint i = 0; i < 32; i++) {
			if(jjPlayers[i].isActive && jjPlayers[i].clientID == clientID)
				@player = @jjPlayers[i];
		}
		chatMessages.insertLast(@ChatMessage(player, stringReceived));
	}
}

void onMain() {
	if(jjIsServer) {
		ChatMessage@ curMessage = null;
		if(chatMessages.length() > 0) {
			@curMessage = @chatMessages[0];
			chatMessages.removeAt(0);
		}
		stateManager.getState().doState(curMessage);
	}
}

bool onLocalChat(string &in stringReceived, CHAT::Type chatType) {
	if(toLower(stringReceived) == "@help") {
		jjPrint("Trivia mutator help!");
		jjPrint("@help  - displays this help text");
		jjPrint("@start - begins the trivia game");
		jjPrint("@score - shows your score in this game");
		jjPrint("@rank  - shows your total points and wins");
		jjPrint("@top   - shows the top 3 players");
		jjPrint("@whois - shows the player with the given rank");
		if(isAdmin(jjLocalPlayers[0])) {
			jjPrint("----ADMIN ONLY COMMANDS----");
			jjPrint("@stop - stops the game");
			jjPrint("@skipq - skips the current question");
			jjPrint("@skipc - skips the current category");
			jjPrint("@numqs - sets the number of questions per round");
			jjPrint("@numrounds - sets the number of rounds per game");
			jjPrint("@numhints - sets the number of hints in a question");
		}
	}
	return false;
}