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 );
}
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 );
}