PDA

View Full Version : I made a dice-roller! It will handle any (sensible) dice input you give it v1.1



Capt Spanner
2014-03-13, 11:03 AM
Hi all,

I made a dice roller in C++. You may find it useful, as it will handle very complex dice rolling situations.

To use it:

1. Grab the source code, and compile it in your favourite compiler.
2. Run it from the command line, passing in dice rolls as arguments.

So if you compile it as DiceRoller.exe, you can call it like this:

diceRoller.exe d20

And it will roll a d20.

It supports the following ways of rolling dice:

Single dice: dS = roll an S-sided dice.
Multiple dice: NdS = roll N S-sided dice, and output the total.
A subset of dice: NdSkX = roll N S-sided dice, and keep the top X. (Whitespace is ignored, so this could be "N d S k X"). e.g.: 4d6k3, for your D&D stats.
Count successes: NdSsW: roll N S-sided dice, and output the number of them that score W or more.
Count successes, with a re-roll threshold: NdSsWrR: As above, but anything die scoring R or above is also re-rolled. So rolling six dice in World of Darkness would be 6d10s7r10.

Modifiers work: you can apply multipliers: x6 (or *6, if you prefer) would multiply a score by 6. + and - also work.

You can also nest rolls within each other. To roll 2d6 and multiply the results: d6 x d6. A contested d20 roll would be d20s(d20).

(Oh, yeah, feel free to abuse brackets too.)

You can therefore do really silly rolls like 2d3d4d5d6s4r5.

(Roll 2d3. Count the pips, and roll that many d4s. Count the pips again, and roll that many d5s. Count the pips a final time and roll that many d6s. Count the ones showing 4+, and then reroll the ones showing 5+.)

So: anyone who wants to try this out, feel free. Send me feature requests, and bugs. (Let's see if anyone can break it.)

All the best,

Capt Spanner.

EDIT: I made some changes based on the feedback below.

"Decimal arguments for number of dice or size of dice - converts to an integer by ignoring the decimal point. "
- It now rounds down at a comma or full-stop. Use spaces to group digits in large numbers.

"Does not accept arguments involving all of its possible utilities in order - the roll fails."
- Everything after the "d" is now parsed together. Ordering no longer matters, and you can use any combination of "r", "k" and "s".

"... doesn't like parentheses ..."
- It now supports the following bracket pairs: (), {}. [], <>. Actually, you can mix styles: { stuff >, and it will work, but you will get a warning telling you about it because I reckon you probably didn't mean to do this.

"Must be run again for each dice roll..."
- You can now keep adding new rolls for it compute. Stop the program with -exit.

"Does not come with a help or instructions menu, as far as I've been able to figure out."
- Help instructions now far more prominent.





// Notes: This should compile cross platform. You need to support some C++11 features, though.
#include <iostream>
#include <random>
#include <string>
#include <vector>
#include <sstream>
#include <algorithm>
#include <stdexcept>
#include <functional>
#include <cctype>
#include <numeric>
#include <ctime>

using namespace std;

#ifndef NDEBUG
bool debugOutput = true;
#else
bool debugOutput = false;
#endif

default_random_engine generator;

void debugPrint( string in ) {
if( debugOutput )
cout << "Debug: " << in << endl;
}

typedef vector<string> StringList;

StringList initialize( int argc , char* argv[] );
StringList getNewArgs();

// Output warnings if brackets are messed up.
bool checkBrackets( const string& in );

// Parse the dice roll.
int parseArg( const string& in );

// Handle -help
void handleHelp()
{
debugPrint( "Argument found: '-help'" );
cout << "Dice Roller, by Captain Spanner\n"
"Usage dice_roller [args]\n"
"Use as many args as you wish, separated by semi-colons: ';'\n"
"Valid args:\n"
"-debug: turns on debug messages.\n"
"-ndebug: turns off debug messages.\n"
"-exit: quit after all the dice rolls have been handled.\n"
"-help: displays this message.\n"
"-seed=VAL: where VAL is any number. Positive, only, please.\n\tSeeds the RNG with a specified value. "
"If this isn't set, the system\n\tclock is used to seed the generator at startup.\n"
"A dice roll. Supported syntax includes:\n"
"\tNdX: Roll N X-sided dice. This can be suffixed with:\n"
"\t\tkX: Keep the top X results\n"
"\t\trX: Reroll all dice that score above X, and add the results to\n\t\t\tthe current roll.\n"
"\t\tsX: Instead of totalling the score, count the number of dice\n\t\t\tscoring X or more.\n"
"\tMultiplication: use 'x' or '*' to multiply two dice rolls, or by\n\t\ta constant.\n"
"\tAddition / subtraction: '+' and '-' are both supported.\n"
"\tBrackets work as expected. (), {}, [] and <> are all equivelent.\n"
"If numbers are omitted, they are assumed to be 1, so d6 = 1d6, 2d6- = 2d6 - 1\n"
"Do opposed rolls with either subtraction: d12 - d20, or the s-suffix: d12sd20\n";
}

// Handle -debug
void handleDebug()
{
debugOutput = true;
debugPrint( "Debug output turned on." );
}

// Handle -ndebug
void handleNDebug()
{
debugPrint( "Turning off debug output." );
debugOutput = false;
}

bool containsArgument( const StringList& args , const string& arg )
{
return end(args) != find( begin(args),end(args) , arg );
}

void checkAndHandleSeed( const StringList& args );

int main( int argc , char* argv[] )
{
cout << "Dice Roller, by Captain Spanner.\n\t"
"Use '-help' for help.\n\n";

// Stuff all the arguments together, for processing. The argv is split by spaces, but I want to ignore spaces and split by semi-colons.
auto argList = initialize( argc , argv );

bool exitFlag = false;

while( true )
{
if( containsArgument( argList , "-debug" ) )
handleDebug();
if( containsArgument( argList , "-ndebug" ) )
handleNDebug();
if( containsArgument( argList , "-exit" ) )
{
cout << "Exiting after once done.\n";
exitFlag = true;
}
if( containsArgument( argList , "-help" ) )
handleHelp();

checkAndHandleSeed( argList );

for( const auto& arg : argList )
{
if( checkBrackets( arg )
&& arg != "-debug"
&& arg != "-ndebug"
&& arg != "-exit"
&& arg != "-help"
&& arg.substr(0,5) != "-seed" )
{
try
{
const auto val = parseArg(arg);
cout << "Roll: " << arg << " - Result: " << val << endl;
} catch( exception ex )
{
cout << "Failed roll: " << arg << "\n\tReason: " << ex.what() << endl;
}
}
}

if( exitFlag )
break;
// Get new arguments.
cout << "\nInput next rolls here, or '-help' to see usage.\nNew rolls: ";

argList = getNewArgs();
}
return 0;

}

static const string OPEN_BRACKETS = "({[<";
static const string CLOSE_BRACKETS = ")}]>";

bool checkBrackets( const string& in )
{
vector<int> bracketStack;
for( auto ch : in )
{
// Find open bracket.
const auto openIt = find( begin(OPEN_BRACKETS),end(OPEN_BRACKETS) , ch );
if( openIt != end(OPEN_BRACKETS) )
{
bracketStack.push_back( distance( begin(OPEN_BRACKETS) , openIt ) );
continue;
}

// Find close bracket.
const auto closeIt = find( begin(CLOSE_BRACKETS),end(CLOSE_BRACKETS) , ch );
if( closeIt != end(CLOSE_BRACKETS) )
{
if( bracketStack.empty() )
{
cout << "Error: In " << in << " a bracket is closed without being opened.\n";
return false;
}
const auto offset = distance( begin(CLOSE_BRACKETS) , closeIt );
const auto openIndex = *(bracketStack.rbegin());
if( openIndex != offset )
{
cout << "Warning: In " << in << " bracket opened with '" << OPEN_BRACKETS[openIndex]
<< "' closed with '" << *closeIt << "'. Have you got your brackets right?\n";
}
}
}
return true;
}

struct OpAndArgs
{
bool found;
char op;
pair<string,string> args;
};

enum class Direction : char
{
LeftToRight,
RightToLeft
};

OpAndArgs parseOperator( const string& in , const string& operators , Direction dir = Direction::LeftToRight );

typedef pair<bool,int> ParseResult;

ParseResult parseEmptyString( const string& in );
ParseResult parseBrackets( const string& in );
ParseResult parseAddSub( const string& in );
ParseResult parseMul( const string& in );
ParseResult parseDiceOp( const string& in );
ParseResult parseConstants( const string& in );

// Try to parse using a certain technique. If the parse succeeds, return that
// value, else just continue. Also, yeah, I know, #defines suck.
#ifdef TRY_PARSE
static_assert( false , "TRY_PARSE is already defined." );
#endif
#define TRY_PARSE( ParseFunc , String ) do {\
const ParseResult res = ParseFunc(String);\
if( res.first ) {\
return res.second;\
}\
} while(false)

int parseArg( const string& in )
{
debugPrint( "Parsing argument: \"" + in + '"' );
try
{
// Step 0: Check for the empty string:
TRY_PARSE( parseEmptyString , in );

// Step 1: Process brackets
TRY_PARSE( parseBrackets , in );

// Step 2: Handle +/- arguments.
TRY_PARSE( parseAddSub , in );

// Step 3: Process multiplication.
TRY_PARSE( parseMul , in );

// Step 4: Process the 'd' operator. Process these backwards so 2d3d4 would roll 2 d3s, and then roll that many d4s.
TRY_PARSE( parseDiceOp , in );

// Step 5: Process constants
TRY_PARSE( parseConstants , in );

return -1;
}
catch( runtime_error ex )
{
stringstream message;
message << ex.what() << "\n\tPart of expression \"" << in << "\".";
throw runtime_error( message.str() );
}
}

// Just in case something else tries to define this.
#undef TRY_PARSE

ParseResult parseEmptyString( const string& in )
{
if( in.empty() )
{
debugPrint( "Empty expression. Interpreting value as 1." );
return make_pair( true , 1 );
} else
return make_pair( false, 0 );
}

bool isContainedIn( char in , const string& str )
{
return end(str) != find( begin(str),end(str) , in );
}

ParseResult parseBrackets( const string& in )
{
if( isContainedIn( *(in.begin()) , OPEN_BRACKETS ) && isContainedIn( *(in.rbegin()) , CLOSE_BRACKETS ) )
{
bool breakFound = false;
int bracketsOpen = 0;
for( size_t i(0) ; i<in.size()-1 ; ++i )
{
if( isContainedIn( in[i] , OPEN_BRACKETS ) )
{
++bracketsOpen;
} else if( isContainedIn( in[i] , CLOSE_BRACKETS ) )
{
--bracketsOpen;
}

if( bracketsOpen == 0 ) {
breakFound = true;
break;
}
}
if( !breakFound ) {
debugPrint( "Stripping brackets from front and back." );
return make_pair( true , parseArg( in.substr(1,in.size()-2) ) );
}
}
return make_pair( false , 0 );
}

ParseResult parseAddSub( const string& in )
{
const auto plusMinusResult = parseOperator( in , "+-" , Direction::RightToLeft );
if( plusMinusResult.found )
{
const auto leftVal = parseArg(plusMinusResult.args.first);
const auto rightVal = parseArg(plusMinusResult.args.second) * (plusMinusResult.op == '+' ? 1 : -1 );
stringstream msg;
msg << "Calulating: " << leftVal << " + " << rightVal;
debugPrint( msg.str() );
return make_pair( true , leftVal + rightVal );
} else
return make_pair( false , 0 );
}


ParseResult parseMul( const string& in )
{
const auto multiplyResult = parseOperator( in , "*x" );
if( multiplyResult.found )
{
const auto leftVal = parseArg(multiplyResult.args.first);
const auto rightVal = parseArg(multiplyResult.args.second);
stringstream msg;
msg << "Calulating: " << leftVal << " x " << rightVal;
debugPrint( msg.str() );
return make_pair( true , leftVal * rightVal );
} else
return make_pair( false, 0 );
}

struct DiceParseResult
{
int no_dice , no_sides , no_keep , success_val , reroll_val;
DiceParseResult() : no_dice(-1) , no_sides(-1) , no_keep(-1) , success_val(-1) , reroll_val(-1) {}
};

void handleSuffixes( DiceParseResult* , const string& suffixes , char previousOp );
int getRollResult( DiceParseResult in );

ParseResult parseDiceOp( const string& in )
{
const auto d_opRes = parseOperator( in , "d" , Direction::RightToLeft );
if( d_opRes.found )
{
DiceParseResult dpr;
dpr.no_dice = parseArg( d_opRes.args.first );
handleSuffixes( &dpr , d_opRes.args.second , 'd' );
return make_pair( true , getRollResult( dpr ) );
} else
return make_pair( false , 1 );
}

ParseResult parseConstants( const string& in )
{
int val(0);
for( auto ch : in )
{
if( isdigit(ch) )
{
val *= 10;
val += ch - '0';
} else
{
switch( ch )
{
case '.':
case ',':
cout << "Warning: Rounding " << in << " to " << val << endl;
return make_pair( true , val );
default:
// Error
{
stringstream message;
message << "Unidentified value: '" << ch << "' found in expression \"" << in << "\".";
throw runtime_error( message.str() );
}
break;
}
}
}
stringstream message;
message << "Found constant value: " << val << " from \"" << in << "\".";
debugPrint( message.str() );
return make_pair( true , val );
}

void handleSuffixes( DiceParseResult* dpr , const string& suffixes , char previousOp )
{
const auto parseResult = parseOperator( suffixes , "ksr" );
const auto leftArg = parseArg( parseResult.found ? parseResult.args.first : suffixes );
switch( previousOp )
{
case 'd':
if( dpr->no_sides != -1 ) {
throw logic_error( "Should not be able to set no_sides twice.\n" );
}
dpr->no_sides = leftArg;
break;
case 'k':
if( dpr->no_keep != -1 )
{
throw runtime_error( "Tried to set number of dice to keep more than once." );
}
dpr->no_keep = leftArg;
break;
case 's':
if( dpr->success_val != -1 )
{
throw runtime_error( "Tried to set success threshold more than once." );
}
dpr->success_val = leftArg;
break;
case 'r':
if( dpr->reroll_val != -1 )
{
throw runtime_error( "Tried to set reroll threshold more than once." );
}
dpr->reroll_val = leftArg;
break;
default:
break;
}

if( parseResult.found )
{
handleSuffixes( dpr , parseResult.args.second , parseResult.op );
}
}

vector<int> rollDice( int no_dice , int no_sides , int keep );


pair<string,string> splitString( const string& in , int pos )
{
pair<string,string> rv;
rv.first = in.substr(0,pos);
rv.second = in.substr(pos+1,in.size());
return rv;
}

int findIgnoringBrackets( const string& in , function<bool(char)> criteria , bool reverseDirection )
{
int pos = reverseDirection ? in.size() - 1 : 0;
int openBrackets = 0;
auto openBracket = [&]() { ++openBrackets; };
auto closeBracket = [&]()
{
if( --openBrackets < 0 )
{
stringstream message;
message << "Error parsing expression \"" << in << "\". " <<
(reverseDirection ? "Unopened" : "Unclosed") << "bracket " <<
(reverseDirection ? "closed" : "opened") << " at position " << pos << ".";
debugPrint( message.str() );
throw runtime_error( message.str() );
}
};
// Get the position of the operator
do
{
// Handle brackets.
if( isContainedIn( in[pos] , OPEN_BRACKETS ) )
{
reverseDirection ? closeBracket() : openBracket();
} else if( isContainedIn( in[pos] , CLOSE_BRACKETS ) )
{
reverseDirection ? openBracket() : closeBracket();
}

if( openBrackets == 0 && criteria( in[pos] ) )
break;
reverseDirection ? --pos : ++pos;
} while( pos < int(in.size()) && pos >= 0 );
if( openBrackets > 0 )
{
throw runtime_error( (reverseDirection ? "Unclosed" : "Unopened") + string(" brackets in expression \"") + in + "\"." );
}
return pos;
}

OpAndArgs parseOperator( const string& in , const string& operators , Direction dir )
{
auto criteria = [&]( char in ) {
return end(operators) != find( begin(operators),end(operators) , in );
};

const auto splitPoint = findIgnoringBrackets( in , criteria , dir == Direction::RightToLeft );

OpAndArgs rv;
if( splitPoint > -1 && static_cast<size_t>(splitPoint) < in.size() ) // Check before && guarantees splitPoint is +ve, so no undefined behaviour.
{
rv.found = true;
rv.op = in[splitPoint];
rv.args = splitString( in , splitPoint );
stringstream message;
message << "Operator found in " << in << ". '" << rv.op << "' at position " << splitPoint + 1 << endl // Humans don't zero-index.
<< "\tLeft arg: \"" << rv.args.first << "\" Right arg: \"" << rv.args.second << "\"";
debugPrint( message.str() );
} else
{
rv.op = 0;
rv.found = false;
}
return rv;
}

string to_lower( const string& in )
{
string res;
res.reserve( in.size() );
transform( begin(in),end(in) , back_inserter(res) , tolower );
return res;
}

StringList splitArguments( const string& in )
{
stringstream merged( in );
StringList rv;
while( merged.good() )
{
string temp;
getline( merged , temp , ';' );
rv.push_back( temp );
}
for( const auto& arg : rv )
{
debugPrint( "Argument: " + arg );
}
return rv;
}

StringList initialize( int argc , char* argv[] )
{
unsigned long seed = static_cast<unsigned long>(time(nullptr));
stringstream merged;
for( int i=1 ; i<argc ; ++i ) // argv[0] is the program path.
{
merged << argv[i];
}

debugPrint( "Seeding with value " + to_string( seed ) );
generator.seed(seed);

// Reset the stream.
return splitArguments( merged.str() );
}

StringList getNewArgs()
{
string newArgs;
getline( cin , newArgs );

// Remove whitespace.
stringstream withWhitespace( newArgs );
stringstream withoutWhiteSpace;
while( withWhitespace.good() )
{
string scratch;
withWhitespace >> scratch;
withoutWhiteSpace << scratch;
}
return splitArguments( withoutWhiteSpace.str() );
}

void checkAndHandleSeed( const StringList& args )
{
for( const auto& arg : args )
{
if( arg.substr(0,6) == "-seed=" )
{
unsigned long seed = static_cast<unsigned long>( parseConstants( arg.substr(6) ).second );
debugPrint( "Seeding with value " + to_string(seed) );
generator.seed( seed );
return;
}
}
return;
}

vector<int> rollDice( int no_dice , int no_sides , int keep )
{
if( no_dice <= 0 )
{
stringstream message;
message << no_dice << " requested. Returning no values." ;
debugPrint( message.str() );
}
vector<int> result;
result.reserve( no_dice );
uniform_int_distribution<> die(1 , no_sides );
stringstream message;
message << "Rolling " << no_dice << " " << no_sides << "-sided dice. ";
if( keep < no_dice )
{
message << "Keeping " << keep << ". ";
}
message << "Results:";
for( int i(0) ; i<no_dice ; ++i )
{
const int value = die(generator);
message << " " << value;
result.push_back( value );
}
if( keep < no_dice )
{
nth_element( begin(result) , begin(result) + keep , end(result) , greater<int>() );
result.erase( begin(result)+keep , end(result) );
message << "\nReturning: ";
for( auto roll : result )
{
message << " " << roll;
}
}
debugPrint( message.str() );
return result;
}

int getRollResult( DiceParseResult in )
{
if( in.no_dice <= 0 ) {
return 0;
}

// Clean a couple of automatic settings.
stringstream prerollmsg;
prerollmsg << "Rolling " << in.no_dice << "d" << in.no_sides;
if( in.no_keep == -1 )
{
in.no_keep = in.no_dice;
} else
{
prerollmsg << "k" << in.no_keep;
}
if( in.success_val != -1 )
{
prerollmsg << "s" << in.success_val;
}
if( in.reroll_val == -1 )
{
in.reroll_val = in.no_sides + 1;
} else
{
prerollmsg << "r" << in.reroll_val;
}
debugPrint( prerollmsg.str() );
const auto rolls = rollDice( in.no_dice , in.no_sides , in.no_keep );
const auto result =
in.success_val == -1
? accumulate( begin(rolls),end(rolls) , 0 )
: count_if( begin(rolls),end(rolls) , [&](int roll) { return roll >= in.success_val; } );

const auto rerolls = count_if( begin(rolls),end(rolls) , [&](int roll) { return roll >= in.reroll_val; } );
stringstream postrollmsg;
postrollmsg << "Result: " << result << " and " << rerolls << " rerolls.";
debugPrint( postrollmsg.str() );
in.no_dice = rerolls;
return result + getRollResult( in );
}

Socksy
2014-03-13, 03:09 PM
Do you have a link?

Try entering fractions. Surds. Imaginary or complex numbers. Irrational numbers. Test it viciously!

The Glyphstone
2014-03-13, 03:59 PM
sqrt (-3di)/0de

Amidus Drexel
2014-03-13, 11:36 PM
Ooh, sweet. I'll gladly test this out for you! :smallcool:

EDIT:
Things your dice roller does not support:

1) Decimal arguments for number of dice or size of dice - converts to an integer by ignoring the decimal point. (i.e. 2.7d2.7 becomes 27d27)

2) Does not accept arguments involving all of its possible utilities in order - the roll fails. (i.e. 5d10k4s5r8 returns a failure). It is impossible to keep only a certain amount of dice and then count/reroll some when inputted in that manner.

3) Either the program does not support it as I am trying to use it, or Windows PowerShell simply doesn't like parentheses being used in that manner. Other uses of brackets also fail (hell, I even tried angle brackets). This prevents the use of rolling a number of dice, then keeping a variable number of dice dependent on another die roll. (i.e 5d10k[1d4], 5d10k{1d4}, and 5d10k<1d4> all fail). When inputted without brackets, the program operates from left to right, producing different results (but ones consistent and sensible with its logic). I will attempt to resolve this by running the program elsewhere and seeing if the problem persists.



------------------------------
Minor technical annoyances:

1) Must be run again for each dice roll - it does not accept additional inputs to run through the function again. Also, requires an additional hit of the enter key to exit the program, and any typing done before that produces no results.

2) Does not come with a help or instructions menu, as far as I've been able to figure out.

Capt Spanner
2014-03-14, 05:40 AM
Thanks, this is all really useful.


Things your dice roller does not support:

1) Decimal arguments for number of dice or size of dice - converts to an integer by ignoring the decimal point. (i.e. 2.7d2.7 becomes 27d27)

I'm not sure how I'd calculate a d2.7, or how to roll 2.7 dice.

In any case, this was to support very large dice: in Europe the full stop is used that way: 1.000 = one thousand. I made the choice just to ignore everything. Rounding is probably a better way to do this, though. (I'll have it round down).


2) Does not accept arguments involving all of its possible utilities in order - the roll fails. (i.e. 5d10k4s5r8 returns a failure). It is impossible to keep only a certain amount of dice and then count/reroll some when inputted in that manner.

I haven't thought of that usage. Consider it a feature request. :smallsmile: It may take a bit of time, as I'll probably have to rejig the parser somewhat.


3) Either the program does not support it as I am trying to use it, or Windows PowerShell simply doesn't like parentheses being used in that manner. Other uses of brackets also fail (hell, I even tried angle brackets). This prevents the use of rolling a number of dice, then keeping a variable number of dice dependent on another die roll. (i.e 5d10k[1d4], 5d10k{1d4}, and 5d10k<1d4> all fail). When inputted without brackets, the program operates from left to right, producing different results (but ones consistent and sensible with its logic). I will attempt to resolve this by running the program elsewhere and seeing if the problem persists.

Having it recognise angle, square, brace and regular brackets would be no problem. I'll just have the input cleaner convert them. It will mean output messages are slightly changed, though.


1) Must be run again for each dice roll - it does not accept additional inputs to run through the function again. Also, requires an additional hit of the enter key to exit the program, and any typing done before that produces no results.

I forgot to get rid of the "press enter" thing! That'll go.

You can chain multiple dice rolls together by separating them with semi-colons. I will switch it wait for more arguments to be given, until it receives an exit command, though. It shouldn't be too hard to make this change.


2) Does not come with a help or instructions menu, as far as I've been able to figure out.

Put in the argument "-help" for a help menu. I'll have it suggest this whenever it finds an input it doesn't like.


sqrt (-3di)/0de

*Twitches*

Capt Spanner
2014-03-14, 10:42 AM
Okay - I've now made all those changes.

Round two of trying to break my code. :smallbiggrin:

sebsmith
2014-03-14, 07:48 PM
It didn't seem like there was a way to have numbers count as multiple successes, like in exalted, so that might be useful.

Amidus Drexel
2014-03-14, 10:19 PM
Okay - I've now made all those changes.

Round two of trying to break my code. :smallbiggrin:

I'll probably get to breaking this here before I go to bed, I think. Stand by for complaints. :smallbiggrin: :smalltongue:

EDIT: Okay, that didn't happen. I'll get to it as soon as I can.

EDIT2: Nothing seems broken so far. I'm a bit bogged down with uni work right now, so I can't do but so much else on this at the moment, although if I find time, I'll work on it later.