Main Page   Namespace List   Class Hierarchy   Alphabetical List   Compound List   File List   Namespace Members   Compound Members  

DieRoll.cpp

00001 //  DieRoll - finger it out
00002 //  Copyright (C) 1999-2002 rusty@sgi.com & stephan@wanderinghorse.net
00003 //
00004 //  This program is free software; you can redistribute it and/or
00005 //  modify it under the terms of the GNU General Public License
00006 //  as published by the Free Software Foundation; either version 2
00007 //  of the License, or (at your option) any later version.
00008 //
00009 //  This program is distributed in the hope that it will be useful,
00010 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
00011 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00012 //  GNU General Public License for more details.
00013 //
00014 //  You should have received a copy of the GNU General Public License
00015 //  along with this program; if not, write to the Free Software
00016 //  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
00017 
00018 #include <fun/DieRoll.h>
00019 #include <fun/Serializer.h>
00020 #include <fun/Deserializer.h>
00021 //#include <string.h>
00022 #include <assert.h>
00023 #include <qdict.h>
00024 #include <qregexp.h>
00025 #include <iostream>
00026 #include <stdlib.h>  //  for rand()
00027 
00028 namespace fun {
00029 
00030 //  USE_DIEHASH is turned off because I wasn't sure it was actually
00031 //  useful.  (In the test program, a DieRoll is created, and used, and then
00032 //  deleted before the next is created.)  If we find ourselves creating a
00033 //  bunch of copies of the same DieRoll (unlikely, I think), then we may
00034 //  want to turn on hashing, so that multiple attempts to load "d20" just
00035 //  get the first instance out of the hash.
00036 //
00037 //  Something else we could do to encourage shared instances is to never
00038 //  delete instances, even when the refcount goes to 0, and just leave it
00039 //  in the hash.  If we do that, we should probably add some static method
00040 //  which goes through the hash and cleans up every instance with a
00041 //  refcount of 0.
00042 //
00043 //  But hashing them probably doesn't save us anything anyway; SolidPlastic
00044 //  subclasses are all pretty small.
00045 #define USE_DIEHASH 0
00046 
00047 //  Macros for incrementing & decrementing ref counts, & deleting
00048 //  when it's decremented to 0.
00049 #if USE_DIEHASH
00050 #define SP_USE(sp) if (sp && !(sp->refcount++)) { diehash.insert(sp->toString(), sp); }
00051 #define SP_UNUSE(sp) if (sp && (!--sp->refcount)) { diehash.remove(sp->toString()); delete sp; }
00052 static QDict<SolidPlastic> diehash;
00053 #else
00054 #define SP_USE(sp) if (sp) { sp->refcount++; }
00055 #define SP_UNUSE(sp) if (sp && (!--sp->refcount)) { delete sp; }
00056 #endif
00057 
00058 static QValueList<PluginDieRollParser> parsers;
00059 
00060 //////////////////////////////////////////////////////////////////////
00061 //
00062 //  base SolidPlastic stuff
00063 //
00064 SolidPlastic::SolidPlastic() : refcount(0) { }
00065 
00066 SolidPlastic::~SolidPlastic()
00067 {
00068     assert(refcount == 0);
00069 }
00070 
00071 //  Default useless version which ambitious subclasses can override
00072 QValueList<DieRoll::WeightedValue>
00073 SolidPlastic::distribution()
00074 {
00075     return QValueList<DieRoll::WeightedValue>();
00076 }
00077 
00078 //////////////////////////////////////////////////////////////////////
00079 //
00080 //  SimpleDieRoll handles simple rolls consisting of one or more dice
00081 //  of the same type, such as d6 or 3d6.  If the number of sides is 1
00082 //  (what would that be, a marble?), it can be used as a constant.
00083 //
00084 class SimpleDieRoll : public SolidPlastic
00085 {
00086     public:
00087     SimpleDieRoll(int dice, int sides);
00088 //  ~SimpleDieRoll();
00089     QString toString() const;
00090     int roll() const;
00091     int min() const;
00092     int max() const;
00093     bool isFlat() const;
00094     QValueList<DieRoll::WeightedValue> distribution();
00095 
00096     private:
00097     int dice;
00098     int sides;
00099     QString str;
00100     QValueList<DieRoll::WeightedValue> dist;  // set lazily, if at all
00101 };
00102 
00103 SimpleDieRoll::SimpleDieRoll(int d, int s) : dice(d), sides(s)
00104 {
00105     if( ! ((sides > 0) && (dice > 0)) ) return;
00106     if (sides > 1)
00107     {
00108         if (dice > 1) str.append(QString::number(dice));
00109         str.append("d");
00110         str.append(QString::number(sides));
00111     }
00112     else str.append(QString::number(dice));
00113 }
00114 
00115 //SimpleDieRoll::~SimpleDieRoll() { }
00116 
00117 QString
00118 SimpleDieRoll::toString() const
00119 {
00120     return str;
00121 }
00122 
00123 int
00124 SimpleDieRoll::roll() const
00125 {
00126     //  special-case this one...
00127     if (sides == 1) return dice;
00128     int result = 0;
00129     for(int i = 0; i < dice; ++i)
00130     {
00131         result += ((rand() % sides) + 1);
00132     }
00133     return result;
00134 }
00135 
00136 int
00137 SimpleDieRoll::min() const
00138 {
00139     return dice;
00140 }
00141 
00142 int
00143 SimpleDieRoll::max() const
00144 {
00145     return dice * sides;
00146 }
00147 
00148 bool
00149 SimpleDieRoll::isFlat() const
00150 {
00151     return ((dice == 1) || (sides == 1));
00152 }
00153 
00154 QValueList<DieRoll::WeightedValue>
00155 SimpleDieRoll::distribution()
00156 {
00157     if (!dist.isEmpty()) return dist;
00158     DieRoll::WeightedValue wv;
00159     if (sides == 1)
00160     {
00161         wv.weight =  1.0;
00162         wv.value = dice;
00163         dist.append(wv);
00164     }
00165     else if (dice == 1)
00166     {
00167         wv.weight =  1.0 / (double)sides;
00168         for (int i = 1; i <= sides; ++i)
00169         {
00170             wv.value = i;
00171             dist.append(wv);
00172         }
00173     }
00174     else
00175     {
00176         NOT_DONE("SimpleDieRoll::distribution() doesn't handle multiple dice!");
00177     }
00178     return dist;
00179 }
00180 
00181 //////////////////////////////////////////////////////////////////////
00182 //
00183 //  CompoundDieRoll handles more complex die rolls made up of different
00184 //  die types (such as d8+d12), die rolls with modifiers (such as 3d6+3
00185 //  or 2d6*1000), and combinations of other complex die rolls (such as
00186 //  d8+d12-1).
00187 //
00188 class CompoundDieRoll : public SolidPlastic
00189 {
00190     public:
00191     //  op should be '+', '-', or '*'
00192     CompoundDieRoll(SolidPlastic *d1, SolidPlastic *d2, int op);
00193     ~CompoundDieRoll();
00194     QString toString() const;
00195     int roll() const;
00196     int min() const;
00197     int max() const;
00198     bool isFlat() const;
00199 //  QValueList<DieRoll::WeightedValue> distribution();
00200 
00201     private:
00202     int doOp(int arg1, int arg2) const;
00203     SolidPlastic *d1;
00204     SolidPlastic *d2;
00205     int op;
00206     QString str;
00207 };
00208 
00209 CompoundDieRoll::CompoundDieRoll(SolidPlastic *ad1, SolidPlastic *ad2, int aop)
00210     : d1(ad1), d2(ad2), op(aop)
00211 {
00212     assert(d1 && d2);
00213     assert((op == '+') || (op == '-') || (op == '*'));
00214     SP_USE(d1)
00215     SP_USE(d2)
00216     str = d1->toString();
00217     str.append(op);
00218     str.append(d2->toString());
00219 }
00220 
00221 CompoundDieRoll::~CompoundDieRoll()
00222 {
00223     SP_UNUSE(d1)
00224     SP_UNUSE(d2)
00225 }
00226 
00227 QString
00228 CompoundDieRoll::toString() const
00229 {
00230     return str;
00231 }
00232 
00233 int
00234 CompoundDieRoll::roll() const
00235 {
00236     return doOp(d1->roll(), d2->roll());
00237 }
00238 
00239 int
00240 CompoundDieRoll::min() const
00241 {
00242     if(op == '-') return d1->min() - d2->max();
00243     return doOp(d1->min(), d2->min());
00244 }
00245 
00246 int
00247 CompoundDieRoll::max() const
00248 {
00249     if(op == '-') return d1->max() - d2->min();
00250     return doOp(d1->max(), d2->max());
00251 }
00252 
00253 bool
00254 CompoundDieRoll::isFlat() const
00255 {
00256     if(!d1->isFlat() || !d2->isFlat()) return false;
00257     return d1->min() == d1->max() || d2->min() == d2->max();
00258 }
00259 
00260 int
00261 CompoundDieRoll::doOp(int arg1, int arg2) const
00262 {
00263     if(op == '+') return arg1 + arg2;
00264     if(op == '-') return arg1 - arg2;
00265     if(op == '*') return arg1 * arg2;
00266     int badOpShouldHaveBeenHandledInCtor = 0;
00267     assert(badOpShouldHaveBeenHandledInCtor);
00268     return 0;  //  shouldn't get reached
00269 }
00270 
00271 //////////////////////////////////////////////////////////////////////
00272 //
00273 //  OK, the real DieRoll stuff
00274 //
00275 
00276 NAMESPACE_INSTANTIATOR(fun, DieRoll);
00277 
00278 //  This is here for compatibility, so that DieRolls which were serialized
00279 //  before DieRoll was moved to the "fun" namespace can still be loaded.
00280 extern "C" { LoadableClass *newDieRoll() { return new fun::DieRoll; } }
00281 
00282 extern std::ostream & operator<<(std::ostream &os, const DieRoll &d)
00283 {
00284     os << d.toString();
00285     return os;
00286 }
00287 
00288 DieRoll::DieRoll()
00289 {
00290     dice = NULL;
00291 }   
00292 
00293 DieRoll::DieRoll(const DieRoll &other)
00294 {
00295     dice = other.dice;
00296     SP_USE(dice)
00297 }
00298 
00299 DieRoll::~DieRoll()
00300 {
00301     SP_UNUSE(dice)
00302 }
00303 
00304 DieRoll &
00305 DieRoll::operator=(const DieRoll &other)
00306 {
00307     SP_UNUSE(dice)
00308     dice = other.dice;
00309     SP_USE(dice)
00310     return *this;
00311 }
00312 
00313 void
00314 DieRoll::serialize(Serializer &ser) const
00315 {
00316     ser.setSerializableClass("fun::DieRoll");
00317     ser.putString("dice", toString());
00318 }
00319 
00320 void
00321 DieRoll::deserialize(const Deserializer &ser)
00322 {
00323     *this = fromString(ser.getString("dice"));
00324 }
00325 
00326 int
00327 DieRoll::roll() const
00328 {
00329     return dice ? dice->roll() : 0;
00330 }
00331 
00332 int
00333 DieRoll::min() const
00334 {
00335     return dice ? dice->min() : 0;
00336 }
00337 
00338 int
00339 DieRoll::max() const
00340 {
00341     return dice ? dice->max() : 0;
00342 }
00343 
00344 bool
00345 DieRoll::isFlat() const
00346 {
00347     return dice ? dice->isFlat() : true;
00348 }
00349 
00350 QValueList<DieRoll::WeightedValue>
00351 DieRoll::distribution()
00352 {
00353     return dice ? dice->distribution() : QValueList<WeightedValue>();
00354 }
00355 
00356 QString
00357 DieRoll::toString() const
00358 {
00359     return dice ? dice->toString() : QString("error");
00360 }
00361 
00362 //  the private constructor.
00363 DieRoll::DieRoll(SolidPlastic *sp) : dice(sp)
00364 {
00365     SP_USE(dice);
00366 }
00367 
00368 //DieRoll
00369 //DieRoll::getDice(int dice, int sides)
00370 //{
00371 //  //  Rather than creating new instances, you may want to see if an
00372 //  //  instance with the same (x)d(y) specifier already exists, and
00373 //  //  return a reference to it?
00374 //NOT_DONE("DieRoll::getDice() should attempt to get dieroll from hash!");
00375 //  return DieRoll(new SimpleDieRoll(dice, sides));
00376 //}
00377 //
00378 //DieRoll
00379 //DieRoll::getDice(int dice, int sides, int mod)
00380 //{
00381 //  //  Rather than creating new instances, you may want to see if an
00382 //  //  instance with the same (x)d(y)+(z) specifier already exists, and
00383 //  //  return a reference to it?
00384 //NOT_DONE("DieRoll::getDice() should attempt to get dieroll from hash!");
00385 //  if (mod == 0) return DieRoll(new SimpleDieRoll(dice, sides));
00386 //  if(mod < 0) return DieRoll(new CompoundDieRoll(new SimpleDieRoll(dice, sides), new SimpleDieRoll(-mod, 1), '-'));
00387 //  return DieRoll(new CompoundDieRoll(new SimpleDieRoll(dice, sides), new SimpleDieRoll(mod, 1), '+'));
00388 //}
00389 
00390 static QRegExp reOperator("[-+*]");
00391 //Qt 3.0 has subexpressions?
00392 //static QRegExp reSimpleDieRoll("^(\\d*)[dD](\\d+)$");
00393 //static QRegExp reModifier("^([-+]?)(\\d*)$");
00394 static QRegExp reSimpleDieRoll("^\\d*[dD]\\d+$");
00395 static QRegExp reD("[dD]");
00396 static QRegExp reModifier("^[-+]?\\d*$");
00397 
00398 static SolidPlastic *
00399 parse(const QString &diestr)
00400 {
00401   QString str = diestr;
00402   str = str.replace( QRegExp( "\\s" ), "" ); // spaces aren't properly handled by the parser (stephan)
00403 #if USE_DIEHASH
00404     //  First let's see if there's already an instance matching this string.
00405     SolidPlastic *sp = diehash.find(str);
00406     if (sp != NULL) return sp;
00407 #endif
00408     //  See if a plugin parser knows what to do with this.
00409     if (!parsers.isEmpty())
00410     {
00411         QValueList<PluginDieRollParser>::Iterator it;
00412         for (it = parsers.begin(); it != parsers.end(); ++it)
00413         {
00414             PluginDieRollParser p = (*it);
00415             SolidPlastic *sp = (*p)(str);
00416             if (sp) return sp;
00417         }
00418     }
00419     //  List of parsers didn't handle it, so let's try it ourselves.
00420     int pos;
00421     if ((pos = str.find(reOperator)) != -1)
00422     {
00423         //  We have an operator!  Split the string and recurse.
00424         QString s1 = str.left(pos);
00425         QString s2 = str.right(str.length() - pos - 1);
00426         int op = str[pos].unicode();
00427         SolidPlastic *sp1, *sp2 = NULL;
00428         sp1 = parse(s1);
00429         if (sp1) sp2 = parse(s2);
00430         if (!sp2) return NULL;
00431         return new CompoundDieRoll(sp1, sp2, op);
00432     }
00433     if (str.find(reSimpleDieRoll) != -1)
00434     {
00435         pos = str.find(reD);  //  we "know" it's found
00436         QString ds = str.left(pos);
00437         QString ss = str.right(str.length() - pos - 1);
00438         int dice = ds.length() > 0 ? ds.toInt() : 1;
00439         int sides = ss.toInt();
00440         return new SimpleDieRoll(dice, sides);
00441     }
00442     if ((pos = str.find(reModifier)) != -1)
00443     {
00444         return new SimpleDieRoll(str.toInt(), 1);
00445     }
00446     //  Hmmm, we couldn't parse the string.
00447     return NULL;
00448 }
00449 
00450 
00451 DieRoll
00452 DieRoll::fromString(const QString &src)
00453 {
00454     return DieRoll(parse(src));
00455 }
00456 
00457 void
00458 DieRoll::addDieRollParser(PluginDieRollParser dp)
00459 {
00460     if (dp) parsers.append(dp);
00461 }
00462 
00463 void
00464 DieRoll::removeDieRollParser(PluginDieRollParser dp)
00465 {
00466     if (dp) parsers.remove(dp);
00467 }
00468 
00469 void
00470 DieRoll::testRolls(const DieRoll &d, int rolls, bool showSubs)
00471 {
00472     cout << "Rolling " << d << " " << dec << rolls << " times" << endl;
00473     cout << "  " << d << ": min " << dec << d.min() << ", max " << d.max() << ", isFlat " << d.isFlat() << endl;
00474 
00475     //  Let's not roll broken dice 10,000 times, OK?
00476     if (d.min() == d.max())
00477     {
00478         cout << "(You're kidding, right?  You rolled a " << dec << d.min() << ".)" << endl;
00479         return;
00480     }
00481 
00482     int bucketmod = d.min() * -1;
00483     //  Really buckets should be the number of elements in
00484     //  d.distribution(), but most dice don't implement that.  This
00485     //  approach will "work", but will be horribly wasteful for dice like
00486     //  d6*1000.
00487     int buckets = d.max() - d.min() + 1;
00488     cout << dec << buckets << " buckets, " << bucketmod << " bucketmod" << endl;
00489     int *bucket = new int[buckets];
00490     if (!bucket)
00491     {
00492         cerr << "DieRoll::testRolls((): out of memory, bailing" << endl;
00493         return;
00494     }
00495     int *prevbucket = showSubs ? new int[buckets * buckets] : NULL;
00496     if (showSubs && !prevbucket)
00497     {
00498         cerr << "DieRoll::testRolls(): not enough memory to calculate subsequent results" << endl;
00499         showSubs = false;
00500     }
00501     int i, j, prev = -1000;
00502 
00503     //  initialize buckets
00504     memset((void *)bucket, 0, sizeof(int) * buckets);
00505     if (prevbucket) memset((void *)prevbucket, 0, sizeof(int) * buckets * buckets);
00506 
00507     //  roll dice
00508     for(i = 0; i < rolls; ++i)
00509     {
00510         j = d.roll();
00511         bucket[j + bucketmod] += 1;
00512         if(showSubs && (prev + bucketmod >= 0)) prevbucket[((prev + bucketmod) * buckets) + j + bucketmod] += 1;
00513         prev = j;
00514     }
00515 
00516     //  report results
00517     cout << "Results after " << dec << rolls << " rolls:" << endl;
00518     int sum = 0;
00519     for(i = 0; i < buckets; ++i)
00520     {
00521         sum += bucket[i];
00522         cout << "  " << dec << (i - bucketmod) << ": " << bucket[i] << "\t" << ((float)(bucket[i]) / rolls * 100) << "%\t" << ((float)sum / rolls * 100) << "%" << endl;
00523     }
00524 
00525     if(showSubs)
00526     {
00527         cout << "Subsequent results:" << endl;
00528         for(i = 0; i < buckets; ++i)
00529         {
00530             cout << "  " << dec << (i - bucketmod) << ":" << endl;
00531             for(j = 0; j < buckets; ++j)
00532             {
00533                 int pb = (i * buckets) + j;
00534                 cout << "    " << dec << (j - bucketmod) << ": " << prevbucket[pb] << " (" << (bucket[i] ? ((float)(prevbucket[pb]) / bucket[i] * 100) : 0) << " %)" << endl;
00535             }
00536         }
00537     }
00538     delete [] bucket;
00539     if (prevbucket) delete [] prevbucket;
00540 }
00541 
00542 }  //  namespace fun

Generated on Mon Aug 11 14:06:55 2003 for libfunutil by doxygen1.2.18