heasm66 / mdlzork

Different versions of original mainframe Zork reconstructed and patched to run under Confusion.
15 stars 6 forks source link

Randomness of the Dungeon Master's quiz #49

Closed eriktorbjorn closed 4 months ago

eriktorbjorn commented 1 year ago

As discussed in https://github.com/heasm66/mdlzork/issues/48

When the Dungeon Master quizzes you, the questions aren't selected quite as randomly as you may first think. The RNG is re-seeded based on your user name and (optionally) the incantation you may have used to skip straight to the endgame. I'm guessing the reasoning was that even with a large number of players, they each get their own customized quiz, making it harder to share clues with others.

With Confusion, the user name is hard-coded:

<DEFINE GXUNAME () "MTRZORK">
<SETG XUNM "MTRZORK">

So every player who plays from the start gets the same three questions (to the extent that they all use the same RNG implementation, at least), and the only way to get other ones is to use an incantation.

While this does match the original behavior for the individual player, should Confusion provide a way to change this? I don't know. What I'll probably end up doing myself, just to check that the other questions work, is to figure out a set of incantations. But I don't know if the RNG is predictable enough for those to then work on other systems. I kind of doubt it.

eriktorbjorn commented 1 year ago

It would be cool if we could emulate the exact random number generation used by MDL. Is https://github.com/PDP-10/its/blob/master/src/mudsys/arith.94 the right place to look? If so, is it really just this?

MFUNCTION RANDOM,SUBR
    ENTRY
    HLRE    A,AB
    CAMGE   A,[-4]      ;At most two arguments to random to set seeds
    JRST    TMA
    JRST    RANDGO(A)
    MOVE    B,VAL2      ;Set second seed
    MOVEM   B,RLOW
    MOVE    A,VAL1      ;Set first seed
    MOVEM   A,RHI
RANDGO: PUSHJ   P,CRAND
    JRST    FINIS

CRAND:  MOVE    A,RHI
    MOVE    B,RLOW
    MOVEM   A,RLOW      ;Update Low seed
    LSHC    A,-1        ;Shift both right one bit
    XORB    B,RHI       ;Generate output and update High seed
    MOVSI   A,TFIX
    POPJ    P,
RHI:    267762113337
RLOW:   155256071112

If so, I guess it shouldn't be that difficult. I don't speak the language, though.

heasm66 commented 1 year ago

Resources of PDP-10 machine language: http://www.hakmem.org/pdp-10.html http://pdp10.nocrew.org/docs/instruction-set/pdp-10.html

Should be possible the rewrite the above code in C with that help.

RHI and RLOW are expressed in octal and represents the high and low of a 72-bit word. This word is shifted right one position (shifting in a zero) and then RHI are XORB (XOR with complement, I think) with RLOW and stored in RHI. RHI is returned.

heasm66 commented 1 year ago

Random-generator converted to C# (desktop application with two text-boxes and a button).

         private void btnRandom_Click(object sender, EventArgs e)
        {
            if (txtRHI.Text.Trim() == "" || txtRLOW.Text.Trim() == "")
            {
                txtRHI.Text =Convert.ToInt64("267762113337", 8).ToString();     // Octal-->Decimal
                txtRLOW.Text = Convert.ToInt64("155256071112", 8).ToString();   // Octal-->Decimal
                return;
            }

            long A = long.Parse(txtRHI.Text);
            long B = long.Parse(txtRLOW.Text);

            if (A < 0) A = A + 0x1000000000;            // Convert 64 bits to 36 bits
            B = (B >> 1);                               // Shift low right one bit
            if (A % 2 != 0) B = B | 0x800000000;        // Shift in bit 0 from high to low
            txtRLOW.Text = A.ToString();                // Update low seed
            A = A ^ B;                                  // XOR
            if (A > 0x7FFFFFFFF) A = A - 0x1000000000;  // Use only 36 bits
            txtRHI.Text = A.ToString();                 // Update high seed / result
        }
eriktorbjorn commented 1 year ago

I tried adapting that to Confusion (I don't have a C# compiler at hand), but I wasn't able to reproduce the series of random numbers given as an example in https://github.com/heasm66/mdlzork/issues/48#issuecomment-1482721909

I'm not sure where I went wrong.

heasm66 commented 1 year ago

Made a quick hack:

MDL_INT RHI = 0267762113337;
MDL_INT RLOW = 0155256071112;

mdl_value_t *mdl_builtin_eval_random(mdl_value_t *form, mdl_value_t *args)
/* SUBR */
{
    mdl_value_t *cursor;
    mdl_value_t *seed1, *seed2;
    MDL_INT rvalue;
    unsigned short rseed[3];

    OARGSETUP(args, cursor);
    OGETNEXTARG(seed1, cursor);
    OGETNEXTARG(seed2, cursor);
    if (cursor) mdl_error("Too many args to RANDOM");
    if (seed1 && seed1->type != MDL_TYPE_FIX)
        mdl_error("RANDOM seeds must be type FIX");
    if (seed2 && seed2->type != MDL_TYPE_FIX)
        mdl_error("RANDOM seeds must be type FIX");
    if (seed1 && seed2)
    {
#ifdef MDL32
        uint64_t tot_seed = ((uint64_t)seed1->v.w << 16) ^ ((uint64_t)seed2->v.w);
        rseed[0] = tot_seed >> 32;
        rseed[1] = tot_seed >> 16;
        rseed[2] = tot_seed;
#else
        // treat the seed as two 36-bit numbers; XOR together for 48 bits
        uint64_t tot_seed = ((MDL_UINT)seed1->v.w << 12) ^ ((MDL_UINT)seed2->v.w);
        rseed[0] = tot_seed >> 32;
        rseed[1] = tot_seed >> 16;
        rseed[2] = tot_seed;
#endif
        seed48(rseed);
        RHI = (MDL_INT)seed1->v.w;
        RLOW = (MDL_INT)seed2->v.w;
    }
    else if (seed1)
    {
#ifdef MDL32
        rseed[0] = seed1->v.w >> 16;
        rseed[1] = seed1->v.w;
        rseed[2] = 1;
#else
        // 36-bit seed
        rseed[0] = seed1->v.w >> 20;
        rseed[1] = seed1->v.w >> 4;
        rseed[2] = seed1->v.w & 0xF;
#endif
        seed48(rseed);
        RHI = (MDL_INT)seed1->v.w;
    }
#ifdef MDL32
    rvalue = mrand48();
#else
    rvalue = (MDL_INT)(((MDL_UINT)mrand48() << 32) ^ ((MDL_UINT)mrand48()));
    MDL_INT A = RHI;
    MDL_INT B = RLOW;
    if (A < 0) A = A + 0x1000000000;
    B = B >> 1;
    if (A % 2 != 0) B = B | 0x800000000;
    RLOW = A;
    A = A ^ B;
    if (A > 0x7ffffffff) A = A - 0x1000000000;
    RHI = A;
    rvalue = A;
#endif
    return mdl_new_fix(rvalue);

}

This test-function produces the same series on both Confusion and ITS/MDL:

<DEFINE TEST ()
        <PRIN1 <RANDOM 359 569>>
        <PRIN1 <RANDOM>> <CRLF>
        <PRIN1 <RANDOM>> <CRLF>
        <PRIN1 <RANDOM>> <CRLF>
        <PRIN1 <RANDOM>> <CRLF>
        <PRIN1 <RANDOM>> <CRLF>
        <PRIN1 <RANDOM>> <CRLF>
        <PRIN1 <RANDOM>> <CRLF>
        <PRIN1 <RANDOM>> <CRLF>
        <PRIN1 <RANDOM>> <CRLF>
        <PRIN1 <RANDOM>> <CRLF>
>
TEST
<TEST>
-34359738245200
17179869429
-17179869039
25769804011
-34359738205
12884902102
30064771207
-6442450708
-12884901713
9663676633
T
eriktorbjorn commented 1 year ago

I think one mistake - possibly one of several - I made was accidentally using an unsigned type for the seeds.

Anyway, if this matches ITS/MDL, I think that would be a great thing to have for the non-MDL32 case. (And come to think of it, since this is a fork, do we even need the MDL32 case if it won't run Zork?) All we need is to add a verb to set the random seed (Infocom called this #RANDOM), and then we could have at least semi-automated regression testing. It might be useful to also have at least the #RECORD / #UNRECORD commands to save a lot of the player's actions.

I don't know about #COMMAND since it's It's already possible to pipe a text file into Confusion. Though at least when I tried it it would complain about an unexpected EOF at the end.

eriktorbjorn commented 1 year ago

RECORD / #UNRECORD would be pretty simple to implement, I think. Here I've piggy-backed on SCRIPT / UNSCRIPT (so error checking isn't as strict as it should be) because I think things get hairy if both of those can be active at the same time.

Whether or not it's useful is, of course, still open to debate.

diff --git a/mdlzork_810722/patched_confusion/dung.mud b/mdlzork_810722/patched_confusion/dung.mud
index 9498983..eac0e14 100644
--- a/mdlzork_810722/patched_confusion/dung.mud
+++ b/mdlzork_810722/patched_confusion/dung.mud
@@ -3744,6 +3744,11 @@ L;"funny verbs"

 <SADD-ACTION "GO-IN" TIME>      ;"funny verb for room actions when entering"

+;"Debug commands"
+
+<SADD-ACTION "#RECO" DO-SCRIPT>
+<SADD-ACTION "#UNRE" DO-UNSCRIPT>
+
 ;"ZORK game commands"

 <SADD-ACTION "BRIEF" BRIEF>
diff --git a/mdlzork_810722/patched_confusion/rooms.mud b/mdlzork_810722/patched_confusion/rooms.mud
index 2df516b..03f1176 100644
--- a/mdlzork_810722/patched_confusion/rooms.mud
+++ b/mdlzork_810722/patched_confusion/rooms.mud
@@ -402,38 +402,48 @@
          <FINISH <>>>
    <FINISH <>>>>

-<DEFINE DO-SCRIPT ("AUX" (CH <>) (UNM ,XUNM) (MUDDLE ,MUDDLE))
-  #DECL ((CH) <OR CHANNEL FALSE> (UNM) STRING (MUDDLE) FIX)
+<DEFINE DO-SCRIPT ("AUX" (CH <>) (UNM ,XUNM) (MUDDLE ,MUDDLE) (ACT "script")
+                         (CACT "Script") (EXT "SCRIPT"))
+  #DECL ((CH) <OR CHANNEL FALSE> (UNM ACT CACT EXT) STRING (MUDDLE) FIX)
+  <COND (<VERB? "#RECO">
+         <SET ACT "record">
+         <SET CACT "Record">
+         <SET EXT "RECORD">)>
   <COND (,MY-SCRIPT
          <DO-UNSCRIPT <>>)>
   <COND (,SCRIPT-CHANNEL
-         <TELL "You are already scripting.">)
+         <TELL "You are already " ,POST-CRLF .ACT "ing.">)
         (<AND
           <OR <G? .MUDDLE 100>
               <AND <SET CH <OPEN "READ" ".FILE." "(DIR)" "DSK" .UNM>>
                    <=? <10 .CH> .UNM>
                    <CLOSE .CH>>>
-          <SET CH <OPEN "PRINT" "ZORK" "SCRIPT" "DSK" .UNM>>>
+          <SET CH <OPEN "PRINT" "ZORK" .EXT "DSK" .UNM>>>
          <PUT <TOP ,INCHAN> 1 (.CH)>
-         <PUT <TOP ,OUTCHAN> 1 (.CH)>
+         <COND (<VERB? "SCRIP">
+                <PUT <TOP ,OUTCHAN> 1 (.CH)>)>
          <SETG SCRIPT-CHANNEL .CH>
          <COND (<L? ,MUDDLE 100>
-                <TELL "Scripting to " ,POST-CRLF ,XUNM ";ZORK SCRIPT">)
+                <TELL .CACT 0 "ing to " ,XUNM>
+                <TELL ";ZORK " ,POST-CRLF .EXT>)
                (T
-                <TELL "Scripting to <" ,POST-CRLF ,XUNM ">ZORK.SCRIPT">)>)
+                <TELL .CACT 0 "ing to <" ,XUNM>
+                <TELL ">ZORK." ,POST-CRLF .EXT>)>)
         (T
          <COND (.CH <CLOSE .CH>)>
-         <TELL "I can't open the script channel.">)>>
+         <TELL "I can't open the " ,POST-CRLF .ACT " channel.">)>>

-<DEFINE DO-UNSCRIPT ("OPTIONAL" (VERBOSE T))
-  #DECL ((VERBOSE) <OR ATOM FALSE>)
+<DEFINE DO-UNSCRIPT ("OPTIONAL" (VERBOSE T) "AUX" (CACT "Script"))
+  #DECL ((VERBOSE) <OR ATOM FALSE> (CACT) STRING)
+  <COND (<VERB? "#UNRE">
+         <SET CACT "Record">)>
   <COND (,SCRIPT-CHANNEL
          <PUT <TOP ,INCHAN> 1 ()>
          <PUT <TOP ,OUTCHAN> 1 ()>
          <CLOSE ,SCRIPT-CHANNEL>
          <SETG SCRIPT-CHANNEL <>>
-         <AND .VERBOSE <TELL "Scripting off.">>)
-        (<AND .VERBOSE <TELL "Scripting wasn't on.">>)>>
+         <AND .VERBOSE <TELL .CACT ,POST-CRLF "ing off.">>)
+        (<AND .VERBOSE <TELL .CACT ,POST-CRLF "ing wasn't on.">>)>>

 <GDECL (THEN) FIX>
heasm66 commented 4 months ago

I've split out #RECORD/#UNRECORD to new issue, #60.

heasm66 commented 4 months ago

XUNM are both used as a seed for RANDOM, and for directory (needs to already exist) name where save- and scripting-files are saved.

It would be nice to allow the user to specify this (and the full name from ) during the construction of the MADADV.SAVE file. Splitting out this to a new issue.