Name | Author | Game Mode | Rating | |||||
---|---|---|---|---|---|---|---|---|
Trivia | sAlAmAnDeR | Mutator | 9.3 |
#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;
}
Jazz2Online © 1999-INFINITY (Site Credits). We have a Privacy Policy. Jazz Jackrabbit, Jazz Jackrabbit 2, Jazz Jackrabbit Advance and all related trademarks and media are ™ and © Epic Games. Lori Jackrabbit is © Dean Dodrill. J2O development powered by Loops of Fury and Chemical Beats.
Eat your lima beans, Johnny.