neilobremski
Experienced Member
Random numbers are great but sometimes you want them to be consistently random (an oxymoron!) so that you can rely on their pattern being the same. For example, in Magenta's Maze everything is generated from random numbers that are seeded by the spell name. This is a string which is boiled down to an integer that is used to reset the pseudo-random number generator. Thus, the level generated for a certain string is always the same even across computers and time.
In the first version, written in C, the following code was used to compute this "string hash":
Note that I wrapped this in a simple main() to demonstrate it at the command-line whereas in the game it is part of the puzzle making code. What this does is turn each character into a number that is then multiplied by the previous result (or used as is if there is no previous result). For example, "Hello" becomes 8 * 5 * 12 * 12 * 15 = 20864.
Hey, wait a minute! Yes, the overflow from the multiplication is being cut off. The previous result mathematically is 86,400 but that is too big to fit into 16-bits. In fact, it occupies 17: 10101000110000000. Truncating to 16-bits on the right yields 0101000110000000 which is indeed 20,864.
Now there is a bug in that old code that I'm keeping. Originally I was only going to support alphanumeric characters and a letter was translated into its place in the alphabet, e.g. 'A' is 1, 'B' is 2, and 'Z' is 26. Then I allowed digits and shoe-horned in that code without testing it thinking that the expression would cause '0' to be 27, '1' to be 28, etc. Unfortunately if you look closely, you'll see that '0' is 91, '1' is 92, etc. Thankfully this doesn't really cause any real problems and is mostly a matter of aesthetics.
Finally, here is the assembler code I've written to produce the same results:
There is a tiny difference in that this code will add two NULL terminators so that it can be used with my TextBlit() function. Here is a screenshot of me testing it. You can see that "NeilO" yields the same value in AX (hex!):
In the first version, written in C, the following code was used to compute this "string hash":
Code:
void main(int argc, char *argv[])
{
unsigned int i, j, seed = 0;
char *spell = argv[1];
/* process spell into random number seed */
for (j = 0, i = 0; j < 8; i++) {
char c = spell[i];
unsigned int m = 0;
if (c >= 'A' && c <= 'Z') {
m = (unsigned int)(c - 'A' + 1);
spell[j++] = c;
} else if (c >= 'a' && c <= 'z') {
m = (unsigned int)(c - 'a' + 1);
spell[j++] = c - 'a' + 'A';
} else if (c >= '0' && c <= '9') {
m = (unsigned int)(c - '0' + 'Z' + 1);
spell[j++] = c;
} else if (!c) {
break;
} else {
continue; /* ignore spaces/invalid characters */
}
printf("'%c' = %u\n", c, m);
seed = seed ? seed * m : m;
}
printf("%s = %u\n", spell, seed);
}
Note that I wrapped this in a simple main() to demonstrate it at the command-line whereas in the game it is part of the puzzle making code. What this does is turn each character into a number that is then multiplied by the previous result (or used as is if there is no previous result). For example, "Hello" becomes 8 * 5 * 12 * 12 * 15 = 20864.
Hey, wait a minute! Yes, the overflow from the multiplication is being cut off. The previous result mathematically is 86,400 but that is too big to fit into 16-bits. In fact, it occupies 17: 10101000110000000. Truncating to 16-bits on the right yields 0101000110000000 which is indeed 20,864.
Now there is a bug in that old code that I'm keeping. Originally I was only going to support alphanumeric characters and a letter was translated into its place in the alphabet, e.g. 'A' is 1, 'B' is 2, and 'Z' is 26. Then I allowed digits and shoe-horned in that code without testing it thinking that the expression would cause '0' to be 27, '1' to be 28, etc. Unfortunately if you look closely, you'll see that '0' is 91, '1' is 92, etc. Thankfully this doesn't really cause any real problems and is mostly a matter of aesthetics.
Finally, here is the assembler code I've written to produce the same results:
Code:
a 540
; ----------------------------------------------------------------------------
; Str2Seed () :STR2SEED
;
; Converts 8 characters from DS:SI into seed in AX for srand().
; Also the string is normalized to uppercase w/o special characters.
;
XOR BX, BX ; 540 reset temp seed
MOV DI, SI ; 542 j
MOV CX, 0008; 544 maximum 8 characters
;
XOR AX, AX ; 547 :STR2SEED_LOOP
LODSB ; 549
;
OR AL, AL ; 54A
JZ 0584 ; 54C if (!c) >STR2SEED_END
;
CMP AL, 30 ; 54E
JB 0547 ; 550 if (c < 48) >STR2SEED_LOOP
;
CMP AL, 39 ; 552 :STR2SEED_DIG
JA 055C ; 554 >STR2SEED_AUPP
MOV [DI],AL ; 556
ADD AL, 2B ; 558 (AL - '0' + 'Z' + 1)
JMP 057A ; 55A >STR2SEED_NEXT
;
CMP AL, 41 ; 55C :STR2SEED_AUPP
JB 0547 ; 55E if (c < 65) >STR2SEED_LOOP
CMP AL, 5A ; 560
JA 056A ; 562 >STR2SEED_ALOW
MOV [DI],AL ; 564
SUB AL, 40 ; 566 (AL - 'A' + 1)
JMP 057A ; 568 >STR2SEED_NEXT
;
CMP AL, 61 ; 56A :STR2SEED_ALOW
JB 0547 ; 56C if (c < 97) >STR2SEED_LOOP
CMP AL, 7A ; 56E
JA 0547 ; 570 >STR2SEED_LOOP
SUB AL, 20 ; 572 (AL - 'a' + 'A')
MOV [DI],AL ; 574
SUB AL, 40 ; 576 (AL - 'A' + 1)
JMP 057A ; 578 >STR2SEED_NEXT
;
INC DI ; 57A :STR2SEED_NEXT
OR BX, BX ; 57B
JZ 0581 ; 57D if (!BP) >STR2SEED_SET
MUL BX ; 57F
XCHG AX, BX ; 581 :STR2SEED_SET
LOOP 0547 ; 582 >STR2SEED_LOOP
;
XOR AX, AX ; 584 :STR2SEED_END
MOV [DI],AL ; 586 NULL terminator
INC DI ; 588
MOV [DI],AL ; 589 double NULL terminator
MOV AX, BX ; 58B
RET ; 58D
There is a tiny difference in that this code will add two NULL terminators so that it can be used with my TextBlit() function. Here is a screenshot of me testing it. You can see that "NeilO" yields the same value in AX (hex!):