Script: mw_quiz_inc

//::///////////////////////////////////////////////
//:: mw_quiz_inc -- MeaningWave guide quiz engine.
//::
//:: Drives a data-driven quiz from the per-guide banks in mw_quiz_data. For each
//:: run we draw MW_QUIZ_DRAW questions at random (no repeats) from the guide's
//:: 20-question bank, and shuffle the four answer choices into random display
//:: slots. Pass MW_QUIZ_PASS of MW_QUIZ_DRAW to unlock. Correctness is tracked in
//:: per-PC LocalInts keyed by guide name; nothing is stored in the dialogue tree.
//::
//:: Runtime tokens (module-global). They are populated by the reply that LEADS
//:: INTO each question node -- the "begin" reply draws question 1, and each answer
//:: reply scores the current question then draws the next -- so the tokens are set
//:: before the following entry renders. This is the same ordering the forge relies
//:: on (a reply's Actions Taken runs before the entry it links to is displayed;
//:: see forge_stg_* -> bellnius_smith.dlg). Setting them on the question entry's
//:: own Actions Taken instead is unreliable (the entry text can render against the
//:: PREVIOUS load), which desynced the displayed question from the scored answer.
//::   7000 = question prompt
//::   7001..7004 = the four answer choices (shuffled)
//::   7005 = "Question N of 5" progress label
//::
//:: Per-PC state (guide = full name, e.g. "jocko"):
//::   mw_active            (String) guide whose quiz is in progress
//::   mw_<guide>_n         questions asked so far this run (0..DRAW)
//::   mw_<guide>_score     running correct count
//::   mw_<guide>_used      bitmask of bank indices already drawn this run
//::   mw_<guide>_cor       display slot (1..4) holding the correct answer now
//::
//:: Multiplayer note: SetCustomToken is module-global, so two PCs mid-quiz could
//:: momentarily see each other's choice text. This is the accepted limitation of
//:: every token system in the module (merit, forge, boss board); the window is a
//:: single synchronous node render. No mitigation attempted.
//:://////////////////////////////////////////////

#include "mw_unlock_inc"
#include "mw_quiz_data"

const int MW_QUIZ_DRAW = 5;   // questions asked per run
const int MW_QUIZ_PASS = 4;   // correct answers needed to unlock
const int MW_QUIZ_OPTS = 4;   // answer choices per question

// Return the idx-th '~'-delimited field of a packed quiz row.
string MW_Field(string sRow, int idx)
{
    int i;
    int p;
    for (i = 0; i < idx; i++)
    {
        p = FindSubString(sRow, "~");
        if (p < 0) return "";
        sRow = GetSubString(sRow, p + 1, GetStringLength(sRow) - p - 1);
    }
    p = FindSubString(sRow, "~");
    if (p < 0) return sRow;              // last field
    return GetSubString(sRow, 0, p);
}

// Begin a fresh run: clear all per-run state and mark this guide active.
// The "begin" reply (mw_q_start) calls this and then MW_QuizLoad, so question 1's
// tokens are set before the first question entry renders.
void MW_QuizStart(object oPC, string sGuide)
{
    DeleteLocalInt(oPC, "mw_" + sGuide + "_n");
    DeleteLocalInt(oPC, "mw_" + sGuide + "_score");
    DeleteLocalInt(oPC, "mw_" + sGuide + "_used");
    DeleteLocalInt(oPC, "mw_" + sGuide + "_cor");
    SetLocalString(oPC, "mw_active", sGuide);
}

// Draw the next unused random question, shuffle its choices into tokens 7001-7004,
// record which slot is correct, and advance the progress counter/token.
void MW_QuizLoad(object oPC, string sGuide)
{
    int nTotal = MW_QCount(sGuide);
    int nUsed  = GetLocalInt(oPC, "mw_" + sGuide + "_used");

    // Pick a random index whose bit is not yet set. Guaranteed to terminate:
    // we only ever draw MW_QUIZ_DRAW (< nTotal) questions per run.
    int idx;
    do { idx = Random(nTotal); } while (nUsed & (1 << idx));
    SetLocalInt(oPC, "mw_" + sGuide + "_used", nUsed | (1 << idx));

    string sRow = MW_QRow(sGuide, idx);
    SetCustomToken(7000, MW_Field(sRow, 0));

    // Fisher-Yates shuffle of answer indices {0,1,2,3}; answer 0 is the correct
    // one (field 1 of the row). Use PC scratch ints as a 4-slot array.
    int i;
    for (i = 0; i < MW_QUIZ_OPTS; i++)
        SetLocalInt(oPC, "mwperm" + IntToString(i), i);
    for (i = MW_QUIZ_OPTS - 1; i >= 1; i--)
    {
        int j  = Random(i + 1);
        int vi = GetLocalInt(oPC, "mwperm" + IntToString(i));
        int vj = GetLocalInt(oPC, "mwperm" + IntToString(j));
        SetLocalInt(oPC, "mwperm" + IntToString(i), vj);
        SetLocalInt(oPC, "mwperm" + IntToString(j), vi);
    }

    int nCor = 1;
    for (i = 0; i < MW_QUIZ_OPTS; i++)
    {
        int ai = GetLocalInt(oPC, "mwperm" + IntToString(i));
        SetCustomToken(7001 + i, MW_Field(sRow, ai + 1));   // fields 1..4 are answers
        if (ai == 0) nCor = i + 1;                          // slot holding the correct answer
        DeleteLocalInt(oPC, "mwperm" + IntToString(i));
    }
    SetLocalInt(oPC, "mw_" + sGuide + "_cor", nCor);

    int n = GetLocalInt(oPC, "mw_" + sGuide + "_n") + 1;
    SetLocalInt(oPC, "mw_" + sGuide + "_n", n);
    SetCustomToken(7005, "Question " + IntToString(n) + " of " + IntToString(MW_QUIZ_DRAW));
}

// Score the answer the PC just picked (slot 1..4).
void MW_QuizAnswer(object oPC, string sGuide, int nSlot)
{
    if (sGuide == "") return;
    if (nSlot == GetLocalInt(oPC, "mw_" + sGuide + "_cor"))
        SetLocalInt(oPC, "mw_" + sGuide + "_score",
                    GetLocalInt(oPC, "mw_" + sGuide + "_score") + 1);
}

// Score the pick, then draw the next question so its tokens are populated BEFORE
// the following entry renders. Loading on the answer reply (rather than on the
// question entry itself) is what keeps the displayed question aligned with the
// scored answer. After the final question (_n == MW_QUIZ_DRAW) there is nothing
// left to draw; the reply falls through to the pass/fail entry instead.
void MW_QuizAnswerNext(object oPC, string sGuide, int nSlot)
{
    MW_QuizAnswer(oPC, sGuide, nSlot);
    if (sGuide == "") return;
    if (GetLocalInt(oPC, "mw_" + sGuide + "_n") < MW_QUIZ_DRAW)
        MW_QuizLoad(oPC, sGuide);
}

// Pass check for the final branch (StartingConditional on the pass entry).
int MW_QuizPass(object oPC, string sGuide)
{
    return GetLocalInt(oPC, "mw_" + sGuide + "_score") >= MW_QUIZ_PASS;
}

// Clear all per-run state (called on unlock).
void MW_QuizClear(object oPC, string sGuide)
{
    DeleteLocalInt(oPC, "mw_" + sGuide + "_n");
    DeleteLocalInt(oPC, "mw_" + sGuide + "_score");
    DeleteLocalInt(oPC, "mw_" + sGuide + "_used");
    DeleteLocalInt(oPC, "mw_" + sGuide + "_cor");
}