adopt github pull request #286 - rndmonst()

Eliminate the cache that was supporting rndmonst() and pick a random
monster in a single pass through mons[] via "weighted reservoir
sampling", a term I'm not familiar with.

It had a couple of bugs:  if the first monster examined happened to
be given a weighting of 0, rn2() would divide by 0.  I didn't try
to figure out how to trigger that.  But the second one was easy to
trigger:  if all eligible monsters were extinct or genocided, it
would issue a warning even though the situation isn't impossible.

Aside from fixing those, the rest is mostly as-is.  I included a bit
of formatting in decl.c, moved some declarations to not require C99,
and changed a couple of macros to not hide and duplicate a call to
level_difficulty().

Fixes #286
This commit is contained in:
PatR
2020-02-22 17:40:55 -08:00
parent 5d4b9784ac
commit cbdda9dc9d
10 changed files with 70 additions and 99 deletions

View File

@@ -464,8 +464,6 @@ const struct instance_globals g_init = {
UNDEFINED_VALUES,
/* makemon.c */
{ -1, /* choice_count */
{ 0 } }, /* mchoices */
/* mhitm.c */
UNDEFINED_VALUE, /* vis */

View File

@@ -1453,7 +1453,6 @@ boolean at_stairs, falling, portal;
|| dunlev(&u.uz) < dunlev_reached(&u.uz))
dunlev_reached(&u.uz) = dunlev(&u.uz);
}
reset_rndmonst(NON_PM); /* u.uz change affects monster generation */
/* set default level change destination areas */
/* the special level code may override these */

View File

@@ -215,7 +215,6 @@ const char *drainer; /* cause of death, if drain should be fatal */
pline("%s level %d.", Goodbye(), u.ulevel--);
/* remove intrinsic abilities */
adjabil(u.ulevel + 1, u.ulevel);
reset_rndmonst(NON_PM); /* new monster selection */
} else {
if (drainer) {
g.killer.format = KILLED_BY;
@@ -315,7 +314,6 @@ boolean incr; /* true iff via incremental experience growth */
if (u.ulevelmax < u.ulevel)
u.ulevelmax = u.ulevel;
adjabil(u.ulevel - 1, u.ulevel); /* give new intrinsics */
reset_rndmonst(NON_PM); /* new monster selection */
}
g.context.botl = TRUE;
}

View File

@@ -27,8 +27,6 @@ static boolean FDECL(makemon_rnd_goodpos, (struct monst *,
#define m_initsgrp(mtmp, x, y, mmf) m_initgrp(mtmp, x, y, 3, mmf)
#define m_initlgrp(mtmp, x, y, mmf) m_initgrp(mtmp, x, y, 10, mmf)
#define toostrong(monindx, lev) (mons[monindx].difficulty > lev)
#define tooweak(monindx, lev) (mons[monindx].difficulty < lev)
boolean
is_home_elemental(ptr)
@@ -945,7 +943,6 @@ boolean ghostly;
makeplural(mons[mndx].mname));
}
g.mvitals[mndx].mvflags |= G_EXTINCT;
reset_rndmonst(mndx);
}
return result;
}
@@ -1517,98 +1514,75 @@ struct permonst *
rndmonst()
{
register struct permonst *ptr;
register int mndx, ct;
register int mndx;
int weight, totalweight, selected_mndx, zlevel, minmlev, maxmlev;
boolean elemlevel, upper;
if (u.uz.dnum == quest_dnum && rn2(7) && (ptr = qt_montype()) != 0)
return ptr;
if (g.rndmonst_state.choice_count < 0) { /* need to recalculate */
int zlevel, minmlev, maxmlev;
boolean elemlevel;
boolean upper;
zlevel = level_difficulty();
minmlev = monmin_difficulty(zlevel);
maxmlev = monmax_difficulty(zlevel);
upper = Is_rogue_level(&u.uz); /* prefer uppercase only on rogue level */
elemlevel = In_endgame(&u.uz) && !Is_astralevel(&u.uz); /* elmntl plane */
g.rndmonst_state.choice_count = 0;
/* look for first common monster */
for (mndx = LOW_PM; mndx < SPECIAL_PM; mndx++) {
if (!uncommon(mndx))
break;
g.rndmonst_state.mchoices[mndx] = 0;
}
if (mndx == SPECIAL_PM) {
/* evidently they've all been exterminated */
debugpline0("rndmonst: no common mons!");
return (struct permonst *) 0;
} /* else `mndx' now ready for use below */
zlevel = level_difficulty();
/* determine the level of the weakest monster to make. */
minmlev = zlevel / 6;
/* determine the level of the strongest monster to make. */
maxmlev = (zlevel + u.ulevel) / 2;
upper = Is_rogue_level(&u.uz);
elemlevel = In_endgame(&u.uz) && !Is_astralevel(&u.uz);
/* amount processed so far */
totalweight = 0;
selected_mndx = NON_PM;
for (mndx = LOW_PM; mndx < SPECIAL_PM; ++mndx) {
ptr = &mons[mndx];
if (montooweak(mndx, minmlev) || montoostrong(mndx, maxmlev))
continue;
if (upper && !isupper((uchar) def_monsyms[(int) ptr->mlet].sym))
continue;
if (elemlevel && wrong_elem_type(ptr))
continue;
if (uncommon(mndx))
continue;
if (Inhell && (ptr->geno & G_NOHELL))
continue;
/*
* Find out how many monsters exist in the range we have selected.
* Weighted reservoir sampling: select ptr with a
* (ptr weight)/(total of all weights so far including ptr's)
* probability. For example, if the previous total is 10, and
* this is now looking at acid blobs with a frequency of 2, it
* has a 2/12 chance of abandoning ptr's previous value in favor
* of acid blobs, and 10/12 chance of keeping whatever it was.
*
* This does not bias results towards either the earlier or the
* later monsters: the smaller pool and better odds from being
* earlier are exactly canceled out by having more monsters to
* potentially steal its spot.
*/
for ( ; mndx < SPECIAL_PM; mndx++) { /* (`mndx' initialized above) */
ptr = &mons[mndx];
g.rndmonst_state.mchoices[mndx] = 0;
if (tooweak(mndx, minmlev) || toostrong(mndx, maxmlev))
continue;
if (upper && !isupper((uchar) def_monsyms[(int) ptr->mlet].sym))
continue;
if (elemlevel && wrong_elem_type(ptr))
continue;
if (uncommon(mndx))
continue;
if (Inhell && (ptr->geno & G_NOHELL))
continue;
ct = (int) (ptr->geno & G_FREQ) + align_shift(ptr);
if (ct < 0 || ct > 127)
panic("rndmonst: bad count [#%d: %d]", mndx, ct);
g.rndmonst_state.choice_count += ct;
g.rndmonst_state.mchoices[mndx] = (char) ct;
weight = (int) (ptr->geno & G_FREQ) + align_shift(ptr);
if (weight < 0 || weight > 127) {
impossible("bad weight in rndmonst for mndx %d", mndx);
weight = 0;
}
/* was unconditional, but if weight==0, rn2() < 0 will always fail;
also need to avoid rn2(0) if totalweight is still 0 so far */
if (weight > 0) {
totalweight += weight; /* totalweight now guaranteed to be > 0 */
if (rn2(totalweight) < weight)
selected_mndx = mndx;
}
/*
* Possible modification: if choice_count is "too low",
* expand minmlev..maxmlev range and try again.
*/
} /* choice_count+mchoices[] recalc */
if (g.rndmonst_state.choice_count <= 0) {
/* maybe no common mons left, or all are too weak or too strong */
debugpline1("rndmonst: choice_count=%d", g.rndmonst_state.choice_count);
return (struct permonst *) 0;
}
/*
* Now, select a monster at random.
* Possible modification: if totalweight is "too low" or nothing
* viable was picked, expand minmlev..maxmlev range and try again.
*/
ct = rnd(g.rndmonst_state.choice_count);
for (mndx = LOW_PM; mndx < SPECIAL_PM; mndx++)
if ((ct -= (int) g.rndmonst_state.mchoices[mndx]) <= 0)
break;
if (mndx == SPECIAL_PM || uncommon(mndx)) { /* shouldn't happen */
impossible("rndmonst: bad `mndx' [#%d]", mndx);
if (selected_mndx == NON_PM || uncommon(selected_mndx)) {
/* maybe no common monsters left, or all are too weak or too strong */
if (selected_mndx != NON_PM)
debugpline1("rndmonst returning Null [uncommon 'mndx'=#%d]",
selected_mndx);
return (struct permonst *) 0;
}
return &mons[mndx];
}
/* called when you change level (experience or dungeon depth) or when
monster species can no longer be created (genocide or extinction) */
void
reset_rndmonst(mndx)
int mndx; /* particular species that can no longer be created */
{
/* cached selection info is out of date */
if (mndx == NON_PM) {
g.rndmonst_state.choice_count = -1; /* full recalc needed */
} else if (mndx < SPECIAL_PM) {
g.rndmonst_state.choice_count -= g.rndmonst_state.mchoices[mndx];
g.rndmonst_state.mchoices[mndx] = 0;
} /* note: safe to ignore extinction of unique monsters */
return &mons[selected_mndx];
}
/* decide whether it's ok to generate a candidate monster by mkclass() */
@@ -1687,7 +1661,7 @@ aligntyp atyp;
(or lower) difficulty as preceding candidate (non-zero
'num' implies last > first so mons[last-1] is safe);
sometimes accept it even if high difficulty */
if (num && toostrong(last, maxmlev)
if (num && montoostrong(last, maxmlev)
&& mons[last].difficulty > mons[last - 1].difficulty
&& rn2(2))
break;

View File

@@ -299,7 +299,6 @@ newman()
change_sex();
adjabil(oldlvl, (int) u.ulevel);
reset_rndmonst(NON_PM); /* new monster generation criteria */
/* random experience points for the new experience level */
u.uexp = rndexp(FALSE);

View File

@@ -2107,7 +2107,6 @@ do_class_genocide()
* have G_GENOD or !G_GENO.
*/
g.mvitals[i].mvflags |= (G_GENOD | G_NOCORPSE);
reset_rndmonst(i);
kill_genocided_monsters();
update_inventory(); /* eggs & tins */
pline("Wiped out all %s.", nam);
@@ -2324,7 +2323,6 @@ int how;
} else if (ptr == g.youmonst.data) {
rehumanize();
}
reset_rndmonst(mndx);
kill_genocided_monsters();
update_inventory(); /* in case identified eggs were affected */
} else {